proxy-pool/internal/adapters/redisprovider/scripts/provider.lua

284 lines
10 KiB
Lua

local operation = ARGV[1]
local max_safe_integer = 9007199254740991
local function now_ms()
local value = redis.call('TIME')
return tonumber(value[1]) * 1000 + math.floor(tonumber(value[2]) / 1000)
end
local function reply(status, generation, epoch, wait_ms)
return cjson.encode({
status = status,
generation = generation,
epoch = tostring(epoch or '0'),
waitMs = wait_ms
})
end
local function read_leader()
local encoded = redis.call('GET', KEYS[3])
if not encoded then
return nil, nil
end
local ok, value = pcall(cjson.decode, encoded)
if not ok or type(value) ~= 'table' or type(value.generation) ~= 'string' or
type(value.holderId) ~= 'string' or type(value.token) ~= 'string' or
type(value.epoch) ~= 'string' then
return nil, 'invalid'
end
return value, nil
end
local function same_leader(value, generation, holder_id, token, epoch)
return value and value.generation == generation and value.holderId == holder_id and
value.token == token and value.epoch == tostring(epoch)
end
local function read_counter(key)
local encoded = redis.call('GET', key)
if not encoded then
return 0, nil
end
local value = tonumber(encoded)
if not value or value < 0 or value ~= math.floor(value) then
return nil, 'invalid'
end
return value, nil
end
local function read_permit(token)
local encoded = redis.call('HGET', KEYS[8], token)
if not encoded then
return nil, nil
end
local ok, value = pcall(cjson.decode, encoded)
if not ok or type(value) ~= 'table' or type(value.state) ~= 'string' or
type(value.expected) ~= 'number' or value.expected <= 0 or
value.expected ~= math.floor(value.expected) then
return nil, 'invalid'
end
return value, nil
end
local function cleanup_expired(now)
local expired = redis.call('ZRANGEBYSCORE', KEYS[9], '-inf', now, 'LIMIT', 0, 256)
if #expired == 0 then
return nil
end
local fetched, fetched_error = read_counter(KEYS[6])
local pending, pending_error = read_counter(KEYS[7])
if fetched_error or pending_error then
return 'invalid'
end
for _, permit_token in ipairs(expired) do
local permit, permit_error = read_permit(permit_token)
if permit_error then
return 'invalid'
end
if permit and permit.state == 'reserved' then
if pending < permit.expected or fetched > max_safe_integer - permit.expected then
return 'invalid'
end
pending = pending - permit.expected
fetched = fetched + permit.expected
end
redis.call('HDEL', KEYS[8], permit_token)
redis.call('ZREM', KEYS[5], permit_token)
redis.call('ZREM', KEYS[9], permit_token)
end
redis.call('SET', KEYS[6], fetched)
redis.call('SET', KEYS[7], pending)
return nil
end
if operation == 'acquire_leader' then
local generation_candidate = ARGV[2]
local holder_id = ARGV[3]
local token = ARGV[4]
local lease_ttl = tonumber(ARGV[5])
if not lease_ttl or lease_ttl <= 0 then
return reply('invalid', '', 0, 0)
end
local created = redis.call('SET', KEYS[1], generation_candidate, 'NX')
if created then
redis.call('DEL', KEYS[2], KEYS[3], KEYS[4], KEYS[5], KEYS[6], KEYS[7], KEYS[8], KEYS[9])
end
local generation = redis.call('GET', KEYS[1])
local current, current_error = read_leader()
if current_error then
return reply('unavailable', generation, 0, 0)
end
if current then
if current.generation == generation and current.holderId == holder_id and current.token == token then
current.expiresAtMs = now_ms() + lease_ttl
redis.call('SET', KEYS[3], cjson.encode(current), 'PX', lease_ttl)
return reply('ok', generation, current.epoch, 0)
end
local remaining = redis.call('PTTL', KEYS[3])
return reply('busy', generation, 0, math.max(remaining, 1))
end
redis.call('INCR', KEYS[2])
local epoch = redis.call('GET', KEYS[2])
local leader = {
version = 1,
generation = generation,
holderId = holder_id,
token = token,
epoch = epoch,
expiresAtMs = now_ms() + lease_ttl
}
redis.call('SET', KEYS[3], cjson.encode(leader), 'PX', lease_ttl)
return reply('ok', generation, epoch, 0)
end
if operation == 'renew_leader' then
local generation = ARGV[2]
local holder_id = ARGV[3]
local token = ARGV[4]
local epoch = ARGV[5]
local lease_ttl = tonumber(ARGV[6])
local current, current_error = read_leader()
if current_error then
return reply('unavailable', generation, epoch or 0, 0)
end
if not same_leader(current, generation, holder_id, token, epoch) then
return reply('stale', generation, epoch or 0, 0)
end
current.expiresAtMs = now_ms() + lease_ttl
redis.call('SET', KEYS[3], cjson.encode(current), 'PX', lease_ttl)
return reply('ok', generation, epoch, 0)
end
if operation == 'release_leader' then
local generation = ARGV[2]
local holder_id = ARGV[3]
local token = ARGV[4]
local epoch = ARGV[5]
local current, current_error = read_leader()
if current_error then
return reply('unavailable', generation, epoch or 0, 0)
end
if same_leader(current, generation, holder_id, token, epoch) then
redis.call('DEL', KEYS[3])
end
return reply('ok', generation, epoch or 0, 0)
end
if operation == 'acquire_fetch' then
local generation = ARGV[2]
local holder_id = ARGV[3]
local leader_token = ARGV[4]
local epoch = ARGV[5]
local permit_token = ARGV[6]
local request_interval = tonumber(ARGV[7])
local max_in_flight = tonumber(ARGV[8])
local permit_ttl = tonumber(ARGV[9])
local expected = tonumber(ARGV[10])
local max_total = tonumber(ARGV[11])
if not expected or expected <= 0 or expected ~= math.floor(expected) or
expected > max_safe_integer or not max_total or max_total < 0 or
max_total > max_safe_integer or max_total ~= math.floor(max_total) then
return reply('invalid', generation, epoch or 0, 0)
end
local current, current_error = read_leader()
if current_error then
return reply('unavailable', generation, epoch or 0, 0)
end
if not same_leader(current, generation, holder_id, leader_token, epoch) then
return reply('stale', generation, epoch or 0, 0)
end
local now = now_ms()
if cleanup_expired(now) then
return reply('unavailable', generation, epoch, 0)
end
local existing, existing_error = read_permit(permit_token)
if existing_error then
return reply('unavailable', generation, epoch, 0)
end
if existing and existing.state == 'reserved' then
return reply('ok', generation, epoch, 0)
end
if existing then
return reply('unavailable', generation, epoch, 0)
end
local fetched, fetched_error = read_counter(KEYS[6])
local pending, pending_error = read_counter(KEYS[7])
if fetched_error or pending_error then
return reply('unavailable', generation, epoch, 0)
end
if max_total > 0 and fetched + pending + expected > max_total then
return reply('quota_exhausted', generation, epoch, 0)
end
local next_request = redis.call('GET', KEYS[4])
if next_request and not tonumber(next_request) then
return reply('unavailable', generation, epoch, 0)
end
if next_request and tonumber(next_request) > now then
return reply('rate_limited', generation, epoch, tonumber(next_request) - now)
end
if redis.call('ZCARD', KEYS[5]) >= max_in_flight then
local earliest = redis.call('ZRANGE', KEYS[5], 0, 0, 'WITHSCORES')
local wait_ms = 1
if earliest[2] then
wait_ms = math.max(tonumber(earliest[2]) - now, 1)
end
return reply('at_capacity', generation, epoch, wait_ms)
end
redis.call('ZADD', KEYS[5], now + permit_ttl, permit_token)
redis.call('ZADD', KEYS[9], now + permit_ttl, permit_token)
redis.call('HSET', KEYS[8], permit_token, cjson.encode({
version = 1,
state = 'reserved',
expected = expected
}))
redis.call('SET', KEYS[7], pending + expected)
if request_interval > 0 then
redis.call('SET', KEYS[4], now + request_interval, 'PX', request_interval)
else
redis.call('DEL', KEYS[4])
end
return reply('ok', generation, epoch, 0)
end
if operation == 'complete_fetch' or operation == 'cancel_fetch' then
local permit_token = ARGV[2]
local fetched_count = tonumber(ARGV[3])
local settlement_ttl = tonumber(ARGV[4])
if not fetched_count or fetched_count < 0 or fetched_count ~= math.floor(fetched_count) or
fetched_count > max_safe_integer or not settlement_ttl or settlement_ttl <= 0 then
return reply('invalid', '', 0, 0)
end
local now = now_ms()
if cleanup_expired(now) then
return reply('unavailable', '', 0, 0)
end
local permit, permit_error = read_permit(permit_token)
if permit_error then
return reply('unavailable', '', 0, 0)
end
if not permit or permit.state ~= 'reserved' then
return reply('ok', '', 0, 0)
end
local fetched, fetched_error = read_counter(KEYS[6])
local pending, pending_error = read_counter(KEYS[7])
if fetched_error or pending_error or pending < permit.expected or
fetched > max_safe_integer - fetched_count then
return reply('unavailable', '', 0, 0)
end
pending = pending - permit.expected
if operation == 'complete_fetch' then
fetched = fetched + fetched_count
permit.state = 'completed'
else
permit.state = 'cancelled'
end
redis.call('SET', KEYS[6], fetched)
redis.call('SET', KEYS[7], pending)
redis.call('HSET', KEYS[8], permit_token, cjson.encode(permit))
redis.call('ZREM', KEYS[5], permit_token)
redis.call('ZADD', KEYS[9], now + settlement_ttl, permit_token)
return reply('ok', '', 0, 0)
end
return reply('invalid', '', 0, 0)