Two shoppers, one unit left, and both of them get it #49103
Replies: 1 comment
|
Nice write-up, and shipping naive.sql next to the fix is a good way to show the bug. I read through 02_functions.sql, and I couldn't find a hole in the locking. The thing I'd poke at is abuse rather than races.
Neither of these is a correctness bug in what you built, but for limited drops they're usually what goes wrong right after the oversell is fixed. I hit the same shape of problem with single-use links in a file-sharing app I'm building, so this was a useful read. |
Uh oh!
There was an error while loading. Please reload this page.
Every shop schema I've written started the same way: read the stock, check it's enough, insert the order. Under one user that's correct. Under two clicking at the same moment it quietly sells the same unit twice, and you find out from the customer.
I put the fix in a standalone MIT module so I'd stop rewriting it:
https://github.com/SoloCron/no-oversell
It's a cart advisory lock, then the stock rows taken in a fixed order with
for no key update, then the reservation — so two carts can never interleave. Holds expire onclock_timestamp(), and commit is idempotent so a repeated webhook can't decrement twice.The part that might be useful to anyone here:
npm testboots a throwaway Postgres and races real connections against each other rather than mocking. It also shipstest/naive.sql— the ordinary read-then-write version — and proves that one overselling in the same scenario, so you can see the failure before the fix.One Supabase-specific thing I got wrong first time and would flag to anyone doing this: my storefront view was auto-updatable and ran as its owner, so with default grants
anoncould UPDATE through the view and write straight into the stock table, past RLS and past every lock in the module. RLS protects tables, not views. REVOKE before you GRANT.Happy to be told I've missed something.
All reactions