Skip to content

fix: add top-level recover guards to callback hook chain#199

Open
bruceding wants to merge 2 commits into
masterfrom
fix/callback-hook-recover-guards
Open

fix: add top-level recover guards to callback hook chain#199
bruceding wants to merge 2 commits into
masterfrom
fix/callback-hook-recover-guards

Conversation

@bruceding

Copy link
Copy Markdown
Collaborator

Summary

Two production-impacting goroutine paths in the callback hook chain had no top-level recover, so any downstream panic — nil deref, type assertion, broken user-registered CallBackProcessFunc, etc. — would crash the entire recommendation server.

  • service/user_recommend.go:173 and service/user_recall_service.go:43 dispatch hooks via raw go hf(...).
  • web/callback_controller_handler.go worker pool runs controller.doCallbackLog() without a guard. A panic both terminates the process and permanently shrinks the worker pool.

This PR localizes failure to the hook/worker level:

  • New hook.SafeRun(hf, ctx, params...) helper wraps a RecommendCleanHookFunc with defer recover + stack-trace log. Both hook-dispatch sites now use it.
  • New runCallbackSafely(c) in the web worker pool provides the same guard for doCallbackLog.
  • Log format mirrors existing project convention (requestId=...\tmodule=...\terror=...\tstack=..., see service/recall.go:135).

Test plan

  • go test ./service/hook/... — 5 new unit tests covering panic recovery, nil context, params pass-through, normal return, goroutine isolation
  • go test ./web/... — added a deterministic injected-panic test via RegisterCallBackProcessFunc, plus a defensive nil-controller test
  • go vet ./service/... ./web/... — clean
  • go build ./... — clean
  • Pre-existing failing test service/recall.TestMultibizRecall confirmed to fail on origin/master before this change (depends on external BE data source) — unrelated to this PR

Code review

A pre-merge code review was performed. The review concluded "可以合并但建议改进" with two Important items, both of which were addressed in the same commit:

  1. Removed dead var _ = context.NewRecommendContext placeholder import in the test file.
  2. Refactored TestRunCallbackSafely_RecoversFromPanic to inject a panic via RegisterCallBackProcessFunc so the panic path is deterministic, instead of relying on a chance nil-deref inside doCallbackLog.

Two Suggestion-level items are recorded as follow-ups in doc/code_reviews/2026-06-01-callback-hook-recover.md:

  • Same file has two more unprotected goroutines (feature_log.FeatureLog, LogSampleResult) that deserve the same protection in a separate PR.
  • RecommendCleanHookFunc still uses params ...interface{}; could be unified to any project-wide later.

🤖 Generated with Claude Code

bruceding and others added 2 commits June 1, 2026 22:06
Two production-impacting goroutine paths in the callback hook chain had no
recover, so any downstream panic (nil deref, type assertion, broken user-
registered CallBackProcessFunc) would crash the whole recommendation server:

1. Hook dispatch in service/user_recommend.go:173 and
   service/user_recall_service.go:43:
       go hf(context, user, items)
2. Worker pool in web/callback_controller_handler.go:
       go func() { for c := range ch { c.doCallbackLog() } }()

Changes:
- service/hook/recommend.go: add SafeRun helper that wraps a
  RecommendCleanHookFunc with deferred recover + stack-trace log,
  preserving the existing dispatch semantics.
- service/user_recommend.go, service/user_recall_service.go: dispatch
  hooks via hook.SafeRun.
- web/callback_controller_handler.go: route worker invocations through
  runCallbackSafely, which provides the same guard for doCallbackLog.

Tests:
- service/hook/recommend_test.go: cover panic recovery, nil context,
  param pass-through, normal return, and goroutine isolation.
- web/callback_controller_handler_test.go: add a deterministic
  injected-panic test using RegisterCallBackProcessFunc, plus a nil
  controller defensive check.

doc/code_reviews/2026-06-01-callback-hook-recover.md records the review
that motivated the test refactor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Production worker goroutines read callBackProcessFuncMap on every callback
request (web/callback_controller.go:159) while user code may still register
new scenes at startup, so the map needs synchronization to be race-free.
Without it, `go test -race` would fail once any test (or production caller)
exercised both sides concurrently.

Changes:
- Add sync.RWMutex protecting callBackProcessFuncMap; expose
  lookupCallBackProcessFunc (RLock) and unregisterCallBackProcessFunc
  (Lock, test-only) helpers. Keep RegisterCallBackProcessFunc as the public
  entry, now locked.
- Switch the dispatch site in callback_controller.go to use the helper.
- Update TestRunCallbackSafely_RecoversFromInjectedPanic cleanup to call
  unregisterCallBackProcessFunc so tests no longer poke the global directly.
- Add TestCallBackProcessFuncMap_ConcurrentAccess: 32 goroutines x 200
  register/lookup/unregister ops to surface any future regression under
  `go test -race`.

Also drop doc/code_reviews/2026-06-01-callback-hook-recover.md (review C5);
self-review notes belong on the PR description, not on master.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM 👍

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM 👍

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.

1 participant