Skip to content

feat(meteor): add Meteor.shutdown() lifecycle hook#14434

Open
dupontbertrand wants to merge 3 commits into
meteor:develfrom
dupontbertrand:feature/meteor-shutdown
Open

feat(meteor): add Meteor.shutdown() lifecycle hook#14434
dupontbertrand wants to merge 3 commits into
meteor:develfrom
dupontbertrand:feature/meteor-shutdown

Conversation

@dupontbertrand
Copy link
Copy Markdown
Contributor

@dupontbertrand dupontbertrand commented May 27, 2026

Summary

This PR adds a server-side Meteor.shutdown(fn) lifecycle hook, intended as the shutdown counterpart to Meteor.startup(fn).

Registered hooks are called on SIGTERM and SIGINT before the process exits, allowing applications to flush logs, drain queues, release locks, close sockets, or finish pending async cleanup before the supervisor escalates to SIGKILL.

Opened as a draft because a few API and semantics details should be confirmed with the core team before landing:

  • API name: Meteor.shutdown(fn) vs Meteor.onShutdown(fn)
  • Hook ordering: LIFO vs FIFO
  • Timeout behavior
  • Whether this should later cover additional shutdown paths such as SIGHUP, Node 'exit', or dev-mode wrapper shutdown

Forum context: https://forums.meteor.com/t/is-there-something-like-meteor-shutdown/64602

Why

Meteor exposes Meteor.startup(fn) for the boot side of the lifecycle, but there's no symmetric API for shutdown. Today, applications that need to clean up on termination attach raw process.on('SIGTERM', …) handlers themselves, and the details that matter — per-hook timeout, error containment, ordering, single-point-of-exit — have to be reimplemented in each app.

This PR codifies the missing half of the lifecycle so applications can rely on a consistent, framework-level shutdown hook.

Use cases

Patterns this enables:

  • Drain in-flight DB writes / transactions before the driver connection closes.
  • Job-queue drain — finish persisting pending jobs before stopping the consumer, so redeploys don't lose work.
  • Telemetry / logger flushpino, winston, Sentry, OpenTelemetry exporters all have async flush methods that need to complete before exit.
  • Distributed lock release — Redis / etcd locks held by the process released proactively rather than waiting for TTL.

What changed

Two commits, +89 lines total, no removed code, no existing content modified:

Commit 1 — feat(meteor): add Meteor.shutdown() lifecycle hook (+56)

  • tools/static-assets/server/boot.js (+39) — shutdownHooks: [] bucket, callShutdownHooks(signal) runner, SIGTERM / SIGINT listeners
  • packages/meteor/startup_server.js (+18) — public Meteor.shutdown(fn) API

Commit 2 — docs(meteor): document Meteor.shutdown() (+33)

  • packages/meteor/startup_server.js (+5) — JSDoc @summary block
  • docs/source/api/core.md (+28) — apibox entry + prose, placed right after Meteor.startup

Usage

import { Meteor } from 'meteor/meteor';

Meteor.shutdown(async (signal) => {
  console.log(`Shutting down on ${signal}, flushing pending writes…`);
  await jobQueue.flush();
  await mongoClient.close();
});

Hooks receive the triggering signal name ('SIGTERM' or 'SIGINT') as their argument.

Design choices

Decision Choice Rationale
Hook ordering LIFO Setup/teardown symmetry — resources initialised later often depend on resources initialised earlier, so they should usually close first
Concurrency Sequential (for…await) Deterministic; allows flushing DB writes before closing the driver
Hook throws Best-effort (log + continue) One bad hook shouldn't prevent the rest from releasing resources
Timeout Hard cap via METEOR_SHUTDOWN_TIMEOUT_MS (default 10000ms) Supervisors (Galaxy / K8s / systemd) escalate to SIGKILL — exit well before
Late registration Warn + run via microtask Mirrors Meteor.startup's "execute immediately when bootstrap is done" path

Exit codes follow POSIX: SIGINT → 130, SIGTERM → 143.

LIFO in practice — if startup is db.connect()queue.start()socket.listen() (each depending on the previous), shutdown should run socket.close()queue.drain()db.close(). FIFO would close the DB before the queue had finished writing, producing MongoError: Topology closed.

Known limitations / out of scope

This PR does not refactor existing shutdown-related listeners such as packages/webapp/socket_file.js, and it does not integrate the dev-mode parent watchdog (startCheckForLiveParent, boot.js:180) with the new hook runner. In dev mode, hooks taking longer than ~3s can therefore be pre-empted by the watchdog process.exit(1); in production builds (no METEOR_PARENT_PID) the METEOR_SHUTDOWN_TIMEOUT_MS cap is the sole guard. If the API direction is accepted, both can be handled in follow-up PRs.

Validation

Manual validation against a fresh meteor create --blaze app. I kept this PR scope-tight without test changes, but I'm happy to add a Tinytest suite on this branch if reviewers prefer the tests to land with the API.

Scenario Result
Baseline LIFO + sequential await ✅ C → B (300ms await respected) → A
Best-effort error handling ✅ Throwing hook logs, subsequent hooks still run
Hard timeout (METEOR_SHUTDOWN_TIMEOUT_MS=2000) ✅ Exits at T+2001ms, code 143
Late registration ✅ Warns and runs via microtask before process.exit
SIGINT + SIGTERM idempotence ✅ Second signal is no-op while shutdown in flight
POSIX exit codes SIGINT → 130, SIGTERM → 143

