@donkeylabs/server 2.0.33 → 2.0.34
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/workflows.md +4 -0
- package/package.json +1 -1
- package/src/core/index.ts +1 -0
- package/src/core/subprocess-bootstrap.ts +53 -17
- package/src/core/workflow-executor.ts +7 -1
- package/src/core/workflows.ts +17 -3
- package/src/server.ts +6 -0
package/docs/workflows.md
CHANGED
|
@@ -334,6 +334,10 @@ You can tune subprocess termination and SQLite pragmas used by isolated workflow
|
|
|
334
334
|
```ts
|
|
335
335
|
const server = new AppServer({
|
|
336
336
|
db,
|
|
337
|
+
database: {
|
|
338
|
+
type: "postgres",
|
|
339
|
+
connectionString: process.env.DATABASE_URL!,
|
|
340
|
+
},
|
|
337
341
|
watchdog: {
|
|
338
342
|
enabled: true,
|
|
339
343
|
intervalMs: 5000,
|
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Kysely } from "kysely";
|
|
1
|
+
import { Kysely, PostgresDialect, MysqlDialect } from "kysely";
|
|
2
2
|
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
3
3
|
import Database from "bun:sqlite";
|
|
4
4
|
import {
|
|
@@ -30,13 +30,17 @@ export interface SubprocessPluginMetadata {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface SubprocessBootstrapOptions {
|
|
33
|
-
dbPath
|
|
33
|
+
dbPath?: string;
|
|
34
34
|
coreConfig?: Record<string, any>;
|
|
35
35
|
sqlitePragmas?: {
|
|
36
36
|
busyTimeout?: number;
|
|
37
37
|
synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
|
|
38
38
|
journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
|
|
39
39
|
};
|
|
40
|
+
database?: {
|
|
41
|
+
type: "sqlite" | "postgres" | "mysql";
|
|
42
|
+
connectionString: string;
|
|
43
|
+
};
|
|
40
44
|
pluginMetadata: SubprocessPluginMetadata;
|
|
41
45
|
startServices?: {
|
|
42
46
|
cron?: boolean;
|
|
@@ -57,20 +61,7 @@ export interface SubprocessBootstrapResult {
|
|
|
57
61
|
export async function bootstrapSubprocess(
|
|
58
62
|
options: SubprocessBootstrapOptions
|
|
59
63
|
): Promise<SubprocessBootstrapResult> {
|
|
60
|
-
const
|
|
61
|
-
const pragmas = options.sqlitePragmas;
|
|
62
|
-
const busyTimeout = pragmas?.busyTimeout ?? 5000;
|
|
63
|
-
sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
64
|
-
if (pragmas?.journalMode) {
|
|
65
|
-
sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
|
|
66
|
-
}
|
|
67
|
-
if (pragmas?.synchronous) {
|
|
68
|
-
sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const db = new Kysely<any>({
|
|
72
|
-
dialect: new BunSqliteDialect({ database: sqlite }),
|
|
73
|
-
});
|
|
64
|
+
const db = await createSubprocessDatabase(options);
|
|
74
65
|
|
|
75
66
|
const cache = createCache();
|
|
76
67
|
const events = createEvents();
|
|
@@ -166,12 +157,57 @@ export async function bootstrapSubprocess(
|
|
|
166
157
|
}
|
|
167
158
|
|
|
168
159
|
await db.destroy();
|
|
169
|
-
sqlite.close();
|
|
170
160
|
};
|
|
171
161
|
|
|
172
162
|
return { core, manager, db, workflowAdapter, cleanup };
|
|
173
163
|
}
|
|
174
164
|
|
|
165
|
+
async function createSubprocessDatabase(options: SubprocessBootstrapOptions): Promise<Kysely<any>> {
|
|
166
|
+
const dbConfig = options.database;
|
|
167
|
+
if (!dbConfig || dbConfig.type === "sqlite") {
|
|
168
|
+
const sqlitePath = dbConfig?.connectionString ?? options.dbPath;
|
|
169
|
+
if (!sqlitePath) {
|
|
170
|
+
throw new Error("SQLite dbPath or connectionString is required for subprocess workflows");
|
|
171
|
+
}
|
|
172
|
+
const sqlite = new Database(sqlitePath);
|
|
173
|
+
const pragmas = options.sqlitePragmas;
|
|
174
|
+
const busyTimeout = pragmas?.busyTimeout ?? 5000;
|
|
175
|
+
sqlite.run(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
176
|
+
if (pragmas?.journalMode) {
|
|
177
|
+
sqlite.run(`PRAGMA journal_mode = ${pragmas.journalMode}`);
|
|
178
|
+
}
|
|
179
|
+
if (pragmas?.synchronous) {
|
|
180
|
+
sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return new Kysely<any>({
|
|
184
|
+
dialect: new BunSqliteDialect({ database: sqlite }),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (dbConfig.type === "postgres") {
|
|
189
|
+
// @ts-ignore optional dependency
|
|
190
|
+
const { Pool: PGPool } = await import("pg");
|
|
191
|
+
return new Kysely<any>({
|
|
192
|
+
dialect: new PostgresDialect({
|
|
193
|
+
pool: new PGPool({ connectionString: dbConfig.connectionString }),
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (dbConfig.type === "mysql") {
|
|
199
|
+
// @ts-ignore optional dependency
|
|
200
|
+
const { createPool: createMySQLPool } = await import("mysql2");
|
|
201
|
+
return new Kysely<any>({
|
|
202
|
+
dialect: new MysqlDialect({
|
|
203
|
+
pool: createMySQLPool(dbConfig.connectionString),
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
throw new Error(`Unsupported database type: ${dbConfig.type}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
175
211
|
async function loadConfiguredPlugins(
|
|
176
212
|
metadata: SubprocessPluginMetadata
|
|
177
213
|
): Promise<ConfiguredPlugin[]> {
|
|
@@ -20,7 +20,7 @@ interface ExecutorConfig {
|
|
|
20
20
|
socketPath?: string;
|
|
21
21
|
tcpPort?: number;
|
|
22
22
|
modulePath: string;
|
|
23
|
-
dbPath
|
|
23
|
+
dbPath?: string;
|
|
24
24
|
pluginNames: string[];
|
|
25
25
|
pluginModulePaths: Record<string, string>;
|
|
26
26
|
pluginConfigs: Record<string, any>;
|
|
@@ -30,6 +30,10 @@ interface ExecutorConfig {
|
|
|
30
30
|
synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
|
|
31
31
|
journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
|
|
32
32
|
};
|
|
33
|
+
database?: {
|
|
34
|
+
type: "sqlite" | "postgres" | "mysql";
|
|
35
|
+
connectionString: string;
|
|
36
|
+
};
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
// ============================================
|
|
@@ -53,6 +57,7 @@ async function main(): Promise<void> {
|
|
|
53
57
|
pluginConfigs,
|
|
54
58
|
coreConfig,
|
|
55
59
|
sqlitePragmas,
|
|
60
|
+
database,
|
|
56
61
|
} = config;
|
|
57
62
|
|
|
58
63
|
const socket = await connectToSocket(socketPath, tcpPort);
|
|
@@ -76,6 +81,7 @@ async function main(): Promise<void> {
|
|
|
76
81
|
|
|
77
82
|
const bootstrap = await bootstrapSubprocess({
|
|
78
83
|
dbPath,
|
|
84
|
+
database,
|
|
79
85
|
coreConfig,
|
|
80
86
|
sqlitePragmas,
|
|
81
87
|
pluginMetadata: {
|
package/src/core/workflows.ts
CHANGED
|
@@ -756,6 +756,8 @@ export interface WorkflowsConfig {
|
|
|
756
756
|
tcpPortRange?: [number, number];
|
|
757
757
|
/** Database file path (required for isolated workflows) */
|
|
758
758
|
dbPath?: string;
|
|
759
|
+
/** Database config for isolated workflow subprocesses */
|
|
760
|
+
database?: WorkflowDatabaseConfig;
|
|
759
761
|
/** Heartbeat timeout in ms (default: 60000) */
|
|
760
762
|
heartbeatTimeout?: number;
|
|
761
763
|
/** Timeout waiting for isolated subprocess readiness (ms, default: 10000) */
|
|
@@ -780,6 +782,11 @@ export interface SqlitePragmaConfig {
|
|
|
780
782
|
journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
|
|
781
783
|
}
|
|
782
784
|
|
|
785
|
+
export interface WorkflowDatabaseConfig {
|
|
786
|
+
type: "sqlite" | "postgres" | "mysql";
|
|
787
|
+
connectionString: string;
|
|
788
|
+
}
|
|
789
|
+
|
|
783
790
|
/** Options for registering a workflow */
|
|
784
791
|
export interface WorkflowRegisterOptions {
|
|
785
792
|
/**
|
|
@@ -870,6 +877,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
870
877
|
private socketDir: string;
|
|
871
878
|
private tcpPortRange: [number, number];
|
|
872
879
|
private dbPath?: string;
|
|
880
|
+
private databaseConfig?: WorkflowDatabaseConfig;
|
|
873
881
|
private heartbeatTimeoutMs: number;
|
|
874
882
|
private readyTimeoutMs: number;
|
|
875
883
|
private killGraceMs: number;
|
|
@@ -909,6 +917,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
909
917
|
this.socketDir = config.socketDir ?? "/tmp/donkeylabs-workflows";
|
|
910
918
|
this.tcpPortRange = config.tcpPortRange ?? [49152, 65535];
|
|
911
919
|
this.dbPath = config.dbPath;
|
|
920
|
+
this.databaseConfig = config.database;
|
|
912
921
|
this.heartbeatTimeoutMs = config.heartbeatTimeout ?? 60000;
|
|
913
922
|
this.readyTimeoutMs = config.readyTimeout ?? 10000;
|
|
914
923
|
this.killGraceMs = config.killGraceMs ?? 5000;
|
|
@@ -948,6 +957,9 @@ class WorkflowsImpl implements Workflows {
|
|
|
948
957
|
}
|
|
949
958
|
|
|
950
959
|
async resolveDbPath(): Promise<void> {
|
|
960
|
+
if (this.databaseConfig && this.databaseConfig.type !== "sqlite") {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
951
963
|
if (this.dbPath) return;
|
|
952
964
|
if (!this.core?.db) return;
|
|
953
965
|
|
|
@@ -1060,8 +1072,9 @@ class WorkflowsImpl implements Workflows {
|
|
|
1060
1072
|
// Start execution (isolated or inline based on definition.isolated)
|
|
1061
1073
|
const isIsolated = definition.isolated !== false;
|
|
1062
1074
|
const modulePath = this.workflowModulePaths.get(workflowName);
|
|
1075
|
+
const canIsolate = Boolean(this.dbPath || this.databaseConfig);
|
|
1063
1076
|
|
|
1064
|
-
if (isIsolated && modulePath &&
|
|
1077
|
+
if (isIsolated && modulePath && canIsolate) {
|
|
1065
1078
|
// Execute in isolated subprocess
|
|
1066
1079
|
await this.executeIsolatedWorkflow(instance.id, definition, input, modulePath);
|
|
1067
1080
|
} else {
|
|
@@ -1070,10 +1083,10 @@ class WorkflowsImpl implements Workflows {
|
|
|
1070
1083
|
console.warn(
|
|
1071
1084
|
`[Workflows] Workflow "${workflowName}" falling back to inline execution (no modulePath)`
|
|
1072
1085
|
);
|
|
1073
|
-
} else if (isIsolated && modulePath && !
|
|
1086
|
+
} else if (isIsolated && modulePath && !canIsolate) {
|
|
1074
1087
|
console.warn(
|
|
1075
1088
|
`[Workflows] Workflow "${workflowName}" falling back to inline execution (dbPath could not be auto-detected). ` +
|
|
1076
|
-
`Set workflows.dbPath in your server config to enable isolated execution.`
|
|
1089
|
+
`Set workflows.dbPath or workflows.database in your server config to enable isolated execution.`
|
|
1077
1090
|
);
|
|
1078
1091
|
}
|
|
1079
1092
|
this.startInlineWorkflow(instance.id, definition);
|
|
@@ -1513,6 +1526,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
1513
1526
|
pluginConfigs,
|
|
1514
1527
|
coreConfig,
|
|
1515
1528
|
sqlitePragmas: this.sqlitePragmas,
|
|
1529
|
+
database: this.databaseConfig,
|
|
1516
1530
|
};
|
|
1517
1531
|
|
|
1518
1532
|
// Spawn the subprocess
|
package/src/server.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface ServerConfig {
|
|
|
69
69
|
/** Maximum port attempts if port is in use. Default: 5 */
|
|
70
70
|
maxPortAttempts?: number;
|
|
71
71
|
db: CoreServices["db"];
|
|
72
|
+
/** Serializable database config for subprocesses */
|
|
73
|
+
database?: {
|
|
74
|
+
type: "sqlite" | "postgres" | "mysql";
|
|
75
|
+
connectionString: string;
|
|
76
|
+
};
|
|
72
77
|
config?: Record<string, any>;
|
|
73
78
|
/** Auto-generate client types on startup in dev mode */
|
|
74
79
|
generateTypes?: TypeGenerationConfig;
|
|
@@ -314,6 +319,7 @@ export class AppServer {
|
|
|
314
319
|
jobs,
|
|
315
320
|
sse,
|
|
316
321
|
adapter: workflowAdapter,
|
|
322
|
+
database: options.database,
|
|
317
323
|
useWatchdog: options.watchdog?.enabled ? true : options.workflows?.useWatchdog,
|
|
318
324
|
});
|
|
319
325
|
|