Skip to content

Commit bd2a860

Browse files
committed
feat: support default locker
1 parent 85c5137 commit bd2a860

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

modules/globallock/globallock.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package globallock
5+
6+
import (
7+
"context"
8+
"sync"
9+
)
10+
11+
var (
12+
defaultLocker Locker
13+
initOnce sync.Once
14+
initFunc = func() {
15+
// TODO: read the setting and initialize the default locker.
16+
// Before implementing this, don't use it.
17+
} // define initFunc as a variable to make it possible to change it in tests
18+
)
19+
20+
// DefaultLocker returns the default locker.
21+
func DefaultLocker() Locker {
22+
initOnce.Do(func() {
23+
initFunc()
24+
})
25+
return defaultLocker
26+
}
27+
28+
// Lock tries to acquire a lock for the given key, it uses the default locker.
29+
// Read the documentation of Locker.Lock for more information about the behavior.
30+
func Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) {
31+
return DefaultLocker().Lock(ctx, key)
32+
}
33+
34+
// TryLock tries to acquire a lock for the given key, it uses the default locker.
35+
// Read the documentation of Locker.TryLock for more information about the behavior.
36+
func TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) {
37+
return DefaultLocker().TryLock(ctx, key)
38+
}
39+
40+
// LockAndDo tries to acquire a lock for the given key and then calls the given function.
41+
// It uses the default locker.
42+
func LockAndDo(ctx context.Context, key string, f func(context.Context) error) error {
43+
ctx, release, err := Lock(ctx, key)
44+
if err != nil {
45+
return err
46+
}
47+
defer release()
48+
49+
return f(ctx)
50+
}
51+
52+
// TryLockAndDo tries to acquire a lock for the given key and then calls the given function.
53+
// It uses the default locker, and it will return false if failed to acquire the lock.
54+
func TryLockAndDo(ctx context.Context, key string, f func(context.Context) error) (bool, error) {
55+
ok, ctx, release, err := TryLock(ctx, key)
56+
if err != nil {
57+
return false, err
58+
}
59+
defer release()
60+
61+
if !ok {
62+
return false, nil
63+
}
64+
65+
return true, f(ctx)
66+
}

modules/globallock/globallock_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package globallock
5+
6+
import (
7+
"context"
8+
"os"
9+
"sync"
10+
"testing"
11+
"time"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestLockAndDo(t *testing.T) {
18+
t.Run("redis", func(t *testing.T) {
19+
url := "redis://127.0.0.1:6379/0"
20+
if os.Getenv("CI") == "" {
21+
// Make it possible to run tests against a local redis instance
22+
url = os.Getenv("TEST_REDIS_URL")
23+
if url == "" {
24+
t.Skip("TEST_REDIS_URL not set and not running in CI")
25+
return
26+
}
27+
}
28+
29+
initOnce = sync.Once{}
30+
initFunc = func() {
31+
defaultLocker = NewRedisLocker(url)
32+
}
33+
34+
testLockAndDo(t)
35+
})
36+
t.Run("memory", func(t *testing.T) {
37+
initOnce = sync.Once{}
38+
initFunc = func() {
39+
defaultLocker = NewMemoryLocker()
40+
}
41+
42+
testLockAndDo(t)
43+
})
44+
}
45+
46+
func testLockAndDo(t *testing.T) {
47+
const (
48+
duration = 2 * time.Second
49+
concurrency = 100
50+
)
51+
52+
ctx := context.Background()
53+
count := 0
54+
wg := sync.WaitGroup{}
55+
wg.Add(concurrency)
56+
for i := 0; i < concurrency; i++ {
57+
go func() {
58+
defer wg.Done()
59+
err := LockAndDo(ctx, "test", func(ctx context.Context) error {
60+
select {
61+
case <-ctx.Done():
62+
return ctx.Err()
63+
case <-time.After(duration / concurrency):
64+
count++
65+
}
66+
return nil
67+
})
68+
require.NoError(t, err)
69+
}()
70+
}
71+
72+
ok, err := TryLockAndDo(ctx, "test", func(ctx context.Context) error {
73+
return nil
74+
})
75+
assert.False(t, ok)
76+
assert.NoError(t, err)
77+
78+
wg.Wait()
79+
80+
ok, err = TryLockAndDo(ctx, "test", func(ctx context.Context) error {
81+
return nil
82+
})
83+
assert.True(t, ok)
84+
assert.NoError(t, err)
85+
86+
assert.Equal(t, concurrency, count)
87+
}

0 commit comments

Comments
 (0)