{"id":50844608,"url":"https://github.com/godaddy/sharded-sink","last_synced_at":"2026-06-14T08:34:00.237Z","repository":{"id":363492965,"uuid":"1263560077","full_name":"godaddy/sharded-sink","owner":"godaddy","description":"A small, high-performance **fire-and-forget sink** for fan-in workloads.","archived":false,"fork":false,"pushed_at":"2026-06-09T05:40:32.000Z","size":66,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-09T07:20:07.653Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/godaddy.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-09T04:13:10.000Z","updated_at":"2026-06-09T05:39:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/godaddy/sharded-sink","commit_stats":null,"previous_names":["godaddy/sharded-sink"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/godaddy/sharded-sink","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/godaddy%2Fsharded-sink","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/godaddy%2Fsharded-sink/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/godaddy%2Fsharded-sink/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/godaddy%2Fsharded-sink/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/godaddy","download_url":"https://codeload.github.com/godaddy/sharded-sink/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/godaddy%2Fsharded-sink/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34315072,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-14T08:34:00.157Z","updated_at":"2026-06-14T08:34:00.228Z","avatar_url":"https://github.com/godaddy.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sharded-sink\n\nA small, high-performance **fire-and-forget sink** for fan-in workloads.\n\nMany async producers hand off items to a bounded, **sharded** set of\n`crossbeam_queue::ArrayQueue` rings (one per shard); one poll-drain worker per\nshard performs the actual sink action off the producer critical path. When a\nring is full the sink **sheds** the item and counts it rather than applying\nbackpressure.\n\nThe design value is **flat, predictable producer latency under high\ncontention**: a successful push is a single lock-free `ArrayQueue::push` to one\nshard — no await, no lock, no allocation, and no counter touched on success.\n\nThis crate is for telemetry, logging, counters, spend-record firehoses, and\nother loss-tolerant streams. **It is not a durable queue** — no at-least-once\ndelivery, no ordering across shards, no dynamic resharding. Items need only\n`T: Send + 'static`.\n\n```toml\n[dependencies]\nsharded-sink = \"0.1\"\n```\n\n## Quick start\n\n```rust\nuse std::sync::Arc;\nuse sharded_sink::{ShardedSink, SinkAction, SinkConfig};\n\n#[derive(Clone, Copy)]\nstruct Event { status: u16 }\n\nstruct PrintDrain;\nimpl SinkAction\u003cEvent\u003e for PrintDrain {\n    async fn drain(\u0026self, batch: \u0026mut Vec\u003cEvent\u003e) {\n        // Encode and ship `batch`; don't retain references past this point.\n        let _ = batch.len();\n    }\n}\n\n# async fn run() {\nlet sink = ShardedSink::spawn_default_overload(\n    SinkConfig::default().with_name(\"events\"),\n    Arc::new(PrintDrain),\n);\n\n// Hot producers reuse a handle (one per request / connection / producer object).\nlet handle = sink.issue();\nlet _accepted = handle.push(Event { status: 200 });\n\n// Caller quiesces producers first, then drains.\nsink.shutdown().await.expect(\"shutdown\");\n# }\n```\n\n## Why sharded crossbeam `ArrayQueue` (the design decision)\n\nThis crate originally specified `thingbuf::mpsc` as the per-shard ring. We\nimplemented the framework generically over the ring primitive and benchmarked\nthe **real framework** on each — identical handles, workers, and lifecycle,\nonly the ring swapped — measuring the cost of a **successful** push (the hot\npath) under many-producer contention. The numbers picked crossbeam decisively,\nso the crate ships only the sharded crossbeam design.\n\nPer-op cost in **per-thread CPU time** (`CLOCK_THREAD_CPUTIME_ID`, which\nexcludes OS scheduler preemption so it reflects real push cost under\noversubscription). Dev machine, `drop% = 0` (true accept path), median of 3\ntrials:\n\n| producers | sharded thingbuf | **sharded crossbeam** | unsharded crossbeam (1 shard) | single `ArrayQueue` | single `Mutex\u003cVec\u003e` |\n|---:|---:|---:|---:|---:|---:|\n| 1   | 5.6 ns | **4.7 ns** | 4.6 ns | 4.5 ns | 5.8 ns |\n| 4   | 5.4 ns | **4.4 ns** | 35 ns | 30 ns | 78 ns |\n| 16  | 395 ns | **18.3 ns** | 541 ns | 768 ns | 184 ns |\n| 64  | 700 ns | **10.8 ns** | 254 ns | 182 ns | 209 ns |\n| 128 | 1077 ns | **13.7 ns** | 220 ns | 208 ns | 187 ns |\n| 256 | 508 ns | **15.6 ns** | 202 ns | 190 ns | 188 ns |\n\nWhat this shows:\n\n- **Sharded crossbeam is the only option that stays flat** — ~4–18 ns from 1 to\n  256 producers (300–600 M pushes/sec).\n- **Sharded thingbuf collapses under contention** (395–1077 ns): every successful\n  `try_send` notifies the shard's single MPSC consumer wait-cell, and with\n  several producers per shard that one cache line bounces between cores.\n- **A single shared structure is ~20× slower** (`ArrayQueue` 190–768 ns,\n  `Mutex\u003cVec\u003e` ~190 ns) because every successful push hits one shared cursor\n  (the queue's tail-CAS, or the lock). Sharding splits that cursor across shards;\n  that *is* the win. (The reject/full path of a single queue is cheap — which is\n  why this had to be measured on the accept path, not under backpressure.)\n- **`Mutex\u003cVec\u003e` also has a catastrophic tail** under contention — p99.9 reached\n  the **milliseconds** (lock-holder preemption / convoy), vs microseconds for the\n  lock-free options.\n\n### Ordering trade-off\n\nSharding sacrifices global ordering: items are FIFO only *within* a shard, with\nno order across shards. If you need approximate global insertion order, set\n`shards: 1` — that's the \"unsharded crossbeam\" row above: one queue, global FIFO\n(the same ordering a `Mutex\u003cVec\u003e` gives, but lock-free and without the convoy\ntail), at ~200 ns/push under heavy contention. You cannot have both a single\nglobal order and flat-contention push — the global order *is* a single contended\ncursor by definition.\n\n## How it works\n\n- **Producer hot path** (`ShardHandle::push`): one immediate `ArrayQueue::push`\n  to one shard. On success it touches no counter, lock, timer, or metric. Only a\n  full-ring rejection increments one relaxed per-shard atomic.\n- **Shard selection** is deterministic, never random:\n  - `issue()` — global round-robin cursor; spreads handles across shards\n    regardless of issuing thread. Not the hot path.\n  - `issue_thread_local()` — stable home shard for the current thread.\n  - handle-less `push()` — uses `ShardSelection` (thread-local home or\n    round-robin).\n- **Drain workers poll** their home shard with `pop` and sleep `idle_sleep` when\n  empty; they never register a waker. After draining their own shard they may\n  steal a bounded number of items from other shards\n  (`WorkStealing::Opportunistic`). `SinkAction::drain` may await and do I/O; a\n  slow drain fills rings (and sheds) but never blocks producers.\n- **No closed state**: producers and the drain worker share each shard's queue,\n  which outlives the worker. A push after shutdown is accepted into the queue\n  (until full), never a \"closed\" drop.\n- **Shutdown is producer-quiesced**: stop accepting work and drop producer\n  handles first, then call `shutdown()`. It drains buffered items and joins\n  workers. Pushes racing with shutdown are explicitly outside the\n  graceful-delivery contract (a late push may sit undrained).\n\n## Benchmarks\n\nRun them on your own hardware — absolute numbers are machine-specific; trust the\nshape.\n\n```bash\ncargo bench --bench uncontended            # Criterion microbench, single producer\ncargo bench --bench contended              # accept-path harness, 1..256 producers\nSHARDED_SINK_BENCH_ITEMS=50000 cargo bench --bench contended\n```\n\n`uncontended` is a Criterion microbench (Criterion amortizes the timer over many\niterations — the only correct way to resolve a ~10 ns op). `contended` is a\ncustom harness (Criterion can't cleanly do multi-threaded contention at\nnanosecond scale): it builds a fresh sink per trial with rings sized to hold the\nburst so every push is accepted, and measures per-op cost in **per-thread CPU\ntime** plus the wall-clock tail (p99/p999/max). It includes the sharded sink,\nthe unsharded (`shards=1`) mode, and single `ArrayQueue` / `Mutex\u003cVec\u003e`\nbaselines.\n\n## Configuration\n\n`SinkConfig::default()` gives sensible values (shards ≈ CPU count clamped to\n1–8, 8192-item rings, 256 drain batch, 100 µs `idle_sleep`, opportunistic work\nstealing). Validate-and-build with `try_spawn*`, or `spawn*` to panic on invalid\nconfig. Set `shards: 1` for global FIFO ordering. See the API docs for every\nfield.\n\n## Observability\n\n`stats()`, `stats_per_shard()`, `dropped_total()` — `dropped` counts items shed\nbecause a ring was full. There is intentionally **no** accepted counter; counting\nevery success would put an atomic on the hot path. Count your own `true` returns\nif you need it. The optional `metrics` feature emits drop/overload counters from\nthe monitor (never from producers).\n\n## License\n\nLicensed under the [Apache License, Version 2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgodaddy%2Fsharded-sink","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgodaddy%2Fsharded-sink","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgodaddy%2Fsharded-sink/lists"}