-
Notifications
You must be signed in to change notification settings - Fork 16
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 all 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 |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| --- | ||
| "@ensnode/integration-test-env": minor | ||
| "@ensnode/ensnode-sdk": minor | ||
| "@ensnode/ensdb-sdk": minor | ||
| "ensindexer": minor | ||
| "ensapi": minor | ||
| --- | ||
|
|
||
| Introduced `IndexingMetadataContext` data model, a single record type in ENSNode Metadata table replacing three separate record types (`ensdb_version`, `ensindexer_public_config`, `ensindexer_indexing_status`). Also, consolidated startup init into `initIndexingOnchainEvents()` for reliable execution on every ENSIndexer startup. | ||
|
|
||
| **ensnode-sdk**: `EnsIndexerStackInfo` added as base type, `EnsNodeStackInfo` refactored to extend it. | ||
|
|
||
| **ensdb-sdk**: For `EnsDbReader`, added following method: `getIndexingMetadataContext()`, `isHealthy()`, `isReady()`. For `EnsDbWriter`, added `upsertIndexingMetadataContext()` method. Old per-record read/write methods removed. `EnsNodeMetadataKeys` reduced to single `IndexingMetadataContext` key. | ||
|
|
||
| **ensindexer**: `IndexingMetadataContextBuilder` and `StackInfoBuilder` added. `EnsDbWriterWorker` simplified to single recurring task. HTTP `/config` and `/indexing-status` endpoints now read from in-memory builders instead of ENSDb. `initializeIndexingSetup`/`initializeIndexingActivation` replaced by `initIndexingOnchainEvents`. | ||
|
|
||
| **ensapi**: `indexing-status.cache` and `stack-info.cache` updated to consume `IndexingMetadataContext`. Config schema updated to fetch `EnsIndexerPublicConfig` from `EnsNodeStackInfo`. | ||
|
|
||
| **integration-test-env**: `pollIndexingStatus` updated to use `getIndexingMetadataContext()`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,50 +1,67 @@ | ||
| import { EnsNodeMetadataKeys } from "@ensnode/ensdb-sdk"; | ||
| import { type CrossChainIndexingStatusSnapshot, SWRCache } from "@ensnode/ensnode-sdk"; | ||
| import { | ||
| type CrossChainIndexingStatusSnapshot, | ||
| IndexingMetadataContextStatusCodes, | ||
| SWRCache, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import { ensDbClient } from "@/lib/ensdb/singleton"; | ||
| import { lazyProxy } from "@/lib/lazy"; | ||
| import { makeLogger } from "@/lib/logger"; | ||
|
|
||
| const logger = makeLogger("indexing-status.cache"); | ||
|
|
||
| export type IndexingStatusCache = SWRCache<CrossChainIndexingStatusSnapshot>; | ||
|
|
||
| // lazyProxy defers construction until first use so that this module can be | ||
| // imported without env vars being present (e.g. during OpenAPI generation). | ||
| // SWRCache with proactivelyInitialize:true starts background polling immediately | ||
| // on construction, which would trigger ensDbClient before env vars are available. | ||
| export const indexingStatusCache = lazyProxy<SWRCache<CrossChainIndexingStatusSnapshot>>( | ||
| /** | ||
| * Cache for {@link CrossChainIndexingStatusSnapshot}, which is loaded | ||
| * from ENSDb on demand. The cached value is expected to be updated | ||
| * very frequently, following the update frequency of | ||
| * {@link IndexingMetadataContextInitialized.indexingStatus} in ENSDb. | ||
| * Therefore, the cache is configured with a very short TTL and | ||
| * proactive revalidation interval to ensure that the cached value is | ||
| * as fresh as possible. | ||
| */ | ||
| export const indexingStatusCache = lazyProxy<IndexingStatusCache>( | ||
| () => | ||
| new SWRCache<CrossChainIndexingStatusSnapshot>({ | ||
| fn: async (_cachedResult) => | ||
| ensDbClient | ||
| .getIndexingStatusSnapshot() // get the latest indexing status snapshot | ||
| .then((snapshot) => { | ||
| if (snapshot === undefined) { | ||
| // An indexing status snapshot has not been found in ENSDb yet. | ||
| // This might happen during application startup, i.e. when ENSDb | ||
| // has not yet been populated with the first snapshot. | ||
| // Therefore, throw an error to trigger the subsequent `.catch` handler. | ||
| throw new Error("Indexing Status snapshot not found in ENSDb yet."); | ||
| } | ||
| fn: async function loadIndexingStatusSnapshot() { | ||
| try { | ||
| const indexingMetadataContext = await ensDbClient.getIndexingMetadataContext(); | ||
|
|
||
| if ( | ||
| indexingMetadataContext.statusCode !== IndexingMetadataContextStatusCodes.Initialized | ||
| ) { | ||
| // The IndexingMetadataContext has not been initialized in ENSDb yet. | ||
| // This might happen during application startup, i.e. when ENSDb | ||
| // has not yet been populated with the IndexingMetadataContext record. | ||
| // Therefore, throw an error to trigger the subsequent catch handler. | ||
| throw new Error("Indexing Metadata Context was uninitialized in ENSDb."); | ||
| } | ||
|
|
||
| // The indexing status snapshot has been fetched and successfully validated for caching. | ||
| // Therefore, return it so that this current invocation of `readCache` will: | ||
| // - Replace the currently cached value (if any) with this new value. | ||
| // - Return this non-null value. | ||
| return snapshot; | ||
| }) | ||
| .catch((error) => { | ||
| // Either the indexing status snapshot fetch failed, or the indexing status snapshot was not found in ENSDb yet. | ||
| // Therefore, throw an error so that this current invocation of `readCache` will: | ||
| // - Reject the newly fetched response (if any) such that it won't be cached. | ||
| // - Return the most recently cached value from prior invocations, or `null` if no prior invocation successfully cached a value. | ||
| logger.error( | ||
| error, | ||
| `Error occurred while loading Indexing Status snapshot record from ENSNode Metadata table in ENSDb. ` + | ||
| `Where clause applied: ("ensIndexerSchemaName" = "${ensDbClient.ensIndexerSchemaName}", "key" = "${EnsNodeMetadataKeys.EnsIndexerIndexingStatus}"). ` + | ||
| `The cached indexing status snapshot (if any) will not be updated.`, | ||
| ); | ||
| throw error; | ||
| }), | ||
| // The CrossChainIndexingStatusSnapshot has been successfully loaded for caching. | ||
| // Therefore, return it so that this current invocation of `readCache` will: | ||
| // - Replace the currently cached value (if any) with this new value. | ||
| // - Return this non-null value. | ||
| return indexingMetadataContext.indexingStatus; | ||
| } catch (error) { | ||
| // IndexingMetadataContext was uninitialized in ENSDb. | ||
| // Therefore, throw an error so that this current invocation of `readCache` will: | ||
| // - Reject the newly fetched response (if any) such that it won't be cached. | ||
| // - Return the most recently cached value from prior invocations, or `null` if no prior invocation successfully cached a value. | ||
|
tk-o marked this conversation as resolved.
|
||
| logger.error( | ||
| error, | ||
| `Error occurred while loading Indexing Metadata Context record from ENSNode Metadata table in ENSDb. ` + | ||
| `Where clause applied: ("ensIndexerSchemaName" = "${ensDbClient.ensIndexerSchemaName}", "key" = "${EnsNodeMetadataKeys.IndexingMetadataContext}"). ` + | ||
| `The cached indexing status snapshot (if any) will not be updated.`, | ||
| ); | ||
|
tk-o marked this conversation as resolved.
|
||
| throw error; | ||
| } | ||
| }, | ||
| // We need to refresh the indexing status cache very frequently. | ||
| // ENSDb won't have issues handling this frequency of queries. | ||
| ttl: 1, // 1 second | ||
|
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. Instead of hardcoding to 1-second, it would be nice to make it configurable with perhaps 1-second just be the default. Goal: Take steps towards moving this into |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,76 +2,91 @@ import config from "@/config"; | |
|
|
||
| import { minutesToSeconds } from "date-fns"; | ||
|
|
||
| import { EnsNodeMetadataKeys } from "@ensnode/ensdb-sdk"; | ||
| import { | ||
| buildEnsNodeStackInfo, | ||
| type CachedResult, | ||
| type EnsNodeStackInfo, | ||
| type IndexingMetadataContextInitialized, | ||
|
tk-o marked this conversation as resolved.
|
||
| IndexingMetadataContextStatusCodes, | ||
| SWRCache, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
tk-o marked this conversation as resolved.
tk-o marked this conversation as resolved.
|
||
|
|
||
| import { buildEnsApiPublicConfig } from "@/config/config.schema"; | ||
| import { ensDbClient } from "@/lib/ensdb/singleton"; | ||
| import { lazyProxy } from "@/lib/lazy"; | ||
| import logger from "@/lib/logger"; | ||
|
|
||
| /** | ||
| * Loads the ENSNode stack info, either from cache if available, | ||
| * or by building it from the public configs of ENSApi and ENSDb. | ||
| * | ||
| * The ENSNode Stack Info object is considered immutable for | ||
| * the lifecycle of an ENSApi process instance, so once it is successfully | ||
| * loaded, it will be cached indefinitely. | ||
| */ | ||
| async function loadEnsNodeStackInfo( | ||
| cachedResult?: CachedResult<EnsNodeStackInfo>, | ||
| ): Promise<EnsNodeStackInfo> { | ||
| if (cachedResult && !(cachedResult.result instanceof Error)) { | ||
| return cachedResult.result; | ||
| } | ||
|
|
||
| const ensApiPublicConfig = buildEnsApiPublicConfig(config); | ||
| const ensDbPublicConfig = await ensDbClient.buildEnsDbPublicConfig(); | ||
| const ensIndexerPublicConfig = ensApiPublicConfig.ensIndexerPublicConfig; | ||
| const ensRainbowPublicConfig = ensIndexerPublicConfig.ensRainbowPublicConfig; | ||
|
|
||
| return buildEnsNodeStackInfo( | ||
| ensApiPublicConfig, | ||
| ensDbPublicConfig, | ||
| ensIndexerPublicConfig, | ||
| ensRainbowPublicConfig, | ||
| ); | ||
| } | ||
| export type EnsNodeStackInfoCache = SWRCache<EnsNodeStackInfo>; | ||
|
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. Hmm. Not sure I understand. I thought the indexing status cache would now hold both the stack info AND the indexing status? Therefore, why have two separate caches? Why are they not combined?
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. @lightwalker-eth we can have a single cache for indexing metadata context. It'd include both, the indexing status snapshot, and the stack info. Would that work? |
||
|
|
||
| // lazyProxy defers construction until first use so that this module can be | ||
| // imported without env vars being present (e.g. during OpenAPI generation). | ||
| // SWRCache with proactivelyInitialize:true starts background polling immediately | ||
| // on construction, which would trigger ensDbClient before env vars are available. | ||
| /** | ||
| * Cache for ENSNode stack info | ||
| * Once successfully loaded, the ENSNode Stack Info is cached indefinitely and | ||
| * never revalidated. This ensures the JSON is only fetched once during | ||
| * the application lifecycle. | ||
| * Cache for {@link EnsNodeStackInfo}, which is loaded from ENSDb on demand. | ||
| * Once successfully loaded, the {@link EnsNodeStackInfo} is cached and kept up-to-date | ||
| * by proactive revalidation, since the {@link EnsNodeStackInfo} might change during | ||
| * the lifecycle of the ENSApi instance, for example, when | ||
| * {@link IndexingMetadataContextInitialized.stackInfo} is updated in ENSDb. | ||
| * This is unlikely to happen at all, and if it does happen, it is likely to be | ||
| * very infrequent. However, proactive revalidation ensures that if such changes do happen, | ||
| * the cached value will be updated in a reasonable time frame without requiring | ||
| * a restart of the ENSApi application. | ||
| * | ||
| * Configuration: | ||
| * - ttl: Infinity - Never expires once cached | ||
| * - errorTtl: 1 minute - If loading fails, retry on next access after 1 minute | ||
| * - proactiveRevalidationInterval: undefined - No proactive revalidation | ||
| * - ttl: 1 minute - Allow cached value to be fresh for up to 1 minute. | ||
| * - errorTtl: 1 minute - If loading fails, retry on next access after 1 minute. | ||
| * - proactiveRevalidationInterval: 5 minutes - Refresh the cached value every 5 minutes. | ||
| * - proactivelyInitialize: true - Load immediately on startup | ||
| */ | ||
| export const stackInfoCache = lazyProxy( | ||
| export const stackInfoCache = lazyProxy<EnsNodeStackInfoCache>( | ||
| () => | ||
| /** | ||
| * Cache for ENSNode stack info | ||
| * | ||
| * Once initialized successfully, this cache will always return | ||
| * the same stack info for the lifecycle of the ENSApi instance. | ||
| * | ||
| * If initialization fails, it will keep retrying on access until it succeeds, which is desirable because the stack info is critical for the functioning of the application and we want to recover from transient initialization failures without requiring a restart. | ||
| */ | ||
| new SWRCache<EnsNodeStackInfo>({ | ||
| fn: loadEnsNodeStackInfo, | ||
| ttl: Number.POSITIVE_INFINITY, | ||
| fn: async function loadEnsNodeStackInfo() { | ||
| try { | ||
| const indexingMetadataContext = await ensDbClient.getIndexingMetadataContext(); | ||
|
|
||
| if ( | ||
| indexingMetadataContext.statusCode !== IndexingMetadataContextStatusCodes.Initialized | ||
| ) { | ||
| // The IndexingMetadataContext has not been initialized in ENSDb yet. | ||
| // This might happen during application startup, i.e. when ENSDb | ||
| // has not yet been populated with the IndexingMetadataContext record. | ||
| // Therefore, throw an error to trigger the subsequent catch handler. | ||
| throw new Error("Indexing Metadata Context was uninitialized in ENSDb."); | ||
| } | ||
|
|
||
| const ensIndexerStackInfo = indexingMetadataContext.stackInfo; | ||
| const ensNodeStackInfo = buildEnsNodeStackInfo( | ||
| buildEnsApiPublicConfig(config, ensIndexerStackInfo.ensIndexer), | ||
| ensIndexerStackInfo.ensDb, | ||
| ensIndexerStackInfo.ensIndexer, | ||
| ensIndexerStackInfo.ensRainbow, | ||
| ); | ||
|
|
||
| // The EnsNodeStackInfo has been successfully built for caching. | ||
| // Therefore, return it so that this current invocation of `readCache` will: | ||
| // - Replace the currently cached value (if any) with this new value. | ||
| // - Return this non-null value. | ||
| return ensNodeStackInfo; | ||
| } catch (error) { | ||
| // IndexingMetadataContext was uninitialized in ENSDb. | ||
| // Therefore, throw an error so that this current invocation of `readCache` will: | ||
| // - Reject the newly fetched response (if any) such that it won't be cached. | ||
| // - Return the most recently cached value from prior invocations, or `null` if no prior invocation successfully cached a value. | ||
|
tk-o marked this conversation as resolved.
|
||
| logger.error( | ||
| error, | ||
| `Error occurred while loading Indexing Metadata Context record from ENSNode Metadata table in ENSDb. ` + | ||
| `Where clause applied: ("ensIndexerSchemaName" = "${ensDbClient.ensIndexerSchemaName}", "key" = "${EnsNodeMetadataKeys.IndexingMetadataContext}"). ` + | ||
| `The cached EnsNodeStackInfo object (if any) will not be updated.`, | ||
| ); | ||
|
|
||
| throw error; | ||
| } | ||
| }, | ||
| ttl: minutesToSeconds(1), | ||
| errorTtl: minutesToSeconds(1), | ||
| proactiveRevalidationInterval: undefined, | ||
| proactiveRevalidationInterval: minutesToSeconds(5), | ||
| proactivelyInitialize: true, | ||
| }), | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import packageJson from "@/../package.json" with { type: "json" }; | ||
|
|
||
| import { | ||
| ChainIndexingStatusIds, | ||
| CrossChainIndexingStrategyIds, | ||
| deserializeIndexingMetadataContext, | ||
| type EnsRainbowPublicConfig, | ||
| type IndexingMetadataContextInitialized, | ||
| IndexingMetadataContextStatusCodes, | ||
| OmnichainIndexingStatusIds, | ||
| PluginName, | ||
| RangeTypeIds, | ||
| type SerializedCrossChainIndexingStatusSnapshot, | ||
| type SerializedEnsDbPublicConfig, | ||
| type SerializedEnsIndexerPublicConfig, | ||
| type SerializedEnsIndexerStackInfo, | ||
| type SerializedIndexingMetadataContextInitialized, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import type { EnsApiEnvironment } from "@/config/environment"; | ||
|
|
||
| export const VALID_RPC_URL = "https://eth-sepolia.g.alchemy.com/v2/1234"; | ||
|
|
||
| export const ENSDB_PUBLIC_CONFIG = { | ||
| versionInfo: { | ||
| postgresql: "17.4", | ||
| }, | ||
| } satisfies SerializedEnsDbPublicConfig; | ||
|
|
||
| export const ENSINDEXER_PUBLIC_CONFIG = { | ||
| namespace: "mainnet", | ||
| ensIndexerSchemaName: "ensindexer_0", | ||
| ensRainbowPublicConfig: { | ||
| serverLabelSet: { labelSetId: "subgraph", highestLabelSetVersion: 0 }, | ||
| versionInfo: { | ||
| ensRainbow: packageJson.version, | ||
| }, | ||
| }, | ||
| indexedChainIds: [1], | ||
| isSubgraphCompatible: false, | ||
|
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 believe we'll want to work towards removing this Goal: We want to begin optimizing more for ENSv2 and the ENS Omnigraph and preparing to leave behind the ENS Subgraph. This idea is also related to the goal of us pushing to get this data model to a place where it is super stable and we don't need to keep breaking it.
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. |
||
| clientLabelSet: { labelSetId: "subgraph", labelSetVersion: 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. I believe we'll want to work towards making it optional to run ENSRainbow. Not in this PR, but could you please create a follow-up issue for us to track this opportunity. Goal: We should make the stack info data model support the possibility that there's no ENSRainbow, and therefore that there also is no
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: |
||
| plugins: [PluginName.Subgraph], | ||
| versionInfo: { | ||
| ensDb: packageJson.version, | ||
| ensIndexer: packageJson.version, | ||
| ensNormalize: "1.11.1", | ||
| ponder: "0.8.0", | ||
| }, | ||
| } satisfies SerializedEnsIndexerPublicConfig; | ||
|
|
||
| const ENSRAINBOW_PUBLIC_CONFIG = { | ||
| serverLabelSet: { labelSetId: "subgraph", highestLabelSetVersion: 0 }, | ||
| versionInfo: { | ||
| ensRainbow: packageJson.version, | ||
| }, | ||
| } satisfies EnsRainbowPublicConfig; | ||
|
|
||
| export const INDEXING_STATUS = { | ||
| strategy: CrossChainIndexingStrategyIds.Omnichain, | ||
| slowestChainIndexingCursor: 1777147427, | ||
| snapshotTime: 1777147440, | ||
| omnichainSnapshot: { | ||
| omnichainStatus: OmnichainIndexingStatusIds.Following, | ||
| chains: { | ||
| "1": { | ||
| chainStatus: ChainIndexingStatusIds.Following, | ||
| config: { | ||
| rangeType: RangeTypeIds.LeftBounded, | ||
| startBlock: { | ||
| timestamp: 1489165544, | ||
| number: 3327417, | ||
| }, | ||
| }, | ||
| latestIndexedBlock: { | ||
| timestamp: 1777147427, | ||
| number: 24959286, | ||
| }, | ||
| latestKnownBlock: { | ||
| timestamp: 1777147427, | ||
| number: 24959286, | ||
| }, | ||
| }, | ||
| }, | ||
| omnichainIndexingCursor: 1777147427, | ||
| }, | ||
| } satisfies SerializedCrossChainIndexingStatusSnapshot; | ||
|
|
||
| export const ENSINDEXER_STACK_INFO = { | ||
| ensDb: ENSDB_PUBLIC_CONFIG, | ||
| ensIndexer: ENSINDEXER_PUBLIC_CONFIG, | ||
| ensRainbow: ENSRAINBOW_PUBLIC_CONFIG, | ||
| } satisfies SerializedEnsIndexerStackInfo; | ||
|
|
||
| export const INDEXING_METADATA_CONTEXT = { | ||
| statusCode: IndexingMetadataContextStatusCodes.Initialized, | ||
| indexingStatus: INDEXING_STATUS, | ||
| stackInfo: ENSINDEXER_STACK_INFO, | ||
| } satisfies SerializedIndexingMetadataContextInitialized; | ||
|
|
||
| export const indexingMetadataContextInitialized = deserializeIndexingMetadataContext( | ||
| INDEXING_METADATA_CONTEXT, | ||
| ) as IndexingMetadataContextInitialized; | ||
|
|
||
| export const BASE_ENV = { | ||
| ENSDB_URL: "postgresql://user:password@localhost:5432/mydb", | ||
| ENSINDEXER_SCHEMA_NAME: "ensindexer_0", | ||
| RPC_URL_1: VALID_RPC_URL, | ||
| } satisfies EnsApiEnvironment; | ||
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.