Skip to content
Merged
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/sharp-moons-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensindexer": minor
---

Introduced indexing event handler preconditions to optimize the cross-service availability in an ENSNode instance when ENSRainbow is performing a cold-start.
21 changes: 0 additions & 21 deletions apps/ensindexer/src/lib/ensraibow-api-client.ts

This file was deleted.

76 changes: 76 additions & 0 deletions apps/ensindexer/src/lib/ensrainbow/singleton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import config from "@/config";

import { secondsToMilliseconds } from "date-fns";
import pRetry from "p-retry";

import { EnsRainbowApiClient } from "@ensnode/ensrainbow-sdk";

const { ensRainbowUrl, labelSet } = config;

if (ensRainbowUrl.href === EnsRainbowApiClient.defaultOptions().endpointUrl.href) {
console.warn(
`Using default public ENSRainbow server which may cause increased network latency. For production, use your own ENSRainbow server that runs on the same network as the ENSIndexer server.`,
);
}

/**
* Singleton ENSRainbow Client instance for ENSIndexer app.
*/
export const ensRainbowClient = new EnsRainbowApiClient({
endpointUrl: ensRainbowUrl,
labelSet,
});

/**
* Cached promise for waiting for ENSRainbow to be ready.
*
* This ensures that multiple concurrent calls to
* {@link waitForEnsRainbowToBeReady} will share the same underlying promise
* in order to use the same retry sequence.
*/
let waitForEnsRainbowToBeReadyPromise: Promise<void> | undefined;

/**
* Wait for ENSRainbow to be ready
*
* Blocks execution until the ENSRainbow instance is ready to serve requests.
*
* Note: It may take 30+ minutes for the ENSRainbow instance to become ready in
* a cold start scenario. We use retries with a fixed interval between attempts
* for the ENSRainbow health check to allow for ample time for ENSRainbow to
* become ready.
*
* @throws When ENSRainbow fails to become ready after all configured retry attempts.
* This error will trigger termination of the ENSIndexer process.
*/
export function waitForEnsRainbowToBeReady(): Promise<void> {
if (waitForEnsRainbowToBeReadyPromise) {
return waitForEnsRainbowToBeReadyPromise;
Comment thread
tk-o marked this conversation as resolved.
}

console.log(`Waiting for ENSRainbow instance to be ready at '${ensRainbowUrl}'...`);

waitForEnsRainbowToBeReadyPromise = pRetry(async () => ensRainbowClient.health(), {
Comment thread
tk-o marked this conversation as resolved.
retries: 60, // This allows for a total of over 1 hour of retries with 1 minute between attempts.
minTimeout: secondsToMilliseconds(60),
maxTimeout: secondsToMilliseconds(60),
onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => {
console.warn(
`Attempt ${attemptNumber} failed for the ENSRainbow health check at '${ensRainbowUrl}' (${error.message}). ${retriesLeft} retries left. This might be due to ENSRainbow having a cold start, which can take 30+ minutes.`,
);
},
})
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
.then(() => console.log(`ENSRainbow instance is ready at '${ensRainbowUrl}'.`))
Comment thread
tk-o marked this conversation as resolved.
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : "Unknown error";

console.error(`ENSRainbow health check failed after multiple attempts: ${errorMessage}`);

// Throw the error to terminate the ENSIndexer process due to the failed health check of a critical dependency
throw new Error(errorMessage, {
cause: error instanceof Error ? error : undefined,
});
Comment thread
tk-o marked this conversation as resolved.
});
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.

return waitForEnsRainbowToBeReadyPromise;
}
10 changes: 4 additions & 6 deletions apps/ensindexer/src/lib/graphnode-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import pRetry from "p-retry";
import type { LabelHash, LiteralLabel } from "@ensnode/ensnode-sdk";
import { type EnsRainbow, ErrorCode, isHealError } from "@ensnode/ensrainbow-sdk";

import { getENSRainbowApiClient } from "@/lib/ensraibow-api-client";

const ensRainbowApiClient = getENSRainbowApiClient();
import { ensRainbowClient } from "@/lib/ensrainbow/singleton";

/**
* Attempt to heal a labelHash to its original label.
Expand Down Expand Up @@ -44,13 +42,13 @@ export async function labelByLabelHash(labelHash: LabelHash): Promise<LiteralLab
// "last failure was HealServerError" (set) from "last failure was a network throw" (undefined).
let lastServerError: EnsRainbow.HealServerError | undefined;

let response: Awaited<ReturnType<typeof ensRainbowApiClient.heal>>;
let response: EnsRainbow.HealResponse;

try {
response = await pRetry(
async () => {
lastServerError = undefined;
const result = await ensRainbowApiClient.heal(labelHash);
const result = await ensRainbowClient.heal(labelHash);

if (isHealError(result) && result.errorCode === ErrorCode.ServerError) {
lastServerError = result;
Expand Down Expand Up @@ -81,7 +79,7 @@ export async function labelByLabelHash(labelHash: LabelHash): Promise<LiteralLab

// Not recoverable; causes the ENSIndexer process to terminate.
if (error instanceof Error) {
error.message = `ENSRainbow Heal Request Failed: ENSRainbow unavailable at '${ensRainbowApiClient.getOptions().endpointUrl}'.`;
error.message = `ENSRainbow Heal Request Failed: ENSRainbow unavailable at '${ensRainbowClient.getOptions().endpointUrl}'.`;
}

throw error;
Expand Down
Loading
Loading