4
4
import org .flywaydb .core .api .FlywayException ;
5
5
import org .flywaydb .core .internal .exception .FlywaySqlException ;
6
6
import org .flywaydb .core .internal .jdbc .JdbcTemplate ;
7
- import org .flywaydb .core .internal .strategy .RetryStrategy ;
8
7
import org .flywaydb .core .internal .util .FlywayDbWebsiteLinks ;
8
+ import org .flywaydb .core .internal .util .SqlCallable ;
9
9
10
10
import java .sql .*;
11
+ import java .time .Instant ;
11
12
import java .util .Map ;
13
+ import java .util .Random ;
12
14
import java .util .concurrent .Callable ;
13
15
import java .util .concurrent .ConcurrentHashMap ;
14
16
@@ -18,6 +20,10 @@ public class YugabyteDBExecutionTemplate {
18
20
private final JdbcTemplate jdbcTemplate ;
19
21
private final String tableName ;
20
22
private static final Map <String , Boolean > tableEntries = new ConcurrentHashMap <>();
23
+ private static final Random random = new Random ();
24
+ public static final int DEFAULT_LOCK_ID_TTL = 1000 * 60 * 5 ;
25
+ public static final int MAX_LOCK_ID_TTL = 1000 * 60 * 60 ;
26
+ public static final String LOCK_ID_TTL_SYS_PROP_NAME = "flyway.yugabytedb.lock-id-ttl-ms" ;
21
27
22
28
YugabyteDBExecutionTemplate (JdbcTemplate jdbcTemplate , String tableName ) {
23
29
this .jdbcTemplate = jdbcTemplate ;
@@ -26,8 +32,9 @@ public class YugabyteDBExecutionTemplate {
26
32
27
33
public <T > T execute (Callable <T > callable ) {
28
34
Exception error = null ;
35
+ long lockId = 0 ;
29
36
try {
30
- lock ();
37
+ lockId = lock ();
31
38
return callable .call ();
32
39
} catch (RuntimeException e ) {
33
40
error = e ;
@@ -36,31 +43,36 @@ public <T> T execute(Callable<T> callable) {
36
43
error = e ;
37
44
throw new FlywayException (e );
38
45
} finally {
39
- unlock (error );
46
+ if (lockId != 0 ) {
47
+ unlock (lockId , error );
48
+ }
40
49
}
41
50
}
42
51
43
- private void lock () throws SQLException {
44
- RetryStrategy strategy = new RetryStrategy ();
45
- strategy .doWithRetries (this ::tryLock , "Interrupted while attempting to acquire lock through SELECT ... FOR UPDATE" ,
52
+ private long lock () throws SQLException {
53
+ YBRetryStrategy strategy = new YBRetryStrategy ();
54
+ return strategy .doWithRetries (this ::tryLock , "Interrupted while attempting to acquire lock through SELECT ... FOR UPDATE" ,
46
55
"Number of retries exceeded while attempting to acquire lock through SELECT ... FOR UPDATE. " +
47
56
"Configure the number of retries with the 'lockRetryCount' configuration option: " + FlywayDbWebsiteLinks .LOCK_RETRY_COUNT );
48
57
49
58
}
50
59
51
- private boolean tryLock () {
60
+ private long tryLock () {
52
61
Exception exception = null ;
53
- boolean txStarted = false , success = false ;
62
+ boolean txStarted = false ;
63
+ long lockIdToBeReturned = 0 ;
54
64
Statement statement = null ;
55
65
try {
56
66
statement = jdbcTemplate .getConnection ().createStatement ();
57
67
58
68
if (!tableEntries .containsKey (tableName )) {
59
69
try {
70
+ String now = new Timestamp (Instant .now ().getEpochSecond ()).toString ();
60
71
statement .executeUpdate ("INSERT INTO "
61
72
+ YugabyteDBDatabase .LOCK_TABLE_NAME
62
- + " VALUES ('" + tableName + "', 'false ')" );
73
+ + " VALUES ('" + tableName + "', 0, '" + now + " ')" );
63
74
tableEntries .put (tableName , true );
75
+ LOG .info ("insert query ts: " + now );
64
76
LOG .info (Thread .currentThread ().getName () + "> Inserted a token row for " + tableName + " in " + YugabyteDBDatabase .LOCK_TABLE_NAME );
65
77
} catch (SQLException e ) {
66
78
if ("23505" .equals (e .getSQLState ())) {
@@ -72,38 +84,53 @@ private boolean tryLock() {
72
84
}
73
85
}
74
86
75
- boolean locked ;
76
- String selectForUpdate = "SELECT locked FROM "
87
+ long lockIdRead = 0 ;
88
+ String selectForUpdate = "SELECT lock_id, ts FROM "
77
89
+ YugabyteDBDatabase .LOCK_TABLE_NAME
78
90
+ " WHERE table_name = '"
79
91
+ tableName
80
92
+ "' FOR UPDATE" ;
81
- String updateLocked = "UPDATE " + YugabyteDBDatabase .LOCK_TABLE_NAME
82
- + " SET locked = true WHERE table_name = '"
83
- + tableName + "'" ;
84
93
85
94
statement .execute ("BEGIN" );
86
95
txStarted = true ;
87
96
ResultSet rs = statement .executeQuery (selectForUpdate );
88
97
if (rs .next ()) {
89
- locked = rs .getBoolean ("locked" );
98
+ lockIdRead = rs .getLong ("lock_id" );
99
+ Timestamp tsRead = rs .getTimestamp ("ts" );
100
+ String current = new Timestamp (Instant .now ().getEpochSecond ()).toString ();
101
+ long lockIdTtl = DEFAULT_LOCK_ID_TTL ;
102
+ String sysProp = System .getProperty (LOCK_ID_TTL_SYS_PROP_NAME );
103
+ if (sysProp != null ) {
104
+ try {
105
+ lockIdTtl = Long .parseLong (sysProp );
106
+ lockIdTtl = lockIdTtl < 0 || lockIdTtl > MAX_LOCK_ID_TTL ? DEFAULT_LOCK_ID_TTL : lockIdTtl ;
107
+ } catch (NumberFormatException e ) {
108
+ LOG .warn ("Invalid value for " + LOCK_ID_TTL_SYS_PROP_NAME + ": " + sysProp + ". Using default value: " + DEFAULT_LOCK_ID_TTL + " ms" );
109
+ }
110
+ }
90
111
91
- if (locked ) {
92
- statement .execute ("COMMIT" );
93
- txStarted = false ;
94
- LOG .debug (Thread .currentThread ().getName () + "> Another Flyway operation is in progress. Allowing it to complete" );
112
+ if (lockIdRead == 0 || Instant .now ().getEpochSecond () - tsRead .getTime () > lockIdTtl ) {
113
+ lockIdToBeReturned = random .nextLong ();
114
+ if (lockIdRead == 0 ) {
115
+ LOG .debug (Thread .currentThread ().getName () + "> Setting lock_id = " + lockIdToBeReturned );
116
+ } else {
117
+ LOG .warn (Thread .currentThread ().getName () + "> Lock with lock_id " + lockIdRead + " is held for more than " + lockIdTtl + " millis. Resetting it with lock_id " + lockIdToBeReturned );
118
+ }
119
+ String updateLockId = "UPDATE " + YugabyteDBDatabase .LOCK_TABLE_NAME
120
+ + " SET lock_id = " + lockIdToBeReturned + ", ts = '" + current + "' WHERE table_name = '"
121
+ + tableName + "'" ;
122
+ LOG .debug (Thread .currentThread ().getName () + "> executing query " + updateLockId );
123
+ statement .executeUpdate (updateLockId );
95
124
} else {
96
- LOG .debug (Thread .currentThread ().getName () + "> Setting locked = true" );
97
- statement .executeUpdate (updateLocked );
98
- success = true ;
125
+ LOG .debug (Thread .currentThread ().getName () + "> Another Flyway operation is in progress. Allowing it to complete" );
99
126
}
100
127
} else {
101
128
// For some reason the record was not found, retry
102
129
tableEntries .remove (tableName );
103
130
}
104
131
105
132
} catch (SQLException e ) {
106
- LOG .warn (Thread .currentThread ().getName () + "> Unable to perform lock action, SQLState: " + e .getSQLState ());
133
+ LOG .debug (Thread .currentThread ().getName () + "> Unable to perform lock action, SQLState: " + e .getSQLState ());
107
134
if (!"40001" .equalsIgnoreCase (e .getSQLState ())) {
108
135
exception = new FlywaySqlException ("Unable to perform lock action" , e );
109
136
throw (FlywaySqlException ) exception ;
@@ -112,56 +139,103 @@ private boolean tryLock() {
112
139
if (txStarted ) {
113
140
try {
114
141
statement .execute ("COMMIT" );
115
- LOG .debug (Thread .currentThread ().getName () + "> Completed the tx to set locked = true" );
142
+ // lock_id may not be set if there is exception in select for update
143
+ LOG .debug (Thread .currentThread ().getName () + "> Completed the tx to attempt to set lock_id" );
116
144
} catch (SQLException e ) {
117
145
if (exception == null ) {
118
- throw new FlywaySqlException ("Failed to commit the tx to set locked = true " , e );
146
+ throw new FlywaySqlException ("Failed to commit the tx to set lock_id " , e );
119
147
}
120
- LOG .warn (Thread .currentThread ().getName () + "> Failed to commit the tx to set locked = true : " + e );
148
+ LOG .warn (Thread .currentThread ().getName () + "> Failed to commit the tx to set lock_id : " + e );
121
149
}
122
150
}
123
151
}
124
- return success ;
152
+ return lockIdToBeReturned ;
125
153
}
126
154
127
- private void unlock (Exception rethrow ) {
155
+ private void unlock (long prevLockId , Exception rethrow ) {
128
156
Statement statement = null ;
129
157
try {
130
158
statement = jdbcTemplate .getConnection ().createStatement ();
131
159
statement .execute ("BEGIN" );
132
- ResultSet rs = statement .executeQuery ("SELECT locked FROM " + YugabyteDBDatabase .LOCK_TABLE_NAME + " WHERE table_name = '" + tableName + "' FOR UPDATE" );
160
+ ResultSet rs = statement .executeQuery ("SELECT lock_id FROM " + YugabyteDBDatabase .LOCK_TABLE_NAME + " WHERE table_name = '" + tableName + "' FOR UPDATE" );
133
161
134
162
if (rs .next ()) {
135
- boolean locked = rs .getBoolean ( "locked " );
136
- if (locked ) {
137
- statement .executeUpdate ("UPDATE " + YugabyteDBDatabase .LOCK_TABLE_NAME + " SET locked = false WHERE table_name = '" + tableName + "'" );
163
+ long lockId = rs .getLong ( "lock_id " );
164
+ if (lockId == prevLockId ) {
165
+ statement .executeUpdate ("UPDATE " + YugabyteDBDatabase .LOCK_TABLE_NAME + " SET lock_id = 0 WHERE table_name = '" + tableName + "'" );
138
166
} else {
139
167
// Unexpected. This may happen only when callable took too long to complete
140
168
// and another thread forcefully reset it.
169
+ String msgLock = "Expected and actual lock_id mismatch. Expected: " + prevLockId + ", Actual: " + lockId ;
141
170
String msg = "Unlock failed but the Flyway operation may have succeeded. Check your Flyway operation before re-trying" ;
142
- LOG .warn (Thread .currentThread ().getName () + "> " + msg );
171
+ LOG .warn (Thread .currentThread ().getName () + "> " + msg + " \n " + msgLock );
143
172
if (rethrow == null ) {
144
173
throw new FlywayException (msg );
145
174
}
146
175
}
147
176
}
148
177
} catch (SQLException e ) {
149
178
if (rethrow == null ) {
150
- rethrow = new FlywayException ("Unable to perform unlock action" , e );
179
+ rethrow = new FlywaySqlException ("Unable to perform unlock action for lock_id " + prevLockId , e );
151
180
throw (FlywaySqlException ) rethrow ;
152
181
}
153
- LOG .warn ("Unable to perform unlock action " + e );
182
+ LOG .warn ("Unable to perform unlock action for lock_id " + prevLockId + ": " + e );
154
183
} finally {
155
184
try {
156
185
statement .execute ("COMMIT" );
157
- LOG .debug (Thread .currentThread ().getName () + "> Completed the tx to set locked = false" );
186
+ LOG .debug (Thread .currentThread ().getName () + "> Completed the tx to reset lock_id " + prevLockId );
158
187
} catch (SQLException e ) {
159
188
if (rethrow == null ) {
160
- throw new FlywaySqlException ("Failed to commit unlock action" , e );
189
+ throw new FlywaySqlException ("Failed to commit unlock action for lock_id " + prevLockId , e );
161
190
}
162
- LOG .warn ("Failed to commit unlock action: " + e );
191
+ LOG .warn ("Failed to commit unlock action for lock_id " + prevLockId + " : " + e );
163
192
}
164
193
}
165
194
}
166
195
196
+ public static class YBRetryStrategy {
197
+ private static int numberOfRetries = 50 ;
198
+ private static boolean unlimitedRetries ;
199
+ private int numberOfRetriesRemaining ;
200
+
201
+ public YBRetryStrategy () {
202
+ this .numberOfRetriesRemaining = numberOfRetries ;
203
+ }
204
+
205
+ public static void setNumberOfRetries (int retries ) {
206
+ numberOfRetries = retries ;
207
+ unlimitedRetries = retries < 0 ;
208
+ }
209
+
210
+ private boolean hasMoreRetries () {
211
+ return unlimitedRetries || this .numberOfRetriesRemaining > 0 ;
212
+ }
213
+
214
+ private void nextRetry () {
215
+ if (!unlimitedRetries ) {
216
+ --this .numberOfRetriesRemaining ;
217
+ }
218
+ }
219
+
220
+ private int nextWaitInMilliseconds () {
221
+ return 1000 ;
222
+ }
223
+
224
+ public long doWithRetries (SqlCallable <Long > callable , String interruptionMessage , String retriesExceededMessage ) throws SQLException {
225
+ long id = 0 ;
226
+ while (id == 0 ) {
227
+ id = callable .call ();
228
+ try {
229
+ Thread .sleep (this .nextWaitInMilliseconds ());
230
+ } catch (InterruptedException e ) {
231
+ throw new FlywayException (interruptionMessage , e );
232
+ }
233
+ if (!this .hasMoreRetries ()) {
234
+ throw new FlywayException (retriesExceededMessage );
235
+ }
236
+ this .nextRetry ();
237
+ }
238
+ return id ;
239
+ }
240
+ }
167
241
}
0 commit comments