mcp-usage-control-redis 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +51 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1149 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-flow.d.ts +59 -0
- package/dist/mcp-flow.d.ts.map +1 -0
- package/dist/mcp-flow.js +331 -0
- package/dist/mcp-flow.js.map +1 -0
- package/dist/scripts.d.ts +12 -0
- package/dist/scripts.d.ts.map +1 -0
- package/dist/scripts.js +719 -0
- package/dist/scripts.js.map +1 -0
- package/package.json +74 -0
package/dist/scripts.js
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
const COMMON = String.raw `
|
|
2
|
+
local MAX_SAFE_INTEGER = 9007199254740991
|
|
3
|
+
local REDIS_RESERVATION_SCHEMA_VERSION = 1
|
|
4
|
+
|
|
5
|
+
local function safeTimeAdd(base, delta)
|
|
6
|
+
if not base or not delta or base < 0 or delta <= 0 then return nil end
|
|
7
|
+
if base > MAX_SAFE_INTEGER or delta > (MAX_SAFE_INTEGER - base) then return nil end
|
|
8
|
+
return base + delta
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
local function saturatingSafeAdd(base, delta)
|
|
12
|
+
if delta >= (MAX_SAFE_INTEGER - base) then return MAX_SAFE_INTEGER end
|
|
13
|
+
return base + delta
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
local function safeIntegerString(value)
|
|
17
|
+
return string.format('%.0f', value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
local function hasSupportedRecordSchema(record)
|
|
21
|
+
return record.schemaVersion == nil or record.schemaVersion == REDIS_RESERVATION_SCHEMA_VERSION
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
local function subtractUsed(budgetHashes, amount)
|
|
25
|
+
if amount <= 0 then return end
|
|
26
|
+
for _, budgetHash in ipairs(budgetHashes) do
|
|
27
|
+
local nextUsed = redis.call('HINCRBY', KEYS[1], budgetHash, -amount)
|
|
28
|
+
if tonumber(nextUsed) <= 0 then redis.call('HDEL', KEYS[1], budgetHash) end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
local function isVector(record)
|
|
33
|
+
return record.mode == 'vector'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
local function vectorBudgetCount(record)
|
|
37
|
+
local count = 0
|
|
38
|
+
for _, dimension in ipairs(record.dimensions or {}) do
|
|
39
|
+
count = count + #(dimension.budgetHashes or {})
|
|
40
|
+
end
|
|
41
|
+
return count
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
local function releaseRecord(record)
|
|
45
|
+
if isVector(record) then
|
|
46
|
+
for _, dimension in ipairs(record.dimensions or {}) do
|
|
47
|
+
subtractUsed(dimension.budgetHashes, tonumber(dimension.reservedUnits))
|
|
48
|
+
end
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
subtractUsed(record.budgetHashes, tonumber(record.reservedUnits))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
local function retainLiable(record)
|
|
55
|
+
record.state = 'settled'
|
|
56
|
+
record.outcome = 'lease_expired_after_execution_started'
|
|
57
|
+
if isVector(record) then
|
|
58
|
+
local actual = {}
|
|
59
|
+
for _, dimension in ipairs(record.dimensions or {}) do
|
|
60
|
+
table.insert(actual, { hash = dimension.hash, actualUnits = tonumber(dimension.reservedUnits) })
|
|
61
|
+
end
|
|
62
|
+
record.actualByDimensions = actual
|
|
63
|
+
else
|
|
64
|
+
record.actualUnits = tonumber(record.reservedUnits)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
local function directExpiryReply(record, expiredState)
|
|
69
|
+
if isVector(record) then
|
|
70
|
+
return { 'expired_vector', expiredState, tostring(#(record.dimensions or {})), tostring(vectorBudgetCount(record)) }
|
|
71
|
+
end
|
|
72
|
+
return { 'expired', expiredState, tostring(tonumber(record.reservedUnits)) }
|
|
73
|
+
end
|
|
74
|
+
`;
|
|
75
|
+
const CLEANUP = String.raw `
|
|
76
|
+
local recoveredPendingCount = 0
|
|
77
|
+
local recoveredPendingUnits = 0
|
|
78
|
+
local recoveredLiableCount = 0
|
|
79
|
+
local recoveredLiableUnits = 0
|
|
80
|
+
local recoveredVectorPendingCount = 0
|
|
81
|
+
local recoveredVectorLiableCount = 0
|
|
82
|
+
|
|
83
|
+
local function withRecovery(reply)
|
|
84
|
+
table.insert(reply, 'recovery')
|
|
85
|
+
table.insert(reply, safeIntegerString(recoveredPendingCount))
|
|
86
|
+
table.insert(reply, safeIntegerString(recoveredPendingUnits))
|
|
87
|
+
table.insert(reply, safeIntegerString(recoveredLiableCount))
|
|
88
|
+
table.insert(reply, safeIntegerString(recoveredLiableUnits))
|
|
89
|
+
table.insert(reply, safeIntegerString(recoveredVectorPendingCount))
|
|
90
|
+
table.insert(reply, safeIntegerString(recoveredVectorLiableCount))
|
|
91
|
+
return reply
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
local expiredReservations = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now, 'LIMIT', 0, cleanupLimit)
|
|
95
|
+
local expiredOperations = redis.call('ZRANGEBYSCORE', KEYS[5], '-inf', now, 'LIMIT', 0, cleanupLimit)
|
|
96
|
+
|
|
97
|
+
-- Redis Lua errors do not roll back writes already performed by a script. Validate
|
|
98
|
+
-- every record in this cleanup batch before mutating any accounting state.
|
|
99
|
+
for _, rid in ipairs(expiredReservations) do
|
|
100
|
+
local raw = redis.call('HGET', KEYS[3], rid)
|
|
101
|
+
if raw then
|
|
102
|
+
local record = cjson.decode(raw)
|
|
103
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
for _, op in ipairs(expiredOperations) do
|
|
107
|
+
local rid = redis.call('HGET', KEYS[4], op)
|
|
108
|
+
if rid then
|
|
109
|
+
local raw = redis.call('HGET', KEYS[3], rid)
|
|
110
|
+
if raw then
|
|
111
|
+
local record = cjson.decode(raw)
|
|
112
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
for _, rid in ipairs(expiredReservations) do
|
|
118
|
+
local raw = redis.call('HGET', KEYS[3], rid)
|
|
119
|
+
if raw then
|
|
120
|
+
local record = cjson.decode(raw)
|
|
121
|
+
if record.state == 'pending' then
|
|
122
|
+
releaseRecord(record)
|
|
123
|
+
redis.call('HDEL', KEYS[4], record.operationKey)
|
|
124
|
+
redis.call('HDEL', KEYS[3], rid)
|
|
125
|
+
if isVector(record) then
|
|
126
|
+
recoveredVectorPendingCount = saturatingSafeAdd(recoveredVectorPendingCount, 1)
|
|
127
|
+
else
|
|
128
|
+
recoveredPendingCount = saturatingSafeAdd(recoveredPendingCount, 1)
|
|
129
|
+
recoveredPendingUnits = saturatingSafeAdd(recoveredPendingUnits, tonumber(record.reservedUnits))
|
|
130
|
+
end
|
|
131
|
+
elseif record.state == 'liable' then
|
|
132
|
+
retainLiable(record)
|
|
133
|
+
redis.call('HSET', KEYS[3], rid, cjson.encode(record))
|
|
134
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
135
|
+
if isVector(record) then
|
|
136
|
+
recoveredVectorLiableCount = saturatingSafeAdd(recoveredVectorLiableCount, 1)
|
|
137
|
+
else
|
|
138
|
+
recoveredLiableCount = saturatingSafeAdd(recoveredLiableCount, 1)
|
|
139
|
+
recoveredLiableUnits = saturatingSafeAdd(recoveredLiableUnits, tonumber(record.reservedUnits))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
redis.call('ZREM', KEYS[2], rid)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
for _, op in ipairs(expiredOperations) do
|
|
147
|
+
local rid = redis.call('HGET', KEYS[4], op)
|
|
148
|
+
if rid then
|
|
149
|
+
local raw = redis.call('HGET', KEYS[3], rid)
|
|
150
|
+
if raw then
|
|
151
|
+
local record = cjson.decode(raw)
|
|
152
|
+
if record.state == 'settled' then redis.call('HDEL', KEYS[3], rid) end
|
|
153
|
+
end
|
|
154
|
+
redis.call('HDEL', KEYS[4], op)
|
|
155
|
+
end
|
|
156
|
+
redis.call('ZREM', KEYS[5], op)
|
|
157
|
+
end
|
|
158
|
+
`;
|
|
159
|
+
export const RESERVE_SCRIPT = String.raw `
|
|
160
|
+
local units = tonumber(ARGV[1])
|
|
161
|
+
local ttlMs = tonumber(ARGV[2])
|
|
162
|
+
local reservationId = ARGV[3]
|
|
163
|
+
local operationKey = ARGV[4]
|
|
164
|
+
local cleanupLimit = tonumber(ARGV[5])
|
|
165
|
+
local idempotencyTtlMs = tonumber(ARGV[6])
|
|
166
|
+
local budgets = cjson.decode(ARGV[7])
|
|
167
|
+
local growthCursor = ARGV[8]
|
|
168
|
+
local redisTime = redis.call('TIME')
|
|
169
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
170
|
+
${COMMON}
|
|
171
|
+
local expiresAt = safeTimeAdd(now, ttlMs)
|
|
172
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
173
|
+
if not expiresAt or not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
174
|
+
${CLEANUP}
|
|
175
|
+
if redis.call('HEXISTS', KEYS[4], operationKey) == 1 then return withRecovery({ 'duplicate_operation' }) end
|
|
176
|
+
local remainingByHash = {}
|
|
177
|
+
for _, budget in ipairs(budgets) do
|
|
178
|
+
local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
|
|
179
|
+
local remaining = tonumber(budget.limit) - used
|
|
180
|
+
if remaining < 0 then remaining = 0 end
|
|
181
|
+
remainingByHash[budget.hash] = remaining
|
|
182
|
+
if units > remaining then return withRecovery({ 'quota_exceeded', budget.hash, tostring(remaining) }) end
|
|
183
|
+
end
|
|
184
|
+
local budgetHashes = {}
|
|
185
|
+
local reply = { 'accepted' }
|
|
186
|
+
table.insert(reply, tostring(expiresAt))
|
|
187
|
+
for _, budget in ipairs(budgets) do
|
|
188
|
+
table.insert(budgetHashes, budget.hash)
|
|
189
|
+
if units > 0 then redis.call('HINCRBY', KEYS[1], budget.hash, units) end
|
|
190
|
+
table.insert(reply, budget.hash)
|
|
191
|
+
table.insert(reply, tostring(remainingByHash[budget.hash] - units))
|
|
192
|
+
end
|
|
193
|
+
local record = cjson.encode({
|
|
194
|
+
schemaVersion = REDIS_RESERVATION_SCHEMA_VERSION,
|
|
195
|
+
state = 'pending', operationKey = operationKey, reservedUnits = units,
|
|
196
|
+
expiresAt = expiresAt, budgetHashes = budgetHashes, growthCursor = growthCursor
|
|
197
|
+
})
|
|
198
|
+
redis.call('HSET', KEYS[3], reservationId, record)
|
|
199
|
+
redis.call('HSET', KEYS[4], operationKey, reservationId)
|
|
200
|
+
redis.call('ZADD', KEYS[2], expiresAt, reservationId)
|
|
201
|
+
return withRecovery(reply)
|
|
202
|
+
`;
|
|
203
|
+
export const RESERVE_VECTOR_SCRIPT = String.raw `
|
|
204
|
+
local ttlMs = tonumber(ARGV[1])
|
|
205
|
+
local reservationId = ARGV[2]
|
|
206
|
+
local operationKey = ARGV[3]
|
|
207
|
+
local cleanupLimit = tonumber(ARGV[4])
|
|
208
|
+
local idempotencyTtlMs = tonumber(ARGV[5])
|
|
209
|
+
local dimensions = cjson.decode(ARGV[6])
|
|
210
|
+
local growthCursor = ARGV[7]
|
|
211
|
+
local redisTime = redis.call('TIME')
|
|
212
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
213
|
+
${COMMON}
|
|
214
|
+
local expiresAt = safeTimeAdd(now, ttlMs)
|
|
215
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
216
|
+
if not expiresAt or not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
217
|
+
${CLEANUP}
|
|
218
|
+
if redis.call('HEXISTS', KEYS[4], operationKey) == 1 then return withRecovery({ 'duplicate_operation' }) end
|
|
219
|
+
local balances = {}
|
|
220
|
+
for _, dimension in ipairs(dimensions) do
|
|
221
|
+
for _, budget in ipairs(dimension.budgets) do
|
|
222
|
+
local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
|
|
223
|
+
local remaining = tonumber(budget.limit) - used
|
|
224
|
+
if remaining < 0 then remaining = 0 end
|
|
225
|
+
table.insert(balances, { dimensionHash = dimension.hash, budgetHash = budget.hash, remaining = remaining - tonumber(dimension.units) })
|
|
226
|
+
if tonumber(dimension.units) > remaining then
|
|
227
|
+
return withRecovery({ 'quota_exceeded', dimension.hash, budget.hash, tostring(remaining) })
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
local storedDimensions = {}
|
|
232
|
+
for _, dimension in ipairs(dimensions) do
|
|
233
|
+
local hashes = {}
|
|
234
|
+
for _, budget in ipairs(dimension.budgets) do
|
|
235
|
+
table.insert(hashes, budget.hash)
|
|
236
|
+
if tonumber(dimension.units) > 0 then redis.call('HINCRBY', KEYS[1], budget.hash, tonumber(dimension.units)) end
|
|
237
|
+
end
|
|
238
|
+
table.insert(storedDimensions, { hash = dimension.hash, reservedUnits = tonumber(dimension.units), budgetHashes = hashes })
|
|
239
|
+
end
|
|
240
|
+
local record = cjson.encode({
|
|
241
|
+
schemaVersion = REDIS_RESERVATION_SCHEMA_VERSION,
|
|
242
|
+
mode = 'vector', state = 'pending', operationKey = operationKey,
|
|
243
|
+
dimensions = storedDimensions, expiresAt = expiresAt, growthCursor = growthCursor
|
|
244
|
+
})
|
|
245
|
+
redis.call('HSET', KEYS[3], reservationId, record)
|
|
246
|
+
redis.call('HSET', KEYS[4], operationKey, reservationId)
|
|
247
|
+
redis.call('ZADD', KEYS[2], expiresAt, reservationId)
|
|
248
|
+
return withRecovery({ 'accepted', tostring(expiresAt), cjson.encode(balances) })
|
|
249
|
+
`;
|
|
250
|
+
export const MARK_LIABLE_SCRIPT = String.raw `
|
|
251
|
+
local reservationId = ARGV[1]
|
|
252
|
+
local idempotencyTtlMs = tonumber(ARGV[2])
|
|
253
|
+
local redisTime = redis.call('TIME')
|
|
254
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
255
|
+
${COMMON}
|
|
256
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
257
|
+
if not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
258
|
+
local raw = redis.call('HGET', KEYS[3], reservationId)
|
|
259
|
+
if not raw then return { 'not_found' } end
|
|
260
|
+
local record = cjson.decode(raw)
|
|
261
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
262
|
+
if record.state == 'settled' then return { 'not_pending' } end
|
|
263
|
+
if tonumber(record.expiresAt) <= now then
|
|
264
|
+
local expiredState = record.state
|
|
265
|
+
if record.state == 'pending' then
|
|
266
|
+
releaseRecord(record)
|
|
267
|
+
redis.call('HDEL', KEYS[4], record.operationKey)
|
|
268
|
+
redis.call('HDEL', KEYS[3], reservationId)
|
|
269
|
+
elseif record.state == 'liable' then
|
|
270
|
+
retainLiable(record)
|
|
271
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
272
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
273
|
+
end
|
|
274
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
275
|
+
return directExpiryReply(record, expiredState)
|
|
276
|
+
end
|
|
277
|
+
if record.state == 'pending' then
|
|
278
|
+
record.state = 'liable'
|
|
279
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
280
|
+
elseif record.state ~= 'liable' then return { 'not_pending' } end
|
|
281
|
+
return { 'marked', tostring(record.expiresAt) }
|
|
282
|
+
`;
|
|
283
|
+
export const RENEW_SCRIPT = String.raw `
|
|
284
|
+
local ttlMs = tonumber(ARGV[1])
|
|
285
|
+
local reservationId = ARGV[2]
|
|
286
|
+
local idempotencyTtlMs = tonumber(ARGV[3])
|
|
287
|
+
local redisTime = redis.call('TIME')
|
|
288
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
289
|
+
${COMMON}
|
|
290
|
+
local expiresAt = safeTimeAdd(now, ttlMs)
|
|
291
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
292
|
+
if not expiresAt or not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
293
|
+
local raw = redis.call('HGET', KEYS[3], reservationId)
|
|
294
|
+
if not raw then return { 'not_found' } end
|
|
295
|
+
local record = cjson.decode(raw)
|
|
296
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
297
|
+
if record.state == 'settled' then return { 'not_pending' } end
|
|
298
|
+
if tonumber(record.expiresAt) <= now then
|
|
299
|
+
local expiredState = record.state
|
|
300
|
+
if record.state == 'pending' then
|
|
301
|
+
releaseRecord(record)
|
|
302
|
+
redis.call('HDEL', KEYS[4], record.operationKey)
|
|
303
|
+
redis.call('HDEL', KEYS[3], reservationId)
|
|
304
|
+
elseif record.state == 'liable' then
|
|
305
|
+
retainLiable(record)
|
|
306
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
307
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
308
|
+
end
|
|
309
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
310
|
+
return directExpiryReply(record, expiredState)
|
|
311
|
+
end
|
|
312
|
+
if record.state ~= 'pending' and record.state ~= 'liable' then return { 'not_pending' } end
|
|
313
|
+
record.expiresAt = expiresAt
|
|
314
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
315
|
+
redis.call('ZADD', KEYS[2], expiresAt, reservationId)
|
|
316
|
+
return { 'renewed', tostring(expiresAt) }
|
|
317
|
+
`;
|
|
318
|
+
export const SETTLE_SCRIPT = String.raw `
|
|
319
|
+
local reservationId = ARGV[1]
|
|
320
|
+
local actualUnits = tonumber(ARGV[2])
|
|
321
|
+
local outcome = ARGV[3]
|
|
322
|
+
local idempotencyTtlMs = tonumber(ARGV[4])
|
|
323
|
+
local redisTime = redis.call('TIME')
|
|
324
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
325
|
+
${COMMON}
|
|
326
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
327
|
+
if not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
328
|
+
local raw = redis.call('HGET', KEYS[3], reservationId)
|
|
329
|
+
if not raw then return { 'not_found' } end
|
|
330
|
+
local record = cjson.decode(raw)
|
|
331
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
332
|
+
if isVector(record) then return { 'mode_mismatch' } end
|
|
333
|
+
local reservedUnits = tonumber(record.reservedUnits)
|
|
334
|
+
if record.state == 'settled' then
|
|
335
|
+
if tonumber(record.actualUnits) == actualUnits and record.outcome == outcome then
|
|
336
|
+
return { 'idempotent', tostring(reservedUnits), tostring(actualUnits), tostring(reservedUnits - actualUnits) }
|
|
337
|
+
end
|
|
338
|
+
return { 'conflict' }
|
|
339
|
+
end
|
|
340
|
+
if record.state ~= 'pending' and record.state ~= 'liable' then return { 'not_pending' } end
|
|
341
|
+
if tonumber(record.expiresAt) <= now then
|
|
342
|
+
local expiredState = record.state
|
|
343
|
+
if record.state == 'pending' then
|
|
344
|
+
releaseRecord(record)
|
|
345
|
+
redis.call('HDEL', KEYS[4], record.operationKey)
|
|
346
|
+
redis.call('HDEL', KEYS[3], reservationId)
|
|
347
|
+
else
|
|
348
|
+
retainLiable(record)
|
|
349
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
350
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
351
|
+
end
|
|
352
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
353
|
+
return directExpiryReply(record, expiredState)
|
|
354
|
+
end
|
|
355
|
+
if actualUnits > reservedUnits then return { 'invalid_units' } end
|
|
356
|
+
local released = reservedUnits - actualUnits
|
|
357
|
+
subtractUsed(record.budgetHashes, released)
|
|
358
|
+
record.state = 'settled'
|
|
359
|
+
record.actualUnits = actualUnits
|
|
360
|
+
record.outcome = outcome
|
|
361
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
362
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
363
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
364
|
+
return { 'settled', tostring(reservedUnits), tostring(actualUnits), tostring(released) }
|
|
365
|
+
`;
|
|
366
|
+
export const SETTLE_VECTOR_SCRIPT = String.raw `
|
|
367
|
+
local reservationId = ARGV[1]
|
|
368
|
+
local actuals = cjson.decode(ARGV[2])
|
|
369
|
+
local outcome = ARGV[3]
|
|
370
|
+
local idempotencyTtlMs = tonumber(ARGV[4])
|
|
371
|
+
local redisTime = redis.call('TIME')
|
|
372
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
373
|
+
${COMMON}
|
|
374
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
375
|
+
if not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
376
|
+
local raw = redis.call('HGET', KEYS[3], reservationId)
|
|
377
|
+
if not raw then return { 'not_found' } end
|
|
378
|
+
local record = cjson.decode(raw)
|
|
379
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
380
|
+
if not isVector(record) then return { 'mode_mismatch' } end
|
|
381
|
+
local function sameActuals(left, right)
|
|
382
|
+
if not left or #left ~= #right then return false end
|
|
383
|
+
for index, item in ipairs(right) do
|
|
384
|
+
if left[index].hash ~= item.hash or tonumber(left[index].actualUnits) ~= tonumber(item.actualUnits) then return false end
|
|
385
|
+
end
|
|
386
|
+
return true
|
|
387
|
+
end
|
|
388
|
+
local function settlementJson()
|
|
389
|
+
local result = {}
|
|
390
|
+
for index, dimension in ipairs(record.dimensions) do
|
|
391
|
+
local actual = tonumber(actuals[index].actualUnits)
|
|
392
|
+
local reserved = tonumber(dimension.reservedUnits)
|
|
393
|
+
table.insert(result, { hash = dimension.hash, reservedUnits = reserved, actualUnits = actual, releasedUnits = reserved - actual })
|
|
394
|
+
end
|
|
395
|
+
return cjson.encode(result)
|
|
396
|
+
end
|
|
397
|
+
if record.state == 'settled' then
|
|
398
|
+
if record.outcome == outcome and sameActuals(record.actualByDimensions, actuals) then return { 'idempotent', settlementJson() } end
|
|
399
|
+
return { 'conflict' }
|
|
400
|
+
end
|
|
401
|
+
if record.state ~= 'pending' and record.state ~= 'liable' then return { 'not_pending' } end
|
|
402
|
+
if tonumber(record.expiresAt) <= now then
|
|
403
|
+
local expiredState = record.state
|
|
404
|
+
if record.state == 'pending' then
|
|
405
|
+
releaseRecord(record)
|
|
406
|
+
redis.call('HDEL', KEYS[4], record.operationKey)
|
|
407
|
+
redis.call('HDEL', KEYS[3], reservationId)
|
|
408
|
+
else
|
|
409
|
+
retainLiable(record)
|
|
410
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
411
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
412
|
+
end
|
|
413
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
414
|
+
return directExpiryReply(record, expiredState)
|
|
415
|
+
end
|
|
416
|
+
if #actuals ~= #record.dimensions then return { 'dimension_mismatch' } end
|
|
417
|
+
for index, actual in ipairs(actuals) do
|
|
418
|
+
local dimension = record.dimensions[index]
|
|
419
|
+
if actual.hash ~= dimension.hash then return { 'dimension_mismatch' } end
|
|
420
|
+
if tonumber(actual.actualUnits) > tonumber(dimension.reservedUnits) then return { 'invalid_units' } end
|
|
421
|
+
end
|
|
422
|
+
for index, dimension in ipairs(record.dimensions) do
|
|
423
|
+
local released = tonumber(dimension.reservedUnits) - tonumber(actuals[index].actualUnits)
|
|
424
|
+
subtractUsed(dimension.budgetHashes, released)
|
|
425
|
+
end
|
|
426
|
+
record.state = 'settled'
|
|
427
|
+
record.actualByDimensions = actuals
|
|
428
|
+
record.outcome = outcome
|
|
429
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
430
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
431
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
432
|
+
return { 'settled', settlementJson() }
|
|
433
|
+
`;
|
|
434
|
+
export const GROW_SCRIPT = String.raw `
|
|
435
|
+
local reservationId = ARGV[1]
|
|
436
|
+
local incrementHash = ARGV[2]
|
|
437
|
+
local expectedGrowthCursor = ARGV[3]
|
|
438
|
+
local additionalUnits = tonumber(ARGV[4])
|
|
439
|
+
local idempotencyTtlMs = tonumber(ARGV[5])
|
|
440
|
+
local budgets = cjson.decode(ARGV[6])
|
|
441
|
+
local fingerprint = ARGV[7]
|
|
442
|
+
local nextGrowthCursor = ARGV[8]
|
|
443
|
+
local redisTime = redis.call('TIME')
|
|
444
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
445
|
+
${COMMON}
|
|
446
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
447
|
+
if not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
448
|
+
local raw = redis.call('HGET', KEYS[3], reservationId)
|
|
449
|
+
if not raw then return { 'not_found' } end
|
|
450
|
+
local record = cjson.decode(raw)
|
|
451
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
452
|
+
if isVector(record) then return { 'mode_mismatch' } end
|
|
453
|
+
local reservedUnits = tonumber(record.reservedUnits)
|
|
454
|
+
if record.state == 'settled' or (record.state ~= 'pending' and record.state ~= 'liable') then return { 'terminal' } end
|
|
455
|
+
if tonumber(record.expiresAt) <= now then
|
|
456
|
+
local expiredState = record.state
|
|
457
|
+
if record.state == 'pending' then
|
|
458
|
+
releaseRecord(record)
|
|
459
|
+
redis.call('HDEL', KEYS[4], record.operationKey)
|
|
460
|
+
redis.call('HDEL', KEYS[3], reservationId)
|
|
461
|
+
else
|
|
462
|
+
retainLiable(record)
|
|
463
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
464
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
465
|
+
end
|
|
466
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
467
|
+
return directExpiryReply(record, expiredState)
|
|
468
|
+
end
|
|
469
|
+
local lastGrowth = record.lastGrowth
|
|
470
|
+
if lastGrowth and lastGrowth.incrementHash == incrementHash then
|
|
471
|
+
if lastGrowth.expectedGrowthCursor ~= expectedGrowthCursor or lastGrowth.fingerprint ~= fingerprint then return { 'conflict' } end
|
|
472
|
+
if lastGrowth.accepted == true then
|
|
473
|
+
return { 'accepted_replay', tostring(lastGrowth.previousReservedUnits), tostring(lastGrowth.reservedUnits), lastGrowth.nextGrowthCursor, cjson.encode(lastGrowth.remainingByHashes) }
|
|
474
|
+
end
|
|
475
|
+
return { 'quota_replay', lastGrowth.nextGrowthCursor, lastGrowth.limitingBudgetHash, tostring(lastGrowth.remaining) }
|
|
476
|
+
end
|
|
477
|
+
if not record.growthCursor then return { 'not_supported' } end
|
|
478
|
+
if record.growthCursor ~= expectedGrowthCursor then return { 'stale_cursor' } end
|
|
479
|
+
if #record.budgetHashes ~= #budgets then return { 'budget_mismatch' } end
|
|
480
|
+
for index, budget in ipairs(budgets) do if record.budgetHashes[index] ~= budget.hash then return { 'budget_mismatch' } end end
|
|
481
|
+
local remainingByHashes = {}
|
|
482
|
+
local limitingBudgetHash = nil
|
|
483
|
+
local limitingRemaining = nil
|
|
484
|
+
for _, budget in ipairs(budgets) do
|
|
485
|
+
local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
|
|
486
|
+
local remaining = tonumber(budget.limit) - used
|
|
487
|
+
if remaining < 0 then remaining = 0 end
|
|
488
|
+
table.insert(remainingByHashes, { hash = budget.hash, remaining = remaining - additionalUnits })
|
|
489
|
+
if not limitingBudgetHash and additionalUnits > remaining then limitingBudgetHash = budget.hash; limitingRemaining = remaining end
|
|
490
|
+
end
|
|
491
|
+
if limitingBudgetHash then
|
|
492
|
+
record.growthCursor = nextGrowthCursor
|
|
493
|
+
record.lastGrowth = {
|
|
494
|
+
accepted = false, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
|
|
495
|
+
fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
|
|
496
|
+
limitingBudgetHash = limitingBudgetHash, remaining = limitingRemaining
|
|
497
|
+
}
|
|
498
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
499
|
+
return { 'quota_exceeded', nextGrowthCursor, limitingBudgetHash, tostring(limitingRemaining) }
|
|
500
|
+
end
|
|
501
|
+
local previousReservedUnits = reservedUnits
|
|
502
|
+
local nextReservedUnits = reservedUnits + additionalUnits
|
|
503
|
+
for _, budget in ipairs(budgets) do redis.call('HINCRBY', KEYS[1], budget.hash, additionalUnits) end
|
|
504
|
+
record.reservedUnits = nextReservedUnits
|
|
505
|
+
record.growthCursor = nextGrowthCursor
|
|
506
|
+
record.lastGrowth = {
|
|
507
|
+
accepted = true, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
|
|
508
|
+
fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
|
|
509
|
+
previousReservedUnits = previousReservedUnits, reservedUnits = nextReservedUnits,
|
|
510
|
+
remainingByHashes = remainingByHashes
|
|
511
|
+
}
|
|
512
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
513
|
+
return { 'accepted', tostring(previousReservedUnits), tostring(nextReservedUnits), nextGrowthCursor, cjson.encode(remainingByHashes) }
|
|
514
|
+
`;
|
|
515
|
+
export const GROW_VECTOR_SCRIPT = String.raw `
|
|
516
|
+
local reservationId = ARGV[1]
|
|
517
|
+
local incrementHash = ARGV[2]
|
|
518
|
+
local expectedGrowthCursor = ARGV[3]
|
|
519
|
+
local idempotencyTtlMs = tonumber(ARGV[4])
|
|
520
|
+
local dimensions = cjson.decode(ARGV[5])
|
|
521
|
+
local fingerprint = ARGV[6]
|
|
522
|
+
local nextGrowthCursor = ARGV[7]
|
|
523
|
+
local redisTime = redis.call('TIME')
|
|
524
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
525
|
+
${COMMON}
|
|
526
|
+
local tombstoneExpiresAt = safeTimeAdd(now, idempotencyTtlMs)
|
|
527
|
+
if not tombstoneExpiresAt then return { 'invalid_time' } end
|
|
528
|
+
local raw = redis.call('HGET', KEYS[3], reservationId)
|
|
529
|
+
if not raw then return { 'not_found' } end
|
|
530
|
+
local record = cjson.decode(raw)
|
|
531
|
+
if not hasSupportedRecordSchema(record) then return { 'unsupported_schema_version' } end
|
|
532
|
+
if not isVector(record) then return { 'mode_mismatch' } end
|
|
533
|
+
if record.state == 'settled' or (record.state ~= 'pending' and record.state ~= 'liable') then return { 'terminal' } end
|
|
534
|
+
if tonumber(record.expiresAt) <= now then
|
|
535
|
+
local expiredState = record.state
|
|
536
|
+
if record.state == 'pending' then
|
|
537
|
+
releaseRecord(record)
|
|
538
|
+
redis.call('HDEL', KEYS[4], record.operationKey)
|
|
539
|
+
redis.call('HDEL', KEYS[3], reservationId)
|
|
540
|
+
else
|
|
541
|
+
retainLiable(record)
|
|
542
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
543
|
+
redis.call('ZADD', KEYS[5], tombstoneExpiresAt, record.operationKey)
|
|
544
|
+
end
|
|
545
|
+
redis.call('ZREM', KEYS[2], reservationId)
|
|
546
|
+
return directExpiryReply(record, expiredState)
|
|
547
|
+
end
|
|
548
|
+
local lastGrowth = record.lastVectorGrowth
|
|
549
|
+
if lastGrowth and lastGrowth.incrementHash == incrementHash then
|
|
550
|
+
if lastGrowth.expectedGrowthCursor ~= expectedGrowthCursor or lastGrowth.fingerprint ~= fingerprint then return { 'conflict' } end
|
|
551
|
+
if lastGrowth.accepted == true then
|
|
552
|
+
return { 'accepted_replay', lastGrowth.nextGrowthCursor, cjson.encode(lastGrowth.previousReservedByDimensions), cjson.encode(lastGrowth.reservedByDimensions), cjson.encode(lastGrowth.remainingByHashes) }
|
|
553
|
+
end
|
|
554
|
+
return { 'quota_replay', lastGrowth.nextGrowthCursor, lastGrowth.limitingDimensionHash, lastGrowth.limitingBudgetHash, tostring(lastGrowth.remaining) }
|
|
555
|
+
end
|
|
556
|
+
if not record.growthCursor then return { 'not_supported' } end
|
|
557
|
+
if record.growthCursor ~= expectedGrowthCursor then return { 'stale_cursor' } end
|
|
558
|
+
if #record.dimensions ~= #dimensions then return { 'dimension_mismatch' } end
|
|
559
|
+
for index, dimension in ipairs(dimensions) do
|
|
560
|
+
local stored = record.dimensions[index]
|
|
561
|
+
if stored.hash ~= dimension.hash or #stored.budgetHashes ~= #dimension.budgets then return { 'dimension_mismatch' } end
|
|
562
|
+
for budgetIndex, budget in ipairs(dimension.budgets) do
|
|
563
|
+
if stored.budgetHashes[budgetIndex] ~= budget.hash then return { 'dimension_mismatch' } end
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
local remainingByHashes = {}
|
|
567
|
+
local limitingDimensionHash = nil
|
|
568
|
+
local limitingBudgetHash = nil
|
|
569
|
+
local limitingRemaining = nil
|
|
570
|
+
for _, dimension in ipairs(dimensions) do
|
|
571
|
+
for _, budget in ipairs(dimension.budgets) do
|
|
572
|
+
local used = tonumber(redis.call('HGET', KEYS[1], budget.hash) or '0')
|
|
573
|
+
local remaining = tonumber(budget.limit) - used
|
|
574
|
+
if remaining < 0 then remaining = 0 end
|
|
575
|
+
table.insert(remainingByHashes, { dimensionHash = dimension.hash, budgetHash = budget.hash, remaining = remaining - tonumber(dimension.additionalUnits) })
|
|
576
|
+
if not limitingBudgetHash and tonumber(dimension.additionalUnits) > remaining then
|
|
577
|
+
limitingDimensionHash = dimension.hash
|
|
578
|
+
limitingBudgetHash = budget.hash
|
|
579
|
+
limitingRemaining = remaining
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
if limitingBudgetHash then
|
|
584
|
+
record.growthCursor = nextGrowthCursor
|
|
585
|
+
record.lastVectorGrowth = {
|
|
586
|
+
accepted = false, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
|
|
587
|
+
fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
|
|
588
|
+
limitingDimensionHash = limitingDimensionHash, limitingBudgetHash = limitingBudgetHash,
|
|
589
|
+
remaining = limitingRemaining
|
|
590
|
+
}
|
|
591
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
592
|
+
return { 'quota_exceeded', nextGrowthCursor, limitingDimensionHash, limitingBudgetHash, tostring(limitingRemaining) }
|
|
593
|
+
end
|
|
594
|
+
local previous = {}
|
|
595
|
+
local current = {}
|
|
596
|
+
for index, dimension in ipairs(dimensions) do
|
|
597
|
+
local stored = record.dimensions[index]
|
|
598
|
+
table.insert(previous, { hash = stored.hash, reservedUnits = tonumber(stored.reservedUnits) })
|
|
599
|
+
local nextReserved = tonumber(stored.reservedUnits) + tonumber(dimension.additionalUnits)
|
|
600
|
+
for _, budget in ipairs(dimension.budgets) do
|
|
601
|
+
if tonumber(dimension.additionalUnits) > 0 then redis.call('HINCRBY', KEYS[1], budget.hash, tonumber(dimension.additionalUnits)) end
|
|
602
|
+
end
|
|
603
|
+
stored.reservedUnits = nextReserved
|
|
604
|
+
table.insert(current, { hash = stored.hash, reservedUnits = nextReserved })
|
|
605
|
+
end
|
|
606
|
+
record.growthCursor = nextGrowthCursor
|
|
607
|
+
record.lastVectorGrowth = {
|
|
608
|
+
accepted = true, incrementHash = incrementHash, expectedGrowthCursor = expectedGrowthCursor,
|
|
609
|
+
fingerprint = fingerprint, nextGrowthCursor = nextGrowthCursor,
|
|
610
|
+
previousReservedByDimensions = previous, reservedByDimensions = current,
|
|
611
|
+
remainingByHashes = remainingByHashes
|
|
612
|
+
}
|
|
613
|
+
redis.call('HSET', KEYS[3], reservationId, cjson.encode(record))
|
|
614
|
+
return { 'accepted', nextGrowthCursor, cjson.encode(previous), cjson.encode(current), cjson.encode(remainingByHashes) }
|
|
615
|
+
`;
|
|
616
|
+
export const RECONCILE_OPERATION_SCRIPT = String.raw `
|
|
617
|
+
local reservationId = ARGV[1]
|
|
618
|
+
local redisTime = redis.call('TIME')
|
|
619
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
620
|
+
local raw = redis.call('HGET', KEYS[1], reservationId)
|
|
621
|
+
if not raw then return { 'absent' } end
|
|
622
|
+
local record = cjson.decode(raw)
|
|
623
|
+
if record.schemaVersion ~= nil and record.schemaVersion ~= 1 then return { 'unsupported_schema_version' } end
|
|
624
|
+
if record.mode == 'vector' then return { 'mode_mismatch' } end
|
|
625
|
+
local state = record.state
|
|
626
|
+
local reservedUnits = tonumber(record.reservedUnits)
|
|
627
|
+
local expiresAt = tonumber(record.expiresAt)
|
|
628
|
+
local budgetHashes = cjson.encode(record.budgetHashes or {})
|
|
629
|
+
local growthCursor = record.growthCursor or ''
|
|
630
|
+
if state == 'pending' or state == 'liable' then
|
|
631
|
+
if expiresAt <= now then
|
|
632
|
+
return { 'expired', state, tostring(reservedUnits), tostring(expiresAt), budgetHashes, growthCursor }
|
|
633
|
+
end
|
|
634
|
+
return { 'active', state, tostring(reservedUnits), tostring(expiresAt), budgetHashes, growthCursor }
|
|
635
|
+
end
|
|
636
|
+
if state ~= 'settled' then return { 'invalid_state' } end
|
|
637
|
+
local tombstoneScore = redis.call('ZSCORE', KEYS[2], record.operationKey)
|
|
638
|
+
if not tombstoneScore then return { 'invalid_state' } end
|
|
639
|
+
local tombstoneExpiresAt = tonumber(tombstoneScore)
|
|
640
|
+
if tombstoneExpiresAt <= now then return { 'absent' } end
|
|
641
|
+
if record.outcome == 'lease_expired_after_execution_started' then
|
|
642
|
+
return { 'expired', 'liable', tostring(reservedUnits), tostring(expiresAt), budgetHashes, growthCursor }
|
|
643
|
+
end
|
|
644
|
+
local actualUnits = tonumber(record.actualUnits)
|
|
645
|
+
if not actualUnits then return { 'invalid_state' } end
|
|
646
|
+
return { 'settled', tostring(reservedUnits), tostring(actualUnits), tostring(tombstoneExpiresAt), budgetHashes }
|
|
647
|
+
`;
|
|
648
|
+
export const RECONCILE_VECTOR_OPERATION_SCRIPT = String.raw `
|
|
649
|
+
local reservationId = ARGV[1]
|
|
650
|
+
local redisTime = redis.call('TIME')
|
|
651
|
+
local now = tonumber(redisTime[1]) * 1000 + math.floor(tonumber(redisTime[2]) / 1000)
|
|
652
|
+
local raw = redis.call('HGET', KEYS[1], reservationId)
|
|
653
|
+
if not raw then return { 'absent' } end
|
|
654
|
+
local record = cjson.decode(raw)
|
|
655
|
+
if record.schemaVersion ~= nil and record.schemaVersion ~= 1 then return { 'unsupported_schema_version' } end
|
|
656
|
+
if record.mode ~= 'vector' then return { 'mode_mismatch' } end
|
|
657
|
+
local state = record.state
|
|
658
|
+
local expiresAt = tonumber(record.expiresAt)
|
|
659
|
+
local dimensions = cjson.encode(record.dimensions or {})
|
|
660
|
+
local growthCursor = record.growthCursor or ''
|
|
661
|
+
if state == 'pending' or state == 'liable' then
|
|
662
|
+
if expiresAt <= now then
|
|
663
|
+
return { 'expired', state, tostring(expiresAt), dimensions, growthCursor }
|
|
664
|
+
end
|
|
665
|
+
return { 'active', state, tostring(expiresAt), dimensions, growthCursor }
|
|
666
|
+
end
|
|
667
|
+
if state ~= 'settled' then return { 'invalid_state' } end
|
|
668
|
+
local tombstoneScore = redis.call('ZSCORE', KEYS[2], record.operationKey)
|
|
669
|
+
if not tombstoneScore then return { 'invalid_state' } end
|
|
670
|
+
local tombstoneExpiresAt = tonumber(tombstoneScore)
|
|
671
|
+
if tombstoneExpiresAt <= now then return { 'absent' } end
|
|
672
|
+
if record.outcome == 'lease_expired_after_execution_started' then
|
|
673
|
+
return { 'expired', 'liable', tostring(expiresAt), dimensions, growthCursor }
|
|
674
|
+
end
|
|
675
|
+
local actual = cjson.encode(record.actualByDimensions or {})
|
|
676
|
+
return { 'settled', tostring(tombstoneExpiresAt), dimensions, actual }
|
|
677
|
+
`;
|
|
678
|
+
export const RETIRE_HISTORICAL_BUDGETS_SCRIPT = String.raw `
|
|
679
|
+
local candidates = cjson.decode(ARGV[1])
|
|
680
|
+
local maxReservations = tonumber(ARGV[2])
|
|
681
|
+
local reservationCount = redis.call('HLEN', KEYS[2])
|
|
682
|
+
if reservationCount > maxReservations then
|
|
683
|
+
return { 'scan_limit', tostring(reservationCount) }
|
|
684
|
+
end
|
|
685
|
+
local candidateSet = {}
|
|
686
|
+
for _, hash in ipairs(candidates) do candidateSet[hash] = true end
|
|
687
|
+
local values = redis.call('HVALS', KEYS[2])
|
|
688
|
+
for _, raw in ipairs(values) do
|
|
689
|
+
local record = cjson.decode(raw)
|
|
690
|
+
if record.schemaVersion ~= nil and record.schemaVersion ~= 1 then
|
|
691
|
+
return { 'unsupported_schema_version' }
|
|
692
|
+
end
|
|
693
|
+
if record.state == 'pending' or record.state == 'liable' then
|
|
694
|
+
if record.mode == 'vector' then
|
|
695
|
+
for _, dimension in ipairs(record.dimensions or {}) do
|
|
696
|
+
for _, budgetHash in ipairs(dimension.budgetHashes or {}) do
|
|
697
|
+
if candidateSet[budgetHash] then return { 'active_reference' } end
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
else
|
|
701
|
+
for _, budgetHash in ipairs(record.budgetHashes or {}) do
|
|
702
|
+
if candidateSet[budgetHash] then return { 'active_reference' } end
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
local retired = 0
|
|
708
|
+
local missing = 0
|
|
709
|
+
for _, hash in ipairs(candidates) do
|
|
710
|
+
if redis.call('HEXISTS', KEYS[1], hash) == 1 then
|
|
711
|
+
redis.call('HDEL', KEYS[1], hash)
|
|
712
|
+
retired = retired + 1
|
|
713
|
+
else
|
|
714
|
+
missing = missing + 1
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
return { 'ok', tostring(retired), tostring(missing) }
|
|
718
|
+
`;
|
|
719
|
+
//# sourceMappingURL=scripts.js.map
|