bunqueue-dashboard 0.0.39 → 0.0.40
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 +5 -5
- package/agent/backup/resultValidation.ts +1 -1
- package/agent/index.ts +3 -0
- package/agent/manager/config.ts +1 -1
- package/agent/manager/process.ts +2 -2
- package/agent/manager/storageMode.ts +20 -0
- package/agent/manager.ts +1 -1
- package/agent/queue/routes.ts +57 -2
- package/agent/queue/runtime.ts +62 -1
- package/agent/queue/types.ts +31 -0
- package/agent/queue/validation.ts +16 -0
- package/agent/server/controlRoutes.ts +2 -93
- package/agent/server/controlStatus.ts +115 -0
- package/agent/workflows.ts +1 -1
- package/dist/.vite/manifest.json +120 -120
- package/dist/assets/{AddJob-d5KBvmN8.js → AddJob-DkbI_J5p.js} +1 -1
- package/dist/assets/{Alerts-5I44QTnQ.js → Alerts-CFj-u0iV.js} +1 -1
- package/dist/assets/{Benchmark-S1afsQRd.js → Benchmark-Ey2sq57q.js} +1 -1
- package/dist/assets/{BulkAddJobs-BO8LWUHE.js → BulkAddJobs-BwPoeSB6.js} +1 -1
- package/dist/assets/{CopilotPanel-DDkRd35p.js → CopilotPanel-CniuIIPn.js} +1 -1
- package/dist/assets/{Cron-X3Y5uISO.js → Cron-B5XFBvU-.js} +1 -1
- package/dist/assets/{CronManager-ugmYEAwY.js → CronManager-Bdg2BECM.js} +1 -1
- package/dist/assets/{Database-koy8haPX.js → Database-CB0oOEFB.js} +1 -1
- package/dist/assets/{Diagnostics-D6oqvkOs.js → Diagnostics-BDRftXQy.js} +1 -1
- package/dist/assets/{Dlq-D70Lzmsa.js → Dlq-CJO9itzJ.js} +1 -1
- package/dist/assets/{DlqControl-nUT5Zzkl.js → DlqControl-DZL2lHOU.js} +1 -1
- package/dist/assets/{DlqPro-VMuBKgP-.js → DlqPro-OqBlzCnN.js} +1 -1
- package/dist/assets/{Flows-CAG0nDnt.js → Flows-BItJzbVQ.js} +2 -2
- package/dist/assets/{JobInspector-CiS5HOSQ.js → JobInspector-CJHX63p_.js} +2 -2
- package/dist/assets/{Jobs-CkyRvzOT.js → Jobs-BwmTsjJx.js} +1 -1
- package/dist/assets/{JobsPro-B4-CSoq3.js → JobsPro-eFVAZJ10.js} +1 -1
- package/dist/assets/{Logs-DBTz2PTz.js → Logs-CXIf58M4.js} +1 -1
- package/dist/assets/{LogsPro-BCFeegvr.js → LogsPro-Xm6rylAd.js} +1 -1
- package/dist/assets/{McpServer-BKlTR1qr.js → McpServer-Rf3i6lRn.js} +2 -2
- package/dist/assets/{Metrics-Du__9Lbn.js → Metrics-Cj3VYxzL.js} +1 -1
- package/dist/assets/{MetricsPro-CqnaO6YX.js → MetricsPro-C63eY00A.js} +1 -1
- package/dist/assets/{Overview-udSSt7ZB.js → Overview-CUM3XG7A.js} +1 -1
- package/dist/assets/{QueueActions-D7cQrGvJ.js → QueueActions-b19K0vCT.js} +1 -1
- package/dist/assets/QueueControl-DusMoa05.js +1 -0
- package/dist/assets/{QueueDetail-C-ms6bMe.js → QueueDetail-CpFIG1PU.js} +1 -1
- package/dist/assets/{QueueDetailPro-DXEdsZx_.js → QueueDetailPro-BNlBOj5s.js} +1 -1
- package/dist/assets/{Queues-BUMwTVAf.js → Queues-CZ2qc7FY.js} +1 -1
- package/dist/assets/{QueuesOverview-Du8XLgN0.js → QueuesOverview-CBFqQ3N_.js} +1 -1
- package/dist/assets/{S3Backup-CDICSzxM.js → S3Backup-x4mOV-AO.js} +1 -1
- package/dist/assets/{S3BackupPro-CBidbI-s.js → S3BackupPro-BtvbPVg-.js} +3 -3
- package/dist/assets/{ServerControl-CPT0LpsC.js → ServerControl-C5nQ7CCp.js} +2 -2
- package/dist/assets/{Settings-CmxfSX_8.js → Settings-B5kCRSBH.js} +1 -1
- package/dist/assets/{Usage-B895tSxi.js → Usage-UYovs3D-.js} +1 -1
- package/dist/assets/{UsagePro-CASN77iV.js → UsagePro-bT82DS-d.js} +1 -1
- package/dist/assets/{Webhooks-CXbZOFFw.js → Webhooks-BjsUMJAn.js} +1 -1
- package/dist/assets/{Workers-BceQlUhX.js → Workers-CD4YZpca.js} +1 -1
- package/dist/assets/{WorkersPro-Dtml89jB.js → WorkersPro-hCPhfb1Z.js} +1 -1
- package/dist/assets/{Workflows-B8wmQN2X.js → Workflows-DOpeo-TP.js} +1 -1
- package/dist/assets/{api-81m1TwB7.js → api-BxYPEynh.js} +1 -1
- package/dist/assets/bq-C7wdABtE.js +1 -0
- package/dist/assets/{flowMutationSafety-6cP-l4Pa.js → flowMutationSafety-DSd3JLAV.js} +1 -1
- package/dist/assets/{index-TploGF7u.js → index-AUHH2nTG.js} +3 -3
- package/dist/assets/{index-B-QjN7pA.css → index-BK7fjWKK.css} +1 -1
- package/dist/assets/{install-BPp8nydm.js → install-C5y2ohOo.js} +2 -2
- package/dist/assets/{responses-C9lP49qu.js → responses-DiIfPXXq.js} +1 -1
- package/dist/assets/{upstreamPaths-ZeijhPPb.js → upstreamPaths-B_cyinGI.js} +1 -1
- package/dist/assets/{useThroughputSeries-_J7gWbcI.js → useThroughputSeries-y7Yyi7Zh.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +10 -7
- package/scripts/bunVersion.ts +10 -0
- package/scripts/embedded.gen.ts +148 -148
- package/scripts/serve.ts +3 -0
- package/dist/assets/QueueControl-BF638r_0.js +0 -1
- package/dist/assets/bq-BOfwNIlJ.js +0 -1
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ server *process* through a separate guarded agent.
|
|
|
96
96
|
|
|
97
97
|
It uses Bunqueue's public HTTP API (`:6790`) for ordinary remote observability and a small local
|
|
98
98
|
**control agent** for process, FlowProducer, Workflow Engine, database, and backup operations. The
|
|
99
|
-
agent is pinned to the server it manages and uses the exact installable Bunqueue 2.9.
|
|
99
|
+
agent is pinned to the server it manages and uses the exact installable Bunqueue 2.9.2 npm client
|
|
100
100
|
contracts; it never patches Bunqueue internals.
|
|
101
101
|
|
|
102
102
|
## Features
|
|
@@ -117,7 +117,7 @@ contracts; it never patches Bunqueue internals.
|
|
|
117
117
|
| **S3 backup** | Management ▸ S3 Backup | Configure, inspect, list, create and guarded-restore official snapshots |
|
|
118
118
|
| **Browse** | Queues / Jobs / DLQ / Cron / Metrics / Workers / Logs | Read-only browsing with basic actions |
|
|
119
119
|
|
|
120
|
-
> Job actions are gated by the v2.9.
|
|
120
|
+
> Job actions are gated by the v2.9.2 flow contract. Every DLQ retry and completed-job requeue is
|
|
121
121
|
> unavailable: the DLQ GET + POST sequence has no atomic generation/state/topology precondition and
|
|
122
122
|
> can target a job recreated under the same ID, while `retryCompleted` does not rebuild dependency
|
|
123
123
|
> registration or flow order. Cancel, Discard, Drain, Clean, Obliterate and DLQ Purge also fail
|
|
@@ -128,7 +128,7 @@ contracts; it never patches Bunqueue internals.
|
|
|
128
128
|
|
|
129
129
|
## Quick start
|
|
130
130
|
|
|
131
|
-
**Prerequisites:** [Bun](https://bun.sh)
|
|
131
|
+
**Prerequisites:** [Bun](https://bun.sh) 1.4.0 and a reachable bunqueue server (or let the control
|
|
132
132
|
agent start one for you from the **Server** page).
|
|
133
133
|
|
|
134
134
|
### Run from npm (no clone)
|
|
@@ -147,7 +147,7 @@ standalone binaries: `PORT` · `BIND_ADDR` · `BUNQUEUE_URL` · `AGENT_PORT` ·
|
|
|
147
147
|
Install it permanently instead of running via `bunx`:
|
|
148
148
|
|
|
149
149
|
```bash
|
|
150
|
-
bun add -g bunqueue-dashboard
|
|
150
|
+
bun add -g bunqueue-dashboard
|
|
151
151
|
bunqueue-dashboard
|
|
152
152
|
```
|
|
153
153
|
|
|
@@ -299,7 +299,7 @@ The canonical gate must be green before a change is considered done; CI runs the
|
|
|
299
299
|
bun run quality # architecture, lint/build, size, docs, coverage, real E2E, packed-bin smoke, audit
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
The E2E stage starts disposable Bunqueue 2.9.
|
|
302
|
+
The E2E stage starts disposable Bunqueue 2.9.2 processes and exercises the complete FlowProducer,
|
|
303
303
|
Workflow Engine, and Queue SDK operator bridges. Run it alone with `bun run test:e2e`.
|
|
304
304
|
|
|
305
305
|
The blocking browser job builds the production bundle under `/e2e/dashboard`, starts an
|
package/agent/index.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* BUNQUEUE_START_CMD, BUNQUEUE_MANAGED, BUNQUEUE_URL, BUNQUEUE_TOKEN,
|
|
13
13
|
* HTTP_PORT, TCP_PORT, BUNQUEUE_DATA_PATH
|
|
14
14
|
*/
|
|
15
|
+
import { assertRequiredBunVersion } from '../scripts/bunVersion';
|
|
15
16
|
import { logger } from './logger';
|
|
16
17
|
import { ProcessManager } from './manager';
|
|
17
18
|
import {
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
} from './server';
|
|
23
24
|
import { installAgentShutdown } from './shutdown';
|
|
24
25
|
|
|
26
|
+
assertRequiredBunVersion();
|
|
27
|
+
|
|
25
28
|
const mgr = new ProcessManager();
|
|
26
29
|
const PORT = Number(process.env.AGENT_PORT) || 6800;
|
|
27
30
|
const allowedOrigins = resolveAllowedOrigins();
|
package/agent/manager/config.ts
CHANGED
|
@@ -10,7 +10,7 @@ const CONFIG_KEYS = new Set<keyof ServerConfig>([
|
|
|
10
10
|
|
|
11
11
|
export function defaultConfig(): ServerConfig {
|
|
12
12
|
return {
|
|
13
|
-
command: process.env.BUNQUEUE_START_CMD || 'bunx bunqueue@2.9.
|
|
13
|
+
command: process.env.BUNQUEUE_START_CMD || 'bunx bunqueue@2.9.2 start',
|
|
14
14
|
httpPort: Number(process.env.HTTP_PORT) || 6790,
|
|
15
15
|
tcpPort: Number(process.env.TCP_PORT) || 6789,
|
|
16
16
|
dataPath: process.env.BUNQUEUE_DATA_PATH || './data/bunq.db',
|
package/agent/manager/process.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { copyConfig, defaultConfig, validateConfigPatch, validateServerConfig } from './config';
|
|
2
2
|
import { ProcessLogs } from './logs';
|
|
3
3
|
import { databaseStats } from './storage';
|
|
4
|
-
import {
|
|
4
|
+
import { removeSqlitePaths, validateManagedStorage } from './storageMode';
|
|
5
5
|
import type {
|
|
6
6
|
DbStats,
|
|
7
7
|
LogLine,
|
|
@@ -110,7 +110,7 @@ export class ProcessManager {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const launchConfig = validateServerConfig(this.config);
|
|
113
|
-
const storageMode =
|
|
113
|
+
const storageMode = validateManagedStorage(launchConfig);
|
|
114
114
|
const [command, ...args] = launchConfig.command.trim().split(/\s+/);
|
|
115
115
|
this.status = 'starting';
|
|
116
116
|
this.exitCode = null;
|
|
@@ -12,6 +12,26 @@ export function managedStorageMode(config: ServerConfig): ManagedStorageMode {
|
|
|
12
12
|
return config.dataPath ? 'sqlite' : 'memory';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function managedPostgresUrl(config: ServerConfig): string | undefined {
|
|
16
|
+
return (config.extraEnv.BUNQUEUE_POSTGRES_URL ?? process.env.BUNQUEUE_POSTGRES_URL)?.trim() || undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function managedPostgresNamespace(config: ServerConfig): string {
|
|
20
|
+
return (
|
|
21
|
+
(config.extraEnv.BUNQUEUE_POSTGRES_NAMESPACE ?? process.env.BUNQUEUE_POSTGRES_NAMESPACE)?.trim() ||
|
|
22
|
+
'default'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Fail before spawning when Bunqueue would reject an incomplete PostgreSQL config. */
|
|
27
|
+
export function validateManagedStorage(config: ServerConfig): ManagedStorageMode {
|
|
28
|
+
const mode = managedStorageMode(config);
|
|
29
|
+
if (mode === 'postgres' && !managedPostgresUrl(config)) {
|
|
30
|
+
throw new Error('PostgreSQL storage requires BUNQUEUE_POSTGRES_URL');
|
|
31
|
+
}
|
|
32
|
+
return mode;
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
/** Non-SQLite drivers cannot be combined with Bunqueue's legacy SQLite path aliases. */
|
|
16
36
|
export function removeSqlitePaths(env: Record<string, string>): void {
|
|
17
37
|
delete env.BUNQUEUE_DATA_PATH;
|
package/agent/manager.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Public compatibility entrypoint for managed bunqueue process supervision. */
|
|
2
2
|
export { validateConfigPatch } from './manager/config';
|
|
3
3
|
export { ProcessManager } from './manager/process';
|
|
4
|
-
export { managedStorageMode } from './manager/storageMode';
|
|
4
|
+
export { managedPostgresNamespace, managedStorageMode } from './manager/storageMode';
|
|
5
5
|
export type {
|
|
6
6
|
DbStats,
|
|
7
7
|
LogLine,
|
package/agent/queue/routes.ts
CHANGED
|
@@ -13,13 +13,17 @@ import {
|
|
|
13
13
|
exactJsonBody,
|
|
14
14
|
exactQuery,
|
|
15
15
|
metricsRange,
|
|
16
|
+
optionalMaxCount,
|
|
16
17
|
optionalMaxJobs,
|
|
18
|
+
positiveSafeInteger,
|
|
17
19
|
requiredDeduplicationId,
|
|
20
|
+
requiredGroupId,
|
|
18
21
|
validateQueueName,
|
|
19
22
|
} from './validation';
|
|
20
23
|
|
|
21
|
-
const READ_ROUTE = /^\/queue-operations\/([^/]+)\/(limits|deduplication|metrics)$/;
|
|
22
|
-
const MUTATION_ROUTE =
|
|
24
|
+
const READ_ROUTE = /^\/queue-operations\/([^/]+)\/(limits|groups|deduplication|metrics)$/;
|
|
25
|
+
const MUTATION_ROUTE =
|
|
26
|
+
/^\/queue-operations\/([^/]+)\/(groups\/(?:rate-limit|concurrency)(?:\/remove)?|deduplication\/remove|events\/trim)$/;
|
|
23
27
|
|
|
24
28
|
export async function routeQueueOperationsRequest(
|
|
25
29
|
request: Request,
|
|
@@ -50,6 +54,21 @@ export async function routeQueueOperationsRequest(
|
|
|
50
54
|
ok({ jobId: await runtime.deduplicationJobId(current, queue, id) })
|
|
51
55
|
);
|
|
52
56
|
}
|
|
57
|
+
if (read[2] === 'groups') {
|
|
58
|
+
const query = pinnedQuery(
|
|
59
|
+
request,
|
|
60
|
+
config,
|
|
61
|
+
['target', 'groupId', 'maxJobs', 'maxCount'],
|
|
62
|
+
targetPolicy
|
|
63
|
+
);
|
|
64
|
+
const groupId = requiredGroupId(query.get('groupId'));
|
|
65
|
+
const maxJobs = optionalMaxJobs(query);
|
|
66
|
+
const maxCount = optionalMaxCount(query);
|
|
67
|
+
assertRunning(running);
|
|
68
|
+
return admitted(query, config, running, admission, targetPolicy, async ({ config: current }) =>
|
|
69
|
+
ok({ group: await runtime.group(current, queue, groupId, maxJobs, maxCount) })
|
|
70
|
+
);
|
|
71
|
+
}
|
|
53
72
|
const query = pinnedQuery(request, config, ['target', 'type', 'start', 'end'], targetPolicy);
|
|
54
73
|
const type = metricType(query.get('type'));
|
|
55
74
|
const range = metricsRange(query);
|
|
@@ -73,6 +92,42 @@ export async function routeQueueOperationsRequest(
|
|
|
73
92
|
ok({ removed: await runtime.removeDeduplicationKey(current, queue, id) })
|
|
74
93
|
);
|
|
75
94
|
}
|
|
95
|
+
if (mutation[2].startsWith('groups/')) {
|
|
96
|
+
const remove = mutation[2].endsWith('/remove');
|
|
97
|
+
const rateLimit = mutation[2].includes('/rate-limit');
|
|
98
|
+
const allowed = remove
|
|
99
|
+
? ['groupId']
|
|
100
|
+
: rateLimit
|
|
101
|
+
? ['groupId', 'max', 'duration']
|
|
102
|
+
: ['groupId', 'concurrency'];
|
|
103
|
+
const body = await exactJsonBody(request, allowed);
|
|
104
|
+
const groupId = requiredGroupId(body.groupId);
|
|
105
|
+
return admitted(query, config, running, admission, targetPolicy, async ({ config: current }) => {
|
|
106
|
+
if (remove) {
|
|
107
|
+
const removed = rateLimit
|
|
108
|
+
? await runtime.removeGroupRateLimit(current, queue, groupId)
|
|
109
|
+
: await runtime.removeGroupConcurrency(current, queue, groupId);
|
|
110
|
+
return ok({ removed });
|
|
111
|
+
}
|
|
112
|
+
if (rateLimit) {
|
|
113
|
+
await runtime.setGroupRateLimit(
|
|
114
|
+
current,
|
|
115
|
+
queue,
|
|
116
|
+
groupId,
|
|
117
|
+
positiveSafeInteger(body.max, 'max'),
|
|
118
|
+
positiveSafeInteger(body.duration, 'duration')
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
await runtime.setGroupConcurrency(
|
|
122
|
+
current,
|
|
123
|
+
queue,
|
|
124
|
+
groupId,
|
|
125
|
+
positiveSafeInteger(body.concurrency, 'concurrency')
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return ok({ applied: true });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
76
131
|
const body = await exactJsonBody(request, ['maxLength']);
|
|
77
132
|
const retention = eventRetention(body.maxLength);
|
|
78
133
|
return admitted(query, config, running, admission, targetPolicy, async ({ config: current }) =>
|
package/agent/queue/runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Queue, type QueueMetricType, type QueueMetrics } from 'bunqueue/client';
|
|
2
2
|
import { managedAuthToken } from '../managedTarget';
|
|
3
3
|
import type { ServerConfig } from '../manager';
|
|
4
|
-
import type { QueueLimitSnapshot, QueueOperationsPort } from './types';
|
|
4
|
+
import type { QueueGroupSnapshot, QueueLimitSnapshot, QueueOperationsPort } from './types';
|
|
5
5
|
import { QueueOperationsUnavailableError } from './types';
|
|
6
6
|
|
|
7
7
|
export type QueueOperationsClient = Pick<
|
|
@@ -11,6 +11,16 @@ export type QueueOperationsClient = Pick<
|
|
|
11
11
|
| 'getGlobalConcurrency'
|
|
12
12
|
| 'getRateLimitTtl'
|
|
13
13
|
| 'isMaxed'
|
|
14
|
+
| 'getGroupJobsCount'
|
|
15
|
+
| 'getGroupsJobsCount'
|
|
16
|
+
| 'getGroupActiveCount'
|
|
17
|
+
| 'setGroupRateLimit'
|
|
18
|
+
| 'getGroupRateLimit'
|
|
19
|
+
| 'removeGroupRateLimit'
|
|
20
|
+
| 'getGroupRateLimitTtl'
|
|
21
|
+
| 'setGroupConcurrency'
|
|
22
|
+
| 'getGroupConcurrency'
|
|
23
|
+
| 'removeGroupConcurrency'
|
|
14
24
|
| 'getDeduplicationJobId'
|
|
15
25
|
| 'removeDeduplicationKey'
|
|
16
26
|
| 'removeDlqJob'
|
|
@@ -42,6 +52,57 @@ export class QueueOperationsRuntime implements QueueOperationsPort {
|
|
|
42
52
|
});
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
group(
|
|
56
|
+
config: ServerConfig,
|
|
57
|
+
queue: string,
|
|
58
|
+
groupId: string,
|
|
59
|
+
maxJobs?: number,
|
|
60
|
+
maxCount?: number
|
|
61
|
+
): Promise<QueueGroupSnapshot> {
|
|
62
|
+
return this.withQueue(config, queue, async (client) => {
|
|
63
|
+
const [jobs, active, totalGrouped, rateLimit, rateLimitTtl, concurrency] = await Promise.all([
|
|
64
|
+
client.getGroupJobsCount(groupId),
|
|
65
|
+
client.getGroupActiveCount(groupId),
|
|
66
|
+
client.getGroupsJobsCount(maxCount),
|
|
67
|
+
client.getGroupRateLimit(groupId),
|
|
68
|
+
client.getGroupRateLimitTtl(groupId, maxJobs),
|
|
69
|
+
client.getGroupConcurrency(groupId),
|
|
70
|
+
]);
|
|
71
|
+
return { jobs, active, totalGrouped, rateLimit, rateLimitTtl, concurrency };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setGroupRateLimit(
|
|
76
|
+
config: ServerConfig,
|
|
77
|
+
queue: string,
|
|
78
|
+
groupId: string,
|
|
79
|
+
max: number,
|
|
80
|
+
duration: number
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
return this.withQueue(config, queue, (client) =>
|
|
83
|
+
client.setGroupRateLimit(groupId, max, duration)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
removeGroupRateLimit(config: ServerConfig, queue: string, groupId: string): Promise<number> {
|
|
88
|
+
return this.withQueue(config, queue, (client) => client.removeGroupRateLimit(groupId));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setGroupConcurrency(
|
|
92
|
+
config: ServerConfig,
|
|
93
|
+
queue: string,
|
|
94
|
+
groupId: string,
|
|
95
|
+
concurrency: number
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
return this.withQueue(config, queue, (client) =>
|
|
98
|
+
client.setGroupConcurrency(groupId, concurrency)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
removeGroupConcurrency(config: ServerConfig, queue: string, groupId: string): Promise<number> {
|
|
103
|
+
return this.withQueue(config, queue, (client) => client.removeGroupConcurrency(groupId));
|
|
104
|
+
}
|
|
105
|
+
|
|
45
106
|
deduplicationJobId(
|
|
46
107
|
config: ServerConfig,
|
|
47
108
|
queue: string,
|
package/agent/queue/types.ts
CHANGED
|
@@ -8,8 +8,39 @@ export interface QueueLimitSnapshot {
|
|
|
8
8
|
maxed: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export interface QueueGroupSnapshot {
|
|
12
|
+
jobs: number;
|
|
13
|
+
active: number;
|
|
14
|
+
totalGrouped: number;
|
|
15
|
+
rateLimit: { max: number; duration: number } | null;
|
|
16
|
+
rateLimitTtl: number;
|
|
17
|
+
concurrency: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export interface QueueOperationsPort {
|
|
12
21
|
limits(config: ServerConfig, queue: string, maxJobs?: number): Promise<QueueLimitSnapshot>;
|
|
22
|
+
group(
|
|
23
|
+
config: ServerConfig,
|
|
24
|
+
queue: string,
|
|
25
|
+
groupId: string,
|
|
26
|
+
maxJobs?: number,
|
|
27
|
+
maxCount?: number
|
|
28
|
+
): Promise<QueueGroupSnapshot>;
|
|
29
|
+
setGroupRateLimit(
|
|
30
|
+
config: ServerConfig,
|
|
31
|
+
queue: string,
|
|
32
|
+
groupId: string,
|
|
33
|
+
max: number,
|
|
34
|
+
duration: number
|
|
35
|
+
): Promise<void>;
|
|
36
|
+
removeGroupRateLimit(config: ServerConfig, queue: string, groupId: string): Promise<number>;
|
|
37
|
+
setGroupConcurrency(
|
|
38
|
+
config: ServerConfig,
|
|
39
|
+
queue: string,
|
|
40
|
+
groupId: string,
|
|
41
|
+
concurrency: number
|
|
42
|
+
): Promise<void>;
|
|
43
|
+
removeGroupConcurrency(config: ServerConfig, queue: string, groupId: string): Promise<number>;
|
|
13
44
|
deduplicationJobId(
|
|
14
45
|
config: ServerConfig,
|
|
15
46
|
queue: string,
|
|
@@ -40,6 +40,22 @@ export function optionalMaxJobs(query: URLSearchParams): number | undefined {
|
|
|
40
40
|
return raw === null ? undefined : boundedInteger(raw, 'maxJobs', 0, 1_000_000);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export function optionalMaxCount(query: URLSearchParams): number | undefined {
|
|
44
|
+
const raw = query.get('maxCount');
|
|
45
|
+
return raw === null ? undefined : boundedInteger(raw, 'maxCount', 1, 1_000_000);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function requiredGroupId(value: unknown): string {
|
|
49
|
+
if (typeof value !== 'string' || !value || value.length > 256 || value.includes('\0')) {
|
|
50
|
+
throw new Error('groupId must be a non-empty string of at most 256 characters without NUL');
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function positiveSafeInteger(value: unknown, label: string): number {
|
|
56
|
+
return bodyInteger(value, label, 1, Number.MAX_SAFE_INTEGER);
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
export function metricsRange(query: URLSearchParams): { start: number; end: number } {
|
|
44
60
|
const start = optionalInteger(query.get('start'), 'start', 0, MAX_PAGE_INDEX, 0);
|
|
45
61
|
const rawEnd = query.get('end');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ProcessManager, validateConfigPatch } from '../manager';
|
|
2
2
|
import { safeErrorMessage } from '../errorMessage';
|
|
3
3
|
import {
|
|
4
4
|
WorkflowRuntimeUnavailableError,
|
|
@@ -7,93 +7,14 @@ import {
|
|
|
7
7
|
import { readLimitedJsonBody } from './jsonBody';
|
|
8
8
|
import {
|
|
9
9
|
MANAGED_CONTROL_TARGET,
|
|
10
|
-
probeExternalHealth,
|
|
11
10
|
type ServerControlTarget,
|
|
12
11
|
} from './controlTarget';
|
|
12
|
+
import { statusWithHealth } from './controlStatus';
|
|
13
13
|
import type { AgentLifecyclePort } from './lifecycle';
|
|
14
14
|
import type { RouteResponse } from './types';
|
|
15
15
|
|
|
16
16
|
const MAX_CONFIG_BODY_BYTES = 64 * 1024;
|
|
17
17
|
|
|
18
|
-
async function statusWithHealth(
|
|
19
|
-
manager: ProcessManager,
|
|
20
|
-
controlTarget: ServerControlTarget,
|
|
21
|
-
retryOnProcessChange = true
|
|
22
|
-
) {
|
|
23
|
-
const snapshot = manager.getStatus();
|
|
24
|
-
const configRevision = snapshot.configRevision ?? manager.getConfigRevision?.() ?? 0;
|
|
25
|
-
if (controlTarget.mode === 'external') {
|
|
26
|
-
const health = await probeExternalHealth(controlTarget);
|
|
27
|
-
return {
|
|
28
|
-
...snapshot,
|
|
29
|
-
configRevision,
|
|
30
|
-
managementMode: 'external' as const,
|
|
31
|
-
healthy: health.healthy,
|
|
32
|
-
version: health.version,
|
|
33
|
-
reachable: health.reachable,
|
|
34
|
-
externalUrl: controlTarget.url,
|
|
35
|
-
healthStatus: health.statusCode,
|
|
36
|
-
healthError: health.error,
|
|
37
|
-
db: null,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
let healthy = false;
|
|
41
|
-
let version: string | undefined;
|
|
42
|
-
if (snapshot.status === 'running') {
|
|
43
|
-
const port = snapshot.runningConfig?.httpPort ?? snapshot.config.httpPort;
|
|
44
|
-
try {
|
|
45
|
-
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
46
|
-
signal: AbortSignal.timeout(1500),
|
|
47
|
-
});
|
|
48
|
-
if (response.ok) {
|
|
49
|
-
const body: unknown = await response.json();
|
|
50
|
-
if (body !== null && typeof body === 'object' && !Array.isArray(body)) {
|
|
51
|
-
const health = body as Record<string, unknown>;
|
|
52
|
-
healthy = health.ok === true;
|
|
53
|
-
if (typeof health.version === 'string') version = health.version;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
} catch {
|
|
57
|
-
// The managed process is not healthy yet.
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const effectiveConfig = snapshot.runningConfig ?? snapshot.config;
|
|
61
|
-
const database = await statusDatabase(manager, effectiveConfig);
|
|
62
|
-
const current = manager.getStatus();
|
|
63
|
-
if (!sameProcessSnapshot(snapshot, current)) {
|
|
64
|
-
if (retryOnProcessChange) return statusWithHealth(manager, controlTarget, false);
|
|
65
|
-
const currentConfig = current.runningConfig ?? current.config;
|
|
66
|
-
return {
|
|
67
|
-
...current,
|
|
68
|
-
configRevision: current.configRevision ?? manager.getConfigRevision?.() ?? 0,
|
|
69
|
-
managementMode: 'managed' as const,
|
|
70
|
-
healthy: false,
|
|
71
|
-
version: undefined,
|
|
72
|
-
db: await statusDatabase(manager, currentConfig),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
...snapshot,
|
|
77
|
-
configRevision,
|
|
78
|
-
managementMode: 'managed' as const,
|
|
79
|
-
healthy,
|
|
80
|
-
version,
|
|
81
|
-
db: database,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function statusDatabase(
|
|
86
|
-
manager: ProcessManager,
|
|
87
|
-
config: ReturnType<ProcessManager['getConfig']>
|
|
88
|
-
) {
|
|
89
|
-
try {
|
|
90
|
-
return managedStorageMode(config) === 'sqlite' ? await manager.dbStats(config.dataPath) : null;
|
|
91
|
-
} catch {
|
|
92
|
-
// Status must remain reachable so an invalid desired storage driver can be corrected.
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
18
|
export async function routeControlRequest(
|
|
98
19
|
request: Request,
|
|
99
20
|
pathname: string,
|
|
@@ -262,18 +183,6 @@ function coordinatedLease<T>(
|
|
|
262
183
|
return lifecycle ? lifecycle.lease(operation) : operation();
|
|
263
184
|
}
|
|
264
185
|
|
|
265
|
-
function sameProcessSnapshot(
|
|
266
|
-
initial: ReturnType<ProcessManager['getStatus']>,
|
|
267
|
-
current: ReturnType<ProcessManager['getStatus']>
|
|
268
|
-
): boolean {
|
|
269
|
-
return (
|
|
270
|
-
initial.status === current.status &&
|
|
271
|
-
initial.generation === current.generation &&
|
|
272
|
-
initial.pid === current.pid &&
|
|
273
|
-
initial.configRevision === current.configRevision
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
186
|
type SettledOperation = { ok: true } | { ok: false; error: unknown };
|
|
278
187
|
|
|
279
188
|
async function settle(operation: () => Promise<unknown>): Promise<SettledOperation> {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
managedPostgresNamespace,
|
|
3
|
+
managedStorageMode,
|
|
4
|
+
type ProcessManager,
|
|
5
|
+
} from '../manager';
|
|
6
|
+
import { probeExternalHealth, type ServerControlTarget } from './controlTarget';
|
|
7
|
+
|
|
8
|
+
export async function statusWithHealth(
|
|
9
|
+
manager: ProcessManager,
|
|
10
|
+
controlTarget: ServerControlTarget,
|
|
11
|
+
retryOnProcessChange = true
|
|
12
|
+
) {
|
|
13
|
+
const snapshot = manager.getStatus();
|
|
14
|
+
const configRevision = snapshot.configRevision ?? manager.getConfigRevision?.() ?? 0;
|
|
15
|
+
if (controlTarget.mode === 'external') {
|
|
16
|
+
const health = await probeExternalHealth(controlTarget);
|
|
17
|
+
return {
|
|
18
|
+
...snapshot,
|
|
19
|
+
configRevision,
|
|
20
|
+
managementMode: 'external' as const,
|
|
21
|
+
healthy: health.healthy,
|
|
22
|
+
version: health.version,
|
|
23
|
+
reachable: health.reachable,
|
|
24
|
+
externalUrl: controlTarget.url,
|
|
25
|
+
healthStatus: health.statusCode,
|
|
26
|
+
healthError: health.error,
|
|
27
|
+
db: null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
let healthy = false;
|
|
31
|
+
let version: string | undefined;
|
|
32
|
+
if (snapshot.status === 'running') {
|
|
33
|
+
const port = snapshot.runningConfig?.httpPort ?? snapshot.config.httpPort;
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
36
|
+
signal: AbortSignal.timeout(1500),
|
|
37
|
+
});
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
const body: unknown = await response.json();
|
|
40
|
+
if (body !== null && typeof body === 'object' && !Array.isArray(body)) {
|
|
41
|
+
const health = body as Record<string, unknown>;
|
|
42
|
+
healthy = health.ok === true;
|
|
43
|
+
if (typeof health.version === 'string') version = health.version;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// The managed process is not healthy yet.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const effectiveConfig = snapshot.runningConfig ?? snapshot.config;
|
|
51
|
+
const storageMode = statusStorageMode(effectiveConfig);
|
|
52
|
+
const database = await statusDatabase(manager, effectiveConfig);
|
|
53
|
+
const current = manager.getStatus();
|
|
54
|
+
if (!sameProcessSnapshot(snapshot, current)) {
|
|
55
|
+
if (retryOnProcessChange) return statusWithHealth(manager, controlTarget, false);
|
|
56
|
+
const currentConfig = current.runningConfig ?? current.config;
|
|
57
|
+
const currentStorageMode = statusStorageMode(currentConfig);
|
|
58
|
+
return {
|
|
59
|
+
...current,
|
|
60
|
+
configRevision: current.configRevision ?? manager.getConfigRevision?.() ?? 0,
|
|
61
|
+
managementMode: 'managed' as const,
|
|
62
|
+
healthy: false,
|
|
63
|
+
version: undefined,
|
|
64
|
+
storageMode: currentStorageMode,
|
|
65
|
+
postgresNamespace:
|
|
66
|
+
currentStorageMode === 'postgres' ? managedPostgresNamespace(currentConfig) : undefined,
|
|
67
|
+
db: await statusDatabase(manager, currentConfig),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...snapshot,
|
|
72
|
+
configRevision,
|
|
73
|
+
managementMode: 'managed' as const,
|
|
74
|
+
healthy,
|
|
75
|
+
version,
|
|
76
|
+
storageMode,
|
|
77
|
+
postgresNamespace:
|
|
78
|
+
storageMode === 'postgres' ? managedPostgresNamespace(effectiveConfig) : undefined,
|
|
79
|
+
db: database,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function statusDatabase(
|
|
84
|
+
manager: ProcessManager,
|
|
85
|
+
config: ReturnType<ProcessManager['getConfig']>
|
|
86
|
+
) {
|
|
87
|
+
try {
|
|
88
|
+
return managedStorageMode(config) === 'sqlite' ? await manager.dbStats(config.dataPath) : null;
|
|
89
|
+
} catch {
|
|
90
|
+
// Status must remain reachable so an invalid desired storage driver can be corrected.
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function statusStorageMode(
|
|
96
|
+
config: ReturnType<ProcessManager['getConfig']>
|
|
97
|
+
): ReturnType<typeof managedStorageMode> | undefined {
|
|
98
|
+
try {
|
|
99
|
+
return managedStorageMode(config);
|
|
100
|
+
} catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sameProcessSnapshot(
|
|
106
|
+
initial: ReturnType<ProcessManager['getStatus']>,
|
|
107
|
+
current: ReturnType<ProcessManager['getStatus']>
|
|
108
|
+
): boolean {
|
|
109
|
+
return (
|
|
110
|
+
initial.status === current.status &&
|
|
111
|
+
initial.generation === current.generation &&
|
|
112
|
+
initial.pid === current.pid &&
|
|
113
|
+
initial.configRevision === current.configRevision
|
|
114
|
+
);
|
|
115
|
+
}
|
package/agent/workflows.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read-only facade for Bunqueue 2.9.
|
|
2
|
+
* Read-only facade for Bunqueue 2.9.2 Workflow Engine observability.
|
|
3
3
|
* Persistence, decoding and validation live behind this stable public module.
|
|
4
4
|
*/
|
|
5
5
|
export { workflowExecution } from './workflow/detail';
|