Skip to content

Commit e6d6c84

Browse files
committed
diskquota/jdbc: make TILEPAGE FK migration crash- and race-safe
migrateForeignKeys dropped and re-added the TILEPAGE -> TILESET FK in one try block, finding a legacy FK. DDL is non-transactional on Oracle, hence a crash between the committed DROP and ADD left no FK, and the next startup silently skipped the table. Split the drop and add into separate committed steps and converge on the migrated FK from whatever the scan finds (already migrated, legacy present, or absent). On a failed step, poll the live FK until a peer's migration completes instead of giving up on the first recheck, which recovers an interrupted migration and tolerates concurrent peers (Oracle's NOWAIT ALTER can fail in the middle of a peer migration with ORA-00054). Move the orchestration out of SQLDialect to a TilepageForeignKeyMigrator, and add an interrupted-migration test on HSQL, PostgreSQL and Oracle. on-behalf-of: @camptocamp <info@camptocamp.com>
1 parent 3b11236 commit e6d6c84

3 files changed

Lines changed: 310 additions & 112 deletions

File tree

geowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/SQLDialect.java

Lines changed: 10 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,7 @@
1919
import java.util.LinkedHashMap;
2020
import java.util.List;
2121
import java.util.Map;
22-
import java.util.Objects;
23-
import java.util.logging.Level;
24-
import java.util.logging.Logger;
2522
import javax.sql.DataSource;
26-
import org.geotools.util.logging.Logging;
27-
import org.springframework.dao.DataAccessException;
28-
import org.springframework.jdbc.core.JdbcOperations;
2923
import org.springframework.jdbc.support.JdbcAccessor;
3024
import org.springframework.jdbc.support.JdbcUtils;
3125
import org.springframework.jdbc.support.MetaDataAccessException;
@@ -38,8 +32,6 @@
3832
*/
3933
public class SQLDialect {
4034

41-
private static final Logger LOG = Logging.getLogger(SQLDialect.class);
42-
4335
// size guesses: 128 characters should be more than enough for layer name, the gridset id
4436
// is normally an epsg code so 32 is way more than enough, the blob format
4537
// is normally a mime plus some extras, again 64 should fit, a param id is
@@ -124,85 +116,28 @@ public void initializeTables(String schema, SimpleJdbcTemplate template) {
124116
}
125117
}
126118
}
127-
// Bring pre-existing installations up to the current FK contract (ON UPDATE CASCADE).
128-
// No-op for fresh schemas created above; no-op for dialects that cannot support it.
119+
// Bring pre-existing installations up to the current per-dialect FK definition (ON UPDATE CASCADE, or
120+
// DEFERRABLE INITIALLY DEFERRED on Oracle). No-op for the fresh schemas created above.
129121
migrateForeignKeys(schema, template);
130122
}
131123

132124
/**
133-
* Upgrades the {@code TILEPAGE -> TILESET} foreign key on installations created before this dialect started
134-
* declaring it {@code ON UPDATE CASCADE}, so that {@link #getRenameLayerStatement(String, String, String) renaming
135-
* a layer} can rewrite {@code TILESET.KEY} without orphaning the dependent {@code TILEPAGE} rows.
136-
*
137-
* <p>Idempotent: looks up the existing FK via {@link DatabaseMetaData#getImportedKeys}, and only rewrites it when
138-
* the current update rule is not {@link DatabaseMetaData#importedKeyCascade}. Dialects that cannot support
139-
* {@code ON UPDATE CASCADE} (notably Oracle) override this method to no-op.
125+
* Upgrades the {@code TILEPAGE -> TILESET} foreign key on installations created before this dialect declared its
126+
* current FK definition, letting {@link #getRenameLayerStatement renaming a layer} rewrite {@code TILESET.KEY}
127+
* without orphaning the dependent {@code TILEPAGE} rows. The dialect contributes the two variation points
128+
* ({@link #tilepageFkIsMigrated} and {@link #tilepageFkAddSql}); the crash- and concurrency-safe orchestration
129+
* lives in {@link TilepageForeignKeyMigrator}.
140130
*/
141131
protected void migrateForeignKeys(String schema, SimpleJdbcTemplate template) {
142-
DataSource ds = Objects.requireNonNull(((JdbcAccessor) template.getJdbcOperations()).getDataSource());
143-
try {
144-
JdbcUtils.extractDatabaseMetaData(ds, dbmd -> upgradeTilepageForeignKey(dbmd, schema, template));
145-
} catch (MetaDataAccessException e) {
146-
LOG.log(
147-
Level.WARNING,
148-
"Could not migrate TILEPAGE foreign key to ON UPDATE CASCADE; layer renames may leave stale rows",
149-
e);
150-
}
151-
}
152-
153-
/**
154-
* {@link DatabaseMetaDataCallback} body for {@link #migrateForeignKeys}: scans the FKs declared on TILEPAGE and,
155-
* for any TILEPAGE -> TILESET FK whose update rule is not {@link DatabaseMetaData#importedKeyCascade}, drops it and
156-
* re-adds it with {@code ON UPDATE CASCADE ON DELETE CASCADE}. Idempotent: a FK that already cascades on update is
157-
* left untouched.
158-
*
159-
* <p>Concurrent-startup safe: if another instance races us to the upgrade, our drop/add may throw because the
160-
* legacy constraint name no longer exists. In that case we re-check the live FK state and, if the cascade form is
161-
* already in place, treat it as a concurrent success rather than a failure.
162-
*/
163-
private Void upgradeTilepageForeignKey(DatabaseMetaData dbmd, String schema, SimpleJdbcTemplate template)
164-
throws SQLException {
165-
166-
final String tilepageName = resolveTableName(dbmd, schema, "TILEPAGE");
167-
if (tilepageName == null) {
168-
return null;
169-
}
170-
final String prefix = schema == null ? "" : schema + ".";
171-
final String prefixedTilepageName = prefix + tilepageName;
172-
try (ResultSet rs = dbmd.getImportedKeys(null, schema, tilepageName)) {
173-
while (rs.next()) {
174-
String fkName = rs.getString("FK_NAME");
175-
if (!isTilepageTilesetFkRow(rs, fkName) || tilepageFkIsMigrated(rs)) {
176-
continue;
177-
}
178-
String drop = "ALTER TABLE %s DROP CONSTRAINT %s".formatted(prefixedTilepageName, fkName);
179-
String add = tilepageFkAddSql(prefixedTilepageName, prefix);
180-
181-
LOG.info(() -> "Migrating TILEPAGE.TILESET_ID foreign key (was constraint %s)".formatted(fkName));
182-
JdbcOperations jdbcOperations = template.getJdbcOperations();
183-
try {
184-
jdbcOperations.execute(drop);
185-
jdbcOperations.execute(add);
186-
} catch (DataAccessException raceLikely) {
187-
if (isTilepageFkAlreadyMigrated(dbmd, schema, tilepageName)) {
188-
LOG.fine(() -> "TILEPAGE FK was migrated concurrently by another instance "
189-
+ "while this instance was trying to drop %s; accepting concurrent migration"
190-
.formatted(fkName));
191-
return null;
192-
}
193-
throw raceLikely;
194-
}
195-
}
196-
}
197-
return null;
132+
new TilepageForeignKeyMigrator(this::tilepageFkIsMigrated, this::tilepageFkAddSql).migrate(schema, template);
198133
}
199134

