Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ensindexer-ponder-fork.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensindexer": patch
---

ENSIndexer now depends on NameHash's published Ponder fork (`@ensnode/ponder`, aliased to `ponder` in the pnpm catalog) instead of upstream `ponder`.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@
"tar@<7.5.16": "^7.5.16",
"ajv@<8.18.0": "^8.18.0",
"minimatch@<10.2.3": "^10.2.3",
"ponder>@hono/node-server@<1.19.13": "catalog:",
"@ensnode/ponder>@hono/node-server@<1.19.13": "catalog:",
"yauzl@<3.2.1": "^3.2.1",
"fast-xml-parser@>=5.0.0 <5.7.0": "^5.7.0",
"kysely@>=0.26.0 <0.28.17": "^0.28.17",
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
"ponder>vite": "^6.4.3",
"@ensnode/ponder>vite": "^6.4.3",
"vite-node>vite": "^6.4.3",
"vite@>=7.0.0 <=7.3.4": "^7.3.5",
"dompurify@<3.4.11": "^3.4.11",
Expand Down Expand Up @@ -106,7 +106,6 @@
"@opentelemetry/api": "patches/@opentelemetry__api.patch",
"@opentelemetry/otlp-exporter-base": "patches/@opentelemetry__otlp-exporter-base.patch",
"js-yaml@4.2.0": "patches/js-yaml@4.2.0.patch",
"ponder@0.16.6": "patches/ponder@0.16.6.patch",
"starlight-llms-txt@0.10.0": "patches/starlight-llms-txt@0.10.0.patch"
}
}
Expand Down
83 changes: 82 additions & 1 deletion packages/integration-test-env/src/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const ENSINDEXER_PORT = 42069;
const ENSAPI_PORT = 4334;
const ENSDB_PORT = 5433;

// docker-compose.orchestrator.yml overrides container_name to these.
const EFP_DEVNET_CONTAINER = "efp-devnet-orchestrator";

// Shared config
const ENSRAINBOW_URL = `http://localhost:${ENSRAINBOW_PORT}`;
const ENSINDEXER_SCHEMA_NAME = "ensindexer_integration_test";
Expand Down Expand Up @@ -170,6 +173,73 @@ async function waitForHealth(url: string, timeoutMs: number, serviceName: string
throw new Error(`${serviceName} did not become healthy within ${timeoutMs / 1000}s`);
}

function containerHealth(container: string): string {
const result = execaSync(
"docker",
["inspect", "--format", "{{.State.Health.Status}}", container],
{
reject: false,
},
);
// `docker inspect` exits non-zero only when the container is missing (or docker itself errors) —
// legitimate `starting`/`unhealthy` states exit 0. Fail loudly instead of returning "" and
// silently looping, which would otherwise mask a container-name mismatch (e.g. if
// docker-compose.orchestrator.yml's container_name diverges from these constants).
if (result.exitCode !== 0) {
throw new Error(
`docker inspect failed for container "${container}" (exit ${result.exitCode}): ${
result.stderr?.trim() || "unknown error"
}. Does this name match docker-compose's container_name?`,
);
}
return result.stdout?.trim() ?? "unknown";
}

/**
* Wait for a docker container to report `healthy`, restarting it if it stalls.
*
* Some images (notably `efp-devnet`, an amd64 image that runs under QEMU emulation on arm64/Apple
* Silicon) intermittently deadlock on their very first start: the process idles at ~0% CPU and
* never brings up its healthcheck endpoint, so the container sits in `starting`/`unhealthy`
* forever. A `docker restart` reliably clears the hang. We poll for `healthy` within a per-attempt
* budget and restart-and-retry rather than passively waiting out the full compose startup timeout.
*/
async function waitForContainerHealthyWithRestart(
container: string,
serviceName: string,
{ attempts, perAttemptMs }: { attempts: number; perAttemptMs: number },
): Promise<void> {
for (let attempt = 1; attempt <= attempts; attempt++) {
const start = Date.now();
while (Date.now() - start < perAttemptMs) {
checkAborted();
const status = containerHealth(container);
if (status === "healthy") {
log(`${serviceName} is healthy`);
return;
}
await new Promise((r) => setTimeout(r, 1000));
}

if (attempt < attempts) {
log(
`${serviceName} did not become healthy within ${perAttemptMs / 1000}s (attempt ${attempt}/${attempts}); restarting container...`,
);
const restart = execaSync("docker", ["restart", container], { reject: false });
if (restart.exitCode !== 0) {
throw new Error(
`Failed to restart container "${container}" (exit ${restart.exitCode}): ${
restart.stderr?.trim() || "unknown error"
}. Does this name match docker-compose's container_name?`,
);
}
}
}
throw new Error(
`${serviceName} did not become healthy after ${attempts} attempts of ${perAttemptMs / 1000}s each`,
);
Comment thread
sevenzing marked this conversation as resolved.
}

function spawnService(
command: string,
args: string[],
Expand Down Expand Up @@ -292,13 +362,24 @@ export async function bringUp(options: { only?: Set<Service> } = {}): Promise<vo
"docker-compose.orchestrator.yml",
)
.withWaitStrategy("devnet-orchestrator", Wait.forHealthCheck())
.withWaitStrategy("efp-devnet-orchestrator", Wait.forHealthCheck())
.withWaitStrategy("ensdb-orchestrator", Wait.forListeningPorts())
// Do NOT wait on efp-devnet's healthcheck here. testcontainers' default wait for a container
// with a HEALTHCHECK is forHealthCheck(), which throws the moment the container goes
// unhealthy — before we get a chance to recover it. efp-devnet intermittently deadlocks on
// first start under QEMU emulation, so instead we only wait for it to reach its first log
// line, then handle real health (with restart-and-retry) in waitForContainerHealthyWithRestart.
.withWaitStrategy("efp-devnet-orchestrator", Wait.forLogMessage(/Starting EFP devnet/))
.withStartupTimeout(180_000)
.up(["ensdb", "devnet", "efp-devnet"]);

log(`ENSDb is ready (port ${ENSDB_PORT})`);

// Wait for efp-devnet to finish deploying, restarting it if it hangs (see helper docs).
await waitForContainerHealthyWithRestart(EFP_DEVNET_CONTAINER, "EFP Devnet", {
attempts: 10,
perAttemptMs: 20_000,
});

// Devnet Chain Id check
const publicClient = createPublicClient({
transport: http(RPC_URL),
Expand Down
7 changes: 7 additions & 0 deletions packages/integration-test-env/src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ async function main() {
}

// Block forever — SIGINT/SIGTERM handlers in lifecycle.ts call cleanup() and exit.
//
// An unresolved Promise does NOT keep the Node.js event loop alive on its own; the process
// only stays up while libuv has an active handle. With `--only devnet` no long-lived child
// processes are spawned (everything is docker-compose/testcontainers), so without an explicit
// keep-alive handle Node would exit immediately after bring-up. A long-interval timer keeps
// the loop alive until a signal handler tears everything down.
setInterval(() => {}, 1 << 30);
await new Promise<never>(() => {});
}

Expand Down
30 changes: 0 additions & 30 deletions patches/ponder@0.16.6.patch

This file was deleted.

Loading
Loading