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 distributed mutex operations. // CWE-755: Methods now return errors to properly handle exceptional conditions type Adapter interface { Acquire(ctx context.Context, key string) error Acquiref(ctx context.Context, format string, a ...any) error Release(ctx context.Context, key string) error Releasef(ctx context.Context, format string, a ...any) error } type distributedMutexAdapter struct { logger *zap.Logger redis redis.UniversalClient locker *redislock.Client lockInstances map[string]*redislock.Lock mutex *sync.Mutex // Mutex for synchronization with goroutines } // NewAdapter constructor that returns the default distributed mutex adapter. func NewAdapter(logger *zap.Logger, redisClient redis.UniversalClient) Adapter { logger = logger.Named("distributed-mutex") // Create a new lock client locker := redislock.New(redisClient) logger.Info("✓ Distributed mutex initialized (Redis-backed)") return &distributedMutexAdapter{ logger: logger, redis: redisClient, locker: locker, lockInstances: make(map[string]*redislock.Lock), mutex: &sync.Mutex{}, // Initialize the mutex } } // Acquire function blocks the current thread if the lock key is currently locked. // CWE-755: Now returns error instead of silently failing func (a *distributedMutexAdapter) Acquire(ctx context.Context, key string) error { startDT := time.Now() a.logger.Debug("acquiring lock", zap.String("key", key)) // 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, key, time.Minute, &redislock.Options{ RetryStrategy: backoff, }) if err == redislock.ErrNotObtained { nowDT := time.Now() diff := nowDT.Sub(startDT) a.logger.Error("could not obtain lock after retries", zap.String("key", key), zap.Time("start_dt", startDT), zap.Time("now_dt", nowDT), zap.Duration("duration", diff), zap.Int("max_retries", 20)) return fmt.Errorf("could not obtain lock after 20 retries (waited %s): %w", diff, err) } else if err != nil { a.logger.Error("failed obtaining lock", zap.String("key", key), zap.Error(err)) return fmt.Errorf("failed to obtain lock: %w", err) } // 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[key] = lock } a.logger.Debug("lock acquired", zap.String("key", key)) return nil // Success } // Acquiref function blocks the current thread if the lock key is currently locked. // CWE-755: Now returns error from Acquire func (a *distributedMutexAdapter) Acquiref(ctx context.Context, format string, args ...any) error { key := fmt.Sprintf(format, args...) return a.Acquire(ctx, key) } // Release function releases the lock for the given key. // CWE-755: Now returns error instead of silently failing func (a *distributedMutexAdapter) Release(ctx context.Context, key string) error { a.logger.Debug("releasing lock", zap.String("key", key)) // 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() lockInstance, ok := a.lockInstances[key] if ok { delete(a.lockInstances, key) } a.mutex.Unlock() if ok { if err := lockInstance.Release(ctx); err != nil { a.logger.Error("failed to release lock", zap.String("key", key), zap.Error(err)) return fmt.Errorf("failed to release lock: %w", err) } a.logger.Debug("lock released", zap.String("key", key)) return nil // Success } // Lock not found - this is a warning but not an error (may have already been released) a.logger.Warn("lock not found for release", zap.String("key", key)) return nil // Not an error, just not found } // Releasef function releases the lock for a formatted key. // CWE-755: Now returns error from Release func (a *distributedMutexAdapter) Releasef(ctx context.Context, format string, args ...any) error { key := fmt.Sprintf(format, args...) return a.Release(ctx, key) }