forked from go-gitea/gitea
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'giteaofficial/main'
* giteaofficial/main: Split org Propfile README to a new tab `overview` (go-gitea#31373) [skip ci] Updated translations via Crowdin Introduce globallock as distributed locks (go-gitea#31908)
- Loading branch information
Showing
15 changed files
with
768 additions
and
47 deletions.
There are no files selected for viewing
This file contains 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 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 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 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,96 @@ | ||
// 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 | ||
oldInitFunc := initFunc | ||
defer func() { | ||
defaultLocker = oldDefaultLocker | ||
initFunc = oldInitFunc | ||
if defaultLocker == nil { | ||
initOnce = sync.Once{} | ||
} | ||
}() | ||
|
||
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 | ||
oldInitFunc := initFunc | ||
defer func() { | ||
defaultLocker = oldDefaultLocker | ||
initFunc = oldInitFunc | ||
if defaultLocker == nil { | ||
initOnce = sync.Once{} | ||
} | ||
}() | ||
|
||
initOnce = sync.Once{} | ||
initFunc = func() { | ||
defaultLocker = NewMemoryLocker() | ||
} | ||
|
||
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 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.