bunqueue-dashboard 0.0.46 → 1.0.1
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/README.md +6 -6
- package/agent/compiledRuntime.ts +27 -0
- package/agent/db/boundedStream.ts +18 -0
- package/agent/db/exportTimeout.ts +8 -6
- package/agent/db/processWorker.ts +121 -0
- package/agent/db/queryTimeout.ts +62 -50
- package/agent/db/readAdmission.ts +53 -0
- package/agent/db/readOperations.ts +27 -0
- package/agent/db/readProcessMain.ts +64 -0
- package/agent/db/types.ts +3 -0
- package/agent/db/workerFactory.ts +12 -10
- package/agent/dbReadWorker.ts +21 -0
- package/agent/index.ts +2 -1
- package/agent/managedTarget.ts +3 -1
- package/agent/manager/commandLine.ts +33 -0
- package/agent/manager/configStore.ts +65 -0
- package/agent/manager/errors.ts +4 -0
- package/agent/manager/process.ts +12 -3
- package/agent/manager.ts +1 -0
- package/agent/server/controlRoutes.ts +10 -0
- package/agent/server/controlStatus.ts +26 -5
- package/agent/server/databaseRoutes.ts +13 -17
- package/agent/server/errors.ts +6 -0
- package/agent/server/handler.ts +2 -1
- package/agent/server/router.ts +2 -1
- package/agent/server/workflowReadRoutes.ts +9 -11
- package/agent/shutdown.ts +3 -2
- package/agent/workflow/sqlite.ts +1 -1
- package/dist/.vite/manifest.json +537 -575
- package/dist/assets/AddJob-CiwWycYG.js +3 -0
- package/dist/assets/Alerts-DzKKoA9U.js +1 -0
- package/dist/assets/AreaChart-Db2UVnaK.js +1 -0
- package/dist/assets/Benchmark-CHYDL31K.js +1 -0
- package/dist/assets/BulkAddJobs-GRMVnf0j.js +5 -0
- package/dist/assets/Button-_aSJvRoW.js +1 -0
- package/dist/assets/Card-egIDLwVk.js +1 -0
- package/dist/assets/Cron-XL0PdUEc.js +1 -0
- package/dist/assets/CronManager-Cqtg9Zr8.js +1 -0
- package/dist/assets/{Database-DV2pcDLL.js → Database-BoxVpjts.js} +3 -3
- package/dist/assets/Diagnostics-B3QSQuFF.js +1 -0
- package/dist/assets/Dlq-DHOfgORa.js +1 -0
- package/dist/assets/DlqControl-BY1e55-x.js +1 -0
- package/dist/assets/DlqPro-CfwKDcpc.js +1 -0
- package/dist/assets/Fleet-Bkbq0dFB.js +1 -0
- package/dist/assets/Flows-CDCQrPit.js +2 -0
- package/dist/assets/JobInspector-Bw3Gs1Iy.js +2 -0
- package/dist/assets/Jobs-IrlneOy6.js +1 -0
- package/dist/assets/JobsPro-vlPd6Ows.js +1 -0
- package/dist/assets/Logs-DG7cK-tF.js +1 -0
- package/dist/assets/LogsPro-ClSqFKys.js +1 -0
- package/dist/assets/{McpServer-fVy27Mof.js → McpServer-DoNFY0Pj.js} +1 -1
- package/dist/assets/{Metrics-Ba46pdzT.js → Metrics-BkePZiCZ.js} +1 -1
- package/dist/assets/MetricsPro-ooSvpQ3H.js +1 -0
- package/dist/assets/{Overview-DV2qo5v3.js → Overview-CuWmPRra.js} +1 -1
- package/dist/assets/{PageHeader-CvIyaFOO.js → PageHeader-CQzNsw8r.js} +1 -1
- package/dist/assets/Pagination-CSkzhNW6.js +1 -0
- package/dist/assets/QueueActions-BBxRt-t0.js +1 -0
- package/dist/assets/QueueControl-C01l6bBJ.js +1 -0
- package/dist/assets/QueueDetail-gSf9Tb7G.js +1 -0
- package/dist/assets/QueueDetailPro-BKszqI0M.js +1 -0
- package/dist/assets/Queues-BAg2nSz0.js +1 -0
- package/dist/assets/QueuesOverview-BLSjiYsA.js +1 -0
- package/dist/assets/{S3Backup-DNABHR2Q.js → S3Backup-Ci4dJPP_.js} +1 -1
- package/dist/assets/S3BackupPro-DcjlrzC5.js +3 -0
- package/dist/assets/ServerControl-SiZBNo9P.js +3 -0
- package/dist/assets/Settings-DR_iqW0T.js +1 -0
- package/dist/assets/StatusBadge-DereLzOj.js +1 -0
- package/dist/assets/{Usage-1KxG1mJ9.js → Usage-ZWfB7PcR.js} +1 -1
- package/dist/assets/UsagePro-BjdeS2zb.js +1 -0
- package/dist/assets/Webhooks-BonYUgMo.js +1 -0
- package/dist/assets/Workers-C1h6hCPp.js +1 -0
- package/dist/assets/WorkersPro-B0f_Nfdz.js +1 -0
- package/dist/assets/Workflows-CqZZ96EK.js +1 -0
- package/dist/assets/{api-BgWE-705.js → api-D9aT0ND7.js} +1 -1
- package/dist/assets/bq-DR2BTISO.js +1 -0
- package/dist/assets/classicJobsData-Csj4iihY.js +1 -0
- package/dist/assets/connectionStore-CVjQyIZv.js +1 -0
- package/dist/assets/controlConnectionEpoch-ChAeMMLk.js +1 -0
- package/dist/assets/feedback-CtGN_kqB.js +1 -0
- package/dist/assets/form-DG3X6cLL.js +1 -0
- package/dist/assets/{icons-BVzwsfbG.js → icons-DH9SS1DA.js} +1 -1
- package/dist/assets/index-Bat1DN5U.js +8 -0
- package/dist/assets/react-vendor-CFL9l8LK.js +11 -0
- package/dist/assets/{cn-zFEt5SO8.js → rolldown-runtime-QTnfLwEv.js} +1 -1
- package/dist/assets/{toastStore-IZn6OPLZ.js → toastStore-XmMX7A5W.js} +1 -1
- package/dist/assets/useControlActionGuard-1i7aFA7E.js +1 -0
- package/dist/assets/usePolledData-CEyyLRYa.js +1 -0
- package/dist/assets/useServerActionGuard-CUx8TOAC.js +1 -0
- package/dist/assets/useThroughputSeries-CeFx-Mwq.js +1 -0
- package/dist/index.html +14 -14
- package/package.json +16 -10
- package/scripts/embedded.gen.ts +152 -162
- package/scripts/serve.ts +10 -15
- package/scripts/serveHandler.ts +28 -1
- package/dist/assets/AddJob-FNaPbs_V.js +0 -3
- package/dist/assets/Alerts-vzIYtFpW.js +0 -1
- package/dist/assets/AreaChart-DEAalJ6P.js +0 -1
- package/dist/assets/Benchmark-C16tT4pF.js +0 -1
- package/dist/assets/BulkAddJobs-IiL1uud0.js +0 -5
- package/dist/assets/Button-Ch1W94fJ.js +0 -1
- package/dist/assets/Card-CWxrC8OY.js +0 -1
- package/dist/assets/CopilotPanel-FlAW3RlL.js +0 -44
- package/dist/assets/Cron-1DZTZWXO.js +0 -1
- package/dist/assets/CronManager-B6xLLgI5.js +0 -1
- package/dist/assets/Diagnostics-B6apZrC5.js +0 -1
- package/dist/assets/Dlq-MRmkQrNC.js +0 -1
- package/dist/assets/DlqControl-DNJPVOTF.js +0 -1
- package/dist/assets/DlqPro-CwtTDwGN.js +0 -1
- package/dist/assets/Fleet-Bdrj9bLI.js +0 -1
- package/dist/assets/Flows-1dC8F_wx.js +0 -2
- package/dist/assets/JobInspector-Ct9Ga-UT.js +0 -2
- package/dist/assets/Jobs-Chqv0RuL.js +0 -1
- package/dist/assets/JobsPro-CLzOhKAL.js +0 -1
- package/dist/assets/Logs-C5PNP9Wq.js +0 -1
- package/dist/assets/LogsPro-B3KwTSsn.js +0 -1
- package/dist/assets/MetricsPro-B9rD_KQc.js +0 -1
- package/dist/assets/Pagination-x-Cwgrul.js +0 -1
- package/dist/assets/QueueActions-BWnmNEqV.js +0 -1
- package/dist/assets/QueueControl-DqBvuDnF.js +0 -1
- package/dist/assets/QueueDetail-COSH9bJl.js +0 -1
- package/dist/assets/QueueDetailPro-ClKtyHIf.js +0 -1
- package/dist/assets/Queues-D2-R8nYh.js +0 -1
- package/dist/assets/QueuesOverview-tokquA3g.js +0 -1
- package/dist/assets/S3BackupPro-CDX5WQUY.js +0 -3
- package/dist/assets/ServerControl-BmccMNW9.js +0 -3
- package/dist/assets/Settings-UcujwvKv.js +0 -1
- package/dist/assets/StatusBadge-BoOVTnKu.js +0 -1
- package/dist/assets/UsagePro-BGLufsE7.js +0 -1
- package/dist/assets/Webhooks-DT2LK17V.js +0 -1
- package/dist/assets/Workers-BX1qspaQ.js +0 -1
- package/dist/assets/WorkersPro-BsUQVgQZ.js +0 -1
- package/dist/assets/Workflows-DZZA6aGW.js +0 -1
- package/dist/assets/bq-F5BbTG1T.js +0 -1
- package/dist/assets/connectionStore-BJLihGaC.js +0 -1
- package/dist/assets/controlConnectionEpoch-CZsS0wAb.js +0 -1
- package/dist/assets/dist-1f1fMHVO.js +0 -11
- package/dist/assets/dist-BlWBaCN6.js +0 -11
- package/dist/assets/dist-DZL8soAh.js +0 -79
- package/dist/assets/dist-DjvZiKhB.js +0 -2
- package/dist/assets/dist-DpZcbP9W.js +0 -11
- package/dist/assets/feedback-QDN1AsYt.js +0 -1
- package/dist/assets/form-C4oby9pI.js +0 -1
- package/dist/assets/index-8Ii-_Rml.js +0 -8
- package/dist/assets/react-vendor-CvwrP0tU.js +0 -11
- package/dist/assets/useControlActionGuard-B-bzKZKI.js +0 -1
- package/dist/assets/usePolledData-0CVelkl2.js +0 -1
- package/dist/assets/useServerActionGuard-uGePilQQ.js +0 -1
- package/dist/assets/useThroughputSeries-DgO8uicf.js +0 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split the managed launch command into argv without a shell. Whitespace
|
|
3
|
+
* separates arguments; a balanced pair of double or single quotes groups an
|
|
4
|
+
* argument that contains spaces (`"C:\Program Files\bun\bun.exe" run main.ts`).
|
|
5
|
+
* Backslashes are literal so Windows paths need no escaping. A command with an
|
|
6
|
+
* unbalanced quote (an apostrophe in a path) keeps the legacy whitespace split,
|
|
7
|
+
* so a configuration that launched before still launches the same way.
|
|
8
|
+
*/
|
|
9
|
+
export function splitCommandLine(command: string): string[] {
|
|
10
|
+
const args: string[] = [];
|
|
11
|
+
let current = '';
|
|
12
|
+
let quote: '"' | "'" | null = null;
|
|
13
|
+
let inArgument = false;
|
|
14
|
+
for (const char of command.trim()) {
|
|
15
|
+
if (quote) {
|
|
16
|
+
if (char === quote) quote = null;
|
|
17
|
+
else current += char;
|
|
18
|
+
} else if (char === '"' || char === "'") {
|
|
19
|
+
quote = char;
|
|
20
|
+
inArgument = true;
|
|
21
|
+
} else if (/\s/.test(char)) {
|
|
22
|
+
if (inArgument) args.push(current);
|
|
23
|
+
current = '';
|
|
24
|
+
inArgument = false;
|
|
25
|
+
} else {
|
|
26
|
+
current += char;
|
|
27
|
+
inArgument = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (quote) return command.trim().split(/\s+/);
|
|
31
|
+
if (inArgument) args.push(current);
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { copyConfig, validateServerConfig } from './config';
|
|
4
|
+
import type { ServerConfig } from './types';
|
|
5
|
+
|
|
6
|
+
export interface ConfigStore {
|
|
7
|
+
load(fallback: ServerConfig): ServerConfig;
|
|
8
|
+
save(config: ServerConfig): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
/** Private atomic snapshots. Errors never include configuration secrets. */
|
|
14
|
+
export class FileConfigStore implements ConfigStore {
|
|
15
|
+
constructor(readonly path: string) {}
|
|
16
|
+
|
|
17
|
+
load(fallback: ServerConfig): ServerConfig {
|
|
18
|
+
let descriptor: number;
|
|
19
|
+
try {
|
|
20
|
+
descriptor = openSync(this.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return copyConfig(fallback);
|
|
23
|
+
throw new Error('Cannot read saved agent configuration');
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
if (fstatSync(descriptor).size > MAX_CONFIG_BYTES) throw new Error('oversize');
|
|
27
|
+
const raw = readFileSync(descriptor);
|
|
28
|
+
if (raw.byteLength > MAX_CONFIG_BYTES) throw new Error('oversize');
|
|
29
|
+
const saved = JSON.parse(raw.toString()) as { version?: unknown; config?: unknown };
|
|
30
|
+
if (saved?.version !== 1) throw new Error('unsupported format');
|
|
31
|
+
return copyConfig(validateServerConfig(saved.config));
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error('Saved agent configuration is invalid; repair or remove AGENT_CONFIG_PATH before restarting');
|
|
34
|
+
} finally {
|
|
35
|
+
closeSync(descriptor);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
save(config: ServerConfig): void {
|
|
40
|
+
const content = JSON.stringify({ version: 1, config: validateServerConfig(config) }) + '\n';
|
|
41
|
+
if (Buffer.byteLength(content) > MAX_CONFIG_BYTES) throw new Error('Agent configuration exceeds 1 MiB');
|
|
42
|
+
const directory = dirname(this.path);
|
|
43
|
+
const temporary = `${this.path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
44
|
+
let descriptor: number | undefined;
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
47
|
+
descriptor = openSync(temporary, 'wx', 0o600);
|
|
48
|
+
writeFileSync(descriptor, content);
|
|
49
|
+
fsyncSync(descriptor);
|
|
50
|
+
closeSync(descriptor);
|
|
51
|
+
descriptor = undefined;
|
|
52
|
+
renameSync(temporary, this.path);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error('Cannot persist agent configuration; the change was not applied');
|
|
55
|
+
} finally {
|
|
56
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
57
|
+
rmSync(temporary, { force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Paths resolve against the launch directory, just like the database path. Use one path per agent. */
|
|
63
|
+
export function agentConfigStore(env: NodeJS.ProcessEnv = process.env): FileConfigStore {
|
|
64
|
+
return new FileConfigStore(resolve(env.AGENT_CONFIG_PATH || '.bunqueue-dashboard/config.json'));
|
|
65
|
+
}
|
package/agent/manager/process.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { copyConfig, defaultConfig, validateConfigPatch, validateServerConfig } from './config';
|
|
2
2
|
import { ProcessLogs } from './logs';
|
|
3
|
+
import { splitCommandLine } from './commandLine';
|
|
4
|
+
import { ManagedProcessStuckError } from './errors';
|
|
5
|
+
import type { ConfigStore } from './configStore';
|
|
3
6
|
import { databaseStats } from './storage';
|
|
4
7
|
import { removeSqlitePaths, validateManagedStorage } from './storageMode';
|
|
5
8
|
import type {
|
|
@@ -40,7 +43,12 @@ export class ProcessManager {
|
|
|
40
43
|
private maintenance: string | null = null;
|
|
41
44
|
private output = new ProcessLogs();
|
|
42
45
|
|
|
43
|
-
constructor(
|
|
46
|
+
constructor(
|
|
47
|
+
private readonly stopTimeoutMs = STOP_TIMEOUT_MS,
|
|
48
|
+
private readonly configStore?: ConfigStore
|
|
49
|
+
) {
|
|
50
|
+
this.config = configStore?.load(this.config) ?? this.config;
|
|
51
|
+
}
|
|
44
52
|
|
|
45
53
|
getConfig(): ServerConfig {
|
|
46
54
|
return copyConfig(this.config);
|
|
@@ -63,6 +71,7 @@ export class ProcessManager {
|
|
|
63
71
|
extraEnv: valid.extraEnv ?? this.config.extraEnv,
|
|
64
72
|
});
|
|
65
73
|
if (!sameConfig(this.config, next)) {
|
|
74
|
+
this.configStore?.save(next);
|
|
66
75
|
this.config = next;
|
|
67
76
|
this.configRevision = nextConfigRevision(this.configRevision);
|
|
68
77
|
}
|
|
@@ -111,7 +120,7 @@ export class ProcessManager {
|
|
|
111
120
|
|
|
112
121
|
const launchConfig = validateServerConfig(this.config);
|
|
113
122
|
const storageMode = validateManagedStorage(launchConfig);
|
|
114
|
-
const [command, ...args] = launchConfig.command
|
|
123
|
+
const [command, ...args] = splitCommandLine(launchConfig.command);
|
|
115
124
|
this.status = 'starting';
|
|
116
125
|
this.exitCode = null;
|
|
117
126
|
const token = ++this.procToken;
|
|
@@ -212,7 +221,7 @@ export class ProcessManager {
|
|
|
212
221
|
if (!(await exitsWithin(process, this.stopTimeoutMs))) {
|
|
213
222
|
const message = `pid ${process.pid} did not exit after SIGKILL`;
|
|
214
223
|
this.output.push('sys', `${message} — retaining failed-process tracking`);
|
|
215
|
-
throw new
|
|
224
|
+
throw new ManagedProcessStuckError(message);
|
|
216
225
|
}
|
|
217
226
|
}
|
|
218
227
|
this.output.cancel(token);
|
package/agent/manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Public compatibility entrypoint for managed bunqueue process supervision. */
|
|
2
2
|
export { validateConfigPatch } from './manager/config';
|
|
3
|
+
export { ManagedProcessStuckError } from './manager/errors';
|
|
3
4
|
export { ProcessManager } from './manager/process';
|
|
4
5
|
export {
|
|
5
6
|
managedPostgresNamespace,
|
|
@@ -123,6 +123,16 @@ export function assertManagedControl(controlTarget: ServerControlTarget): void {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Release the Workflow Engine's read-write SQLite connection before the data
|
|
128
|
+
* file is replaced. A broker that crashed (or was killed externally) leaves the
|
|
129
|
+
* engine open; after a restore it would keep writing to the unlinked file.
|
|
130
|
+
*/
|
|
131
|
+
export async function closeWorkflowRuntime(runtime: WorkflowRuntimePort): Promise<void> {
|
|
132
|
+
const closeResult = await settle(() => runtime.close());
|
|
133
|
+
if (!closeResult.ok) throw workflowCloseError(closeResult.error);
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
async function stopWithRuntimeCleanup(
|
|
127
137
|
manager: ProcessManager,
|
|
128
138
|
runtime: WorkflowRuntimePort
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
managedStorageMode,
|
|
5
5
|
type ProcessManager,
|
|
6
6
|
} from '../manager';
|
|
7
|
+
import { managedConnection } from '../managedConnection';
|
|
7
8
|
import { probeExternalHealth, type ServerControlTarget } from './controlTarget';
|
|
8
9
|
|
|
9
10
|
export async function statusWithHealth(
|
|
@@ -30,12 +31,10 @@ export async function statusWithHealth(
|
|
|
30
31
|
}
|
|
31
32
|
let healthy = false;
|
|
32
33
|
let version: string | undefined;
|
|
34
|
+
const effectiveConfig = snapshot.runningConfig ?? snapshot.config;
|
|
33
35
|
if (snapshot.status === 'running') {
|
|
34
|
-
const port = snapshot.runningConfig?.httpPort ?? snapshot.config.httpPort;
|
|
35
36
|
try {
|
|
36
|
-
const response = await
|
|
37
|
-
signal: AbortSignal.timeout(1500),
|
|
38
|
-
});
|
|
37
|
+
const response = await fetchManagedHealth(effectiveConfig);
|
|
39
38
|
if (response.ok) {
|
|
40
39
|
const body: unknown = await response.json();
|
|
41
40
|
if (body !== null && typeof body === 'object' && !Array.isArray(body)) {
|
|
@@ -48,7 +47,6 @@ export async function statusWithHealth(
|
|
|
48
47
|
// The managed process is not healthy yet.
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
|
-
const effectiveConfig = snapshot.runningConfig ?? snapshot.config;
|
|
52
50
|
const storageMode = statusStorageMode(effectiveConfig);
|
|
53
51
|
const database = await statusDatabase(manager, effectiveConfig);
|
|
54
52
|
const current = manager.getStatus();
|
|
@@ -85,6 +83,29 @@ export async function statusWithHealth(
|
|
|
85
83
|
};
|
|
86
84
|
}
|
|
87
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Bunqueue serves HTTPS on its HTTP port once server TLS is configured, so the
|
|
88
|
+
* probe follows the same TLS resolution (and private CA) as the agent's TCP
|
|
89
|
+
* clients; a plain-HTTP probe would report a healthy TLS server as down forever.
|
|
90
|
+
*/
|
|
91
|
+
async function fetchManagedHealth(
|
|
92
|
+
config: ReturnType<ProcessManager['getConfig']>
|
|
93
|
+
): Promise<Response> {
|
|
94
|
+
let tls: ReturnType<typeof managedConnection>['tls'];
|
|
95
|
+
try {
|
|
96
|
+
tls = managedConnection(config).tls;
|
|
97
|
+
} catch {
|
|
98
|
+
// Invalid TLS settings are reported by the routes that use them.
|
|
99
|
+
tls = undefined;
|
|
100
|
+
}
|
|
101
|
+
const caFile = typeof tls === 'object' ? tls.caFile : undefined;
|
|
102
|
+
const ca = caFile ? await Bun.file(caFile).text() : undefined;
|
|
103
|
+
return fetch(`${tls ? 'https' : 'http'}://127.0.0.1:${config.httpPort}/health`, {
|
|
104
|
+
signal: AbortSignal.timeout(1500),
|
|
105
|
+
...(ca ? { tls: { ca } } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
async function statusDatabase(
|
|
89
110
|
manager: ProcessManager,
|
|
90
111
|
config: ReturnType<ProcessManager['getConfig']>
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
+
import { readWithTimeout } from '../db/queryTimeout';
|
|
1
2
|
import {
|
|
2
3
|
type DbFilter,
|
|
3
|
-
dbCell,
|
|
4
|
-
dbInfo,
|
|
5
|
-
dbRows,
|
|
6
|
-
dbSchema,
|
|
7
|
-
dbTables,
|
|
8
4
|
exportWithTimeout,
|
|
9
5
|
queryWithTimeout,
|
|
10
6
|
} from '../db';
|
|
@@ -105,15 +101,15 @@ export async function routeDatabaseRequest(
|
|
|
105
101
|
admission?: ManagedDatabaseAdmission
|
|
106
102
|
): Promise<RouteResponse | Response | null> {
|
|
107
103
|
if (pathname === '/db/info' && method === 'GET') {
|
|
108
|
-
return withDatabase(dataPath, admission, (path) => ({
|
|
104
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
109
105
|
status: 200,
|
|
110
|
-
body: { ok: true, ...dbInfo
|
|
106
|
+
body: { ok: true, ...(await readWithTimeout('dbInfo', [path], request.signal)) },
|
|
111
107
|
}));
|
|
112
108
|
}
|
|
113
109
|
if (pathname === '/db/tables' && method === 'GET') {
|
|
114
|
-
return withDatabase(dataPath, admission, (path) => ({
|
|
110
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
115
111
|
status: 200,
|
|
116
|
-
body: { ok: true, tables: dbTables
|
|
112
|
+
body: { ok: true, tables: await readWithTimeout('dbTables', [path], request.signal) },
|
|
117
113
|
}));
|
|
118
114
|
}
|
|
119
115
|
if (pathname.startsWith('/db/tables/') && method === 'GET') {
|
|
@@ -126,20 +122,20 @@ export async function routeDatabaseRequest(
|
|
|
126
122
|
);
|
|
127
123
|
}
|
|
128
124
|
if (segments.length === 2 && subresource === 'schema') {
|
|
129
|
-
return withDatabase(dataPath, admission, (path) => ({
|
|
125
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
130
126
|
status: 200,
|
|
131
|
-
body: { ok: true, ...dbSchema
|
|
127
|
+
body: { ok: true, ...(await readWithTimeout('dbSchema', [path, table], request.signal)) },
|
|
132
128
|
}));
|
|
133
129
|
}
|
|
134
130
|
if (segments.length === 2 && subresource === 'cell') {
|
|
135
131
|
const query = new URL(request.url).searchParams;
|
|
136
132
|
const rowid = query.get('rowid');
|
|
137
133
|
if (rowid === null) throw new Error('rowid is required');
|
|
138
|
-
return withDatabase(dataPath, admission, (path) => ({
|
|
134
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
139
135
|
status: 200,
|
|
140
136
|
body: {
|
|
141
137
|
ok: true,
|
|
142
|
-
...dbCell
|
|
138
|
+
...(await readWithTimeout('dbCell', [path, table, rowid, query.get('column') ?? ''], request.signal)),
|
|
143
139
|
},
|
|
144
140
|
}));
|
|
145
141
|
}
|
|
@@ -147,11 +143,11 @@ export async function routeDatabaseRequest(
|
|
|
147
143
|
return { status: 404, body: { ok: false, error: 'Not found' } };
|
|
148
144
|
}
|
|
149
145
|
const query = new URL(request.url).searchParams;
|
|
150
|
-
return withDatabase(dataPath, admission, (path) => ({
|
|
146
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
151
147
|
status: 200,
|
|
152
148
|
body: {
|
|
153
149
|
ok: true,
|
|
154
|
-
...dbRows
|
|
150
|
+
...(await readWithTimeout('dbRows', [
|
|
155
151
|
path,
|
|
156
152
|
table,
|
|
157
153
|
Number(query.get('limit')) || 50,
|
|
@@ -159,7 +155,7 @@ export async function routeDatabaseRequest(
|
|
|
159
155
|
query.get('orderBy') || undefined,
|
|
160
156
|
query.get('dir') === 'desc' ? 'desc' : 'asc',
|
|
161
157
|
browseFilter(query)
|
|
162
|
-
),
|
|
158
|
+
], request.signal)),
|
|
163
159
|
},
|
|
164
160
|
}));
|
|
165
161
|
}
|
|
@@ -171,7 +167,7 @@ export async function routeDatabaseRequest(
|
|
|
171
167
|
})) as { sql?: string };
|
|
172
168
|
return withDatabase(dataPath, admission, async (path) => ({
|
|
173
169
|
status: 200,
|
|
174
|
-
body: { ok: true, ...(await queryWithTimeout(path, sql ?? '')) },
|
|
170
|
+
body: { ok: true, ...(await queryWithTimeout(path, sql ?? '', request.signal)) },
|
|
175
171
|
}));
|
|
176
172
|
}
|
|
177
173
|
return null;
|
package/agent/server/errors.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
DbExportUnavailableError,
|
|
4
4
|
MissingDbError,
|
|
5
5
|
} from '../db';
|
|
6
|
+
import { DbReadBusyError, DbReadUnavailableError } from '../db/types';
|
|
7
|
+
import { ManagedProcessStuckError } from '../manager';
|
|
6
8
|
import { safeErrorMessage } from '../errorMessage';
|
|
7
9
|
import { QueueOperationsUnavailableError } from '../queue/types';
|
|
8
10
|
import { WorkflowRuntimeUnavailableError } from '../workflow/runtime';
|
|
@@ -14,6 +16,7 @@ const CONFLICT_MESSAGES = [
|
|
|
14
16
|
'Another backup operation is already running',
|
|
15
17
|
'Another maintenance operation is already running',
|
|
16
18
|
'Cannot start the managed Bunqueue server',
|
|
19
|
+
'Cannot start while the previous managed process',
|
|
17
20
|
'Cannot restart the managed Bunqueue server',
|
|
18
21
|
'BUNQUEUE_WORKFLOW_QUEUE_NAME must',
|
|
19
22
|
'BUNQUEUE_WORKFLOW_CONCURRENCY must',
|
|
@@ -25,7 +28,10 @@ const CONFLICT_MESSAGES = [
|
|
|
25
28
|
export function errorStatus(error: unknown): number {
|
|
26
29
|
try {
|
|
27
30
|
if (error instanceof AgentLifecycleClosedError) return 503;
|
|
31
|
+
if (error instanceof ManagedProcessStuckError) return 500;
|
|
28
32
|
if (error instanceof MissingDbError) return 404;
|
|
33
|
+
if (error instanceof DbReadBusyError) return 429;
|
|
34
|
+
if (error instanceof DbReadUnavailableError) return 503;
|
|
29
35
|
if (error instanceof DbExportBusyError) return 429;
|
|
30
36
|
if (error instanceof DbExportUnavailableError) return 503;
|
|
31
37
|
if (error instanceof QueueOperationsUnavailableError) {
|
package/agent/server/handler.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { terminateDatabaseProcesses } from '../db/processWorker';
|
|
1
2
|
import { BunqueueBackupRunner, type BackupRunnerPort } from '../backup/runner';
|
|
2
3
|
import type { ProcessManager } from '../manager';
|
|
3
4
|
import { QueueOperationsRuntime } from '../queue/runtime';
|
|
@@ -110,7 +111,7 @@ export function createFetchHandler(
|
|
|
110
111
|
shuttingDown ??= closeThenShutdown(beginShutdown, manager);
|
|
111
112
|
return shuttingDown;
|
|
112
113
|
};
|
|
113
|
-
const forceShutdown = () => manager.forceShutdown();
|
|
114
|
+
const forceShutdown = () => { terminateDatabaseProcesses(); manager.forceShutdown(); };
|
|
114
115
|
return Object.assign(handle, { close, beginShutdown, shutdown, forceShutdown });
|
|
115
116
|
}
|
|
116
117
|
|
package/agent/server/router.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { routeQueueOperationsRequest } from '../queue/routes';
|
|
|
10
10
|
import type { QueueOperationsPort } from '../queue/types';
|
|
11
11
|
import type { WorkflowRuntimePort } from '../workflow/runtime';
|
|
12
12
|
import { routeWorkflowRuntimeRequest } from '../workflow/runtimeRoutes';
|
|
13
|
-
import { assertManagedControl, routeControlRequest } from './controlRoutes';
|
|
13
|
+
import { assertManagedControl, closeWorkflowRuntime, routeControlRequest } from './controlRoutes';
|
|
14
14
|
import type { ServerControlTarget } from './controlTarget';
|
|
15
15
|
import { routeDatabaseRequest } from './databaseRoutes';
|
|
16
16
|
import type { AgentLifecyclePort } from './lifecycle';
|
|
@@ -123,6 +123,7 @@ export async function routeAgentRequest(
|
|
|
123
123
|
return sqliteOnly('S3 backup', currentStorageMode);
|
|
124
124
|
}
|
|
125
125
|
return manager.withStoppedMaintenance('restoring a backup', async () => {
|
|
126
|
+
await closeWorkflowRuntime(runtime);
|
|
126
127
|
return executePreparedBackupRequest(
|
|
127
128
|
prepared,
|
|
128
129
|
restoreConfig,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
+
import { readWithTimeout } from '../db/queryTimeout';
|
|
1
2
|
import {
|
|
2
3
|
type WorkflowStateFilter,
|
|
3
4
|
type WorkflowStoreKind,
|
|
4
|
-
workflowExecution,
|
|
5
|
-
workflowExecutions,
|
|
6
|
-
workflowStats,
|
|
7
5
|
WORKFLOW_STATES,
|
|
8
6
|
} from '../workflows';
|
|
9
7
|
import type { RouteResponse } from './types';
|
|
@@ -32,9 +30,9 @@ export async function routeWorkflowReadRequest(
|
|
|
32
30
|
admission?: ManagedDatabaseAdmission
|
|
33
31
|
): Promise<RouteResponse | null> {
|
|
34
32
|
if (pathname === '/workflows/stats' && method === 'GET') {
|
|
35
|
-
return withDatabase(dataPath, admission, (path) => ({
|
|
33
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
36
34
|
status: 200,
|
|
37
|
-
body: { ok: true, ...workflowStats
|
|
35
|
+
body: { ok: true, ...(await readWithTimeout('workflowStats', [path], request.signal)) },
|
|
38
36
|
}));
|
|
39
37
|
}
|
|
40
38
|
if (pathname === '/workflows' && method === 'GET') {
|
|
@@ -52,17 +50,17 @@ export async function routeWorkflowReadRequest(
|
|
|
52
50
|
) {
|
|
53
51
|
throw new Error('Unknown workflow execution state');
|
|
54
52
|
}
|
|
55
|
-
return withDatabase(dataPath, admission, (path) => ({
|
|
53
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
56
54
|
status: 200,
|
|
57
55
|
body: {
|
|
58
56
|
ok: true,
|
|
59
|
-
...workflowExecutions
|
|
57
|
+
...(await readWithTimeout('workflowExecutions', [path, {
|
|
60
58
|
kind: workflowKind(query),
|
|
61
59
|
workflowName: query.get('workflowName') || undefined,
|
|
62
60
|
state: state as WorkflowStateFilter | undefined,
|
|
63
61
|
limit: integer(query, 'limit', 50),
|
|
64
62
|
offset: integer(query, 'offset', 0),
|
|
65
|
-
}),
|
|
63
|
+
}], request.signal)),
|
|
66
64
|
},
|
|
67
65
|
}));
|
|
68
66
|
}
|
|
@@ -78,8 +76,8 @@ export async function routeWorkflowReadRequest(
|
|
|
78
76
|
throw new Error(`Duplicate workflow detail option: ${key}`);
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
|
-
return withDatabase(dataPath, admission, (path) => {
|
|
82
|
-
const execution = workflowExecution
|
|
79
|
+
return withDatabase(dataPath, admission, async (path) => {
|
|
80
|
+
const execution = await readWithTimeout('workflowExecution', [path, decodeURIComponent(rawId), workflowKind(query)], request.signal);
|
|
83
81
|
return execution
|
|
84
82
|
? { status: 200, body: { ok: true, execution } }
|
|
85
83
|
: { status: 404, body: { ok: false, error: 'Workflow execution not found' } };
|
|
@@ -89,7 +87,7 @@ export async function routeWorkflowReadRequest(
|
|
|
89
87
|
function withDatabase<T>(
|
|
90
88
|
dataPath: string,
|
|
91
89
|
admission: ManagedDatabaseAdmission | undefined,
|
|
92
|
-
operation: (path: string) => T
|
|
90
|
+
operation: (path: string) => T | Promise<T>
|
|
93
91
|
): Promise<T> {
|
|
94
92
|
return admission
|
|
95
93
|
? admission((snapshot) => operation(snapshot.dataPath))
|
package/agent/shutdown.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { logger } from './logger';
|
|
|
2
2
|
import { safeErrorMessage } from './errorMessage';
|
|
3
3
|
import type { AgentFetchHandler } from './server';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Upper bound for a graceful agent drain; supervisors must wait at least this long. */
|
|
6
|
+
export const AGENT_SHUTDOWN_GRACE_MS = 30_000;
|
|
6
7
|
|
|
7
8
|
export interface AgentShutdownOptions {
|
|
8
9
|
/** Stop listeners synchronously; returned promises drain in-flight HTTP requests. */
|
|
@@ -28,7 +29,7 @@ export function createAgentShutdown(
|
|
|
28
29
|
options: AgentShutdownOptions = {}
|
|
29
30
|
): (signal: string) => void {
|
|
30
31
|
const exit = options.exit ?? ((code: number) => process.exit(code));
|
|
31
|
-
const graceMs = options.graceMs ??
|
|
32
|
+
const graceMs = options.graceMs ?? AGENT_SHUTDOWN_GRACE_MS;
|
|
32
33
|
let shuttingDown = false;
|
|
33
34
|
let finished = false;
|
|
34
35
|
let graceTimer: ReturnType<typeof setTimeout> | null = null;
|
package/agent/workflow/sqlite.ts
CHANGED