@@ -24,8 +24,15 @@ const (
2424)
2525
2626// Executor implements migrate.Executor for MySQL.
27+ //
28+ // When the underlying driver supports [driver.ConnAcquirer], the executor
29+ // acquires a dedicated connection in [AcquireLock] and routes ALL subsequent
30+ // operations through it until [ReleaseLock]. This guarantees that the
31+ // session-level GET_LOCK is acquired and released on the same connection,
32+ // preventing advisory-lock leaks in pooled environments.
2733type Executor struct {
28- drv driver.Driver
34+ drv driver.Driver
35+ dedicated driver.DedicatedConn // non-nil while migration lock is held
2936}
3037
3138var _ migrate.Executor = (* Executor )(nil )
@@ -35,14 +42,44 @@ func New(drv driver.Driver) *Executor {
3542 return & Executor {drv : drv }
3643}
3744
45+ // --- routing helpers: prefer dedicated conn when available ---
46+
47+ func (e * Executor ) exec (ctx context.Context , query string , args ... any ) (driver.Result , error ) {
48+ if e .dedicated != nil {
49+ return e .dedicated .Exec (ctx , query , args ... )
50+ }
51+ return e .drv .Exec (ctx , query , args ... )
52+ }
53+
54+ func (e * Executor ) query (ctx context.Context , query string , args ... any ) (driver.Rows , error ) {
55+ if e .dedicated != nil {
56+ return e .dedicated .Query (ctx , query , args ... )
57+ }
58+ return e .drv .Query (ctx , query , args ... )
59+ }
60+
61+ func (e * Executor ) queryRow (ctx context.Context , query string , args ... any ) driver.Row {
62+ if e .dedicated != nil {
63+ return e .dedicated .QueryRow (ctx , query , args ... )
64+ }
65+ return e .drv .QueryRow (ctx , query , args ... )
66+ }
67+
68+ func (e * Executor ) releaseDedicated () {
69+ if e .dedicated != nil {
70+ e .dedicated .Release ()
71+ e .dedicated = nil
72+ }
73+ }
74+
3875// Exec executes a SQL statement that does not return rows.
3976func (e * Executor ) Exec (ctx context.Context , query string , args ... any ) (driver.Result , error ) {
40- return e .drv . Exec (ctx , query , args ... )
77+ return e .exec (ctx , query , args ... )
4178}
4279
4380// Query executes a SQL statement that returns rows.
4481func (e * Executor ) Query (ctx context.Context , query string , args ... any ) (driver.Rows , error ) {
45- return e .drv . Query (ctx , query , args ... )
82+ return e .query (ctx , query , args ... )
4683}
4784
4885// EnsureMigrationTable creates the grove_migrations table if it doesn't exist.
@@ -55,7 +92,7 @@ func (e *Executor) EnsureMigrationTable(ctx context.Context) error {
5592 "`migrated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), " +
5693 "UNIQUE KEY `uq_version_group` (`version`, `group`)" +
5794 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" , migrationTableName )
58- _ , err := e .drv . Exec (ctx , query )
95+ _ , err := e .exec (ctx , query )
5996 return err
6097}
6198
@@ -67,21 +104,37 @@ func (e *Executor) EnsureLockTable(ctx context.Context) error {
67104 "`locked_by` VARCHAR(255), " +
68105 "CONSTRAINT `single_lock` CHECK (`id` = 1)" +
69106 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" , lockTableName )
70- _ , err := e .drv . Exec (ctx , query )
107+ _ , err := e .exec (ctx , query )
71108 return err
72109}
73110
74111// AcquireLock attempts to acquire the distributed migration lock using
75112// MySQL advisory locks (GET_LOCK) for immediate feedback.
113+ //
114+ // If the driver implements [driver.ConnAcquirer], a dedicated connection is
115+ // acquired first so that the advisory lock and all subsequent migration
116+ // operations share the same session. This prevents lock leaks when using
117+ // connection pools.
76118func (e * Executor ) AcquireLock (ctx context.Context , lockedBy string ) error {
119+ // Acquire a dedicated connection if the driver supports it.
120+ if acq , ok := e .drv .(driver.ConnAcquirer ); ok {
121+ conn , err := acq .AcquireConn (ctx )
122+ if err != nil {
123+ return fmt .Errorf ("mysqlmigrate: acquire dedicated conn: %w" , err )
124+ }
125+ e .dedicated = conn
126+ }
127+
77128 // Use MySQL advisory lock. GET_LOCK returns 1 if acquired, 0 if timeout.
78129 // Timeout of 0 means try immediately without waiting.
79- row := e .drv . QueryRow (ctx , "SELECT GET_LOCK(?, 0)" , advisoryLockName )
130+ row := e .queryRow (ctx , "SELECT GET_LOCK(?, 0)" , advisoryLockName )
80131 var acquired int
81132 if err := row .Scan (& acquired ); err != nil {
133+ e .releaseDedicated ()
82134 return fmt .Errorf ("mysqlmigrate: advisory lock: %w" , err )
83135 }
84136 if acquired != 1 {
137+ e .releaseDedicated ()
85138 return fmt .Errorf ("mysqlmigrate: migration lock is held by another process" )
86139 }
87140
@@ -90,19 +143,32 @@ func (e *Executor) AcquireLock(ctx context.Context, lockedBy string) error {
90143 "VALUES (1, NOW(6), ?) " +
91144 "ON DUPLICATE KEY UPDATE `locked_at` = NOW(6), `locked_by` = ?" ,
92145 lockTableName )
93- _ , err := e .drv .Exec (ctx , query , lockedBy , lockedBy )
94- return err
146+ if _ , err := e .exec (ctx , query , lockedBy , lockedBy ); err != nil {
147+ // Best-effort unlock before releasing the connection.
148+ _ , _ = e .exec (ctx , "SELECT RELEASE_LOCK(?)" , advisoryLockName ) //nolint:errcheck // best-effort cleanup on error path
149+ e .releaseDedicated ()
150+ return err
151+ }
152+
153+ return nil
95154}
96155
97156// ReleaseLock releases the distributed migration lock.
157+ // If a dedicated connection was acquired in [AcquireLock], the advisory lock
158+ // is released on that same connection before the connection is returned to
159+ // the pool.
98160func (e * Executor ) ReleaseLock (ctx context.Context ) error {
99161 // Clear the lock record.
100162 query := fmt .Sprintf ("UPDATE `%s` SET `locked_at` = NULL, `locked_by` = NULL WHERE `id` = 1" ,
101163 lockTableName )
102- _ , _ = e .drv .Exec (ctx , query )
164+ _ , _ = e .exec (ctx , query ) //nolint:errcheck // best-effort lock record clearing
165+
166+ // Release the advisory lock (on the SAME connection that acquired it).
167+ _ , err := e .exec (ctx , "SELECT RELEASE_LOCK(?)" , advisoryLockName )
168+
169+ // Release the dedicated connection back to the pool.
170+ e .releaseDedicated ()
103171
104- // Release the advisory lock.
105- _ , err := e .drv .Exec (ctx , "SELECT RELEASE_LOCK(?)" , advisoryLockName )
106172 return err
107173}
108174
@@ -113,7 +179,7 @@ func (e *Executor) ListApplied(ctx context.Context) ([]*migrate.AppliedMigration
113179 "SELECT `id`, `version`, `name`, `group`, CAST(`migrated_at` AS CHAR) FROM `%s` ORDER BY `id` ASC" ,
114180 migrationTableName )
115181
116- rows , err := e .drv . Query (ctx , query )
182+ rows , err := e .query (ctx , query )
117183 if err != nil {
118184 return nil , err
119185 }
@@ -135,7 +201,7 @@ func (e *Executor) RecordApplied(ctx context.Context, m *migrate.Migration) erro
135201 query := fmt .Sprintf (
136202 "INSERT INTO `%s` (`version`, `name`, `group`) VALUES (?, ?, ?)" ,
137203 migrationTableName )
138- _ , err := e .drv . Exec (ctx , query , m .Version , m .Name , m .Group )
204+ _ , err := e .exec (ctx , query , m .Version , m .Name , m .Group )
139205 return err
140206}
141207
@@ -144,6 +210,6 @@ func (e *Executor) RemoveApplied(ctx context.Context, m *migrate.Migration) erro
144210 query := fmt .Sprintf (
145211 "DELETE FROM `%s` WHERE `version` = ? AND `group` = ?" ,
146212 migrationTableName )
147- _ , err := e .drv . Exec (ctx , query , m .Version , m .Group )
213+ _ , err := e .exec (ctx , query , m .Version , m .Group )
148214 return err
149215}
0 commit comments