First successful crates.io release of the split workspace crates
(capp-config, capp-queue, capp-cache, capp-router, capp-urls)
alongside capp. No functional changes since 0.7.1 — the 0.7.x publishes
failed because the sub-crates had never been pushed to the registry and
the publish workflow's dry-run loop couldn't resolve them; the workflow
now publishes the workspace in dependency order.
FjallRoundRobinTaskQueue::pop()no longer evicts a tag from the rotation the moment its sub-queue transiently empties. It now scans forward from the cursor, skipping empty tags without removing them, and only advances the cursor past a tag it actually serves. Previously an evicted tag only rejoined at the tail of the rotation on its nextpush(), which starved tags whose backlog repeatedly grazed zero under concurrent consumption (e.g.concurrency_limitclose to the number of distinct tags) in favor of tags with deeper, steadier backlogs.QueueEmptyis now returned only after a full lap of all known tags finds no queued work, matching the otherTaskQueuebackends' contract.
Major architecture change: the mailbox worker pool is gone. The runtime
is now one dispatcher task driving one tower service; concurrency,
rate-limiting, retry, etc. all live in the caller's ServiceBuilder
chain. Stock tower middleware composes without Clone shims because
there is exactly one service instance.
- Breaking:
MailboxConfiglostworker_countandprefetch_per_worker. Add.concurrency_limit(N)to yourServiceBuilderchain instead. Remaining fields:producer_buffer,result_buffer,max_retries,dequeue_backoff,stop_when_idle. - Breaking:
MailboxService<D, Ctx>is nowBoxService<ServiceRequest<D, Ctx>, (), BoxError>(wasBoxCloneService). Services no longer need to beClone. - Breaking:
build_service_stackdrops theClonebound on the inner service; signature otherwise unchanged. - Breaking:
ServiceRequestlost theworker_idfield. - Breaking:
StatsSnapshotis now flat —processed,succeeded,failed,last_latency,in_flight(new),terminal_failures. The per-workerHashMap<usize, WorkerMetrics>is gone, along withWorkerMetrics. The internalStatsEvent::Workervariant was replaced byCall { success, latency }+InFlight(u64). - Breaking:
Envelope<D>type removed (was only used between the dispatcher and the removed worker pool). - Breaking: OpenTelemetry metrics no longer carry a
worker_idattribute — they're emitted as plain counters/histogram. examples/hackernews/main.rsreplaces the inlinetokio::sleep(POLITE_DELAY)with a towerconcurrency_limit(2).rate_limit(2, 1s)chain.examples/httpbin_tower.rsdrops theBuffer+BoxCloneServiceshim that was previously needed to make the rate-limited serviceClone.
MailboxConfig::stop_when_idle: bool— auto-stop the dispatcher once the queue is empty and all in-flight futures have completed. Suitable for finite crawls; existing tests cover the spawn-future drain semantics.StatsSnapshot::in_flight: u64— live gauge of dispatched-but-not- yet-completed futures.- New in-tree tests:
concurrency_limit_caps_in_flight,stop_when_idle_with_spawned_futures,service_without_clone_compiles.
async-channeldependency. The shared MPMC channel that fed envelopes to the worker pool is gone with the pool.
// before
MailboxConfig {
worker_count: 4,
prefetch_per_worker: 1,
producer_buffer: 128,
result_buffer: 128,
max_retries: 2,
dequeue_backoff: Duration::from_millis(50),
stop_when_idle: false,
}
// after
let inner = ServiceBuilder::new()
.concurrency_limit(4) // ← where worker_count went
.service(my_service);
let service = build_service_stack(inner, ServiceStackOptions::default());
MailboxConfig {
producer_buffer: 128,
result_buffer: 128,
max_retries: 2,
dequeue_backoff: Duration::from_millis(50),
stop_when_idle: false,
}If you logged req.worker_id inside service handlers, drop the field.
If you read StatsSnapshot.workers, read the flat counters instead.
- Internal
capp-testkitworkspace crate for reusable local HTTP fixtures used by tests and examples. - Hyper/Tokio-backed local fixture server with request logging and
/statsreporting. - Built-in local fixture scenarios:
blog,blog_with_posts(n),catalog,infinite. local_blog_crawlmailbox example that crawls a generated local 100-post blog and validates coverage against fixture stats.TaskQueue::recover_inflighttrait method (default returnsOk(0)) for reclaiming tasks owned by a previous, crashed process at startup.FjallTaskQueueandInMemoryTaskQueuenow track aninflightset:popmoves the task there, andack/nack/pushclear it.
- Reintroduced a realistic crawler example path using the mailbox runtime and a local in-process site instead of only external-service demos.
- Expanded test coverage around
capp-testkit, healthcheck timeout/failure behavior, and queue serializer success/error paths. healthcheck::internetno longer panics on reqwest errors and no longer depends on a404 + content_length == 9magic check. New contract: returnstrueiff any HTTP response is received within 1 second; any error or timeout yieldsfalse.- Breaking:
TaskId::from_stringnow returnsResult<Self, uuid::Error>instead of panicking on malformed input.TaskIdalso implementsFromStrso callers cans.parse::<TaskId>(). - Breaking:
MailboxConfig.inbox_capacityrenamed toprefetch_per_worker. The mailbox runtime now uses a single sharedasync-channelMPMC channel of capacityprefetch_per_worker * worker_countinstead of N per-workermpscinboxes. Eliminates head-of-line blocking when one worker is slow. (Note: the worker pool was removed entirely in 0.7.0.) - Breaking:
WorkerResult::Returnvariant removed. Workers no longer produce "return" results; on shutdown the dispatcher closes the shared channel and any tasks not picked up remain in the queue'sinflightset for the next process to recover. - Breaking:
ServiceStackOptionsreduced to a single field (timeout: Option<Duration>). Thebufferandconcurrency_limitfields were removed. - Breaking:
build_service_stackno longer addsConcurrencyLimitLayer,.load_shed(), or.buffer()to the service stack — only.timeout()remains.MailboxConfig.worker_countis now the sole concurrency knob; the previous defaults (concurrency_limit = Some(1)+buffer = 1) silently serialized the worker pool through a shared semaphore and a redundant mpsc. Callers that need a per-resource sub-cap should compose their ownServiceBuilderchain before passing the service in. - Breaking:
StatsSnapshot.queue_depthfield,StatsEvent::QueueDepthvariant, and the OTelcapp_queue_depthobservable gauge were removed. Nothing was sampling queue depth, so the metric was always zero. Real depth observability needsTaskQueue::len()+ periodic sampling and is not part of this release. - Mailbox dispatcher gives
ControlCommandpriority over other events viabiased;selection. handle_worker_result::Ackno longer writes the task before acking; ack removes the row.
- Crashed-mid-flight tasks on the Fjall backend are no longer lost. On
startup,
spawn_mailbox_runtimecallsrecover_inflightto move any leased-but-unacked rows back to the queue.