Skip to content

run(): a failed polling pump leaves the process alive and silent (#1351) - #1360

Merged
yagop merged 7 commits into
masterfrom
issue-1351
Sep 6, 2026
Merged

run(): a failed polling pump leaves the process alive and silent (#1351)#1360
yagop merged 7 commits into
masterfrom
issue-1351

Conversation

@yagop

@yagop yagop commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #1351

Suggestion #1 (log the fatal poll-stop to stderr before rethrowing) already landed in #1358. This PR adds the remaining scope.

  • T1 run(): add opt-in exitOnError (default false) - on a fatal poll-stop, after teardown + the stderr log, exit non-zero so a supervisor restarts the bot; new RunOptions type; unit test (stubbed process.exit)
  • T2 Update the examples that end in a bare await run(bot) to model failure handling (await run(bot, { exitOnError: true }))
  • T3 Regenerate doc/api.md for the new RunOptions / run signature
  • T4 RedisSessionStorage: add a url option so the store can own the client it opens and close() it on teardown (mirrors the sql/sqlite owned pattern) - closes the one teardown gap found while verifying bot.close() for run(): a failed polling pump leaves the process alive and silent #1351; unit test + docs
  • T5 Harden exitOnError (Codex + Copilot): gate the hard exit on owning the pump (a losing double-run must not kill the healthy process), run it in a nested finally so a throwing teardown can't skip it, and flush the stderr diagnostic before exiting; tests for teardown-throws and lost-race
  • T6 Cover the url-owned Redis client path via a protected createClient seam - construct/close the owned client, idempotent close, and use-after-close rejects (Copilot)

Michi-Task: r1ex
Michi-Issue: 1351
Michi-Task: r3dc
Michi-Issue: 1351
@yagop
yagop marked this pull request as ready for review September 6, 2026 21:21
@yagop

yagop commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

😺 Michi — all tasks done, CI green, marked ready for review.

Suggestion #1 (log the fatal poll-stop to stderr before rethrowing) landed earlier in #1358, so this PR covers the rest:

  • T1 (e6865ba): run() gains an opt-in exitOnError (new RunOptions type). On a fatal poll-stop it still logs to stderr and re-throws, and additionally - after teardown (bot.close()) - exits the process non-zero, so a supervisor (systemd/Docker/pm2) restarts a clean process instead of a live-but-silent one. Default false to preserve the re-throw contract and avoid a surprise process.exit (which would also kill test runners). Unit tests stub process.exit and cover the fatal-exit path and no-exit-on-clean-stop.
  • T2 (7015c20): the six examples that ended in a bare await run(bot) now use await run(bot, { exitOnError: true }), with a comment on the primary example explaining the supervisor-restart rationale (they're what people copy).
  • T3 (f8b31ea): regenerated doc/api.md for RunOptions / the run signature (also swept in a stale startWebhook docstring line longPoll: a 409 Conflict permanently stops the polling loop (#1350) #1358's regen had missed).

Note on the default: I chose exitOnError: false rather than the issue's tentative "default on?" - a library function that calls process.exit by default is surprising and would terminate test/embedding processes. The examples opt in, which is where the supervisor pattern belongs. Easy to flip to default-on if you'd prefer.

CI: all GitHub Actions + AppVeyor green; E2E skipped by design. Full local gate (npm run check, 237 tests) + typecheck:examples green.

Waiting for your merge. 🐾

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Exit handling has ownership, teardown-error, and stderr-flush gaps, while the new Redis ownership path lacks direct coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds managed failure exit behavior for polling and owned Redis client teardown.

Changes:

  • Adds RunOptions.exitOnError and updates polling examples.
  • Adds URL-owned Redis clients with teardown support.
  • Updates tests and generated API documentation.
File summaries
File Description
src/node/run.ts Adds optional process exit on polling failure.
src/bun/redis-storage.ts Adds URL-created, owned Redis clients.
test/unit/run.test.ts Tests polling exit behavior.
test/unit/bun/redis-storage.test.ts Tests injected-client ownership.
examples/01-polling-bot.ts Demonstrates exitOnError.
examples/06-keyboards.ts Enables exitOnError.
examples/09-middleware.ts Enables exitOnError.
examples/12-conversation.ts Enables exitOnError.
examples/16-sessions.ts Enables exitOnError.
examples/17-callback-tracking.ts Enables exitOnError.
doc/api.md Documents the updated public API.
Review details
  • Files reviewed: 11/11 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/node/run.ts Outdated
Comment on lines +59 to +61
if (owned) await bot.close();
// Teardown is done; a supervisor can now restart a clean process.
if (failed && exitOnError) process.exit(1);
Comment thread src/node/run.ts Outdated
} finally {
if (owned) await bot.close();
// Teardown is done; a supervisor can now restart a clean process.
if (failed && exitOnError) process.exit(1);
Comment thread src/bun/redis-storage.ts Outdated
Comment on lines +43 to +44
this.owned = options.client === undefined && options.url !== undefined;
this.client = options.client ?? (options.url !== undefined ? new RedisClient(options.url) : redis);
@yagop

yagop commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

😺 Michi — applied the Codex + Copilot review; CI green.

  • Exit skipped if teardown throws (Codex + Copilot): T5 (71cb9fb) moves the hard exit into a nested finally, so exitOnError fires even when bot.close() rejects - it can't be defeated by the very stuck resource it exists to escape.
  • Exit fired when this call doesn't own the pump (Copilot): T5 gates the exit on owned (via a failed flag only set on the owning path). A second run(bot, { exitOnError: true }) that loses the already-running race now just re-throws to its caller instead of process.exit-ing the healthy pump's process. The misleading "polling stopped" stderr line is also gated on ownership.
  • stderr truncated by process.exit (Copilot): T5 awaits the process.stderr.write flush callback before exiting, so the fatal diagnostic survives even when stderr is piped to a supervisor. Test stub updated to invoke the callback.
  • Redis owned-url path untested (Copilot): T6 (02a8366) adds a protected createClient() seam (excluded from public docs) so a fake owned client is testable without a live Redis; new test covers construct -> close (owned) -> idempotent -> use-after-close rejects. Added an open() guard so a reused closed owned store throws a clear "was closed" error (parity with the sql/sqlite stores).

New tests: teardown-throws-still-exits, lost-race-does-not-exit, and the full url-owned Redis lifecycle.

CI: all GitHub Actions + AppVeyor green; E2E skipped by design. Full local gate (npm run check, 241 tests) + typecheck:bun/test/examples green.

Waiting for your merge. 🐾

@yagop
yagop merged commit 35d8244 into master Sep 6, 2026
8 checks passed
@yagop

yagop commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

😺 Michi — merged, all done 🐾

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.

run(): a failed polling pump leaves the process alive and silent

2 participants