mcp-pacemaker 2.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/CHANGELOG.md +452 -0
- package/CONTRIBUTING.md +222 -0
- package/LICENSE +21 -0
- package/README.md +621 -0
- package/SECURITY.md +120 -0
- package/THIRD_PARTY_NOTICES.txt +366 -0
- package/autostart/windows/launcher.vbs +11 -0
- package/autostart/windows/register-task.ps1 +63 -0
- package/bin/cli.mjs +880 -0
- package/bin/config-checks.mjs +62 -0
- package/bin/logs.mjs +465 -0
- package/bin/mcp-bridge.mjs +2162 -0
- package/bin/pooling-batch-scheduler.mjs +129 -0
- package/bin/pooling-batches.mjs +270 -0
- package/bin/pooling-config.mjs +577 -0
- package/bin/pooling-editor.mjs +128 -0
- package/bin/pooling-errors.mjs +23 -0
- package/bin/pooling-execution.mjs +104 -0
- package/bin/pooling-files.mjs +541 -0
- package/bin/pooling-trace.mjs +218 -0
- package/bin/pooling-writer.mjs +242 -0
- package/bin/prewarm.mjs +233 -0
- package/bin/server-metrics.mjs +69 -0
- package/bin/service-control.mjs +124 -0
- package/bin/shared-sessions.mjs +1083 -0
- package/bin/top.mjs +115 -0
- package/bin/update-check.mjs +153 -0
- package/bin/windows/PoolingSecurityHelper.build.json +42 -0
- package/bin/windows/PoolingSecurityHelper.exe +0 -0
- package/bin/windows/src/AssemblyInfo.cs +11 -0
- package/bin/windows/src/PoolingNativeFiles.cs +678 -0
- package/bin/windows/src/PoolingSecurityHelper.cs +178 -0
- package/bin/windows/src/PoolingSecurityReader.cs +50 -0
- package/docs/shared-sessions.md +181 -0
- package/examples/servers.example.json +31 -0
- package/package.json +77 -0
- package/supervisor/bridge-child.mjs +14 -0
- package/supervisor/supervise.mjs +172 -0
- package/supervisor/supervise.ps1 +23 -0
- package/supervisor/supervise.sh +11 -0
- package/tools/windows-security-helper/README.md +238 -0
- package/tools/windows-security-helper/build.ps1 +262 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +366 -0
- package/ui/dist/assets/index-CaLPg0c-.css +1 -0
- package/ui/dist/assets/index-CgPruWYi.js +9 -0
- package/ui/dist/index.html +14 -0
- package/ui/dist/third-party-manifest.json +597 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { threadId } from 'node:worker_threads';
|
|
5
|
+
|
|
6
|
+
export const TRACE_MAX_EVENTS = 192;
|
|
7
|
+
export const TRACE_MAX_BYTES = 65536;
|
|
8
|
+
export const TRACE_MAX_LINE_BYTES = 1024;
|
|
9
|
+
const methods = ['apply', 'undo', 'stageApply', 'stageUndo', 'rejectStage', 'commitBatch'];
|
|
10
|
+
const codes = ['NONE', 'UNKNOWN', 'ETIMEDOUT', 'EACCES', 'EPERM', 'ENOENT', 'EIO',
|
|
11
|
+
'WRITER_DEADLINE', 'WRITER_CANCELLED', 'WRITER_FAILED', 'WRITER_OUTCOME_UNKNOWN',
|
|
12
|
+
'WRITER_DEADLINE_COMMITTED', 'WRITER_BUSY', 'WRITER_CLOSED', 'REVISION_CONFLICT',
|
|
13
|
+
'IO_ERROR', 'ACCESS_DENIED', 'FILE_BUSY', 'RECOVERY_REQUIRED', 'STORE_BUSY'];
|
|
14
|
+
const integer = (value) => Number.isSafeInteger(value) && value >= 0 && value <= 0xffffffff;
|
|
15
|
+
const state = (value) => Number.isInteger(value) && value >= 0 && value <= 6;
|
|
16
|
+
const duration = (value) => Number.isFinite(value) && Math.abs(value) <= 120000;
|
|
17
|
+
const boolean = (value) => typeof value === 'boolean';
|
|
18
|
+
const method = (value) => methods.includes(value);
|
|
19
|
+
const code = (value) => codes.includes(value);
|
|
20
|
+
const action = (value) => ['inspect-access', 'stage', 'move-no-replace'].includes(value);
|
|
21
|
+
const fields = {
|
|
22
|
+
'request-start': {},
|
|
23
|
+
'body-end': { bytes: integer },
|
|
24
|
+
'response': { status: (value) => Number.isInteger(value) && value >= 100 && value <= 599 },
|
|
25
|
+
'queue-enter': { queued: integer, applying: boolean },
|
|
26
|
+
'queue-leave': {},
|
|
27
|
+
'writer-send': { method, queued: integer, cold: boolean },
|
|
28
|
+
'worker-ready': {},
|
|
29
|
+
'worker-enter': { method, state },
|
|
30
|
+
'worker-result': { state },
|
|
31
|
+
'worker-error': { code, state },
|
|
32
|
+
'writer-expire': { expired: boolean, state },
|
|
33
|
+
'writer-result': { code, state },
|
|
34
|
+
'store-start': {},
|
|
35
|
+
'store-ready': {},
|
|
36
|
+
'helper-start': { action, call: integer, timeoutMs: duration },
|
|
37
|
+
'helper-end': { action, call: integer, helperPid: integer,
|
|
38
|
+
exit: (value) => value === null || Number.isInteger(value) && Math.abs(value) <= 0xffffffff, code },
|
|
39
|
+
'publish-start': {},
|
|
40
|
+
'publish-end': {},
|
|
41
|
+
'capture-truncated': {},
|
|
42
|
+
'capture-failed': {},
|
|
43
|
+
'capture-invalid': {},
|
|
44
|
+
};
|
|
45
|
+
const base = ['version', 'event', 'operation', 'pid', 'thread', 'sequence', 'elapsedMs', 'remainingMs'];
|
|
46
|
+
const uuid = (value) => typeof value === 'string' &&
|
|
47
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
|
|
48
|
+
|
|
49
|
+
export function validateTraceRecord(record) {
|
|
50
|
+
if (!record ||
|
|
51
|
+
Object.getPrototypeOf(record) !== Object.prototype ||
|
|
52
|
+
!Object.hasOwn(fields, record.event)) throw new Error('Invalid pooling trace record.');
|
|
53
|
+
const schema = fields[record.event];
|
|
54
|
+
const keys = [...base, ...Object.keys(schema)];
|
|
55
|
+
const valid = record.version === 1 && uuid(record.operation) &&
|
|
56
|
+
integer(record.pid) && integer(record.thread) && integer(record.sequence) &&
|
|
57
|
+
record.sequence > 0 && record.sequence <= TRACE_MAX_EVENTS &&
|
|
58
|
+
duration(record.elapsedMs) && duration(record.remainingMs) &&
|
|
59
|
+
Object.keys(record).length === keys.length &&
|
|
60
|
+
keys.every((key) => Object.hasOwn(record, key)) &&
|
|
61
|
+
Object.entries(schema).every(([key, validate]) => validate(record[key]));
|
|
62
|
+
if (!valid ||
|
|
63
|
+
Buffer.byteLength(JSON.stringify(record) + '\n') > TRACE_MAX_LINE_BYTES) {
|
|
64
|
+
throw new Error('Invalid pooling trace record.');
|
|
65
|
+
}
|
|
66
|
+
return record;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function traceErrorCode(error) {
|
|
70
|
+
try {
|
|
71
|
+
return error == null ? 'NONE' : codes.includes(error.code) ? error.code : 'UNKNOWN';
|
|
72
|
+
} catch {
|
|
73
|
+
return 'UNKNOWN';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class PoolingTraceOperation {
|
|
78
|
+
constructor(sink, deadline, context) {
|
|
79
|
+
this.sink = sink;
|
|
80
|
+
this.deadline = deadline;
|
|
81
|
+
this.context = context;
|
|
82
|
+
this.calls = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
helper() {
|
|
86
|
+
return ++this.calls;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
record(event, extra = {}) {
|
|
90
|
+
this.sink.record(this, event, extra);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class PoolingTrace {
|
|
95
|
+
#descriptor;
|
|
96
|
+
#path;
|
|
97
|
+
#identity;
|
|
98
|
+
#written = 0;
|
|
99
|
+
#owned = false;
|
|
100
|
+
#closed = false;
|
|
101
|
+
|
|
102
|
+
constructor(directory) {
|
|
103
|
+
this.directory = directory;
|
|
104
|
+
this.events = 0;
|
|
105
|
+
this.bytes = 0;
|
|
106
|
+
this.stopped = false;
|
|
107
|
+
this.failed = false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
operation(deadline, context) {
|
|
111
|
+
if (!this.directory ||
|
|
112
|
+
this.#closed) return undefined;
|
|
113
|
+
try {
|
|
114
|
+
context ??= { operation: randomUUID(), started: String(process.hrtime.bigint()), deadline: String(deadline) };
|
|
115
|
+
const valid = uuid(context.operation) &&
|
|
116
|
+
typeof context.started === 'string' && /^\d{1,24}$/.test(context.started) &&
|
|
117
|
+
typeof context.deadline === 'string' && /^\d{1,24}$/.test(context.deadline);
|
|
118
|
+
if (!valid) return undefined;
|
|
119
|
+
return new PoolingTraceOperation(this, deadline ?? BigInt(context.deadline), {
|
|
120
|
+
operation: context.operation, started: context.started, deadline: context.deadline,
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
this.failed = true;
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
record(operation, event, extra) {
|
|
129
|
+
if (this.stopped ||
|
|
130
|
+
this.#closed) return;
|
|
131
|
+
try {
|
|
132
|
+
const now = process.hrtime.bigint();
|
|
133
|
+
const milliseconds = (value) => Math.round(Math.max(-120000, Math.min(120000, Number(value) / 1e6)) * 1000) / 1000;
|
|
134
|
+
const record = {
|
|
135
|
+
version: 1, event, operation: operation.context.operation,
|
|
136
|
+
pid: process.pid, thread: threadId, sequence: this.events + 1,
|
|
137
|
+
elapsedMs: milliseconds(now - BigInt(operation.context.started)),
|
|
138
|
+
remainingMs: milliseconds(operation.deadline - now),
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
if (!Object.hasOwn(fields, event) ||
|
|
142
|
+
Object.keys(extra).some((key) => !Object.hasOwn(fields[event], key))) {
|
|
143
|
+
throw new Error('Invalid pooling trace fields.');
|
|
144
|
+
}
|
|
145
|
+
Object.assign(record, extra);
|
|
146
|
+
validateTraceRecord(record);
|
|
147
|
+
} catch {
|
|
148
|
+
record.event = 'capture-invalid';
|
|
149
|
+
for (const key of Object.keys(record)) {
|
|
150
|
+
if (!base.includes(key)) delete record[key];
|
|
151
|
+
}
|
|
152
|
+
this.stopped = true;
|
|
153
|
+
}
|
|
154
|
+
const full = this.events >= TRACE_MAX_EVENTS - 1 ||
|
|
155
|
+
this.bytes + Buffer.byteLength(JSON.stringify(record) + '\n') > TRACE_MAX_BYTES - TRACE_MAX_LINE_BYTES;
|
|
156
|
+
if (full ||
|
|
157
|
+
this.failed) {
|
|
158
|
+
record.event = this.failed ? 'capture-failed' : 'capture-truncated';
|
|
159
|
+
for (const key of Object.keys(record)) {
|
|
160
|
+
if (!base.includes(key)) delete record[key];
|
|
161
|
+
}
|
|
162
|
+
this.stopped = true;
|
|
163
|
+
}
|
|
164
|
+
validateTraceRecord(record);
|
|
165
|
+
const line = JSON.stringify(record) + '\n';
|
|
166
|
+
this.#append(line);
|
|
167
|
+
this.events++;
|
|
168
|
+
this.bytes += Buffer.byteLength(line);
|
|
169
|
+
if (this.stopped) this.close();
|
|
170
|
+
} catch {
|
|
171
|
+
// Diagnostics must never replace the operation's result or error.
|
|
172
|
+
this.failed = true;
|
|
173
|
+
if (this.#owned) this.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#append(line) {
|
|
178
|
+
if (this.#descriptor === undefined) {
|
|
179
|
+
this.#path = join(this.directory, `${process.pid}-${threadId}.jsonl`);
|
|
180
|
+
this.#descriptor = fs.openSync(this.#path, 'wx', 0o600);
|
|
181
|
+
this.#owned = true;
|
|
182
|
+
this.#identity = fs.fstatSync(this.#descriptor, { bigint: true });
|
|
183
|
+
}
|
|
184
|
+
this.#checkOwned(this.#written);
|
|
185
|
+
// A replacement after the check cannot redirect this write to another file.
|
|
186
|
+
fs.appendFileSync(this.#descriptor, line);
|
|
187
|
+
const written = this.#written + Buffer.byteLength(line);
|
|
188
|
+
this.#checkOwned(written);
|
|
189
|
+
this.#written = written;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#checkOwned(bytes) {
|
|
193
|
+
const open = fs.fstatSync(this.#descriptor, { bigint: true });
|
|
194
|
+
const named = fs.lstatSync(this.#path, { bigint: true });
|
|
195
|
+
const owned = [open, named].every((stat) =>
|
|
196
|
+
stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1n &&
|
|
197
|
+
stat.dev === this.#identity.dev && stat.ino === this.#identity.ino &&
|
|
198
|
+
stat.size === BigInt(bytes));
|
|
199
|
+
if (!owned) throw new Error('Pooling trace file ownership changed.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
close() {
|
|
203
|
+
if (this.#closed) return;
|
|
204
|
+
this.#closed = true;
|
|
205
|
+
this.stopped = true;
|
|
206
|
+
const descriptor = this.#descriptor;
|
|
207
|
+
this.#descriptor = undefined;
|
|
208
|
+
if (descriptor === undefined) return;
|
|
209
|
+
try {
|
|
210
|
+
fs.closeSync(descriptor);
|
|
211
|
+
} catch {
|
|
212
|
+
this.failed = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const poolingTrace = new PoolingTrace(process.env.MCP_POOLING_TRACE_DIR);
|
|
218
|
+
if (poolingTrace.directory) process.once('exit', () => poolingTrace.close());
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { PoolingConfigStore, PoolingConfigError } from './pooling-config.mjs';
|
|
3
|
+
import { PoolingExecution, POOLING_BUDGET_MS } from './pooling-execution.mjs';
|
|
4
|
+
import { poolingTrace, traceErrorCode } from './pooling-trace.mjs';
|
|
5
|
+
|
|
6
|
+
if (!isMainThread &&
|
|
7
|
+
workerData?.kind === 'mcp-pooling-writer') {
|
|
8
|
+
poolingTrace.operation(undefined, workerData.traceContext)?.record('worker-ready');
|
|
9
|
+
const store = new PoolingConfigStore(workerData.configPath);
|
|
10
|
+
parentPort.on('message', ({ id, method, request, deadline, buffer, traceContext }) => {
|
|
11
|
+
if (method === 'close') {
|
|
12
|
+
store.close();
|
|
13
|
+
parentPort.close();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const execution = new PoolingExecution(deadline, buffer);
|
|
17
|
+
execution.trace = poolingTrace.operation(deadline, traceContext);
|
|
18
|
+
execution.trace?.record('worker-enter', { method, state: Atomics.load(execution.state, 0) });
|
|
19
|
+
let cancelledBatchId;
|
|
20
|
+
try {
|
|
21
|
+
execution.check();
|
|
22
|
+
if (!['apply', 'undo', 'stageApply', 'stageUndo', 'rejectStage', 'commitBatch'].includes(method)) {
|
|
23
|
+
throw new PoolingConfigError(400, 'INVALID_REQUEST', 'Unknown pooling operation.');
|
|
24
|
+
}
|
|
25
|
+
const result = store[method](request, execution);
|
|
26
|
+
if (result.cancelled) cancelledBatchId = result.batchId;
|
|
27
|
+
let completedLate;
|
|
28
|
+
try {
|
|
29
|
+
completedLate = execution.complete();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (result.pending &&
|
|
32
|
+
result.accepted !== false) {
|
|
33
|
+
store.rejectStage(result, new PoolingExecution(
|
|
34
|
+
process.hrtime.bigint() + BigInt(POOLING_BUDGET_MS) * 1000000n));
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
execution.trace?.record('worker-result', { state: Atomics.load(execution.state, 0) });
|
|
39
|
+
parentPort.postMessage({ id, result, completedLate });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
execution.trace?.record('worker-error', { code: traceErrorCode(error), state: Atomics.load(execution.state, 0) });
|
|
42
|
+
execution.fail(error.commitState);
|
|
43
|
+
const known = error instanceof PoolingConfigError;
|
|
44
|
+
parentPort.postMessage({ id, cancelledBatchId, error: {
|
|
45
|
+
statusCode: known ? error.statusCode : 500,
|
|
46
|
+
code: known ? error.code : 'WRITER_FAILED',
|
|
47
|
+
message: known ? error.message : 'Pooling writer failed. Reread the config before retrying.',
|
|
48
|
+
commitState: error.commitState,
|
|
49
|
+
} });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class PoolingConfigWriter {
|
|
55
|
+
#configPath;
|
|
56
|
+
#worker;
|
|
57
|
+
#pending = new Map();
|
|
58
|
+
#sequence = 0;
|
|
59
|
+
#closed = false;
|
|
60
|
+
#closing;
|
|
61
|
+
#resolveClose;
|
|
62
|
+
#failure;
|
|
63
|
+
|
|
64
|
+
constructor(configPath) {
|
|
65
|
+
this.#configPath = configPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
apply(request, options = {}) {
|
|
69
|
+
return this.#send('apply', request, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
undo(request, options = {}) {
|
|
73
|
+
return this.#send('undo', request, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
stageApply(request, options = {}) {
|
|
77
|
+
return this.#send('stageApply', request, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stageUndo(request, options = {}) {
|
|
81
|
+
return this.#send('stageUndo', request, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
commitBatch(request, options = {}) {
|
|
85
|
+
return this.#send('commitBatch', request, options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
rejectStage(request) {
|
|
89
|
+
return this.#send('rejectStage', request, {});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#send(method, request, options) {
|
|
93
|
+
if (this.#closed) {
|
|
94
|
+
return Promise.reject(new PoolingConfigError(503, 'WRITER_CLOSED', 'Pooling writer is shutting down.'));
|
|
95
|
+
}
|
|
96
|
+
if (this.#failure) return Promise.reject(this.#failure);
|
|
97
|
+
const defaultDeadline = process.hrtime.bigint() + BigInt(POOLING_BUDGET_MS) * 1000000n;
|
|
98
|
+
const deadline = options.deadline === undefined ? defaultDeadline
|
|
99
|
+
: options.deadline < defaultDeadline ? options.deadline : defaultDeadline;
|
|
100
|
+
const execution = new PoolingExecution(deadline);
|
|
101
|
+
execution.trace = options.trace ?? poolingTrace.operation(deadline);
|
|
102
|
+
if (options.signal?.aborted) return Promise.reject(execution.cancel());
|
|
103
|
+
try {
|
|
104
|
+
execution.check();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return Promise.reject(error);
|
|
107
|
+
}
|
|
108
|
+
if (this.#pending.size >= 16) {
|
|
109
|
+
return Promise.reject(new PoolingConfigError(503, 'WRITER_BUSY', 'Too many pending pooling changes.'));
|
|
110
|
+
}
|
|
111
|
+
execution.trace?.record('writer-send', { method, queued: this.#pending.size, cold: !this.#worker });
|
|
112
|
+
if (!this.#worker) this.#start(execution.trace);
|
|
113
|
+
const id = ++this.#sequence;
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const pending = { resolve, reject, execution, options, method, settled: false };
|
|
116
|
+
const cancel = (expired) => {
|
|
117
|
+
if (pending.settled) return;
|
|
118
|
+
execution.trace?.record('writer-expire', { expired, state: Atomics.load(execution.state, 0) });
|
|
119
|
+
const error = execution.cancel(expired);
|
|
120
|
+
if (!error) return;
|
|
121
|
+
this.#settle(pending, error);
|
|
122
|
+
};
|
|
123
|
+
pending.abort = () => cancel(false);
|
|
124
|
+
this.#pending.set(id, pending);
|
|
125
|
+
try {
|
|
126
|
+
options.signal?.addEventListener('abort', pending.abort, { once: true });
|
|
127
|
+
pending.timer = setTimeout(() => cancel(true), execution.remainingMs());
|
|
128
|
+
this.#worker.postMessage({ id, method, request, deadline, buffer: execution.state.buffer,
|
|
129
|
+
traceContext: execution.trace?.context });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
this.#pending.delete(id);
|
|
132
|
+
this.#settle(pending, error);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#start(trace) {
|
|
138
|
+
this.#worker = new Worker(new URL(import.meta.url), {
|
|
139
|
+
workerData: { kind: 'mcp-pooling-writer', configPath: this.#configPath, traceContext: trace?.context },
|
|
140
|
+
});
|
|
141
|
+
this.#worker.on('message', async ({ id, result, error, completedLate, cancelledBatchId }) => {
|
|
142
|
+
const pending = this.#pending.get(id);
|
|
143
|
+
if (!pending) return;
|
|
144
|
+
pending.execution.trace?.record('writer-result', {
|
|
145
|
+
code: traceErrorCode(error), state: Atomics.load(pending.execution.state, 0),
|
|
146
|
+
});
|
|
147
|
+
this.#pending.delete(id);
|
|
148
|
+
let failure = error
|
|
149
|
+
? new PoolingConfigError(error.statusCode, error.code, error.message)
|
|
150
|
+
: completedLate ? pending.execution.committedLate() : undefined;
|
|
151
|
+
cancelledBatchId ??= result?.cancelled ? result.batchId : undefined;
|
|
152
|
+
if (!failure &&
|
|
153
|
+
cancelledBatchId &&
|
|
154
|
+
pending.options.signal?.aborted) {
|
|
155
|
+
failure = new PoolingConfigError(408, 'WRITER_CANCELLED',
|
|
156
|
+
'Pooling change was cancelled before config commit.');
|
|
157
|
+
}
|
|
158
|
+
if (failure) {
|
|
159
|
+
failure.commitState = error?.commitState ?? failure.commitState ??
|
|
160
|
+
(pending.method === 'commitBatch' ? 'not-committed' : undefined);
|
|
161
|
+
if (cancelledBatchId) failure.cancelledBatchId = cancelledBatchId;
|
|
162
|
+
}
|
|
163
|
+
if (!failure &&
|
|
164
|
+
result?.pending &&
|
|
165
|
+
result.accepted !== false &&
|
|
166
|
+
pending.options.signal?.aborted) {
|
|
167
|
+
try {
|
|
168
|
+
await this.rejectStage({ generation: result.generation });
|
|
169
|
+
this.#settle(pending, new PoolingConfigError(408, 'WRITER_CANCELLED',
|
|
170
|
+
'Pooling change was cancelled before config commit.'));
|
|
171
|
+
} catch (cleanupError) {
|
|
172
|
+
this.#settle(pending, cleanupError);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (pending.settled ||
|
|
177
|
+
completedLate) this.#lateSettlement(pending, failure, result);
|
|
178
|
+
this.#settle(pending, failure, result);
|
|
179
|
+
});
|
|
180
|
+
this.#worker.on('error', () => this.#failed());
|
|
181
|
+
this.#worker.on('exit', (code) => {
|
|
182
|
+
if (code !== 0 ||
|
|
183
|
+
this.#pending.size > 0 ||
|
|
184
|
+
!this.#closed) {
|
|
185
|
+
this.#failed();
|
|
186
|
+
}
|
|
187
|
+
this.#worker = null;
|
|
188
|
+
this.#resolveClose?.();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#failed() {
|
|
193
|
+
this.#failure ??= new PoolingConfigError(500, 'WRITER_FAILED',
|
|
194
|
+
'Pooling writer stopped. Reread the config and restart the bridge before retrying.');
|
|
195
|
+
for (const pending of this.#pending.values()) {
|
|
196
|
+
const commitFailure = pending.execution.workerFailure();
|
|
197
|
+
const failure = commitFailure ?? this.#failure;
|
|
198
|
+
if (pending.settled ||
|
|
199
|
+
commitFailure) this.#lateSettlement(pending, failure, undefined, Boolean(commitFailure));
|
|
200
|
+
this.#settle(pending, failure);
|
|
201
|
+
}
|
|
202
|
+
this.#pending.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#settle(pending, error, result) {
|
|
206
|
+
if (pending.settled) return;
|
|
207
|
+
pending.settled = true;
|
|
208
|
+
clearTimeout(pending.timer);
|
|
209
|
+
pending.options.signal?.removeEventListener('abort', pending.abort);
|
|
210
|
+
if (error) pending.reject(error);
|
|
211
|
+
else pending.resolve(result);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#lateSettlement(pending, error, result, commitStarted = false) {
|
|
215
|
+
if (pending.options.onLateSettlement) {
|
|
216
|
+
try {
|
|
217
|
+
pending.options.onLateSettlement({
|
|
218
|
+
error, result,
|
|
219
|
+
committed: Boolean(result && !result.pending && !result.cancelled) ||
|
|
220
|
+
error?.commitState === 'committed',
|
|
221
|
+
cancelledBatchId: error?.cancelledBatchId ?? (result?.cancelled ? result.batchId : undefined),
|
|
222
|
+
commitStarted,
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
process.emitWarning('Unable to reconcile a pooling operation without a normal result.');
|
|
226
|
+
}
|
|
227
|
+
} else if (error &&
|
|
228
|
+
error.code !== 'WRITER_CANCELLED' &&
|
|
229
|
+
error.code !== 'WRITER_DEADLINE') {
|
|
230
|
+
process.emitWarning(`Pooling operation settled without a normal result (${error.code}). Reread the config.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
close() {
|
|
235
|
+
this.#closed = true;
|
|
236
|
+
if (this.#closing) return this.#closing;
|
|
237
|
+
if (!this.#worker) return Promise.resolve();
|
|
238
|
+
this.#closing = new Promise((resolve) => { this.#resolveClose = resolve; });
|
|
239
|
+
this.#worker.postMessage({ method: 'close' });
|
|
240
|
+
return this.#closing;
|
|
241
|
+
}
|
|
242
|
+
}
|