200-
/** Hook: {@code true} when this dialect's FK row already reflects the migrated shape. Oracle overrides. */
135+
/** Hook: {@code true} when this dialect's FK row is already migrated. Oracle overrides. */
201136
protected boolean tilepageFkIsMigrated(ResultSet rs) throws SQLException {
202137
return rs.getShort("UPDATE_RULE") == DatabaseMetaData.importedKeyCascade;
203138
}
204139

205-
/** Hook: the {@code ALTER TABLE ... ADD FOREIGN KEY} statement for this dialect's migrated FK shape. */
140+
/** Hook: the {@code ALTER TABLE ... ADD FOREIGN KEY} statement for this dialect's migrated FK. */
206141
protected String tilepageFkAddSql(String prefixedTilepageName, String prefix) {
207142
return """
208143
ALTER TABLE %s ADD FOREIGN KEY (TILESET_ID)
@@ -212,43 +147,6 @@ protected String tilepageFkAddSql(String prefixedTilepageName, String prefix) {
212147
.formatted(prefixedTilepageName, prefix);
213148
}
214149

215-
/** Re-checks FK state after a failed migration attempt to detect a concurrent migration from another instance. */
216-
private boolean isTilepageFkAlreadyMigrated(DatabaseMetaData dbmd, String schema, String tilepageName)
217-
throws SQLException {
218-
try (ResultSet rs = dbmd.getImportedKeys(null, schema, tilepageName)) {
219-
while (rs.next()) {
220-
if (isTilepageTilesetFkRow(rs, rs.getString("FK_NAME")) && tilepageFkIsMigrated(rs)) {
221-
return true;
222-
}
223-
}
224-
}
225-
return false;
226-
}
227-
228-
/** Identity-only check: is this metadata row the TILEPAGE -> TILESET(KEY) FK? */
229-
private static boolean isTilepageTilesetFkRow(ResultSet rs, String fkName) throws SQLException {
230-
if (fkName == null || fkName.isEmpty()) {
231-
return false;
232-
}
233-
String pkTable = rs.getString("PKTABLE_NAME");
234-
String fkColumn = rs.getString("FKCOLUMN_NAME");
235-
return "TILESET".equalsIgnoreCase(pkTable) && "TILESET_ID".equalsIgnoreCase(fkColumn);
236-
}
237-
238-
private static String resolveTableName(DatabaseMetaData dbmd, String schema, String tableName) throws SQLException {
239-
try (ResultSet rs = dbmd.getTables(null, schema, tableName.toLowerCase(), null)) {
240-
if (rs.next()) {
241-
return rs.getString("TABLE_NAME");
242-
}
243-
}
244-
try (ResultSet rs = dbmd.getTables(null, schema, tableName, null)) {
245-
if (rs.next()) {
246-
return rs.getString("TABLE_NAME");
247-
}
248-
}
249-
return null;
250-
}
251-
252150
/** Checks if the specified table exists */
253151
private boolean tableExists(SimpleJdbcTemplate template, final String schema, final String tableName) {
254152
try {

0 commit comments

Comments
 (0)