feat(sync-service): hibernate before suspend to enable GC#4284
feat(sync-service): hibernate before suspend to enable GC#4284
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4284 +/- ##
==========================================
+ Coverage 54.87% 58.82% +3.95%
==========================================
Files 193 218 +25
Lines 19567 22112 +2545
Branches 5062 5704 +642
==========================================
+ Hits 10737 13008 +2271
- Misses 8828 9100 +272
- Partials 2 4 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Claude Code ReviewSummaryIteration 2 review. The author addressed every actionable item from iteration 1 (changeset added, docstring/comment updates, What's Working Well (Carry-overs and New)
Issues FoundCritical (Must Fix)None. Important (Should Fix)1. Activity does not actually cancel the pending suspend timer when it arrives in the last File: Self-correction from iteration 1: My previous review described a Issue: Every activity handler (e.g.
Concrete bug zone with current defaults ( Why it's not Critical: Suspended consumers are restartable on the next request, so this is an unnecessary tear-down/rebuild rather than data loss or a crash. But it directly contradicts the PR description ("Any activity cancels the pending suspend timer, restarting the cycle") and the docstring on Why the new test passes anyway: Suggested fix (least invasive): bump the generation from any handler that resets # Reply/no-reply with the standard hibernate-after timeout, marking any
# pending suspend timer as stale.
defp reply_with_hibernate(state, reply) do
state = invalidate_suspend_timer(state)
{:reply, reply, state, state.hibernate_after}
end
defp noreply_with_hibernate(state) do
state = invalidate_suspend_timer(state)
{:noreply, state, state.hibernate_after}
end
defp invalidate_suspend_timer(%{suspend_generation: gen} = state) do
%{state | suspend_generation: gen + 1}
endThen route the activity handlers through these helpers. (If you'd rather keep call sites explicit, just bump Alternative: actually cancel the pending Erlang timer ( 2. File: def handle_info({:configure_suspend, hibernate_after, suspend_after, jitter_period}, state) do
state = %{state | hibernate_after: hibernate_after, suspend_after: suspend_after}
{:noreply, state, Enum.random(hibernate_after..jitter_period)}
endSame root cause as #1. If suspend was already enabled and a timer is pending (gen=1), then someone calls Suggestions (Nice to Have)1. Add an explicit assertion in the existing test that the suspend timer was actually cancelled, not just delayed File: The test currently waits 160ms past the original suspend window and then 2. File: Bumping the generation on every activity will eventually wrap a BigNum… in many years. Not worth a code change, but a one-liner comment that the integer is unbounded by design and Erlang handles arbitrary integers natively will save a future reader the worry. Issue ConformanceNo linked issue (carry-over from iteration 1). PR description and code comments now claim "Any activity cancels the pending suspend timer, restarting the cycle"; once Issue #1 is fixed, that claim will actually be true. Until then, the description is more aspirational than accurate. Previous Review Status
The two iteration-1 items that praised non-existent helpers ( Review iteration: 2 | 2026-05-07 |
Add new config option to control the delay between hibernation and suspension. Default is 60 seconds. This prepares for hibernate-then-suspend behavior where consumers first hibernate (to trigger GC) before suspending. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…tate Add state fields to track the scheduled suspend timer and the configured delay between hibernation and suspension. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When suspend is enabled, consumers now: 1. Hibernate first on timeout (triggering GC) 2. Schedule a suspend timer for suspend_after ms later 3. Suspend (terminate) when the suspend timer fires Any activity cancels the pending suspend timer, restarting the cycle. This ensures GC runs before eventual process termination. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change enable_suspend/3 to enable_suspend/4, adding the suspend_after parameter to configure the delay between hibernation and suspension. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add tests verifying: - Consumer hibernates first, then suspends after suspend_after ms - Activity during hibernation cancels the pending suspend timer - Update enable_suspend test for new 4-arity function - Update existing suspend test to use explicit suspend_after tag Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Clarify enable_suspend/4 docstring: jitter controls hibernation timing, suspension happens suspend_after ms after hibernation - Update :configure_suspend handler comment to describe hibernate-then-suspend - Add defensive guard for suspend_after: nil in schedule_suspend_timer/1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…n test With hibernate-then-suspend, consumers hibernate first then wait suspend_after before terminating. Set ELECTRIC_SHAPE_SUSPEND_AFTER=200ms alongside ELECTRIC_SHAPE_HIBERNATE_AFTER=200ms so the test completes within the expected timeframe. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3d7ade0 to
6029612
Compare
Summary
When consumer suspend is enabled, consumers now hibernate first (triggering GC) before eventually suspending (terminating). Previously, consumers went directly to suspend without hibernating, and due to a large timeout for suspend, consumer processes could hold onto garbage memory for longer than necessary.
shape_suspend_afterconfig (default 60s) - delay between hibernation and suspensionhibernate_aftertimeout, schedules suspend timersuspend_afterms, terminates process if still idleConsumerRegistry.enable_suspend/4to include suspend_after parameterTest Plan
🤖 Generated with Claude Code