Skip to content

JDBCQuotaStore.renameLayer only updates LAYER_NAME, leaves TILESET.KEY and TILEPAGE.TILESET_ID stale #1526

@groldan

Description

@groldan

Renaming a layer in a GeoServer + GeoWebCache deployment using the JDBC quota store leaves the quota tables in a half-renamed state: the
LAYER_NAME column reflects the new name, but the KEY column on TILESET and the TILESET_ID column on TILEPAGE still carry the old layer-name prefix. Subsequent quota operations don't find the row by KEY and insert a duplicate, splitting accounting between two rows.

Steps to reproduce

  1. Create a tile layer pmtiles:europe. Seed some tiles so the quota
    store creates TILESET and TILEPAGE rows. The DB looks like:

    SELECT key, layer_name FROM tileset;
                     key                |   layer_name
    ------------------------------------+----------------
     pmtiles:europe#EPSG:4326#image/png | pmtiles:europe
     pmtiles:europe#EPSG:900913#image/png | pmtiles:europe
    
    SELECT DISTINCT tileset_id FROM tilepage;
                 tileset_id
    ------------------------------------
     pmtiles:europe#EPSG:4326#image/png
     pmtiles:europe#EPSG:900913#image/png
    
  2. Rename the layer's workspace from pmtiles to pmtiles_renamed (or rename the layer directly). The prefixed layer name goes from
    pmtiles:europe to pmtiles_renamed:europe. The catalog change fires GWC.layerRenamed("pmtiles:europe", "pmtiles_renamed:europe") -> BlobStoreListener.layerRenamed -> JDBCQuotaStore.renameLayer("pmtiles:europe", "pmtiles_renamed:europe").

  3. Expected DB state after the rename:

    SELECT key, layer_name FROM tileset;
                        key                       |       layer_name
    --------------------------------------------+-------------------------
     pmtiles_renamed:europe#EPSG:4326#image/png  | pmtiles_renamed:europe
     pmtiles_renamed:europe#EPSG:900913#image/png| pmtiles_renamed:europe
    
    SELECT DISTINCT tileset_id FROM tilepage;
                   tileset_id
    --------------------------------------------
     pmtiles_renamed:europe#EPSG:4326#image/png
     pmtiles_renamed:europe#EPSG:900913#image/png
    
  4. Actual DB state after the rename:

    SELECT key, layer_name FROM tileset;
                        key                |       layer_name
    ----------------------------------------+-------------------------
     pmtiles:europe#EPSG:4326#image/png    | pmtiles_renamed:europe   <- KEY not updated
     pmtiles:europe#EPSG:900913#image/png  | pmtiles_renamed:europe   <- KEY not updated
    
    SELECT DISTINCT tileset_id FROM tilepage;
                 tileset_id
    ------------------------------------
     pmtiles:europe#EPSG:4326#image/png   <- still references the old KEY
     pmtiles:europe#EPSG:900913#image/png
    

    LAYER_NAME is updated to the new prefix. TILESET.KEY and TILEPAGE.TILESET_ID still carry the old prefix.

Why it matters

  • getTileSetById(<new-prefix>#<gridset>#<format>) returns null.
  • The next addToQuotaAndTileCounts for the renamed layer goes through getOrCreateTileSet, doesn't find a row by the new KEY, and inserts a duplicate row. New TILEPAGE rows are written under the new key.
    Old rows linger under the stale key and keep accounting bytes against the now-orphaned tileset.
  • Quota lookup by id is split across two rows; quota lookup by layer name still works (it's the column we do update).
  • The existing tests JDBCQuotaStoreTest.testRenameLayer and testRenameLayer2 only check rows via LAYER_NAME (countTileSetsByLayerName), so the bug is invisible to current CI.

Root cause

SQLDialect.getRenameLayerStatement at
geowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/SQLDialect.java:

public String getRenameLayerStatement(String schema, String oldLayerName, String newLayerName) {
    StringBuilder sb = new StringBuilder("UPDATE ");
    if (schema != null) sb.append(schema).append(".");
    sb.append("TILESET SET LAYER_NAME = :").append(newLayerName)
      .append(" WHERE LAYER_NAME = :").append(oldLayerName);
    return sb.toString();
}

KEY is built as <layerName>#<gridsetId>#<blobFormat>[#<parametersId>] and is never touched. The TILEPAGE FK is REFERENCES TILESET(KEY) ON DELETE CASCADE - no ON UPDATE CASCADE, so even adding a KEY update needs the cascade (or a second SQL statement).

Proposed fix

Two changes in SQLDialect plus a per-dialect audit:

  1. Add ON UPDATE CASCADE to the TILEPAGE -> TILESET FK in TABLE_CREATION_MAP. Emit an idempotent ALTER TABLE ... DROP CONSTRAINT ... ADD CONSTRAINT ... ON UPDATE CASCADE ON DELETE CASCADE from JDBCQuotaStore.initialize() so existing installations pick up the cascade on next startup.

  2. Rewrite getRenameLayerStatement to also update KEY:

    sb.append("TILESET SET LAYER_NAME = :").append(newLayerName);
    sb.append(", KEY = :").append(newLayerName)
      .append(" || SUBSTRING(KEY FROM POSITION('#' IN KEY))");
    sb.append(" WHERE LAYER_NAME = :").append(oldLayerName);

    SUBSTRING(KEY FROM POSITION('#' IN KEY)) is standard SQL on Postgres, H2, HSQL. Oracle needs an override using SUBSTR(KEY, INSTR(KEY, '#')).

  3. Extend JDBCQuotaStoreTest.testRenameLayer and testRenameLayer2 to also assert that TILESET.KEY and TILEPAGE.TILESET_ID carry the new prefix.

Affected versions

Present on main and on the 1.28.x series (and probably every release that has the JDBC quota store). Backport target: 1.28.x (consumed by GeoServer 2.28.x).

Out of scope

The wider clustered-deployment concurrency story (SERIALIZABLE isolation + tolerant loops causing CannotSerializeTransactionException) is tracked in

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions