Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3f5dc4f
Create `ensRainbowClient` singleton for ENSIndexer app
tk-o Mar 30, 2026
f9e4e11
Simplify `ensrainbow/signleton.ts` file
tk-o Mar 30, 2026
40bab87
Create `waitForEnsRainbowToBeReady` function
tk-o Mar 30, 2026
e84c0c7
Implement `eventHandlerPreconditios`
tk-o Mar 30, 2026
febed1c
docs(changeset): Introduced event handler preconditions to improve re…
tk-o Mar 30, 2026
911035a
Merge remote-tracking branch 'origin/main' into feat/onchain-event-ha…
tk-o Mar 30, 2026
22c3c22
Fix URL comparison for ENSRainbow singleton instnace
tk-o Mar 30, 2026
d0dd98c
Apply suggestions from code review
tk-o Mar 30, 2026
b9a0829
Apply PR feedback
tk-o Mar 30, 2026
ef53bf8
Update testing suite
tk-o Mar 30, 2026
d51b2ea
Update testing suite
tk-o Mar 31, 2026
e60b06f
Update ENSDb SDK to allow storing and reading `EnsRainbowPublicConfig…
tk-o Mar 31, 2026
f929618
docs(changeset): Extended the ENSNode Metadata with ENSRainbow Public…
tk-o Mar 31, 2026
63fbec5
Enable ENSDb Writer Worker to store "unstarted" Indexing Status object
tk-o Mar 31, 2026
fb1d93e
Require valid ENSRainbow connection before starting indexing
tk-o Mar 31, 2026
c3967b9
Update testing suite
tk-o Mar 31, 2026
f346b2a
Apply AI PR feedback
tk-o Apr 1, 2026
4e2454b
Improve docs for Ponder Indexing Engine
tk-o Apr 1, 2026
caef441
Merge remote-tracking branch 'origin/main' into feat/ensure-valid-ens…
tk-o Apr 2, 2026
3d7bfb3
Apply PR feedback
tk-o Apr 2, 2026
8b96b5d
Merge branch 'main' into feat/ensure-valid-ensrainbow-connection
shrugs Apr 3, 2026
4a80427
fix: tidy up ensdb reader fns
shrugs Apr 3, 2026
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/whole-lines-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensdb-sdk": minor
---

Extended the ENSNode Metadata with ENSRainbow Public Config.
Original file line number Diff line number Diff line change
Expand Up @@ -263,21 +263,28 @@ describe("EnsDbWriterWorker", () => {
});

describe("interval behavior - snapshot upserts", () => {
it("continues upserting after snapshot validation errors", async () => {
it("upserts snapshots across different omnichain statuses", async () => {
// arrange
const unstartedSnapshot = createMockOmnichainSnapshot({
omnichainStatus: OmnichainIndexingStatusIds.Unstarted,
});
const validSnapshot = createMockOmnichainSnapshot({
omnichainIndexingCursor: 200,
});
const crossChainSnapshot = createMockCrossChainSnapshot({
const unstartedCrossChainSnapshot = createMockCrossChainSnapshot({
slowestChainIndexingCursor: 100,
snapshotTime: 200,
omnichainSnapshot: unstartedSnapshot,
});
const validCrossChainSnapshot = createMockCrossChainSnapshot({
slowestChainIndexingCursor: 200,
snapshotTime: 300,
omnichainSnapshot: validSnapshot,
});

vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(crossChainSnapshot);
vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain)
.mockReturnValueOnce(unstartedCrossChainSnapshot)
.mockReturnValueOnce(validCrossChainSnapshot);

const ensDbClient = createMockEnsDbWriter();
const indexingStatusBuilder = {
Expand All @@ -299,8 +306,15 @@ describe("EnsDbWriterWorker", () => {

// assert
expect(indexingStatusBuilder.getOmnichainIndexingStatusSnapshot).toHaveBeenCalledTimes(2);
expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(1);
expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledWith(crossChainSnapshot);
expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenCalledTimes(2);
expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenNthCalledWith(
1,
unstartedCrossChainSnapshot,
);
expect(ensDbClient.upsertIndexingStatusSnapshot).toHaveBeenNthCalledWith(
2,
validCrossChainSnapshot,
);

// cleanup
worker.stop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {
type CrossChainIndexingStatusSnapshot,
type Duration,
type EnsIndexerPublicConfig,
OmnichainIndexingStatusIds,
type OmnichainIndexingStatusSnapshot,
validateEnsIndexerPublicConfigCompatibility,
} from "@ensnode/ensnode-sdk";
import type { LocalPonderClient } from "@ensnode/ponder-sdk";
Expand Down Expand Up @@ -260,7 +258,8 @@ export class EnsDbWriterWorker {
// get system timestamp for the current iteration
const snapshotTime = getUnixTime(new Date());

const omnichainSnapshot = await this.getValidatedIndexingStatusSnapshot();
const omnichainSnapshot =
await this.indexingStatusBuilder.getOmnichainIndexingStatusSnapshot();

const crossChainSnapshot = buildCrossChainIndexingStatusSnapshotOmnichain(
omnichainSnapshot,
Expand All @@ -278,24 +277,4 @@ export class EnsDbWriterWorker {
// should not cause the ENSDb Writer Worker to stop functioning.
}
}

/**
* Get validated Omnichain Indexing Status Snapshot
*
* @returns Validated Omnichain Indexing Status Snapshot.
* @throws Error if the Omnichain Indexing Status is not in expected status yet.
*/
private async getValidatedIndexingStatusSnapshot(): Promise<OmnichainIndexingStatusSnapshot> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not confident it's right to completely remove this logic.

My worry is: how do we tell the difference between the following situations:

  1. Omnichain status is Unstarted because no indexing has started in ENSDb yet.
  2. Indexing has started in ENSDb with an earlier instance of ENSIndexer, but now ENSIndexer is being restarted and it's still working to discover it's true omnichain indexing status from the state in ENSDb. During this case I'm worried that ENSIndexer also thinks it's "Unstarted"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omnichain status is "unstarted" if an only if no indexing has started. This is related to the fact that the omnichain status can only be "unstarted" if and only if all chains are queued. And a chain is queued if and only if its config.startBlock is equal to its checkpointBlock (checkpointBlock is sourced from Ponder Indexing Status, which is sourced from _ponder_checkpoint table in the ENSIndexer Schema).

If indexing has started and ENSIndexer has written any indexed data into the ENSIndexer Schema, the checkpointBlock for some indexed chain has also been stored in the _ponder_checkpoint table in the ENSIndexer Schema in ENSDb. Therefore, if ENSIndexer instance restarts, some of the indexed chains will not be "queued" anyomore as checkpointBlock will be ahead of config.startBlock for that chain. That leads us to the fact that the omnichain status cannot be "unstarted" anymore, and goes straight to "backfill".

const omnichainSnapshot = await this.indexingStatusBuilder.getOmnichainIndexingStatusSnapshot();

// It only makes sense to write Indexing Status Snapshots into ENSDb once
// the indexing process has started, as before that there is no meaningful
// status to record.
// Invariant: the Omnichain Status must indicate that indexing has started already.
if (omnichainSnapshot.omnichainStatus === OmnichainIndexingStatusIds.Unstarted) {
throw new Error("Omnichain Status must not be 'Unstarted'.");
}

return omnichainSnapshot;
}
}
42 changes: 22 additions & 20 deletions apps/ensindexer/src/lib/ensrainbow/singleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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";
Expand Down Expand Up @@ -47,28 +48,34 @@ let waitForEnsRainbowToBeReadyPromise: Promise<void> | undefined;
* This error will trigger termination of the ENSIndexer process.
*/
export function waitForEnsRainbowToBeReady(): Promise<void> {
if (waitForEnsRainbowToBeReadyPromise) {
return waitForEnsRainbowToBeReadyPromise;
}
if (waitForEnsRainbowToBeReadyPromise) return waitForEnsRainbowToBeReadyPromise;

logger.info({
msg: `Waiting for ENSRainbow instance to be ready`,
ensRainbowInstance: ensRainbowUrl.href,
});

const retryInterval: Duration = 5;
const retryIntervalMs = secondsToMilliseconds(retryInterval);
const retriesPerMinute = 60 / retryInterval;

waitForEnsRainbowToBeReadyPromise = pRetry(async () => ensRainbowClient.health(), {
retries: 60, // This allows for a total of over 1 hour of retries with 1 minute between attempts.
minTimeout: secondsToMilliseconds(60),
maxTimeout: secondsToMilliseconds(60),
retries: retriesPerMinute * 60, // This allows for a total of over 1 hour of retries with `retryInterval` between attempts.
minTimeout: retryIntervalMs,
maxTimeout: retryIntervalMs,
onFailedAttempt: ({ error, attemptNumber, retriesLeft }) => {
logger.warn({
msg: `ENSRainbow health check failed`,
attempt: attemptNumber,
retriesLeft,
error: retriesLeft === 0 ? error : undefined,
ensRainbowInstance: ensRainbowUrl.href,
advice: `This might be due to ENSRainbow having a cold start, which can take 30+ minutes.`,
});
// Log once every minute to avoid excessive logging during ENSRainbow cold start,
// while still providing visibility into the retry process.
if (attemptNumber % 12 === 0) {
logger.warn({
msg: `ENSRainbow health check failed`,
attempt: attemptNumber,
retriesLeft,
error: retriesLeft === 0 ? error : undefined,
ensRainbowInstance: ensRainbowUrl.href,
advice: `This might be due to ENSRainbow having a cold start, which can take 30+ minutes.`,
});
}
},
})
.then(() => {
Expand All @@ -78,18 +85,13 @@ export function waitForEnsRainbowToBeReady(): Promise<void> {
});
})
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : "Unknown error";

logger.error({
msg: `ENSRainbow health check failed after multiple attempts`,
error,
ensRainbowInstance: ensRainbowUrl.href,
});

// 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,
});
throw error;
});

return waitForEnsRainbowToBeReadyPromise;
Expand Down
Loading
Loading