Skip to content

refactor(egg): remove buildSnapshot/restoreSnapshot that never worked under --build-snapshot#5861

Open
killagu wants to merge 1 commit intonextfrom
fix/remove-nonworking-snapshot-api
Open

refactor(egg): remove buildSnapshot/restoreSnapshot that never worked under --build-snapshot#5861
killagu wants to merge 1 commit intonextfrom
fix/remove-nonworking-snapshot-api

Conversation

@killagu
Copy link
Copy Markdown
Contributor

@killagu killagu commented Apr 11, 2026

Summary

Remove buildSnapshot() / restoreSnapshot() from packages/egg — they never worked in a real node --build-snapshot environment.

Keep the triggerSnapshotWillSerialize / triggerSnapshotDidDeserialize lifecycle hooks in packages/core/src/lifecycle.ts as a forward-compatible abstraction for future serialization mechanisms (e.g. fork-based snapshot).

Why

PR #5856 shipped buildSnapshot() / restoreSnapshot() with passing tests. The tests exercised the hooks from a normal Node.js process and mocked V8's startup snapshot API — they never ran the real `--build-snapshot` environment, which is where the architectural constraints manifest.

Verified during Track B spike:

  1. minimalRunCjs only supports builtin requirerequireForUserSnapshot blocks any userland package (egg, koa, etc.) with MODULE_NOT_FOUND. The API's internal `startEgg()` call cannot possibly load.

  2. SpinEventLoopInternal triggers libuv timer callbacks before serialization — causes `async hook stack has become corrupted (actual: 8, expected: 0)`. egg's loader creates timers, setImmediates, and AsyncLocalStorage contexts during load, which cannot be drained by `triggerSnapshotWillSerialize` because they live in libuv's internal state, not in any JS-reachable queue.

The fix for (2) would require rewriting egg's entire async load path as pure-sync. Framework-level change, out of scope.

What's removed

  • `packages/egg/src/lib/snapshot.ts` (entire file, 90 lines)
  • Exports of `buildSnapshot` / `restoreSnapshot` from `packages/egg/src/index.ts`
  • `restoreSnapshot` tests in `packages/egg/test/snapshot.test.ts`

What's kept

  • `triggerSnapshotWillSerialize` / `triggerSnapshotDidDeserialize` in `packages/core/src/lifecycle.ts` (hooks remain valid abstraction)
  • `EggApplicationCore.snapshotWillSerialize` / `snapshotDidDeserialize` (boot hook registrations — hooks into the core lifecycle)
  • `StartEggOptions.snapshot` flag and related guards (used by `tools/scripts` snapshot-build module-cache approach)
  • `loadFinished` promise
  • Agent keepalive refactor (moved to `configDidLoad` boot hook, unrelated cleanup)

Test plan

  • `pnpm --filter=egg run typecheck`
  • `pnpm --filter=@eggjs/core run typecheck`
  • `pnpm --filter=egg exec vitest run test/snapshot.test.ts` — 9 passed
  • `pnpm --filter=egg exec vitest run test/index.test.ts` — 1 passed (exports snapshot updated)
  • `pnpm --filter=@eggjs/core exec vitest run test/snapshot.test.ts` — 16 passed (hook mechanism unchanged)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Breaking Changes

    • Removed snapshot utilities (buildSnapshot and restoreSnapshot functions) from the public API.
    • Snapshot-related APIs are no longer re-exported from the package root.
  • Documentation

    • Enhanced lifecycle hook documentation for serialization scenarios.

… under --build-snapshot

The `buildSnapshot()` / `restoreSnapshot()` facade added in #5856 cannot
work under `node --build-snapshot`. It was merged because its tests
exercised the `triggerSnapshotWillSerialize` / `triggerSnapshotDidDeserialize`
hooks from a normal Node.js process and mocked V8's startup snapshot API —
they never ran the real `--build-snapshot` environment, which is where the
architectural constraints manifest:

1. Node 22's `internal/main/mksnapshot.js` compiles the entry via
   `compileSerializeMain` and hands it a custom `require` that only
   resolves **builtin** modules. Any `require('egg')`, `require('koa')`,
   or other userland import throws `MODULE_NOT_FOUND`. ESM entry scripts
   are rejected (`SyntaxError: Cannot use import statement outside a
   module`) and `dynamic import()` throws `Error: Not supported`. The
   facade's `await startEgg({ snapshot: true })` call-chain is
   unreachable from inside mksnapshot without a bundle — and the
   `buildSnapshot` API itself provided no bundling.

