Skip to content

Add Locker interface with Postgres table-based locking implementation#993

Merged
mfridman merged 13 commits intomainfrom
mf/locker
Sep 24, 2025
Merged

Add Locker interface with Postgres table-based locking implementation#993
mfridman merged 13 commits intomainfrom
mf/locker

Conversation

@mfridman
Copy link
Copy Markdown
Collaborator

@mfridman mfridman commented Sep 23, 2025

Fix #972 and should help with improved goose support for cockroachdb/cockroach#13546 and PgBouncer when used with transaction pooling.

This PR adds a few things.

  1. A new goose provider option WithLocker which uses the new lock.Locker interface:
// Locker is the interface to lock and unlock the database.
//
// Unlike [SessionLocker], the Lock and Unlock methods are called on a [*sql.DB] and do not require
// the same connection to be used for both methods.
type Locker interface {
	Lock(ctx context.Context, db *sql.DB) error
	Unlock(ctx context.Context, db *sql.DB) error
}

Note, although we added Postgres support, this could easily be extended to other databases either via a custom Locker implementation or a solid PR within Goose for one of the natively supported databases.

  1. A NewPostgresTableLocker implementation which uses a new table goose_lock (configurable) to provide connection-independent locking with automatic lease expiration

Usage:

locker, err := lock.NewPostgresTableLocker() // <--- NEW
if err != nil {
	return err
}
provider, err := goose.NewProvider(
	database.DialectPostgres,
	db,
	os.DirFS(dir),
	goose.WithLocker(locker), // <--- NEW
)
if err != nil {
	return err
}

The defaults are documented, but I'll add them here for brevity.

  • Table: "goose_lock"
  • Lock ID: 4097083626 (crc32 IEEE of "goose")
  • Lease duration: 30s (lock expires if heartbeat stops)
  • Heartbeat interval: 5s (keeps lock alive)
  • Lock acquisition: 5s retry intervals, 5min timeout
  • Lock release: 2s retry intervals, 1min timeout

In other words, when multiple instances start simultaneously, one acquires the lock and runs migrations (refreshing every 5 seconds) while others retry every 5 seconds for up to 5 minutes, with automatic failover after 30 seconds if the heartbeat stops.

@mfridman mfridman merged commit 57d20f1 into main Sep 24, 2025
4 checks passed
@mfridman mfridman deleted the mf/locker branch September 24, 2025 12:40
akshah123 pushed a commit to akshah123/goose that referenced this pull request Dec 14, 2025
This adds a complete locking mechanism for SQL Server, similar to
the existing PostgreSQL implementation from PR pressly#993.

Features:
- Table-based locking using MERGE for atomic upserts
- Session-based locking using sp_getapplock/sp_releaseapplock
- Automatic lease renewal via heartbeat mechanism
- Stale lock cleanup for expired leases

Implementation:
- lock/internal/store/sqlserver.go: LockStore implementation
- lock/sqlserver.go: Public API (NewSqlserverTableLocker, NewSqlserverSessionLocker)
- internal/testing/testdb/sqlserver.go: Docker test helper
- SQL Server specific test migrations
- Comprehensive integration tests for both locking modes

The table-based locker uses SQL Server's MERGE statement with HOLDLOCK
for atomic lock acquisition and SYSUTCDATETIME() for server-side time.
The session-based locker uses SQL Server's built-in application lock
stored procedures which provide session-level advisory locking.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add table-based migration locking for databases

1 participant