1import * as Fly from "@/Fly";
2
3export const Cache = Fly.Redis("WorkerQueue", { eviction: false });
4export const LedgerSite = Fly.App("LedgerSite");
5export const WorkerSite = Fly.App("WorkerSite");
6
7export const services: Fly.MachineService[] = [
8 {
9 protocol: "tcp",
10 internalPort: 3000,
11 autostop: "off",
12 ports: [{ port: 443, handlers: ["tls", "http"] }],
13 checks: [
14 {
15 type: "http",
16 port: 3000,
17 path: "/health",
18 interval: "2s",
19 timeout: "1s",
20 },
21 ],
22 },
23];
24
25export const scripts = {
26 event: `
27local t=redis.call('TIME')
28local e=cjson.decode(ARGV[1]); e.at=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
29redis.call('RPUSH','bg:events',cjson.encode(e)); return e.at`,
30 enqueue: `
31if redis.call('SADD','bg:produced',ARGV[1])==0 then return 0 end
32return redis.call('XADD','bg:jobs','*','job',ARGV[1],'kind',ARGV[2])`,
33 pause: `return redis.call('SET','bg:producer-paused','1')`,
34 tick: `
35if redis.call('EXISTS','bg:producer-paused')==1 then return 0 end
36local t=redis.call('TIME'); local slot='tick:'..t[1]; local fresh=redis.call('SADD','bg:produced',slot)
37if fresh==1 then redis.call('XADD','bg:jobs','*','job',slot,'kind','quick') end
38redis.call('RPUSH','bg:events',cjson.encode({event='producer',machine=ARGV[1],version=ARGV[2],job=slot,fresh=fresh,at=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)})); return fresh`,
39 claim: `
40redis.pcall('XGROUP','CREATE','bg:jobs','workers','0','MKSTREAM')
41local id=redis.call('SPOP','bg:reclaim')
42local rows
43if id then rows=redis.call('XCLAIM','bg:jobs','workers',ARGV[1],0,id)
44else local read=redis.call('XREADGROUP','GROUP','workers',ARGV[1],'COUNT',1,'STREAMS','bg:jobs','>'); if read then rows=read[1][2] end end
45if not rows or #rows==0 then return nil end
46local row=rows[1]; local fields=row[2]; local job=fields[2]; local kind=fields[4]
47local t=redis.call('TIME'); local at=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
48redis.call('RPUSH','bg:events',cjson.encode({event=id and 'reclaimed' or 'claimed',machine=ARGV[1],version=ARGV[2],job=job,id=row[1],at=at}))
49return cjson.encode({id=row[1],job=job,kind=kind,checkpoint=redis.call('HGET','bg:checkpoints',job) or ''})`,
50 finish: `
51local fresh=redis.call('HSETNX','bg:results',ARGV[2],ARGV[3])
52local ack=redis.call('XACK','bg:jobs','workers',ARGV[1])
53local t=redis.call('TIME'); local at=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
54redis.call('RPUSH','bg:events',cjson.encode({event='acked',machine=ARGV[3],job=ARGV[2],id=ARGV[1],fresh=fresh,ack=ack,at=at})); return ack`,
55 checkpoint: `
56redis.call('HSET','bg:checkpoints',ARGV[2],'saved-step-1')
57redis.call('SADD','bg:reclaim',ARGV[1])
58local t=redis.call('TIME'); local at=tonumber(t[1])*1000+math.floor(tonumber(t[2])/1000)
59redis.call('RPUSH','bg:events',cjson.encode({event='checkpoint',machine=ARGV[3],job=ARGV[2],id=ARGV[1],at=at})); return 1`,
60 snapshot: `
61local pending=redis.pcall('XPENDING','bg:jobs','workers')
62return cjson.encode({events=redis.call('LRANGE','bg:events',0,-1),results=redis.call('HGETALL','bg:results'),checkpoints=redis.call('HGETALL','bg:checkpoints'),produced=redis.call('SMEMBERS','bg:produced'),entries=redis.call('XLEN','bg:jobs'),pending=pending.err and 0 or pending[1]})`,
63} as const;
64
65export interface Job {
66 id: string;
67 job: string;
68 kind: string;
69 checkpoint: string;
70}
71export interface LedgerEvent {
72 event: string;
73 machine: string;
74 version?: string;
75 job?: string;
76 id?: string;
77 at: number;
78 ack?: number;
79 fresh?: number;
80 worker?: string;
81 signal?: string;
82}
83export interface Snapshot {
84 events: string[];
85 results: string[];
86 checkpoints: string[];
87 produced: string[];
88 pending: number;
89 entries: number;
90}