@ash-ai/server 0.0.2 → 0.0.4
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/__tests__/attachments.test.d.ts +2 -0
- package/dist/__tests__/attachments.test.d.ts.map +1 -0
- package/dist/__tests__/attachments.test.js +57 -0
- package/dist/__tests__/attachments.test.js.map +1 -0
- package/dist/__tests__/bundle.test.d.ts +2 -0
- package/dist/__tests__/bundle.test.d.ts.map +1 -0
- package/dist/__tests__/bundle.test.js +55 -0
- package/dist/__tests__/bundle.test.js.map +1 -0
- package/dist/__tests__/coordinator.test.d.ts +2 -0
- package/dist/__tests__/coordinator.test.d.ts.map +1 -0
- package/dist/__tests__/coordinator.test.js +283 -0
- package/dist/__tests__/coordinator.test.js.map +1 -0
- package/dist/__tests__/crypto.test.d.ts +2 -0
- package/dist/__tests__/crypto.test.d.ts.map +1 -0
- package/dist/__tests__/crypto.test.js +45 -0
- package/dist/__tests__/crypto.test.js.map +1 -0
- package/dist/__tests__/file-store.test.d.ts +2 -0
- package/dist/__tests__/file-store.test.d.ts.map +1 -0
- package/dist/__tests__/file-store.test.js +105 -0
- package/dist/__tests__/file-store.test.js.map +1 -0
- package/dist/__tests__/files.test.js +3 -3
- package/dist/__tests__/files.test.js.map +1 -1
- package/dist/__tests__/openapi.test.js +6 -3
- package/dist/__tests__/openapi.test.js.map +1 -1
- package/dist/__tests__/queue.test.d.ts +2 -0
- package/dist/__tests__/queue.test.d.ts.map +1 -0
- package/dist/__tests__/queue.test.js +151 -0
- package/dist/__tests__/queue.test.js.map +1 -0
- package/dist/__tests__/usage.test.d.ts +2 -0
- package/dist/__tests__/usage.test.d.ts.map +1 -0
- package/dist/__tests__/usage.test.js +74 -0
- package/dist/__tests__/usage.test.js.map +1 -0
- package/dist/crypto.d.ts +8 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +29 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db/drizzle-db.d.ts +128 -0
- package/dist/db/drizzle-db.d.ts.map +1 -0
- package/dist/db/drizzle-db.js +789 -0
- package/dist/db/drizzle-db.js.map +1 -0
- package/dist/db/index.d.ts +161 -3
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +164 -8
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.pg.d.ts +1625 -0
- package/dist/db/schema.pg.d.ts.map +1 -0
- package/dist/db/schema.pg.js +150 -0
- package/dist/db/schema.pg.js.map +1 -0
- package/dist/db/schema.sqlite.d.ts +1781 -0
- package/dist/db/schema.sqlite.d.ts.map +1 -0
- package/dist/db/schema.sqlite.js +150 -0
- package/dist/db/schema.sqlite.js.map +1 -0
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/queue/processor.d.ts +51 -0
- package/dist/queue/processor.d.ts.map +1 -0
- package/dist/queue/processor.js +98 -0
- package/dist/queue/processor.js.map +1 -0
- package/dist/routes/attachments.d.ts +3 -0
- package/dist/routes/attachments.d.ts.map +1 -0
- package/dist/routes/attachments.js +168 -0
- package/dist/routes/attachments.js.map +1 -0
- package/dist/routes/credentials.d.ts +11 -0
- package/dist/routes/credentials.d.ts.map +1 -0
- package/dist/routes/credentials.js +120 -0
- package/dist/routes/credentials.js.map +1 -0
- package/dist/routes/files.js +5 -5
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +9 -1
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/queue.d.ts +3 -0
- package/dist/routes/queue.d.ts.map +1 -0
- package/dist/routes/queue.js +144 -0
- package/dist/routes/queue.js.map +1 -0
- package/dist/routes/runners.d.ts +5 -0
- package/dist/routes/runners.d.ts.map +1 -1
- package/dist/routes/runners.js +42 -5
- package/dist/routes/runners.js.map +1 -1
- package/dist/routes/sessions.d.ts +2 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +236 -11
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/usage.d.ts +3 -0
- package/dist/routes/usage.d.ts.map +1 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/usage.js.map +1 -0
- package/dist/routes/workspace.d.ts +4 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +123 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/runner/coordinator.d.ts +77 -9
- package/dist/runner/coordinator.d.ts.map +1 -1
- package/dist/runner/coordinator.js +163 -89
- package/dist/runner/coordinator.js.map +1 -1
- package/dist/runner/local-backend.d.ts +1 -0
- package/dist/runner/local-backend.d.ts.map +1 -1
- package/dist/runner/local-backend.js +7 -0
- package/dist/runner/local-backend.js.map +1 -1
- package/dist/runner/remote-backend.d.ts +2 -0
- package/dist/runner/remote-backend.d.ts.map +1 -1
- package/dist/runner/remote-backend.js +7 -0
- package/dist/runner/remote-backend.js.map +1 -1
- package/dist/runner/runner-client.d.ts +4 -0
- package/dist/runner/runner-client.d.ts.map +1 -1
- package/dist/runner/runner-client.js +12 -0
- package/dist/runner/runner-client.js.map +1 -1
- package/dist/runner/types.d.ts +4 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +115 -1
- package/dist/schemas.js.map +1 -1
- package/dist/telemetry/exporter.d.ts +16 -0
- package/dist/telemetry/exporter.d.ts.map +1 -0
- package/dist/telemetry/exporter.js +89 -0
- package/dist/telemetry/exporter.js.map +1 -0
- package/dist/usage/extractor.d.ts +18 -0
- package/dist/usage/extractor.d.ts.map +1 -0
- package/dist/usage/extractor.js +48 -0
- package/dist/usage/extractor.js.map +1 -0
- package/drizzle/pg/0000_thick_loners.sql +75 -0
- package/drizzle/pg/0001_rare_lester.sql +13 -0
- package/drizzle/pg/0002_short_shinko_yamashiro.sql +1 -0
- package/drizzle/pg/0003_remarkable_mastermind.sql +14 -0
- package/drizzle/pg/0004_warm_reaper.sql +18 -0
- package/drizzle/pg/0005_overconfident_mole_man.sql +14 -0
- package/drizzle/pg/0006_third_shiva.sql +13 -0
- package/drizzle/pg/0007_keen_shockwave.sql +2 -0
- package/drizzle/pg/meta/0000_snapshot.json +648 -0
- package/drizzle/pg/meta/0001_snapshot.json +743 -0
- package/drizzle/pg/meta/0002_snapshot.json +749 -0
- package/drizzle/pg/meta/0003_snapshot.json +841 -0
- package/drizzle/pg/meta/0004_snapshot.json +974 -0
- package/drizzle/pg/meta/0005_snapshot.json +1079 -0
- package/drizzle/pg/meta/0006_snapshot.json +1193 -0
- package/drizzle/pg/meta/0007_snapshot.json +1199 -0
- package/drizzle/pg/meta/_journal.json +62 -0
- package/drizzle/sqlite/0000_massive_kinsey_walden.sql +75 -0
- package/drizzle/sqlite/0001_quiet_phantom_reporter.sql +13 -0
- package/drizzle/sqlite/0002_broad_sheva_callister.sql +1 -0
- package/drizzle/sqlite/0003_thankful_agent_brand.sql +14 -0
- package/drizzle/sqlite/0004_productive_wolverine.sql +18 -0
- package/drizzle/sqlite/0005_chilly_carlie_cooper.sql +14 -0
- package/drizzle/sqlite/0006_workable_starfox.sql +13 -0
- package/drizzle/sqlite/0007_quick_hemingway.sql +19 -0
- package/drizzle/sqlite/meta/0000_snapshot.json +503 -0
- package/drizzle/sqlite/meta/0001_snapshot.json +587 -0
- package/drizzle/sqlite/meta/0002_snapshot.json +594 -0
- package/drizzle/sqlite/meta/0003_snapshot.json +685 -0
- package/drizzle/sqlite/meta/0004_snapshot.json +807 -0
- package/drizzle/sqlite/meta/0005_snapshot.json +897 -0
- package/drizzle/sqlite/meta/0006_snapshot.json +981 -0
- package/drizzle/sqlite/meta/0007_snapshot.json +988 -0
- package/drizzle/sqlite/meta/_journal.json +62 -0
- package/package.json +10 -5
- package/dist/__tests__/schema.test.d.ts +0 -2
- package/dist/__tests__/schema.test.d.ts.map +0 -1
- package/dist/__tests__/schema.test.js +0 -31
- package/dist/__tests__/schema.test.js.map +0 -1
- package/dist/db/dump-schema.d.ts +0 -10
- package/dist/db/dump-schema.d.ts.map +0 -1
- package/dist/db/dump-schema.js +0 -64
- package/dist/db/dump-schema.js.map +0 -1
- package/dist/db/pg.d.ts +0 -35
- package/dist/db/pg.d.ts.map +0 -1
- package/dist/db/pg.js +0 -272
- package/dist/db/pg.js.map +0 -1
- package/dist/db/sqlite.d.ts +0 -34
- package/dist/db/sqlite.d.ts.map +0 -1
- package/dist/db/sqlite.js +0 -296
- package/dist/db/sqlite.js.map +0 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
3
|
+
import { getSession } from '../db/index.js';
|
|
4
|
+
import { createBundle, extractBundle, hasPersistedState, restoreSessionState } from '@ash-ai/sandbox';
|
|
5
|
+
/** Max body size for workspace uploads: ~134MB base64 ≈ 100MB binary. */
|
|
6
|
+
const WORKSPACE_BODY_LIMIT = 134 * 1024 * 1024;
|
|
7
|
+
export function workspaceRoutes(app, coordinator, dataDir) {
|
|
8
|
+
// Download workspace as tar.gz bundle
|
|
9
|
+
app.get('/api/sessions/:id/workspace', {
|
|
10
|
+
schema: {
|
|
11
|
+
tags: ['sessions'],
|
|
12
|
+
params: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: { id: { type: 'string', format: 'uuid' } },
|
|
15
|
+
required: ['id'],
|
|
16
|
+
},
|
|
17
|
+
response: {
|
|
18
|
+
404: { $ref: 'ApiError#' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}, async (req, reply) => {
|
|
22
|
+
const session = await getSession(req.params.id);
|
|
23
|
+
if (!session || session.tenantId !== req.tenantId) {
|
|
24
|
+
return reply.status(404).send({ error: 'Session not found', statusCode: 404 });
|
|
25
|
+
}
|
|
26
|
+
// Try live sandbox workspace first
|
|
27
|
+
let workspaceDir = null;
|
|
28
|
+
let tempDir = null;
|
|
29
|
+
try {
|
|
30
|
+
const backend = await coordinator.getBackendForRunnerAsync(session.runnerId);
|
|
31
|
+
const sandbox = backend.getSandbox(session.sandboxId);
|
|
32
|
+
if (sandbox) {
|
|
33
|
+
workspaceDir = sandbox.workspaceDir;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { /* runner may be gone */ }
|
|
37
|
+
// Fall back to persisted snapshot
|
|
38
|
+
if (!workspaceDir || !existsSync(workspaceDir)) {
|
|
39
|
+
const snapshotDir = join(dataDir, 'sessions', session.id, 'workspace');
|
|
40
|
+
if (existsSync(snapshotDir)) {
|
|
41
|
+
workspaceDir = snapshotDir;
|
|
42
|
+
}
|
|
43
|
+
else if (hasPersistedState(dataDir, session.id, session.tenantId)) {
|
|
44
|
+
tempDir = join(dataDir, 'tmp', `bundle-${session.id}-${Date.now()}`);
|
|
45
|
+
restoreSessionState(dataDir, session.id, tempDir, session.tenantId);
|
|
46
|
+
workspaceDir = tempDir;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!workspaceDir || !existsSync(workspaceDir)) {
|
|
50
|
+
return reply.status(404).send({ error: 'No workspace available for this session', statusCode: 404 });
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const bundle = createBundle(workspaceDir);
|
|
54
|
+
return reply
|
|
55
|
+
.header('Content-Type', 'application/gzip')
|
|
56
|
+
.header('Content-Disposition', `attachment; filename="${session.id}.tar.gz"`)
|
|
57
|
+
.send(bundle);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
// Clean up temp directory if we created one
|
|
61
|
+
if (tempDir) {
|
|
62
|
+
try {
|
|
63
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
catch { /* best effort */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// Upload/restore workspace from tar.gz bundle
|
|
70
|
+
app.post('/api/sessions/:id/workspace', {
|
|
71
|
+
bodyLimit: WORKSPACE_BODY_LIMIT,
|
|
72
|
+
schema: {
|
|
73
|
+
tags: ['sessions'],
|
|
74
|
+
params: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: { id: { type: 'string', format: 'uuid' } },
|
|
77
|
+
required: ['id'],
|
|
78
|
+
},
|
|
79
|
+
body: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
bundle: { type: 'string', description: 'Base64-encoded tar.gz bundle' },
|
|
83
|
+
},
|
|
84
|
+
required: ['bundle'],
|
|
85
|
+
},
|
|
86
|
+
response: {
|
|
87
|
+
200: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
message: { type: 'string' },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
400: { $ref: 'ApiError#' },
|
|
94
|
+
404: { $ref: 'ApiError#' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}, async (req, reply) => {
|
|
98
|
+
const session = await getSession(req.params.id);
|
|
99
|
+
if (!session || session.tenantId !== req.tenantId) {
|
|
100
|
+
return reply.status(404).send({ error: 'Session not found', statusCode: 404 });
|
|
101
|
+
}
|
|
102
|
+
const { bundle: b64 } = req.body;
|
|
103
|
+
const bundleBuffer = Buffer.from(b64, 'base64');
|
|
104
|
+
if (bundleBuffer.length === 0) {
|
|
105
|
+
return reply.status(400).send({ error: 'Empty bundle', statusCode: 400 });
|
|
106
|
+
}
|
|
107
|
+
// Extract into live sandbox workspace if available
|
|
108
|
+
try {
|
|
109
|
+
const backend = await coordinator.getBackendForRunnerAsync(session.runnerId);
|
|
110
|
+
const sandbox = backend.getSandbox(session.sandboxId);
|
|
111
|
+
if (sandbox) {
|
|
112
|
+
extractBundle(bundleBuffer, sandbox.workspaceDir);
|
|
113
|
+
return reply.send({ message: 'Workspace restored to live sandbox' });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { /* runner may be gone */ }
|
|
117
|
+
// Fall back to persisting as a snapshot
|
|
118
|
+
const snapshotDir = join(dataDir, 'sessions', session.id, 'workspace');
|
|
119
|
+
extractBundle(bundleBuffer, snapshotDir);
|
|
120
|
+
return reply.send({ message: 'Workspace saved as snapshot' });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/routes/workspace.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGtG,yEAAyE;AACzE,MAAM,oBAAoB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/C,MAAM,UAAU,eAAe,CAAC,GAAoB,EAAE,WAA8B,EAAE,OAAe;IACnG,sCAAsC;IACtC,GAAG,CAAC,GAAG,CAA6B,6BAA6B,EAAE;QACjE,MAAM,EAAE;YACN,IAAI,EAAE,CAAC,UAAU,CAAC;YAClB,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;gBACtD,QAAQ,EAAE,CAAC,IAAI,CAAC;aACjB;YACD,QAAQ,EAAE;gBACR,GAAG,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;aAC3B;SACF;KACF,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,mCAAmC;QACnC,IAAI,YAAY,GAAkB,IAAI,CAAC;QACvC,IAAI,OAAO,GAAkB,IAAI,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,wBAAwB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC7E,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACtD,IAAI,OAAO,EAAE,CAAC;gBACZ,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;YACtC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAC,wBAAwB,CAAC,CAAC;QAEpC,kCAAkC;QAClC,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;YACvE,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC5B,YAAY,GAAG,WAAW,CAAC;YAC7B,CAAC;iBAAM,IAAI,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACpE,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACrE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACpE,YAAY,GAAG,OAAO,CAAC;YACzB,CAAC;QACH,CAAC;QAED,IAAI,CAAC,YAAY,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/C,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;QACvG,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;YAC1C,OAAO,KAAK;iBACT,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAAC;iBAC1C,MAAM,CAAC,qBAAqB,EAAE,yBAAyB,OAAO,CAAC,EAAE,UAAU,CAAC;iBAC5E,IAAI,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC;gBAAS,CAAC;YACT,4CAA4C;YAC5C,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC;oBAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;YACxF,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,8CAA8C;IAC9C,GAAG,CAAC,IAAI,CAA6B,6BAA6B,EAAE;QAClE,SAAS,EAAE,oBAAoB;QAC/B,MAAM,EAAE;YACN,IAAI,EAAE,CAAC,UAAU,CAAC;YAClB,MAAM,EAAE;gBACN,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;gBACtD,QAAQ,EAAE,CAAC,IAAI,CAAC;aACjB;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8BAA8B,EAAE;iBACxE;gBACD,QAAQ,EAAE,CAAC,QAAQ,CAAC;aACrB;YACD,QAAQ,EAAE;gBACR,GAAG,EAAE;oBACH,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACV,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;qBAC5B;iBACF;gBACD,GAAG,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;gBAC1B,GAAG,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;aAC3B;SACF;KACF,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,GAAG,CAAC,QAAQ,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,IAA0B,CAAC;QACvD,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAEhD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;QAC5E,CAAC;QAED,mDAAmD;QACnD,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,wBAAwB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC7E,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACtD,IAAI,OAAO,EAAE,CAAC;gBACZ,aAAa,CAAC,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;gBAClD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAC,wBAAwB,CAAC,CAAC;QAEpC,wCAAwC;QACxC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;QACvE,aAAa,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;QACzC,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -3,50 +3,118 @@ import type { RunnerBackend } from './types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Coordinates multiple runners. Routes session creation to the least-loaded runner.
|
|
5
5
|
* Detects dead runners and marks their sessions as paused.
|
|
6
|
+
*
|
|
7
|
+
* In multi-coordinator mode, the runner registry lives in the shared database
|
|
8
|
+
* (Postgres/CRDB). Any coordinator can discover all healthy runners by querying
|
|
9
|
+
* the DB. The in-memory `backends` map is a local connection cache — each
|
|
10
|
+
* coordinator creates RemoteRunnerBackend instances on demand when it first
|
|
11
|
+
* needs to talk to a runner it hasn't seen before.
|
|
12
|
+
*
|
|
13
|
+
* Design principles:
|
|
14
|
+
* - DB is the source of truth for runner discovery (multi-coordinator safe)
|
|
15
|
+
* - Local Map is a connection cache only (avoids creating new HTTP clients per request)
|
|
16
|
+
* - All write operations (register, heartbeat, delete) go through DB
|
|
17
|
+
* - All read operations for routing go through DB (selectBestRunner)
|
|
18
|
+
* - Liveness sweep is idempotent (safe to run on multiple coordinators)
|
|
6
19
|
*/
|
|
7
20
|
export declare class RunnerCoordinator {
|
|
8
|
-
|
|
21
|
+
/** Local connection cache: runnerId -> RemoteRunnerBackend. Lazily populated from DB. */
|
|
22
|
+
private backends;
|
|
9
23
|
private localBackend;
|
|
10
24
|
private localRunnerId;
|
|
11
25
|
private livenessSweepTimer;
|
|
12
26
|
constructor(opts: {
|
|
13
27
|
localBackend?: RunnerBackend;
|
|
14
28
|
});
|
|
29
|
+
/**
|
|
30
|
+
* Register or re-register a runner. Persists to DB so all coordinators see it.
|
|
31
|
+
*/
|
|
15
32
|
registerRunner(info: {
|
|
16
33
|
runnerId: string;
|
|
17
34
|
host: string;
|
|
18
35
|
port: number;
|
|
19
36
|
maxSandboxes: number;
|
|
20
|
-
}): void
|
|
21
|
-
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Process a heartbeat. Updates DB so all coordinators see fresh capacity stats.
|
|
40
|
+
*/
|
|
41
|
+
heartbeat(runnerId: string, stats: PoolStats): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Graceful deregistration. Called when a runner shuts down cleanly.
|
|
44
|
+
* Immediately pauses its sessions and removes it from the registry,
|
|
45
|
+
* rather than waiting 30s for the liveness sweep to notice.
|
|
46
|
+
*/
|
|
47
|
+
deregisterRunner(runnerId: string): Promise<void>;
|
|
22
48
|
/**
|
|
23
49
|
* Select the best backend for a new session.
|
|
24
|
-
*
|
|
50
|
+
* Reads from DB to discover all healthy runners (multi-coordinator safe).
|
|
51
|
+
* Falls back to local backend if no remote runners available.
|
|
25
52
|
*/
|
|
26
|
-
selectBackend(): {
|
|
53
|
+
selectBackend(): Promise<{
|
|
27
54
|
backend: RunnerBackend;
|
|
28
55
|
runnerId: string;
|
|
29
|
-
}
|
|
56
|
+
}>;
|
|
30
57
|
/**
|
|
31
58
|
* Get the backend for a specific runner. Used for routing messages to existing sessions.
|
|
59
|
+
*
|
|
60
|
+
* Synchronous fast path: checks local cache first. If not found, falls back to local
|
|
61
|
+
* backend (standalone mode) or throws. For multi-coordinator mode where a runner was
|
|
62
|
+
* registered by a different coordinator, use getBackendForRunnerAsync().
|
|
32
63
|
*/
|
|
33
64
|
getBackendForRunner(runnerId: string | null | undefined): RunnerBackend;
|
|
65
|
+
/**
|
|
66
|
+
* Async version: looks up runner from DB if not in local cache.
|
|
67
|
+
* Required in multi-coordinator mode where a different coordinator may have
|
|
68
|
+
* registered the runner. Creates a RemoteRunnerBackend on the fly from the
|
|
69
|
+
* DB record and caches it locally.
|
|
70
|
+
*/
|
|
71
|
+
getBackendForRunnerAsync(runnerId: string | null | undefined): Promise<RunnerBackend>;
|
|
72
|
+
/**
|
|
73
|
+
* Get or lazily create a RemoteRunnerBackend from a DB record.
|
|
74
|
+
* The backends map is a local connection cache — avoids creating
|
|
75
|
+
* new HTTP clients on every request.
|
|
76
|
+
*/
|
|
77
|
+
private getOrCreateBackend;
|
|
34
78
|
/**
|
|
35
79
|
* Start periodic liveness checks for remote runners.
|
|
80
|
+
* Safe to run on multiple coordinators — all operations are idempotent.
|
|
81
|
+
* Each coordinator runs independently; no leader election needed.
|
|
82
|
+
*
|
|
83
|
+
* Uses random jitter (0-5s) on each interval to prevent thundering herd
|
|
84
|
+
* when multiple coordinators run the same sweep at the same time.
|
|
36
85
|
*/
|
|
37
86
|
startLivenessSweep(): void;
|
|
38
87
|
stopLivenessSweep(): void;
|
|
88
|
+
/**
|
|
89
|
+
* Single-query dead runner detection. Queries directly for runners past
|
|
90
|
+
* the heartbeat cutoff instead of listing ALL runners then filtering in JS.
|
|
91
|
+
*
|
|
92
|
+
* Cache cleanup: stale backend cache entries are cleaned up in handleDeadRunner()
|
|
93
|
+
* (for this coordinator's kills) and lazily on next use via getOrCreateBackend()
|
|
94
|
+
* (for kills by other coordinators). No need to list all runners every sweep.
|
|
95
|
+
*/
|
|
39
96
|
private checkLiveness;
|
|
97
|
+
/**
|
|
98
|
+
* Handle a dead runner: pause its sessions and remove from registry.
|
|
99
|
+
* Idempotent — safe to call from multiple coordinators concurrently.
|
|
100
|
+
* Uses a single bulk UPDATE instead of per-session queries.
|
|
101
|
+
*/
|
|
40
102
|
handleDeadRunner(runnerId: string): Promise<void>;
|
|
41
103
|
get runnerCount(): number;
|
|
42
104
|
get hasLocalBackend(): boolean;
|
|
43
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Get runner info from DB (not just local cache).
|
|
107
|
+
* Any coordinator gets the same view. Use this for monitoring/admin.
|
|
108
|
+
*/
|
|
109
|
+
getRunnerInfoFromDb(): Promise<Array<{
|
|
44
110
|
runnerId: string;
|
|
45
111
|
host: string;
|
|
46
112
|
port: number;
|
|
47
113
|
active: number;
|
|
48
114
|
max: number;
|
|
49
|
-
lastHeartbeat:
|
|
50
|
-
}
|
|
115
|
+
lastHeartbeat: string;
|
|
116
|
+
}>>;
|
|
117
|
+
/** Number of locally cached backend connections. */
|
|
118
|
+
get cachedBackendCount(): number;
|
|
51
119
|
}
|
|
52
120
|
//# sourceMappingURL=coordinator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/runner/coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/runner/coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAgB,MAAM,gBAAgB,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,iBAAiB;IAC5B,yFAAyF;IACzF,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,aAAa,CAAe;IACpC,OAAO,CAAC,kBAAkB,CAA+B;gBAE7C,IAAI,EAAE;QAAE,YAAY,CAAC,EAAE,aAAa,CAAA;KAAE;IAIlD;;OAEG;IACG,cAAc,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAajH;;OAEG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlE;;;;OAIG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvD;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAmB5E;;;;;;OAMG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa;IAevE;;;;;OAKG;IACG,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC;IAqB3F;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;;;;;;OAOG;IACH,kBAAkB,IAAI,IAAI;IAe1B,iBAAiB,IAAI,IAAI;IAOzB;;;;;;;OAOG;YACW,aAAa;IAS3B;;;;OAIG;IACG,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvD,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,IAAI,eAAe,IAAI,OAAO,CAE7B;IAED;;;OAGG;IACG,mBAAmB,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAYjJ,oDAAoD;IACpD,IAAI,kBAAkB,IAAI,MAAM,CAE/B;CACF"}
|
|
@@ -1,73 +1,76 @@
|
|
|
1
1
|
import { RUNNER_LIVENESS_TIMEOUT_MS } from '@ash-ai/shared';
|
|
2
2
|
import { RemoteRunnerBackend } from './remote-backend.js';
|
|
3
|
-
import {
|
|
3
|
+
import { bulkPauseSessionsByRunner, upsertRunner, heartbeatRunner, selectBestRunner, deleteRunner, listAllRunners, listDeadRunners, getRunner } from '../db/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Coordinates multiple runners. Routes session creation to the least-loaded runner.
|
|
6
6
|
* Detects dead runners and marks their sessions as paused.
|
|
7
|
+
*
|
|
8
|
+
* In multi-coordinator mode, the runner registry lives in the shared database
|
|
9
|
+
* (Postgres/CRDB). Any coordinator can discover all healthy runners by querying
|
|
10
|
+
* the DB. The in-memory `backends` map is a local connection cache — each
|
|
11
|
+
* coordinator creates RemoteRunnerBackend instances on demand when it first
|
|
12
|
+
* needs to talk to a runner it hasn't seen before.
|
|
13
|
+
*
|
|
14
|
+
* Design principles:
|
|
15
|
+
* - DB is the source of truth for runner discovery (multi-coordinator safe)
|
|
16
|
+
* - Local Map is a connection cache only (avoids creating new HTTP clients per request)
|
|
17
|
+
* - All write operations (register, heartbeat, delete) go through DB
|
|
18
|
+
* - All read operations for routing go through DB (selectBestRunner)
|
|
19
|
+
* - Liveness sweep is idempotent (safe to run on multiple coordinators)
|
|
7
20
|
*/
|
|
8
21
|
export class RunnerCoordinator {
|
|
9
|
-
|
|
22
|
+
/** Local connection cache: runnerId -> RemoteRunnerBackend. Lazily populated from DB. */
|
|
23
|
+
backends = new Map();
|
|
10
24
|
localBackend;
|
|
11
25
|
localRunnerId = '__local__';
|
|
12
26
|
livenessSweepTimer = null;
|
|
13
27
|
constructor(opts) {
|
|
14
28
|
this.localBackend = opts.localBackend ?? null;
|
|
15
29
|
}
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Register or re-register a runner. Persists to DB so all coordinators see it.
|
|
32
|
+
*/
|
|
33
|
+
async registerRunner(info) {
|
|
34
|
+
// Persist to DB (upsert — idempotent, safe for concurrent coordinators)
|
|
35
|
+
await upsertRunner(info.runnerId, info.host, info.port, info.maxSandboxes);
|
|
36
|
+
// Update local backend cache
|
|
37
|
+
const existing = this.backends.get(info.runnerId);
|
|
18
38
|
if (existing) {
|
|
19
|
-
|
|
20
|
-
existing.host = info.host;
|
|
21
|
-
existing.port = info.port;
|
|
22
|
-
existing.maxSandboxes = info.maxSandboxes;
|
|
23
|
-
existing.lastHeartbeat = Date.now();
|
|
24
|
-
console.log(`[coordinator] Runner ${info.runnerId} re-registered at ${info.host}:${info.port}`);
|
|
25
|
-
return;
|
|
39
|
+
existing.close();
|
|
26
40
|
}
|
|
27
|
-
|
|
28
|
-
this.runners.set(info.runnerId, {
|
|
29
|
-
...info,
|
|
30
|
-
backend,
|
|
31
|
-
lastHeartbeat: Date.now(),
|
|
32
|
-
stats: null,
|
|
33
|
-
});
|
|
41
|
+
this.backends.set(info.runnerId, new RemoteRunnerBackend({ host: info.host, port: info.port }));
|
|
34
42
|
console.log(`[coordinator] Runner ${info.runnerId} registered at ${info.host}:${info.port} (max ${info.maxSandboxes})`);
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Process a heartbeat. Updates DB so all coordinators see fresh capacity stats.
|
|
46
|
+
*/
|
|
47
|
+
async heartbeat(runnerId, stats) {
|
|
48
|
+
await heartbeatRunner(runnerId, stats.running ?? 0, stats.warming ?? 0);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Graceful deregistration. Called when a runner shuts down cleanly.
|
|
52
|
+
* Immediately pauses its sessions and removes it from the registry,
|
|
53
|
+
* rather than waiting 30s for the liveness sweep to notice.
|
|
54
|
+
*/
|
|
55
|
+
async deregisterRunner(runnerId) {
|
|
56
|
+
console.log(`[coordinator] Runner ${runnerId} deregistering gracefully`);
|
|
57
|
+
await this.handleDeadRunner(runnerId);
|
|
44
58
|
}
|
|
45
59
|
/**
|
|
46
60
|
* Select the best backend for a new session.
|
|
47
|
-
*
|
|
61
|
+
* Reads from DB to discover all healthy runners (multi-coordinator safe).
|
|
62
|
+
* Falls back to local backend if no remote runners available.
|
|
48
63
|
*/
|
|
49
|
-
selectBackend() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
continue;
|
|
58
|
-
const available = runner.stats
|
|
59
|
-
? runner.maxSandboxes - runner.stats.running - runner.stats.warming
|
|
60
|
-
: runner.maxSandboxes;
|
|
61
|
-
if (available > bestAvailable) {
|
|
62
|
-
bestAvailable = available;
|
|
63
|
-
bestRunner = runner;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (bestRunner && bestAvailable > 0) {
|
|
67
|
-
return { backend: bestRunner.backend, runnerId: bestRunner.runnerId };
|
|
68
|
-
}
|
|
64
|
+
async selectBackend() {
|
|
65
|
+
const cutoff = new Date(Date.now() - RUNNER_LIVENESS_TIMEOUT_MS).toISOString();
|
|
66
|
+
const bestRunner = await selectBestRunner(cutoff);
|
|
67
|
+
if (bestRunner) {
|
|
68
|
+
// selectBestRunner() already orders by available capacity DESC and only
|
|
69
|
+
// returns healthy runners. Trust the query — no redundant capacity check.
|
|
70
|
+
const backend = this.getOrCreateBackend(bestRunner);
|
|
71
|
+
return { backend, runnerId: bestRunner.id };
|
|
69
72
|
}
|
|
70
|
-
// Fall back to local backend
|
|
73
|
+
// Fall back to local backend (standalone mode)
|
|
71
74
|
if (this.localBackend) {
|
|
72
75
|
return { backend: this.localBackend, runnerId: this.localRunnerId };
|
|
73
76
|
}
|
|
@@ -75,6 +78,10 @@ export class RunnerCoordinator {
|
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
80
|
* Get the backend for a specific runner. Used for routing messages to existing sessions.
|
|
81
|
+
*
|
|
82
|
+
* Synchronous fast path: checks local cache first. If not found, falls back to local
|
|
83
|
+
* backend (standalone mode) or throws. For multi-coordinator mode where a runner was
|
|
84
|
+
* registered by a different coordinator, use getBackendForRunnerAsync().
|
|
78
85
|
*/
|
|
79
86
|
getBackendForRunner(runnerId) {
|
|
80
87
|
if (!runnerId || runnerId === this.localRunnerId) {
|
|
@@ -82,25 +89,76 @@ export class RunnerCoordinator {
|
|
|
82
89
|
return this.localBackend;
|
|
83
90
|
throw new Error('No local backend configured');
|
|
84
91
|
}
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
|
|
92
|
+
const cached = this.backends.get(runnerId);
|
|
93
|
+
if (cached && !cached.closed)
|
|
94
|
+
return cached;
|
|
95
|
+
// Not in cache. In standalone mode, fall back to local.
|
|
96
|
+
// In coordinator mode, caller should use getBackendForRunnerAsync().
|
|
97
|
+
if (this.localBackend)
|
|
98
|
+
return this.localBackend;
|
|
99
|
+
throw new Error(`Runner ${runnerId} not found in local cache — use getBackendForRunnerAsync() in multi-coordinator mode`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Async version: looks up runner from DB if not in local cache.
|
|
103
|
+
* Required in multi-coordinator mode where a different coordinator may have
|
|
104
|
+
* registered the runner. Creates a RemoteRunnerBackend on the fly from the
|
|
105
|
+
* DB record and caches it locally.
|
|
106
|
+
*/
|
|
107
|
+
async getBackendForRunnerAsync(runnerId) {
|
|
108
|
+
if (!runnerId || runnerId === this.localRunnerId) {
|
|
88
109
|
if (this.localBackend)
|
|
89
110
|
return this.localBackend;
|
|
90
|
-
throw new Error(
|
|
111
|
+
throw new Error('No local backend configured');
|
|
112
|
+
}
|
|
113
|
+
// Fast path: already cached
|
|
114
|
+
const cached = this.backends.get(runnerId);
|
|
115
|
+
if (cached && !cached.closed)
|
|
116
|
+
return cached;
|
|
117
|
+
// Slow path: look up from shared DB
|
|
118
|
+
const record = await getRunner(runnerId);
|
|
119
|
+
if (record) {
|
|
120
|
+
return this.getOrCreateBackend(record);
|
|
91
121
|
}
|
|
92
|
-
|
|
122
|
+
// Runner gone — fall back to local if available
|
|
123
|
+
if (this.localBackend)
|
|
124
|
+
return this.localBackend;
|
|
125
|
+
throw new Error(`Runner ${runnerId} not found`);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get or lazily create a RemoteRunnerBackend from a DB record.
|
|
129
|
+
* The backends map is a local connection cache — avoids creating
|
|
130
|
+
* new HTTP clients on every request.
|
|
131
|
+
*/
|
|
132
|
+
getOrCreateBackend(record) {
|
|
133
|
+
let backend = this.backends.get(record.id);
|
|
134
|
+
if (backend && !backend.closed)
|
|
135
|
+
return backend;
|
|
136
|
+
backend = new RemoteRunnerBackend({ host: record.host, port: record.port });
|
|
137
|
+
this.backends.set(record.id, backend);
|
|
138
|
+
return backend;
|
|
93
139
|
}
|
|
94
140
|
/**
|
|
95
141
|
* Start periodic liveness checks for remote runners.
|
|
142
|
+
* Safe to run on multiple coordinators — all operations are idempotent.
|
|
143
|
+
* Each coordinator runs independently; no leader election needed.
|
|
144
|
+
*
|
|
145
|
+
* Uses random jitter (0-5s) on each interval to prevent thundering herd
|
|
146
|
+
* when multiple coordinators run the same sweep at the same time.
|
|
96
147
|
*/
|
|
97
148
|
startLivenessSweep() {
|
|
98
149
|
if (this.livenessSweepTimer)
|
|
99
150
|
return;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
151
|
+
const scheduleNext = () => {
|
|
152
|
+
// Add 0-5s random jitter to prevent thundering herd across coordinators
|
|
153
|
+
const jitter = Math.floor(Math.random() * 5000);
|
|
154
|
+
this.livenessSweepTimer = setTimeout(() => {
|
|
155
|
+
this.checkLiveness()
|
|
156
|
+
.catch((err) => console.error('[coordinator] Liveness sweep error:', err))
|
|
157
|
+
.finally(() => scheduleNext());
|
|
158
|
+
}, RUNNER_LIVENESS_TIMEOUT_MS + jitter);
|
|
159
|
+
this.livenessSweepTimer.unref();
|
|
160
|
+
};
|
|
161
|
+
scheduleNext();
|
|
104
162
|
}
|
|
105
163
|
stopLivenessSweep() {
|
|
106
164
|
if (this.livenessSweepTimer) {
|
|
@@ -108,50 +166,66 @@ export class RunnerCoordinator {
|
|
|
108
166
|
this.livenessSweepTimer = null;
|
|
109
167
|
}
|
|
110
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Single-query dead runner detection. Queries directly for runners past
|
|
171
|
+
* the heartbeat cutoff instead of listing ALL runners then filtering in JS.
|
|
172
|
+
*
|
|
173
|
+
* Cache cleanup: stale backend cache entries are cleaned up in handleDeadRunner()
|
|
174
|
+
* (for this coordinator's kills) and lazily on next use via getOrCreateBackend()
|
|
175
|
+
* (for kills by other coordinators). No need to list all runners every sweep.
|
|
176
|
+
*/
|
|
111
177
|
async checkLiveness() {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
178
|
+
const cutoff = new Date(Date.now() - RUNNER_LIVENESS_TIMEOUT_MS).toISOString();
|
|
179
|
+
const deadRunners = await listDeadRunners(cutoff);
|
|
180
|
+
for (const runner of deadRunners) {
|
|
181
|
+
console.warn(`[coordinator] Runner ${runner.id} missed heartbeat — marking sessions paused`);
|
|
182
|
+
await this.handleDeadRunner(runner.id);
|
|
118
183
|
}
|
|
119
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Handle a dead runner: pause its sessions and remove from registry.
|
|
187
|
+
* Idempotent — safe to call from multiple coordinators concurrently.
|
|
188
|
+
* Uses a single bulk UPDATE instead of per-session queries.
|
|
189
|
+
*/
|
|
120
190
|
async handleDeadRunner(runnerId) {
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await updateSessionStatus(session.id, 'paused');
|
|
126
|
-
console.log(`[coordinator] Paused session ${session.id} (runner ${runnerId} dead)`);
|
|
127
|
-
}
|
|
191
|
+
// Single bulk UPDATE — no N+1
|
|
192
|
+
const pausedCount = await bulkPauseSessionsByRunner(runnerId);
|
|
193
|
+
if (pausedCount > 0) {
|
|
194
|
+
console.log(`[coordinator] Paused ${pausedCount} session(s) on runner ${runnerId}`);
|
|
128
195
|
}
|
|
129
|
-
// Remove
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
// Remove from DB (all coordinators will see this)
|
|
197
|
+
await deleteRunner(runnerId);
|
|
198
|
+
// Clean up local cache
|
|
199
|
+
const backend = this.backends.get(runnerId);
|
|
200
|
+
if (backend) {
|
|
201
|
+
backend.close();
|
|
202
|
+
this.backends.delete(runnerId);
|
|
134
203
|
}
|
|
135
204
|
}
|
|
136
205
|
get runnerCount() {
|
|
137
|
-
return this.
|
|
206
|
+
return this.backends.size;
|
|
138
207
|
}
|
|
139
208
|
get hasLocalBackend() {
|
|
140
209
|
return this.localBackend !== null;
|
|
141
210
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Get runner info from DB (not just local cache).
|
|
213
|
+
* Any coordinator gets the same view. Use this for monitoring/admin.
|
|
214
|
+
*/
|
|
215
|
+
async getRunnerInfoFromDb() {
|
|
216
|
+
const allRunners = await listAllRunners();
|
|
217
|
+
return allRunners.map((r) => ({
|
|
218
|
+
runnerId: r.id,
|
|
219
|
+
host: r.host,
|
|
220
|
+
port: r.port,
|
|
221
|
+
active: r.activeCount,
|
|
222
|
+
max: r.maxSandboxes,
|
|
223
|
+
lastHeartbeat: r.lastHeartbeatAt,
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
/** Number of locally cached backend connections. */
|
|
227
|
+
get cachedBackendCount() {
|
|
228
|
+
return this.backends.size;
|
|
155
229
|
}
|
|
156
230
|
}
|
|
157
231
|
//# sourceMappingURL=coordinator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordinator.js","sourceRoot":"","sources":["../../src/runner/coordinator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAE5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"coordinator.js","sourceRoot":"","sources":["../../src/runner/coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,7 @@ export declare class LocalRunnerBackend implements RunnerBackend {
|
|
|
13
13
|
destroySandbox(sandboxId: string): Promise<void>;
|
|
14
14
|
destroyAll(): Promise<void>;
|
|
15
15
|
sendCommand(sandboxId: string, cmd: BridgeCommand): AsyncGenerator<BridgeEvent>;
|
|
16
|
+
interrupt(sandboxId: string): void;
|
|
16
17
|
getSandbox(sandboxId: string): SandboxHandle | undefined;
|
|
17
18
|
isSandboxAlive(sandboxId: string): boolean;
|
|
18
19
|
markRunning(sandboxId: string): void;
|