Skip to content

Commit 2a34da8

Browse files
committed
test(data-manager): capture staging-dir + container-view diagnostics on E9 canary failure
The canary now fails fast with `docker logs`, but the MOTIS "unable to import: basic_ios::clear: iostream error" it surfaces doesn't say which input broke, and the identical feeds+config import cleanly via direct `docker run` on both arches — so the failure is specific to how the data is assembled/placed on the hosted runner. assemble-staging hardlinks the GTFS archives into the container bind-mount (a prod zero-copy optimization); a hardlink/bind-mount that resolves to an empty or unreadable file on the runner's storage driver would produce exactly this error while looking fine in prod and on dev machines. Add two decisive, tool-free diagnostics to the failure path: a host-side report of the assembled staging dir (byte sizes + zip local-header magic) and the container's own `ls -la /motis-data` (the entrypoint keeps the container alive after an import failure). Together they distinguish "assemble produced a bad archive" from "the container can't read a valid archive", and bump the log tail to 200 lines. No behavior change on the healthy path.
1 parent 2f0d2e4 commit 2a34da8

1 file changed

Lines changed: 61 additions & 3 deletions

File tree

services/data-manager/__tests__/transitous/pipeline-live-motis.test.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@
2626
* dir, runs the pipeline, then tears everything down in `afterAll`. No reliance
2727
* on the operator's `infra/docker/data` tree.
2828
*/
29-
import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
29+
import {
30+
cpSync,
31+
existsSync,
32+
mkdirSync,
33+
mkdtempSync,
34+
readdirSync,
35+
readFileSync,
36+
rmSync,
37+
statSync,
38+
writeFileSync,
39+
} from "node:fs";
3040
import { tmpdir } from "node:os";
3141
import { dirname, join, resolve } from "node:path";
3242
import { fileURLToPath } from "node:url";
@@ -180,12 +190,60 @@ async function failureDiagnostics(
180190
): Promise<string> {
181191
const lines = [`${label}${message ? `: ${message}` : ""}`];
182192
try {
183-
const logs = await execa("docker", ["logs", "--tail", "80", service], { reject: false });
193+
const logs = await execa("docker", ["logs", "--tail", "200", service], { reject: false });
184194
if (logs.stdout) lines.push(`--- docker logs ${service} (stdout) ---`, logs.stdout);
185195
if (logs.stderr) lines.push(`--- docker logs ${service} (stderr) ---`, logs.stderr);
186196
} catch (err) {
187197
lines.push(`(could not fetch ${service} logs: ${(err as Error).message})`);
188198
}
199+
// Container-side view of the mount MOTIS imports from. On an import failure the
200+
// container entrypoint drops into a keep-alive sleep (see motisService), so
201+
// `exec` still works — this shows the archive sizes MOTIS itself sees, which
202+
// reveals a hardlink/bind-mount that resolved to an empty or unreadable file.
203+
const view = await execa("docker", ["exec", service, "sh", "-c", "ls -la /motis-data"], {
204+
reject: false,
205+
});
206+
if (view.stdout) lines.push(`--- ${service}:/motis-data (container view) ---`, view.stdout);
207+
if (view.exitCode !== 0 && view.stderr) lines.push(`--- ${service} exec stderr ---`, view.stderr);
208+
return lines.join("\n");
209+
}
210+
211+
/**
212+
* Host-side view of the assembled staging dir: entry names, byte sizes, and
213+
* whether each `*.zip` still opens with a valid local-file-header magic
214+
* (`PK\x03\x04`). This distinguishes the two very different roots of a MOTIS
215+
* import `iostream error`: assemble-staging produced a bad/zero-byte archive
216+
* (the host sees it broken) vs. the container can't read a structurally-valid
217+
* archive the host sees intact (a bind-mount/hardlink visibility problem on the
218+
* runner's storage driver).
219+
*/
220+
function stagingDirReport(dir: string): string {
221+
const lines = [`--- host view of staging dir ${dir} ---`];
222+
try {
223+
for (const name of readdirSync(dir).sort()) {
224+
const p = join(dir, name);
225+
try {
226+
const st = statSync(p);
227+
if (st.isDirectory()) {
228+
lines.push(` ${name}/ (dir)`);
229+
continue;
230+
}
231+
let note = "";
232+
if (name.endsWith(".zip")) {
233+
const head = readFileSync(p).subarray(0, 4);
234+
const ok = head[0] === 0x50 && head[1] === 0x4b; // "PK"
235+
note = ok
236+
? " zip-magic OK"
237+
: ` BAD zip-magic <${[...head].map((b) => b.toString(16).padStart(2, "0")).join(" ")}>`;
238+
}
239+
lines.push(` ${name} ${st.size}B${note}`);
240+
} catch (err) {
241+
lines.push(` ${name} (stat failed: ${(err as Error).message})`);
242+
}
243+
}
244+
} catch (err) {
245+
lines.push(` (could not read staging dir: ${(err as Error).message})`);
246+
}
189247
return lines.join("\n");
190248
}
191249

@@ -365,7 +423,7 @@ describeLive("transitous pipeline end-to-end against real motis containers", ()
365423
for (const stage of ["motis-import", "motis-health"] as const) {
366424
if (byStage[stage]?.status !== "ok") {
367425
const diag = await failureDiagnostics(stage, STAGING_SERVICE, byStage[stage]?.message);
368-
expect(byStage[stage]?.status, diag).toBe("ok");
426+
expect(byStage[stage]?.status, `${diag}\n${stagingDirReport(stagingDataDir)}`).toBe("ok");
369427
}
370428
}
371429
if (byStage.promote?.status !== "ok") {

0 commit comments

Comments
 (0)