Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
109
cloud/maplefile-backend/pkg/cache/cassandra.go
vendored
Normal file
109
cloud/maplefile-backend/pkg/cache/cassandra.go
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CassandraCacher defines the interface for Cassandra cache operations
|
||||
type CassandraCacher interface {
|
||||
Shutdown(ctx context.Context)
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
Set(ctx context.Context, key string, val []byte) error
|
||||
SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
PurgeExpired(ctx context.Context) error
|
||||
}
|
||||
|
||||
type cassandraCache struct {
|
||||
session *gocql.Session
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCassandraCache creates a new Cassandra cache instance
|
||||
func NewCassandraCache(session *gocql.Session, logger *zap.Logger) CassandraCacher {
|
||||
logger = logger.Named("cassandra-cache")
|
||||
logger.Info("✓ Cassandra cache layer initialized")
|
||||
return &cassandraCache{
|
||||
session: session,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cassandraCache) Shutdown(ctx context.Context) {
|
||||
c.logger.Info("shutting down Cassandra cache")
|
||||
// Note: Don't close the session here as it's managed by the database layer
|
||||
}
|
||||
|
||||
func (c *cassandraCache) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
var value []byte
|
||||
var expiresAt time.Time
|
||||
|
||||
query := `SELECT value, expires_at FROM cache WHERE key = ?`
|
||||
err := c.session.Query(query, key).WithContext(ctx).Consistency(gocql.LocalQuorum).Scan(&value, &expiresAt)
|
||||
|
||||
if err == gocql.ErrNotFound {
|
||||
// Key doesn't exist - this is not an error
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if expired in application code
|
||||
if time.Now().After(expiresAt) {
|
||||
// Entry is expired, delete it and return nil
|
||||
_ = c.Delete(ctx, key) // Clean up expired entry
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (c *cassandraCache) Set(ctx context.Context, key string, val []byte) error {
|
||||
expiresAt := time.Now().Add(24 * time.Hour) // Default 24 hour expiry
|
||||
query := `INSERT INTO cache (key, expires_at, value) VALUES (?, ?, ?)`
|
||||
return c.session.Query(query, key, expiresAt, val).WithContext(ctx).Consistency(gocql.LocalQuorum).Exec()
|
||||
}
|
||||
|
||||
func (c *cassandraCache) SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error {
|
||||
expiresAt := time.Now().Add(expiry)
|
||||
query := `INSERT INTO cache (key, expires_at, value) VALUES (?, ?, ?)`
|
||||
return c.session.Query(query, key, expiresAt, val).WithContext(ctx).Consistency(gocql.LocalQuorum).Exec()
|
||||
}
|
||||
|
||||
func (c *cassandraCache) Delete(ctx context.Context, key string) error {
|
||||
query := `DELETE FROM cache WHERE key = ?`
|
||||
return c.session.Query(query, key).WithContext(ctx).Consistency(gocql.LocalQuorum).Exec()
|
||||
}
|
||||
|
||||
func (c *cassandraCache) PurgeExpired(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
|
||||
// Thanks to the index on expires_at, this query is efficient
|
||||
iter := c.session.Query(`SELECT key FROM cache WHERE expires_at < ? ALLOW FILTERING`, now).WithContext(ctx).Iter()
|
||||
|
||||
var expiredKeys []string
|
||||
var key string
|
||||
for iter.Scan(&key) {
|
||||
expiredKeys = append(expiredKeys, key)
|
||||
}
|
||||
|
||||
if err := iter.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete expired keys in batch
|
||||
if len(expiredKeys) > 0 {
|
||||
batch := c.session.NewBatch(gocql.LoggedBatch).WithContext(ctx)
|
||||
for _, expiredKey := range expiredKeys {
|
||||
batch.Query(`DELETE FROM cache WHERE key = ?`, expiredKey)
|
||||
}
|
||||
return c.session.ExecuteBatch(batch)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
23
cloud/maplefile-backend/pkg/cache/provider.go
vendored
Normal file
23
cloud/maplefile-backend/pkg/cache/provider.go
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"github.com/gocql/gocql"
|
||||
)
|
||||
|
||||
// ProvideRedisCache provides a Redis cache instance
|
||||
func ProvideRedisCache(cfg *config.Config, logger *zap.Logger) (RedisCacher, error) {
|
||||
return NewRedisCache(cfg, logger)
|
||||
}
|
||||
|
||||
// ProvideCassandraCache provides a Cassandra cache instance
|
||||
func ProvideCassandraCache(session *gocql.Session, logger *zap.Logger) CassandraCacher {
|
||||
return NewCassandraCache(session, logger)
|
||||
}
|
||||
|
||||
// ProvideTwoTierCache provides a two-tier cache instance
|
||||
func ProvideTwoTierCache(redisCache RedisCacher, cassandraCache CassandraCacher, logger *zap.Logger) TwoTierCacher {
|
||||
return NewTwoTierCache(redisCache, cassandraCache, logger)
|
||||
}
|
||||
144
cloud/maplefile-backend/pkg/cache/redis.go
vendored
Normal file
144
cloud/maplefile-backend/pkg/cache/redis.go
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
// silentRedisLogger filters out noisy "maintnotifications" warnings from go-redis
|
||||
// This warning occurs when the Redis client tries to use newer Redis 7.2+ features
|
||||
// that may not be fully supported by the current Redis version.
|
||||
// The client automatically falls back to compatible mode, so this is harmless.
|
||||
type silentRedisLogger struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (l *silentRedisLogger) Printf(ctx context.Context, format string, v ...interface{}) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
|
||||
// Filter out harmless compatibility warnings
|
||||
if strings.Contains(msg, "maintnotifications disabled") ||
|
||||
strings.Contains(msg, "auto mode fallback") {
|
||||
return
|
||||
}
|
||||
|
||||
// Log other Redis messages at debug level
|
||||
l.logger.Debug(msg)
|
||||
}
|
||||
|
||||
// RedisCacher defines the interface for Redis cache operations
|
||||
type RedisCacher interface {
|
||||
Shutdown(ctx context.Context)
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
Set(ctx context.Context, key string, val []byte) error
|
||||
SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
type redisCache struct {
|
||||
client *redis.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewRedisCache creates a new Redis cache instance
|
||||
func NewRedisCache(cfg *config.Config, logger *zap.Logger) (RedisCacher, error) {
|
||||
logger = logger.Named("redis-cache")
|
||||
|
||||
logger.Info("⏳ Connecting to Redis...",
|
||||
zap.String("host", cfg.Cache.Host),
|
||||
zap.Int("port", cfg.Cache.Port))
|
||||
|
||||
// Build Redis URL from config
|
||||
redisURL := fmt.Sprintf("redis://:%s@%s:%d/%d",
|
||||
cfg.Cache.Password,
|
||||
cfg.Cache.Host,
|
||||
cfg.Cache.Port,
|
||||
cfg.Cache.DB,
|
||||
)
|
||||
|
||||
// If no password, use simpler URL format
|
||||
if cfg.Cache.Password == "" {
|
||||
redisURL = fmt.Sprintf("redis://%s:%d/%d",
|
||||
cfg.Cache.Host,
|
||||
cfg.Cache.Port,
|
||||
cfg.Cache.DB,
|
||||
)
|
||||
}
|
||||
|
||||
opt, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Redis URL: %w", err)
|
||||
}
|
||||
|
||||
// Suppress noisy "maintnotifications" warnings from go-redis
|
||||
// Use a custom logger that filters out these harmless compatibility warnings
|
||||
redis.SetLogger(&silentRedisLogger{logger: logger.Named("redis-client")})
|
||||
|
||||
client := redis.NewClient(opt)
|
||||
|
||||
// Test connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err = client.Ping(ctx).Result(); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("✓ Redis connected",
|
||||
zap.String("host", cfg.Cache.Host),
|
||||
zap.Int("port", cfg.Cache.Port),
|
||||
zap.Int("db", cfg.Cache.DB))
|
||||
|
||||
return &redisCache{
|
||||
client: client,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *redisCache) Shutdown(ctx context.Context) {
|
||||
c.logger.Info("shutting down Redis cache")
|
||||
if err := c.client.Close(); err != nil {
|
||||
c.logger.Error("error closing Redis connection", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *redisCache) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
val, err := c.client.Get(ctx, key).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
// Key doesn't exist - this is not an error
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redis get failed: %w", err)
|
||||
}
|
||||
return []byte(val), nil
|
||||
}
|
||||
|
||||
func (c *redisCache) Set(ctx context.Context, key string, val []byte) error {
|
||||
if err := c.client.Set(ctx, key, val, 0).Err(); err != nil {
|
||||
return fmt.Errorf("redis set failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *redisCache) SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error {
|
||||
if err := c.client.Set(ctx, key, val, expiry).Err(); err != nil {
|
||||
return fmt.Errorf("redis set with expiry failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *redisCache) Delete(ctx context.Context, key string) error {
|
||||
if err := c.client.Del(ctx, key).Err(); err != nil {
|
||||
return fmt.Errorf("redis delete failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
114
cloud/maplefile-backend/pkg/cache/twotier.go
vendored
Normal file
114
cloud/maplefile-backend/pkg/cache/twotier.go
vendored
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// File Path: monorepo/cloud/maplepress-backend/pkg/cache/twotier.go
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TwoTierCacher defines the interface for two-tier cache operations
|
||||
type TwoTierCacher interface {
|
||||
Shutdown(ctx context.Context)
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
Set(ctx context.Context, key string, val []byte) error
|
||||
SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
PurgeExpired(ctx context.Context) error
|
||||
}
|
||||
|
||||
// twoTierCache implements a clean 2-layer (read-through write-through) cache
|
||||
//
|
||||
// L1: Redis (fast, in-memory)
|
||||
// L2: Cassandra (persistent)
|
||||
//
|
||||
// On Get: check Redis → then Cassandra → if found in Cassandra → populate Redis
|
||||
// On Set: write to both
|
||||
// On SetWithExpiry: write to both with expiry
|
||||
// On Delete: remove from both
|
||||
type twoTierCache struct {
|
||||
redisCache RedisCacher
|
||||
cassandraCache CassandraCacher
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTwoTierCache creates a new two-tier cache instance
|
||||
func NewTwoTierCache(redisCache RedisCacher, cassandraCache CassandraCacher, logger *zap.Logger) TwoTierCacher {
|
||||
logger = logger.Named("two-tier-cache")
|
||||
logger.Info("✓ Two-tier cache initialized (Redis L1 + Cassandra L2)")
|
||||
return &twoTierCache{
|
||||
redisCache: redisCache,
|
||||
cassandraCache: cassandraCache,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *twoTierCache) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
// Try L1 (Redis) first
|
||||
val, err := c.redisCache.Get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val != nil {
|
||||
c.logger.Debug("cache hit from Redis", zap.String("key", key))
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Not in Redis, try L2 (Cassandra)
|
||||
val, err = c.cassandraCache.Get(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val != nil {
|
||||
// Found in Cassandra, populate Redis for future lookups
|
||||
c.logger.Debug("cache hit from Cassandra, writing back to Redis", zap.String("key", key))
|
||||
_ = c.redisCache.Set(ctx, key, val) // Best effort, don't fail if Redis write fails
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (c *twoTierCache) Set(ctx context.Context, key string, val []byte) error {
|
||||
// Write to both layers
|
||||
if err := c.redisCache.Set(ctx, key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.cassandraCache.Set(ctx, key, val); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoTierCache) SetWithExpiry(ctx context.Context, key string, val []byte, expiry time.Duration) error {
|
||||
// Write to both layers with expiry
|
||||
if err := c.redisCache.SetWithExpiry(ctx, key, val, expiry); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.cassandraCache.SetWithExpiry(ctx, key, val, expiry); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoTierCache) Delete(ctx context.Context, key string) error {
|
||||
// Remove from both layers
|
||||
if err := c.redisCache.Delete(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.cassandraCache.Delete(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *twoTierCache) PurgeExpired(ctx context.Context) error {
|
||||
// Only Cassandra needs purging (Redis handles TTL automatically)
|
||||
return c.cassandraCache.PurgeExpired(ctx)
|
||||
}
|
||||
|
||||
func (c *twoTierCache) Shutdown(ctx context.Context) {
|
||||
c.logger.Info("shutting down two-tier cache")
|
||||
c.redisCache.Shutdown(ctx)
|
||||
c.cassandraCache.Shutdown(ctx)
|
||||
c.logger.Info("two-tier cache shutdown complete")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue