-
Notifications
You must be signed in to change notification settings - Fork 17
fix(ensindexer): hot-reload safe writer worker #1928
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
Closed
Closed
Changes from 5 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
73689cc
fix(ensindexer): make EnsDbWriterWorker hot-reload safe
shrugs 31604d9
add changeset
shrugs d80e3d4
fix(ensindexer): address PR review feedback
shrugs 1617314
refactor(ensindexer): cleanups from simplify pass
shrugs 46b4f4d
fix(ensindexer): tighten singleton catch path and widen shutdown call…
shrugs 7bb950f
fix(ensindexer): cancel in-progress run on stop, fail-fast on real wo…
shrugs f57ae1d
fix(ensindexer): treat superseded worker as intentional stop in catch…
shrugs 23070cf
docs(ensindexer): explain DOMException AbortError choice in checkCanc…
shrugs cb59db0
refactor(ensindexer): replace localPonderContext Proxy with explicit …
shrugs 8702d0d
docs(ensindexer): document AbortError DOMException in run() JSDoc
shrugs 60b269e
fix(ensindexer): clear singleton before awaiting stop, add cancellati…
shrugs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "ensindexer": patch | ||
| "@ensnode/ponder-sdk": patch | ||
| --- | ||
|
|
||
| ENSIndexer in dev mode no longer crashes during hot reloading due to EnsDbWriterWorker failure. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,48 +1,105 @@ | ||
| import { ensDbClient } from "@/lib/ensdb/singleton"; | ||
| import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; | ||
| import { localPonderClient } from "@/lib/local-ponder-client"; | ||
| import { localPonderContext } from "@/lib/local-ponder-context"; | ||
| import { logger } from "@/lib/logger"; | ||
| import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; | ||
|
|
||
| import { EnsDbWriterWorker } from "./ensdb-writer-worker"; | ||
|
|
||
| let ensDbWriterWorker: EnsDbWriterWorker; | ||
| let ensDbWriterWorker: EnsDbWriterWorker | undefined; | ||
|
|
||
| function isAbortError(error: unknown): boolean { | ||
| // `fetch` aborts reject with a `DOMException` whose `name === "AbortError"`, | ||
| // which is not always `instanceof Error` across runtimes. Check by name. | ||
| return ( | ||
| typeof error === "object" && | ||
| error !== null && | ||
| (error as { name?: unknown }).name === "AbortError" | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Starts the EnsDbWriterWorker in a new asynchronous context. | ||
| * Stop the given worker (if it is still the active singleton) and clear the | ||
| * singleton reference. Safe to call multiple times. | ||
| */ | ||
| async function gracefulShutdown(worker: EnsDbWriterWorker, reason: string): Promise<void> { | ||
| logger.info({ | ||
| msg: `Stopping EnsDbWriterWorker: ${reason}`, | ||
| module: "EnsDbWriterWorker", | ||
| }); | ||
| await worker.stop(); | ||
| if (ensDbWriterWorker === worker) { | ||
| ensDbWriterWorker = undefined; | ||
| } | ||
|
shrugs marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /** | ||
| * Start (or restart) the EnsDbWriterWorker. | ||
| * | ||
| * The worker will run indefinitely until it is stopped via {@link EnsDbWriterWorker.stop}, | ||
| * for example in response to a process termination signal or an internal error, at | ||
| * which point it will attempt to gracefully shut down. | ||
| * Called from `apps/ensindexer/ponder/src/api/index.ts` on every Ponder | ||
| * API exec. Ponder re-executes the API entry file on hot reload, but this | ||
| * module is cached by vite-node, so module-level state survives across | ||
| * reloads. This function therefore must: | ||
| * | ||
| * @throws Error if the worker is already running when this function is called. | ||
| * 1. Be idempotent — treat a re-call as "the previous instance is dead, | ||
| * replace it" rather than throwing. | ||
| * 2. Re-bind reload-scoped resources (e.g. `apiShutdown`) fresh from | ||
| * `localPonderContext` on every call. Never hoist them to module | ||
| * scope. See `local-ponder-context.ts` for the staleness contract. | ||
| */ | ||
| export function startEnsDbWriterWorker() { | ||
| if (typeof ensDbWriterWorker !== "undefined") { | ||
| throw new Error("EnsDbWriterWorker has already been initialized"); | ||
| export async function startEnsDbWriterWorker(): Promise<void> { | ||
| // Defensively reset any prior instance. The apiShutdown.add() callback | ||
| // from the previous API exec is the primary cleanup path on hot reload; | ||
| // this is a safety net for cases where the callback didn't run (e.g. | ||
| // unexpected shutdown ordering). | ||
| if (ensDbWriterWorker) { | ||
| await gracefulShutdown(ensDbWriterWorker, "stale instance from previous API exec"); | ||
| } | ||
|
|
||
| ensDbWriterWorker = new EnsDbWriterWorker( | ||
| const worker = new EnsDbWriterWorker( | ||
| ensDbClient, | ||
| publicConfigBuilder, | ||
| indexingStatusBuilder, | ||
| localPonderClient, | ||
| ); | ||
| ensDbWriterWorker = worker; | ||
|
|
||
| // Read apiShutdown FRESH from the reactive context. Ponder kills and | ||
| // replaces this on every dev-mode hot reload, so this read MUST happen | ||
| // inside the function call (not at module scope). | ||
| const apiShutdown = localPonderContext.apiShutdown; | ||
| const abortSignal = apiShutdown.abortController.signal; | ||
|
|
||
| ensDbWriterWorker | ||
| .run() | ||
| apiShutdown.add(() => gracefulShutdown(worker, "API shutdown")); | ||
|
|
||
| worker | ||
| .run(abortSignal) | ||
| // Handle any uncaught errors from the worker | ||
| .catch((error) => { | ||
| // Abort the worker on error to trigger cleanup | ||
| ensDbWriterWorker.stop(); | ||
| .catch(async (error) => { | ||
| // Treat as a clean stop only when BOTH the captured shutdown signal | ||
| // is aborted AND the error is an AbortError. Either condition alone | ||
| // could mask a real failure: a non-AbortError thrown after Ponder | ||
| // killed the signal is still a bug worth surfacing, and an | ||
| // AbortError without our signal aborted means it came from | ||
| // somewhere else (e.g. a reactive-getter race) and shouldn't be | ||
| // silently swallowed. | ||
| if (abortSignal.aborted && isAbortError(error)) { | ||
|
shrugs marked this conversation as resolved.
Outdated
|
||
| await gracefulShutdown(worker, "API shutdown (run aborted)"); | ||
| return; | ||
| } | ||
|
|
||
| // Real worker error — clean up and trigger non-zero exit. | ||
| await gracefulShutdown(worker, "uncaught error"); | ||
|
|
||
| logger.error({ | ||
| msg: "EnsDbWriterWorker encountered an error", | ||
| error, | ||
| }); | ||
|
|
||
| // Re-throw the error to ensure the application shuts down with a non-zero exit code. | ||
| // Set a non-zero exit code so the process terminates with failure. | ||
| // Don't rethrow — this catch handler is on a fire-and-forget promise, | ||
| // so a rethrow becomes an unhandled rejection. | ||
| process.exitCode = 1; | ||
|
shrugs marked this conversation as resolved.
Outdated
|
||
| throw error; | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.