@donkeylabs/server 2.0.28 → 2.0.30
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/docs/jobs.md +27 -0
- package/docs/processes.md +29 -0
- package/docs/workflows.md +32 -0
- package/package.json +1 -1
- package/src/core/external-job-socket.ts +20 -1
- package/src/core/external-jobs.ts +6 -0
- package/src/core/index.ts +1 -0
- package/src/core/jobs.ts +110 -52
- package/src/core/process-socket.ts +20 -1
- package/src/core/processes.ts +100 -4
- package/src/core/subprocess-bootstrap.ts +14 -1
- package/src/core/watchdog-executor.ts +80 -0
- package/src/core/watchdog-runner.ts +276 -0
- package/src/core/workflow-executor.ts +7 -0
- package/src/core/workflow-socket.ts +21 -2
- package/src/core/workflows.ts +88 -12
- package/src/server.ts +84 -1
package/src/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { PluginManager, type CoreServices, type ConfiguredPlugin } from "./core";
|
|
5
6
|
import { type IRouter, type RouteDefinition, type ServerContext, type HandlerRegistry } from "./router";
|
|
6
7
|
import { Handlers } from "./handlers";
|
|
@@ -87,6 +88,13 @@ export interface ServerConfig {
|
|
|
87
88
|
*/
|
|
88
89
|
workflowsResumeStrategy?: "blocking" | "background" | "skip";
|
|
89
90
|
processes?: ProcessesConfig;
|
|
91
|
+
/** Watchdog subprocess configuration */
|
|
92
|
+
watchdog?: {
|
|
93
|
+
enabled?: boolean;
|
|
94
|
+
intervalMs?: number;
|
|
95
|
+
services?: ("workflows" | "jobs" | "processes")[];
|
|
96
|
+
killGraceMs?: number;
|
|
97
|
+
};
|
|
90
98
|
audit?: AuditConfig;
|
|
91
99
|
websocket?: WebSocketConfig;
|
|
92
100
|
storage?: StorageConfig;
|
|
@@ -221,6 +229,9 @@ export class AppServer {
|
|
|
221
229
|
private generateModeSetup = false;
|
|
222
230
|
private initMode: "adapter" | "server" = "server";
|
|
223
231
|
private workflowsResumeStrategy?: "blocking" | "background" | "skip";
|
|
232
|
+
private watchdogConfig?: ServerConfig["watchdog"];
|
|
233
|
+
private watchdogStarted = false;
|
|
234
|
+
private options: ServerConfig;
|
|
224
235
|
|
|
225
236
|
// Custom services registry
|
|
226
237
|
private serviceFactories = new Map<string, ServiceFactory<any>>();
|
|
@@ -228,11 +239,13 @@ export class AppServer {
|
|
|
228
239
|
private generateModeTimer?: ReturnType<typeof setTimeout>;
|
|
229
240
|
|
|
230
241
|
constructor(options: ServerConfig) {
|
|
242
|
+
this.options = options;
|
|
231
243
|
// Port priority: explicit config > PORT env var > default 3000
|
|
232
244
|
const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
233
245
|
this.port = options.port ?? envPort ?? 3000;
|
|
234
246
|
this.maxPortAttempts = options.maxPortAttempts ?? 5;
|
|
235
247
|
this.workflowsResumeStrategy = options.workflowsResumeStrategy ?? options.workflows?.resumeStrategy;
|
|
248
|
+
this.watchdogConfig = options.watchdog;
|
|
236
249
|
|
|
237
250
|
// Determine if we should use legacy databases
|
|
238
251
|
const useLegacy = options.useLegacyCoreDatabases ?? false;
|
|
@@ -286,6 +299,10 @@ export class AppServer {
|
|
|
286
299
|
events,
|
|
287
300
|
logger,
|
|
288
301
|
adapter: jobAdapter,
|
|
302
|
+
external: {
|
|
303
|
+
...options.jobs?.external,
|
|
304
|
+
useWatchdog: options.watchdog?.enabled ? true : options.jobs?.external?.useWatchdog,
|
|
305
|
+
},
|
|
289
306
|
// Disable built-in persistence when using Kysely adapter
|
|
290
307
|
persist: useLegacy ? options.jobs?.persist : false,
|
|
291
308
|
});
|
|
@@ -297,12 +314,14 @@ export class AppServer {
|
|
|
297
314
|
jobs,
|
|
298
315
|
sse,
|
|
299
316
|
adapter: workflowAdapter,
|
|
317
|
+
useWatchdog: options.watchdog?.enabled ? true : options.workflows?.useWatchdog,
|
|
300
318
|
});
|
|
301
319
|
|
|
302
320
|
// Processes - still uses its own adapter pattern but can use Kysely
|
|
303
321
|
const processes = createProcesses({
|
|
304
322
|
...options.processes,
|
|
305
323
|
events,
|
|
324
|
+
useWatchdog: options.watchdog?.enabled ? true : options.processes?.useWatchdog,
|
|
306
325
|
});
|
|
307
326
|
|
|
308
327
|
// New services
|
|
@@ -1055,6 +1074,7 @@ ${factoryFunction}
|
|
|
1055
1074
|
await this.coreServices.workflows.resume();
|
|
1056
1075
|
}
|
|
1057
1076
|
this.coreServices.processes.start();
|
|
1077
|
+
await this.startWatchdog();
|
|
1058
1078
|
logger.info("Background services started (cron, jobs, workflows, processes)");
|
|
1059
1079
|
|
|
1060
1080
|
for (const router of this.routers) {
|
|
@@ -1073,6 +1093,69 @@ ${factoryFunction}
|
|
|
1073
1093
|
await this.runReadyHandlers();
|
|
1074
1094
|
}
|
|
1075
1095
|
|
|
1096
|
+
private async startWatchdog(): Promise<void> {
|
|
1097
|
+
if (!this.watchdogConfig?.enabled) return;
|
|
1098
|
+
if (this.watchdogStarted) return;
|
|
1099
|
+
|
|
1100
|
+
const executorPath = join(dirname(fileURLToPath(import.meta.url)), "core", "watchdog-executor.ts");
|
|
1101
|
+
const services = this.watchdogConfig.services ?? ["workflows", "jobs", "processes"];
|
|
1102
|
+
const workflowsDbPath = this.coreServices.workflows.getDbPath?.();
|
|
1103
|
+
const jobsDbPath = (this.options.jobs?.dbPath ?? workflowsDbPath ?? ".donkeylabs/jobs.db") as string;
|
|
1104
|
+
const processesDbPath = (this.options.processes?.adapter?.path ?? ".donkeylabs/processes.db") as string;
|
|
1105
|
+
|
|
1106
|
+
const config = {
|
|
1107
|
+
intervalMs: this.watchdogConfig.intervalMs ?? 5000,
|
|
1108
|
+
services,
|
|
1109
|
+
killGraceMs: this.watchdogConfig.killGraceMs ?? 5000,
|
|
1110
|
+
workflowHeartbeatTimeoutMs: this.options.workflows?.heartbeatTimeout ?? 60000,
|
|
1111
|
+
jobDefaults: {
|
|
1112
|
+
heartbeatTimeoutMs: this.options.jobs?.external?.defaultHeartbeatTimeout ?? 30000,
|
|
1113
|
+
killGraceMs: this.options.jobs?.external?.killGraceMs ?? this.watchdogConfig.killGraceMs ?? 5000,
|
|
1114
|
+
},
|
|
1115
|
+
jobConfigs: this.coreServices.jobs.getExternalJobConfigs(),
|
|
1116
|
+
workflows: workflowsDbPath ? { dbPath: workflowsDbPath } : undefined,
|
|
1117
|
+
jobs: jobsDbPath ? { dbPath: jobsDbPath } : undefined,
|
|
1118
|
+
processes: processesDbPath ? { dbPath: processesDbPath } : undefined,
|
|
1119
|
+
sqlitePragmas: this.options.workflows?.sqlitePragmas,
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
try {
|
|
1123
|
+
this.coreServices.processes.register({
|
|
1124
|
+
name: "__watchdog",
|
|
1125
|
+
config: {
|
|
1126
|
+
command: "bun",
|
|
1127
|
+
args: ["run", executorPath],
|
|
1128
|
+
env: {
|
|
1129
|
+
DONKEYLABS_WATCHDOG_CONFIG: JSON.stringify(config),
|
|
1130
|
+
},
|
|
1131
|
+
heartbeat: { intervalMs: 5000, timeoutMs: 30000 },
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
} catch {
|
|
1135
|
+
// Already registered
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
await this.coreServices.processes.spawn("__watchdog", {
|
|
1139
|
+
metadata: { role: "watchdog" },
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
this.coreServices.events.on("process.event", async (data: any) => {
|
|
1143
|
+
if (data?.name !== "__watchdog") return;
|
|
1144
|
+
if (!data.event) return;
|
|
1145
|
+
|
|
1146
|
+
await this.coreServices.events.emit(data.event, data.data ?? {});
|
|
1147
|
+
|
|
1148
|
+
if (data.event.startsWith("workflow.watchdog")) {
|
|
1149
|
+
const instanceId = data.data?.instanceId;
|
|
1150
|
+
if (instanceId && this.coreServices.sse) {
|
|
1151
|
+
this.coreServices.sse.broadcast(`workflow:${instanceId}`, data.event, data.data ?? {});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
this.watchdogStarted = true;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1076
1159
|
/**
|
|
1077
1160
|
* Handle a single API request. Used by adapters.
|
|
1078
1161
|
* Returns null if the route is not found.
|