-
Notifications
You must be signed in to change notification settings - Fork 16
Ensure valid ENSRainbow connection before indexing starts #1850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
3f5dc4f
f9e4e11
40bab87
e84c0c7
febed1c
911035a
22c3c22
d0dd98c
b9a0829
ef53bf8
d51b2ea
e60b06f
f929618
63fbec5
fb1d93e
c3967b9
f346b2a
4e2454b
caef441
3d7bfb3
8b96b5d
4a80427
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,15 +1,20 @@ | ||||||
| import config from "@/config"; | ||||||
|
|
||||||
| import { secondsToMilliseconds } from "date-fns"; | ||||||
| import pRetry from "p-retry"; | ||||||
|
|
||||||
| import type { Duration } from "@ensnode/ensnode-sdk"; | ||||||
| import { EnsRainbowApiClient } from "@ensnode/ensrainbow-sdk"; | ||||||
|
|
||||||
| import { logger } from "@/lib/logger"; | ||||||
|
|
||||||
| 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.`, | ||||||
| ); | ||||||
| logger.warn({ | ||||||
| msg: `Using default public ENSRainbow server which may cause increased network latency`, | ||||||
| advice: `For production, use your own ENSRainbow server that runs on the same network as the ENSIndexer server.`, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I note a few lines below that |
||||||
|
|
@@ -35,38 +40,58 @@ let waitForEnsRainbowToBeReadyPromise: Promise<void> | undefined; | |||||
| * 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 | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| * a cold start scenario. We use retries for the ENSRainbow health check with | ||||||
| * an exponential backoff strategy to handle this. | ||||||
| * 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. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| * | ||||||
| * @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> { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Please note: There's many places in this file where the word "healthy" is written. I assume every single instance where this word is used should change to "ready". We should always write in a way that is maximally accurate. Here we are not checking the |
||||||
| if (waitForEnsRainbowToBeReadyPromise) { | ||||||
| return waitForEnsRainbowToBeReadyPromise; | ||||||
| } | ||||||
| if (waitForEnsRainbowToBeReadyPromise) return waitForEnsRainbowToBeReadyPromise; | ||||||
|
|
||||||
| logger.info({ | ||||||
| msg: `Waiting for ENSRainbow instance to be ready`, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ensRainbowInstance: ensRainbowUrl.href, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| }); | ||||||
|
|
||||||
| console.log(`Waiting for ENSRainbow instance to be ready at '${ensRainbowUrl}'...`); | ||||||
| const retryInterval: Duration = 5; | ||||||
| const retryIntervalMs = secondsToMilliseconds(retryInterval); | ||||||
| const retriesPerMinute = 60 / retryInterval; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
import from |
||||||
|
|
||||||
| waitForEnsRainbowToBeReadyPromise = pRetry(async () => ensRainbowClient.health(), { | ||||||
| retries: 12, // This allows for a total of over 1 hour of retries with the exponential backoff strategy. | ||||||
| // 1 + 2 + 4 + ... + 2048 = 2^12 - 1 = 4,095s ≈ 1h 8m | ||||||
| retries: retriesPerMinute * 60, // This allows for a total of over 1 hour of retries with `retryInterval` between attempts. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| minTimeout: retryIntervalMs, | ||||||
| maxTimeout: retryIntervalMs, | ||||||
| onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => { | ||||||
| console.warn( | ||||||
| `Attempt ${attemptNumber} failed for the ENSRainbow health check at '${ensRainbowUrl}' (${error.message}). ${retriesLeft} retries left.`, | ||||||
| ); | ||||||
| // Log once every minute to avoid excessive logging during ENSRainbow cold start, | ||||||
| // while still providing visibility into the retry process. | ||||||
| if (attemptNumber % 12 === 0) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| logger.warn({ | ||||||
| msg: `ENSRainbow health check failed`, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| attempt: attemptNumber, | ||||||
| retriesLeft, | ||||||
| error: retriesLeft === 0 ? error : undefined, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do this? Why not always display the error, even if Then, in the case of the other special final error log statement after all retries have failed, we might not log any Please ignore if a good reason. |
||||||
| ensRainbowInstance: ensRainbowUrl.href, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| advice: `This might be due to ENSRainbow having a cold start, which can take 30+ minutes.`, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| }); | ||||||
| } | ||||||
| }, | ||||||
| }) | ||||||
| .then(() => console.log(`ENSRainbow instance is ready at '${ensRainbowUrl}'.`)) | ||||||
| .then(() => { | ||||||
| logger.info({ | ||||||
| msg: `ENSRainbow instance is ready`, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ensRainbowInstance: ensRainbowUrl.href, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| }); | ||||||
| }) | ||||||
| .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, | ||||||
| logger.error({ | ||||||
| msg: `ENSRainbow health check failed after multiple attempts`, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| error, | ||||||
| ensRainbowInstance: ensRainbowUrl.href, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| }); | ||||||
|
|
||||||
| throw error; | ||||||
| }); | ||||||
|
|
||||||
| return waitForEnsRainbowToBeReadyPromise; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -109,9 +109,10 @@ const EventTypeIds = { | |||||
| Setup: "Setup", | ||||||
|
|
||||||
| /** | ||||||
| * Onchain event | ||||||
| * Onchain log event | ||||||
| * | ||||||
| * Driven by an onchain event emitted by an indexed contract. | ||||||
| * This is Ponder's `LogEvent`, driven by an onchain log event emitted by | ||||||
| * an indexed contract. | ||||||
| */ | ||||||
| Onchain: "Onchain", | ||||||
|
tk-o marked this conversation as resolved.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } as const; | ||||||
|
|
@@ -129,107 +130,86 @@ function buildEventTypeId(eventName: EventNames): EventTypeId { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| let preparedIndexingSetup = false; | ||||||
|
|
||||||
| /** | ||||||
| * Prepare for executing the "setup" event handlers. | ||||||
| * | ||||||
| * This function is guaranteed to be executed just once before | ||||||
| * the first "setup" event handler is executed. | ||||||
| * This is to ensure that we affect the "hot path" of | ||||||
| * indexing as little as possible, since this function is | ||||||
| * called as part of preconditions for every "setup" event. | ||||||
| * During Ponder startup, the "setup" event handlers are executed: | ||||||
| * - After Ponder completed database migrations for ENSIndexer Schema in ENSDb. | ||||||
| * - Before Ponder starts processing any onchain events for indexed chains. | ||||||
| * | ||||||
| * Note that this functions should not have any long-running operations. | ||||||
| * That would delay the population of Ponder Indexing Metrics for | ||||||
| * all indexed chains. Ponder Indexing Metrics are populated only after | ||||||
| * all setup handlers have completed. ENSIndexer relies on | ||||||
| * Ponder Indexing Metrics being immediately available on startup to | ||||||
| * build and store the current Indexing Status snapshot in ENSDb. | ||||||
| * This function is useful to make sure ENSDb is ready for writes, for example, | ||||||
| * by ensuring all required Postgres extensions are installed, etc. | ||||||
| */ | ||||||
| async function prepareIndexingSetup(): Promise<void> { | ||||||
| if (preparedIndexingSetup) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| preparedIndexingSetup = true; | ||||||
|
|
||||||
| // Currently, we don't have any indexing setup preconditions. | ||||||
| async function initializeIndexingSetup(): Promise<void> { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /** | ||||||
| * Setup event handlers should not have any *long-running* preconditions. This is because | ||||||
| * Ponder populates the indexing metrics for all indexed chains only after all setup handlers have run. | ||||||
| * ENSIndexer relies on these indexing metrics being immediately available on startup to build and | ||||||
| * store the current Indexing Status in ENSDb. | ||||||
| */ | ||||||
| } | ||||||
|
|
||||||
| let preparedIndexingActivation = false; | ||||||
|
|
||||||
| /** | ||||||
| * Prepare for executing the "onchain" event handlers. | ||||||
| * | ||||||
| * This function is guaranteed to be executed just once before | ||||||
| * the first "onchain" event handler is executed. | ||||||
| * This is to ensure that we affect the "hot path" of | ||||||
| * indexing as little as possible, since this function is | ||||||
| * called as part of preconditions for every "onchain" event. | ||||||
| * During Ponder startup, the "onchain" event handlers are executed | ||||||
| * after all "setup" event handlers have completed. | ||||||
| * | ||||||
| * @throws If valid ENSRainbow connection could not be established after | ||||||
| * multiple attempts. | ||||||
| * This function is useful to make sure any long-running preconditions for | ||||||
| * onchain event handlers are met, for example, waiting for | ||||||
| * the ENSRainbow instance to be ready before processing any onchain events | ||||||
| * that require data from ENSRainbow. | ||||||
| * | ||||||
| * @example A single blocking precondition | ||||||
| * ```ts | ||||||
| * await ensureValidEnsRainbowConnection(); | ||||||
| * await waitForEnsRainbowToBeReady(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see related comments on renaming this. |
||||||
| * ``` | ||||||
|
tk-o marked this conversation as resolved.
|
||||||
| * | ||||||
| * @example Multiple concurrent blocking preconditions | ||||||
| * @example Multiple blocking preconditions | ||||||
| * ```ts | ||||||
| * await Promise.all([ | ||||||
| * ensureValidEnsRainbowConnection(), | ||||||
| * waitForEnsRainbowToBeReady(), | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please see related comments on renaming this. |
||||||
| * waitForAnotherPrecondition(), | ||||||
| * ]); | ||||||
| * ``` | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tk-o Important: I see a key issue with the current way They both seem to make the incorrect assumption that ENSIndexer never restarts itself. But what happens if ENSIndexer is restarted at particular moments in time? For example:
We really need you to think more strictly and systematically about all the possible state interleavings and ensure that for all possible state interleavings we have a mature solution. I assume that one possible strategy for making this implementation more mature would be to query the state of the ENSNode Metadatadata table for the configured ENSIndexer Schema Name, and then based on the state reflected there determine if it's a completely new instance or not? This needs careful planning as even the way I've described the idea here would not be reliable enough as some additional state would likely need to be added into the ENSNode Metadata table (an additional key?) to identify the exact state of executing these init functions in an idempotent way.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a special reason why |
||||||
| * | ||||||
| * @example Multiple sequential blocking preconditions | ||||||
| * ```ts | ||||||
| * await ensureValidEnsRainbowConnection(); | ||||||
| * await waitForAnotherPrecondition(); | ||||||
| * ``` | ||||||
| */ | ||||||
| async function prepareIndexingActivation() { | ||||||
| if (preparedIndexingActivation) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| preparedIndexingActivation = true; | ||||||
|
|
||||||
| try { | ||||||
| await ensureValidEnsRainbowConnection(); | ||||||
| } catch (error) { | ||||||
| const errorMessage = error instanceof Error ? error.message : "Unknown error"; | ||||||
|
|
||||||
| console.error( | ||||||
| `[Ponder Indexing Engine]: Failed to establish a valid connection to ENSRainbow: ${errorMessage}`, | ||||||
| ); | ||||||
|
|
||||||
| // Throw the error to terminate the ENSIndexer process due to failed connection to critical dependency | ||||||
| throw new Error(errorMessage, { | ||||||
| cause: error, | ||||||
| }); | ||||||
| } | ||||||
| async function initializeIndexingActivation(): Promise<void> { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| await ensureValidEnsRainbowConnection(); | ||||||
| } | ||||||
|
|
||||||
| let indexingSetupPromise: Promise<void> | null = null; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| let indexingActivationPromise: Promise<void> | null = null; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| /** | ||||||
| * Execute any necessary preconditions before running an event handler | ||||||
| * for a given event type. | ||||||
| * | ||||||
| * Some event handlers may have preconditions that need to be met before | ||||||
| * they can run. | ||||||
| * | ||||||
| * This function is idempotent and will only execute its logic once, even if | ||||||
| * called multiple times. This is to ensure that we affect the "hot path" of | ||||||
| * indexing as little as possible, since this function is called for every | ||||||
| * "onchain" event. | ||||||
| */ | ||||||
| async function eventHandlerPreconditions<EventName extends EventNames>( | ||||||
| eventName: EventName, | ||||||
| ): Promise<void> { | ||||||
| const eventType = buildEventTypeId(eventName); | ||||||
|
|
||||||
| async function eventHandlerPreconditions(eventType: EventTypeId): Promise<void> { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| switch (eventType) { | ||||||
| case EventTypeIds.Setup: | ||||||
| return prepareIndexingSetup(); | ||||||
| case EventTypeIds.Setup: { | ||||||
| // Initialize the indexing setup just once | ||||||
| if (indexingSetupPromise === null) { | ||||||
| indexingSetupPromise = initializeIndexingSetup(); | ||||||
| } | ||||||
|
|
||||||
| return indexingSetupPromise; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this function defined such that it returns a If it returns a Why doesn't the code here take the responsibility for calling and awaiting Also I believe the state management here is flawed and needs improvement. Currently your state model only uses
Why isn't the state model improved to represent three states:
It might also be good to set the |
||||||
| } | ||||||
|
|
||||||
| case EventTypeIds.Onchain: { | ||||||
| return prepareIndexingActivation(); | ||||||
| // Initialize the indexing activation just once | ||||||
| if (indexingActivationPromise === null) { | ||||||
| indexingActivationPromise = initializeIndexingActivation(); | ||||||
| } | ||||||
|
|
||||||
| return indexingActivationPromise; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -249,12 +229,10 @@ export function addOnchainEventListener<EventName extends EventNames>( | |||||
| eventName: EventName, | ||||||
| eventHandler: (args: IndexingEngineEventHandlerArgs<EventName>) => Promise<void> | void, | ||||||
| ) { | ||||||
| return ponder.on(eventName, async ({ context, event }) => { | ||||||
| await eventHandlerPreconditions(eventName); | ||||||
| const eventType = buildEventTypeId(eventName); | ||||||
|
|
||||||
| await eventHandler({ | ||||||
| context: buildIndexingEngineContext(context), | ||||||
| event, | ||||||
| }); | ||||||
| return ponder.on(eventName, async ({ context, event }) => { | ||||||
| await eventHandlerPreconditions(eventType); | ||||||
| await eventHandler({ context: buildIndexingEngineContext(context), event }); | ||||||
| }); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,7 +133,7 @@ export async function ensureValidEnsRainbowConnection(): Promise<void> { | |
| */ | ||
| if (!storedConfig) { | ||
| console.log( | ||
| 'No stored ENSRainbow Public Config found in ENSDb. Validating the omnichain indexing status is "Unstarted"...', | ||
| "No stored ENSRainbow Public Config found in ENSDb. Validating the omnichain indexing status is 'Unstarted'...", | ||
| ); | ||
|
Comment on lines
+134
to
+137
|
||
| /** | ||
| * Fetch the indexing status snapshot with retries, to handle potential | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.