2. Even if the framework is inlined via an esbuild bundle so the top of
   the snapshot builder can reach egg at all, V8 snapshot serialization
   crashes with `async hook stack has become corrupted (actual: 8,
   expected: 0)`. `BuildSnapshotWithoutCodeCache` calls
   `SpinEventLoopInternal` before dumping the heap, which fires pending
   libuv timer callbacks; each callback pushes an `async_context` via
   `InternalCallbackScope`, and the stack is never drained back to depth
   0 because egg's loader inherently creates timers, AsyncLocalStorage
   contexts, and setImmediate callbacks during `agent.ready()` /
   `application.ready()`. Switching to `metadataOnly: true` does not
   help — the app loader path still creates pending timers. The
   `triggerSnapshotWillSerialize` lifecycle hook has no visibility into
   libuv's internal timer queue and cannot drain it.

A viable "pre-warm app instance" snapshot would require either a
fork-based mechanism (spawn a worker, run egg to ready, `v8.serialize()`
to a custom blob, restore in a fresh process) or a framework-wide
rewrite of egg's loader to be fully synchronous — both of which are
separate projects outside this branch's scope.

Changes:
- Delete `packages/egg/src/lib/snapshot.ts` (the two broken functions
  and the `__egg_snapshot_app` global)
- Remove the export from `packages/egg/src/index.ts`
- Drop `buildSnapshot` / `restoreSnapshot` from the exports snapshot
  in `packages/egg/test/__snapshots__/index.test.ts.snap`
- Remove the `restoreSnapshot` test case from
  `packages/egg/test/snapshot.test.ts`; document at the top of the file
  that the remaining tests run under a normal Node process rather than
  `--build-snapshot`
- Extend the JSDoc on
  `Lifecycle.triggerSnapshotWillSerialize` in
  `packages/core/src/lifecycle.ts` to explain the constraint and why
  the hook mechanism is retained as a forward-compatible abstraction

Kept intentionally:
- `triggerSnapshotWillSerialize` / `triggerSnapshotDidDeserialize`
  methods on `Lifecycle` and `EggApplicationCore` — general-purpose
  resource-cleanup hooks that a future non-mksnapshot serialization
  mechanism can drive
- The `snapshot?: boolean` option on `StartEggOptions` and the guards
  in `egg.ts` / `agent.ts` / `start.ts` — inert without a caller but
  harmless, and match the hook infrastructure they were designed for
- `packages/core/test/snapshot.test.ts` — unit tests for the hook
  mechanism itself

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 11, 2026 03:34
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

The PR removes the V8 startup snapshot implementation from the egg package, including buildSnapshot() and restoreSnapshot() exported functions and their re-exports from the package root. Documentation for snapshot-related lifecycle hooks is enhanced to describe their purpose. No control flow logic is altered.

Changes

Cohort / File(s) Summary
Documentation Enhancement
packages/core/src/lifecycle.ts
Updated JSDoc for triggerSnapshotWillSerialize() to clarify it as a general-purpose "clean up non-serializable resources" hook, with examples and a note that there is currently no runtime caller under node --build-snapshot.
Snapshot Utilities Removal
packages/egg/src/lib/snapshot.ts, packages/egg/src/index.ts
Deleted entire V8 startup snapshot implementation including buildSnapshot() and restoreSnapshot() async functions, internal SnapshotData interface, and global __egg_snapshot_app declaration. Removed re-export of snapshot utilities from package root entrypoint.
Test Update
packages/egg/test/snapshot.test.ts
Removed restoreSnapshot() test case assertion and added clarifying comment that snapshot lifecycle hooks are tested under normal Node.js execution, not node --build-snapshot.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Suggested reviewers

  • jerryliang64

Poem

🐰 Snapshot utilities hop away,
Cleaned up code saves the day!
Lifecycle docs grow crystal clear,
Legacy V8 magic disappears,
Simpler paths for all to cheer! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: removing non-functional buildSnapshot/restoreSnapshot APIs that never worked under Node's --build-snapshot environment, which is the core objective of this refactoring PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/remove-nonworking-snapshot-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: c4a609c
Status: ✅  Deploy successful!
Preview URL: https://99dd8fb7.egg-cci.pages.dev
Branch Preview URL: https://fix-remove-nonworking-snapsh.egg-cci.pages.dev

View logs

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR removes the buildSnapshot() / restoreSnapshot() public APIs from packages/egg because they cannot function under a real node --build-snapshot (mksnapshot) environment, while keeping the core snapshot lifecycle hooks as a forward-compatible abstraction.

