@@ -4,9 +4,11 @@ import (
44 "context"
55 "sync"
66 "sync/atomic"
7- "testing"
87 "time"
98
9+ . "github.com/onsi/ginkgo/v2"
10+ . "github.com/onsi/gomega"
11+
1012 "github.com/mudler/LocalAI/core/services/messaging"
1113
1214 "github.com/testcontainers/testcontainers-go"
@@ -17,11 +19,8 @@ import (
1719 "gorm.io/gorm/logger"
1820)
1921
20- func setupTestDB (t * testing.T ) * gorm.DB {
21- t .Helper ()
22- t .Skip ("requires docker" )
23-
24- ctx := t .Context ()
22+ func setupTestDB () * gorm.DB {
23+ ctx := context .Background ()
2524
2625 pgC , err := tcpostgres .Run (ctx , "postgres:16" ,
2726 tcpostgres .WithDatabase ("testdb" ),
@@ -30,132 +29,109 @@ func setupTestDB(t *testing.T) *gorm.DB {
3029 testcontainers .WithWaitStrategyAndDeadline (60 * time .Second ,
3130 wait .ForLog ("database system is ready to accept connections" ).WithOccurrence (2 )),
3231 )
33- if err != nil {
34- t .Fatalf ("start postgres container: %v" , err )
35- }
36- t .Cleanup (func () { pgC .Terminate (context .Background ()) })
32+ Expect (err ).ToNot (HaveOccurred ())
33+ DeferCleanup (func () { pgC .Terminate (context .Background ()) })
3734
3835 connStr , err := pgC .ConnectionString (ctx , "sslmode=disable" )
39- if err != nil {
40- t .Fatalf ("connection string: %v" , err )
41- }
36+ Expect (err ).ToNot (HaveOccurred ())
4237
4338 db , err := gorm .Open (postgres .Open (connStr ), & gorm.Config {
4439 Logger : logger .Default .LogMode (logger .Silent ),
4540 })
46- if err != nil {
47- t .Fatalf ("open gorm: %v" , err )
48- }
41+ Expect (err ).ToNot (HaveOccurred ())
4942 return db
5043}
5144
52- func TestWithLock_PreventsConcurrentExecution (t * testing.T ) {
53- db := setupTestDB (t )
54-
55- const lockKey int64 = 999
56-
57- var (
58- mu sync.Mutex
59- maxRunning int32
60- running int32
61- concurrency int32
62- )
63-
64- var wg sync.WaitGroup
65- wg .Add (2 )
66-
67- for i := 0 ; i < 2 ; i ++ {
68- go func () {
69- defer wg .Done ()
70- err := WithLock (db , lockKey , func () error {
71- cur := atomic .AddInt32 (& running , 1 )
72- mu .Lock ()
73- if cur > maxRunning {
74- maxRunning = cur
75- }
76- if cur > 1 {
77- atomic .AddInt32 (& concurrency , 1 )
78- }
79- mu .Unlock ()
80-
81- // Hold the lock briefly so the other goroutine has a chance to contend.
82- time .Sleep (50 * time .Millisecond )
83-
84- atomic .AddInt32 (& running , - 1 )
85- return nil
86- })
87- if err != nil {
88- t .Errorf ("WithLock returned error: %v" , err )
45+ var _ = Describe ("AdvisoryLock" , func () {
46+ Context ("PostgreSQL advisory locks" , func () {
47+ var db * gorm.DB
48+
49+ BeforeEach (func () {
50+ db = setupTestDB ()
51+ })
52+
53+ It ("prevents concurrent execution with WithLock" , func () {
54+ const lockKey int64 = 999
55+
56+ var (
57+ mu sync.Mutex
58+ maxRunning int32
59+ running int32
60+ concurrency int32
61+ )
62+
63+ var wg sync.WaitGroup
64+ wg .Add (2 )
65+
66+ for i := 0 ; i < 2 ; i ++ {
67+ go func () {
68+ defer GinkgoRecover ()
69+ defer wg .Done ()
70+ err := WithLock (db , lockKey , func () error {
71+ cur := atomic .AddInt32 (& running , 1 )
72+ mu .Lock ()
73+ if cur > maxRunning {
74+ maxRunning = cur
75+ }
76+ if cur > 1 {
77+ atomic .AddInt32 (& concurrency , 1 )
78+ }
79+ mu .Unlock ()
80+
81+ time .Sleep (50 * time .Millisecond )
82+
83+ atomic .AddInt32 (& running , - 1 )
84+ return nil
85+ })
86+ Expect (err ).ToNot (HaveOccurred ())
87+ }()
8988 }
90- }()
91- }
92-
93- wg .Wait ()
94-
95- if maxRunning > 1 {
96- t .Errorf ("expected max 1 goroutine inside lock at a time, observed %d" , maxRunning )
97- }
98- if concurrency > 0 {
99- t .Errorf ("detected concurrent execution inside advisory lock" )
100- }
101- }
102-
103- func TestTryLock_AcquiresAndUnlocks (t * testing.T ) {
104- db := setupTestDB (t )
10589
106- const lockKey int64 = 888
90+ wg . Wait ()
10791
108- // NOTE: TryLock and Unlock may use different pool connections,
109- // so this test documents the API contract but may sometimes pass
110- // even when the implementation has a connection-affinity bug.
111- acquired := TryLock (db , lockKey )
112- if ! acquired {
113- t .Fatal ("expected TryLock to acquire the lock" )
114- }
92+ Expect (maxRunning ).To (BeNumerically ("<=" , 1 ), "expected max 1 goroutine inside lock at a time" )
93+ Expect (concurrency ).To (BeZero (), "detected concurrent execution inside advisory lock" )
94+ })
11595
116- Unlock (db , lockKey )
96+ It ("acquires and unlocks with TryLock" , func () {
97+ const lockKey int64 = 888
11798
118- reacquired := TryLock (db , lockKey )
119- if ! reacquired {
120- t .Error ("expected TryLock to re-acquire the lock after Unlock" )
121- }
99+ acquired := TryLock (db , lockKey )
100+ Expect (acquired ).To (BeTrue (), "expected TryLock to acquire the lock" )
122101
123- // Clean up
124- Unlock (db , lockKey )
125- }
102+ Unlock (db , lockKey )
126103
127- func TestKeyFromUUID_Deterministic (t * testing.T ) {
128- a := [16 ]byte {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 }
129- b := [16 ]byte {16 , 15 , 14 , 13 , 12 , 11 , 10 , 9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 }
104+ reacquired := TryLock (db , lockKey )
105+ Expect (reacquired ).To (BeTrue (), "expected TryLock to re-acquire the lock after Unlock" )
130106
131- keyA1 := KeyFromUUID ( a )
132- keyA2 := KeyFromUUID ( a )
133- keyB := KeyFromUUID ( b )
107+ Unlock ( db , lockKey )
108+ } )
109+ } )
134110
135- if keyA1 != keyA2 {
136- t .Errorf ("KeyFromUUID not deterministic: got %d and %d for same input" , keyA1 , keyA2 )
137- }
138- if keyA1 == keyB {
139- t .Errorf ("KeyFromUUID returned same key %d for different inputs" , keyA1 )
140- }
141- }
111+ Context ("pure logic" , func () {
112+ It ("KeyFromUUID is deterministic" , func () {
113+ a := [16 ]byte {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 }
114+ b := [16 ]byte {16 , 15 , 14 , 13 , 12 , 11 , 10 , 9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 }
115+
116+ keyA1 := KeyFromUUID (a )
117+ keyA2 := KeyFromUUID (a )
118+ keyB := KeyFromUUID (b )
119+
120+ Expect (keyA1 ).To (Equal (keyA2 ), "KeyFromUUID not deterministic" )
121+ Expect (keyA1 ).ToNot (Equal (keyB ), "KeyFromUUID returned same key for different inputs" )
122+ })
123+
124+ It ("all advisory lock keys are unique" , func () {
125+ keys := map [int64 ]string {
126+ messaging .AdvisoryLockCronScheduler : "messaging.AdvisoryLockCronScheduler" ,
127+ messaging .AdvisoryLockStaleNodeCleanup : "messaging.AdvisoryLockStaleNodeCleanup" ,
128+ messaging .AdvisoryLockGalleryDedup : "messaging.AdvisoryLockGalleryDedup" ,
129+ messaging .AdvisoryLockAgentScheduler : "messaging.AdvisoryLockAgentScheduler" ,
130+ messaging .AdvisoryLockHealthCheck : "messaging.AdvisoryLockHealthCheck" ,
131+ messaging .AdvisoryLockSchemaMigrate : "messaging.AdvisoryLockSchemaMigrate" ,
132+ }
142133
143- func TestAllLockKeysUnique (t * testing.T ) {
144- // Collect all advisory lock keys from both the advisorylock package
145- // and the messaging package. They share the same PostgreSQL lock
146- // namespace so every key must be unique.
147- keys := map [int64 ]string {
148- messaging .AdvisoryLockCronScheduler : "messaging.AdvisoryLockCronScheduler" ,
149- messaging .AdvisoryLockStaleNodeCleanup : "messaging.AdvisoryLockStaleNodeCleanup" ,
150- messaging .AdvisoryLockGalleryDedup : "messaging.AdvisoryLockGalleryDedup" ,
151- messaging .AdvisoryLockAgentScheduler : "messaging.AdvisoryLockAgentScheduler" ,
152- messaging .AdvisoryLockHealthCheck : "messaging.AdvisoryLockHealthCheck" ,
153- messaging .AdvisoryLockSchemaMigrate : "messaging.AdvisoryLockSchemaMigrate" ,
154- }
155-
156- // If any keys collide, the map will have fewer entries than expected.
157- expectedCount := 6
158- if len (keys ) != expectedCount {
159- t .Errorf ("expected %d unique advisory lock keys, got %d — some keys have the same value" , expectedCount , len (keys ))
160- }
161- }
134+ Expect (keys ).To (HaveLen (6 ), "some advisory lock keys have the same value" )
135+ })
136+ })
137+ })
0 commit comments