Skip to content

fix(mail): replace os.Exit with graceful shutdown in mail watch#350

Merged
chanthuang merged 4 commits intomainfrom
fix/mail-watch-graceful-shutdown
Apr 9, 2026
Merged

fix(mail): replace os.Exit with graceful shutdown in mail watch#350
chanthuang merged 4 commits intomainfrom
fix/mail-watch-graceful-shutdown

Conversation

@chanthuang
Copy link
Copy Markdown
Collaborator

@chanthuang chanthuang commented Apr 8, 2026

Summary

  • Remove os.Exit(0) from the signal handler in mail +watch and replace with context-based graceful shutdown
  • Run cli.Start in a separate goroutine to avoid blocking on signal receipt (the Lark WebSocket SDK does not return promptly after context cancellation)
  • Extract handleMailWatchSignal as a testable standalone function
  • Use sync.Once + defer for idempotent cleanup on all exit paths
  • Fix eventCount data race with atomic.Int64
  • Add signal.Reset to support forced termination via a second Ctrl+C

Context

Three existing PRs (#269, #273, #311) independently addressed the os.Exit issue but each had gaps. Through local verification we found that cli.Start(watchCtx) does not return after context cancellation, so it must run in a goroutine — #269 and #273 both call it synchronously and hang on Ctrl+C. This PR combines the best approaches: goroutine-based cli.Start from #311, sync.Once + defer unified cleanup and signal.Reset from #269, and testable extracted function with tests from #311.

Closes #268

Test plan

  • go test -race ./shortcuts/mail/... — all pass, no race warnings
  • make unit-test — full suite pass, no regressions
  • Local verification: Ctrl+C exits immediately with defers executed (confirmed via defer probe)
  • Local verification: no duplicate "Unsubscribing" log output
  • Local verification: second Ctrl+C force-kills if graceful shutdown is stuck

Summary by CodeRabbit

  • Bug Fixes

    • More reliable graceful shutdown on interruption, accurately reporting processed event counts.
    • Ensures cleanup/unsubscribe runs once with a single logged outcome and clearer unsubscribe error reporting.
    • Avoids spurious network error reports when a watch is cancelled during shutdown.
  • Tests

    • Added unit tests covering shutdown sequencing, unsubscribe warnings, ordered cleanup callbacks, and behavior when unsubscribe logic panics.

The signal handler in mail +watch called os.Exit(0), which bypassed all
deferred cleanup functions, made the code path untestable, and did not
follow Go's idiomatic context cancellation pattern.

Key changes:
- Remove os.Exit(0) and use context.WithCancel to propagate shutdown
- Run cli.Start in a separate goroutine so the main goroutine can return
  immediately on signal receipt (the Lark WebSocket SDK does not return
  promptly after context cancellation)
- Extract handleMailWatchSignal as a testable standalone function
- Use sync.Once + defer for idempotent cleanup on all exit paths
- Fix eventCount data race with atomic.Int64
- Add signal.Reset to support forced termination via a second Ctrl+C

Closes #268
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions github-actions bot added domain/mail PR touches the mail domain size/M Single-domain feat or fix with limited business impact labels Apr 8, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f889898-5867-4800-aa31-d96334589ae8

📥 Commits

Reviewing files that changed from the base of the PR and between d2b1e42 and c1cb75e.

📒 Files selected for processing (2)
  • shortcuts/mail/mail_watch.go
  • shortcuts/mail/mail_watch_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • shortcuts/mail/mail_watch_test.go
  • shortcuts/mail/mail_watch.go

📝 Walkthrough

Walkthrough

Replace immediate process exit on signal with context-driven graceful shutdown: add handleMailWatchSignal, watchCtx/cancelWatch, change eventCount to atomic.Int64, add unsubscribeWithLog() guarded by sync.Once, run cli.Start in a goroutine and coordinate shutdown via channel/context.

Changes

Cohort / File(s) Summary
Mail watch runtime & signal handling
shortcuts/mail/mail_watch.go
Centralized shutdown in handleMailWatchSignal(); introduced watchCtx/cancelWatch; replaced eventCount with atomic.Int64; added unsubscribeWithLog() guarded by sync.Once and deferred unconditionally; moved cli.Start into a goroutine and coordinated shutdown via channel/context instead of os.Exit.
Unit tests for shutdown behavior
shortcuts/mail/mail_watch_test.go
Added four unit tests for handleMailWatchSignal() covering unsubscribe/stop/cancel invocation, unsubscribe warnings, panic-in-unsubscribe recovery, and strict callback ordering; added test-only imports (fmt, io, os, sync, time).

Sequence Diagram(s)

sequenceDiagram
    participant OS as "OS (SIGINT/SIGTERM)"
    participant Sig as "Signal Handler"
    participant Main as "Main Goroutine"
    participant CLI as "cli.Start (goroutine)"
    participant Mail as "Mailbox / unsubscribe"

    OS->>Sig: send signal
    Sig->>Sig: handleMailWatchSignal(signal)
    Sig->>Mail: unsubscribeWithLog()  -- once
    Sig->>Main: notify shutdownBySignal (chan)
    Sig->>Main: cancel(watchCtx)
    Main->>CLI: wait (select) for CLI error or shutdown
    CLI-->>Main: returns error (if any)
    Main-->>OS: exit after graceful shutdown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • infeng

Poem

🐰 I nibbled code with gentle paws tonight,
No sudden exits — just a soft goodnight,
Atoms tally hops, Once ensures just one,
Unsubscribe whispers, then the cancel is done,
The watch bows out calm under moonlight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing os.Exit with graceful shutdown in the mail watch command.
Description check ✅ Passed The description comprehensively covers all required sections: Summary, Changes, Test Plan, and Related Issues with detailed verification steps.
Linked Issues check ✅ Passed All coding requirements from issue #268 are met: os.Exit removed, context-based shutdown implemented, deferred cleanup with sync.Once, signal.Reset for forced termination, and shutdown testability via extracted handleMailWatchSignal function.
Out of Scope Changes check ✅ Passed All changes are within scope of issue #268: graceful shutdown in mail watch, context-based coordination, testable signal handling, and data race fix with atomic.Int64 are all directly related to the objective.

✏️ 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/mail-watch-graceful-shutdown

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@c1cb75e0bf87ab28a26658ca10fb8bbce5a3c7c2

🧩 Skill update

npx skills add larksuite/cli#fix/mail-watch-graceful-shutdown -y -g

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 8, 2026

Greptile Summary

This PR replaces os.Exit(0) in the mail +watch signal handler with a robust context-based graceful shutdown. The core mechanics — running cli.Start in a goroutine backed by a buffered startErrCh, using sync.Once-guarded triggerShutdown to close shutdownBySignal, and recovering panics by calling triggerShutdown() (not just cancelWatch()) — all appear correct. The previous thread concern about the panic path hanging the process is addressed: triggerShutdown() closes shutdownBySignal directly, so the main select unblocks immediately without waiting for the WebSocket SDK to return.

Confidence Score: 5/5

Safe to merge — graceful shutdown is correctly implemented and previous panic-path concerns are fully resolved.

All previously raised P0/P1 concerns are addressed: the panic-recovery path now calls triggerShutdown() (closes shutdownBySignal directly) rather than only cancelWatch(), so the main select unblocks immediately even when the WebSocket SDK hangs. The eventCount data race is fixed with atomic.Int64, sync.Once guards prevent duplicate unsubscribe logs, and the startErrCh buffered channel ensures no goroutine leak. No new P0/P1 issues found in this review pass.

No files require special attention.

Vulnerabilities

No security concerns identified. The PR does not introduce new input handling, authentication changes, or secret exposure. The existing prompt-injection detection and TOCTOU mitigations for --output-dir are unaffected.

Important Files Changed

Filename Overview
shortcuts/mail/mail_watch.go Replaces os.Exit with context-based graceful shutdown using sync.Once, triggerShutdown, goroutine-based cli.Start, and atomic.Int64 for eventCount; panic recovery now correctly closes shutdownBySignal.
shortcuts/mail/mail_watch_test.go Adds unit tests for handleMailWatchSignal covering callback ordering, unsubscribe-failure reporting, and panic-recovery-unblocks-shutdown; existing tests unchanged.

Sequence Diagram

sequenceDiagram
    participant Main as Execute goroutine
    participant SigG as Signal goroutine
    participant StartG as cli.Start goroutine

    Main->>StartG: go cli.Start(watchCtx)
    Main->>SigG: go signal handler goroutine
    Main->>Main: select on shutdownBySignal or startErrCh

    SigG->>SigG: receive SIGINT via sigCh
    SigG->>SigG: handleMailWatchSignal()
    Note over SigG: stopSignals() + signal.Reset()
    Note over SigG: unsubscribeWithLog() via sync.Once
    Note over SigG: cancelWatch() cancels watchCtx
    SigG->>Main: triggerShutdown() closes shutdownBySignal
    Main-->>Main: select fires on shutdownBySignal, return nil

    Note over StartG: cli.Start still running (SDK does not exit promptly)
    StartG-->>StartG: eventually returns, writes to buffered startErrCh

    alt Panic inside handleMailWatchSignal
        SigG->>SigG: recover() calls triggerShutdown()
        SigG->>Main: shutdownBySignal closed via sync.Once
        Main-->>Main: select fires on shutdownBySignal, return nil
    end
Loading

Reviews (4): Last reviewed commit: "fix(mail): use triggerShutdown to unbloc..." | Re-trigger Greptile

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 9, 2026

Tip:

Greploop — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.

Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

If handleMailWatchSignal panics, the recover block now calls
cancelWatch() to unblock the main select. Without this, a panic
would leave shutdownBySignal unclosed and watchCtx uncancelled,
causing the process to hang.
…er panic

The previous panic recovery only called cancelWatch(), but since the
WebSocket SDK does not return promptly after context cancellation,
the main select could still hang waiting on startErrCh.

Introduce triggerShutdown() that closes shutdownBySignal (via
sync.Once) and cancels the watch context, used by both the normal
signal path and the panic recovery path. This ensures the main
select unblocks immediately regardless of how the signal goroutine
exits.

Add regression test that forces a panic and asserts shutdownBySignal
is closed promptly.
Copy link
Copy Markdown
Collaborator

@infeng infeng left a comment

Choose a reason for hiding this comment

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

LGTM. No issues found.

Review baseline: base main, head fix/mail-watch-graceful-shutdown.

@chanthuang chanthuang merged commit c16a021 into main Apr 9, 2026
15 of 16 checks passed
@chanthuang chanthuang deleted the fix/mail-watch-graceful-shutdown branch April 9, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/mail PR touches the mail domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: remove os.Exit from mail watch signal handler

3 participants