Open questions

  1. API nameMeteor.shutdown(fn) (symmetric with Meteor.startup) vs Meteor.onShutdown(fn) (avoids verb-form "shut down the server now" ambiguity).
  2. Hook ordering — LIFO (chosen here, teardown symmetry) vs FIFO (consistency with Meteor.startup).
  3. Default timeout — 10s reasonable, or should it be configurable per-hook (Meteor.shutdown(fn, { timeout: 5000 }))?
  4. Additional signals — should Meteor.shutdown also fire on SIGHUP and Node's 'exit' event? Currently only SIGINT / SIGTERM. Tied to the socket_file.js follow-up.

Summary by CodeRabbit

  • New Features

    • Added a server-only shutdown API to register graceful shutdown handlers.
    • Handlers run in LIFO order, support async cleanup, and have best-effort error handling.
    • Shutdown enforces a configurable timeout (default ~10s) and maps exit codes to the triggering signal.
  • Documentation

    • Added a guide with usage examples for registering and running shutdown hooks.

Symmetric to Meteor.startup(), Meteor.shutdown(fn) registers a callback
fired on SIGTERM / SIGINT before process exit. Useful for closing DB
connections, flushing queues, releasing locks, etc.

Hooks run in LIFO order sequentially with await. Per-hook errors are
logged but do not abort subsequent hooks (best-effort cleanup). Total
runtime is capped by METEOR_SHUTDOWN_TIMEOUT_MS (default 10000ms) to
avoid stalling supervisor escalation (Galaxy, K8s, systemd) to SIGKILL.

Exit codes follow POSIX: SIGINT -> 130, SIGTERM -> 143.

Forum thread: https://forums.meteor.com/t/is-there-something-like-meteor-shutdown/64602
Adds the JSDoc @summary block consumed by the apibox tag plus an entry
in docs/source/api/core.md, placed right after Meteor.startup since
the two are conceptually paired.

The prose covers: LIFO ordering with sequential await, best-effort
error handling, hard timeout via METEOR_SHUTDOWN_TIMEOUT_MS, POSIX
exit codes, server-only locus.
@netlify
Copy link
Copy Markdown

netlify Bot commented May 27, 2026

Deploy Preview for v3-meteor-api-docs ready!

Name Link
🔨 Latest commit 7d808fa
🔍 Latest deploy log https://app.netlify.com/projects/v3-meteor-api-docs/deploys/6a1ea6253cb13200083feb82
😎 Deploy Preview https://deploy-preview-14434.docs-online.meteor.com
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 27, 2026

Deploy Preview for v3-migration-docs ready!

Name Link
🔨 Latest commit 7d808fa
🔍 Latest deploy log https://app.netlify.com/projects/v3-migration-docs/deploys/6a1ea62507549100084bc67e
😎 Deploy Preview https://deploy-preview-14434.v3-docs.meteor.com
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e7f4533-0700-414c-b188-5005d2972d29

📥 Commits

Reviewing files that changed from the base of the PR and between cfed879 and 7d808fa.

📒 Files selected for processing (1)
  • tools/static-assets/server/boot.js
💤 Files with no reviewable changes (1)
  • tools/static-assets/server/boot.js

📝 Walkthrough

Walkthrough

This PR adds a graceful server shutdown API for Meteor. It implements a Meteor.shutdown(callback) function that registers cleanup handlers, executes them in reverse order on process termination signals (SIGTERM/SIGINT), enforces a configurable timeout, logs errors, and exits with appropriate POSIX codes.

Changes

Server Shutdown

Layer / File(s) Summary
Shutdown infrastructure and bootstrap initialization
tools/static-assets/server/boot.js
__meteor_bootstrap__ gains a shutdownHooks array; core shutdown logic adds callShutdownHooks(signal) that executes registered hooks LIFO with timing/error handling, enforces timeout via METEOR_SHUTDOWN_TIMEOUT_MS (default 10s), computes POSIX exit codes, and registers process listeners for SIGTERM and SIGINT.
Public Meteor.shutdown API
packages/meteor/startup_server.js
Exports Meteor.shutdown(callback) to register shutdown handlers into the bootstrap hooks array; warns and runs callback immediately via microtask if shutdown has already begun or bootstrap is unavailable, with error logging.
Shutdown API documentation
docs/source/api/core.md
Documents sequential reverse-order hook execution, async hook support, error handling with best-effort cleanup, configurable timeout behavior, signal-to-exit-code mapping, server-only availability, and example showing job queue flushing and Mongo client closure.

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Suggested reviewers:
    • nachocodoner
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: adding a new Meteor.shutdown() lifecycle hook for server-side shutdown handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

@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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/static-assets/server/boot.js`:
- Around line 473-477: The safety timeout created by setTimeout (the variable
timer) must not be unreferenced because timer.unref() can allow the event loop
to empty and prevent the timeout from firing; remove the call to timer.unref()
so the timeout reliably keeps the process alive until either clearTimeout(timer)
is called or it fires and calls process.exit(exitCode), leaving the existing
clearTimeout(timer) and the timeoutMs/exitCode behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a069aa43-2e0e-4e97-b82d-7ff730a7a183

📥 Commits

Reviewing files that changed from the base of the PR and between 76f4402 and cfed879.

📒 Files selected for processing (3)
  • docs/source/api/core.md
  • packages/meteor/startup_server.js
  • tools/static-assets/server/boot.js

Comment thread tools/static-assets/server/boot.js Outdated
An unref()-ed timer does not keep the event loop alive. If a shutdown
hook hangs on a promise with no other pending I/O (e.g. after earlier
hooks have closed the server's sockets and Mongo connection), the loop
empties and the process exits 0 before the timeout fires, defeating the
documented METEOR_SHUTDOWN_TIMEOUT_MS hard cap. clearTimeout() already
prevents the timer from delaying the fast path, so unref() only weakened
the guarantee.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant