Semaphore Class
A named, process-global counting semaphore: “at most N of these running at once” inside this process — e.g. a cron handler that must not overlap itself. Tokens are explicit; cross-process mutual exclusion belongs to job claiming. Semaphore.reset(name) drops a name and every token on it — the recovery path when a job raises before releasing (release in a finally to avoid needing it). A name keeps the limit its first caller fixed; at the 1000-name cap, names nobody currently holds are reclaimed, so per-key names ("import-#{tenant}") are safe to use.
Semaphore.try_acquire(name, limit)
Returns an Int token when a slot is free, null otherwise. The first call fixes the limit for that name; later calls reuse it.
let token = Semaphore.try_acquire("nightly-report", 1);
if token.present? {
generate_nightly_report();
Semaphore.release("nightly-report", token);
} else {
print("already running");
}
Semaphore.release(name, token) / Semaphore.count(name)
Release returns true when the token was actually held, so double-release is detectable. Count reports current occupancy.
Semaphore.count("nightly-report");
# { "limit": 1, "held": 1 }