Repository navigation
Conversation
Design for an opt-in mode in which one controller per process divides a process-wide concurrency target among its workers in proportion to queue depth, grows the target under demand with CPU and post-GC heap headroom, shrinks it under pressure, and reserves capacity before every poll. Uses the existing batch poll, update, update-v2 and queue sizes APIs; no server protocol change.
Size a process-wide slot pool from CPU, post-GC heap and queue backlog, and share it across task queues by weight. A queue is polled only when it holds an open slot. Queues whose share is fully held keep one single-task standby poll open on unclaimed slots, so idle pickup stays close to fixed-thread latency. Heap growth uses the highest post-GC reading of the last 30 seconds, and the control tick runs on a fixed-delay timer so an OutOfMemoryError cannot stop it. Configurable through AdaptiveConcurrencyConfig and the Spring client properties.
Mixed-workload and functional scenarios (weights, rotation, starvation, scaling, warm start, heap, cpu) against a Conductor server started with the Conductor CLI, with workers as host JVMs.
A failure after completePoll (metadata stamping, event publish) dropped the delivered batch with its slots held forever; a throw from lease scheduling mid-dispatch leaked the rest of the batch the same way. Settle now wraps only the network call, post-settle bookkeeping cannot drop tasks, and dispatch releases the slot on any submit failure while lease-scheduling rejection only logs. Also removes the latent double-settle when completePoll itself threw. Docs: add the missing controller state (depthAtEmptyPoll, nextStandbyAt, standbyPolls, pollableAtSample, heapGrowthCeiling, heap baseline and 30s readings window) and the pause-entry growth ceiling to the pseudocode; document the bounded one-second reservation wait; mark the design as implemented. Harness: add ControllerOverheadMain, a controller-overhead scenario (hundreds of idle queues plus a governed busy load) probing lock acquisition latency and per-thread-group CPU; measured p99 15us at 305 queues. Main class is overridable via -PharnessMain.
Three silent-degradation paths become visible or impossible: - A stalled GC notification pipeline no longer freezes the heap estimate: when collections keep advancing without a notification the sampler falls back to collection usage until they resume. - Standby polls now also require a free process slot; unclaimed slots are computed over registered queues and could overstate capacity while a deregistered worker's tasks drain. - Slot counters are guarded: a release bug that would drive them negative (inflating deficits and over-admitting) is clamped and reported instead of compounding. New observability: adaptive_worker_task_slot_age_seconds, seconds since a worker last released a task slot while holding any — the signal that separates a wedged worker from a busy one.
This branch has not been deployed
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.
Pull Request type
Changes in this PR
Adaptive worker concurrency — an opt-in mode where workers size themselves instead of using fixed thread counts.
Today you guess a
threadCountper worker and hope it's right: too low leaves backlog with an idle machine, too high risks OOM or CPU saturation. In adaptive mode, one controller per process continuously answers three questions:GET /tasks/queue/sizesfor all queues) and the process's capacity is divided among workers in proportion to their backlog — a queue with 600 waiting tasks gets ~6x the slots of one with 100.Also: idle queues hold no threads but stay alive via standby long-polls (instant pickup) or 5s heartbeats (keeps the server's 10s active-poller window for domain routing); empty polls back off from the worker's configured polling interval up to 5s; an idle process decays back to minimum capacity.
Usage:
Spring:
conductor.client.adaptive-concurrency.enabled=true; per-worker ceiling viaconductor.worker.<task>.adaptive_max_concurrency.Included: controller + config + JVM resource sampler, TaskRunner integration (fixed mode unchanged), Spring/annotated-worker wiring, new
AdaptiveConcurrencyChangedevent + Prometheus gauges (includingadaptive_worker_task_slot_age_seconds, the wedged-worker signal), design doc (design/adaptive-worker-concurrency.md), unit tests, e2e suites, and a harness overhead scenario (controller lock p99 was 15µs with 305 queues).Self-monitoring hardenings: a stalled GC-notification pipeline falls back to collection usage instead of freezing the heap estimate; standby polls require a free process slot; slot counters that would go negative are clamped and reported.
Measured (local server): 30 min at 8 wf/s — adaptive completed 98.7% of 14,400 workflows self-adjusting between 17 and 250 slots with zero client errors and flat memory; fixed 10-threads/worker completed 7% (13,331 of 14,400 workflows still queued or mid-pipeline at cutoff) with a linearly growing backlog, using 1.8x the poll requests. Full curves in the harness logs.
Behavior notes: disabled by default — fixed mode is untouched. In adaptive mode N pollers for a task share one pool (poller count no longer multiplies capacity).
withTaskPollCountis deprecated. Virtual threads remain opt-in and use a thread-per-task executor.Issue #
Alternatives considered