leader_lock_cache_test.go 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. //go:build unit
  2. package repository
  3. import (
  4. "context"
  5. "testing"
  6. "time"
  7. "github.com/alicebob/miniredis/v2"
  8. "github.com/redis/go-redis/v9"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func newLeaderLockTestCache(t *testing.T) (*leaderLockCache, *miniredis.Miniredis) {
  12. t.Helper()
  13. mr := miniredis.RunT(t)
  14. rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
  15. t.Cleanup(func() { _ = rdb.Close() })
  16. return &leaderLockCache{rdb: rdb}, mr
  17. }
  18. func TestLeaderLockCache_AcquireContendedRelease(t *testing.T) {
  19. cache, _ := newLeaderLockTestCache(t)
  20. ctx := context.Background()
  21. const key = "dashboard:aggregation:leader"
  22. ok, err := cache.TryAcquireLeaderLock(ctx, key, "A", time.Minute)
  23. require.NoError(t, err)
  24. require.True(t, ok, "first owner should acquire")
  25. ok, err = cache.TryAcquireLeaderLock(ctx, key, "B", time.Minute)
  26. require.NoError(t, err)
  27. require.False(t, ok, "peer must be locked out while held")
  28. require.NoError(t, cache.ReleaseLeaderLock(ctx, key, "A"))
  29. ok, err = cache.TryAcquireLeaderLock(ctx, key, "B", time.Minute)
  30. require.NoError(t, err)
  31. require.True(t, ok, "peer should acquire after release")
  32. }
  33. // A stale owner whose lock expired and was re-acquired by a peer must not delete
  34. // the peer's lock when its late release fires (compare-and-delete by owner).
  35. func TestLeaderLockCache_ReleaseIsCompareAndDelete(t *testing.T) {
  36. cache, _ := newLeaderLockTestCache(t)
  37. ctx := context.Background()
  38. const key = "payment:order:expiry:leader"
  39. ok, err := cache.TryAcquireLeaderLock(ctx, key, "A", time.Minute)
  40. require.NoError(t, err)
  41. require.True(t, ok)
  42. // Simulate A's lock expiring and peer B taking ownership.
  43. require.NoError(t, cache.rdb.Set(ctx, leaderLockKeyPrefix+key, "B", time.Minute).Err())
  44. // A's stale release must be a no-op against B's lock.
  45. require.NoError(t, cache.ReleaseLeaderLock(ctx, key, "A"))
  46. val, err := cache.rdb.Get(ctx, leaderLockKeyPrefix+key).Result()
  47. require.NoError(t, err)
  48. require.Equal(t, "B", val, "stale owner must not delete the new owner's lock")
  49. }
  50. func TestLeaderLockCache_TTLExpires(t *testing.T) {
  51. cache, mr := newLeaderLockTestCache(t)
  52. ctx := context.Background()
  53. const key = "subscription:expiry:reminder:leader"
  54. ok, err := cache.TryAcquireLeaderLock(ctx, key, "A", time.Minute)
  55. require.NoError(t, err)
  56. require.True(t, ok)
  57. mr.FastForward(2 * time.Minute)
  58. ok, err = cache.TryAcquireLeaderLock(ctx, key, "B", time.Minute)
  59. require.NoError(t, err)
  60. require.True(t, ok, "lock should be re-acquirable after the TTL expires")
  61. }