feat(pool): decay warm-process refill target after idle TTL - #182
Open
epicvinny wants to merge 8 commits into
Open
feat(pool): decay warm-process refill target after idle TTL#182epicvinny wants to merge 8 commits into
epicvinny wants to merge 8 commits into
Conversation
The warm pool ratchets its geometric refill target up after bursts of acquisitions and never decays it, so a single burst pins the excess warm processes (and their memory) for the lifetime of the server. Add PoolConfig::idle_ttl: when no acquisition happens within the TTL, the maintenance worker decays the fill target back to the low watermark and drains the excess entries. Acquisitions reset the idle clock. Wire it as [pool.firecracker] idle_ttl_secs (default 600, 0 disables decay), documented in config/default.toml and the configuration reference. The network-slot and ublk pools keep the historical ratchet behavior (idle_ttl: None): they hold no guest processes, so idle decay buys nothing there. Tests: idle_ttl_decays_fill_target_and_drains_to_low_watermark, maintenance_worker_wakes_on_idle_ttl_and_drains, acquisitions_reset_idle_ttl_clock (cargo test -p warm-pool: 16 ok).
Contributor
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 7 selected item(s). |
decay_fill_target_if_idle reset last_acquisition on expiry, so a single action computation consumed the decay event: if the drain cycle only partially succeeded (e.g. resource cleanup failed mid-cycle) or another caller computed the action first, the next computation fell back to the high watermark and retained the excess for another full TTL. Keep a persistent decaying flag instead: set on expiry, cleared by any acquisition, and cleared only once the pool reaches the decayed fill target, so the drain target stays pinned to the low watermark until the excess is actually drained. PoolConfig::validate now normalizes idle_ttl = Some(Duration::ZERO) to None, matching the documented external config semantics of 0 = never decay. A zero TTL made idle_ttl_remaining always return zero, so the maintenance worker scheduled back-to-back cycles (busy loop) even after reaching the target. Tests: widen TTL/sleep margins so loaded CI workers cannot cross the deadline (pre-expiry margin is now 800ms instead of 50ms), poll the maintenance-worker test with a 10s deadline instead of a fixed 300ms sleep, and cover the zero-TTL normalization and the non-consumable decay.
Author
|
One related finding was reported without an inline thread, addressed here for completeness: |
The decay decision was not an atomic snapshot: last_acquisition, decaying, and fill_target lived under separate locks, so a computation concurrent with an acquisition could observe the new acquisition timestamp together with the stale decaying flag and old fill target and return a low-watermark Drain for demand that had just resumed; the unconditional decaying clear could also erase a newer decay transition started by another caller. Move the three fields into a DemandState struct under a single mutex. compute_maintenance_action and record_acquisition each hold the lock for their full transition, so the decay lifecycle, fill target, and idle clock always move together. Tests: acquisition_clears_decay_state_before_next_computation covers the resumed-demand-mid-drain transition deterministically, and concurrent_acquisition_and_decay_keep_state_consistent races four threads of release/acquire/compute against a 5ms TTL, asserting the fill target stays within the watermarks.
Address remaining review threads on idle-TTL decay: - A computed Drain action could go stale before execution: an acquisition after the computation clears the decaying state via record_acquisition, but the maintenance cycle still drained the previously returned count toward the low watermark. Maintenance drains now claim each resource through WarmPool::try_drain_one_for_maintenance, which re-checks the drain decision under the demand-state and pool locks at execution time, so resumed demand actually cancels an in-progress decay. - Decay completion was decided from a pool length sampled separately from the demand state, so a concurrent release could strand resources above the decayed target for another full TTL. Completion is now marked only when try_drain_one_for_maintenance verifies, under lock, that the pool actually reached the decayed fill target. - TTL tests no longer rely on wall-clock sleeps: DemandState methods take an injected clock and tests rewind last_acquisition directly; only the maintenance-worker condvar test keeps a bounded polling deadline.
5 tasks
… under both locks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
PoolConfig::idle_ttlto thewarm-poolcrate: when no warm-process acquisition happens within the TTL, the maintenance worker decays the geometric refill target back to the low watermark and drains the excess warm entries. Wired as[pool.firecracker] idle_ttl_secs(default 600,0disables decay).Why
The pool's fill target only ever ratchets up on acquisitions, so a single burst pins the excess warm Firecracker processes, and the memory they hold, for the lifetime of the server. On bursty shared hosts (business-hours sandbox creation, idle overnight) this wastes memory with no recourse short of restarting the server.
Related issue
Closes #180
Scope and non-goals
warm-pool, config plumbing (idle_ttl_secs), docs (config/default.toml, configuration reference), unit tests.idle_ttl: Nonesince they hold no guest processes).Design and behavior changes
idle_ttl_secs = 0preserves the exact previous ratchet behavior.Compatibility and operations
[pool.firecracker] idle_ttl_secs(default 600). Old builds ignore the key. Operators wanting the historical behavior set0.Validation
make fmt(viacargo fmt --check)make clippy(viacargo clippy -p warm-pool)make test-unit(scoped:cargo test -p warm-pool, 16 passed, incl. 3 new)make -C services test(required whenservices/changes)maketargetCommands and results:
Skipped checks and reasons: integration tests require root and
/dev/kvm, not available on the Windows dev box where this was written. The behavior has also been validated on our staging fleet (GKE KVM nodes) via the fork build.Risks and reviewer notes
0.crates/warm-pool/src/lib.rs(TTL bookkeeping in the maintenance loop) andsrc/cfg.rs(config wiring).Checklist