Repository navigation
Add nonblocking activity admission control - #74
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Positive sub-millisecond deferrals are truncated to zero by SQLite, allowing an older item to be fetched repeatedly and starve ready activities without reaching the poison limit.
1 open issue
What changed in this PR
Adds nonblocking activity admission control to Duroxide, allowing activities to reserve resources or defer execution without holding worker slots or recording activity results.
Changes:
- Introduces admission decisions and registry callbacks for resource reservation.
- Integrates durable deferral and handler-lifetime resource guards into worker execution.
- Documents admission semantics and adds regression tests for restart recovery, rejection, and guard cleanup.
| File | Description |
|---|---|
tests/activity_admission_tests.rs |
Tests deferral, restart recovery, rejection, and guard cleanup. |
src/runtime/registry.rs |
Adds activity registration with admission callbacks. |
src/runtime/mod.rs |
Defines admission decisions and the handler admission hook. |
src/runtime/dispatchers/worker.rs |
Handles admission, durable deferral, and reservation lifetime. |
docs/ORCHESTRATION-GUIDE.md |
Explains admission behavior, constraints, and semaphore usage. |
Review effort: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
a4cc556 to
de0cafa
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Oversized defer delays can overflow SQLite timestamp conversion, making activities immediately eligible for repeated refetching without consuming their attempt budget.
1 open issue
1 resolved since last review
Review effort: Balanced
- Deferred work now yields to already-ready work in both SQLite and PostgreSQL. - Activity metadata avoids full-history reads for current executions and remains execution-specific. - Admission delays are clamped to 1 ms–24 hours. - Failed deferrals retry with the same delay and attempt-restoration flag. - Corrected start logging, added a committed-deferral metric, and updated documentation/changelogs. - Added Debug and #[non_exhaustive]. Typed registration remains deferred as agreed.
Add register_typed_with_admission with borrowed typed admission input, owned typed handler input, and the existing typed codec. Preserve deferral and guard-lifetime semantics, and validate decoding, encoding, and registry errors. Ref: #74 (typed admission) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expose admission deferrals through a MetricsProvider accessor instead of adding a public snapshot field. Keep downstream struct literals and exhaustive patterns source-compatible, with an integration-test compile guard. Ref: #74 (74-N1) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Install idx_worker_ready_order through the file-backed migration path as well as direct schema creation. Verify fresh databases and upgrades record the migration, create the expected index columns, and preserve queued work. Ref: #74 (74-N2) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep Defer as the explicit signal for temporary resource shortages. Admission errors, panics, and zero delays intentionally produce non-retryable application failures rather than automatic worker retries, which could repeat invalid input or faulty configuration indefinitely. Verify the exact durable ActivityFailed classification for all three cases. Ref: #74 (74-S6) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instrument lightweight and execution-history metadata reads with shared latency and error metrics. Log metadata failures separately from acknowledgments and deferrals, and remove the unreachable successful infrastructure-error outcome. Cover ordinary and admission-controlled activities, transient recovery, persistent failures, actual metric labels and values. Document metadata failure behavior and per-attempt history fallback costs, add admission deferrals to both telemetry specifications, and correct the remaining visibility-order example. Ref: #74 (74-S4, 74-N3, 74-N4) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Admission failures are intentionally terminal. Temporary capacity shortages should return Defer; Err represents permanent rejection, while panic and zero delay indicate faulty admission logic. These produce a durable ActivityFailed application result with retryable: false, rather than automatic worker retries that could repeat invalid input or broken configuration indefinitely. The orchestration can handle the failure and choose recovery. |
Explain orchestration retry helpers rescheduling admission failures despite retryable: false, and show how to floor computed delays that reach zero. Add a compiled ten-line typed semaphore example, enumerate the public admission APIs and failure behavior in the changelog, and align the deferral metric wording with observed abandon success. Ref: #74 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Approving at 112dcf0 on behalf of Affan Dar (@affandar), after his review. Posted by the Waldemort agent.
Admission control, the deferral path, and the new SQLite worker fetch order look good, and the earlier review findings are fixed or documented.
Verified on the SQLite provider: the new validation test test_worker_deferred_item_does_not_starve_ready_work fails 3 of 3 on main with only the test added, and passes 3 of 3 at 112dcf0. Reverting either fetch-order change (src/providers/sqlite.rs:1849 session, :1870 plain) makes it fail again.
Follow-ups:
- Docs: activities that can wait forever. See the inline comment on the admission section.
- SQLite orchestration fetch order: filed as #77 (low priority, for consistency with PostgreSQL).
| leave room for recovery attempts. This is a fetch-attempt budget, not a count of | ||
| handler starts: an activity can become poison without ever invoking its handler. | ||
| Failures loading activity metadata before admission also consume attempts. | ||
| Deferral is not a retry of an executed activity. |
There was a problem hiding this comment.
Please document what follows from this: an admission callback that never admits keeps the activity pending forever. Each committed deferral restores the attempt, so max_attempts is never reached, and the orchestration gets no result and no error. Correct-looking code can do this: a permit that leaks (a guard moved into a detached task, or a handler that hangs while holding it), a limit of 0, or a circuit breaker that never closes.
This is the second case in an existing category. The first is filtering: when no running worker can pick an item up, for example because no worker's TagFilter matches its tag (line 613), the item is never fetched and also waits forever.
Suggest documenting both in one place, with the same remedy:
- Wait on such activities with a timeout:
select2against a timer, orRetryPolicy::with_timeout. When the timeout wins, the losing activity is cancelled and its queued work item is deleted, which also ends a deferral loop. - Alert when
duroxide_activity_admission_deferrals_totalkeeps rising for one activity over a long period.

Summary
Add opt-in, nonblocking resource admission for activities, motivated by HTTP concurrency limits and policy enforcement in pg_durable. Activities that cannot acquire capacity can remain durably queued instead of occupying worker slots while waiting.
register_with_admission()andregister_typed_with_admission()for untyped and typed activity registration.Admit(guard)retains a resource reservation for the handler task's lifetime, releasing it on success, error, panic, or cancellation.Defer(delay)releases the worker slot while preserving the pending activity, without recording an activity result. A successfully committed deferral restores its fetch attempt.Admission errors, panics, and zero delays intentionally produce a durable
ActivityFailedapplication result withretryable: false, without invoking the handler.Deferis the signal for temporary backpressure; automatically retrying faulty admission logic could repeat invalid input or broken configuration indefinitely. The orchestration can catch the failure and choose recovery.Existing registration APIs remain available, and activities without an admission callback admit by default. Regression tests cover typed codecs, deferral and restart recovery, guard cleanup, terminal rejection, delay bounds, backlog progress, metadata failures, public API compatibility, and file-backed database upgrades.
Checklist
cargo nt)cargo test --doc --all-features)docs/if introducing a new area — N/A; existing guides extended.docs/README.md— N/A; this file is not present.Links / Design notes
unknownmetadata. This applies to ordinary and admission-controlled activities.Latest targeted validation
cargo check --all-targets --all-features— passedcargo fmt --all -- --check— passed