Skip to content

Add ticket counters per raffle and measure the live site - #1

Merged
poly-glot merged 5 commits into
mainfrom
feature/sharded-counter
Sep 19, 2026
Merged

poly-glot merged 5 commits into
mainfrom
feature/sharded-counter

Conversation

@poly-glot

Copy link
Copy Markdown
Owner

What this changes

A raffle can be created with several ticket counters. A raffle created without a count keeps today's single counter on its own row, which the live raffle has, so there is no migration. A raffle created with one gets that many counter rows, each with an equal share of the cap in its own partition; its totals are the sum of the counters, read in one batch. One allocator serves both, drawing the next run from the raffle row or from the first counter with room, starting at the one the order id hashes to, and committing the same three-item transaction. Entries carry their shard and live in the counter's partition with the same sort key, so who holds a ticket stays one query. The draw freezes one count per counter, picks one uniform number and maps it to a counter and a ticket by prefix sums. The reconciliation walks each counter's run. The admin console takes the count at creation, refuses to change it or the cap afterwards, pages the ledger counter by counter and finds a ticket as 3-412. The page shows a counter's ticket the same way.

The stress test now runs against a deployment. With STRESS_BASE_URL it pays real orders through Stripe test mode; with STRESS_WEBHOOK_URL and STRESS_WEBHOOK_SECRET as well it signs charge.succeeded events itself and posts them straight at the webhook, so the rate is bounded by the stack rather than by Stripe. STRESS_RAFFLE_ID points it at a named raffle.

The README's scalability section is grounded in the live numbers. What the live site handled, what limits it, what to change to handle more, and how to run the test. The spec records the counters setting and its ceiling.

Measured on the live site, single counter

Delivery Accepted first time Refused first time Typical wait Recovered on resend
40 at once 35 5 1.3 s all
80 at once 66 14 1.3 s all
160 at once 117 43 2.6 s all
20 a second for 5 s 100 0 60 ms none needed
50 a second for 3 s 93 57 0.24 s all

The counter drains about 30 paid orders a second; a same-instant burst of 40 already sheds one in eight to the retry budget; every refusal lands on redelivery.

After the merge

Create a demo raffle with eight counters through the console and run the direct stress test against it with STRESS_RAFFLE_ID, to put the two tables side by side.

Checks

cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings and cargo test --workspace --locked against DynamoDB Local all pass. New integration tests cover allocation under contention across counters, the draw across counters, the reconciliation walk, the admin's creation and refusals, and the shard on the order view.

The stress test gains a second ignored mode that drives a deployment named
by STRESS_BASE_URL through its public API: a real checkout per sale, a
Stripe test-mode confirmation with the Visa debit test card through the
publishable key, and a poll of the order view every two seconds the way
the confirmation page does. It reports checkout, confirmation and
tickets-visible latencies per run.

Run against donation.junaid.guru at the free capacity: 140 sales offered
at 2, 5 and 10 a second were all allocated, tickets visible at the first
poll up to 5 a second and a 4 second median at 10 a second. The README's
scalability section carries the table. Stripe test mode's 25 requests a
second cap the offered rate near 10 sales a second.

reqwest is a dev-dependency of the webhook crate for this; the workspace
build has no json feature, so bodies are parsed through text.
A third stress mode prepares orders through the checkout, then signs
charge.succeeded events itself and posts them to the webhook's function
URL, so the delivery rate is bounded by the stack rather than by Stripe's
test-mode limit. It tallies the answers, polls every order for its
tickets, and redelivers refused events once after five seconds as Stripe
would. The endpoint and its signing secret come from the environment.

Run against donation.junaid.guru: the counter drains about thirty
allocations a second, a same-instant burst of forty already sheds one in
eight to the retry budget, twenty a second goes through clean, and every
refused event lands on redelivery. The README and the spec's ceiling now
say so instead of the figure DynamoDB Local suggested.
The scalability section now carries one account of the stress test run
against the live site: what the test does, the results, what they mean
for a launch, what to change to handle more, and how to run it. The
DynamoDB Local tables and the through-Stripe run are gone from the
README; the shard bench keeps its own section.
The sharded counter's section now opens by stating that no function uses
it and that every measurement above is of the single counter the site
runs today, and it describes the alternative and its cost in plain terms.
A raffle row gains an optional shards count. Without it a raffle keeps
today's single counter on its own row; with it, that many counter rows
each own an equal share of the cap in their own partition, and the
raffle's totals are the sum of the counters, read in one batch. One
allocator serves both: it draws the next run from the raffle row or from
the first counter with room, starting at the one the order id hashes to,
and commits the same three-item transaction.

Entries on a counter carry their shard and live in its partition with
the same sort key, so who holds a ticket stays one query. The draw
freezes one count per counter, picks one uniform number and maps it to a
counter and a ticket by prefix sums; winners carry the shard. The
reconciliation walks each counter's run against that counter. The admin
takes the count at creation, writes the counters before the raffle,
refuses to change the count or the cap afterwards, and lists and finds
tickets by shard. The page and the console show a counter's ticket as
3-412.

The spec records the setting and its ceiling, the README describes it
plainly, and the stress test can be pointed at a named raffle.
@poly-glot
poly-glot merged commit ee9192a into main Sep 19, 2026
1 check passed
@poly-glot
poly-glot deleted the feature/sharded-counter branch September 19, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant