Skip to content

Commit 0dc990b

Browse files
dmealingclaude
andcommitted
test(integration): widen the Postgres readiness window, which was the flake
Three red gates in one session, three different tests across two ports, one signature: "postgres container '...' did not become ready within 30s". Python query scenarios, Java query scenarios, Python jsonb. Each passed on a re-run with nothing else going. It is not a product failure and it never reaches test logic -- it fires while the container is still booting. But a gate that goes red for a reason indistinguishable from a real failure is a red issue in its own right: the only way to tell the two apart is to re-run and see, which is exactly the habit that lets a genuine failure get waved through as "probably the flake". The cause is arithmetic rather than mysterious. A FULL local-CI run has several ports spinning their own Postgres concurrently; 30s is a guess that holds when one port runs alone and does not when four do. All four ports hardcoded the same 30 -- TS, Java, Kotlin and Python each with their own copy of the same loop and the same message. Now 120s in all four, overridable with MO_PG_READY_TIMEOUT_S, and the failure message reports the window actually used instead of a hardcoded "30s" that could drift from the constant beside it. Raising the bound costs nothing in the normal case: every one of these polls at 250ms and returns the instant the container answers, so a fast container is unaffected. The only thing that changes is how long a genuinely broken container takes to give up -- and for a CI gate, 90 extra seconds once is cheaper than a false red that has to be re-run to interpret. Compiles: java + kotlin integration modules (both sit outside the reactor, so they are built directly), TS workspace typecheck clean, python parses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ad6ea53 commit 0dc990b

4 files changed

Lines changed: 41 additions & 8 deletions

File tree

‎server/java/integration-tests-kotlin/src/test/kotlin/com/metaobjects/integration/kotlin/PostgresContainer.kt‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ class PostgresContainer : AutoCloseable {
100100
}
101101

