@@ -23,6 +23,12 @@ import (
23
23
"github.com/jackc/pgconn"
24
24
"github.com/jackc/pgerrcode"
25
25
_ "github.com/jackc/pgx/v4/stdlib"
26
+ "github.com/lib/pq"
27
+ )
28
+
29
+ const (
30
+ LockStrategyAdvisory = "advisory"
31
+ LockStrategyTable = "table"
26
32
)
27
33
28
34
func init () {
36
42
37
43
DefaultMigrationsTable = "schema_migrations"
38
44
DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB
45
+ DefaultLockTable = "schema_lock"
46
+ DefaultLockStrategy = LockStrategyAdvisory
39
47
)
40
48
41
49
var (
@@ -49,6 +57,8 @@ type Config struct {
49
57
MigrationsTable string
50
58
DatabaseName string
51
59
SchemaName string
60
+ LockTable string
61
+ LockStrategy string
52
62
migrationsSchemaName string
53
63
migrationsTableName string
54
64
StatementTimeout time.Duration
@@ -108,6 +118,14 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
108
118
config .MigrationsTable = DefaultMigrationsTable
109
119
}
110
120
121
+ if len (config .LockTable ) == 0 {
122
+ config .LockTable = DefaultLockTable
123
+ }
124
+
125
+ if len (config .LockStrategy ) == 0 {
126
+ config .LockStrategy = DefaultLockStrategy
127
+ }
128
+
111
129
config .migrationsSchemaName = config .SchemaName
112
130
config .migrationsTableName = config .MigrationsTable
113
131
if config .MigrationsTableQuoted {
@@ -133,6 +151,10 @@ func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
133
151
config : config ,
134
152
}
135
153
154
+ if err := px .ensureLockTable (); err != nil {
155
+ return nil , err
156
+ }
157
+
136
158
if err := px .ensureVersionTable (); err != nil {
137
159
return nil , err
138
160
}
@@ -196,13 +218,18 @@ func (p *Postgres) Open(url string) (database.Driver, error) {
196
218
}
197
219
}
198
220
221
+ lockStrategy := purl .Query ().Get ("x-lock-strategy" )
222
+ lockTable := purl .Query ().Get ("x-lock-table" )
223
+
199
224
px , err := WithInstance (db , & Config {
200
225
DatabaseName : purl .Path ,
201
226
MigrationsTable : migrationsTable ,
202
227
MigrationsTableQuoted : migrationsTableQuoted ,
203
228
StatementTimeout : time .Duration (statementTimeout ) * time .Millisecond ,
204
229
MultiStatementEnabled : multiStatementEnabled ,
205
230
MultiStatementMaxSize : multiStatementMaxSize ,
231
+ LockStrategy : lockStrategy ,
232
+ LockTable : lockTable ,
206
233
})
207
234
208
235
if err != nil {
@@ -221,36 +248,116 @@ func (p *Postgres) Close() error {
221
248
return nil
222
249
}
223
250
224
- // https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS
225
251
func (p * Postgres ) Lock () error {
226
252
return database .CasRestoreOnErr (& p .isLocked , false , true , database .ErrLocked , func () error {
227
- aid , err := database .GenerateAdvisoryLockId (p .config .DatabaseName , p .config .migrationsSchemaName , p .config .migrationsTableName )
228
- if err != nil {
229
- return err
230
- }
231
-
232
- // This will wait indefinitely until the lock can be acquired.
233
- query := `SELECT pg_advisory_lock($1)`
234
- if _ , err := p .conn .ExecContext (context .Background (), query , aid ); err != nil {
235
- return & database.Error {OrigErr : err , Err : "try lock failed" , Query : []byte (query )}
253
+ switch p .config .LockStrategy {
254
+ case LockStrategyAdvisory :
255
+ return p .applyAdvisoryLock ()
256
+ case LockStrategyTable :
257
+ return p .applyTableLock ()
258
+ default :
259
+ return fmt .Errorf ("unknown lock strategy \" %s\" " , p .config .LockStrategy )
236
260
}
237
- return nil
238
261
})
239
262
}
240
263
241
264
func (p * Postgres ) Unlock () error {
242
265
return database .CasRestoreOnErr (& p .isLocked , true , false , database .ErrNotLocked , func () error {
243
- aid , err := database .GenerateAdvisoryLockId (p .config .DatabaseName , p .config .migrationsSchemaName , p .config .migrationsTableName )
244
- if err != nil {
245
- return err
266
+ switch p .config .LockStrategy {
267
+ case LockStrategyAdvisory :
268
+ return p .releaseAdvisoryLock ()
269
+ case LockStrategyTable :
270
+ return p .releaseTableLock ()
271
+ default :
272
+ return fmt .Errorf ("unknown lock strategy \" %s\" " , p .config .LockStrategy )
246
273
}
274
+ })
275
+ }
247
276
248
- query := `SELECT pg_advisory_unlock($1)`
249
- if _ , err := p .conn .ExecContext (context .Background (), query , aid ); err != nil {
250
- return & database.Error {OrigErr : err , Query : []byte (query )}
277
+ // https://www.postgresql.org/docs/9.6/static/explicit-locking.html#ADVISORY-LOCKS
278
+ func (p * Postgres ) applyAdvisoryLock () error {
279
+ aid , err := database .GenerateAdvisoryLockId (p .config .DatabaseName , p .config .migrationsSchemaName , p .config .migrationsTableName )
280
+ if err != nil {
281
+ return err
282
+ }
283
+
284
+ // This will wait indefinitely until the lock can be acquired.
285
+ query := `SELECT pg_advisory_lock($1)`
286
+ if _ , err := p .conn .ExecContext (context .Background (), query , aid ); err != nil {
287
+ return & database.Error {OrigErr : err , Err : "try lock failed" , Query : []byte (query )}
288
+ }
289
+ return nil
290
+ }
291
+
292
+ func (p * Postgres ) applyTableLock () error {
293
+ tx , err := p .conn .BeginTx (context .Background (), & sql.TxOptions {})
294
+ if err != nil {
295
+ return & database.Error {OrigErr : err , Err : "transaction start failed" }
296
+ }
297
+ defer func () {
298
+ errRollback := tx .Rollback ()
299
+ if errRollback != nil {
300
+ err = multierror .Append (err , errRollback )
251
301
}
252
- return nil
253
- })
302
+ }()
303
+
304
+ aid , err := database .GenerateAdvisoryLockId (p .config .DatabaseName )
305
+ if err != nil {
306
+ return err
307
+ }
308
+
309
+ query := "SELECT * FROM " + pq .QuoteIdentifier (p .config .LockTable ) + " WHERE lock_id = $1"
310
+ rows , err := tx .Query (query , aid )
311
+ if err != nil {
312
+ return database.Error {OrigErr : err , Err : "failed to fetch migration lock" , Query : []byte (query )}
313
+ }
314
+
315
+ defer func () {
316
+ if errClose := rows .Close (); errClose != nil {
317
+ err = multierror .Append (err , errClose )
318
+ }
319
+ }()
320
+
321
+ // If row exists at all, lock is present
322
+ locked := rows .Next ()
323
+ if locked {
324
+ return database .ErrLocked
325
+ }
326
+
327
+ query = "INSERT INTO " + pq .QuoteIdentifier (p .config .LockTable ) + " (lock_id) VALUES ($1)"
328
+ if _ , err := tx .Exec (query , aid ); err != nil {
329
+ return database.Error {OrigErr : err , Err : "failed to set migration lock" , Query : []byte (query )}
330
+ }
331
+
332
+ return tx .Commit ()
333
+ }
334
+
335
+ func (p * Postgres ) releaseAdvisoryLock () error {
336
+ aid , err := database .GenerateAdvisoryLockId (p .config .DatabaseName , p .config .migrationsSchemaName , p .config .migrationsTableName )
337
+ if err != nil {
338
+ return err
339
+ }
340
+
341
+ query := `SELECT pg_advisory_unlock($1)`
342
+ if _ , err := p .conn .ExecContext (context .Background (), query , aid ); err != nil {
343
+ return & database.Error {OrigErr : err , Query : []byte (query )}
344
+ }
345
+
346
+ return nil
347
+ }
348
+
349
+ func (p * Postgres ) releaseTableLock () error {
350
+ aid , err := database .GenerateAdvisoryLockId (p .config .DatabaseName )
351
+ if err != nil {
352
+ return err
353
+ }
354
+
355
+ query := "DELETE FROM " + pq .QuoteIdentifier (p .config .LockTable ) + " WHERE lock_id = $1"
356
+ if _ , err := p .db .Exec (query , aid ); err != nil {
357
+ return database.Error {OrigErr : err , Err : "failed to release migration lock" , Query : []byte (query )}
358
+ }
359
+
360
+ return nil
254
361
}
255
362
256
363
func (p * Postgres ) Run (migration io.Reader ) error {
@@ -414,6 +521,12 @@ func (p *Postgres) Drop() (err error) {
414
521
if err := tables .Scan (& tableName ); err != nil {
415
522
return err
416
523
}
524
+
525
+ // do not drop lock table
526
+ if tableName == p .config .LockTable && p .config .LockStrategy == LockStrategyTable {
527
+ continue
528
+ }
529
+
417
530
if len (tableName ) > 0 {
418
531
tableNames = append (tableNames , tableName )
419
532
}
@@ -478,6 +591,28 @@ func (p *Postgres) ensureVersionTable() (err error) {
478
591
return nil
479
592
}
480
593
594
+ func (p * Postgres ) ensureLockTable () error {
595
+ if p .config .LockStrategy != LockStrategyTable {
596
+ return nil
597
+ }
598
+
599
+ var count int
600
+ query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1`
601
+ if err := p .db .QueryRow (query , p .config .LockTable ).Scan (& count ); err != nil {
602
+ return & database.Error {OrigErr : err , Query : []byte (query )}
603
+ }
604
+ if count == 1 {
605
+ return nil
606
+ }
607
+
608
+ query = `CREATE TABLE ` + pq .QuoteIdentifier (p .config .LockTable ) + ` (lock_id BIGINT NOT NULL PRIMARY KEY)`
609
+ if _ , err := p .db .Exec (query ); err != nil {
610
+ return & database.Error {OrigErr : err , Query : []byte (query )}
611
+ }
612
+
613
+ return nil
614
+ }
615
+
481
616
// Copied from lib/pq implementation: https://github.com/lib/pq/blob/v1.9.0/conn.go#L1611
482
617
func quoteIdentifier (name string ) string {
483
618
end := strings .IndexRune (name , 0 )
0 commit comments