GOBLIN.DECRPOS

GOBLIN.DECRPOS key

Decrement a counter, but only while it is positive. Read the integer at key (an absent key is 0); if it is > 0, decrement it by one — preserving any TTL — and return the new value. Otherwise leave the key untouched (never creating it) and return -1. It is the atomic native form of the decrement-if-positive idiom: stock reservation, a permit draw from a counting semaphore, any "take one while some remain" counter.

The idiom it replaces

A plain DECR has no floor — it runs straight past zero into negative numbers, so a stock counter would happily "reserve" units that do not exist. Guarding it with a separate GET is a race (two clients both read 1, both DECR, and the count goes to -1 — two reservations against one unit). The correct version is a read-test-decrement done atomically, which in Redis means Lua — the tell that it wants to be a command:

local v = tonumber(redis.call("get", KEYS[1]) or "0")
if v > 0 then return redis.call("decr", KEYS[1]) end
return -1

GOBLIN.DECRPOS key does exactly this in one atomic C++ op — an integer parse, a positivity test, and a conditional store. The redis.call("get") and redis.call("decr") become direct calls to the store's primitives, with no interpreter and no command re-entry.

Semantics

Return value

Situation Reply
decremented (value was > 0) (integer) the new value (>= 0)
rejected (value <= 0, or key absent) (integer) -1; the key is left untouched
key holds a non-integer string ERR value is not an integer or out of range
key holds a non-string (zset / hash) WRONGTYPE error

Because the floor is zero, a successful reply is always >= 0 and a reject is always -1, so — unlike GOBLIN.INCRBOUND — the reply alone distinguishes the two cases unambiguously.

Examples

Stock reservation — hand out units until they run out:

> SET stock:item9 3
OK
> GOBLIN.DECRPOS stock:item9
(integer) 2          # reserved one, 2 left
> GOBLIN.DECRPOS stock:item9
(integer) 1
> GOBLIN.DECRPOS stock:item9
(integer) 0          # last one reserved
> GOBLIN.DECRPOS stock:item9
(integer) -1         # sold out -> reservation denied, count stays 0

A counting semaphore — N permits, DECRPOS to acquire, INCR to release:

> SET sem:pool 5      # 5 permits
OK
> GOBLIN.DECRPOS sem:pool
(integer) 4          # acquired a permit
# ... work ...
> INCR sem:pool       # release it back
(integer) 5
# when DECRPOS returns -1, all permits are out -> caller must wait or back off

The client's rule is simply: -1 means none left (denied); any other reply is the number remaining after taking one.

See also