102102
private fun waitForReady() {
103-
val deadline = System.currentTimeMillis() + 30_000
103+
// READINESS WINDOW. 30s was too tight: under a FULL local-CI run several ports
104+
// spin their own Postgres concurrently, and a container that is merely slow to
105+
// boot produces a red gate indistinguishable from a real failure -- it fires
106+
// BEFORE any test logic. Raising the bound costs nothing when the container is
107+
// ready quickly (this polls every 250ms and returns immediately).
108+
val deadline = System.currentTimeMillis() + (READY_TIMEOUT_S * 1000L)
104109
while (System.currentTimeMillis() < deadline) {
105110
try {
106111
val p = ProcessBuilder("docker", "exec", name, "pg_isready", "-U", PG_USER)
@@ -117,7 +122,7 @@ class PostgresContainer : AutoCloseable {
117122
return
118123
}
119124
}
120-
throw RuntimeException("postgres container '$name' did not become ready within 30s")
125+
throw RuntimeException("postgres container '$name' did not become ready within ${READY_TIMEOUT_S}s")
121126
}
122127

123128
private fun canConnect(): Boolean = try {
@@ -133,6 +138,10 @@ class PostgresContainer : AutoCloseable {
133138
private const val PG_USER = "postgres"
134139
private const val PG_PASSWORD = "test"
135140

141+
/** Container readiness bound, seconds. Overridable so a loaded CI box can widen it. */
142+
private val READY_TIMEOUT_S: Int =
143+
(System.getenv("MO_PG_READY_TIMEOUT_S") ?: "120").toInt()
144+
136145
private fun pickFreePort(): Int = ServerSocket(0).use { it.localPort }
137146

138147
private fun runDocker(vararg args: String): String {

‎server/java/integration-tests/src/test/java/com/metaobjects/integration/PostgresContainer.java‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,17 @@ private static String runDocker(String... args) {
148148
* the first window, so we also verify a real JDBC client connection from
149149
* the host succeeds before returning. Mirrors the TS port's two-phase wait.
150150
*/
151+
// READINESS WINDOW. 30s was too tight: under a FULL local-CI run several ports spin
152+
// their own Postgres concurrently, and a container that is merely slow to boot
153+
// produces a red gate indistinguishable from a real failure -- it fires BEFORE any
154+
// test logic. Raising the bound costs nothing when the container is ready quickly
155+
// (this polls every 250ms and returns immediately) and only changes how long the
156+
// pathological case takes to give up.
157+
private static final int READY_TIMEOUT_S =
158+
Integer.parseInt(System.getenv().getOrDefault("MO_PG_READY_TIMEOUT_S", "120"));
159+
151160
private void waitForReady() {
152-
long deadline = System.currentTimeMillis() + 30_000;
161+
long deadline = System.currentTimeMillis() + (READY_TIMEOUT_S * 1000L);
153162
while (System.currentTimeMillis() < deadline) {
154163
try {
155164
Process p = new ProcessBuilder("docker", "exec", name, "pg_isready", "-U", PG_USER)
@@ -160,7 +169,7 @@ private void waitForReady() {
160169
Thread.currentThread().interrupt(); return;
161170
}
162171
}
163-
throw new RuntimeException("postgres container '" + name + "' did not become ready within 30s");
172+
throw new RuntimeException("postgres container '" + name + "' did not become ready within " + READY_TIMEOUT_S + "s");
164173
}
165174

166175
private boolean canConnect() {

‎server/python/tests/integration/postgres_container.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,16 @@ def __exit__(self, *exc: object) -> None:
138138
# finishes; mirrors the TS/Java two-phase wait).
139139
# -----------------------------------------------------------------------
140140

141+
# READINESS WINDOW. 30s was too tight: under a FULL local-CI run several ports
142+
# spin their own Postgres concurrently, and a container that is merely slow to
143+
# boot produces a red gate indistinguishable from a real failure -- it fires
144+
# BEFORE any test logic. Raising the bound costs nothing when the container is
145+
# ready quickly (the loop polls every 250ms and returns immediately) and only
146+
# changes how long the pathological case takes to give up.
141147
def _wait_ready(self) -> None:
142148
import pg8000.dbapi # local import — pg8000 lives in the `integration` extra
143-
deadline = time.monotonic() + 30
149+
timeout_s = int(os.environ.get("MO_PG_READY_TIMEOUT_S", "120"))
150+
deadline = time.monotonic() + timeout_s
144151
last_err: Exception | None = None
145152
while time.monotonic() < deadline:
146153
ready = subprocess.run(
@@ -159,7 +166,7 @@ def _wait_ready(self) -> None:
159166
last_err = e
160167
time.sleep(0.25)
161168
raise RuntimeError(
162-
f"postgres container '{self._name}' did not become ready within 30s; last err: {last_err}"
169+
f"postgres container '{self._name}' did not become ready within {timeout_s}s; last err: {last_err}"
163170
)
164171

165172

‎server/typescript/packages/integration-tests/src/postgres-container.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,22 @@ function runDocker(args: string[]): string {
133133
// Postgres' first-boot entrypoint starts the server twice (initial init, then
134134
// the real boot). `pg_isready` can return success during the first window, so
135135
// we additionally verify a real client connection from the host succeeds.
136+
// READINESS WINDOW. 30s was too tight: under a FULL local-CI run several ports spin
137+
// their own Postgres concurrently, and a container that is merely slow to boot
138+
// produces a red gate indistinguishable from a real failure -- it fires BEFORE any
139+
// test logic. Raising the bound costs nothing when the container is ready quickly
140+
// (this polls every 250ms and returns immediately) and only changes how long the
141+
// pathological case takes to give up.
142+
const PG_READY_TIMEOUT_S = Number(process.env["MO_PG_READY_TIMEOUT_S"] ?? "120");
143+
136144
async function waitForPgReady(name: string, uri: string): Promise<void> {
137-
const deadline = Date.now() + 30_000;
145+
const deadline = Date.now() + PG_READY_TIMEOUT_S * 1000;
138146
while (Date.now() < deadline) {
139147
const r = spawnSync("docker", ["exec", name, "pg_isready", "-U", "postgres"], { encoding: "utf8" });
140148
if (r.status === 0 && (await canConnect(uri))) return;
141149
await new Promise((res) => setTimeout(res, 250));
142150
}
143-
throw new Error(`postgres container '${name}' did not become ready within 30s`);
151+
throw new Error(`postgres container '${name}' did not become ready within ${PG_READY_TIMEOUT_S}s`);
144152
}
145153

146154
async function canConnect(uri: string): Promise<boolean> {

0 commit comments

Comments
 (0)