Account syncs were launched independently from two places (the Mail.Sync RPC handler and the daemon's CoreCommand consumer), each running an account's sync to completion before the next. One slow/hung account head-of-line-blocked the rest, and nothing bounded concurrency or per-account duration.
What this does
Introduces nunciod::sync_dispatcher::SyncDispatcher — the single place every production account sync now funnels through:
Bounded concurrency: a tokio::sync::Semaphore caps simultaneous account syncs. Env-tunable via NUNCIO_SYNC_CONCURRENCY (default 4); excess requests queue on the permit.
Per-account in-flight coalescing: a second request for an account already syncing joins the in-flight run (via a per-account watch channel) instead of double-fetching, and observes the same outcome.
Per-account timeout: exactly one place bounds each sync via tokio::time::timeout. Env-tunable via NUNCIO_SYNC_TIMEOUT seconds (default 300). A timed-out sync is cancelled cleanly and surfaces a typed SyncDispatchError::Timeout without poisoning the dispatcher.
Shutdown integration (WS-C1): the dispatcher holds the shared ShutdownSignal and races each in-flight sync against it (biased select!, mirroring WS-C2's outbox executor). Once shutdown is requested it admits no new work and abandons in-flight syncs for the next start, so a hung account can't delay graceful shutdown.
The actual per-account work is injected behind an AccountSyncer trait; production wires ProductionAccountSyncer (delegates to sync::run_account_sync). main.rs builds one dispatcher and shares it between the gRPC Mail.Sync handler and the CoreCommand sync consumer, so both origins share one cap/coalescing-map/timeout. An all-accounts sync now fans every configured mail account out through the same per-account path via sync_all_configured (replacing the old serial run_all_accounts_sync), which is the head-of-line fix. The injected-test-backend Sync seam is unchanged (a single in-memory double, deliberately bypassing the dispatcher).
cargo test --workspace — 542 passed; 0 failed; 0 ignored.
No unsafe/unwrap/expect/panic/todo in non-test code. SSH-signed commit. No proto/golden change.
Closes #292 (WS-C4). No proto change.
## Problem
Account syncs were launched independently from two places (the `Mail.Sync` RPC handler and the daemon's `CoreCommand` consumer), each running an account's sync to completion before the next. One slow/hung account head-of-line-blocked the rest, and nothing bounded concurrency or per-account duration.
## What this does
Introduces `nunciod::sync_dispatcher::SyncDispatcher` — the single place every production account sync now funnels through:
- **Bounded concurrency:** a `tokio::sync::Semaphore` caps simultaneous account syncs. Env-tunable via `NUNCIO_SYNC_CONCURRENCY` (default **4**); excess requests queue on the permit.
- **Per-account in-flight coalescing:** a second request for an account already syncing **joins** the in-flight run (via a per-account `watch` channel) instead of double-fetching, and observes the same outcome.
- **Per-account timeout:** exactly one place bounds each sync via `tokio::time::timeout`. Env-tunable via `NUNCIO_SYNC_TIMEOUT` seconds (default **300**). A timed-out sync is cancelled cleanly and surfaces a typed `SyncDispatchError::Timeout` without poisoning the dispatcher.
- **Shutdown integration (WS-C1):** the dispatcher holds the shared `ShutdownSignal` and races each in-flight sync against it (biased `select!`, mirroring WS-C2's outbox executor). Once shutdown is requested it admits no new work and abandons in-flight syncs for the next start, so a hung account can't delay graceful shutdown.
The actual per-account work is injected behind an `AccountSyncer` trait; production wires `ProductionAccountSyncer` (delegates to `sync::run_account_sync`). `main.rs` builds **one** dispatcher and shares it between the gRPC `Mail.Sync` handler and the `CoreCommand` sync consumer, so both origins share one cap/coalescing-map/timeout. An all-accounts sync now fans every configured mail account out through the same per-account path via `sync_all_configured` (replacing the old serial `run_all_accounts_sync`), which is the head-of-line fix. The injected-test-backend `Sync` seam is unchanged (a single in-memory double, deliberately bypassing the dispatcher).
## Tests (all deterministic, offline, mock backends)
New `sync_dispatcher` unit tests:
- `bounds_simultaneous_syncs_to_the_configured_cap` — counting mock proves peak concurrency == cap.
- `coalesces_a_duplicate_in_flight_sync_for_the_same_account` — underlying sync runs exactly once for concurrent same-account requests.
- `a_hung_account_does_not_block_other_accounts` — **head-of-line regression:** account A blocks forever, account B still completes.
- `a_hung_account_times_out_without_poisoning_the_dispatcher` — typed `Timeout`, then a later healthy sync still works.
- `surfaces_the_underlying_sync_error_typed`.
- `rejects_new_work_once_shutdown_is_requested` / `shutdown_unblocks_an_in_flight_sync_promptly`.
- `sync_all_configured_dispatches_every_mail_account_and_skips_dav`.
Existing `Mail.Sync` gRPC E2E and the spine E2E stay green (client Sync still works end to end).
## Gate (three commands, all green, 0 ignored)
- `cargo fmt --all -- --check` — clean.
- `cargo clippy --all-targets --workspace -- -D warnings` — clean.
- `cargo test --workspace` — **542 passed; 0 failed; 0 ignored**.
No `unsafe`/`unwrap`/`expect`/`panic`/`todo` in non-test code. SSH-signed commit. No proto/golden change.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #292 (WS-C4). No proto change.
Problem
Account syncs were launched independently from two places (the
Mail.SyncRPC handler and the daemon'sCoreCommandconsumer), each running an account's sync to completion before the next. One slow/hung account head-of-line-blocked the rest, and nothing bounded concurrency or per-account duration.What this does
Introduces
nunciod::sync_dispatcher::SyncDispatcher— the single place every production account sync now funnels through:tokio::sync::Semaphorecaps simultaneous account syncs. Env-tunable viaNUNCIO_SYNC_CONCURRENCY(default 4); excess requests queue on the permit.watchchannel) instead of double-fetching, and observes the same outcome.tokio::time::timeout. Env-tunable viaNUNCIO_SYNC_TIMEOUTseconds (default 300). A timed-out sync is cancelled cleanly and surfaces a typedSyncDispatchError::Timeoutwithout poisoning the dispatcher.ShutdownSignaland races each in-flight sync against it (biasedselect!, mirroring WS-C2's outbox executor). Once shutdown is requested it admits no new work and abandons in-flight syncs for the next start, so a hung account can't delay graceful shutdown.The actual per-account work is injected behind an
AccountSyncertrait; production wiresProductionAccountSyncer(delegates tosync::run_account_sync).main.rsbuilds one dispatcher and shares it between the gRPCMail.Synchandler and theCoreCommandsync consumer, so both origins share one cap/coalescing-map/timeout. An all-accounts sync now fans every configured mail account out through the same per-account path viasync_all_configured(replacing the old serialrun_all_accounts_sync), which is the head-of-line fix. The injected-test-backendSyncseam is unchanged (a single in-memory double, deliberately bypassing the dispatcher).Tests (all deterministic, offline, mock backends)
New
sync_dispatcherunit tests:bounds_simultaneous_syncs_to_the_configured_cap— counting mock proves peak concurrency == cap.coalesces_a_duplicate_in_flight_sync_for_the_same_account— underlying sync runs exactly once for concurrent same-account requests.a_hung_account_does_not_block_other_accounts— head-of-line regression: account A blocks forever, account B still completes.a_hung_account_times_out_without_poisoning_the_dispatcher— typedTimeout, then a later healthy sync still works.surfaces_the_underlying_sync_error_typed.rejects_new_work_once_shutdown_is_requested/shutdown_unblocks_an_in_flight_sync_promptly.sync_all_configured_dispatches_every_mail_account_and_skips_dav.Existing
Mail.SyncgRPC E2E and the spine E2E stay green (client Sync still works end to end).Gate (three commands, all green, 0 ignored)
cargo fmt --all -- --check— clean.cargo clippy --all-targets --workspace -- -D warnings— clean.cargo test --workspace— 542 passed; 0 failed; 0 ignored.No
unsafe/unwrap/expect/panic/todoin non-test code. SSH-signed commit. No proto/golden change.