-
Notifications
You must be signed in to change notification settings - Fork 17
Introduce indexing metadata context data model #1997
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 9 commits
4b125d0
dcad533
a5a8bd6
c57c2ac
a28b0de
e91bd13
8892dd5
ee1190e
4954e21
2f8532e
361e99d
7595a41
1902bfb
6bda3d6
e3ddda0
4684fb4
1ff960e
41060d0
e1d6d04
98e6c45
eebe386
11711ca
89c974a
292ed35
159c4ff
b36418b
f3355ef
40961af
6279dea
43ef45b
ad1564f
110927d
bcd81fe
698191f
725901f
55ae265
84b52e1
5cc44ea
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 |
|---|---|---|
|
|
@@ -5,27 +5,10 @@ import { cors } from "hono/cors"; | |
|
|
||
| import type { ErrorResponse } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import { migrateEnsNodeSchema } from "@/lib/ensdb/migrate-ensnode-schema"; | ||
| import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; | ||
| import { logger } from "@/lib/logger"; | ||
|
|
||
| import ensNodeApi from "./handlers/ensnode-api"; | ||
|
|
||
| // Before starting the ENSDb Writer Worker, we need to ensure that | ||
| // the ENSNode Schema in ENSDb is up to date by running any pending migrations. | ||
| await migrateEnsNodeSchema().catch((error) => { | ||
| logger.error({ | ||
| msg: "Failed to initialize ENSNode metadata", | ||
| error, | ||
| module: "ponder-api", | ||
| }); | ||
| process.exitCode = 1; | ||
| throw error; | ||
| }); | ||
|
|
||
| // The entry point for the ENSDb Writer Worker. | ||
| startEnsDbWriterWorker(); | ||
|
|
||
| const app = new Hono(); | ||
|
|
||
|
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. Several lines below I see: Please see related feedback above on the terminology in the comment |
||
| // set the X-ENSIndexer-Version header to the current version | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,8 +5,10 @@ import pRetry from "p-retry"; | |||||||||||||||
| import type { EnsDbWriter } from "@ensnode/ensdb-sdk"; | ||||||||||||||||
| import { | ||||||||||||||||
| buildCrossChainIndexingStatusSnapshotOmnichain, | ||||||||||||||||
| buildIndexingMetadataContextInitialized, | ||||||||||||||||
| type CrossChainIndexingStatusSnapshot, | ||||||||||||||||
| type EnsIndexerPublicConfig, | ||||||||||||||||
|
tk-o marked this conversation as resolved.
Outdated
|
||||||||||||||||
| IndexingMetadataContextStatusCodes, | ||||||||||||||||
| OmnichainIndexingStatusIds, | ||||||||||||||||
| type OmnichainIndexingStatusSnapshot, | ||||||||||||||||
| validateEnsIndexerPublicConfigCompatibility, | ||||||||||||||||
|
|
@@ -97,32 +99,9 @@ export class EnsDbWriterWorker { | |||||||||||||||
| throw new Error("EnsDbWriterWorker is already running"); | ||||||||||||||||
| } | ||||||||||||||||
|
tk-o marked this conversation as resolved.
|
||||||||||||||||
|
|
||||||||||||||||
| // Fetch data required for task 1 and task 2. | ||||||||||||||||
| const inMemoryConfig = await this.getValidatedEnsIndexerPublicConfig(); | ||||||||||||||||
|
|
||||||||||||||||
| // Task 1: upsert ENSDb version into ENSDb. | ||||||||||||||||
| logger.debug({ msg: "Upserting ENSDb version", module: "EnsDbWriterWorker" }); | ||||||||||||||||
| await this.ensDbClient.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); | ||||||||||||||||
| logger.info({ | ||||||||||||||||
| msg: "Upserted ENSDb version", | ||||||||||||||||
| ensDbVersion: inMemoryConfig.versionInfo.ensDb, | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // Task 2: upsert of EnsIndexerPublicConfig into ENSDb. | ||||||||||||||||
| logger.debug({ | ||||||||||||||||
| msg: "Upserting ENSIndexer public config", | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| }); | ||||||||||||||||
| await this.ensDbClient.upsertEnsIndexerPublicConfig(inMemoryConfig); | ||||||||||||||||
| logger.info({ | ||||||||||||||||
| msg: "Upserted ENSIndexer public config", | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // Task 3: recurring upsert of Indexing Status Snapshot into ENSDb. | ||||||||||||||||
| // Task 1: recurring upsert of Indexing Metadata Context into ENSDb. | ||||||||||||||||
| this.indexingStatusInterval = setInterval( | ||||||||||||||||
| () => this.upsertIndexingStatusSnapshot(), | ||||||||||||||||
| () => this.upsertIndexingMetadataContext(), | ||||||||||||||||
| secondsToMilliseconds(INDEXING_STATUS_RECORD_UPDATE_INTERVAL), | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
tk-o marked this conversation as resolved.
|
||||||||||||||||
|
|
@@ -146,156 +125,41 @@ export class EnsDbWriterWorker { | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Get validated ENSIndexer Public Config object for the ENSDb Writer Worker. | ||||||||||||||||
| * | ||||||||||||||||
| * The function retrieves the ENSIndexer Public Config object from both: | ||||||||||||||||
| * - stored config in ENSDb, if available, and | ||||||||||||||||
| * - in-memory config from ENSIndexer Client. | ||||||||||||||||
| * | ||||||||||||||||
| * If a stored config exists **and** the local Ponder app is **not** in dev | ||||||||||||||||
| * mode, the in-memory config is validated for compatibility against the | ||||||||||||||||
| * stored one. Validation is skipped if the local Ponder app is in dev mode, | ||||||||||||||||
| * allowing to override the stored config in ENSDb with the current in-memory | ||||||||||||||||
| * config, without having to keep them compatible. | ||||||||||||||||
| * | ||||||||||||||||
| * @returns The in-memory config when validation passes or no stored config | ||||||||||||||||
| * exists. | ||||||||||||||||
| * @throws Error if either fetch fails, or if the in-memory config is | ||||||||||||||||
| * incompatible with the stored config. | ||||||||||||||||
| */ | ||||||||||||||||
| private async getValidatedEnsIndexerPublicConfig(): Promise<EnsIndexerPublicConfig> { | ||||||||||||||||
| /** | ||||||||||||||||
| * Fetch the in-memory config with retries, to handle potential transient errors | ||||||||||||||||
| * in the ENSIndexer Public Config Builder (e.g. due to network issues). | ||||||||||||||||
| * If the fetch fails after the defined number of retries, the error | ||||||||||||||||
| * will be thrown and the worker will not start, as the ENSIndexer Public Config | ||||||||||||||||
| * is a critical dependency for the worker's tasks. | ||||||||||||||||
| */ | ||||||||||||||||
| const configFetchRetries = 3; | ||||||||||||||||
|
|
||||||||||||||||
| logger.debug({ | ||||||||||||||||
| msg: "Fetching ENSIndexer public config", | ||||||||||||||||
| retries: configFetchRetries, | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| const inMemoryConfigPromise = pRetry(() => this.publicConfigBuilder.getPublicConfig(), { | ||||||||||||||||
| retries: configFetchRetries, | ||||||||||||||||
| onFailedAttempt: ({ attemptNumber, retriesLeft }) => { | ||||||||||||||||
| logger.warn({ | ||||||||||||||||
| msg: "Config fetch attempt failed", | ||||||||||||||||
| attempt: attemptNumber, | ||||||||||||||||
| retriesLeft, | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| }); | ||||||||||||||||
| }, | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| let storedConfig: EnsIndexerPublicConfig | undefined; | ||||||||||||||||
| let inMemoryConfig: EnsIndexerPublicConfig; | ||||||||||||||||
|
|
||||||||||||||||
| try { | ||||||||||||||||
| [storedConfig, inMemoryConfig] = await Promise.all([ | ||||||||||||||||
| this.ensDbClient.getEnsIndexerPublicConfig(), | ||||||||||||||||
| inMemoryConfigPromise, | ||||||||||||||||
| ]); | ||||||||||||||||
| logger.info({ | ||||||||||||||||
| msg: "Fetched ENSIndexer public config", | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| config: inMemoryConfig, | ||||||||||||||||
| }); | ||||||||||||||||
| } catch (error) { | ||||||||||||||||
| const errorMessage = error instanceof Error ? error.message : "Unknown error"; | ||||||||||||||||
|
|
||||||||||||||||
| logger.error({ | ||||||||||||||||
| msg: "Failed to fetch ENSIndexer public config", | ||||||||||||||||
| error, | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // Throw the error to terminate the ENSIndexer process due to failed fetch of critical dependency | ||||||||||||||||
| throw new Error(errorMessage, { | ||||||||||||||||
| cause: error, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Validate in-memory config object compatibility with the stored one, | ||||||||||||||||
| // if the stored one is available. | ||||||||||||||||
| // The validation is skipped if the local Ponder app is running in dev mode. | ||||||||||||||||
| // This is to improve the development experience during ENSIndexer | ||||||||||||||||
| // development, by allowing to override the stored config in ENSDb with | ||||||||||||||||
| // the current in-memory config, without having to keep them compatible. | ||||||||||||||||
| if (storedConfig && !this.localPonderClient.isInDevMode) { | ||||||||||||||||
| try { | ||||||||||||||||
| validateEnsIndexerPublicConfigCompatibility(storedConfig, inMemoryConfig); | ||||||||||||||||
| } catch (error) { | ||||||||||||||||
| const errorMessage = error instanceof Error ? error.message : "Unknown error"; | ||||||||||||||||
|
|
||||||||||||||||
| logger.error({ | ||||||||||||||||
| msg: "In-memory config incompatible with stored config", | ||||||||||||||||
| error, | ||||||||||||||||
| module: "EnsDbWriterWorker", | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // Throw the error to terminate the ENSIndexer process due to | ||||||||||||||||
| // found config incompatibility | ||||||||||||||||
| throw new Error(errorMessage, { | ||||||||||||||||
| cause: error, | ||||||||||||||||
| }); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return inMemoryConfig; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Upsert the current Indexing Status Snapshot into ENSDb. | ||||||||||||||||
| * | ||||||||||||||||
| * This method is called by the scheduler at regular intervals. | ||||||||||||||||
|
||||||||||||||||
| * Upsert the current Indexing Status Snapshot into ENSDb. | |
| * | |
| * This method is called by the scheduler at regular intervals. | |
| * Upsert the current Indexing Metadata Context into ENSDb. | |
| * | |
| * This method is called by the scheduler at regular intervals to refresh | |
| * the indexing metadata context written to ENSDb. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { getUnixTime } from "date-fns/fp/getUnixTime"; | ||
|
|
||
| import { | ||
| buildCrossChainIndexingStatusSnapshotOmnichain, | ||
| buildEnsIndexerStackInfo, | ||
| buildIndexingMetadataContextInitialized, | ||
| IndexingMetadataContextStatusCodes, | ||
| OmnichainIndexingStatusIds, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import { ensDbClient } from "@/lib/ensdb/singleton"; | ||
| import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; | ||
| import { ensRainbowClient, waitForEnsRainbowToBeReady } from "@/lib/ensrainbow/singleton"; | ||
| import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; | ||
| import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; | ||
|
|
||
| /** | ||
| * Prepare for executing the "onchain" event handlers. | ||
| * | ||
| * During Ponder startup, the "onchain" event handlers are executed | ||
| * after all "setup" event handlers have completed. | ||
| * | ||
| * 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 waitForEnsRainbowToBeReady(); | ||
| * ``` | ||
| * | ||
| * @example Multiple blocking preconditions | ||
| * ```ts | ||
| * await Promise.all([ | ||
| * waitForEnsRainbowToBeReady(), | ||
| * waitForAnotherPrecondition(), | ||
| * ]); | ||
| * ``` | ||
| */ | ||
| export async function initIndexingOnchainEvents(): Promise<void> { | ||
| try { | ||
| const indexingMetadataContext = await ensDbClient.getIndexingMetadataContext(); | ||
| console.log("Indexing Metadata Context:", indexingMetadataContext); | ||
|
tk-o marked this conversation as resolved.
Outdated
|
||
| const indexingStatus = await indexingStatusBuilder.getOmnichainIndexingStatusSnapshot(); | ||
| const ensIndexerPublicConfig = await publicConfigBuilder.getPublicConfig(); | ||
| const ensDbPublicConfig = await ensDbClient.buildEnsDbPublicConfig(); | ||
|
|
||
|
tk-o marked this conversation as resolved.
Outdated
|
||
| if (indexingMetadataContext.statusCode === IndexingMetadataContextStatusCodes.Uninitialized) { | ||
| // Invariant: indexing status must be "unstarted" | ||
| if (indexingStatus.omnichainStatus !== OmnichainIndexingStatusIds.Unstarted) { | ||
| throw new Error( | ||
| `Invariant violation: expected omnichain indexing status to be "unstarted" when initializing indexing of onchain events for the first time, but got "${indexingStatus.omnichainStatus}" instead`, | ||
| ); | ||
| } | ||
| } else { | ||
| // if (ensIndexerPublicConfig.ensIndexerBuildId !== indexingMetadataContext.stackInfo.ensIndexer.ensIndexerBuildId) { | ||
| // TODO: store the `ensIndexerPublicConfig` object in ENSDb so `indexingMetadataContext.stackInfo.ensIndexer` is updated | ||
| // } | ||
| } | ||
|
tk-o marked this conversation as resolved.
|
||
|
|
||
| await waitForEnsRainbowToBeReady(); | ||
|
|
||
| const ensRainbowPublicConfig = await ensRainbowClient.config(); | ||
| const now = getUnixTime(new Date()); | ||
| const updatedIndexingMetadataContext = buildIndexingMetadataContextInitialized( | ||
| buildCrossChainIndexingStatusSnapshotOmnichain(indexingStatus, now), | ||
| buildEnsIndexerStackInfo(ensDbPublicConfig, ensIndexerPublicConfig, ensRainbowPublicConfig), | ||
| ); | ||
|
|
||
| // TODO: check ENSRainbow compatibility | ||
| if ( | ||
| ensRainbowPublicConfig.serverLabelSet.labelSetId < | ||
| ensIndexerPublicConfig.clientLabelSet.labelSetId | ||
| ) { | ||
| throw new Error( | ||
| `ENSRainbow instance is not compatible with the current ENSIndexer instance: ENSRainbow serverLabelSetId (${ensRainbowPublicConfig.serverLabelSet.labelSetId}) is less than ENSIndexer clientLabelSetId (${ensIndexerPublicConfig.clientLabelSet.labelSetId})`, | ||
| ); | ||
| } | ||
|
|
||
| await ensDbClient.upsertIndexingMetadataContext(updatedIndexingMetadataContext); | ||
|
|
||
| // TODO: start Indexing Status Sync worker | ||
|
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. Could you please add a follow-up issue to our GitHub for this and add a link to it in a comment here? Thanks
Member
Author
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. Issue logged here: |
||
| // It will be responsible for keeping the indexing status stored within Indexing Metadata Context record in ENSDb up to date | ||
| // await indexingStatusSyncWorker.start(); | ||
| startEnsDbWriterWorker(); | ||
|
tk-o marked this conversation as resolved.
Outdated
tk-o marked this conversation as resolved.
|
||
| } catch (error) { | ||
| // If any error happens during the execution of the preconditions for onchain events, | ||
| // we want to log the error and exit the process with a non-zero exit code, | ||
| // since this is a critical failure that prevents the ENSIndexer instance from functioning properly. | ||
| console.error("Failed to execute preconditions for onchain events:", error); | ||
| process.exit(1); | ||
|
tk-o marked this conversation as resolved.
Outdated
|
||
| } | ||
|
tk-o marked this conversation as resolved.
Outdated
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| /** | ||
| * This module defines the initialization logic for the setup handlers of | ||
| * the Ponder indexing engine executed in an ENSIndexer instance. | ||
| * | ||
| * Setup handlers are executed by Ponder once per ENSIndexer instance lifetime, | ||
| * at the start of the omnichain indexing process. | ||
| * | ||
| * ENSIndexer startup sequence executed by Ponder: | ||
| * 1. Connect to the database and initialize required database objects. | ||
| * 2. Start the omnichain indexing process. | ||
| * 3. Check whether Ponder Checkpoints are already initialized. | ||
| * 4. If not: | ||
| * a) Execute setup handlers. | ||
| * b) Initialize Ponder Checkpoints. | ||
| * 5. a) Make Ponder HTTP API usable. | ||
| * 5. b) Start executing "onchain" event handlers. | ||
| * | ||
| * Step 4 is skipped on ENSIndexer instance restart if Ponder Checkpoints were | ||
| * already initialized in a previous run. | ||
| */ | ||
|
|
||
| import { logger } from "@/lib/logger"; | ||
|
|
||
| /** | ||
| * Initialize indexing setup | ||
| * | ||
| * Runs once per ENSIndexer instance lifetime to initialize indexing setup. | ||
| * | ||
| * Since multiple ENSIndexer instances may run concurrently against the same | ||
| * ENSDb instance, this function MUST BE idempotent and race-condition-safe. | ||
| * | ||
| * Completion of this function unblocks the following sequence of events | ||
| * during ENSIndexer startup: | ||
| * 1. "setup" event handlers execute | ||
| * 2. Ponder Checkpoints initialize | ||
| * 3. IndexingStatusBuilder can build OmnichainIndexingStatusSnapshot | ||
| * via LocalPonderClient (which queries the Ponder HTTP API) | ||
| * | ||
| * @throws Error if any precondition is not satisfied. | ||
| */ | ||
| export async function initIndexingSetup(): Promise<void> { | ||
| const { migrateEnsNodeSchema } = await import("@/lib/ensdb/migrate-ensnode-schema"); | ||
|
tk-o marked this conversation as resolved.
Outdated
|
||
| // Ensure the ENSNode Schema in ENSDb is up to date by running any pending migrations. | ||
| await migrateEnsNodeSchema().catch((error) => { | ||
| logger.error({ | ||
| msg: "Failed to initialize ENSNode metadata", | ||
| error, | ||
| module: "ponder-api", | ||
|
tk-o marked this conversation as resolved.
Outdated
vercel[bot] marked this conversation as resolved.
Outdated
tk-o marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| process.exitCode = 1; | ||
| throw error; | ||
| }); | ||
| } | ||
|
tk-o marked this conversation as resolved.
Outdated
|
||
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.
Hmm. Should we rename this? It seems strange to call it an "ENSNode API".