Changes:

  • Deleted packages/egg/src/lib/snapshot.ts and stopped exporting snapshot facade APIs from packages/egg/src/index.ts.
  • Removed restoreSnapshot test coverage and updated the public-export snapshot to reflect the removed exports.
  • Expanded the Lifecycle.triggerSnapshotWillSerialize() doc comment to clarify intended usage and current limitations under --build-snapshot.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/egg/test/snapshot.test.ts Removes restoreSnapshot-specific tests and clarifies that remaining tests only cover lifecycle hooks in normal Node runtime.
packages/egg/test/snapshots/index.test.ts.snap Updates export surface snapshot to remove buildSnapshot / restoreSnapshot.
packages/egg/src/lib/snapshot.ts Removes the non-working snapshot facade implementation entirely.
packages/egg/src/index.ts Stops re-exporting the removed snapshot utilities.
packages/core/src/lifecycle.ts Updates documentation for snapshot lifecycle hook intent and current lack of a --build-snapshot driver.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.04%. Comparing base (09222b7) to head (c4a609c).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5861      +/-   ##
==========================================
+ Coverage   86.00%   86.04%   +0.04%     
==========================================
  Files         667      666       -1     
  Lines       18945    18928      -17     
  Branches     3652     3648       -4     
==========================================
- Hits        16294    16287       -7     
+ Misses       2297     2289       -8     
+ Partials      354      352       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: c4a609c
Status: ✅  Deploy successful!
Preview URL: https://f7675eb4.egg-v3.pages.dev
Branch Preview URL: https://fix-remove-nonworking-snapsh.egg-v3.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/egg/test/snapshot.test.ts (1)

80-184: Well-structured lifecycle hook tests.

The tests comprehensively verify resource cleanup on serialize and restoration on deserialize for messenger, loggers, and the unhandledRejection handler. The use of process.nextTick() to wait for async lifecycle hooks is a standard Node.js testing pattern.

Consider adding a test case for the error path where triggerSnapshotWillSerialize() is called without snapshot: true. Based on the lifecycle implementation (which throws in this case per context snippet 1), verifying this guard would improve coverage:

it('should throw when calling triggerSnapshotWillSerialize without snapshot mode', async () => {
  const app = new Application({
    baseDir: demoApp,
    mode: 'single',
    // snapshot: false (default)
  });
  await app.ready();

  await assert.rejects(
    () => app.triggerSnapshotWillSerialize(),
    /can only be called on a snapshot-mode lifecycle/
  );

  await app.close();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/egg/test/snapshot.test.ts` around lines 80 - 184, Add a test that
verifies Application.triggerSnapshotWillSerialize throws when the app is not
started in snapshot mode: create an Application without snapshot: true, await
app.ready(), then use assert.rejects(() => app.triggerSnapshotWillSerialize(),
/can only be called on a snapshot-mode lifecycle/) to assert the error, and
finally await app.close() to clean up; reference the Application constructor and
the triggerSnapshotWillSerialize method to locate where to add the case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/egg/test/snapshot.test.ts`:
- Around line 80-184: Add a test that verifies
Application.triggerSnapshotWillSerialize throws when the app is not started in
snapshot mode: create an Application without snapshot: true, await app.ready(),
then use assert.rejects(() => app.triggerSnapshotWillSerialize(), /can only be
called on a snapshot-mode lifecycle/) to assert the error, and finally await
app.close() to clean up; reference the Application constructor and the
triggerSnapshotWillSerialize method to locate where to add the case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56b517f6-5432-4aa0-a348-8e6a7a31b21c

📥 Commits

Reviewing files that changed from the base of the PR and between 09222b7 and c4a609c.

⛔ Files ignored due to path filters (1)
  • packages/egg/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • packages/core/src/lifecycle.ts
  • packages/egg/src/index.ts
  • packages/egg/src/lib/snapshot.ts
  • packages/egg/test/snapshot.test.ts
💤 Files with no reviewable changes (2)
  • packages/egg/src/index.ts
  • packages/egg/src/lib/snapshot.ts

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request removes the buildSnapshot and restoreSnapshot utility functions from the egg package. These facades were removed because they are incompatible with Node 22's mksnapshot constraints, specifically regarding blocked userland require() calls and async context issues. The underlying lifecycle hooks, such as snapshotWillSerialize, are retained as stable abstractions for potential future serialization mechanisms. Corresponding tests and exports have been updated to reflect these removals. I have no feedback to provide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants