Skip to content

Commit 94d8a0f

Browse files
groldanaaime
authored andcommitted
diskquota/jdbc: Testcontainers-backed integration tests + CI workflow
Add failsafe-driven integration tests that run the JDBCQuotaStoreTest suite against real PostgreSQL and Oracle XE via Testcontainers. Add a GitHub Actions workflow that runs them on PRs touching the diskquota module. The legacy fixture-based tests stay in place and keep skipping cleanly when no fixture file is present. The Oracle IT is @ignored for now: SERIALIZABLE transactions in JDBCQuotaStore hit ORA-08176 on freshly-indexed tables, which the Oracle-recommended retry-on-serialization-failure pattern resolves; the IT could be re-checked and enabled once retry is implemented. No production code is modified. on-behalf-of: @camptocamp <info@camptocamp.com>
1 parent 06ad555 commit 94d8a0f

7 files changed

Lines changed: 492 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: DiskQuota JDBC Integration
2+
3+
on:
4+
push:
5+
tags:
6+
- "**"
7+
pull_request:
8+
paths:
9+
- ".github/workflows/diskquota-jdbc-integration.yml"
10+
- "pom.xml"
11+
- "geowebcache/pom.xml"
12+
- "geowebcache/core/**"
13+
- "geowebcache/diskquota/**"
14+
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
testcontainers:
21+
name: DiskQuota JDBC Testcontainers (Postgres + Oracle XE)
22+
runs-on: ubuntu-latest
23+
strategy:
24+
matrix:
25+
java-version: [ 17, 21 ]
26+
steps:
27+
- uses: actions/checkout@v6
28+
- uses: actions/setup-java@v5
29+
with:
30+
distribution: 'temurin'
31+
java-version: ${{ matrix.java-version }}
32+
cache: 'maven'
33+
34+
- name: Tests against PostgreSQL and Oracle XE TestContainers
35+
run: |
36+
mvn verify -f geowebcache/pom.xml -pl :gwc-diskquota-jdbc -am \
37+
-Ponline \
38+
-DskipTests=true \
39+
-DskipITs=false -B -ntp
40+
41+
- name: Remove SNAPSHOT jars from repository
42+
run: |
43+
find .m2/repository -name "*SNAPSHOT*" -type d | xargs rm -rf {}

geowebcache/diskquota/jdbc/pom.xml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,30 @@
5858
<artifactId>mockito-core</artifactId>
5959
<scope>test</scope>
6060
</dependency>
61+
<!-- testcontainers for integration tests against real DB engines; see *IT classes under src/test -->
62+
<dependency>
63+
<groupId>org.testcontainers</groupId>
64+
<artifactId>testcontainers</artifactId>
65+
<scope>test</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>org.testcontainers</groupId>
69+
<artifactId>postgresql</artifactId>
70+
<scope>test</scope>
71+
</dependency>
72+
<dependency>
73+
<groupId>org.testcontainers</groupId>
74+
<artifactId>oracle-xe</artifactId>
75+
<scope>test</scope>
76+
</dependency>
77+
<!-- Modern Oracle JDBC driver (the legacy ojdbc14 in the 'oracle' profile only loads when -Doracle is set
78+
and is for Oracle 10g; the testcontainers IT uses the current driver against gvenzl/oracle-xe). -->
79+
<dependency>
80+
<groupId>com.oracle.database.jdbc</groupId>
81+
<artifactId>ojdbc11</artifactId>
82+
<version>23.4.0.24.05</version>
83+
<scope>test</scope>
84+
</dependency>
6185
</dependencies>
6286

