-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Introduce globallock as distributed locks #31908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
925e844
feat: globallock
wolfogre 15c41b4
test: TestLocker
wolfogre 2895437
test: add tests
wolfogre 0b0424f
feat: ReleaseFunc
wolfogre 97a4c23
fix: releaseOnce
wolfogre a1eea5c
feat: ErrLockReleased
wolfogre 85c5137
chore: rename locker
wolfogre bd2a860
feat: support default locker
wolfogre f4d545c
test: improve TestLockAndDo
wolfogre 0289b78
chore: improve comment
wolfogre 570860f
docs: always defer release()
wolfogre cae33ec
chore: lint
wolfogre 302b099
chore: go mod tidy
wolfogre a07b776
chore: lint
wolfogre 4613c40
fix: close
wolfogre a02f809
test: avoid data race
wolfogre c6300df
test: wait close avoid data race
wolfogre da3f5a9
test: fix
wolfogre 57cb17a
test: fix reset initOnce
wolfogre 240c8d0
Merge branch 'main' into bugfix/globallock
GiteaBot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright 2024 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package globallock | ||
|
||
import ( | ||
"context" | ||
"sync" | ||
) | ||
|
||
var ( | ||
defaultLocker Locker | ||
initOnce sync.Once | ||
initFunc = func() { | ||
// TODO: read the setting and initialize the default locker. | ||
// Before implementing this, don't use it. | ||
} // define initFunc as a variable to make it possible to change it in tests | ||
) | ||
|
||
// DefaultLocker returns the default locker. | ||
func DefaultLocker() Locker { | ||
initOnce.Do(func() { | ||
initFunc() | ||
}) | ||
return defaultLocker | ||
} | ||
|
||
// Lock tries to acquire a lock for the given key, it uses the default locker. | ||
// Read the documentation of Locker.Lock for more information about the behavior. | ||
func Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) { | ||
return DefaultLocker().Lock(ctx, key) | ||
} | ||
|
||
// TryLock tries to acquire a lock for the given key, it uses the default locker. | ||
// Read the documentation of Locker.TryLock for more information about the behavior. | ||
func TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) { | ||
return DefaultLocker().TryLock(ctx, key) | ||
} | ||
|
||
// LockAndDo tries to acquire a lock for the given key and then calls the given function. | ||
// It uses the default locker, and it will return an error if failed to acquire the lock. | ||
func LockAndDo(ctx context.Context, key string, f func(context.Context) error) error { | ||
ctx, release, err := Lock(ctx, key) | ||
if err != nil { | ||
return err | ||
} | ||
defer release() | ||
|
||
return f(ctx) | ||
} | ||
|
||
// TryLockAndDo tries to acquire a lock for the given key and then calls the given function. | ||
// It uses the default locker, and it will return false if failed to acquire the lock. | ||
func TryLockAndDo(ctx context.Context, key string, f func(context.Context) error) (bool, error) { | ||
ok, ctx, release, err := TryLock(ctx, key) | ||
if err != nil { | ||
return false, err | ||
} | ||
defer release() | ||
|
||
if !ok { | ||
return false, nil | ||
} | ||
|
||
return true, f(ctx) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright 2024 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package globallock | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"sync" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestLockAndDo(t *testing.T) { | ||
t.Run("redis", func(t *testing.T) { | ||
url := "redis://127.0.0.1:6379/0" | ||
if os.Getenv("CI") == "" { | ||
// Make it possible to run tests against a local redis instance | ||
url = os.Getenv("TEST_REDIS_URL") | ||
if url == "" { | ||
t.Skip("TEST_REDIS_URL not set and not running in CI") | ||
return | ||
} | ||
} | ||
|
||
oldDefaultLocker := defaultLocker | ||
defer func() { | ||
defaultLocker = oldDefaultLocker | ||
}() | ||
|
||
initOnce = sync.Once{} | ||
initFunc = func() { | ||
defaultLocker = NewRedisLocker(url) | ||
} | ||
|
||
testLockAndDo(t) | ||
require.NoError(t, defaultLocker.(*redisLocker).Close()) | ||
}) | ||
t.Run("memory", func(t *testing.T) { | ||
oldDefaultLocker := defaultLocker | ||
defer func() { | ||
defaultLocker = oldDefaultLocker | ||
}() | ||
|
||
initOnce = sync.Once{} | ||
initFunc = func() { | ||
defaultLocker = NewMemoryLocker() | ||
} | ||
wolfogre marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
testLockAndDo(t) | ||
}) | ||
} | ||
|
||
func testLockAndDo(t *testing.T) { | ||
const concurrency = 1000 | ||
|
||
ctx := context.Background() | ||
count := 0 | ||
wg := sync.WaitGroup{} | ||
wg.Add(concurrency) | ||
for i := 0; i < concurrency; i++ { | ||
go func() { | ||
defer wg.Done() | ||
err := LockAndDo(ctx, "test", func(ctx context.Context) error { | ||
count++ | ||
|
||
// It's impossible to acquire the lock inner the function | ||
ok, err := TryLockAndDo(ctx, "test", func(ctx context.Context) error { | ||
assert.Fail(t, "should not acquire the lock") | ||
return nil | ||
}) | ||
assert.False(t, ok) | ||
assert.NoError(t, err) | ||
|
||
return nil | ||
}) | ||
require.NoError(t, err) | ||
}() | ||
} | ||
|
||
wg.Wait() | ||
|
||
assert.Equal(t, concurrency, count) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// Copyright 2024 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package globallock | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
) | ||
|
||
type Locker interface { | ||
// Lock tries to acquire a lock for the given key, it blocks until the lock is acquired or the context is canceled. | ||
// | ||
// Lock returns a new context which should be used in the following code. | ||
// The new context will be canceled when the lock is released or lost - yes, it's possible to lose a lock. | ||
// For example, it lost the connection to the redis server while holding the lock. | ||
// If it fails to acquire the lock, the returned context will be the same as the input context. | ||
// | ||
// Lock returns a ReleaseFunc to release the lock, it cannot be nil. | ||
// It's always safe to call this function even if it fails to acquire the lock, and it will do nothing in that case. | ||
// And it's also safe to call it multiple times, but it will only release the lock once. | ||
// That's why it's called ReleaseFunc, not UnlockFunc. | ||
// But be aware that it's not safe to not call it at all; it could lead to a memory leak. | ||
// So a recommended pattern is to use defer to call it: | ||
// ctx, release, err := locker.Lock(ctx, "key") | ||
// if err != nil { | ||
// return err | ||
// } | ||
// defer release() | ||
// The ReleaseFunc will return the original context which was used to acquire the lock. | ||
// It's useful when you want to continue to do something after releasing the lock. | ||
// At that time, the ctx will be canceled, and you can use the returned context by the ReleaseFunc to continue: | ||
// ctx, release, err := locker.Lock(ctx, "key") | ||
// if err != nil { | ||
// return err | ||
// } | ||
// defer release() | ||
// doSomething(ctx) | ||
// ctx = release() | ||
// doSomethingElse(ctx) | ||
// Please ignore it and use `defer release()` instead if you don't need this, to avoid forgetting to release the lock. | ||
// | ||
// Lock returns an error if failed to acquire the lock. | ||
// Be aware that even the context is not canceled, it's still possible to fail to acquire the lock. | ||
// For example, redis is down, or it reached the maximum number of tries. | ||
Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) | ||
|
||
// TryLock tries to acquire a lock for the given key, it returns immediately. | ||
// It follows the same pattern as Lock, but it doesn't block. | ||
// And if it fails to acquire the lock because it's already locked, not other reasons like redis is down, | ||
// it will return false without any error. | ||
TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) | ||
} | ||
|
||
// ReleaseFunc is a function that releases a lock. | ||
// It returns the original context which was used to acquire the lock. | ||
type ReleaseFunc func() context.Context | ||
|
||
// ErrLockReleased is used as context cause when a lock is released | ||
var ErrLockReleased = fmt.Errorf("lock released") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.