380 lines
14 KiB
Lua
380 lines
14 KiB
Lua
local records_key = KEYS[1]
|
|
local unique_key = KEYS[2]
|
|
local idkeys_key = KEYS[3]
|
|
local expiry_key = KEYS[4]
|
|
local available_key = KEYS[5]
|
|
local inventory_key = KEYS[6]
|
|
local state_inventory_key = KEYS[7]
|
|
local owners_key = KEYS[8]
|
|
local owner_expiry_key = KEYS[9]
|
|
local epoch_key = KEYS[10]
|
|
local operation_key = KEYS[11]
|
|
local health_due_key = KEYS[12]
|
|
local health_queued_key = KEYS[13]
|
|
local health_leases_key = KEYS[14]
|
|
local health_tasks_key = KEYS[15]
|
|
local health_task_expiry_key = KEYS[16]
|
|
local health_ref_task_key = KEYS[17]
|
|
|
|
local operation = ARGV[1]
|
|
local operation_ttl_ms = tonumber(ARGV[2])
|
|
local cleanup_limit = tonumber(ARGV[3])
|
|
local now_ms = tonumber(ARGV[4])
|
|
local proxy_id = ARGV[5]
|
|
local worker_id = ARGV[6]
|
|
local epoch = tonumber(ARGV[7])
|
|
local value = tonumber(ARGV[8])
|
|
local active = tonumber(ARGV[9])
|
|
local reserved = tonumber(ARGV[10])
|
|
local worker_index_key = ARGV[11]
|
|
local mutating = operation ~= 'get'
|
|
|
|
local function finish(reply)
|
|
local encoded = cjson.encode(reply)
|
|
if mutating then
|
|
redis.call('SET', operation_key, encoded, 'PX', operation_ttl_ms)
|
|
end
|
|
return encoded
|
|
end
|
|
|
|
if mutating then
|
|
local committed = redis.call('GET', operation_key)
|
|
if committed then
|
|
return committed
|
|
end
|
|
end
|
|
|
|
local function is_managed(state)
|
|
return state == 'FETCHED' or state == 'CHECKING' or state == 'AVAILABLE' or
|
|
state == 'SUSPECT' or state == 'DRAINING'
|
|
end
|
|
|
|
local function decrement_inventory(upstream)
|
|
if type(upstream) ~= 'string' or upstream == '' then
|
|
return
|
|
end
|
|
local count = redis.call('HINCRBY', inventory_key, upstream, -1)
|
|
if count < 0 then
|
|
redis.call('HSET', inventory_key, upstream, 0)
|
|
end
|
|
end
|
|
|
|
local function state_field(upstream, state)
|
|
return string.len(upstream) .. ':' .. upstream .. ':' .. state
|
|
end
|
|
|
|
local function is_counted(state)
|
|
return state == 'FETCHED' or state == 'CHECKING' or state == 'AVAILABLE' or
|
|
state == 'SUSPECT' or state == 'DRAINING' or state == 'UNHEALTHY' or state == 'EXTRACTED'
|
|
end
|
|
|
|
local function decrement_state(upstream, state)
|
|
if type(upstream) ~= 'string' or upstream == '' or not is_counted(state) then
|
|
return
|
|
end
|
|
local field = state_field(upstream, state)
|
|
local count = redis.call('HINCRBY', state_inventory_key, field, -1)
|
|
if count <= 0 then
|
|
redis.call('HDEL', state_inventory_key, field)
|
|
end
|
|
end
|
|
|
|
local function touch(key, expires_at_ms)
|
|
if redis.call('EXISTS', key) == 0 then
|
|
return
|
|
end
|
|
local current = redis.call('PEXPIRETIME', key)
|
|
if current < expires_at_ms then
|
|
redis.call('PEXPIREAT', key, expires_at_ms)
|
|
end
|
|
end
|
|
|
|
local function remove_available(id, record)
|
|
redis.call('ZREM', available_key, id)
|
|
local index_keys = record and record.indexKeys
|
|
if type(index_keys) == 'table' then
|
|
for _, index_key in ipairs(index_keys) do
|
|
if type(index_key) == 'string' and index_key ~= '' then
|
|
redis.call('ZREM', index_key, id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function remove_owned(id, record)
|
|
if record and type(record.ownerIndexKey) == 'string' and record.ownerIndexKey ~= '' then
|
|
redis.call('ZREM', record.ownerIndexKey, id)
|
|
end
|
|
end
|
|
|
|
local function add_available(id, record, at_ms)
|
|
local usable_until_ms = record and tonumber(record.usableUntilMs)
|
|
if not usable_until_ms or record.state ~= 'AVAILABLE' or usable_until_ms <= at_ms then
|
|
return
|
|
end
|
|
redis.call('ZADD', available_key, usable_until_ms, id)
|
|
touch(available_key, tonumber(record.expiresAtMs))
|
|
if type(record.indexKeys) == 'table' then
|
|
for _, index_key in ipairs(record.indexKeys) do
|
|
if type(index_key) == 'string' and index_key ~= '' then
|
|
redis.call('ZADD', index_key, usable_until_ms, id)
|
|
touch(index_key, tonumber(record.expiresAtMs))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function remove_health_task(proxy_id)
|
|
redis.call('ZREM', health_due_key, proxy_id)
|
|
local task_id = redis.call('HGET', health_ref_task_key, proxy_id)
|
|
if not task_id then
|
|
return
|
|
end
|
|
local raw = redis.call('HGET', health_tasks_key, task_id)
|
|
if raw then
|
|
local decoded, task = pcall(cjson.decode, raw)
|
|
if decoded and type(task) == 'table' then
|
|
if type(task.checkerLeaseKey) == 'string' and task.checkerLeaseKey ~= '' then
|
|
redis.call('ZREM', task.checkerLeaseKey, task_id)
|
|
end
|
|
if type(task.upstreamTasksKey) == 'string' and task.upstreamTasksKey ~= '' then
|
|
redis.call('ZREM', task.upstreamTasksKey, task_id)
|
|
end
|
|
end
|
|
end
|
|
redis.call('ZREM', health_queued_key, task_id)
|
|
redis.call('ZREM', health_leases_key, task_id)
|
|
redis.call('ZREM', health_task_expiry_key, task_id)
|
|
redis.call('HDEL', health_tasks_key, task_id)
|
|
redis.call('HDEL', health_ref_task_key, proxy_id)
|
|
end
|
|
|
|
local function remove_proxy(id)
|
|
local raw = redis.call('HGET', records_key, id)
|
|
local record = nil
|
|
if raw then
|
|
local decoded
|
|
decoded, record = pcall(cjson.decode, raw)
|
|
remove_available(id, decoded and record or nil)
|
|
remove_owned(id, decoded and record or nil)
|
|
if decoded and type(record) == 'table' and is_managed(record.state) then
|
|
decrement_inventory(record.sourceUpstream)
|
|
end
|
|
if decoded and type(record) == 'table' then
|
|
decrement_state(record.sourceUpstream, record.state)
|
|
end
|
|
else
|
|
redis.call('ZREM', available_key, id)
|
|
end
|
|
local digest = redis.call('HGET', idkeys_key, id)
|
|
if digest and redis.call('HGET', unique_key, digest) == id then
|
|
redis.call('HDEL', unique_key, digest)
|
|
end
|
|
redis.call('HDEL', idkeys_key, id)
|
|
redis.call('HDEL', records_key, id)
|
|
redis.call('ZREM', expiry_key, id)
|
|
redis.call('HDEL', owners_key, id)
|
|
redis.call('ZREM', owner_expiry_key, id)
|
|
remove_health_task(id)
|
|
end
|
|
|
|
local function cleanup_hard_expired(at_ms)
|
|
local expired = redis.call('ZRANGEBYSCORE', expiry_key, '-inf', at_ms, 'LIMIT', 0, cleanup_limit)
|
|
for _, id in ipairs(expired) do
|
|
remove_proxy(id)
|
|
end
|
|
end
|
|
|
|
local function decode_table(raw)
|
|
if not raw then
|
|
return nil
|
|
end
|
|
local decoded, value = pcall(cjson.decode, raw)
|
|
if not decoded or type(value) ~= 'table' then
|
|
return nil
|
|
end
|
|
return value
|
|
end
|
|
|
|
local function valid_assignment(assignment)
|
|
return assignment and assignment.version == 1 and type(assignment.proxyId) == 'string' and
|
|
assignment.proxyId ~= '' and type(assignment.workerId) == 'string' and assignment.workerId ~= '' and
|
|
tonumber(assignment.epoch) and tonumber(assignment.epoch) > 0 and
|
|
tonumber(assignment.assignmentVersion) and tonumber(assignment.assignmentVersion) > 0 and
|
|
tonumber(assignment.expiresAtMs) and tonumber(assignment.expiresAtMs) > 0 and
|
|
type(assignment.draining) == 'boolean'
|
|
end
|
|
|
|
local function clear_owner(id, assignment, at_ms, restore)
|
|
local raw_record = redis.call('HGET', records_key, id)
|
|
local record = decode_table(raw_record)
|
|
if record and (not assignment or record.ownerWorkerId == assignment.workerId) then
|
|
remove_owned(id, record)
|
|
record.ownerWorkerId = nil
|
|
redis.call('HSET', records_key, id, cjson.encode(record))
|
|
if restore then
|
|
add_available(id, record, at_ms)
|
|
end
|
|
end
|
|
redis.call('HDEL', owners_key, id)
|
|
redis.call('ZREM', owner_expiry_key, id)
|
|
if assignment and type(assignment.workerIndexKey) == 'string' and assignment.workerIndexKey ~= '' then
|
|
redis.call('ZREM', assignment.workerIndexKey, id)
|
|
end
|
|
end
|
|
|
|
if operation == 'assign' then
|
|
cleanup_hard_expired(now_ms)
|
|
local current_raw = redis.call('HGET', owners_key, proxy_id)
|
|
if current_raw then
|
|
local current = decode_table(current_raw)
|
|
if not valid_assignment(current) then
|
|
return finish({status = 'unavailable'})
|
|
end
|
|
if tonumber(current.expiresAtMs) > now_ms then
|
|
return finish({status = 'already_owned'})
|
|
end
|
|
clear_owner(proxy_id, current, now_ms, true)
|
|
end
|
|
|
|
local record = decode_table(redis.call('HGET', records_key, proxy_id))
|
|
if not record or record.state ~= 'AVAILABLE' or
|
|
type(record.ownerIndexKey) ~= 'string' or record.ownerIndexKey == '' or
|
|
(record.ownerWorkerId and record.ownerWorkerId ~= '') or
|
|
redis.call('HEXISTS', owners_key, proxy_id) == 1 or
|
|
not tonumber(record.usableUntilMs) or tonumber(record.usableUntilMs) <= now_ms then
|
|
return finish({status = 'unavailable'})
|
|
end
|
|
if type(worker_index_key) ~= 'string' or worker_index_key == '' or
|
|
not string.find(worker_index_key, '{activity}', 1, true) then
|
|
return finish({status = 'invalid'})
|
|
end
|
|
local expires_at_ms = now_ms + value
|
|
if tonumber(record.usableUntilMs) < expires_at_ms then
|
|
expires_at_ms = tonumber(record.usableUntilMs)
|
|
end
|
|
local next_epoch = redis.call('INCR', epoch_key)
|
|
redis.call('PERSIST', epoch_key)
|
|
local assignment = {
|
|
version = 1,
|
|
proxyId = proxy_id,
|
|
workerId = worker_id,
|
|
workerIndexKey = worker_index_key,
|
|
epoch = next_epoch,
|
|
assignmentVersion = 1,
|
|
expiresAtMs = expires_at_ms,
|
|
draining = false,
|
|
}
|
|
local encoded = cjson.encode(assignment)
|
|
redis.call('HSET', owners_key, proxy_id, encoded)
|
|
redis.call('ZADD', owner_expiry_key, expires_at_ms, proxy_id)
|
|
touch(owners_key, tonumber(record.expiresAtMs))
|
|
touch(owner_expiry_key, tonumber(record.expiresAtMs))
|
|
record.ownerWorkerId = worker_id
|
|
redis.call('HSET', records_key, proxy_id, cjson.encode(record))
|
|
remove_available(proxy_id, record)
|
|
redis.call('ZADD', record.ownerIndexKey, record.usableUntilMs, proxy_id)
|
|
touch(record.ownerIndexKey, tonumber(record.expiresAtMs))
|
|
redis.call('ZADD', worker_index_key, expires_at_ms, proxy_id)
|
|
touch(worker_index_key, tonumber(record.expiresAtMs))
|
|
return finish({status = 'ok', record = encoded})
|
|
end
|
|
|
|
if operation == 'renew' then
|
|
cleanup_hard_expired(now_ms)
|
|
local current = decode_table(redis.call('HGET', owners_key, proxy_id))
|
|
if not valid_assignment(current) or current.workerId ~= worker_id or tonumber(current.epoch) ~= epoch then
|
|
return finish({status = 'stale'})
|
|
end
|
|
if tonumber(current.expiresAtMs) <= now_ms then
|
|
clear_owner(proxy_id, current, now_ms, true)
|
|
return finish({status = 'stale'})
|
|
end
|
|
local record = decode_table(redis.call('HGET', records_key, proxy_id))
|
|
if not record or record.ownerWorkerId ~= worker_id or
|
|
not tonumber(record.usableUntilMs) or tonumber(record.usableUntilMs) <= now_ms then
|
|
clear_owner(proxy_id, current, now_ms, false)
|
|
return finish({status = 'stale'})
|
|
end
|
|
local expires_at_ms = now_ms + value
|
|
if tonumber(record.usableUntilMs) < expires_at_ms then
|
|
expires_at_ms = tonumber(record.usableUntilMs)
|
|
end
|
|
current.assignmentVersion = tonumber(current.assignmentVersion) + 1
|
|
current.expiresAtMs = expires_at_ms
|
|
local encoded = cjson.encode(current)
|
|
redis.call('HSET', owners_key, proxy_id, encoded)
|
|
redis.call('ZADD', owner_expiry_key, expires_at_ms, proxy_id)
|
|
touch(owners_key, tonumber(record.expiresAtMs))
|
|
touch(owner_expiry_key, tonumber(record.expiresAtMs))
|
|
if not current.draining and type(current.workerIndexKey) == 'string' and current.workerIndexKey ~= '' then
|
|
redis.call('ZADD', current.workerIndexKey, expires_at_ms, proxy_id)
|
|
touch(current.workerIndexKey, tonumber(record.expiresAtMs))
|
|
end
|
|
return finish({status = 'ok', record = encoded})
|
|
end
|
|
|
|
if operation == 'begin_drain' then
|
|
local current = decode_table(redis.call('HGET', owners_key, proxy_id))
|
|
if not valid_assignment(current) or current.workerId ~= worker_id or tonumber(current.epoch) ~= epoch then
|
|
return finish({status = 'stale'})
|
|
end
|
|
if not current.draining then
|
|
current.draining = true
|
|
current.assignmentVersion = tonumber(current.assignmentVersion) + 1
|
|
local encoded = cjson.encode(current)
|
|
redis.call('HSET', owners_key, proxy_id, encoded)
|
|
local record = decode_table(redis.call('HGET', records_key, proxy_id))
|
|
remove_owned(proxy_id, record)
|
|
if type(current.workerIndexKey) == 'string' and current.workerIndexKey ~= '' then
|
|
redis.call('ZREM', current.workerIndexKey, proxy_id)
|
|
end
|
|
return finish({status = 'ok', record = encoded})
|
|
end
|
|
return finish({status = 'ok', record = cjson.encode(current)})
|
|
end
|
|
|
|
if operation == 'acknowledge_drain' then
|
|
local current = decode_table(redis.call('HGET', owners_key, proxy_id))
|
|
if not valid_assignment(current) or current.workerId ~= worker_id or tonumber(current.epoch) ~= epoch then
|
|
return finish({status = 'stale'})
|
|
end
|
|
if not current.draining then
|
|
return finish({status = 'not_draining'})
|
|
end
|
|
if active > 0 or reserved > 0 then
|
|
return finish({status = 'drain_not_ready'})
|
|
end
|
|
local server_time = redis.call('TIME')
|
|
local server_now_ms = tonumber(server_time[1]) * 1000 + math.floor(tonumber(server_time[2]) / 1000)
|
|
clear_owner(proxy_id, current, server_now_ms, true)
|
|
return finish({status = 'ok'})
|
|
end
|
|
|
|
if operation == 'get' then
|
|
local raw = redis.call('HGET', owners_key, proxy_id)
|
|
if not raw then
|
|
return finish({status = 'not_found'})
|
|
end
|
|
return finish({status = 'ok', record = raw})
|
|
end
|
|
|
|
if operation == 'expire' then
|
|
local ids = redis.call('ZRANGEBYSCORE', owner_expiry_key, '-inf', now_ms, 'LIMIT', 0, value)
|
|
local expired = {}
|
|
for _, id in ipairs(ids) do
|
|
local current = decode_table(redis.call('HGET', owners_key, id))
|
|
if valid_assignment(current) then
|
|
expired[#expired + 1] = current
|
|
end
|
|
clear_owner(id, current, now_ms, true)
|
|
end
|
|
local encoded = '[]'
|
|
if #expired > 0 then
|
|
encoded = cjson.encode(expired)
|
|
end
|
|
return finish({status = 'ok', record = encoded})
|
|
end
|
|
|
|
return finish({status = 'invalid'})
|