[MOD-16610] Lazy spawn + logical shrink for shared SVS pool: unblock CONFIG SET WORKERS#989
Draft
dor-forer wants to merge 2 commits into
Draft
[MOD-16610] Lazy spawn + logical shrink for shared SVS pool: unblock CONFIG SET WORKERS#989dor-forer wants to merge 2 commits into
dor-forer wants to merge 2 commits into
Conversation
…CONFIG SET WORKERS
FT.CONFIG SET WORKERS N resizes the shared SVS thread pool synchronously on
the Redis main thread. Each slot eagerly constructed an svs::threads::Thread —
a serialized pthread_create plus a boot handshake that blocks until the worker
reaches its spinning state — and freshly booted threads burn a 1M-pause idle
spin with no work, oversubscribing the CPU so the handshakes stretch
superlinearly: 52.5 s at N=2000 (vs ~60 us on 8.6, which only toggled the
write mode). On Redis Enterprise this stalls the shard past CNM's 3 s socket
timeout. Shrinking a warmed pool had the symmetric cost: a shutdown handshake
plus join per spawned thread on the main thread (~3.9 s at N=2000).
Make both resize directions O(slots) on the main thread — never O(threads):
* Lazy spawn (grow): ThreadSlot holds std::optional<svs::threads::Thread>,
empty at creation. Resize only allocates slots; the OS thread is spawned by
the renter that wins the occupancy CAS (ensureThread), interleaved with
assign so the new thread's first spin window immediately catches its
partition. Spawn cost moves to the first job that uses the slot, on a
background thread.
* Logical shrink: physical slots are never destroyed. Shrink lowers
logical_size_ (tracked separately from capacity; grows/shrinks are
classified against it) through the existing deferred-resize protocol; rent()
only hands out slots below the limit. Excess threads park asleep (one final
idle-spin window, no periodic wakeup) and are reused warm if the pool grows
again. This mirrors the logical-only resize the per-index pools used before
the shared pool existed, minus its defect: capacity can still grow above the
high-water mark. Slots are heap-allocated and immortal, so renters' raw
ThreadSlot pointers are structurally safe.
* Thread reclaim ("GC"): reclaimExcessThreads() — noexcept, detach-under-lock /
join-outside-lock — joins parked threads above the logical limit. Triggered
only from the worker-executed completion path of a scheduled job (gated on
isScheduled saved before the job self-deletes), so the periodic SVS GC and
update jobs drive reclamation. Deliberately NOT in endScheduledJob(): that
also fires from JobsRegistry teardown of unexecuted jobs on the Redis main
thread, which must stay counter-only. Accepted gap: in write-in-place mode
(WORKERS 0) or after the last SVS index is dropped, parked threads persist
until async SVS activity resumes.
* parallel_for gains a unified degraded-execution path: a spawn failure
(std::system_error — thread exhaustion now happens at first use, mid-job),
an assign failure (crashed worker), or a rent shortfall all fall back to
running the missing partitions on the calling thread — partitions are never
dropped (previously a shortfall silently ran only partition 0 in release
builds). Crashed workers are retired to the unspawned state and lazily
respawn on the next rent.
* ExecuteMultiThreadJobImpl gets an RAII completion guard: a throwing task can
no longer leave reserve jobs blocked forever or wedge the pending-jobs
reservation that deferred shrink and reclaim depend on. Task failures are
logged with index context by the typed wrappers.
* VecSim_GetSharedMemory() contract change: slot capacity is a high-water mark
(~64 bytes/slot), so the reported allocation no longer drops on shrink;
sharedMemoryTracksThreadPoolResize is updated accordingly.
Measured on 14 cores (pool-level bench): grow to N=2000 drops from 52.5 s to
0.19 ms on the main thread; warmed shrink from ~3.9 s to ~1 us (a size-field
update). The joins (~3.1 s at N=2000 worst case) run on the job-completion
path off the main thread; the first big rent pays the spawn cost off the main
thread; reuse and warm regrow pay nothing.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #989 +/- ##
==========================================
- Coverage 97.12% 96.88% -0.25%
==========================================
Files 141 141
Lines 8164 8239 +75
==========================================
+ Hits 7929 7982 +53
- Misses 235 257 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
pthread_join returning does not guarantee the joined thread's /proc/self/task entry is gone — the kernel wakes the joiner before it releases the task entry, so a just-joined thread can linger in the count briefly (caught by the sanitizer job's slowdown). Post-reclaim OS-thread counts now poll with a deadline, and baselines are captured only after the count holds steady; spawn-side assertions stay exact (thread creation is synchronous).
c87ab5b to
7b401a9
Compare
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.
Describe the changes in the pull request
FT.CONFIG SET WORKERS Nresizes the shared SVS thread pool synchronously on the Redis main thread. Each slot eagerly constructed ansvs::threads::Thread— a serializedpthread_createplus a boot handshake that blocks until the worker reaches itsSpinningstate — and freshly booted threads burn a 1M-pause idle spin with no work, oversubscribing the CPU so the handshakes stretch superlinearly: 52.5 s at N=2000 (vs ~60 µs on 8.6, which only toggled the write mode). On Redis Enterprise this stalls the shard past CNM's 3 s socket timeout (PUT /v2/bdbsflakes). Shrinking a warmed pool had the symmetric cost: a shutdown handshake plus join per spawned thread on the main thread (~3.9 s at N=2000).This PR makes both resize directions O(slots) on the main thread — never O(threads):
ThreadSlotholdsstd::optional<svs::threads::Thread>, empty at creation. Resize only allocates slots; the OS thread is spawned by the renter that wins theoccupiedCAS, interleaved with work assignment and outsidepool_mutex_, so the fresh thread's first spin window immediately catches its partition.logical_size_(the rent limit) is tracked separately from physical capacity (a high-water mark). Shrink is a size-field write through the existing deferred-resize protocol; parked threads sleep (one final spin window, no periodic wakeup) and are reused warm on regrow.reclaimExcessThreads()(noexcept, detach-under-lock / join-outside) joins unoccupied slots above the logical limit. Triggered only from the worker-executed completion path of a scheduledSVSMultiThreadJob— never fromendScheduledJob, which also fires fromFT.DROPINDEXteardown on the main thread (counter-only there).std::system_error), assign failure, or rent shortfall stops dispatch and runs the remaining partitions on the calling thread — partitions are never dropped (this also fixes a pre-existing silent partition-drop on rent shortfall in release builds). Crashed workers are retired to the unspawned state and lazily respawn.ExecuteMultiThreadJobImpl: a throwing task can no longer wedge reserve jobs or thepending_jobs_counter (which deferred shrink and reclaim depend on). Typed task wrappers log failures with index context before rethrowing.Measurements (16-core Linux, N=2000 workers):
Semantics note:
VecSim_GetSharedMemory()now reports slot capacity at its high-water mark (~64 B/slot) — it no longer drops on shrink, since slots are retained for warm reuse. Known accepted gaps (documented in code): no reclaim trigger in write-in-place mode (WORKERS 0) or after the last SVS index is dropped, until async SVS activity resumes.Tests: lazy-spawn / park / warm-reuse / reclaim OS-thread-count tests in an isolated fixture; real-trigger integration tests for the reclaim path (executed scheduled job reclaims, unexecuted-job teardown does not); completion-guard exception test; crashed-worker retire/respawn; degraded-execution release-mode semantics; shared-memory high-water contract.
Which issues this PR fixes
Main objects this PR modified
ThreadSlot/VecSimSVSThreadPoolImpl(svs_utils.h) — lazy spawn, logical size vs. physical capacity,reclaimExcessThreads(), degraded-execution dispatchSVSMultiThreadJob::ExecuteMultiThreadJobImpl(svs_tiered.h) — RAII completion guard, reclaim trigger on the worker-executed completion pathMark if applicable
🤖 Generated with Claude Code
Note
Medium Risk
Changes core tiered SVS threading, deferred resize, and async job lifecycle; mis-ordering reclaim vs main-thread teardown could wedge workers or leak threads, though the PR explicitly gates joins to background completion paths.
Overview
CONFIG SET WORKERSno longer blocks the Redis main thread on masspthread_create/join during shared SVS pool resize (MOD-16610).VecSimSVSThreadPoolImplnow separates logical rent limit (logical_size_) from physical slot capacity (high-water). Resize only allocates slot structs; OS threads spawn on first rent viaThreadSlot::ensureThread(). Shrink lowers the logical limit and parks excess threads;reclaimExcessThreads()joins parked threads off the main thread.rent()only hands out slots below the logical limit.parallel_foruses degraded execution: spawn/assign/rent failures run remaining partitions on the caller instead of dropping work (release builds previously could run only partition 0). Crashed workers are retired withresetThreadAtfor lazy respawn.VecSim_GetSharedMemory()reflects slot high-water—it no longer drops on shrink.SVSMultiThreadJob::ExecuteMultiThreadJobImpladds an RAII completion guard so task exceptions still release reserve jobs, delete the job, and decrement scheduled-job state; exceptions are swallowed at the job boundary whileupdateSVSIndexWrapper/SVSIndexGCWrapperlog with index context then rethrow.reclaimExcessThreads()runs only after scheduled jobs complete on a worker—not fromendScheduledJob()(main-thread teardown).Tests cover lazy spawn, logical shrink/park/warm reuse, reclaim triggers, completion guard on failure, shared-memory contract, and degraded
parallel_forsemantics.Reviewed by Cursor Bugbot for commit 7b401a9. Bugbot is set up for automated code reviews on this repo. Configure here.