package locker import ( "context" "fmt" "time" "github.com/rs/zerolog" "github.com/redis/go-redis/v9" ) type locker struct { client *redis.Client logger zerolog.Logger } func NewLocker(client *redis.Client, logger zerolog.Logger) Locker { return &locker{ client: client, logger: logger, } } func (l locker) Lock(ctx context.Context, id string, ttl time.Duration) (bool, error) { status := l.client.SetNX(ctx, id, "locked", ttl) if err := status.Err(); err != nil { return false, err } // Return whether the lock was acquired return status.Val(), nil } func (l locker) Unlock(ctx context.Context, id string) error { // Delete the lock by its ID _, err := l.client.Del(ctx, id).Result() return err } // WithLock acquires a lock for a specific vendor, executes the provided function, // and ensures that the lock is released afterward. If any error occurs, it returns the // error, preserving the context of the lock operation. func (l locker) WithLock( ctx context.Context, lockKey string, lockTime time.Duration, fn func(context.Context) error, ) error { lg := l.logger.With(). Str("method", "WithLock"). Str("key", lockKey). Logger() maxRetries := 5 retryDelay := 10 * time.Millisecond //TODO: Replace with proper logging var locked bool var acquireErr error for attempt := 0; attempt <= maxRetries; attempt++ { locked, acquireErr = l.Lock(ctx, lockKey, lockTime) if locked { lg.Info().Msg("LockAcquired") break } if attempt == maxRetries { break } select { case <-ctx.Done(): return ctx.Err() case <-time.After(retryDelay): retryDelay *= 2 // Exponential backoff } } if !locked || acquireErr != nil { lg.Error().Err(acquireErr).Msg("LockErr") return NewLockError(lockKey, uint32(maxRetries), acquireErr) } fnErr := fn(ctx) if fnErr != nil { return fmt.Errorf("failed to execute function for %s due to error %v", lockKey, fnErr) } if unlockErr := l.Unlock(ctx, lockKey); unlockErr != nil { return fmt.Errorf("failed to unlock lock for %s: %v", lockKey, unlockErr) } lg.Info().Msg("Unlocked") return nil }