Skip to content

Commit 78a9f80

Browse files
committed
Fix sqlserver lock
When acquiring the `sqlserver` lock, it fails immediately if the lock can't be acquired. When running multiple migrations concurrently, only one of them will succeed and the other ones will fail. Wait for up to 10 seconds when acquiring the lock. This way it lets the other entity that has acquired the lock do its job first. This is consistent with what this library does for `postgres` (though it waits indefinitely) and for `mysql`.
1 parent c378583 commit 78a9f80

File tree

2 files changed

+89
-3
lines changed

2 files changed

+89
-3
lines changed

database/sqlserver/sqlserver.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,10 @@ func (ss *SQLServer) Lock() error {
198198
return err
199199
}
200200

201-
// This will either obtain the lock immediately and return true,
202-
// or return false if the lock cannot be acquired immediately.
201+
// This will either obtain the lock within 10 seconds and return true,
202+
// or return false if the lock cannot be acquired within 10 seconds.
203203
// MS Docs: sp_getapplock: https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-getapplock-transact-sql?view=sql-server-2017
204-
query := `EXEC sp_getapplock @Resource = @p1, @LockMode = 'Update', @LockOwner = 'Session', @LockTimeout = 0`
204+
query := `EXEC sp_getapplock @Resource = @p1, @LockMode = 'Update', @LockOwner = 'Session', @LockTimeout = 10000`
205205

206206
var status mssql.ReturnStatus
207207
if _, err = ss.conn.ExecContext(context.Background(), query, aid, &status); err == nil && status > -1 {

database/sqlserver/sqlserver_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log"
99
"runtime"
1010
"strings"
11+
"sync"
1112
"testing"
1213
"time"
1314

@@ -104,6 +105,8 @@ func Test(t *testing.T) {
104105
t.Run("testMsiTrue", testMsiTrue)
105106
t.Run("testOpenWithPasswordAndMSI", testOpenWithPasswordAndMSI)
106107
t.Run("testMsiFalse", testMsiFalse)
108+
t.Run("testLock", testLock)
109+
t.Run("testWithInstanceConcurrent", testWithInstanceConcurrent)
107110

108111
t.Cleanup(func() {
109112
for _, spec := range specs {
@@ -339,3 +342,86 @@ func testMsiFalse(t *testing.T) {
339342
}
340343
})
341344
}
345+
346+
func testLock(t *testing.T) {
347+
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
348+
ip, port, err := c.FirstPort()
349+
if err != nil {
350+
t.Fatal(err)
351+
}
352+
353+
addr := msConnectionString(ip, port)
354+
p := &SQLServer{}
355+
d, err := p.Open(addr)
356+
if err != nil {
357+
t.Fatal(err)
358+
}
359+
360+
dt.Test(t, d, []byte("SELECT 1"))
361+
362+
ps := d.(*SQLServer)
363+
364+
err = ps.Lock()
365+
if err != nil {
366+
t.Fatal(err)
367+
}
368+
369+
err = ps.Unlock()
370+
if err != nil {
371+
t.Fatal(err)
372+
}
373+
374+
err = ps.Lock()
375+
if err != nil {
376+
t.Fatal(err)
377+
}
378+
379+
err = ps.Unlock()
380+
if err != nil {
381+
t.Fatal(err)
382+
}
383+
})
384+
}
385+
386+
func testWithInstanceConcurrent(t *testing.T) {
387+
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
388+
ip, port, err := c.FirstPort()
389+
if err != nil {
390+
t.Fatal(err)
391+
}
392+
393+
// The number of concurrent processes running WithInstance
394+
const concurrency = 30
395+
396+
// We can instantiate a single database handle because it is
397+
// actually a connection pool, and so, each of the below go
398+
// routines will have a high probability of using a separate
399+
// connection, which is something we want to exercise.
400+
db, err := sql.Open("sqlserver", msConnectionString(ip, port))
401+
if err != nil {
402+
t.Fatal(err)
403+
}
404+
defer func() {
405+
if err := db.Close(); err != nil {
406+
t.Error(err)
407+
}
408+
}()
409+
410+
db.SetMaxIdleConns(concurrency)
411+
db.SetMaxOpenConns(concurrency)
412+
413+
var wg sync.WaitGroup
414+
defer wg.Wait()
415+
416+
wg.Add(concurrency)
417+
for i := 0; i < concurrency; i++ {
418+
go func(i int) {
419+
defer wg.Done()
420+
_, err := WithInstance(db, &Config{})
421+
if err != nil {
422+
t.Errorf("process %d error: %s", i, err)
423+
}
424+
}(i)
425+
}
426+
})
427+
}

0 commit comments

Comments
 (0)