Description
Currently, all deferred tasks share the same execution pool with no way to limit how many instances of a specific task run concurrently. This can be problematic when tasks interact with rate-limited external services or shared resources that can't handle unbounded parallel access.
For example, consider a task that calls a third-party API with a rate limit of 10 requests per second. If 500 of these tasks are enqueued at once, they'll all compete for execution, overwhelming the external service and likely triggering rate-limit errors — which in turn causes retries, compounding the problem.
A per-task concurrency setting would let users declare the maximum number of concurrent executions for a given task class:
Example:
class SyncToExternalService
include Rage::Deferred::Task
max_concurrency 5
def perform(record_id:)
ExternalAPI.sync(Record.find(record_id))
end
end
With this configuration, no more than 5 instances of SyncToExternalService would run at the same time. Additional tasks would remain in the queue and execute as slots become available.
Design considerations
- Queueing behavior. When the concurrency limit is reached, excess tasks should stay in the queue rather than being dropped. The scheduler needs a mechanism to re-check limited tasks as running ones complete.
- Interaction with backpressure. The existing backpressure mechanism controls overall queue size. Concurrency limits are orthogonal — they constrain execution, not enqueuing. The two features should work independently without interfering.
- Scope of the limit. Since
Rage::Deferred runs in-process, the limit naturally applies per worker. Consider whether the API should make this explicit or whether cross-worker coordination is out of scope for an initial implementation.
- Counting active tasks. The implementation needs an efficient way to track how many instances of a task class are currently running. This counter must be updated reliably, including when tasks fail or are interrupted during shutdown.
Tips
- Review the Deferred docs to understand how task scheduling and execution work.
- Check the architecture doc to see how Rage's core components interact and to understand the design principles.
- Read the contributing guide for coding conventions and design principles used across the codebase.
- Before starting the implementation, please share your proposed design approach. Discussing the approach early will drastically increase the chances of acceptance and help avoid rework.
- Feel free to ask any questions or request help in the comments below!
Description
Currently, all deferred tasks share the same execution pool with no way to limit how many instances of a specific task run concurrently. This can be problematic when tasks interact with rate-limited external services or shared resources that can't handle unbounded parallel access.
For example, consider a task that calls a third-party API with a rate limit of 10 requests per second. If 500 of these tasks are enqueued at once, they'll all compete for execution, overwhelming the external service and likely triggering rate-limit errors — which in turn causes retries, compounding the problem.
A per-task concurrency setting would let users declare the maximum number of concurrent executions for a given task class:
Example:
With this configuration, no more than 5 instances of
SyncToExternalServicewould run at the same time. Additional tasks would remain in the queue and execute as slots become available.Design considerations
Rage::Deferredruns in-process, the limit naturally applies per worker. Consider whether the API should make this explicit or whether cross-worker coordination is out of scope for an initial implementation.Tips