Skip to content

Commit cd306ed

Browse files
committed
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)
2 parents 4e8c340 + 39d2fde commit cd306ed

File tree

15 files changed

+768
-47
lines changed

15 files changed

+768
-47
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ require (
4949
github.com/go-git/go-billy/v5 v5.5.0
5050
github.com/go-git/go-git/v5 v5.12.0
5151
github.com/go-ldap/ldap/v3 v3.4.6
52+
github.com/go-redsync/redsync/v4 v4.13.0
5253
github.com/go-sql-driver/mysql v1.8.1
5354
github.com/go-swagger/go-swagger v0.31.0
5455
github.com/go-testfixtures/testfixtures/v3 v3.11.0
@@ -218,7 +219,9 @@ require (
218219
github.com/gorilla/mux v1.8.1 // indirect
219220
github.com/gorilla/securecookie v1.1.2 // indirect
220221
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
222+
github.com/hashicorp/errwrap v1.1.0 // indirect
221223
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
224+
github.com/hashicorp/go-multierror v1.1.1 // indirect
222225
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
223226
github.com/hashicorp/hcl v1.0.0 // indirect
224227
github.com/imdario/mergo v0.3.16 // indirect

go.sum

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,14 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
342342
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
343343
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
344344
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
345+
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
346+
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
347+
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
348+
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
349+
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
350+
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
351+
github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA=
352+
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
345353
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
346354
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
347355
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -397,6 +405,8 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
397405
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
398406
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
399407
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
408+
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
409+
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
400410
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
401411
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
402412
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -449,10 +459,15 @@ github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
449459
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
450460
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
451461
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
462+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
463+
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
464+
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
452465
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
453466
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
454467
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
455468
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
469+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
470+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
456471
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
457472
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
458473
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -674,6 +689,8 @@ github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKc
674689
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
675690
github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA=
676691
github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
692+
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
693+
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
677694
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
678695
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
679696
github.com/rhysd/actionlint v1.7.1 h1:WJaDzyT1StBWVKGSsZPYnbV0HF9Y9/vD6KFdZQL42qE=
@@ -765,6 +782,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
765782
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
766783
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
767784
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
785+
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
786+
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
768787
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
769788
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
770789
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=

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, and it will return an error if failed to acquire the lock.
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+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestLockAndDo(t *testing.T) {
17+
t.Run("redis", func(t *testing.T) {
18+
url := "redis://127.0.0.1:6379/0"
19+
if os.Getenv("CI") == "" {
20+
// Make it possible to run tests against a local redis instance
21+
url = os.Getenv("TEST_REDIS_URL")
22+
if url == "" {
23+
t.Skip("TEST_REDIS_URL not set and not running in CI")
24+
return
25+
}
26+
}
27+
28+
oldDefaultLocker := defaultLocker
29+
oldInitFunc := initFunc
30+
defer func() {
31+
defaultLocker = oldDefaultLocker
32+
initFunc = oldInitFunc
33+
if defaultLocker == nil {
34+
initOnce = sync.Once{}
35+
}
36+
}()
37+
38+
initOnce = sync.Once{}
39+
initFunc = func() {
40+
defaultLocker = NewRedisLocker(url)
41+
}
42+
43+
testLockAndDo(t)
44+
require.NoError(t, defaultLocker.(*redisLocker).Close())
45+
})
46+
t.Run("memory", func(t *testing.T) {
47+
oldDefaultLocker := defaultLocker
48+
oldInitFunc := initFunc
49+
defer func() {
50+
defaultLocker = oldDefaultLocker
51+
initFunc = oldInitFunc
52+
if defaultLocker == nil {
53+
initOnce = sync.Once{}
54+
}
55+
}()
56+
57+
initOnce = sync.Once{}
58+
initFunc = func() {
59+
defaultLocker = NewMemoryLocker()
60+
}
61+
62+
testLockAndDo(t)
63+
})
64+
}
65+
66+
func testLockAndDo(t *testing.T) {
67+
const concurrency = 1000
68+
69+
ctx := context.Background()
70+
count := 0
71+
wg := sync.WaitGroup{}
72+
wg.Add(concurrency)
73+
for i := 0; i < concurrency; i++ {
74+
go func() {
75+
defer wg.Done()
76+
err := LockAndDo(ctx, "test", func(ctx context.Context) error {
77+
count++
78+
79+
// It's impossible to acquire the lock inner the function
80+
ok, err := TryLockAndDo(ctx, "test", func(ctx context.Context) error {
81+
assert.Fail(t, "should not acquire the lock")
82+
return nil
83+
})
84+
assert.False(t, ok)
85+
assert.NoError(t, err)
86+
87+
return nil
88+
})
89+
require.NoError(t, err)
90+
}()
91+
}
92+
93+
wg.Wait()
94+
95+
assert.Equal(t, concurrency, count)
96+
}

modules/globallock/locker.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package globallock
5+
6+
import (
7+
"context"
8+
"fmt"
9+
)
10+
11+
type Locker interface {
12+
// Lock tries to acquire a lock for the given key, it blocks until the lock is acquired or the context is canceled.
13+
//
14+
// Lock returns a new context which should be used in the following code.
15+
// The new context will be canceled when the lock is released or lost - yes, it's possible to lose a lock.
16+
// For example, it lost the connection to the redis server while holding the lock.
17+
// If it fails to acquire the lock, the returned context will be the same as the input context.
18+
//
19+
// Lock returns a ReleaseFunc to release the lock, it cannot be nil.
20+
// It's always safe to call this function even if it fails to acquire the lock, and it will do nothing in that case.
21+
// And it's also safe to call it multiple times, but it will only release the lock once.
22+
// That's why it's called ReleaseFunc, not UnlockFunc.
23+
// But be aware that it's not safe to not call it at all; it could lead to a memory leak.
24+
// So a recommended pattern is to use defer to call it:
25+
// ctx, release, err := locker.Lock(ctx, "key")
26+
// if err != nil {
27+
// return err
28+
// }
29+
// defer release()
30+
// The ReleaseFunc will return the original context which was used to acquire the lock.
31+
// It's useful when you want to continue to do something after releasing the lock.
32+
// At that time, the ctx will be canceled, and you can use the returned context by the ReleaseFunc to continue:
33+
// ctx, release, err := locker.Lock(ctx, "key")
34+
// if err != nil {
35+
// return err
36+
// }
37+
// defer release()
38+
// doSomething(ctx)
39+
// ctx = release()
40+
// doSomethingElse(ctx)
41+
// Please ignore it and use `defer release()` instead if you don't need this, to avoid forgetting to release the lock.
42+
//
43+
// Lock returns an error if failed to acquire the lock.
44+
// Be aware that even the context is not canceled, it's still possible to fail to acquire the lock.
45+
// For example, redis is down, or it reached the maximum number of tries.
46+
Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error)
47+
48+
// TryLock tries to acquire a lock for the given key, it returns immediately.
49+
// It follows the same pattern as Lock, but it doesn't block.
50+
// And if it fails to acquire the lock because it's already locked, not other reasons like redis is down,
51+
// it will return false without any error.
52+
TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error)
53+
}
54+
55+
// ReleaseFunc is a function that releases a lock.
56+
// It returns the original context which was used to acquire the lock.
57+
type ReleaseFunc func() context.Context
58+
59+
// ErrLockReleased is used as context cause when a lock is released
60+
var ErrLockReleased = fmt.Errorf("lock released")

0 commit comments

Comments
 (0)