6387
<profiles>
@@ -76,5 +100,45 @@
76100
</dependency>
77101
</dependencies>
78102
</profile>
103+
<profile>
104+
<!-- activates failsafe for the testcontainers-backed *IT tests under
105+
src/test/java/org/geowebcache/diskquota/jdbc/tests/container/ -->
106+
<id>online</id>
107+
<activation>
108+
<activeByDefault>false</activeByDefault>
109+
</activation>
110+
<build>
111+
<plugins>
112+
<plugin>
113+
<artifactId>maven-failsafe-plugin</artifactId>
114+
<configuration>
115+
<forkCount>1</forkCount>
116+
<reuseForks>false</reuseForks>
117+
</configuration>
118+
</plugin>
119+
</plugins>
120+
</build>
121+
</profile>
122+
<profile>
123+
<!-- skips the testcontainers ITs (used by CI builds without Docker) -->
124+
<id>excludeDockerTests</id>
125+
<activation>
126+
<activeByDefault>false</activeByDefault>
127+
</activation>
128+
<build>
129+
<plugins>
130+
<plugin>
131+
<artifactId>maven-failsafe-plugin</artifactId>
132+
<configuration>
133+
<forkCount>1</forkCount>
134+
<reuseForks>false</reuseForks>
135+
<excludes>
136+
<exclude>org.geowebcache.diskquota.jdbc.tests.container.*IT</exclude>
137+
</excludes>
138+
</configuration>
139+
</plugin>
140+
</plugins>
141+
</build>
142+
</profile>
79143
</profiles>
80144
</project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
3+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
4+
* later version.
5+
*
6+
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
7+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8+
*
9+
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
10+
* <http://www.gnu.org/licenses/>.
11+
*
12+
* <p>Copyright 2026
13+
*/
14+
package org.geowebcache.diskquota.jdbc.tests.container;
15+
16+
import java.util.Properties;
17+
import org.geowebcache.diskquota.jdbc.JDBCQuotaStoreTest;
18+
import org.testcontainers.containers.JdbcDatabaseContainer;
19+
20+
/**
21+
* Abstract base for {@link JDBCQuotaStoreTest} subclasses that source their JDBC connection from a Testcontainers
22+
* {@link JdbcDatabaseContainer} instead of the legacy {@code ~/.geowebcache/<fixture>.properties} file.
23+
*
24+
* <p>The whole {@link JDBCQuotaStoreTest} suite is reused as-is: the only deviation is that this base overrides the
25+
* inner fixture rule to short-circuit the fixture-file lookup with {@link #containerFixture()}, which derives the
26+
* connection {@link Properties} from the running container provided by {@link #getContainer()}.
27+
*
28+
* <p>Subclasses are picked up by Failsafe (their names end in {@code IT}). Surefire ignores them, so the existing
29+
* fixture-based subclasses keep their {@code Test}-suffix convention.
30+
*/
31+
public abstract class AbstractJDBCQuotaStoreIT extends JDBCQuotaStoreTest {
32+
33+
/**
34+
* @return the started {@link JdbcDatabaseContainer} the IT runs against. Subclasses typically expose it as a
35+
* {@code @ClassRule} so it spins up once per test class.
36+
*/
37+
protected abstract JdbcDatabaseContainer<?> getContainer();
38+
39+
/**
40+
* Builds the {@link Properties} the base {@code JDBCFixtureRule} expects. Sourced from the container so that no
41+
* filesystem fixture is needed.
42+
*/
43+
@SuppressWarnings("PMD.CloseResource")
44+
protected Properties containerFixture() {
45+
JdbcDatabaseContainer<?> container = getContainer();
46+
Properties fixture = new Properties();
47+
fixture.put("driver", container.getDriverClassName());
48+
fixture.put("url", container.getJdbcUrl());
49+
fixture.put("username", container.getUsername());
50+
fixture.put("password", container.getPassword());
51+
return fixture;
52+
}
53+
54+
@Override
55+
protected JDBCFixtureRule makeFixtureRule() {
56+
return new JDBCFixtureRule(getFixtureId()) {
57+
@Override
58+
protected Properties createOfflineFixture() {
59+
return containerFixture();
60+
}
61+
};
62+
}
63+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
3+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
4+
* later version.
5+
*
6+
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
7+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8+
*
9+
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
10+
* <http://www.gnu.org/licenses/>.
11+
*
12+
* <p>Copyright 2026
13+
*/
14+
package org.geowebcache.diskquota.jdbc.tests.container;
15+
16+
import java.sql.Connection;
17+
import java.sql.SQLException;
18+
import java.sql.Statement;
19+
import javax.sql.DataSource;
20+
import org.geowebcache.diskquota.jdbc.OracleDialect;
21+
import org.geowebcache.diskquota.jdbc.SQLDialect;
22+
import org.geowebcache.testcontainers.jdbc.OracleXEContainer;
23+
import org.junit.ClassRule;
24+
import org.junit.Ignore;
25+
import org.testcontainers.containers.JdbcDatabaseContainer;
26+
27+
/**
28+
* Runs the full {@code JDBCQuotaStoreTest} suite against a real Oracle Express Edition via Testcontainers.
29+
*
30+
* <p>If Docker is unavailable the class is skipped cleanly through {@link OracleXEContainer#disabledWithoutDocker()}.
31+
*
32+
* <h2>This is {@link Ignore @Ignore}d for now</h2>
33+
*
34+
* <p>Running the suite against Oracle XE 21c surfaces a longstanding gap, not specific to this CI plumbing: any
35+
* SERIALIZABLE transaction in {@code JDBCQuotaStore} whose first read goes through TILEPAGE (or, less often, TILESET)
36+
* fails with <a href="https://docs.oracle.com/error-help/db/ora-08176/">{@code ORA-08176: consistent read failure;
37+
* rollback data not available}</a>. Oracle's own diagnostic for this error names the cause: <em>"Encountered data
38+
* changed by an operation that does not generate rollback data: create index, direct load or discrete
39+
* transaction."</em> The quota store creates four indexes on TILEPAGE at startup, and Oracle XE's snapshot machinery
40+
* cannot reconstruct a consistent read across that recent DDL within a SERIALIZABLE transaction.
41+
*
42+
* <p>The Oracle-recommended remedy is to retry the transaction so a fresh snapshot SCN is taken. Once
43+
* {@code JDBCQuotaStore} wraps each {@code tt.execute(...)} with bounded retry on serialization failures, ORA-08176
44+
* will succeed on a re-attempt - exactly the pattern Oracle's diagnostics recommend.
45+
*
46+
* <p>Concretely, what we observed running this IT against {@code gvenzl/oracle-xe:21-slim-faststart}:
47+
*
48+
* <ul>
49+
* <li>10/19 tests error with ORA-08176 on {@code INSERT INTO TILEPAGE ... SELECT ... FROM DUAL WHERE NOT EXISTS}
50+
* inside the SERIALIZABLE runtime path ({@code addToQuotaAndTileCounts}, {@code addHitsAndSetAccesTime},
51+
* {@code setTruncated}).
52+
* <li>The remaining 9 either touch TILESET only or fire async writes without awaiting them, so they don't observe the
53+
* failure.
54+
* <li>Rewriting the conditional INSERTs as Oracle {@code MERGE INTO} immediately surfaces a second issue (Spring's
55+
* named-parameter binding can't decide a SQL type for a {@code null} parametersId, hitting ORA-17004), and even
56+
* with that fixed the underlying snapshot read on freshly indexed tables is the real blocker. The retry layer is
57+
* the right level to fix this.
58+
* </ul>
59+
*
60+
* <p>The class is kept in the suite (rather than deleted) so:
61+
*
62+
* <ol>
63+
* <li>The CI workflow's claim that it covers the Oracle dialect via Testcontainers stays honest: the infrastructure
64+
* is in place; only the {@code @Ignore} flips off when the retry layer lands.
65+
* <li>The Oracle XE container plumbing is exercised by the workflow at least up to {@code @ClassRule} startup, so it
66+
* doesn't bit-rot.
67+
* <li>The next person to look at Oracle support has a working scaffold and a clear pointer at the root cause.
68+
* </ol>
69+
*/
70+
@Ignore("Pending the SERIALIZABLE retry layer; see class javadoc for ORA-08176 root cause.")
71+
public class OracleQuotaStoreIT extends AbstractJDBCQuotaStoreIT {
72+
73+
@ClassRule
74+
public static final OracleXEContainer ORACLE = OracleXEContainer.latest().disabledWithoutDocker();
75+
76+
@Override
77+
protected SQLDialect getDialect() {
78+
return new OracleDialect();
79+
}
80+
81+
@Override
82+
protected String getFixtureId() {
83+
return "oracle-testcontainer";
84+
}
85+
86+
@Override
87+
protected JdbcDatabaseContainer<?> getContainer() {
88+
return ORACLE;
89+
}
90+
91+
/**
92+
* Oracle requires {@code CASCADE CONSTRAINTS}, not just {@code CASCADE}, to drop tables with dependents; the base
93+
* cleanup uses the standard SQL form which silently fails on Oracle.
94+
*/
95+
@Override
96+
protected void cleanupDatabase(DataSource dataSource) throws SQLException {
97+
try (Connection cx = dataSource.getConnection();
98+
Statement st = cx.createStatement()) {
99+
try {
100+
st.execute("DROP TABLE TILEPAGE CASCADE CONSTRAINTS");
101+
} catch (Exception e) {
102+
// fine, table may not exist
103+
}
104+
try {
105+
st.execute("DROP TABLE TILESET CASCADE CONSTRAINTS");
106+
} catch (Exception e) {
107+
// fine too
108+
}
109+
}
110+
}
111+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
3+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
4+
* later version.
5+
*
6+
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
7+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8+
*
9+
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
10+
* <http://www.gnu.org/licenses/>.
11+
*
12+
* <p>Copyright 2026
13+
*/
14+
package org.geowebcache.diskquota.jdbc.tests.container;
15+
16+
import org.geowebcache.diskquota.jdbc.PostgreSQLDialect;
17+
import org.geowebcache.diskquota.jdbc.SQLDialect;
18+
import org.geowebcache.testcontainers.jdbc.PostgresContainer;
19+
import org.junit.ClassRule;
20+
import org.testcontainers.containers.JdbcDatabaseContainer;
21+
22+
/**
23+
* Runs the full {@code JDBCQuotaStoreTest} suite against a real PostgreSQL via Testcontainers.
24+
*
25+
* <p>Failsafe picks this up via the {@code *IT} naming convention. If Docker is unavailable the class is skipped
26+
* cleanly through {@link PostgresContainer#disabledWithoutDocker()}.
27+
*/
28+
public class PostgreSQLQuotaStoreIT extends AbstractJDBCQuotaStoreIT {
29+
30+
@ClassRule
31+
public static final PostgresContainer POSTGRES = PostgresContainer.latest().disabledWithoutDocker();
32+
33+
@Override
34+
protected SQLDialect getDialect() {
35+
return new PostgreSQLDialect();
36+
}
37+
38+
@Override
39+
protected String getFixtureId() {
40+
return "postgresql-testcontainer";
41+
}
42+
43+
@Override
44+
protected JdbcDatabaseContainer<?> getContainer() {
45+
return POSTGRES;
46+
}
47+
}

0 commit comments

Comments
 (0)