Locks / Semaphores
Semaphores
A semaphore is a lock whose key up to limit holders may hold at once: three
uploads per user, two calls to a model per workspace, eight exports on the
whole server. The calls are a lock's: hold, tryHold, wait, maxHold,
a lease renewed over a connection.
const uploads = store.semaphore('uploads', { limit: 3, ttl: '30s' })
await uploads.hold(user.id, () => upload(file)) // the fourth upload of a user waits for a place
const tried = await uploads.tryHold(user.id, () => upload(file))
if (!tried.acquired) return tooMany()uploads = store.semaphore("uploads", limit=3, ttl="30s")
async with uploads.hold(user.id):
await upload(file)
async with uploads.try_hold(user.id) as permit:
if permit is None:
return too_many()
await upload(file)uploads, err := locks.OpenSemaphore(store, "uploads", locks.Limit(3), locks.TTL(30*time.Second))
err = uploads.Hold(ctx, user.ID, func(ctx context.Context, _ *locks.Lease) error {
return upload(ctx, file)
})
acquired, err := uploads.TryHold(ctx, user.ID, func(ctx context.Context, _ *locks.Lease) error {
return upload(ctx, file)
})- Each key has its own places: user 42's three uploads never take user 43's.
For one bound over everything, use one key:
uploads.hold('all', …). - Waiters stand in line for the next free place, first come first served,
waitat most. limitis 1 to 1,000. A semaphore oflimit: 1is a lock without fences.
No fences
A semaphore's lease has no fence. Three holders are valid at once, and a
system that refused all but the largest fence would refuse two of them. A
holder that stalled past its lease is told lost as a lock's is, through its
signal, its context or lost().
Semaphore, rate limit or concurrency?
| You want | Use |
|---|---|
| at most N running at once, anywhere in the program | a semaphore |
| at most N starting per second or minute | a rate limit |
| at most N of a queue's jobs running, by group | the queue's concurrency |
| uses counted against a plan, refunded on failure | a quota |
A semaphore counts what runs now and forgets it when it ends; a rate limit counts what started recently, even if it finished. A queue's concurrency is taken in the same transaction as the job, so it survives restarts; a semaphore lives in memory.
Two opens with different limits
The limit is given where the semaphore opens, and a program may change it between deploys. A key admits by the limit of the open that waits: a smaller limit lets no holder go, it only lets fewer in until the holders above it finish.
See also
- Locks: the engine's overview.
- Holding and waiting:
hold,tryHold,wait,maxHold.