@sema-agent/server 1.314.0 → 1.315.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/dist/approval-hmac.d.ts +17 -0
- package/dist/approval-hmac.js +27 -0
- package/dist/config-center/apply-effective.d.ts +22 -0
- package/dist/config-center/apply-effective.js +283 -0
- package/dist/config-center/http-client.d.ts +23 -0
- package/dist/config-center/http-client.js +109 -0
- package/dist/config-center/restart-signal.d.ts +12 -0
- package/dist/config-center/restart-signal.js +70 -0
- package/dist/config-center/skills-mcp.d.ts +13 -0
- package/dist/config-center/skills-mcp.js +113 -0
- package/dist/config-center/types.d.ts +143 -0
- package/dist/config-center/types.js +2 -0
- package/dist/fleet/fleet-bus.js +92 -83
- package/dist/hooks/hook-runner.js +18 -9
- package/dist/http/server.d.ts +95 -69
- package/dist/http/server.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.js +169 -165
- package/dist/main.js +462 -451
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +224 -0
- package/dist/plugins/image-bake-store-sql.d.ts +53 -0
- package/dist/plugins/image-bake-store-sql.js +463 -0
- package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
- package/dist/plugins/k8s-bg-scripts.js +129 -0
- package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
- package/dist/plugins/k8s-exec-protocol.js +87 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
- package/dist/plugins/pg-checkpoint-store.js +1 -189
- package/dist/plugins/pg-cost-quota.js +3 -143
- package/dist/plugins/pg-image-bake.d.ts +1 -40
- package/dist/plugins/pg-image-bake.js +1 -420
- package/dist/plugins/pg-rate-limiter.d.ts +2 -32
- package/dist/plugins/pg-rate-limiter.js +4 -147
- package/dist/plugins/remote-env-k8s.d.ts +5 -38
- package/dist/plugins/remote-env-k8s.js +7 -213
- package/dist/plugins/sql-driver.d.ts +25 -0
- package/dist/plugins/sql-driver.js +59 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
- package/dist/plugins/tidb-checkpoint-store.js +1 -191
- package/dist/plugins/tidb-image-bake.d.ts +1 -38
- package/dist/plugins/tidb-image-bake.js +1 -348
- package/dist/plugins/write-behind-counter.d.ts +14 -3
- package/dist/plugins/write-behind-counter.js +39 -12
- package/dist/principal-jwt.d.ts +44 -0
- package/dist/principal-jwt.js +95 -0
- package/dist/security.d.ts +2 -58
- package/dist/security.js +3 -118
- package/dist/sema-registry.d.ts +5 -200
- package/dist/sema-registry.js +4 -567
- package/package.json +1 -1
|
@@ -1,421 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { pgSanitizeText, pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
|
|
4
|
-
import { parseJsonOr as parseJson, toIso as iso } from "./sql-row-helpers.js";
|
|
5
|
-
import { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS } from "./store-contracts.js";
|
|
6
|
-
export const PG_IMAGE_BAKE_SCHEMA = [
|
|
7
|
-
`CREATE TABLE IF NOT EXISTS image_bake (
|
|
8
|
-
bake_id VARCHAR(64) NOT NULL,
|
|
9
|
-
profile VARCHAR(128) NOT NULL,
|
|
10
|
-
bands JSONB NULL,
|
|
11
|
-
base_ref VARCHAR(255) NULL,
|
|
12
|
-
push SMALLINT NOT NULL DEFAULT 0,
|
|
13
|
-
dry_run SMALLINT NOT NULL DEFAULT 0,
|
|
14
|
-
logs SMALLINT NOT NULL DEFAULT 0,
|
|
15
|
-
argv JSONB NOT NULL,
|
|
16
|
-
status VARCHAR(16) NOT NULL,
|
|
17
|
-
state VARCHAR(16) NULL,
|
|
18
|
-
digest VARCHAR(255) NULL,
|
|
19
|
-
repo VARCHAR(255) NULL,
|
|
20
|
-
ref VARCHAR(320) NULL,
|
|
21
|
-
index_id VARCHAR(64) NULL,
|
|
22
|
-
exit_code INT NULL,
|
|
23
|
-
error TEXT NULL,
|
|
24
|
-
error_code VARCHAR(32) NULL,
|
|
25
|
-
manifest_sha VARCHAR(64) NULL,
|
|
26
|
-
tag VARCHAR(255) NULL,
|
|
27
|
-
idem_key VARCHAR(255) NULL,
|
|
28
|
-
ingest_secret VARCHAR(64) NULL,
|
|
29
|
-
runner_id VARCHAR(64) NULL,
|
|
30
|
-
lease_until TIMESTAMPTZ(3) NULL,
|
|
31
|
-
cancel_requested SMALLINT NOT NULL DEFAULT 0,
|
|
32
|
-
requested_by VARCHAR(128) NULL,
|
|
33
|
-
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
34
|
-
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
35
|
-
PRIMARY KEY (bake_id),
|
|
36
|
-
CONSTRAINT uq_image_bake_idem UNIQUE (idem_key)
|
|
37
|
-
)`,
|
|
38
|
-
`CREATE INDEX IF NOT EXISTS idx_image_bake_status ON image_bake (status)`,
|
|
39
|
-
`CREATE INDEX IF NOT EXISTS idx_image_bake_created ON image_bake (created_at)`,
|
|
40
|
-
`CREATE TABLE IF NOT EXISTS image_bake_event (
|
|
41
|
-
bake_id VARCHAR(64) NOT NULL,
|
|
42
|
-
seq INT NOT NULL,
|
|
43
|
-
line_ord INT NULL,
|
|
44
|
-
kind VARCHAR(24) NOT NULL,
|
|
45
|
-
level VARCHAR(8) NULL,
|
|
46
|
-
data JSONB NULL,
|
|
47
|
-
ts TIMESTAMPTZ(3) NOT NULL,
|
|
48
|
-
PRIMARY KEY (bake_id, seq),
|
|
49
|
-
CONSTRAINT uq_image_bake_event_line UNIQUE (bake_id, line_ord)
|
|
50
|
-
)`,
|
|
51
|
-
`CREATE TABLE IF NOT EXISTS image_bake_lease (
|
|
52
|
-
pool VARCHAR(32) NOT NULL DEFAULT 'standard',
|
|
53
|
-
bake_id VARCHAR(64) NOT NULL,
|
|
54
|
-
acquired_at TIMESTAMPTZ(3) NOT NULL,
|
|
55
|
-
expires_at TIMESTAMPTZ(3) NOT NULL,
|
|
56
|
-
PRIMARY KEY (pool)
|
|
57
|
-
)`,
|
|
58
|
-
`CREATE TABLE IF NOT EXISTS image_bake_admit (
|
|
59
|
-
pool VARCHAR(32) NOT NULL,
|
|
60
|
-
PRIMARY KEY (pool)
|
|
61
|
-
)`,
|
|
62
|
-
];
|
|
63
|
-
export async function ensureSchema(pool, poolName = "standard") {
|
|
64
|
-
for (const stmt of PG_IMAGE_BAKE_SCHEMA)
|
|
65
|
-
await pool.query(stmt);
|
|
66
|
-
await pool.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [poolName]);
|
|
67
|
-
}
|
|
68
|
-
function isDupKey(err) {
|
|
69
|
-
return Boolean(err) && err.code === "23505";
|
|
70
|
-
}
|
|
71
|
-
function dupKeyName(err) {
|
|
72
|
-
const c = String(err?.constraint ?? "");
|
|
73
|
-
if (c === "uq_image_bake_event_line")
|
|
74
|
-
return "uq_line";
|
|
75
|
-
if (/pkey$/i.test(c))
|
|
76
|
-
return "primary";
|
|
77
|
-
return "other";
|
|
78
|
-
}
|
|
79
|
-
export class PgImageBake {
|
|
80
|
-
pool;
|
|
81
|
-
poolName;
|
|
82
|
-
constructor(pool, poolName = "standard") {
|
|
83
|
-
this.pool = pool;
|
|
84
|
-
this.poolName = poolName;
|
|
85
|
-
}
|
|
86
|
-
async createBake(input) {
|
|
87
|
-
const bakeId = uuidv7();
|
|
88
|
-
const now = new Date();
|
|
89
|
-
try {
|
|
90
|
-
await this.pool.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
|
|
91
|
-
"idem_key, cancel_requested, requested_by, created_at, updated_at) " +
|
|
92
|
-
"VALUES ($1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12)", [
|
|
93
|
-
bakeId,
|
|
94
|
-
input.profile,
|
|
95
|
-
input.bands == null ? null : JSON.stringify(input.bands),
|
|
96
|
-
input.baseRef,
|
|
97
|
-
input.push ? 1 : 0,
|
|
98
|
-
input.dryRun ? 1 : 0,
|
|
99
|
-
input.logs ? 1 : 0,
|
|
100
|
-
JSON.stringify(input.argv),
|
|
101
|
-
input.idemKey,
|
|
102
|
-
input.requestedBy,
|
|
103
|
-
now,
|
|
104
|
-
now,
|
|
105
|
-
]);
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
if (isDupKey(err) && input.idemKey) {
|
|
109
|
-
const existing = await this.getBakeByIdem(input.idemKey);
|
|
110
|
-
if (existing)
|
|
111
|
-
return { created: false, bake: existing };
|
|
112
|
-
}
|
|
113
|
-
throw err;
|
|
114
|
-
}
|
|
115
|
-
const bake = await this.getBake(bakeId);
|
|
116
|
-
if (!bake)
|
|
117
|
-
throw new Error("createBake: row vanished after insert");
|
|
118
|
-
return { created: true, bake };
|
|
119
|
-
}
|
|
120
|
-
async createBakeIfIdle(input) {
|
|
121
|
-
const bakeId = uuidv7();
|
|
122
|
-
const now = new Date();
|
|
123
|
-
const conn = await this.pool.connect();
|
|
124
|
-
try {
|
|
125
|
-
await conn.query("BEGIN");
|
|
126
|
-
await conn.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [this.poolName]);
|
|
127
|
-
await conn.query("SELECT pool FROM image_bake_admit WHERE pool = $1 FOR UPDATE", [this.poolName]);
|
|
128
|
-
const res = await conn.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
|
|
129
|
-
"idem_key, cancel_requested, requested_by, created_at, updated_at) " +
|
|
130
|
-
"SELECT $1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12 WHERE NOT EXISTS " +
|
|
131
|
-
"(SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0)", [
|
|
132
|
-
bakeId,
|
|
133
|
-
input.profile,
|
|
134
|
-
input.bands == null ? null : JSON.stringify(input.bands),
|
|
135
|
-
input.baseRef,
|
|
136
|
-
input.push ? 1 : 0,
|
|
137
|
-
input.dryRun ? 1 : 0,
|
|
138
|
-
input.logs ? 1 : 0,
|
|
139
|
-
JSON.stringify(input.argv),
|
|
140
|
-
input.idemKey,
|
|
141
|
-
input.requestedBy,
|
|
142
|
-
now,
|
|
143
|
-
now,
|
|
144
|
-
]);
|
|
145
|
-
const inserted = (res.rowCount ?? 0) > 0;
|
|
146
|
-
await conn.query("COMMIT");
|
|
147
|
-
if (!inserted)
|
|
148
|
-
return { created: false, bake: null };
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
try {
|
|
152
|
-
await conn.query("ROLLBACK");
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
}
|
|
156
|
-
throw err;
|
|
157
|
-
}
|
|
158
|
-
finally {
|
|
159
|
-
conn.release();
|
|
160
|
-
}
|
|
161
|
-
const bake = await this.getBake(bakeId);
|
|
162
|
-
if (!bake)
|
|
163
|
-
throw new Error("createBakeIfIdle: row vanished after insert");
|
|
164
|
-
return { created: true, bake };
|
|
165
|
-
}
|
|
166
|
-
async getBake(bakeId) {
|
|
167
|
-
const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE bake_id = $1 LIMIT 1`, [bakeId]);
|
|
168
|
-
return res.rows[0] ? mapRow(res.rows[0]) : null;
|
|
169
|
-
}
|
|
170
|
-
async getBakeByIdem(idemKey) {
|
|
171
|
-
const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = $1 LIMIT 1`, [idemKey]);
|
|
172
|
-
return res.rows[0] ? mapRow(res.rows[0]) : null;
|
|
173
|
-
}
|
|
174
|
-
async findActiveByArgv(argv) {
|
|
175
|
-
const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = $1::jsonb ` +
|
|
176
|
-
"ORDER BY created_at DESC LIMIT 1", [JSON.stringify(argv)]);
|
|
177
|
-
return res.rows[0] ? mapRow(res.rows[0]) : null;
|
|
178
|
-
}
|
|
179
|
-
async findActiveAny() {
|
|
180
|
-
const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 ` +
|
|
181
|
-
"ORDER BY created_at ASC LIMIT 1");
|
|
182
|
-
return res.rows[0] ? mapRow(res.rows[0]) : null;
|
|
183
|
-
}
|
|
184
|
-
async findNextQueuedId() {
|
|
185
|
-
const res = await this.pool.query("SELECT bake_id FROM image_bake WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1");
|
|
186
|
-
return res.rows[0] ? String(res.rows[0].bake_id) : null;
|
|
187
|
-
}
|
|
188
|
-
async claimBake(bakeId, runnerId, leaseMs) {
|
|
189
|
-
const conn = await this.pool.connect();
|
|
190
|
-
try {
|
|
191
|
-
await conn.query("BEGIN");
|
|
192
|
-
const now = new Date();
|
|
193
|
-
const leaseUntil = new Date(now.getTime() + leaseMs);
|
|
194
|
-
const ingestSecret = randomBytes(24).toString("hex");
|
|
195
|
-
const bakeRes = await conn.query("UPDATE image_bake SET status='running', state='PENDING', runner_id=$1, ingest_secret=$2, " +
|
|
196
|
-
"lease_until=$3, updated_at=$4 WHERE bake_id=$5 AND status='queued'", [runnerId, ingestSecret, leaseUntil, now, bakeId]);
|
|
197
|
-
if ((bakeRes.rowCount ?? 0) !== 1) {
|
|
198
|
-
await conn.query("ROLLBACK");
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
const dryRes = await conn.query("SELECT dry_run FROM image_bake WHERE bake_id=$1", [bakeId]);
|
|
202
|
-
if (Boolean(dryRes.rows[0]?.dry_run)) {
|
|
203
|
-
await conn.query("COMMIT");
|
|
204
|
-
return (await this.getBake(bakeId)) ?? null;
|
|
205
|
-
}
|
|
206
|
-
const leaseSel = await conn.query("SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=$1", [this.poolName]);
|
|
207
|
-
const cur = leaseSel.rows[0];
|
|
208
|
-
if (!cur) {
|
|
209
|
-
try {
|
|
210
|
-
await conn.query("INSERT INTO image_bake_lease (pool, bake_id, acquired_at, expires_at) VALUES ($1,$2,$3,$4)", [this.poolName, bakeId, now, leaseUntil]);
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
if (isDupKey(err)) {
|
|
214
|
-
await conn.query("ROLLBACK");
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
throw err;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
if (new Date(cur.expires_at).getTime() >= now.getTime()) {
|
|
222
|
-
await conn.query("ROLLBACK");
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
const holderId = String(cur.bake_id ?? "");
|
|
226
|
-
const holderRes = await conn.query("SELECT status FROM image_bake WHERE bake_id=$1", [holderId]);
|
|
227
|
-
const holderStatus = String(holderRes.rows[0]?.status ?? "");
|
|
228
|
-
if (holderStatus === "queued" || holderStatus === "running") {
|
|
229
|
-
await conn.query("ROLLBACK");
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
const stealRes = await conn.query("UPDATE image_bake_lease SET bake_id=$1, acquired_at=$2, expires_at=$3 WHERE pool=$4 AND bake_id=$5 AND expires_at=$6", [bakeId, now, leaseUntil, this.poolName, holderId, cur.expires_at]);
|
|
233
|
-
if ((stealRes.rowCount ?? 0) === 0) {
|
|
234
|
-
await conn.query("ROLLBACK");
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const check = await conn.query("SELECT bake_id FROM image_bake_lease WHERE pool=$1", [this.poolName]);
|
|
239
|
-
if (String(check.rows[0]?.bake_id ?? "") !== bakeId) {
|
|
240
|
-
await conn.query("ROLLBACK");
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
await conn.query("COMMIT");
|
|
244
|
-
return (await this.getBake(bakeId)) ?? null;
|
|
245
|
-
}
|
|
246
|
-
catch (err) {
|
|
247
|
-
try {
|
|
248
|
-
await conn.query("ROLLBACK");
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
}
|
|
252
|
-
throw err;
|
|
253
|
-
}
|
|
254
|
-
finally {
|
|
255
|
-
conn.release();
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
async appendEvent(bakeId, kind, data, opts = {}) {
|
|
259
|
-
const lineOrd = opts.lineOrd ?? null;
|
|
260
|
-
if (lineOrd !== null && (await this.lineOrdExists(bakeId, lineOrd)))
|
|
261
|
-
return null;
|
|
262
|
-
const payload = data == null ? null : pgSafeJsonStringify(data);
|
|
263
|
-
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
264
|
-
const seq = (await this.maxSeq(bakeId)) + 1;
|
|
265
|
-
try {
|
|
266
|
-
await this.pool.query("INSERT INTO image_bake_event (bake_id, seq, line_ord, kind, level, data, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [bakeId, seq, lineOrd, kind, opts.level ?? null, payload, new Date()]);
|
|
267
|
-
return seq;
|
|
268
|
-
}
|
|
269
|
-
catch (err) {
|
|
270
|
-
if (!isDupKey(err))
|
|
271
|
-
throw err;
|
|
272
|
-
const which = dupKeyName(err);
|
|
273
|
-
if (which === "uq_line" && lineOrd !== null)
|
|
274
|
-
return null;
|
|
275
|
-
await new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5)));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
throw new Error(`appendEvent: seq allocation lost the (bake_id,seq) race after retries (bakeId=${bakeId})`);
|
|
279
|
-
}
|
|
280
|
-
async lineOrdExists(bakeId, lineOrd) {
|
|
281
|
-
const res = await this.pool.query("SELECT 1 FROM image_bake_event WHERE bake_id = $1 AND line_ord = $2 LIMIT 1", [bakeId, lineOrd]);
|
|
282
|
-
return (res.rowCount ?? 0) > 0;
|
|
283
|
-
}
|
|
284
|
-
async maxSeq(bakeId) {
|
|
285
|
-
const res = await this.pool.query("SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = $1", [bakeId]);
|
|
286
|
-
const m = res.rows[0]?.m;
|
|
287
|
-
return m == null ? 0 : Number(m);
|
|
288
|
-
}
|
|
289
|
-
async retainedFrom(bakeId) {
|
|
290
|
-
const res = await this.pool.query("SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = $1", [bakeId]);
|
|
291
|
-
const m = res.rows[0]?.m;
|
|
292
|
-
return m == null ? 0 : Number(m);
|
|
293
|
-
}
|
|
294
|
-
async getEvents(bakeId, afterSeq) {
|
|
295
|
-
const res = await this.pool.query("SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = $1 AND seq > $2 ORDER BY seq ASC", [bakeId, afterSeq]);
|
|
296
|
-
return res.rows.map((r) => ({
|
|
297
|
-
seq: Number(r.seq),
|
|
298
|
-
lineOrd: r.line_ord === null ? null : Number(r.line_ord),
|
|
299
|
-
kind: String(r.kind),
|
|
300
|
-
level: r.level ?? null,
|
|
301
|
-
data: parseJson(r.data, null),
|
|
302
|
-
ts: iso(r.ts),
|
|
303
|
-
}));
|
|
304
|
-
}
|
|
305
|
-
async setStatus(bakeId, status) {
|
|
306
|
-
await this.pool.query("UPDATE image_bake SET status = $1, updated_at = $2 WHERE bake_id = $3", [
|
|
307
|
-
status,
|
|
308
|
-
new Date(),
|
|
309
|
-
bakeId,
|
|
310
|
-
]);
|
|
311
|
-
}
|
|
312
|
-
async setState(bakeId, state) {
|
|
313
|
-
await this.pool.query("UPDATE image_bake SET state = $1, updated_at = $2 WHERE bake_id = $3", [
|
|
314
|
-
state,
|
|
315
|
-
new Date(),
|
|
316
|
-
bakeId,
|
|
317
|
-
]);
|
|
318
|
-
}
|
|
319
|
-
async setTerminal(bakeId, t) {
|
|
320
|
-
let affectedRows = 0;
|
|
321
|
-
await this.tx(async (conn) => {
|
|
322
|
-
const invalidIdentity = (t.digest != null && pgHasUnstorable(t.digest)) ||
|
|
323
|
-
(t.manifestSha != null && pgHasUnstorable(t.manifestSha)) ||
|
|
324
|
-
(t.repo != null && pgHasUnstorable(t.repo)) ||
|
|
325
|
-
(t.ref != null && pgHasUnstorable(t.ref));
|
|
326
|
-
const res = await conn.query("UPDATE image_bake SET status=$1, state=$2, digest=$3, repo=$4, ref=$5, index_id=$6, exit_code=$7, error=$8, " +
|
|
327
|
-
"error_code=$9, manifest_sha=$10, tag=$11, ingest_secret=NULL, updated_at=$12 WHERE bake_id=$13 AND status IN ('queued','running')", [
|
|
328
|
-
invalidIdentity ? "failed" : t.status,
|
|
329
|
-
t.state,
|
|
330
|
-
invalidIdentity ? null : t.digest ?? null,
|
|
331
|
-
invalidIdentity ? null : t.repo ?? null,
|
|
332
|
-
invalidIdentity ? null : t.ref ?? null,
|
|
333
|
-
t.indexId ?? null,
|
|
334
|
-
t.exitCode ?? null,
|
|
335
|
-
t.error == null ? null : pgSanitizeText(t.error),
|
|
336
|
-
t.errorCode == null ? null : pgSanitizeText(t.errorCode),
|
|
337
|
-
invalidIdentity ? null : t.manifestSha ?? null,
|
|
338
|
-
t.tag == null ? null : pgSanitizeText(t.tag),
|
|
339
|
-
new Date(),
|
|
340
|
-
bakeId,
|
|
341
|
-
]);
|
|
342
|
-
affectedRows = res.rowCount ?? 0;
|
|
343
|
-
await conn.query("DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2", [this.poolName, bakeId]);
|
|
344
|
-
});
|
|
345
|
-
return affectedRows;
|
|
346
|
-
}
|
|
347
|
-
async setIndexId(bakeId, indexId) {
|
|
348
|
-
const res = await this.pool.query("UPDATE image_bake SET index_id = $1, updated_at = $2 WHERE bake_id = $3 AND status IN ('queued','running')", [indexId, new Date(), bakeId]);
|
|
349
|
-
return (res.rowCount ?? 0) >= 1;
|
|
350
|
-
}
|
|
351
|
-
async heartbeatLease(bakeId, runnerId, leaseMs) {
|
|
352
|
-
const now = new Date();
|
|
353
|
-
const leaseUntil = new Date(now.getTime() + leaseMs);
|
|
354
|
-
const res = await this.pool.query("UPDATE image_bake SET lease_until = $1, updated_at = $2 WHERE bake_id = $3 AND runner_id = $4 AND status = 'running'", [leaseUntil, now, bakeId, runnerId]);
|
|
355
|
-
if ((res.rowCount ?? 0) < 1)
|
|
356
|
-
return false;
|
|
357
|
-
const dryRes = await this.pool.query("SELECT dry_run FROM image_bake WHERE bake_id = $1", [bakeId]);
|
|
358
|
-
if (Boolean(dryRes.rows[0]?.dry_run))
|
|
359
|
-
return true;
|
|
360
|
-
const leaseRes = await this.pool.query("UPDATE image_bake_lease SET expires_at = $1 WHERE pool = $2 AND bake_id = $3", [leaseUntil, this.poolName, bakeId]);
|
|
361
|
-
return (leaseRes.rowCount ?? 0) >= 1;
|
|
362
|
-
}
|
|
363
|
-
async requestCancel(bakeId) {
|
|
364
|
-
const res = await this.pool.query("UPDATE image_bake SET cancel_requested = 1, updated_at = $1 WHERE bake_id = $2 AND status IN ('queued','running')", [new Date(), bakeId]);
|
|
365
|
-
return (res.rowCount ?? 0) > 0;
|
|
366
|
-
}
|
|
367
|
-
async isCancelRequested(bakeId) {
|
|
368
|
-
const res = await this.pool.query("SELECT cancel_requested FROM image_bake WHERE bake_id = $1", [bakeId]);
|
|
369
|
-
return Boolean(res.rows[0]?.cancel_requested);
|
|
370
|
-
}
|
|
371
|
-
async reapStaleBakes(olderThanMs) {
|
|
372
|
-
const cutoff = new Date(Date.now() - olderThanMs);
|
|
373
|
-
const victimsRes = await this.pool.query("SELECT bake_id FROM image_bake WHERE status = 'running' AND " +
|
|
374
|
-
"(lease_until IS NULL OR lease_until < $1) AND updated_at < $2", [cutoff, cutoff]);
|
|
375
|
-
let reaped = 0;
|
|
376
|
-
for (const v of victimsRes.rows) {
|
|
377
|
-
const bakeId = String(v.bake_id);
|
|
378
|
-
const res = await this.pool.query("UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
|
|
379
|
-
"ingest_secret=NULL, index_id=NULL, updated_at=$1 WHERE bake_id=$2 AND status='running'", [new Date(), bakeId]);
|
|
380
|
-
if ((res.rowCount ?? 0) === 1) {
|
|
381
|
-
reaped += 1;
|
|
382
|
-
try {
|
|
383
|
-
await this.appendEvent(bakeId, "done", {
|
|
384
|
-
status: "FAILED",
|
|
385
|
-
digest: null,
|
|
386
|
-
repo: null,
|
|
387
|
-
ref: null,
|
|
388
|
-
indexId: null,
|
|
389
|
-
exitCode: null,
|
|
390
|
-
error: "bake-runner lost (lease expired)",
|
|
391
|
-
errorCode: null,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
}
|
|
396
|
-
await this.pool.query("DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2", [this.poolName, bakeId]);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return reaped;
|
|
400
|
-
}
|
|
401
|
-
async tx(fn) {
|
|
402
|
-
const conn = await this.pool.connect();
|
|
403
|
-
try {
|
|
404
|
-
await conn.query("BEGIN");
|
|
405
|
-
await fn(conn);
|
|
406
|
-
await conn.query("COMMIT");
|
|
407
|
-
}
|
|
408
|
-
catch (err) {
|
|
409
|
-
try {
|
|
410
|
-
await conn.query("ROLLBACK");
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
}
|
|
414
|
-
throw err;
|
|
415
|
-
}
|
|
416
|
-
finally {
|
|
417
|
-
conn.release();
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
1
|
+
export { PgImageBake, PG_IMAGE_BAKE_SCHEMA, ensurePgImageBakeSchema as ensureSchema } from "./image-bake-store-sql.js";
|
|
421
2
|
//# sourceMappingURL=pg-image-bake.js.map
|
|
@@ -1,38 +1,8 @@
|
|
|
1
1
|
import type { Pool, PoolClient } from "pg";
|
|
2
2
|
import type { RateDecision, RateGate } from "../observability/rate-limit.js";
|
|
3
3
|
import { type CounterDegradeHook } from "./write-behind-counter.js";
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
keyCol: string;
|
|
7
|
-
valCol: string;
|
|
8
|
-
}
|
|
9
|
-
export declare class PgWriteBehindCounter {
|
|
10
|
-
private readonly pool;
|
|
11
|
-
private readonly t;
|
|
12
|
-
private readonly windowMs;
|
|
13
|
-
private readonly now;
|
|
14
|
-
private readonly onDegraded?;
|
|
15
|
-
private readonly queryTimeoutMs;
|
|
16
|
-
private pendingDelta;
|
|
17
|
-
private fleetTotal;
|
|
18
|
-
private timer?;
|
|
19
|
-
private flushing;
|
|
20
|
-
private flushStartedAt;
|
|
21
|
-
private failStreak;
|
|
22
|
-
constructor(pool: Pool, t: CounterTable, windowMs: number, now?: () => number, onDegraded?: CounterDegradeHook | undefined, queryTimeoutMs?: number);
|
|
23
|
-
private bump;
|
|
24
|
-
bucket(): number;
|
|
25
|
-
private rk;
|
|
26
|
-
used(key: string): number;
|
|
27
|
-
addLocal(key: string, n: number): void;
|
|
28
|
-
retryAfterSec(): number;
|
|
29
|
-
flush(): Promise<void>;
|
|
30
|
-
private flushOnce;
|
|
31
|
-
refresh(): Promise<void>;
|
|
32
|
-
reap(): Promise<number>;
|
|
33
|
-
startRefresh(intervalMs?: number): this;
|
|
34
|
-
stop(): void;
|
|
35
|
-
}
|
|
4
|
+
export { PgWriteBehindCounter } from "./write-behind-counter.js";
|
|
5
|
+
export type { CounterTable } from "./write-behind-counter.js";
|
|
36
6
|
export declare class PgRateLimiter implements RateGate {
|
|
37
7
|
private readonly limit;
|
|
38
8
|
private readonly counter;
|
|
@@ -1,155 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
pool;
|
|
5
|
-
t;
|
|
6
|
-
windowMs;
|
|
7
|
-
now;
|
|
8
|
-
onDegraded;
|
|
9
|
-
queryTimeoutMs;
|
|
10
|
-
pendingDelta = new Map();
|
|
11
|
-
fleetTotal = new Map();
|
|
12
|
-
timer;
|
|
13
|
-
flushing = false;
|
|
14
|
-
flushStartedAt = 0;
|
|
15
|
-
failStreak = new Map();
|
|
16
|
-
constructor(pool, t, windowMs, now = () => Date.now(), onDegraded, queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
|
|
17
|
-
this.pool = pool;
|
|
18
|
-
this.t = t;
|
|
19
|
-
this.windowMs = windowMs;
|
|
20
|
-
this.now = now;
|
|
21
|
-
this.onDegraded = onDegraded;
|
|
22
|
-
this.queryTimeoutMs = queryTimeoutMs;
|
|
23
|
-
}
|
|
24
|
-
bump(kind, failed, error) {
|
|
25
|
-
if (!this.onDegraded)
|
|
26
|
-
return;
|
|
27
|
-
const prev = this.failStreak.get(kind) ?? 0;
|
|
28
|
-
if (!failed && prev === 0)
|
|
29
|
-
return;
|
|
30
|
-
const next = failed ? prev + 1 : 0;
|
|
31
|
-
this.failStreak.set(kind, next);
|
|
32
|
-
try {
|
|
33
|
-
this.onDegraded({ table: this.t.table, kind, streak: next, prevStreak: prev, ...(failed && error !== undefined ? { error: error instanceof Error ? error.message : String(error) } : {}) });
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
bucket() {
|
|
39
|
-
return Math.floor(this.now() / this.windowMs);
|
|
40
|
-
}
|
|
41
|
-
rk(key) {
|
|
42
|
-
return `${key}${SEP}${this.bucket()}`;
|
|
43
|
-
}
|
|
44
|
-
used(key) {
|
|
45
|
-
const rk = this.rk(key);
|
|
46
|
-
return (this.fleetTotal.get(rk) ?? 0) + (this.pendingDelta.get(rk) ?? 0);
|
|
47
|
-
}
|
|
48
|
-
addLocal(key, n) {
|
|
49
|
-
if (n <= 0)
|
|
50
|
-
return;
|
|
51
|
-
const rk = this.rk(key);
|
|
52
|
-
this.pendingDelta.set(rk, (this.pendingDelta.get(rk) ?? 0) + n);
|
|
53
|
-
}
|
|
54
|
-
retryAfterSec() {
|
|
55
|
-
return Math.ceil(((this.bucket() + 1) * this.windowMs - this.now()) / 1000);
|
|
56
|
-
}
|
|
57
|
-
async flush() {
|
|
58
|
-
if (this.flushing) {
|
|
59
|
-
if (this.now() - this.flushStartedAt > 60_000)
|
|
60
|
-
this.bump("stalled", true, "flush round stalled >60s (hung query?)");
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
this.flushing = true;
|
|
64
|
-
this.flushStartedAt = this.now();
|
|
65
|
-
try {
|
|
66
|
-
await this.flushOnce();
|
|
67
|
-
}
|
|
68
|
-
finally {
|
|
69
|
-
this.flushing = false;
|
|
70
|
-
this.bump("stalled", false);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
async flushOnce() {
|
|
74
|
-
const { table, keyCol, valCol } = this.t;
|
|
75
|
-
let attempted = false;
|
|
76
|
-
let failure;
|
|
77
|
-
let hadFailure = false;
|
|
78
|
-
for (const [rk, delta] of [...this.pendingDelta]) {
|
|
79
|
-
if (delta <= 0)
|
|
80
|
-
continue;
|
|
81
|
-
const sep = rk.lastIndexOf(SEP);
|
|
82
|
-
const key = rk.slice(0, sep);
|
|
83
|
-
const windowBucket = Number(rk.slice(sep + 1));
|
|
84
|
-
attempted = true;
|
|
85
|
-
try {
|
|
86
|
-
const q = {
|
|
87
|
-
text: `INSERT INTO ${table} (${keyCol}, window_bucket, ${valCol}, updated_at) VALUES ($1,$2,$3,$4) ` +
|
|
88
|
-
`ON CONFLICT (${keyCol}, window_bucket) DO UPDATE SET ${valCol} = ${table}.${valCol} + EXCLUDED.${valCol}, updated_at = EXCLUDED.updated_at`,
|
|
89
|
-
values: [key, windowBucket, delta, new Date()],
|
|
90
|
-
query_timeout: this.queryTimeoutMs,
|
|
91
|
-
};
|
|
92
|
-
await this.pool.query(q);
|
|
93
|
-
const remaining = (this.pendingDelta.get(rk) ?? 0) - delta;
|
|
94
|
-
if (remaining > 0)
|
|
95
|
-
this.pendingDelta.set(rk, remaining);
|
|
96
|
-
else
|
|
97
|
-
this.pendingDelta.delete(rk);
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
hadFailure = true;
|
|
101
|
-
failure = err;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (attempted)
|
|
105
|
-
this.bump("flush", hadFailure, failure);
|
|
106
|
-
await this.refresh();
|
|
107
|
-
await this.reap();
|
|
108
|
-
}
|
|
109
|
-
async refresh() {
|
|
110
|
-
const { table, keyCol, valCol } = this.t;
|
|
111
|
-
const b = this.bucket();
|
|
112
|
-
try {
|
|
113
|
-
const q = { text: `SELECT ${keyCol}, ${valCol} FROM ${table} WHERE window_bucket = $1`, values: [b], query_timeout: this.queryTimeoutMs };
|
|
114
|
-
const res = await this.pool.query(q);
|
|
115
|
-
const next = new Map();
|
|
116
|
-
for (const row of res.rows) {
|
|
117
|
-
next.set(`${String(row[keyCol])}${SEP}${b}`, Number(row[valCol]));
|
|
118
|
-
}
|
|
119
|
-
this.fleetTotal = next;
|
|
120
|
-
this.bump("refresh", false);
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
this.bump("refresh", true, err);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
async reap() {
|
|
127
|
-
try {
|
|
128
|
-
const q = { text: `DELETE FROM ${this.t.table} WHERE window_bucket < $1`, values: [this.bucket()], query_timeout: this.queryTimeoutMs };
|
|
129
|
-
const res = await this.pool.query(q);
|
|
130
|
-
return res.rowCount ?? 0;
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return 0;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
startRefresh(intervalMs = 5_000) {
|
|
137
|
-
this.timer ??= setInterval(() => void this.flush(), intervalMs);
|
|
138
|
-
this.timer.unref?.();
|
|
139
|
-
return this;
|
|
140
|
-
}
|
|
141
|
-
stop() {
|
|
142
|
-
if (this.timer)
|
|
143
|
-
clearInterval(this.timer);
|
|
144
|
-
this.timer = undefined;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
1
|
+
import { PgWriteBehindCounter } from "./write-behind-counter.js";
|
|
2
|
+
export { PgWriteBehindCounter } from "./write-behind-counter.js";
|
|
3
|
+
const RATE_LIMIT_TABLE = { table: "rate_limit", keyCol: "limit_key", valCol: "n" };
|
|
147
4
|
export class PgRateLimiter {
|
|
148
5
|
limit;
|
|
149
6
|
counter;
|
|
150
7
|
constructor(pool, limit, windowMs = 60_000, now, onDegraded, queryTimeoutMs) {
|
|
151
8
|
this.limit = limit;
|
|
152
|
-
this.counter = new PgWriteBehindCounter(pool,
|
|
9
|
+
this.counter = new PgWriteBehindCounter(pool, RATE_LIMIT_TABLE, windowMs, now, onDegraded, queryTimeoutMs);
|
|
153
10
|
}
|
|
154
11
|
check(key) {
|
|
155
12
|
if (this.limit <= 0)
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
|
|
2
|
+
import type { PodSpecPatch } from "./k8s-exec-protocol.js";
|
|
2
3
|
type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
|
|
3
|
-
export
|
|
4
|
-
export declare function buildBgLauncherScript(dir: string, cmdShB64: string, runnerShB64: string): string;
|
|
5
|
-
export declare function buildBgPollScript(dir: string, stdoutCursor: number, stderrCursor: number, readCap: number): string;
|
|
6
|
-
export declare function buildBgKillScript(dir: string): string;
|
|
7
|
-
export declare function buildBgDisposeScript(dir: string): string;
|
|
8
|
-
export declare function buildDetachCapableExec(dir: string, command: string): string;
|
|
9
|
-
export interface ParsedBgPoll {
|
|
10
|
-
gone: boolean;
|
|
11
|
-
outTotal: number;
|
|
12
|
-
errTotal: number;
|
|
13
|
-
exitCode?: number;
|
|
14
|
-
alive: boolean;
|
|
15
|
-
stdout: string;
|
|
16
|
-
stdoutBytes: number;
|
|
17
|
-
stderr: string;
|
|
18
|
-
stderrBytes: number;
|
|
19
|
-
}
|
|
20
|
-
export declare function parseBgPollOutput(raw: string): ParsedBgPoll | undefined;
|
|
4
|
+
export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, type ParsedBgPoll, } from "./k8s-bg-scripts.js";
|
|
21
5
|
export interface K8sEnvConfig {
|
|
22
6
|
apiUrl?: string;
|
|
23
7
|
token?: string;
|
|
@@ -51,19 +35,7 @@ export interface K8sEnvConfig {
|
|
|
51
35
|
podSpecPatch?: PodSpecPatch;
|
|
52
36
|
podEnv?: Record<string, string>;
|
|
53
37
|
}
|
|
54
|
-
export
|
|
55
|
-
volumes?: unknown[];
|
|
56
|
-
volumeMounts?: unknown[];
|
|
57
|
-
podSecurityContext?: Record<string, unknown>;
|
|
58
|
-
containerSecurityContext?: Record<string, unknown>;
|
|
59
|
-
labels?: Record<string, string>;
|
|
60
|
-
}
|
|
61
|
-
export declare function applyPodSpecPatch(pod: {
|
|
62
|
-
metadata: {
|
|
63
|
-
labels?: Record<string, string>;
|
|
64
|
-
};
|
|
65
|
-
spec: Record<string, unknown>;
|
|
66
|
-
}, patch: PodSpecPatch): void;
|
|
38
|
+
export { type PodSpecPatch, applyPodSpecPatch } from "./k8s-exec-protocol.js";
|
|
67
39
|
type ConnectResult = {
|
|
68
40
|
ok: true;
|
|
69
41
|
value: WorkspaceHandle;
|
|
@@ -87,13 +59,9 @@ export interface K8sEnvDeps {
|
|
|
87
59
|
openExec?: (url: string, headers: Record<string, string>) => ExecSocket;
|
|
88
60
|
connectOnce?: (signal?: AbortSignal) => Promise<ConnectResult>;
|
|
89
61
|
}
|
|
90
|
-
export
|
|
62
|
+
export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
|
|
91
63
|
export declare function errorMessageOf(e: unknown): string;
|
|
92
|
-
export
|
|
93
|
-
exitCode: number;
|
|
94
|
-
} | {
|
|
95
|
-
error: string;
|
|
96
|
-
};
|
|
64
|
+
export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
|
|
97
65
|
export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, BackgroundShellCapability {
|
|
98
66
|
readonly capabilities: {
|
|
99
67
|
isolation: boolean;
|
|
@@ -207,5 +175,4 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
|
|
|
207
175
|
private tempResult;
|
|
208
176
|
}
|
|
209
177
|
export declare function k8sExecutionEnvFactory(config: K8sEnvConfig): ExecutionEnvFactory;
|
|
210
|
-
export {};
|
|
211
178
|
//# sourceMappingURL=remote-env-k8s.d.ts.map
|