Skip to content

Commit 38aeac2

Browse files
committed
Add a fastMode for the common case when there are no new migrations to run
1 parent ab5e52f commit 38aeac2

File tree

8 files changed

+158
-30
lines changed

8 files changed

+158
-30
lines changed

ebean-migration/src/main/java/io/ebean/migration/MigrationConfig.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class MigrationConfig {
6666
private String platform;
6767
private Properties properties;
6868
private boolean earlyChecksumMode;
69+
private boolean fastMode;
6970

7071
/**
7172
* Return the name of the migration table.
@@ -468,6 +469,7 @@ public void load(Properties props) {
468469
dbPassword = property("password", dbPassword);
469470
dbUrl = property("url", dbUrl);
470471
dbSchema = property("schema", dbSchema);
472+
fastMode = property("fastMode", fastMode);
471473
skipMigrationRun = property("skipMigrationRun", skipMigrationRun);
472474
skipChecksum = property("skipChecksum", skipChecksum);
473475
earlyChecksumMode = property("earlyChecksumMode", earlyChecksumMode);
@@ -592,6 +594,22 @@ public void setEarlyChecksumMode(boolean earlyChecksumMode) {
592594
this.earlyChecksumMode = earlyChecksumMode;
593595
}
594596

597+
/**
598+
* Return true if fastMode is turned on.
599+
*/
600+
public boolean isFastMode() {
601+
return fastMode;
602+
}
603+
604+
/**
605+
* Set true to enable fastMode. This will perform an initial check for the exact same number
606+
* of migrations and matching checksums without any locking. If anything does not match
607+
* then the normal migration is performed with appropriate locking etc.
608+
*/
609+
public void setFastMode(boolean fastMode) {
610+
this.fastMode = fastMode;
611+
}
612+
595613
/**
596614
* Default factory. Uses the migration's class loader and injects the config if necessary.
597615
*

ebean-migration/src/main/java/io/ebean/migration/runner/MigrationEngine.java

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import java.sql.SQLException;
1010
import java.util.Collections;
1111
import java.util.List;
12+
import java.util.Map;
13+
import java.util.stream.Collectors;
1214

1315
import static java.lang.System.Logger.Level.*;
1416
import static java.lang.System.Logger.Level.WARNING;
@@ -22,13 +24,17 @@ public class MigrationEngine {
2224

2325
private final MigrationConfig migrationConfig;
2426
private final boolean checkStateOnly;
27+
private final boolean fastMode;
28+
private int fastModeCount;
29+
private MigrationTable table;
2530

2631
/**
2732
* Create with the MigrationConfig.
2833
*/
2934
public MigrationEngine(MigrationConfig migrationConfig, boolean checkStateOnly) {
3035
this.migrationConfig = migrationConfig;
3136
this.checkStateOnly = checkStateOnly;
37+
this.fastMode = !checkStateOnly && migrationConfig.isFastMode();
3238
}
3339

3440
/**
@@ -41,11 +47,18 @@ public List<MigrationResource> run(Connection connection) {
4147
log.log(DEBUG, "no migrations to check");
4248
return Collections.emptyList();
4349
}
44-
4550
long startMs = System.currentTimeMillis();
46-
MigrationTable table = initialiseMigrationTable(connection);
51+
setAutoCommitFalse(connection);
52+
table = initMigrationTable(connection);
53+
if (fastMode && fastModeCheck(resources.versions())) {
54+
long checkMs = System.currentTimeMillis() - startMs;
55+
log.log(INFO, "DB migrations completed in {0}ms - totalMigrations:{1}", checkMs, fastModeCount);
56+
return Collections.emptyList();
57+
}
58+
59+
initialiseMigrationTable(connection);
4760
try {
48-
List<MigrationResource> result = runMigrations(resources.versions(), table, checkStateOnly);
61+
List<MigrationResource> result = runMigrations(resources.versions());
4962
connection.commit();
5063
if (!checkStateOnly) {
5164
long commitMs = System.currentTimeMillis();
@@ -71,15 +84,65 @@ public List<MigrationResource> run(Connection connection) {
7184
}
7285
}
7386

74-
private MigrationTable initialiseMigrationTable(Connection connection) {
87+
private static void setAutoCommitFalse(Connection connection) {
7588
try {
7689
connection.setAutoCommit(false);
77-
MigrationPlatform platform = derivePlatformName(migrationConfig, connection);
78-
new MigrationSchema(migrationConfig, connection).createAndSetIfNeeded();
90+
} catch (SQLException e) {
91+
throw new MigrationException("Error running DB migrations", e);
92+
}
93+
}
94+
95+
private MigrationTable initMigrationTable(Connection connection) {
96+
final MigrationPlatform platform = derivePlatformName(migrationConfig, connection);
97+
return new MigrationTable(migrationConfig, connection, checkStateOnly, platform);
98+
}
99+
100+
private boolean fastModeCheck(List<LocalMigrationResource> versions) {
101+
try {
102+
final List<MigrationMetaRow> rows = table.fastRead();
103+
if (rows.size() != versions.size() + 1) {
104+
// difference in count of migrations
105+
return false;
106+
}
107+
final Map<String, Integer> dbChecksums = dbChecksumMap(rows);
108+
for (LocalMigrationResource local : versions) {
109+
Integer dbChecksum = dbChecksums.get(local.key());
110+
if (dbChecksum == null) {
111+
// no match, unexpected missing migration
112+
return false;
113+
}
114+
int localChecksum = checksumFor(local);
115+
if (localChecksum != dbChecksum) {
116+
// no match, perhaps repeatable migration change
117+
return false;
118+
}
119+
}
120+
// successful fast check
121+
fastModeCount = versions.size();
122+
return true;
123+
} catch (SQLException e) {
124+
// probably migration table does not exist
125+
return false;
126+
}
127+
}
79128

80-
final MigrationTable table = new MigrationTable(migrationConfig, connection, checkStateOnly, platform);
129+
private static Map<String, Integer> dbChecksumMap(List<MigrationMetaRow> rows) {
130+
return rows.stream().collect(Collectors.toMap(MigrationMetaRow::version, MigrationMetaRow::checksum));
131+
}
132+
133+
private int checksumFor(LocalMigrationResource local) {
134+
if (local instanceof LocalUriMigrationResource) {
135+
return ((LocalUriMigrationResource)local).checksum();
136+
} else if (local instanceof LocalDdlMigrationResource) {
137+
return Checksum.calculate(local.content());
138+
} else {
139+
return ((LocalJdbcMigrationResource) local).checksum();
140+
}
141+
}
142+
143+
private void initialiseMigrationTable(Connection connection) {
144+
try {
81145
table.createIfNeededAndLock();
82-
return table;
83146
} catch (Throwable e) {
84147
rollback(connection);
85148
throw new MigrationException("Error initialising db migrations table", e);
@@ -89,13 +152,13 @@ private MigrationTable initialiseMigrationTable(Connection connection) {
89152
/**
90153
* Run all the migrations as needed.
91154
*/
92-
private List<MigrationResource> runMigrations(List<LocalMigrationResource> localVersions, MigrationTable table, boolean checkStateMode) throws SQLException {
155+
private List<MigrationResource> runMigrations(List<LocalMigrationResource> localVersions) throws SQLException {
93156
// get the migrations in version order
94157
if (table.isEmpty()) {
95158
LocalMigrationResource initVersion = lastInitVersion();
96159
if (initVersion != null) {
97160
// run using a dbinit script
98-
log.log(INFO, "dbinit migration version:{0} local migrations:{1} checkState:{2}", initVersion, localVersions.size(), checkStateMode);
161+
log.log(INFO, "dbinit migration version:{0} local migrations:{1} checkState:{2}", initVersion, localVersions.size(), checkStateOnly);
99162
return table.runInit(initVersion, localVersions);
100163
}
101164
}

ebean-migration/src/main/java/io/ebean/migration/runner/MigrationMetaRow.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
@SuppressWarnings("SqlSourceToSinkFlow")
1313
final class MigrationMetaRow {
1414

15-
private final int id;
16-
private final String type;
15+
private int id;
16+
private String type;
1717
private final String version;
1818
private String comment;
1919
private int checksum;
@@ -45,6 +45,17 @@ final class MigrationMetaRow {
4545
checksum = row.getInt(4);
4646
}
4747

48+
private MigrationMetaRow(int checksum, String version) {
49+
this.checksum = checksum;
50+
this.version = version;
51+
}
52+
53+
static MigrationMetaRow fastRead(ResultSet row) throws SQLException {
54+
final var checksum = row.getInt(1);
55+
final var version = row.getString(2);
56+
return new MigrationMetaRow(checksum, version);
57+
}
58+
4859
@Override
4960
public String toString() {
5061
return "id:" + id + " type:" + type + " checksum:" + checksum + " version:" + version;

ebean-migration/src/main/java/io/ebean/migration/runner/MigrationPlatform.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class MigrationPlatform {
1919

2020
private static final String BASE_SELECT_ID = "select id from ";
2121
private static final String BASE_SELECT = "select id, mtype, mversion, mchecksum from ";
22+
private static final String SELECT_FAST_READ = "select mchecksum, mversion from ";
2223

2324
/**
2425
* Standard row locking for db migration table.
@@ -66,9 +67,8 @@ private static void backoff(int attempt) {
6667

6768
private int lockRows(String sqlTable, Connection connection) throws SQLException {
6869
int rowCount = 0;
69-
final String selectSql = sqlSelectForUpdate(sqlTable);
7070
try (Statement query = connection.createStatement()) {
71-
try (ResultSet resultSet = query.executeQuery(selectSql)) {
71+
try (ResultSet resultSet = query.executeQuery(sqlSelectForUpdate(sqlTable))) {
7272
while (resultSet.next()) {
7373
resultSet.getInt(1);
7474
rowCount++;
@@ -78,6 +78,20 @@ private int lockRows(String sqlTable, Connection connection) throws SQLException
7878
return rowCount;
7979
}
8080

81+
List<MigrationMetaRow> fastReadMigrations(String sqlTable, Connection connection) throws SQLException {
82+
List<MigrationMetaRow> rows = new ArrayList<>();
83+
try (Statement query = connection.createStatement()) {
84+
try (ResultSet resultSet = query.executeQuery(sqlSelectForFastRead(sqlTable))) {
85+
while (resultSet.next()) {
86+
rows.add(MigrationMetaRow.fastRead(resultSet));
87+
}
88+
}
89+
} finally {
90+
connection.rollback();
91+
}
92+
return rows;
93+
}
94+
8195
/**
8296
* Read the existing migrations from the db migration table.
8397
*/
@@ -108,6 +122,10 @@ String sqlSelectForReading(String table) {
108122
return BASE_SELECT + table + forUpdateSuffix;
109123
}
110124

125+
String sqlSelectForFastRead(String table) {
126+
return SELECT_FAST_READ + table;
127+
}
128+
111129
static final class LogicalLock extends MigrationPlatform {
112130

113131
@Override

ebean-migration/src/main/java/io/ebean/migration/runner/MigrationSchema.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ final class MigrationSchema {
2626
/**
2727
* Construct with configuration and connection.
2828
*/
29-
MigrationSchema(MigrationConfig migrationConfig, Connection connection) {
29+
private MigrationSchema(MigrationConfig migrationConfig, Connection connection) {
3030
this.dbSchema = trim(migrationConfig.getDbSchema());
3131
this.createSchemaIfNotExists = migrationConfig.isCreateSchemaIfNotExists();
3232
this.setCurrentSchema = migrationConfig.isSetCurrentSchema();
3333
this.connection = connection;
3434
}
3535

36+
static void createIfNeeded(MigrationConfig config, Connection connection) throws SQLException {
37+
new MigrationSchema(config, connection).createAndSetIfNeeded();
38+
}
39+
3640
private String trim(String dbSchema) {
3741
return (dbSchema == null) ? null : dbSchema.trim();
3842
}
@@ -52,6 +56,7 @@ void createAndSetIfNeeded() throws SQLException {
5256
}
5357
}
5458

59+
@SuppressWarnings("SqlDialectInspection")
5560
private void createSchemaIfNeeded() throws SQLException {
5661
if (!schemaExists()) {
5762
log.log(INFO, "Creating schema: {0}", dbSchema);

ebean-migration/src/main/java/io/ebean/migration/runner/MigrationTable.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ final class MigrationTable {
2525
private static final int EARLY_MODE_CHECKSUM = 1;
2626
private static final int AUTO_PATCH_CHECKSUM = -1;
2727

28+
private final MigrationConfig config;
2829
private final Connection connection;
2930
private final boolean checkStateOnly;
3031
private boolean earlyChecksumMode;
@@ -71,11 +72,13 @@ final class MigrationTable {
7172
private int executionCount;
7273
private boolean patchLegacyChecksums;
7374
private MigrationMetaRow initMetaRow;
75+
private boolean tableKnownToExist;
7476

7577
/**
7678
* Construct with server, configuration and jdbc connection (DB admin user).
7779
*/
7880
MigrationTable(MigrationConfig config, Connection connection, boolean checkStateOnly, MigrationPlatform platform) {
81+
this.config = config;
7982
this.platform = platform;
8083
this.connection = connection;
8184
this.scriptRunner = new MigrationScriptRunner(connection, platform);
@@ -146,25 +149,28 @@ private ScriptTransform createScriptTransform(MigrationConfig config) {
146149
* </p>
147150
*/
148151
void createIfNeededAndLock() throws SQLException, IOException {
149-
SQLException sqlEx = null;
150-
if (!tableExists()) {
151-
try {
152-
createTable();
153-
} catch (SQLException e) {
154-
if (tableExists()) {
155-
sqlEx = e;
156-
log.log(INFO, "Ignoring error during table creation, as an other process may have created the table", e);
157-
} else {
158-
throw e;
152+
SQLException suppressedException = null;
153+
if (!tableKnownToExist) {
154+
MigrationSchema.createIfNeeded(config, connection);
155+
if (!tableExists()) {
156+
try {
157+
createTable();
158+
} catch (SQLException e) {
159+
if (tableExists()) {
160+
suppressedException = e;
161+
log.log(INFO, "Ignoring error during table creation, as an other process may have created the table", e);
162+
} else {
163+
throw e;
164+
}
159165
}
160166
}
161167
}
162168
try {
163169
obtainLockWithWait();
164170
} catch (RuntimeException re) {
165171
// catch "failed to obtain row locks"
166-
if (sqlEx != null) {
167-
re.addSuppressed(sqlEx);
172+
if (suppressedException != null) {
173+
re.addSuppressed(suppressedException);
168174
}
169175
throw re;
170176
}
@@ -187,6 +193,13 @@ void unlockMigrationTable() {
187193
platform.unlockMigrationTable(sqlTable, connection);
188194
}
189195

196+
List<MigrationMetaRow> fastRead() throws SQLException {
197+
final var result = platform.fastReadMigrations(sqlTable, connection);
198+
// if we know the migration table exists we can skip those checks
199+
tableKnownToExist = !result.isEmpty();
200+
return result;
201+
}
202+
190203
/**
191204
* Read the migration table with details on what migrations have run.
192205
* This must execute after we have completed the wait for the lock on

ebean-migration/src/test/java/io/ebean/migration/runner/MigrationEarlyModeTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ void testEarlyMode() {
4747
config.setDbPassword(pw);
4848
config.setMigrationPath("dbmig_postgres_early");
4949
config.setRunPlaceholderMap(Map.of("my_table_name", "my_table"));
50+
config.setFastMode(true);
5051

5152
// legacy mode
5253
new MigrationRunner(config).run(dataSource);

ebean-migration/src/test/java/io/ebean/migration/runner/MigrationSchemaTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ void testCreateAndSetIfNeeded() throws Exception {
1616

1717
Connection connection = config.createConnection();
1818

19-
MigrationSchema migrationSchema = new MigrationSchema(config, connection);
20-
migrationSchema.createAndSetIfNeeded();
19+
MigrationSchema.createIfNeeded(config, connection);
2120
}
2221

2322
private MigrationConfig createMigrationConfig() {
@@ -28,4 +27,4 @@ private MigrationConfig createMigrationConfig() {
2827
config.setDbUrl("jdbc:h2:mem:db1");
2928
return config;
3029
}
31-
}
30+
}

0 commit comments

Comments
 (0)