Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,220 @@
// File Path: monorepo/cloud/maplefile-backend/pkg/distributedmutex/distributedmutex.go
package distributedmutex
import (
"context"
"fmt"
"sync"
"time"
"github.com/bsm/redislock"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
// Adapter provides interface for abstracting distributedmutex generation.
type Adapter interface {
// Blocking acquire - waits until lock is obtained or timeout
Acquire(ctx context.Context, key string)
Acquiref(ctx context.Context, format string, a ...any)
Release(ctx context.Context, key string)
Releasef(ctx context.Context, format string, a ...any)
// Non-blocking operations for leader election
// TryAcquire attempts to acquire a lock without blocking
// Returns true if lock was acquired, false if already held by someone else
TryAcquire(ctx context.Context, key string, ttl time.Duration) (bool, error)
// Extend renews the TTL of an existing lock
// Returns error if the lock is not owned by this instance
Extend(ctx context.Context, key string, ttl time.Duration) error
// IsOwner checks if this instance owns the given lock
IsOwner(ctx context.Context, key string) (bool, error)
}
type distributedLockerAdapter struct {
Logger *zap.Logger
Redis redis.UniversalClient
Locker *redislock.Client
LockInstances map[string]*redislock.Lock
Mutex *sync.Mutex // Add a mutex for synchronization with goroutines
}
// NewAdapter constructor that returns the default DistributedLocker generator.
func NewAdapter(loggerp *zap.Logger, redisClient redis.UniversalClient) Adapter {
loggerp = loggerp.Named("DistributedMutex")
loggerp.Debug("distributed mutex starting and connecting...")
// Create a new lock client.
locker := redislock.New(redisClient)
loggerp.Debug("distributed mutex initialized")
return distributedLockerAdapter{
Logger: loggerp,
Redis: redisClient,
Locker: locker,
LockInstances: make(map[string]*redislock.Lock, 0),
Mutex: &sync.Mutex{}, // Initialize the mutex
}
}
// Acquire function blocks the current thread if the lock key is currently locked.
func (a distributedLockerAdapter) Acquire(ctx context.Context, k string) {
startDT := time.Now()
a.Logger.Debug(fmt.Sprintf("locking for key: %v", k))
// Retry every 250ms, for up-to 20x
backoff := redislock.LimitRetry(redislock.LinearBackoff(250*time.Millisecond), 20)
// Obtain lock with retry
lock, err := a.Locker.Obtain(ctx, k, time.Minute, &redislock.Options{
RetryStrategy: backoff,
})
if err == redislock.ErrNotObtained {
nowDT := time.Now()
diff := nowDT.Sub(startDT)
a.Logger.Error("could not obtain lock",
zap.String("key", k),
zap.Time("start_dt", startDT),
zap.Time("now_dt", nowDT),
zap.Any("duration_in_minutes", diff.Minutes()))
return
} else if err != nil {
a.Logger.Error("failed obtaining lock",
zap.String("key", k),
zap.Any("error", err),
)
return
}
// DEVELOPERS NOTE:
// The `map` datastructure in Golang is not concurrently safe, therefore we
// need to use mutex to coordinate access of our `LockInstances` map
// resource between all the goroutines.
a.Mutex.Lock()
defer a.Mutex.Unlock()
if a.LockInstances != nil { // Defensive code.
a.LockInstances[k] = lock
}
}
// Acquiref function blocks the current thread if the lock key is currently locked.
func (u distributedLockerAdapter) Acquiref(ctx context.Context, format string, a ...any) {
k := fmt.Sprintf(format, a...)
u.Acquire(ctx, k)
return
}
// Release function blocks the current thread if the lock key is currently locked.
func (a distributedLockerAdapter) Release(ctx context.Context, k string) {
a.Logger.Debug(fmt.Sprintf("unlocking for key: %v", k))
lockInstance, ok := a.LockInstances[k]
if ok {
defer lockInstance.Release(ctx)
} else {
a.Logger.Error("could not obtain to unlock", zap.String("key", k))
}
return
}
// Releasef
func (u distributedLockerAdapter) Releasef(ctx context.Context, format string, a ...any) {
k := fmt.Sprintf(format, a...) //TODO: https://github.com/bsm/redislock/blob/main/README.md
u.Release(ctx, k)
return
}
// TryAcquire attempts to acquire a lock without blocking.
// Returns true if lock was acquired, false if already held by someone else.
func (a distributedLockerAdapter) TryAcquire(ctx context.Context, k string, ttl time.Duration) (bool, error) {
a.Logger.Debug(fmt.Sprintf("trying to acquire lock for key: %v with ttl: %v", k, ttl))
// Try to obtain lock without retries (non-blocking)
lock, err := a.Locker.Obtain(ctx, k, ttl, &redislock.Options{
RetryStrategy: redislock.NoRetry(),
})
if err == redislock.ErrNotObtained {
// Lock is held by someone else
a.Logger.Debug("lock not obtained, already held by another instance",
zap.String("key", k))
return false, nil
}
if err != nil {
// Actual error occurred
a.Logger.Error("failed trying to obtain lock",
zap.String("key", k),
zap.Error(err))
return false, err
}
// Successfully acquired lock
a.Mutex.Lock()
defer a.Mutex.Unlock()
if a.LockInstances != nil {
a.LockInstances[k] = lock
}
a.Logger.Debug("successfully acquired lock",
zap.String("key", k),
zap.Duration("ttl", ttl))
return true, nil
}
// Extend renews the TTL of an existing lock.
// Returns error if the lock is not owned by this instance.
func (a distributedLockerAdapter) Extend(ctx context.Context, k string, ttl time.Duration) error {
a.Logger.Debug(fmt.Sprintf("extending lock for key: %v with ttl: %v", k, ttl))
a.Mutex.Lock()
lockInstance, ok := a.LockInstances[k]
a.Mutex.Unlock()
if !ok {
err := fmt.Errorf("lock not found in instances map")
a.Logger.Error("cannot extend lock, not owned by this instance",
zap.String("key", k),
zap.Error(err))
return err
}
// Extend the lock TTL
err := lockInstance.Refresh(ctx, ttl, nil)
if err != nil {
a.Logger.Error("failed to extend lock",
zap.String("key", k),
zap.Error(err))
return err
}
a.Logger.Debug("successfully extended lock",
zap.String("key", k),
zap.Duration("ttl", ttl))
return nil
}
// IsOwner checks if this instance owns the given lock.
func (a distributedLockerAdapter) IsOwner(ctx context.Context, k string) (bool, error) {
a.Mutex.Lock()
lockInstance, ok := a.LockInstances[k]
a.Mutex.Unlock()
if !ok {
// Not in our instances map
return false, nil
}
// Get the lock metadata to check if we still own it
metadata := lockInstance.Metadata()
// If metadata is empty, we don't own it
return metadata != "", nil
}

View file

@ -0,0 +1,60 @@
// File Path: monorepo/cloud/maplefile-backend/pkg/distributedmutex/distributedmutex_test.go
package distributedmutex
import (
"context"
"testing"
"time"
"go.uber.org/zap"
"github.com/redis/go-redis/v9"
)
// mockRedisClient implements minimal required methods
type mockRedisClient struct {
redis.UniversalClient
}
func (m *mockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd {
return redis.NewStringCmd(ctx)
}
func (m *mockRedisClient) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd {
return redis.NewStatusCmd(ctx)
}
func (m *mockRedisClient) Eval(ctx context.Context, script string, keys []string, args ...any) *redis.Cmd {
return redis.NewCmd(ctx)
}
func (m *mockRedisClient) EvalSha(ctx context.Context, sha string, keys []string, args ...any) *redis.Cmd {
return redis.NewCmd(ctx)
}
func (m *mockRedisClient) ScriptExists(ctx context.Context, scripts ...string) *redis.BoolSliceCmd {
return redis.NewBoolSliceCmd(ctx)
}
func (m *mockRedisClient) ScriptLoad(ctx context.Context, script string) *redis.StringCmd {
return redis.NewStringCmd(ctx)
}
func TestNewAdapter(t *testing.T) {
logger, _ := zap.NewDevelopment()
adapter := NewAdapter(logger, &mockRedisClient{})
if adapter == nil {
t.Fatal("expected non-nil adapter")
}
}
func TestAcquireAndRelease(t *testing.T) {
ctx := context.Background()
logger, _ := zap.NewDevelopment()
adapter := NewAdapter(logger, &mockRedisClient{})
adapter.Acquire(ctx, "test-key")
adapter.Acquiref(ctx, "test-key-%d", 1)
adapter.Release(ctx, "test-key")
adapter.Releasef(ctx, "test-key-%d", 1)
}

View file

@ -0,0 +1,23 @@
package distributedmutex
import (
"fmt"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
)
// ProvideDistributedMutexAdapter provides a distributed mutex adapter for Wire DI
func ProvideDistributedMutexAdapter(cfg *config.Config, logger *zap.Logger) Adapter {
// Create Redis client for distributed locking
// Note: This is separate from the cache redis client
redisClient := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Cache.Host, cfg.Cache.Port),
Password: cfg.Cache.Password,
DB: cfg.Cache.DB,
})
return NewAdapter(logger, redisClient)
}