@donkeylabs/server 2.0.32 → 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 +5 -5
- 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 -10
- package/src/server.ts +6 -0
package/docs/workflows.md
CHANGED
|
@@ -98,14 +98,10 @@ const server = new AppServer({
|
|
|
98
98
|
db,
|
|
99
99
|
workflows: {
|
|
100
100
|
concurrentWorkflows: 1, // default for all workflows (0 = unlimited)
|
|
101
|
-
concurrentWorkflowsByName: {
|
|
102
|
-
testWorkflow: 1,
|
|
103
|
-
ingestionWorkflow: 1,
|
|
104
|
-
},
|
|
105
101
|
},
|
|
106
102
|
});
|
|
107
103
|
|
|
108
|
-
//
|
|
104
|
+
// Per-register override
|
|
109
105
|
ctx.core.workflows.register(orderWorkflow, { maxConcurrent: 1 });
|
|
110
106
|
```
|
|
111
107
|
|
|
@@ -338,6 +334,10 @@ You can tune subprocess termination and SQLite pragmas used by isolated workflow
|
|
|
338
334
|
```ts
|
|
339
335
|
const server = new AppServer({
|
|
340
336
|
db,
|
|
337
|
+
database: {
|
|
338
|
+
type: "postgres",
|
|
339
|
+
connectionString: process.env.DATABASE_URL!,
|
|
340
|
+
},
|
|
341
341
|
watchdog: {
|
|
342
342
|
enabled: true,
|
|
343
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) */
|
|
@@ -768,8 +770,6 @@ export interface WorkflowsConfig {
|
|
|
768
770
|
useWatchdog?: boolean;
|
|
769
771
|
/** Default max concurrent instances per workflow name (0 = unlimited, default: 0) */
|
|
770
772
|
concurrentWorkflows?: number;
|
|
771
|
-
/** Per-workflow concurrency overrides */
|
|
772
|
-
concurrentWorkflowsByName?: Record<string, number>;
|
|
773
773
|
/** Resume strategy for orphaned workflows (default: "blocking") */
|
|
774
774
|
resumeStrategy?: WorkflowResumeStrategy;
|
|
775
775
|
}
|
|
@@ -782,6 +782,11 @@ export interface SqlitePragmaConfig {
|
|
|
782
782
|
journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF";
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
export interface WorkflowDatabaseConfig {
|
|
786
|
+
type: "sqlite" | "postgres" | "mysql";
|
|
787
|
+
connectionString: string;
|
|
788
|
+
}
|
|
789
|
+
|
|
785
790
|
/** Options for registering a workflow */
|
|
786
791
|
export interface WorkflowRegisterOptions {
|
|
787
792
|
/**
|
|
@@ -872,13 +877,13 @@ class WorkflowsImpl implements Workflows {
|
|
|
872
877
|
private socketDir: string;
|
|
873
878
|
private tcpPortRange: [number, number];
|
|
874
879
|
private dbPath?: string;
|
|
880
|
+
private databaseConfig?: WorkflowDatabaseConfig;
|
|
875
881
|
private heartbeatTimeoutMs: number;
|
|
876
882
|
private readyTimeoutMs: number;
|
|
877
883
|
private killGraceMs: number;
|
|
878
884
|
private sqlitePragmas?: SqlitePragmaConfig;
|
|
879
885
|
private useWatchdog: boolean;
|
|
880
886
|
private concurrentWorkflows: number;
|
|
881
|
-
private concurrentWorkflowsByName: Record<string, number>;
|
|
882
887
|
private workflowConcurrencyOverrides = new Map<string, number>();
|
|
883
888
|
private resumeStrategy!: WorkflowResumeStrategy;
|
|
884
889
|
private workflowModulePaths = new Map<string, string>();
|
|
@@ -912,13 +917,13 @@ class WorkflowsImpl implements Workflows {
|
|
|
912
917
|
this.socketDir = config.socketDir ?? "/tmp/donkeylabs-workflows";
|
|
913
918
|
this.tcpPortRange = config.tcpPortRange ?? [49152, 65535];
|
|
914
919
|
this.dbPath = config.dbPath;
|
|
920
|
+
this.databaseConfig = config.database;
|
|
915
921
|
this.heartbeatTimeoutMs = config.heartbeatTimeout ?? 60000;
|
|
916
922
|
this.readyTimeoutMs = config.readyTimeout ?? 10000;
|
|
917
923
|
this.killGraceMs = config.killGraceMs ?? 5000;
|
|
918
924
|
this.sqlitePragmas = config.sqlitePragmas;
|
|
919
925
|
this.useWatchdog = config.useWatchdog ?? false;
|
|
920
926
|
this.concurrentWorkflows = config.concurrentWorkflows ?? 0;
|
|
921
|
-
this.concurrentWorkflowsByName = config.concurrentWorkflowsByName ?? {};
|
|
922
927
|
this.resumeStrategy = config.resumeStrategy ?? "blocking";
|
|
923
928
|
}
|
|
924
929
|
|
|
@@ -952,6 +957,9 @@ class WorkflowsImpl implements Workflows {
|
|
|
952
957
|
}
|
|
953
958
|
|
|
954
959
|
async resolveDbPath(): Promise<void> {
|
|
960
|
+
if (this.databaseConfig && this.databaseConfig.type !== "sqlite") {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
955
963
|
if (this.dbPath) return;
|
|
956
964
|
if (!this.core?.db) return;
|
|
957
965
|
|
|
@@ -1064,8 +1072,9 @@ class WorkflowsImpl implements Workflows {
|
|
|
1064
1072
|
// Start execution (isolated or inline based on definition.isolated)
|
|
1065
1073
|
const isIsolated = definition.isolated !== false;
|
|
1066
1074
|
const modulePath = this.workflowModulePaths.get(workflowName);
|
|
1075
|
+
const canIsolate = Boolean(this.dbPath || this.databaseConfig);
|
|
1067
1076
|
|
|
1068
|
-
if (isIsolated && modulePath &&
|
|
1077
|
+
if (isIsolated && modulePath && canIsolate) {
|
|
1069
1078
|
// Execute in isolated subprocess
|
|
1070
1079
|
await this.executeIsolatedWorkflow(instance.id, definition, input, modulePath);
|
|
1071
1080
|
} else {
|
|
@@ -1074,10 +1083,10 @@ class WorkflowsImpl implements Workflows {
|
|
|
1074
1083
|
console.warn(
|
|
1075
1084
|
`[Workflows] Workflow "${workflowName}" falling back to inline execution (no modulePath)`
|
|
1076
1085
|
);
|
|
1077
|
-
} else if (isIsolated && modulePath && !
|
|
1086
|
+
} else if (isIsolated && modulePath && !canIsolate) {
|
|
1078
1087
|
console.warn(
|
|
1079
1088
|
`[Workflows] Workflow "${workflowName}" falling back to inline execution (dbPath could not be auto-detected). ` +
|
|
1080
|
-
`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.`
|
|
1081
1090
|
);
|
|
1082
1091
|
}
|
|
1083
1092
|
this.startInlineWorkflow(instance.id, definition);
|
|
@@ -1517,6 +1526,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
1517
1526
|
pluginConfigs,
|
|
1518
1527
|
coreConfig,
|
|
1519
1528
|
sqlitePragmas: this.sqlitePragmas,
|
|
1529
|
+
database: this.databaseConfig,
|
|
1520
1530
|
};
|
|
1521
1531
|
|
|
1522
1532
|
// Spawn the subprocess
|
|
@@ -2021,9 +2031,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
2021
2031
|
if (this.workflowConcurrencyOverrides.has(workflowName)) {
|
|
2022
2032
|
return this.workflowConcurrencyOverrides.get(workflowName) ?? 0;
|
|
2023
2033
|
}
|
|
2024
|
-
if (this.concurrentWorkflowsByName[workflowName] !== undefined) {
|
|
2025
|
-
return this.concurrentWorkflowsByName[workflowName] ?? 0;
|
|
2026
|
-
}
|
|
2027
2034
|
return this.concurrentWorkflows;
|
|
2028
2035
|
}
|
|
2029
2036
|
|
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
|
|