@lark-apaas/fullstack-cli 1.1.59-alpha.20260722050835 → 1.1.59-alpha.20260722071909
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.
|
@@ -1,2155 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { spawnSync: nodeSpawnSync } = require('child_process');
|
|
6
|
-
|
|
7
|
-
const DEFAULT_OUTBOX_ROOT = '/tmp/event/miaoda-preview-phase/v1';
|
|
8
|
-
const MAX_EVENT_BYTES = 16 * 1024;
|
|
9
|
-
const DEFAULT_MAX_RUN_BYTES = 2 * 1024 * 1024;
|
|
10
|
-
const DEFAULT_MAX_ROOT_BYTES = 64 * 1024 * 1024;
|
|
11
|
-
const DEFAULT_MAX_ROOT_DIRECTORIES = 128;
|
|
12
|
-
const DEFAULT_MAX_ROOT_SCAN_DIRECTORIES = 256;
|
|
13
|
-
const DEFAULT_MAX_RUN_SCAN_ENTRIES = 64;
|
|
14
|
-
const DEFAULT_MAX_FALLBACK_FILES = 32;
|
|
15
|
-
const DEFAULT_MAX_FALLBACK_BYTES = 2 * 1024 * 1024;
|
|
16
|
-
const DEFAULT_MAX_FALLBACK_EVENTS = 256;
|
|
17
|
-
const MAX_FALLBACK_RECORD_BYTES = MAX_EVENT_BYTES + 4 * 1024;
|
|
18
|
-
const FALLBACK_TERMINAL_RESERVE_BYTES = 1024;
|
|
19
|
-
const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,60}$/;
|
|
20
|
-
const STATE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
21
|
-
const PRODUCER_INSTANCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
22
|
-
const FALLBACK_SESSION_ID_PATTERN = /^pfs_[a-f0-9]{32}$/;
|
|
23
|
-
const PROCESS_START_TICKS_PATTERN = /^\d+$/;
|
|
24
|
-
const PRODUCER_EPOCH_PATTERN = /^ppe_[a-f0-9]{32}$/;
|
|
25
|
-
const INSTANCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
|
|
26
|
-
const PRODUCER_REGISTER_URL = 'http://127.0.0.1:8091/shell/exec';
|
|
27
|
-
const PRODUCER_REGISTER_ATTEMPTS = 2;
|
|
28
|
-
const PRODUCER_REGISTER_CURL_TIMEOUT_SECONDS = '0.35';
|
|
29
|
-
const PRODUCER_REGISTER_PROCESS_TIMEOUT_MS = 400;
|
|
30
|
-
const PRODUCER_REGISTER_MAX_RESPONSE_BYTES = 64 * 1024;
|
|
31
|
-
const TRANSIENT_ERRNO_CODES = new Set([
|
|
32
|
-
'EAGAIN',
|
|
33
|
-
'EINTR',
|
|
34
|
-
'EBUSY',
|
|
35
|
-
'EMFILE',
|
|
36
|
-
'ENFILE',
|
|
37
|
-
'ENOMEM',
|
|
38
|
-
]);
|
|
39
|
-
const MAX_TRANSIENT_ATTEMPTS = 3;
|
|
40
|
-
|
|
41
|
-
function producerRegistrationError(code, message, cause) {
|
|
42
|
-
const error = new Error(message, cause == null ? undefined : { cause });
|
|
43
|
-
error.code = code;
|
|
44
|
-
return error;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function validateProducerRegistrationResponse(response, request) {
|
|
48
|
-
const commonInvalid =
|
|
49
|
-
response?.protocol !== 'preview-phase-outbox-v1' ||
|
|
50
|
-
response.capability_status !== 'supported' ||
|
|
51
|
-
response.generation !== request.generation ||
|
|
52
|
-
response.producer_epoch !== request.producer_epoch ||
|
|
53
|
-
response.producer_generation !== request.generation;
|
|
54
|
-
const rejected = response?.registration_status === 'rejected';
|
|
55
|
-
const rejectedInvalid =
|
|
56
|
-
rejected &&
|
|
57
|
-
(response.registration_reason !== 'stale_epoch' ||
|
|
58
|
-
response.writer_active === true ||
|
|
59
|
-
response.sealed !== true ||
|
|
60
|
-
response.fallback_required !== true ||
|
|
61
|
-
response.fallback_session_id !== request.fallback_session_id ||
|
|
62
|
-
response.fallback_final_seq !== 1 ||
|
|
63
|
-
response.producer_state !== 'rejected' ||
|
|
64
|
-
request.rejected_event == null);
|
|
65
|
-
const acceptedInvalid =
|
|
66
|
-
!rejected &&
|
|
67
|
-
((response?.registration_status != null &&
|
|
68
|
-
response.registration_status !== 'accepted') ||
|
|
69
|
-
response.writer_active !== true ||
|
|
70
|
-
response.producer_state !== 'active');
|
|
71
|
-
if (commonInvalid || rejectedInvalid || acceptedInvalid) {
|
|
72
|
-
throw producerRegistrationError(
|
|
73
|
-
'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
|
|
74
|
-
'invalid preview phase producer registration response'
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
return response;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function registerPreviewPhaseProducerViaShellServer(
|
|
81
|
-
request,
|
|
82
|
-
spawnSync = nodeSpawnSync
|
|
83
|
-
) {
|
|
84
|
-
const wireRequest = {
|
|
85
|
-
phase_outbox: {
|
|
86
|
-
protocol: 'preview-phase-outbox-v1',
|
|
87
|
-
operation: 'register',
|
|
88
|
-
run_id: request.run_id,
|
|
89
|
-
generation: request.generation,
|
|
90
|
-
expected_instance_id: request.expected_instance_id,
|
|
91
|
-
producer_epoch: request.producer_epoch,
|
|
92
|
-
writer_instance_id: request.writer_instance_id,
|
|
93
|
-
pid: request.pid,
|
|
94
|
-
process_start_ticks: request.process_start_ticks,
|
|
95
|
-
...(request.rejected_event == null
|
|
96
|
-
? {}
|
|
97
|
-
: {
|
|
98
|
-
producer_instance_id: request.producer_instance_id,
|
|
99
|
-
fallback_session_id: request.fallback_session_id,
|
|
100
|
-
rejected_event: request.rejected_event,
|
|
101
|
-
}),
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
const result = spawnSync(
|
|
105
|
-
'curl',
|
|
106
|
-
[
|
|
107
|
-
'--silent',
|
|
108
|
-
'--show-error',
|
|
109
|
-
'--connect-timeout',
|
|
110
|
-
PRODUCER_REGISTER_CURL_TIMEOUT_SECONDS,
|
|
111
|
-
'--max-time',
|
|
112
|
-
PRODUCER_REGISTER_CURL_TIMEOUT_SECONDS,
|
|
113
|
-
'--request',
|
|
114
|
-
'POST',
|
|
115
|
-
'--header',
|
|
116
|
-
'Content-Type: application/json',
|
|
117
|
-
'--data-binary',
|
|
118
|
-
'@-',
|
|
119
|
-
PRODUCER_REGISTER_URL,
|
|
120
|
-
],
|
|
121
|
-
{
|
|
122
|
-
input: Buffer.from(JSON.stringify(wireRequest), 'utf8'),
|
|
123
|
-
encoding: 'utf8',
|
|
124
|
-
timeout: PRODUCER_REGISTER_PROCESS_TIMEOUT_MS,
|
|
125
|
-
maxBuffer: PRODUCER_REGISTER_MAX_RESPONSE_BYTES,
|
|
126
|
-
windowsHide: true,
|
|
127
|
-
}
|
|
128
|
-
);
|
|
129
|
-
if (result?.error) {
|
|
130
|
-
const code =
|
|
131
|
-
result.error.code === 'ETIMEDOUT'
|
|
132
|
-
? 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN'
|
|
133
|
-
: 'PREVIEW_PHASE_PRODUCER_REGISTRATION_UNAVAILABLE';
|
|
134
|
-
throw producerRegistrationError(
|
|
135
|
-
code,
|
|
136
|
-
`preview phase producer registration transport failed: ${result.error.message}`,
|
|
137
|
-
result.error
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
if (result?.signal || result?.status === 28) {
|
|
141
|
-
throw producerRegistrationError(
|
|
142
|
-
'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
|
|
143
|
-
'preview phase producer registration acknowledgement timed out'
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
if (result?.status !== 0) {
|
|
147
|
-
throw producerRegistrationError(
|
|
148
|
-
'PREVIEW_PHASE_PRODUCER_REGISTRATION_UNAVAILABLE',
|
|
149
|
-
`preview phase producer registration transport exited ${String(result?.status)}`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
let envelope;
|
|
153
|
-
try {
|
|
154
|
-
envelope = JSON.parse(result.stdout || '');
|
|
155
|
-
} catch (error) {
|
|
156
|
-
throw producerRegistrationError(
|
|
157
|
-
'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
|
|
158
|
-
'preview phase producer registration returned invalid JSON',
|
|
159
|
-
error
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
if (envelope?.success !== true || envelope?.data?.success !== true) {
|
|
163
|
-
const detail =
|
|
164
|
-
envelope?.data?.error || envelope?.error || envelope?.message || 'rejected';
|
|
165
|
-
const detailText = String(detail);
|
|
166
|
-
let code = 'PREVIEW_PHASE_PRODUCER_REGISTRATION_REJECTED';
|
|
167
|
-
if (detailText.includes('registration acknowledgement unknown')) {
|
|
168
|
-
code = 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN';
|
|
169
|
-
} else if (detailText.includes('producer epoch already belongs')) {
|
|
170
|
-
code = 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT';
|
|
171
|
-
} else if (
|
|
172
|
-
detailText.includes('publish preview phase canonical registration') ||
|
|
173
|
-
detailText.includes('conflicts with canonical run')
|
|
174
|
-
) {
|
|
175
|
-
code = 'PREVIEW_PHASE_RUN_CONFLICT';
|
|
176
|
-
}
|
|
177
|
-
throw producerRegistrationError(
|
|
178
|
-
code,
|
|
179
|
-
`preview phase producer registration rejected: ${detailText}`
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
let response;
|
|
183
|
-
try {
|
|
184
|
-
response = JSON.parse(envelope.data.output || '');
|
|
185
|
-
} catch (error) {
|
|
186
|
-
throw producerRegistrationError(
|
|
187
|
-
'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
|
|
188
|
-
'preview phase producer registration acknowledgement was invalid',
|
|
189
|
-
error
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
return validateProducerRegistrationResponse(response, request);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function readLinuxProcessStartTicks(fileSystem, pid) {
|
|
196
|
-
try {
|
|
197
|
-
const stat = fileSystem.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
198
|
-
// comm (field 2) may contain spaces or parentheses, so split only after
|
|
199
|
-
// its final closing parenthesis. The remaining first field is state (3),
|
|
200
|
-
// therefore starttime (22) is at index 19.
|
|
201
|
-
const commEnd = stat.lastIndexOf(')');
|
|
202
|
-
if (commEnd < 0) return null;
|
|
203
|
-
const fields = stat
|
|
204
|
-
.slice(commEnd + 1)
|
|
205
|
-
.trim()
|
|
206
|
-
.split(/\s+/);
|
|
207
|
-
const startTicks = fields[19];
|
|
208
|
-
return PROCESS_START_TICKS_PATTERN.test(startTicks || '')
|
|
209
|
-
? startTicks
|
|
210
|
-
: null;
|
|
211
|
-
} catch {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function assertSafeDirectory(fileSystem, directoryPath) {
|
|
217
|
-
if (!fileSystem.existsSync(directoryPath)) {
|
|
218
|
-
fileSystem.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
|
|
219
|
-
}
|
|
220
|
-
const stat = fileSystem.lstatSync(directoryPath);
|
|
221
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
222
|
-
throw new Error(`unsafe preview phase outbox path: ${directoryPath}`);
|
|
223
|
-
}
|
|
224
|
-
fileSystem.chmodSync(directoryPath, 0o700);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Canonical generations are published only by the staging-directory rename.
|
|
228
|
-
// Every later lookup must be existing-only: recreating a path that the
|
|
229
|
-
// controller just quarantined would hide the durable generation and could
|
|
230
|
-
// manufacture a second, unrelated canonical lifecycle.
|
|
231
|
-
function assertExistingSafeDirectory(fileSystem, directoryPath) {
|
|
232
|
-
const stat = fileSystem.lstatSync(directoryPath);
|
|
233
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
234
|
-
throw new Error(`unsafe preview phase outbox path: ${directoryPath}`);
|
|
235
|
-
}
|
|
236
|
-
fileSystem.chmodSync(directoryPath, 0o700);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function createFallbackSessionID(randomUUID = require('crypto').randomUUID) {
|
|
240
|
-
const sessionID = `pfs_${String(randomUUID())
|
|
241
|
-
.replaceAll('-', '')
|
|
242
|
-
.toLowerCase()}`;
|
|
243
|
-
if (!FALLBACK_SESSION_ID_PATTERN.test(sessionID)) {
|
|
244
|
-
throw new Error('invalid preview phase fallback session UUID');
|
|
245
|
-
}
|
|
246
|
-
return sessionID;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function createPreviewPhaseOutbox(options) {
|
|
250
|
-
const fileSystem = options.fs || fs;
|
|
251
|
-
const root = options.root || DEFAULT_OUTBOX_ROOT;
|
|
252
|
-
const randomUUID = options.random_uuid || require('crypto').randomUUID;
|
|
253
|
-
const now = options.now || Date.now;
|
|
254
|
-
const writerInstanceID = options.writer_instance_id || randomUUID();
|
|
255
|
-
const producerInstanceID = options.producer_instance_id || writerInstanceID;
|
|
256
|
-
const fallbackSessionID =
|
|
257
|
-
options.fallback_session_id || createFallbackSessionID();
|
|
258
|
-
const producerEpoch = options.producer_epoch;
|
|
259
|
-
const expectedInstanceID = options.expected_instance_id;
|
|
260
|
-
const registerProducer =
|
|
261
|
-
options.register_producer ||
|
|
262
|
-
(request =>
|
|
263
|
-
registerPreviewPhaseProducerViaShellServer(
|
|
264
|
-
request,
|
|
265
|
-
options.spawn_sync || nodeSpawnSync
|
|
266
|
-
));
|
|
267
|
-
const getProcessStartTicks =
|
|
268
|
-
options.get_process_start_ticks ||
|
|
269
|
-
(pid => readLinuxProcessStartTicks(fileSystem, pid));
|
|
270
|
-
const configuredProcessStartTicks = options.process_start_ticks;
|
|
271
|
-
const currentProcessStartTicks =
|
|
272
|
-
configuredProcessStartTicks == null
|
|
273
|
-
? getProcessStartTicks(process.pid)
|
|
274
|
-
: configuredProcessStartTicks;
|
|
275
|
-
const maxRunBytes = options.max_run_bytes ?? DEFAULT_MAX_RUN_BYTES;
|
|
276
|
-
const maxRootBytes = options.max_root_bytes ?? DEFAULT_MAX_ROOT_BYTES;
|
|
277
|
-
const maxRootDirectories =
|
|
278
|
-
options.max_root_directories ?? DEFAULT_MAX_ROOT_DIRECTORIES;
|
|
279
|
-
const maxRootScanDirectories =
|
|
280
|
-
options.max_root_scan_directories ?? DEFAULT_MAX_ROOT_SCAN_DIRECTORIES;
|
|
281
|
-
const maxRunScanEntries =
|
|
282
|
-
options.max_run_scan_entries ?? DEFAULT_MAX_RUN_SCAN_ENTRIES;
|
|
283
|
-
const maxFallbackFiles =
|
|
284
|
-
options.max_fallback_files ??
|
|
285
|
-
Math.min(DEFAULT_MAX_FALLBACK_FILES, maxRunScanEntries);
|
|
286
|
-
const maxFallbackBytes =
|
|
287
|
-
options.max_fallback_bytes ?? DEFAULT_MAX_FALLBACK_BYTES;
|
|
288
|
-
const maxFallbackEvents =
|
|
289
|
-
options.max_fallback_events ?? DEFAULT_MAX_FALLBACK_EVENTS;
|
|
290
|
-
const maxFallbackRecordBytes =
|
|
291
|
-
options.max_fallback_record_bytes ?? MAX_FALLBACK_RECORD_BYTES;
|
|
292
|
-
if (!RUN_ID_PATTERN.test(options.run_id || '')) {
|
|
293
|
-
throw new Error('invalid preview run_id for phase outbox');
|
|
294
|
-
}
|
|
295
|
-
if (!PRODUCER_EPOCH_PATTERN.test(producerEpoch || '')) {
|
|
296
|
-
throw new Error('invalid preview phase producer_epoch');
|
|
297
|
-
}
|
|
298
|
-
if (!INSTANCE_ID_PATTERN.test(expectedInstanceID || '')) {
|
|
299
|
-
throw new Error('invalid preview phase expected_instance_id');
|
|
300
|
-
}
|
|
301
|
-
if (typeof registerProducer !== 'function') {
|
|
302
|
-
throw new Error('invalid preview phase producer registrar');
|
|
303
|
-
}
|
|
304
|
-
if (!STATE_ID_PATTERN.test(writerInstanceID)) {
|
|
305
|
-
throw new Error('invalid preview phase writer_instance_id');
|
|
306
|
-
}
|
|
307
|
-
if (!STATE_ID_PATTERN.test(producerInstanceID)) {
|
|
308
|
-
throw new Error('invalid preview phase producer_instance_id');
|
|
309
|
-
}
|
|
310
|
-
if (!FALLBACK_SESSION_ID_PATTERN.test(fallbackSessionID)) {
|
|
311
|
-
throw new Error('invalid preview phase fallback_session_id');
|
|
312
|
-
}
|
|
313
|
-
if (!PROCESS_START_TICKS_PATTERN.test(currentProcessStartTicks || '')) {
|
|
314
|
-
throw new Error('invalid preview phase process_start_ticks');
|
|
315
|
-
}
|
|
316
|
-
if (!Number.isSafeInteger(maxRunBytes) || maxRunBytes <= 0) {
|
|
317
|
-
throw new Error('invalid preview phase max_run_bytes');
|
|
318
|
-
}
|
|
319
|
-
if (!Number.isSafeInteger(maxRootBytes) || maxRootBytes <= 0) {
|
|
320
|
-
throw new Error('invalid preview phase max_root_bytes');
|
|
321
|
-
}
|
|
322
|
-
if (!Number.isSafeInteger(maxRootDirectories) || maxRootDirectories <= 0) {
|
|
323
|
-
throw new Error('invalid preview phase max_root_directories');
|
|
324
|
-
}
|
|
325
|
-
if (
|
|
326
|
-
!Number.isSafeInteger(maxRootScanDirectories) ||
|
|
327
|
-
maxRootScanDirectories < maxRootDirectories
|
|
328
|
-
) {
|
|
329
|
-
throw new Error('invalid preview phase max_root_scan_directories');
|
|
330
|
-
}
|
|
331
|
-
if (!Number.isSafeInteger(maxRunScanEntries) || maxRunScanEntries <= 0) {
|
|
332
|
-
throw new Error('invalid preview phase max_run_scan_entries');
|
|
333
|
-
}
|
|
334
|
-
if (
|
|
335
|
-
!Number.isSafeInteger(maxFallbackFiles) ||
|
|
336
|
-
maxFallbackFiles <= 0 ||
|
|
337
|
-
maxFallbackFiles > maxRunScanEntries
|
|
338
|
-
) {
|
|
339
|
-
throw new Error('invalid preview phase max_fallback_files');
|
|
340
|
-
}
|
|
341
|
-
if (!Number.isSafeInteger(maxFallbackBytes) || maxFallbackBytes <= 0) {
|
|
342
|
-
throw new Error('invalid preview phase max_fallback_bytes');
|
|
343
|
-
}
|
|
344
|
-
if (!Number.isSafeInteger(maxFallbackEvents) || maxFallbackEvents <= 0) {
|
|
345
|
-
throw new Error('invalid preview phase max_fallback_events');
|
|
346
|
-
}
|
|
347
|
-
if (
|
|
348
|
-
!Number.isSafeInteger(maxFallbackRecordBytes) ||
|
|
349
|
-
maxFallbackRecordBytes <= 0
|
|
350
|
-
) {
|
|
351
|
-
throw new Error('invalid preview phase max_fallback_record_bytes');
|
|
352
|
-
}
|
|
353
|
-
const canonicalRunDir = path.join(root, options.run_id);
|
|
354
|
-
const stagingName = `.staging-${process.pid}-${writerInstanceID}`;
|
|
355
|
-
const stagingRunDir = path.join(root, stagingName);
|
|
356
|
-
let runDir = canonicalRunDir;
|
|
357
|
-
let eventsPath = path.join(runDir, 'events.jsonl');
|
|
358
|
-
let metaPath = path.join(runDir, 'meta.json');
|
|
359
|
-
let writerLockPath = path.join(runDir, 'writer.lock');
|
|
360
|
-
let sealPath = path.join(runDir, 'sealed.json');
|
|
361
|
-
let degradedPath = path.join(runDir, 'degraded.json');
|
|
362
|
-
let fallbackIntentPath = path.join(runDir, 'fallback.intent.json');
|
|
363
|
-
let quiesceRequestPath = path.join(runDir, 'quiesce.request.json');
|
|
364
|
-
let unboundFallbackPath = path.join(runDir, 'producer.unbound.json');
|
|
365
|
-
const fallbackSpoolPath = path.join(
|
|
366
|
-
canonicalRunDir,
|
|
367
|
-
`fallback-${fallbackSessionID}.jsonl`
|
|
368
|
-
);
|
|
369
|
-
const producerEpochMarkerPath = path.join(
|
|
370
|
-
root,
|
|
371
|
-
`producer-epoch-${producerEpoch}.json`
|
|
372
|
-
);
|
|
373
|
-
let recovered = false;
|
|
374
|
-
let metadata = null;
|
|
375
|
-
let writerLockOwned = false;
|
|
376
|
-
let writerLockDirectory = null;
|
|
377
|
-
let eventsFD = null;
|
|
378
|
-
let committedBytes = null;
|
|
379
|
-
let degraded = false;
|
|
380
|
-
let fallbackRequired = false;
|
|
381
|
-
let sealRequested = false;
|
|
382
|
-
let closed = false;
|
|
383
|
-
let closeResult = false;
|
|
384
|
-
let producerMarkerActive = false;
|
|
385
|
-
let producerMarkerTerminal = false;
|
|
386
|
-
let producerRegistrationDurable = false;
|
|
387
|
-
let producerRegistrationAttemptCount = 0;
|
|
388
|
-
let producerRegistrationLastError = null;
|
|
389
|
-
let producerStartedAtMs = null;
|
|
390
|
-
let producerTerminalAtMs = null;
|
|
391
|
-
let canonicalPublicationDurable = false;
|
|
392
|
-
let everOwnedCanonicalWriter = false;
|
|
393
|
-
let fallbackFD = null;
|
|
394
|
-
let fallbackBytes = 0;
|
|
395
|
-
let fallbackSeq = 0;
|
|
396
|
-
let fallbackStarted = false;
|
|
397
|
-
let fallbackSealed = false;
|
|
398
|
-
let fallbackFailed = false;
|
|
399
|
-
let fallbackCreatedAt = null;
|
|
400
|
-
function selectRunDirectory(directoryPath) {
|
|
401
|
-
if (runDir !== directoryPath) {
|
|
402
|
-
if (eventsFD != null) {
|
|
403
|
-
throw new Error(
|
|
404
|
-
'cannot switch preview phase directory with an open events fd'
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
metadata = null;
|
|
408
|
-
recovered = false;
|
|
409
|
-
committedBytes = null;
|
|
410
|
-
}
|
|
411
|
-
runDir = directoryPath;
|
|
412
|
-
eventsPath = path.join(runDir, 'events.jsonl');
|
|
413
|
-
metaPath = path.join(runDir, 'meta.json');
|
|
414
|
-
writerLockPath = path.join(runDir, 'writer.lock');
|
|
415
|
-
sealPath = path.join(runDir, 'sealed.json');
|
|
416
|
-
degradedPath = path.join(runDir, 'degraded.json');
|
|
417
|
-
fallbackIntentPath = path.join(runDir, 'fallback.intent.json');
|
|
418
|
-
quiesceRequestPath = path.join(runDir, 'quiesce.request.json');
|
|
419
|
-
unboundFallbackPath = path.join(runDir, 'producer.unbound.json');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function ownsSelectedWriterLock() {
|
|
423
|
-
return writerLockOwned && writerLockDirectory === runDir;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function isTransientFilesystemError(error) {
|
|
427
|
-
return TRANSIENT_ERRNO_CODES.has(error?.code);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function rootQuotaError(message) {
|
|
431
|
-
const error = new Error(message);
|
|
432
|
-
error.code = 'PREVIEW_PHASE_ROOT_QUOTA_EXCEEDED';
|
|
433
|
-
return error;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function readDirectoryEntriesBounded(directoryPath, limit, quotaMessage) {
|
|
437
|
-
const entries = [];
|
|
438
|
-
const directory = fileSystem.opendirSync(directoryPath);
|
|
439
|
-
try {
|
|
440
|
-
while (entries.length <= limit) {
|
|
441
|
-
const entry = directory.readSync();
|
|
442
|
-
if (!entry) break;
|
|
443
|
-
entries.push(entry);
|
|
444
|
-
if (entries.length > limit) {
|
|
445
|
-
throw rootQuotaError(quotaMessage);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
} finally {
|
|
449
|
-
directory.closeSync();
|
|
450
|
-
}
|
|
451
|
-
return entries;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function inspectRootUsage() {
|
|
455
|
-
const entries = readDirectoryEntriesBounded(
|
|
456
|
-
root,
|
|
457
|
-
maxRootScanDirectories,
|
|
458
|
-
`preview phase root scan exceeds ${maxRootScanDirectories} entries`
|
|
459
|
-
);
|
|
460
|
-
let directories = 0;
|
|
461
|
-
let bytes = 0;
|
|
462
|
-
const disappearingEntry = error =>
|
|
463
|
-
error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
|
|
464
|
-
for (const entry of entries) {
|
|
465
|
-
const entryPath = path.join(root, entry.name);
|
|
466
|
-
let stat;
|
|
467
|
-
try {
|
|
468
|
-
stat = fileSystem.lstatSync(entryPath);
|
|
469
|
-
} catch (error) {
|
|
470
|
-
// The Go consumer atomically renames and removes acknowledged runs.
|
|
471
|
-
// A root snapshot can therefore contain an entry that is gone by the
|
|
472
|
-
// following lstat; disappearance is successful GC, not degradation.
|
|
473
|
-
if (disappearingEntry(error)) continue;
|
|
474
|
-
throw error;
|
|
475
|
-
}
|
|
476
|
-
if (stat.isSymbolicLink()) {
|
|
477
|
-
throw new Error(`unsafe preview phase outbox path: ${entryPath}`);
|
|
478
|
-
}
|
|
479
|
-
if (stat.isFile()) {
|
|
480
|
-
bytes += stat.size;
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
if (!stat.isDirectory()) {
|
|
484
|
-
throw new Error(`unsafe preview phase outbox path: ${entryPath}`);
|
|
485
|
-
}
|
|
486
|
-
directories += 1;
|
|
487
|
-
let runEntries;
|
|
488
|
-
try {
|
|
489
|
-
runEntries = readDirectoryEntriesBounded(
|
|
490
|
-
entryPath,
|
|
491
|
-
maxRunScanEntries,
|
|
492
|
-
`preview phase run scan exceeds ${maxRunScanEntries} entries: ${entryPath}`
|
|
493
|
-
);
|
|
494
|
-
} catch (error) {
|
|
495
|
-
if (disappearingEntry(error)) continue;
|
|
496
|
-
throw error;
|
|
497
|
-
}
|
|
498
|
-
for (const runEntry of runEntries) {
|
|
499
|
-
const statePath = path.join(entryPath, runEntry.name);
|
|
500
|
-
let stateStat;
|
|
501
|
-
try {
|
|
502
|
-
stateStat = fileSystem.lstatSync(statePath);
|
|
503
|
-
} catch (error) {
|
|
504
|
-
if (disappearingEntry(error)) continue;
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
if (stateStat.isSymbolicLink() || !stateStat.isFile()) {
|
|
508
|
-
throw new Error(`unsafe preview phase outbox path: ${statePath}`);
|
|
509
|
-
}
|
|
510
|
-
bytes += stateStat.size;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return { bytes, directories };
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function assertRootDirectoryQuota() {
|
|
517
|
-
const usage = inspectRootUsage();
|
|
518
|
-
if (
|
|
519
|
-
!fileSystem.existsSync(runDir) &&
|
|
520
|
-
usage.directories >= maxRootDirectories
|
|
521
|
-
) {
|
|
522
|
-
throw rootQuotaError(
|
|
523
|
-
`preview phase root directory quota reached ${maxRootDirectories}`
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function assertRootByteQuota(recordBytes) {
|
|
529
|
-
const usage = inspectRootUsage();
|
|
530
|
-
if (usage.bytes + recordBytes > maxRootBytes) {
|
|
531
|
-
throw rootQuotaError(
|
|
532
|
-
`preview phase root quota would exceed ${maxRootBytes} bytes`
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function readRegularJSON(filePath, description) {
|
|
538
|
-
const stat = fileSystem.lstatSync(filePath);
|
|
539
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
540
|
-
throw new Error(`unsafe preview phase outbox path: ${filePath}`);
|
|
541
|
-
}
|
|
542
|
-
const value = JSON.parse(fileSystem.readFileSync(filePath, 'utf8'));
|
|
543
|
-
if (!value || typeof value !== 'object') {
|
|
544
|
-
throw new Error(`invalid preview phase ${description}: ${filePath}`);
|
|
545
|
-
}
|
|
546
|
-
return value;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function syncDirectory(directoryPath) {
|
|
550
|
-
const flags =
|
|
551
|
-
fs.constants.O_RDONLY |
|
|
552
|
-
(fs.constants.O_DIRECTORY || 0) |
|
|
553
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
554
|
-
const fd = fileSystem.openSync(directoryPath, flags);
|
|
555
|
-
try {
|
|
556
|
-
fileSystem.fsyncSync(fd);
|
|
557
|
-
} finally {
|
|
558
|
-
fileSystem.closeSync(fd);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function syncRunDirectory() {
|
|
563
|
-
syncDirectory(runDir);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function syncRootDirectory() {
|
|
567
|
-
const flags =
|
|
568
|
-
fs.constants.O_RDONLY |
|
|
569
|
-
(fs.constants.O_DIRECTORY || 0) |
|
|
570
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
571
|
-
const fd = fileSystem.openSync(root, flags);
|
|
572
|
-
try {
|
|
573
|
-
fileSystem.fsyncSync(fd);
|
|
574
|
-
} finally {
|
|
575
|
-
fileSystem.closeSync(fd);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function writeAll(fd, payload) {
|
|
580
|
-
let offset = 0;
|
|
581
|
-
while (offset < payload.length) {
|
|
582
|
-
const written = fileSystem.writeSync(
|
|
583
|
-
fd,
|
|
584
|
-
payload,
|
|
585
|
-
offset,
|
|
586
|
-
payload.length - offset
|
|
587
|
-
);
|
|
588
|
-
if (!Number.isInteger(written) || written <= 0) {
|
|
589
|
-
throw new Error('preview phase state write made no progress');
|
|
590
|
-
}
|
|
591
|
-
offset += written;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function writeDurableTempFile(tempPath, payload) {
|
|
596
|
-
let fd = null;
|
|
597
|
-
try {
|
|
598
|
-
const flags =
|
|
599
|
-
fs.constants.O_CREAT |
|
|
600
|
-
fs.constants.O_EXCL |
|
|
601
|
-
fs.constants.O_WRONLY |
|
|
602
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
603
|
-
fd = fileSystem.openSync(tempPath, flags, 0o600);
|
|
604
|
-
writeAll(fd, payload);
|
|
605
|
-
fileSystem.fsyncSync(fd);
|
|
606
|
-
} finally {
|
|
607
|
-
if (fd != null) {
|
|
608
|
-
fileSystem.closeSync(fd);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function publishImmutableState(filePath, label, value) {
|
|
614
|
-
const tempPath = path.join(
|
|
615
|
-
runDir,
|
|
616
|
-
`.${label}.${process.pid}.${writerInstanceID}.tmp`
|
|
617
|
-
);
|
|
618
|
-
let fd = null;
|
|
619
|
-
try {
|
|
620
|
-
const flags =
|
|
621
|
-
fs.constants.O_CREAT |
|
|
622
|
-
fs.constants.O_EXCL |
|
|
623
|
-
fs.constants.O_WRONLY |
|
|
624
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
625
|
-
fd = fileSystem.openSync(tempPath, flags, 0o600);
|
|
626
|
-
writeAll(fd, Buffer.from(`${JSON.stringify(value)}\n`, 'utf8'));
|
|
627
|
-
fileSystem.fsyncSync(fd);
|
|
628
|
-
fileSystem.closeSync(fd);
|
|
629
|
-
fd = null;
|
|
630
|
-
fileSystem.linkSync(tempPath, filePath);
|
|
631
|
-
syncRunDirectory();
|
|
632
|
-
} finally {
|
|
633
|
-
if (fd != null) {
|
|
634
|
-
try {
|
|
635
|
-
fileSystem.closeSync(fd);
|
|
636
|
-
} catch {
|
|
637
|
-
// Best-effort fd cleanup.
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
try {
|
|
641
|
-
fileSystem.rmSync(tempPath, { force: true });
|
|
642
|
-
} catch {
|
|
643
|
-
// A stale temp file is harmless and never consumed by the reader.
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function producerMarkerConflict() {
|
|
649
|
-
const error = new Error(
|
|
650
|
-
`preview phase producer epoch already belongs to another writer: ${producerEpoch}`
|
|
651
|
-
);
|
|
652
|
-
error.code = 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT';
|
|
653
|
-
return error;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function validateProducerMarker(marker) {
|
|
657
|
-
const terminal = marker?.state === 'terminal';
|
|
658
|
-
if (
|
|
659
|
-
marker?.protocol !== 'preview-phase-producer-epoch-v1' ||
|
|
660
|
-
marker.schema_version !== 1 ||
|
|
661
|
-
marker.epoch !== producerEpoch ||
|
|
662
|
-
!RUN_ID_PATTERN.test(marker.run_id || '') ||
|
|
663
|
-
!STATE_ID_PATTERN.test(marker.generation || '') ||
|
|
664
|
-
!STATE_ID_PATTERN.test(marker.writer_instance_id || '') ||
|
|
665
|
-
!Number.isSafeInteger(marker.pid) ||
|
|
666
|
-
marker.pid <= 0 ||
|
|
667
|
-
!PROCESS_START_TICKS_PATTERN.test(marker.process_start_ticks || '') ||
|
|
668
|
-
!Number.isFinite(marker.started_at_ms) ||
|
|
669
|
-
marker.started_at_ms <= 0 ||
|
|
670
|
-
(marker.state !== 'active' && !terminal) ||
|
|
671
|
-
(terminal &&
|
|
672
|
-
(!Number.isFinite(marker.terminal_at_ms) ||
|
|
673
|
-
marker.terminal_at_ms < marker.started_at_ms)) ||
|
|
674
|
-
(!terminal && marker.terminal_at_ms !== undefined)
|
|
675
|
-
) {
|
|
676
|
-
throw new Error(
|
|
677
|
-
`invalid preview phase producer epoch marker: ${producerEpochMarkerPath}`
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
return marker;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function markerBelongsToThisWriter(marker) {
|
|
684
|
-
return (
|
|
685
|
-
marker.epoch === producerEpoch &&
|
|
686
|
-
marker.run_id === options.run_id &&
|
|
687
|
-
marker.generation === metadata.generation &&
|
|
688
|
-
marker.writer_instance_id === writerInstanceID &&
|
|
689
|
-
marker.pid === process.pid &&
|
|
690
|
-
(marker.process_start_ticks || '') === (currentProcessStartTicks || '')
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function readAndValidateProducerMarker() {
|
|
695
|
-
return validateProducerMarker(
|
|
696
|
-
readRegularJSON(producerEpochMarkerPath, 'producer epoch marker')
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function buildProducerMarker(state) {
|
|
701
|
-
return {
|
|
702
|
-
protocol: 'preview-phase-producer-epoch-v1',
|
|
703
|
-
schema_version: 1,
|
|
704
|
-
epoch: producerEpoch,
|
|
705
|
-
run_id: options.run_id,
|
|
706
|
-
generation: metadata.generation,
|
|
707
|
-
writer_instance_id: writerInstanceID,
|
|
708
|
-
pid: process.pid,
|
|
709
|
-
process_start_ticks: currentProcessStartTicks,
|
|
710
|
-
started_at_ms: producerStartedAtMs,
|
|
711
|
-
state,
|
|
712
|
-
...(state === 'terminal' ? { terminal_at_ms: producerTerminalAtMs } : {}),
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function publishActiveProducerMarker() {
|
|
717
|
-
if (producerMarkerActive) return;
|
|
718
|
-
if (
|
|
719
|
-
!producerRegistrationDurable ||
|
|
720
|
-
!metadata ||
|
|
721
|
-
!ownsSelectedWriterLock()
|
|
722
|
-
) {
|
|
723
|
-
throw new Error(
|
|
724
|
-
'preview phase producer marker requires authoritative registration'
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
const existing = readAndValidateProducerMarker();
|
|
728
|
-
if (!markerBelongsToThisWriter(existing) || existing.state !== 'active') {
|
|
729
|
-
throw producerMarkerConflict();
|
|
730
|
-
}
|
|
731
|
-
producerStartedAtMs = existing.started_at_ms;
|
|
732
|
-
producerMarkerActive = true;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function publishTerminalProducerMarker() {
|
|
736
|
-
if (producerMarkerTerminal) return;
|
|
737
|
-
const existing = readAndValidateProducerMarker();
|
|
738
|
-
if (!markerBelongsToThisWriter(existing)) {
|
|
739
|
-
throw producerMarkerConflict();
|
|
740
|
-
}
|
|
741
|
-
producerStartedAtMs = existing.started_at_ms;
|
|
742
|
-
if (existing.state === 'terminal') {
|
|
743
|
-
producerTerminalAtMs = existing.terminal_at_ms;
|
|
744
|
-
producerMarkerActive = true;
|
|
745
|
-
producerMarkerTerminal = true;
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
producerTerminalAtMs =
|
|
749
|
-
producerTerminalAtMs ?? Math.max(now(), producerStartedAtMs);
|
|
750
|
-
const marker = buildProducerMarker('terminal');
|
|
751
|
-
const tempPath = path.join(
|
|
752
|
-
root,
|
|
753
|
-
`.producer-terminal.${producerEpoch}.${process.pid}.${writerInstanceID}.tmp`
|
|
754
|
-
);
|
|
755
|
-
let fd = null;
|
|
756
|
-
try {
|
|
757
|
-
const flags =
|
|
758
|
-
fs.constants.O_CREAT |
|
|
759
|
-
fs.constants.O_EXCL |
|
|
760
|
-
fs.constants.O_WRONLY |
|
|
761
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
762
|
-
fd = fileSystem.openSync(tempPath, flags, 0o600);
|
|
763
|
-
writeAll(fd, Buffer.from(`${JSON.stringify(marker)}\n`, 'utf8'));
|
|
764
|
-
fileSystem.fsyncSync(fd);
|
|
765
|
-
fileSystem.closeSync(fd);
|
|
766
|
-
fd = null;
|
|
767
|
-
fileSystem.renameSync(tempPath, producerEpochMarkerPath);
|
|
768
|
-
syncRootDirectory();
|
|
769
|
-
producerMarkerActive = true;
|
|
770
|
-
producerMarkerTerminal = true;
|
|
771
|
-
} finally {
|
|
772
|
-
if (fd != null) {
|
|
773
|
-
try {
|
|
774
|
-
fileSystem.closeSync(fd);
|
|
775
|
-
} catch {
|
|
776
|
-
// Best-effort fd cleanup.
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
let tempRemoved = false;
|
|
780
|
-
try {
|
|
781
|
-
fileSystem.rmSync(tempPath, { force: true });
|
|
782
|
-
tempRemoved = true;
|
|
783
|
-
} catch {
|
|
784
|
-
// A stale temp file is never accepted as lifecycle proof.
|
|
785
|
-
}
|
|
786
|
-
if (tempRemoved) syncRootDirectory();
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function readAndValidateMetadataAt(directoryPath) {
|
|
791
|
-
const candidateMetaPath = path.join(directoryPath, 'meta.json');
|
|
792
|
-
const stat = fileSystem.lstatSync(candidateMetaPath);
|
|
793
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
794
|
-
throw new Error(`unsafe preview phase outbox path: ${candidateMetaPath}`);
|
|
795
|
-
}
|
|
796
|
-
fileSystem.chmodSync(candidateMetaPath, 0o600);
|
|
797
|
-
const metadata = JSON.parse(
|
|
798
|
-
fileSystem.readFileSync(candidateMetaPath, 'utf8')
|
|
799
|
-
);
|
|
800
|
-
if (
|
|
801
|
-
metadata.protocol !== 'preview-phase-outbox-v1' ||
|
|
802
|
-
metadata.schema_version !== 1 ||
|
|
803
|
-
metadata.producer_epoch !== producerEpoch ||
|
|
804
|
-
metadata.run_id !== options.run_id ||
|
|
805
|
-
!/^[A-Za-z0-9_-]{1,128}$/.test(metadata.generation || '') ||
|
|
806
|
-
!Number.isFinite(metadata.created_at_ms) ||
|
|
807
|
-
metadata.created_at_ms <= 0 ||
|
|
808
|
-
(metadata.quiesce_supported !== undefined &&
|
|
809
|
-
metadata.quiesce_supported !== true)
|
|
810
|
-
) {
|
|
811
|
-
throw new Error(
|
|
812
|
-
`invalid preview phase outbox metadata: ${candidateMetaPath}`
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
return metadata;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function readAndValidateMetadata() {
|
|
819
|
-
return readAndValidateMetadataAt(runDir);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function ensureMetadata() {
|
|
823
|
-
if (metadata) return;
|
|
824
|
-
if (fileSystem.existsSync(metaPath)) {
|
|
825
|
-
metadata = readAndValidateMetadata();
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
const candidate = {
|
|
829
|
-
protocol: 'preview-phase-outbox-v1',
|
|
830
|
-
schema_version: 1,
|
|
831
|
-
producer_epoch: producerEpoch,
|
|
832
|
-
generation: randomUUID(),
|
|
833
|
-
run_id: options.run_id,
|
|
834
|
-
created_at_ms: now(),
|
|
835
|
-
quiesce_supported: true,
|
|
836
|
-
};
|
|
837
|
-
const tempPath = path.join(
|
|
838
|
-
runDir,
|
|
839
|
-
`.meta.${process.pid}.${candidate.generation}.tmp`
|
|
840
|
-
);
|
|
841
|
-
try {
|
|
842
|
-
writeDurableTempFile(
|
|
843
|
-
tempPath,
|
|
844
|
-
Buffer.from(`${JSON.stringify(candidate)}\n`, 'utf8')
|
|
845
|
-
);
|
|
846
|
-
try {
|
|
847
|
-
fileSystem.linkSync(tempPath, metaPath);
|
|
848
|
-
syncRunDirectory();
|
|
849
|
-
} catch (error) {
|
|
850
|
-
if (error?.code !== 'EEXIST') throw error;
|
|
851
|
-
}
|
|
852
|
-
} finally {
|
|
853
|
-
let removed = false;
|
|
854
|
-
try {
|
|
855
|
-
fileSystem.rmSync(tempPath, { force: true });
|
|
856
|
-
removed = true;
|
|
857
|
-
} catch {
|
|
858
|
-
// A stale temp file is harmless and never consumed by the reader.
|
|
859
|
-
}
|
|
860
|
-
if (removed) syncRunDirectory();
|
|
861
|
-
}
|
|
862
|
-
metadata = readAndValidateMetadata();
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function validateFallbackIntent(intent, canonicalMetadata) {
|
|
866
|
-
if (
|
|
867
|
-
intent.protocol !== 'preview-phase-fallback-intent-v1' ||
|
|
868
|
-
intent.schema_version !== 1 ||
|
|
869
|
-
intent.producer_epoch !== producerEpoch ||
|
|
870
|
-
intent.run_id !== options.run_id ||
|
|
871
|
-
intent.generation !== canonicalMetadata.generation ||
|
|
872
|
-
!Number.isFinite(intent.created_at_ms) ||
|
|
873
|
-
intent.created_at_ms <= 0 ||
|
|
874
|
-
!STATE_ID_PATTERN.test(intent.reason || '')
|
|
875
|
-
) {
|
|
876
|
-
throw new Error(
|
|
877
|
-
`invalid preview phase fallback intent: ${path.join(
|
|
878
|
-
canonicalRunDir,
|
|
879
|
-
'fallback.intent.json'
|
|
880
|
-
)}`
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
return intent;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function fallbackSpoolError(code, message, cause) {
|
|
887
|
-
const error = new Error(message, cause == null ? undefined : { cause });
|
|
888
|
-
error.code = code;
|
|
889
|
-
return error;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function encodeFallbackRecord(record, description) {
|
|
893
|
-
const payload = JSON.stringify(record);
|
|
894
|
-
if (typeof payload !== 'string') {
|
|
895
|
-
throw fallbackSpoolError(
|
|
896
|
-
'PREVIEW_PHASE_FALLBACK_RECORD_INVALID',
|
|
897
|
-
`invalid preview phase fallback ${description}`
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
const encoded = Buffer.from(`${payload}\n`, 'utf8');
|
|
901
|
-
if (encoded.length > maxFallbackRecordBytes) {
|
|
902
|
-
throw fallbackSpoolError(
|
|
903
|
-
'PREVIEW_PHASE_FALLBACK_RECORD_TOO_LARGE',
|
|
904
|
-
`preview phase fallback ${description} exceeds ${maxFallbackRecordBytes} bytes`
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
return encoded;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
function fallbackTimestamp() {
|
|
911
|
-
const timestamp = now();
|
|
912
|
-
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
|
913
|
-
throw fallbackSpoolError(
|
|
914
|
-
'PREVIEW_PHASE_FALLBACK_TIMESTAMP_INVALID',
|
|
915
|
-
'invalid preview phase fallback timestamp'
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
return timestamp;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function closeFallbackFD() {
|
|
922
|
-
if (fallbackFD == null) return;
|
|
923
|
-
try {
|
|
924
|
-
fileSystem.closeSync(fallbackFD);
|
|
925
|
-
} catch {
|
|
926
|
-
// The durable boundary is fdatasync, not close. Never reuse this fd.
|
|
927
|
-
}
|
|
928
|
-
fallbackFD = null;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function ensureFallbackSpool(initialEventBytes) {
|
|
932
|
-
if (fallbackFD != null) return;
|
|
933
|
-
if (fallbackSealed) {
|
|
934
|
-
throw fallbackSpoolError(
|
|
935
|
-
'PREVIEW_PHASE_FALLBACK_SEALED',
|
|
936
|
-
'preview phase fallback spool is sealed'
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
if (fallbackFailed) {
|
|
940
|
-
throw fallbackSpoolError(
|
|
941
|
-
'PREVIEW_PHASE_FALLBACK_DEGRADED',
|
|
942
|
-
'preview phase fallback spool is degraded'
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
assertSafeDirectory(fileSystem, root);
|
|
947
|
-
assertExistingSafeDirectory(fileSystem, canonicalRunDir);
|
|
948
|
-
const parentMetadata = readAndValidateMetadataAt(canonicalRunDir);
|
|
949
|
-
const entries = readDirectoryEntriesBounded(
|
|
950
|
-
canonicalRunDir,
|
|
951
|
-
maxRunScanEntries,
|
|
952
|
-
`preview phase run scan exceeds ${maxRunScanEntries} entries: ${canonicalRunDir}`
|
|
953
|
-
);
|
|
954
|
-
const fallbackFiles = entries.filter(entry =>
|
|
955
|
-
/^fallback-pfs_[a-f0-9]{32}\.jsonl$/.test(entry.name)
|
|
956
|
-
);
|
|
957
|
-
if (fallbackFiles.length >= maxFallbackFiles) {
|
|
958
|
-
throw fallbackSpoolError(
|
|
959
|
-
'PREVIEW_PHASE_FALLBACK_FILE_LIMIT',
|
|
960
|
-
`preview phase fallback file limit reached ${maxFallbackFiles}`
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
if (fileSystem.existsSync(fallbackSpoolPath)) {
|
|
964
|
-
throw fallbackSpoolError(
|
|
965
|
-
'PREVIEW_PHASE_FALLBACK_SESSION_CONFLICT',
|
|
966
|
-
`preview phase fallback session already exists: ${fallbackSessionID}`
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const createdAt = fallbackTimestamp();
|
|
971
|
-
const metaRecord = encodeFallbackRecord(
|
|
972
|
-
{
|
|
973
|
-
protocol: 'preview-phase-fallback-spool-v1',
|
|
974
|
-
schema_version: 1,
|
|
975
|
-
record_type: 'meta',
|
|
976
|
-
session_id: fallbackSessionID,
|
|
977
|
-
producer_epoch: producerEpoch,
|
|
978
|
-
run_id: options.run_id,
|
|
979
|
-
parent_generation: parentMetadata.generation,
|
|
980
|
-
producer_instance_id: producerInstanceID,
|
|
981
|
-
pid: process.pid,
|
|
982
|
-
process_start_ticks: currentProcessStartTicks,
|
|
983
|
-
// Protocol timestamps use Unix epoch milliseconds. The compact field
|
|
984
|
-
// name is retained because this is a versioned wire contract.
|
|
985
|
-
created_at: createdAt,
|
|
986
|
-
},
|
|
987
|
-
'meta record'
|
|
988
|
-
);
|
|
989
|
-
if (
|
|
990
|
-
metaRecord.length + initialEventBytes + FALLBACK_TERMINAL_RESERVE_BYTES >
|
|
991
|
-
maxFallbackBytes
|
|
992
|
-
) {
|
|
993
|
-
throw fallbackSpoolError(
|
|
994
|
-
'PREVIEW_PHASE_FALLBACK_FILE_QUOTA_EXCEEDED',
|
|
995
|
-
`preview phase fallback file quota would exceed ${maxFallbackBytes} bytes`
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
assertRootByteQuota(
|
|
999
|
-
metaRecord.length + initialEventBytes + FALLBACK_TERMINAL_RESERVE_BYTES
|
|
1000
|
-
);
|
|
1001
|
-
|
|
1002
|
-
const tempPath = path.join(
|
|
1003
|
-
canonicalRunDir,
|
|
1004
|
-
`.fallback-${fallbackSessionID}.${process.pid}.${writerInstanceID}.tmp`
|
|
1005
|
-
);
|
|
1006
|
-
const directoryFlags =
|
|
1007
|
-
fs.constants.O_RDONLY |
|
|
1008
|
-
(fs.constants.O_DIRECTORY || 0) |
|
|
1009
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
1010
|
-
let directoryFD = null;
|
|
1011
|
-
let fd = null;
|
|
1012
|
-
let published = false;
|
|
1013
|
-
try {
|
|
1014
|
-
directoryFD = fileSystem.openSync(canonicalRunDir, directoryFlags);
|
|
1015
|
-
const fileFlags =
|
|
1016
|
-
fs.constants.O_CREAT |
|
|
1017
|
-
fs.constants.O_EXCL |
|
|
1018
|
-
fs.constants.O_WRONLY |
|
|
1019
|
-
fs.constants.O_APPEND |
|
|
1020
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
1021
|
-
fd = fileSystem.openSync(tempPath, fileFlags, 0o600);
|
|
1022
|
-
fileSystem.fchmodSync(fd, 0o600);
|
|
1023
|
-
writeAll(fd, metaRecord);
|
|
1024
|
-
fileSystem.fsyncSync(fd);
|
|
1025
|
-
try {
|
|
1026
|
-
fileSystem.linkSync(tempPath, fallbackSpoolPath);
|
|
1027
|
-
} catch (error) {
|
|
1028
|
-
if (error?.code === 'EEXIST') {
|
|
1029
|
-
throw fallbackSpoolError(
|
|
1030
|
-
'PREVIEW_PHASE_FALLBACK_SESSION_CONFLICT',
|
|
1031
|
-
`preview phase fallback session already exists: ${fallbackSessionID}`,
|
|
1032
|
-
error
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
throw error;
|
|
1036
|
-
}
|
|
1037
|
-
// Sync the already-open directory, so quarantine rename after link does
|
|
1038
|
-
// not invalidate the durability proof for the published file name.
|
|
1039
|
-
fileSystem.fsyncSync(directoryFD);
|
|
1040
|
-
published = true;
|
|
1041
|
-
try {
|
|
1042
|
-
fileSystem.rmSync(tempPath);
|
|
1043
|
-
fileSystem.fsyncSync(directoryFD);
|
|
1044
|
-
} catch {
|
|
1045
|
-
// A quarantine rename can make the old path unreachable. The hidden
|
|
1046
|
-
// hard-link alias is harmless; the published session remains bounded.
|
|
1047
|
-
}
|
|
1048
|
-
fallbackFD = fd;
|
|
1049
|
-
fd = null;
|
|
1050
|
-
fallbackBytes = metaRecord.length;
|
|
1051
|
-
fallbackStarted = true;
|
|
1052
|
-
fallbackCreatedAt = createdAt;
|
|
1053
|
-
} catch (error) {
|
|
1054
|
-
fallbackFailed = true;
|
|
1055
|
-
throw error;
|
|
1056
|
-
} finally {
|
|
1057
|
-
if (fd != null) {
|
|
1058
|
-
try {
|
|
1059
|
-
fileSystem.closeSync(fd);
|
|
1060
|
-
} catch {
|
|
1061
|
-
// Best-effort fd cleanup.
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (!published) {
|
|
1065
|
-
try {
|
|
1066
|
-
fileSystem.rmSync(tempPath, { force: true });
|
|
1067
|
-
if (directoryFD != null) fileSystem.fsyncSync(directoryFD);
|
|
1068
|
-
} catch {
|
|
1069
|
-
// Best-effort temp cleanup after a failed publication.
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
if (directoryFD != null) {
|
|
1073
|
-
try {
|
|
1074
|
-
fileSystem.closeSync(directoryFD);
|
|
1075
|
-
} catch {
|
|
1076
|
-
// Best-effort directory-fd cleanup.
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function appendFallback(event) {
|
|
1083
|
-
if (fallbackSealed) {
|
|
1084
|
-
throw fallbackSpoolError(
|
|
1085
|
-
'PREVIEW_PHASE_FALLBACK_SEALED',
|
|
1086
|
-
'preview phase fallback spool is sealed'
|
|
1087
|
-
);
|
|
1088
|
-
}
|
|
1089
|
-
if (fallbackFailed) {
|
|
1090
|
-
throw fallbackSpoolError(
|
|
1091
|
-
'PREVIEW_PHASE_FALLBACK_DEGRADED',
|
|
1092
|
-
'preview phase fallback spool is degraded'
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
1096
|
-
throw fallbackSpoolError(
|
|
1097
|
-
'PREVIEW_PHASE_FALLBACK_RECORD_INVALID',
|
|
1098
|
-
'invalid preview phase fallback event'
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
if (fallbackSeq >= maxFallbackEvents) {
|
|
1102
|
-
throw fallbackSpoolError(
|
|
1103
|
-
'PREVIEW_PHASE_FALLBACK_EVENT_LIMIT',
|
|
1104
|
-
`preview phase fallback event limit reached ${maxFallbackEvents}`
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
const nextSeq = fallbackSeq + 1;
|
|
1108
|
-
const fallbackEvent = {
|
|
1109
|
-
...event,
|
|
1110
|
-
transport: 'fallback_spool',
|
|
1111
|
-
fallback_session_id: fallbackSessionID,
|
|
1112
|
-
fallback_seq: nextSeq,
|
|
1113
|
-
};
|
|
1114
|
-
const eventPayload = JSON.stringify(fallbackEvent);
|
|
1115
|
-
if (
|
|
1116
|
-
typeof eventPayload !== 'string' ||
|
|
1117
|
-
Buffer.byteLength(eventPayload, 'utf8') > MAX_EVENT_BYTES
|
|
1118
|
-
) {
|
|
1119
|
-
throw fallbackSpoolError(
|
|
1120
|
-
'PREVIEW_PHASE_EVENT_TOO_LARGE',
|
|
1121
|
-
`preview phase event exceeds ${MAX_EVENT_BYTES} bytes`
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
const record = encodeFallbackRecord(
|
|
1125
|
-
{
|
|
1126
|
-
protocol: 'preview-phase-fallback-spool-v1',
|
|
1127
|
-
schema_version: 1,
|
|
1128
|
-
record_type: 'event',
|
|
1129
|
-
session_id: fallbackSessionID,
|
|
1130
|
-
seq: nextSeq,
|
|
1131
|
-
event: fallbackEvent,
|
|
1132
|
-
},
|
|
1133
|
-
'event record'
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
ensureFallbackSpool(record.length);
|
|
1137
|
-
if (
|
|
1138
|
-
fallbackBytes + record.length + FALLBACK_TERMINAL_RESERVE_BYTES >
|
|
1139
|
-
maxFallbackBytes
|
|
1140
|
-
) {
|
|
1141
|
-
throw fallbackSpoolError(
|
|
1142
|
-
'PREVIEW_PHASE_FALLBACK_FILE_QUOTA_EXCEEDED',
|
|
1143
|
-
`preview phase fallback file quota would exceed ${maxFallbackBytes} bytes`
|
|
1144
|
-
);
|
|
1145
|
-
}
|
|
1146
|
-
try {
|
|
1147
|
-
writeAll(fallbackFD, record);
|
|
1148
|
-
fileSystem.fdatasyncSync(fallbackFD);
|
|
1149
|
-
fallbackBytes += record.length;
|
|
1150
|
-
fallbackSeq = nextSeq;
|
|
1151
|
-
fallbackRequired = true;
|
|
1152
|
-
return true;
|
|
1153
|
-
} catch (error) {
|
|
1154
|
-
fallbackFailed = true;
|
|
1155
|
-
closeFallbackFD();
|
|
1156
|
-
throw error;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function sealFallback() {
|
|
1161
|
-
if (!fallbackStarted) return !fallbackFailed;
|
|
1162
|
-
if (fallbackSealed) return true;
|
|
1163
|
-
if (fallbackFailed || fallbackFD == null) return false;
|
|
1164
|
-
const terminal = encodeFallbackRecord(
|
|
1165
|
-
{
|
|
1166
|
-
protocol: 'preview-phase-fallback-spool-v1',
|
|
1167
|
-
schema_version: 1,
|
|
1168
|
-
record_type: 'terminal',
|
|
1169
|
-
session_id: fallbackSessionID,
|
|
1170
|
-
final_cursor: fallbackBytes,
|
|
1171
|
-
final_seq: fallbackSeq,
|
|
1172
|
-
// Unix epoch milliseconds; never precedes this session's meta record.
|
|
1173
|
-
sealed_at: Math.max(fallbackTimestamp(), fallbackCreatedAt),
|
|
1174
|
-
recovered: false,
|
|
1175
|
-
},
|
|
1176
|
-
'terminal record'
|
|
1177
|
-
);
|
|
1178
|
-
if (fallbackBytes + terminal.length > maxFallbackBytes) {
|
|
1179
|
-
fallbackFailed = true;
|
|
1180
|
-
closeFallbackFD();
|
|
1181
|
-
return false;
|
|
1182
|
-
}
|
|
1183
|
-
try {
|
|
1184
|
-
writeAll(fallbackFD, terminal);
|
|
1185
|
-
fileSystem.fdatasyncSync(fallbackFD);
|
|
1186
|
-
fallbackBytes += terminal.length;
|
|
1187
|
-
fallbackSealed = true;
|
|
1188
|
-
closeFallbackFD();
|
|
1189
|
-
return true;
|
|
1190
|
-
} catch {
|
|
1191
|
-
fallbackFailed = true;
|
|
1192
|
-
closeFallbackFD();
|
|
1193
|
-
return false;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
function validateProcessIdentityState(state, description) {
|
|
1198
|
-
if (
|
|
1199
|
-
!Number.isSafeInteger(state.pid) ||
|
|
1200
|
-
state.pid <= 0 ||
|
|
1201
|
-
(state.process_start_ticks !== undefined &&
|
|
1202
|
-
!PROCESS_START_TICKS_PATTERN.test(state.process_start_ticks))
|
|
1203
|
-
) {
|
|
1204
|
-
throw new Error(`invalid preview phase ${description}`);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
function validateWriterLock(lock) {
|
|
1209
|
-
assertStateGeneration(lock, 'writer lock');
|
|
1210
|
-
validateProcessIdentityState(lock, 'writer lock');
|
|
1211
|
-
if (
|
|
1212
|
-
lock.protocol !== 'preview-phase-writer-v1' ||
|
|
1213
|
-
lock.schema_version !== 1 ||
|
|
1214
|
-
!STATE_ID_PATTERN.test(lock.writer_instance_id || '') ||
|
|
1215
|
-
!Number.isFinite(lock.created_at_ms) ||
|
|
1216
|
-
lock.created_at_ms <= 0
|
|
1217
|
-
) {
|
|
1218
|
-
throw new Error(`invalid preview phase writer lock: ${writerLockPath}`);
|
|
1219
|
-
}
|
|
1220
|
-
return lock;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function writerBusyError() {
|
|
1224
|
-
const busy = new Error(
|
|
1225
|
-
`preview phase generation already has an active writer: ${options.run_id}`
|
|
1226
|
-
);
|
|
1227
|
-
busy.code = 'PREVIEW_PHASE_WRITER_BUSY';
|
|
1228
|
-
return busy;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
function runConflictError(cause) {
|
|
1232
|
-
const conflict = new Error(
|
|
1233
|
-
`preview phase canonical run was published by another writer: ${options.run_id}`,
|
|
1234
|
-
{ cause }
|
|
1235
|
-
);
|
|
1236
|
-
conflict.code = 'PREVIEW_PHASE_RUN_CONFLICT';
|
|
1237
|
-
return conflict;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function acquireWriterLock() {
|
|
1241
|
-
if (ownsSelectedWriterLock()) return;
|
|
1242
|
-
if (writerLockOwned) {
|
|
1243
|
-
throw new Error(
|
|
1244
|
-
'preview phase writer ownership belongs to another directory'
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
1247
|
-
const lock = {
|
|
1248
|
-
protocol: 'preview-phase-writer-v1',
|
|
1249
|
-
schema_version: 1,
|
|
1250
|
-
run_id: options.run_id,
|
|
1251
|
-
generation: metadata.generation,
|
|
1252
|
-
writer_instance_id: writerInstanceID,
|
|
1253
|
-
pid: process.pid,
|
|
1254
|
-
...(currentProcessStartTicks == null
|
|
1255
|
-
? {}
|
|
1256
|
-
: { process_start_ticks: currentProcessStartTicks }),
|
|
1257
|
-
created_at_ms: now(),
|
|
1258
|
-
};
|
|
1259
|
-
const tempPath = path.join(
|
|
1260
|
-
runDir,
|
|
1261
|
-
`.writer.${process.pid}.${writerInstanceID}.tmp`
|
|
1262
|
-
);
|
|
1263
|
-
try {
|
|
1264
|
-
writeDurableTempFile(
|
|
1265
|
-
tempPath,
|
|
1266
|
-
Buffer.from(`${JSON.stringify(lock)}\n`, 'utf8')
|
|
1267
|
-
);
|
|
1268
|
-
try {
|
|
1269
|
-
fileSystem.linkSync(tempPath, writerLockPath);
|
|
1270
|
-
writerLockOwned = true;
|
|
1271
|
-
writerLockDirectory = runDir;
|
|
1272
|
-
if (runDir === canonicalRunDir) everOwnedCanonicalWriter = true;
|
|
1273
|
-
syncRunDirectory();
|
|
1274
|
-
} catch (error) {
|
|
1275
|
-
if (error?.code !== 'EEXIST') throw error;
|
|
1276
|
-
// Producers never delete or replace a lock they did not publish. Only
|
|
1277
|
-
// the shell-server can prove owner death and finalize the generation;
|
|
1278
|
-
// this removes path-ABA races between concurrent JS successors.
|
|
1279
|
-
validateWriterLock(readRegularJSON(writerLockPath, 'writer lock'));
|
|
1280
|
-
throw writerBusyError();
|
|
1281
|
-
}
|
|
1282
|
-
} finally {
|
|
1283
|
-
let removed = false;
|
|
1284
|
-
try {
|
|
1285
|
-
fileSystem.rmSync(tempPath, { force: true });
|
|
1286
|
-
removed = true;
|
|
1287
|
-
} catch {
|
|
1288
|
-
// A stale temp file is harmless and never consumed by the reader.
|
|
1289
|
-
}
|
|
1290
|
-
if (removed) syncRunDirectory();
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
function ensureEmptyEventsFile() {
|
|
1295
|
-
if (fileSystem.existsSync(eventsPath)) {
|
|
1296
|
-
const stat = fileSystem.lstatSync(eventsPath);
|
|
1297
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1298
|
-
throw new Error(`unsafe preview phase outbox path: ${eventsPath}`);
|
|
1299
|
-
}
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
const flags =
|
|
1303
|
-
fs.constants.O_CREAT |
|
|
1304
|
-
fs.constants.O_EXCL |
|
|
1305
|
-
fs.constants.O_RDWR |
|
|
1306
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
1307
|
-
const fd = fileSystem.openSync(eventsPath, flags, 0o600);
|
|
1308
|
-
try {
|
|
1309
|
-
fileSystem.fchmodSync(fd, 0o600);
|
|
1310
|
-
fileSystem.fdatasyncSync(fd);
|
|
1311
|
-
} finally {
|
|
1312
|
-
fileSystem.closeSync(fd);
|
|
1313
|
-
}
|
|
1314
|
-
syncRunDirectory();
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
function validateOwnedWriterLock() {
|
|
1318
|
-
const lock = validateWriterLock(
|
|
1319
|
-
readRegularJSON(writerLockPath, 'writer lock')
|
|
1320
|
-
);
|
|
1321
|
-
if (
|
|
1322
|
-
lock.writer_instance_id !== writerInstanceID ||
|
|
1323
|
-
lock.pid !== process.pid ||
|
|
1324
|
-
(lock.process_start_ticks || '') !== (currentProcessStartTicks || '')
|
|
1325
|
-
) {
|
|
1326
|
-
throw writerBusyError();
|
|
1327
|
-
}
|
|
1328
|
-
writerLockOwned = true;
|
|
1329
|
-
writerLockDirectory = runDir;
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
let stagingOwned = false;
|
|
1333
|
-
|
|
1334
|
-
function tryAdoptCanonicalWriter() {
|
|
1335
|
-
const previousDirectory = runDir;
|
|
1336
|
-
const previousWriterLockOwned = writerLockOwned;
|
|
1337
|
-
const previousWriterLockDirectory = writerLockDirectory;
|
|
1338
|
-
try {
|
|
1339
|
-
selectRunDirectory(canonicalRunDir);
|
|
1340
|
-
assertExistingSafeDirectory(fileSystem, canonicalRunDir);
|
|
1341
|
-
metadata = readAndValidateMetadata();
|
|
1342
|
-
writerLockOwned = false;
|
|
1343
|
-
writerLockDirectory = null;
|
|
1344
|
-
validateOwnedWriterLock();
|
|
1345
|
-
stagingOwned = false;
|
|
1346
|
-
everOwnedCanonicalWriter = true;
|
|
1347
|
-
return true;
|
|
1348
|
-
} catch {
|
|
1349
|
-
selectRunDirectory(previousDirectory);
|
|
1350
|
-
writerLockOwned = previousWriterLockOwned;
|
|
1351
|
-
writerLockDirectory = previousWriterLockDirectory;
|
|
1352
|
-
return false;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
function ensureCanonicalPublicationDurable() {
|
|
1357
|
-
if (canonicalPublicationDurable) return;
|
|
1358
|
-
try {
|
|
1359
|
-
syncRootDirectory();
|
|
1360
|
-
} catch (firstError) {
|
|
1361
|
-
// rename may have committed even when its first parent-directory fsync
|
|
1362
|
-
// failed. Re-prove the exact writer identity, then retry that commit
|
|
1363
|
-
// point before any event or producer marker can be published.
|
|
1364
|
-
if (!tryAdoptCanonicalWriter()) throw firstError;
|
|
1365
|
-
syncRootDirectory();
|
|
1366
|
-
}
|
|
1367
|
-
canonicalPublicationDurable = true;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function registerCanonicalWriter(rejectedEvent) {
|
|
1371
|
-
if (producerRegistrationDurable) return;
|
|
1372
|
-
producerStartedAtMs = producerStartedAtMs ?? now();
|
|
1373
|
-
const rejectedEventForRegistration =
|
|
1374
|
-
rejectedEvent?.schema_version === 1 &&
|
|
1375
|
-
typeof rejectedEvent.event_id === 'string' &&
|
|
1376
|
-
rejectedEvent.event_id.startsWith(`${options.run_id}:`) &&
|
|
1377
|
-
rejectedEvent.run_id === options.run_id &&
|
|
1378
|
-
PRODUCER_INSTANCE_ID_PATTERN.test(
|
|
1379
|
-
rejectedEvent.producer_instance_id || ''
|
|
1380
|
-
) &&
|
|
1381
|
-
rejectedEvent.producer_seq === 1 &&
|
|
1382
|
-
rejectedEvent.transport === 'outbox'
|
|
1383
|
-
? rejectedEvent
|
|
1384
|
-
: null;
|
|
1385
|
-
const request = {
|
|
1386
|
-
// root/staging_name/started_at_ms are local callback context only. The
|
|
1387
|
-
// default registrar serializes the fixed protocol whitelist below and
|
|
1388
|
-
// never sends a caller-controlled path to shell-server.
|
|
1389
|
-
root,
|
|
1390
|
-
staging_name: stagingName,
|
|
1391
|
-
run_id: options.run_id,
|
|
1392
|
-
generation: metadata.generation,
|
|
1393
|
-
expected_instance_id: expectedInstanceID,
|
|
1394
|
-
producer_epoch: producerEpoch,
|
|
1395
|
-
writer_instance_id: writerInstanceID,
|
|
1396
|
-
pid: process.pid,
|
|
1397
|
-
process_start_ticks: currentProcessStartTicks,
|
|
1398
|
-
started_at_ms: producerStartedAtMs,
|
|
1399
|
-
...(rejectedEventForRegistration == null
|
|
1400
|
-
? {}
|
|
1401
|
-
: {
|
|
1402
|
-
producer_instance_id:
|
|
1403
|
-
rejectedEventForRegistration.producer_instance_id,
|
|
1404
|
-
fallback_session_id: fallbackSessionID,
|
|
1405
|
-
rejected_event: rejectedEventForRegistration,
|
|
1406
|
-
}),
|
|
1407
|
-
};
|
|
1408
|
-
if (producerRegistrationAttemptCount >= PRODUCER_REGISTER_ATTEMPTS) {
|
|
1409
|
-
throw producerRegistrationLastError;
|
|
1410
|
-
}
|
|
1411
|
-
while (producerRegistrationAttemptCount < PRODUCER_REGISTER_ATTEMPTS) {
|
|
1412
|
-
producerRegistrationAttemptCount += 1;
|
|
1413
|
-
try {
|
|
1414
|
-
const response = validateProducerRegistrationResponse(
|
|
1415
|
-
registerProducer(request),
|
|
1416
|
-
request
|
|
1417
|
-
);
|
|
1418
|
-
if (response.registration_status === 'rejected') {
|
|
1419
|
-
request.rejected_event.transport = 'fallback_spool';
|
|
1420
|
-
request.rejected_event.fallback_session_id = fallbackSessionID;
|
|
1421
|
-
request.rejected_event.fallback_seq = 1;
|
|
1422
|
-
fallbackRequired = true;
|
|
1423
|
-
fallbackSeq = 1;
|
|
1424
|
-
fallbackSealed = true;
|
|
1425
|
-
sealRequested = true;
|
|
1426
|
-
closed = true;
|
|
1427
|
-
closeResult = true;
|
|
1428
|
-
writerLockOwned = false;
|
|
1429
|
-
writerLockDirectory = null;
|
|
1430
|
-
stagingOwned = false;
|
|
1431
|
-
producerRegistrationDurable = true;
|
|
1432
|
-
selectRunDirectory(canonicalRunDir);
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
if (!tryAdoptCanonicalWriter()) {
|
|
1436
|
-
throw producerRegistrationError(
|
|
1437
|
-
'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
|
|
1438
|
-
'authoritative registration did not publish the expected canonical writer'
|
|
1439
|
-
);
|
|
1440
|
-
}
|
|
1441
|
-
// A successful authoritative response is issued only after the
|
|
1442
|
-
// canonical rename and root-directory fsync. Re-fsyncing here would
|
|
1443
|
-
// add latency without strengthening the controller's commit proof.
|
|
1444
|
-
canonicalPublicationDurable = true;
|
|
1445
|
-
const marker = readAndValidateProducerMarker();
|
|
1446
|
-
if (!markerBelongsToThisWriter(marker) || marker.state !== 'active') {
|
|
1447
|
-
throw producerRegistrationError(
|
|
1448
|
-
'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
|
|
1449
|
-
'authoritative registration did not publish the expected active marker'
|
|
1450
|
-
);
|
|
1451
|
-
}
|
|
1452
|
-
producerStartedAtMs = marker.started_at_ms;
|
|
1453
|
-
producerMarkerActive = true;
|
|
1454
|
-
producerRegistrationDurable = true;
|
|
1455
|
-
return;
|
|
1456
|
-
} catch (error) {
|
|
1457
|
-
producerRegistrationLastError = error;
|
|
1458
|
-
if (
|
|
1459
|
-
error?.code === 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT' &&
|
|
1460
|
-
tryAdoptCanonicalWriter()
|
|
1461
|
-
) {
|
|
1462
|
-
canonicalPublicationDurable = true;
|
|
1463
|
-
}
|
|
1464
|
-
if (
|
|
1465
|
-
error?.code === 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN' &&
|
|
1466
|
-
producerRegistrationAttemptCount < PRODUCER_REGISTER_ATTEMPTS
|
|
1467
|
-
) {
|
|
1468
|
-
continue;
|
|
1469
|
-
}
|
|
1470
|
-
throw error;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
throw producerRegistrationLastError;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
function ensureCanonicalWriter(rejectedEvent) {
|
|
1477
|
-
if (producerRegistrationDurable && runDir === canonicalRunDir && metadata) {
|
|
1478
|
-
ensureCanonicalPublicationDurable();
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
if (!stagingOwned && fileSystem.existsSync(canonicalRunDir)) {
|
|
1483
|
-
// Existing canonical state is controller-owned. A JS successor may
|
|
1484
|
-
// inspect it to produce a precise fail-closed reason, but it must never
|
|
1485
|
-
// acquire/remove its writer lock or republish it under a captured epoch.
|
|
1486
|
-
selectRunDirectory(canonicalRunDir);
|
|
1487
|
-
assertExistingSafeDirectory(fileSystem, canonicalRunDir);
|
|
1488
|
-
metadata = readAndValidateMetadata();
|
|
1489
|
-
if (fileSystem.existsSync(sealPath)) {
|
|
1490
|
-
assertGenerationIsOpen();
|
|
1491
|
-
}
|
|
1492
|
-
if (fileSystem.existsSync(writerLockPath)) {
|
|
1493
|
-
validateWriterLock(readRegularJSON(writerLockPath, 'writer lock'));
|
|
1494
|
-
throw writerBusyError();
|
|
1495
|
-
}
|
|
1496
|
-
throw runConflictError();
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
selectRunDirectory(stagingRunDir);
|
|
1500
|
-
if (!stagingOwned) {
|
|
1501
|
-
fileSystem.mkdirSync(stagingRunDir, { mode: 0o700 });
|
|
1502
|
-
stagingOwned = true;
|
|
1503
|
-
syncRootDirectory();
|
|
1504
|
-
}
|
|
1505
|
-
assertSafeDirectory(fileSystem, stagingRunDir);
|
|
1506
|
-
ensureMetadata();
|
|
1507
|
-
if (!ownsSelectedWriterLock()) acquireWriterLock();
|
|
1508
|
-
ensureEmptyEventsFile();
|
|
1509
|
-
syncRunDirectory();
|
|
1510
|
-
registerCanonicalWriter(rejectedEvent);
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
function assertStateGeneration(state, description) {
|
|
1514
|
-
if (
|
|
1515
|
-
state.run_id !== options.run_id ||
|
|
1516
|
-
state.generation !== metadata.generation
|
|
1517
|
-
) {
|
|
1518
|
-
const error = new Error(
|
|
1519
|
-
`preview phase ${description} generation mismatch: ${options.run_id}`
|
|
1520
|
-
);
|
|
1521
|
-
error.code = 'PREVIEW_PHASE_GENERATION_MISMATCH';
|
|
1522
|
-
throw error;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
function assertGenerationIsOpen() {
|
|
1527
|
-
if (!fileSystem.existsSync(sealPath)) return;
|
|
1528
|
-
readAndValidateSeal();
|
|
1529
|
-
const error = new Error(
|
|
1530
|
-
`preview phase sealed generation cannot be reopened: ${options.run_id}`
|
|
1531
|
-
);
|
|
1532
|
-
error.code = 'PREVIEW_PHASE_GENERATION_SEALED';
|
|
1533
|
-
throw error;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function readAndValidateSeal() {
|
|
1537
|
-
const seal = readRegularJSON(sealPath, 'seal');
|
|
1538
|
-
assertStateGeneration(seal, 'seal');
|
|
1539
|
-
if (
|
|
1540
|
-
seal.protocol !== 'preview-phase-seal-v1' ||
|
|
1541
|
-
seal.schema_version !== 1 ||
|
|
1542
|
-
!Number.isSafeInteger(seal.final_size_bytes) ||
|
|
1543
|
-
seal.final_size_bytes < 0 ||
|
|
1544
|
-
!Number.isFinite(seal.sealed_at_ms) ||
|
|
1545
|
-
seal.sealed_at_ms <= 0 ||
|
|
1546
|
-
(seal.fallback_required !== undefined &&
|
|
1547
|
-
typeof seal.fallback_required !== 'boolean')
|
|
1548
|
-
) {
|
|
1549
|
-
throw new Error(`invalid preview phase seal: ${sealPath}`);
|
|
1550
|
-
}
|
|
1551
|
-
return seal;
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
function observeDegradedMarker() {
|
|
1555
|
-
if (!fileSystem.existsSync(degradedPath)) return false;
|
|
1556
|
-
const marker = readRegularJSON(degradedPath, 'degraded marker');
|
|
1557
|
-
assertStateGeneration(marker, 'degraded marker');
|
|
1558
|
-
if (
|
|
1559
|
-
marker.protocol !== 'preview-phase-degraded-v1' ||
|
|
1560
|
-
marker.schema_version !== 1 ||
|
|
1561
|
-
typeof marker.first_failed_event_id !== 'string' ||
|
|
1562
|
-
!Number.isFinite(marker.degraded_at_ms) ||
|
|
1563
|
-
marker.degraded_at_ms <= 0 ||
|
|
1564
|
-
!STATE_ID_PATTERN.test(marker.reason || '')
|
|
1565
|
-
) {
|
|
1566
|
-
throw new Error(`invalid preview phase degraded marker: ${degradedPath}`);
|
|
1567
|
-
}
|
|
1568
|
-
degraded = true;
|
|
1569
|
-
fallbackRequired = true;
|
|
1570
|
-
return true;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function observeFallbackIntent() {
|
|
1574
|
-
if (!fileSystem.existsSync(fallbackIntentPath)) return false;
|
|
1575
|
-
validateFallbackIntent(
|
|
1576
|
-
readRegularJSON(fallbackIntentPath, 'fallback intent'),
|
|
1577
|
-
metadata
|
|
1578
|
-
);
|
|
1579
|
-
fallbackRequired = true;
|
|
1580
|
-
return true;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
function assertGenerationIsNotDegraded() {
|
|
1584
|
-
if (!observeDegradedMarker()) return;
|
|
1585
|
-
throw degradedError();
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
function removeOwnedWriterLock() {
|
|
1589
|
-
const lock = readRegularJSON(writerLockPath, 'writer lock');
|
|
1590
|
-
if (
|
|
1591
|
-
lock.protocol !== 'preview-phase-writer-v1' ||
|
|
1592
|
-
lock.schema_version !== 1 ||
|
|
1593
|
-
lock.run_id !== options.run_id ||
|
|
1594
|
-
lock.generation !== metadata.generation ||
|
|
1595
|
-
lock.writer_instance_id !== writerInstanceID
|
|
1596
|
-
) {
|
|
1597
|
-
throw new Error('preview phase writer lock ownership changed');
|
|
1598
|
-
}
|
|
1599
|
-
fileSystem.rmSync(writerLockPath);
|
|
1600
|
-
writerLockOwned = false;
|
|
1601
|
-
writerLockDirectory = null;
|
|
1602
|
-
syncRunDirectory();
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
function recoverTornTail() {
|
|
1606
|
-
if (recovered) return;
|
|
1607
|
-
if (!fileSystem.existsSync(eventsPath)) {
|
|
1608
|
-
recovered = true;
|
|
1609
|
-
return;
|
|
1610
|
-
}
|
|
1611
|
-
const readFlags = fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0);
|
|
1612
|
-
const fd = fileSystem.openSync(eventsPath, readFlags);
|
|
1613
|
-
try {
|
|
1614
|
-
const size = fileSystem.fstatSync(fd).size;
|
|
1615
|
-
if (size === 0) {
|
|
1616
|
-
recovered = true;
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
const tailByte = Buffer.allocUnsafe(1);
|
|
1620
|
-
fileSystem.readSync(fd, tailByte, 0, 1, size - 1);
|
|
1621
|
-
if (tailByte[0] === 0x0a) {
|
|
1622
|
-
recovered = true;
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
1627
|
-
let scanEnd = size;
|
|
1628
|
-
while (scanEnd > 0) {
|
|
1629
|
-
const scanStart = Math.max(0, scanEnd - chunk.length);
|
|
1630
|
-
const length = scanEnd - scanStart;
|
|
1631
|
-
const bytesRead = fileSystem.readSync(fd, chunk, 0, length, scanStart);
|
|
1632
|
-
const lastNewline = chunk.lastIndexOf(0x0a, bytesRead - 1);
|
|
1633
|
-
if (lastNewline >= 0) {
|
|
1634
|
-
fileSystem.ftruncateSync(fd, scanStart + lastNewline + 1);
|
|
1635
|
-
recovered = true;
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
scanEnd = scanStart;
|
|
1639
|
-
}
|
|
1640
|
-
fileSystem.ftruncateSync(fd, 0);
|
|
1641
|
-
recovered = true;
|
|
1642
|
-
} finally {
|
|
1643
|
-
fileSystem.closeSync(fd);
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
function degradedError() {
|
|
1648
|
-
const error = new Error('preview phase outbox is degraded');
|
|
1649
|
-
error.code = 'PREVIEW_PHASE_OUTBOX_DEGRADED';
|
|
1650
|
-
return error;
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
function latchDegraded(event, cause) {
|
|
1654
|
-
fallbackRequired = true;
|
|
1655
|
-
if (degraded) return;
|
|
1656
|
-
degraded = true;
|
|
1657
|
-
if (!metadata || !ownsSelectedWriterLock()) return;
|
|
1658
|
-
const reason =
|
|
1659
|
-
typeof cause?.code === 'string' && STATE_ID_PATTERN.test(cause.code)
|
|
1660
|
-
? cause.code
|
|
1661
|
-
: 'append_failed';
|
|
1662
|
-
try {
|
|
1663
|
-
publishImmutableState(degradedPath, 'degraded', {
|
|
1664
|
-
protocol: 'preview-phase-degraded-v1',
|
|
1665
|
-
schema_version: 1,
|
|
1666
|
-
run_id: options.run_id,
|
|
1667
|
-
generation: metadata.generation,
|
|
1668
|
-
first_failed_event_id:
|
|
1669
|
-
typeof event?.event_id === 'string'
|
|
1670
|
-
? event.event_id
|
|
1671
|
-
: `${options.run_id}:transport-fallback`,
|
|
1672
|
-
degraded_at_ms: now(),
|
|
1673
|
-
reason,
|
|
1674
|
-
});
|
|
1675
|
-
} catch {
|
|
1676
|
-
// A total filesystem failure cannot be made durable. The in-memory latch
|
|
1677
|
-
// still prevents this writer from creating holes in the JSONL prefix.
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
function finalizeUnboundFallback(event, conflict) {
|
|
1682
|
-
fallbackRequired = true;
|
|
1683
|
-
latchDegraded(event, conflict);
|
|
1684
|
-
recoverTornTail();
|
|
1685
|
-
ensureEmptyEventsFile();
|
|
1686
|
-
const fd = fileSystem.openSync(
|
|
1687
|
-
eventsPath,
|
|
1688
|
-
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
1689
|
-
);
|
|
1690
|
-
let finalSize;
|
|
1691
|
-
try {
|
|
1692
|
-
finalSize = fileSystem.fstatSync(fd).size;
|
|
1693
|
-
} finally {
|
|
1694
|
-
fileSystem.closeSync(fd);
|
|
1695
|
-
}
|
|
1696
|
-
const ownedWriter = validateWriterLock(
|
|
1697
|
-
readRegularJSON(writerLockPath, 'writer lock')
|
|
1698
|
-
);
|
|
1699
|
-
if (
|
|
1700
|
-
ownedWriter.writer_instance_id !== writerInstanceID ||
|
|
1701
|
-
ownedWriter.pid !== process.pid ||
|
|
1702
|
-
(ownedWriter.process_start_ticks || '') !==
|
|
1703
|
-
(currentProcessStartTicks || '')
|
|
1704
|
-
) {
|
|
1705
|
-
throw writerBusyError();
|
|
1706
|
-
}
|
|
1707
|
-
const terminalAt = Math.max(now(), ownedWriter.created_at_ms);
|
|
1708
|
-
if (!fileSystem.existsSync(sealPath)) {
|
|
1709
|
-
publishImmutableState(sealPath, 'sealed', {
|
|
1710
|
-
protocol: 'preview-phase-seal-v1',
|
|
1711
|
-
schema_version: 1,
|
|
1712
|
-
run_id: options.run_id,
|
|
1713
|
-
generation: metadata.generation,
|
|
1714
|
-
final_size_bytes: finalSize,
|
|
1715
|
-
sealed_at_ms: terminalAt,
|
|
1716
|
-
fallback_required: true,
|
|
1717
|
-
});
|
|
1718
|
-
}
|
|
1719
|
-
const observedOwner = readAndValidateProducerMarker();
|
|
1720
|
-
if (!fileSystem.existsSync(unboundFallbackPath)) {
|
|
1721
|
-
publishImmutableState(unboundFallbackPath, 'producer-unbound', {
|
|
1722
|
-
protocol: 'preview-phase-unbound-fallback-v1',
|
|
1723
|
-
schema_version: 1,
|
|
1724
|
-
epoch: producerEpoch,
|
|
1725
|
-
run_id: options.run_id,
|
|
1726
|
-
generation: metadata.generation,
|
|
1727
|
-
writer_instance_id: writerInstanceID,
|
|
1728
|
-
pid: process.pid,
|
|
1729
|
-
process_start_ticks: currentProcessStartTicks,
|
|
1730
|
-
started_at_ms: ownedWriter.created_at_ms,
|
|
1731
|
-
state: 'terminal',
|
|
1732
|
-
terminal_at_ms: terminalAt,
|
|
1733
|
-
observed_owner_run_id: observedOwner.run_id,
|
|
1734
|
-
observed_owner_generation: observedOwner.generation,
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
if (ownsSelectedWriterLock()) removeOwnedWriterLock();
|
|
1738
|
-
closed = true;
|
|
1739
|
-
closeResult = true;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
function finalizeUnpublishedStagingFallback(event, conflict) {
|
|
1743
|
-
if (
|
|
1744
|
-
runDir !== stagingRunDir ||
|
|
1745
|
-
!stagingOwned ||
|
|
1746
|
-
!ownsSelectedWriterLock()
|
|
1747
|
-
) {
|
|
1748
|
-
return false;
|
|
1749
|
-
}
|
|
1750
|
-
ensureMetadata();
|
|
1751
|
-
fallbackRequired = true;
|
|
1752
|
-
latchDegraded(event, conflict);
|
|
1753
|
-
recoverTornTail();
|
|
1754
|
-
ensureEmptyEventsFile();
|
|
1755
|
-
const fd = fileSystem.openSync(
|
|
1756
|
-
eventsPath,
|
|
1757
|
-
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
|
1758
|
-
);
|
|
1759
|
-
let finalSize;
|
|
1760
|
-
try {
|
|
1761
|
-
finalSize = fileSystem.fstatSync(fd).size;
|
|
1762
|
-
} finally {
|
|
1763
|
-
fileSystem.closeSync(fd);
|
|
1764
|
-
}
|
|
1765
|
-
const ownedWriter = validateWriterLock(
|
|
1766
|
-
readRegularJSON(writerLockPath, 'writer lock')
|
|
1767
|
-
);
|
|
1768
|
-
if (
|
|
1769
|
-
ownedWriter.writer_instance_id !== writerInstanceID ||
|
|
1770
|
-
ownedWriter.pid !== process.pid ||
|
|
1771
|
-
(ownedWriter.process_start_ticks || '') !==
|
|
1772
|
-
(currentProcessStartTicks || '')
|
|
1773
|
-
) {
|
|
1774
|
-
throw writerBusyError();
|
|
1775
|
-
}
|
|
1776
|
-
if (!fileSystem.existsSync(sealPath)) {
|
|
1777
|
-
publishImmutableState(sealPath, 'sealed', {
|
|
1778
|
-
protocol: 'preview-phase-seal-v1',
|
|
1779
|
-
schema_version: 1,
|
|
1780
|
-
run_id: options.run_id,
|
|
1781
|
-
generation: metadata.generation,
|
|
1782
|
-
final_size_bytes: finalSize,
|
|
1783
|
-
sealed_at_ms: Math.max(now(), ownedWriter.created_at_ms),
|
|
1784
|
-
fallback_required: true,
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
removeOwnedWriterLock();
|
|
1788
|
-
closed = true;
|
|
1789
|
-
closeResult = true;
|
|
1790
|
-
try {
|
|
1791
|
-
const allowed = new Set([
|
|
1792
|
-
'meta.json',
|
|
1793
|
-
'events.jsonl',
|
|
1794
|
-
'degraded.json',
|
|
1795
|
-
'sealed.json',
|
|
1796
|
-
]);
|
|
1797
|
-
const entries = readDirectoryEntriesBounded(
|
|
1798
|
-
stagingRunDir,
|
|
1799
|
-
allowed.size,
|
|
1800
|
-
'preview phase unpublished staging exceeds cleanup entry limit'
|
|
1801
|
-
);
|
|
1802
|
-
for (const entry of entries) {
|
|
1803
|
-
if (!allowed.has(entry.name)) {
|
|
1804
|
-
throw new Error(
|
|
1805
|
-
'preview phase unpublished staging contains unknown state'
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
const entryPath = path.join(stagingRunDir, entry.name);
|
|
1809
|
-
const stat = fileSystem.lstatSync(entryPath);
|
|
1810
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1811
|
-
throw new Error(
|
|
1812
|
-
'preview phase unpublished staging contains unsafe state'
|
|
1813
|
-
);
|
|
1814
|
-
}
|
|
1815
|
-
fileSystem.rmSync(entryPath);
|
|
1816
|
-
}
|
|
1817
|
-
syncRunDirectory();
|
|
1818
|
-
fileSystem.rmdirSync(stagingRunDir);
|
|
1819
|
-
syncRootDirectory();
|
|
1820
|
-
stagingOwned = false;
|
|
1821
|
-
} catch {
|
|
1822
|
-
// The writer lock is already durably absent. Trusted controller GC can
|
|
1823
|
-
// finish an unexpected cleanup remainder without touching canonical.
|
|
1824
|
-
}
|
|
1825
|
-
return true;
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
function append(event) {
|
|
1829
|
-
if (closed) throw new Error('preview phase outbox is closed');
|
|
1830
|
-
if (degraded) throw degradedError();
|
|
1831
|
-
const payload = JSON.stringify(event);
|
|
1832
|
-
if (Buffer.byteLength(payload, 'utf8') > MAX_EVENT_BYTES) {
|
|
1833
|
-
const error = new Error(
|
|
1834
|
-
`preview phase event exceeds ${MAX_EVENT_BYTES} bytes`
|
|
1835
|
-
);
|
|
1836
|
-
error.code = 'PREVIEW_PHASE_EVENT_TOO_LARGE';
|
|
1837
|
-
throw error;
|
|
1838
|
-
}
|
|
1839
|
-
const record = Buffer.from(`${payload}\n`, 'utf8');
|
|
1840
|
-
for (let attempt = 1; attempt <= MAX_TRANSIENT_ATTEMPTS; attempt += 1) {
|
|
1841
|
-
try {
|
|
1842
|
-
assertSafeDirectory(fileSystem, root);
|
|
1843
|
-
assertRootDirectoryQuota();
|
|
1844
|
-
ensureCanonicalWriter(event);
|
|
1845
|
-
if (
|
|
1846
|
-
closed &&
|
|
1847
|
-
producerRegistrationDurable &&
|
|
1848
|
-
!everOwnedCanonicalWriter
|
|
1849
|
-
) {
|
|
1850
|
-
return true;
|
|
1851
|
-
}
|
|
1852
|
-
// The authoritative registrar proves the initial writer identity, but
|
|
1853
|
-
// every append must still fail closed if a controller or an unexpected
|
|
1854
|
-
// same-UID actor replaces auxiliary state after registration.
|
|
1855
|
-
validateOwnedWriterLock();
|
|
1856
|
-
assertGenerationIsOpen();
|
|
1857
|
-
publishActiveProducerMarker();
|
|
1858
|
-
// Any existing writer identity is authoritative to JS. Dead-owner
|
|
1859
|
-
// recovery and torn-tail finalization belong exclusively to shell-server.
|
|
1860
|
-
assertGenerationIsNotDegraded();
|
|
1861
|
-
assertRootByteQuota(record.length);
|
|
1862
|
-
if (eventsFD == null) {
|
|
1863
|
-
if (fileSystem.existsSync(eventsPath)) {
|
|
1864
|
-
const stat = fileSystem.lstatSync(eventsPath);
|
|
1865
|
-
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1866
|
-
throw new Error(
|
|
1867
|
-
`unsafe preview phase outbox path: ${eventsPath}`
|
|
1868
|
-
);
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
recoverTornTail();
|
|
1872
|
-
const flags =
|
|
1873
|
-
fs.constants.O_CREAT |
|
|
1874
|
-
fs.constants.O_WRONLY |
|
|
1875
|
-
fs.constants.O_APPEND |
|
|
1876
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
1877
|
-
eventsFD = fileSystem.openSync(eventsPath, flags, 0o600);
|
|
1878
|
-
fileSystem.fchmodSync(eventsFD, 0o600);
|
|
1879
|
-
committedBytes = fileSystem.fstatSync(eventsFD).size;
|
|
1880
|
-
}
|
|
1881
|
-
if (committedBytes + record.length > maxRunBytes) {
|
|
1882
|
-
const error = new Error(
|
|
1883
|
-
`preview phase run quota would exceed ${maxRunBytes} bytes`
|
|
1884
|
-
);
|
|
1885
|
-
error.code = 'PREVIEW_PHASE_RUN_QUOTA_EXCEEDED';
|
|
1886
|
-
throw error;
|
|
1887
|
-
}
|
|
1888
|
-
let offset = 0;
|
|
1889
|
-
while (offset < record.length) {
|
|
1890
|
-
const written = fileSystem.writeSync(
|
|
1891
|
-
eventsFD,
|
|
1892
|
-
record,
|
|
1893
|
-
offset,
|
|
1894
|
-
record.length - offset
|
|
1895
|
-
);
|
|
1896
|
-
if (!Number.isInteger(written) || written <= 0) {
|
|
1897
|
-
throw new Error('preview phase outbox write made no progress');
|
|
1898
|
-
}
|
|
1899
|
-
offset += written;
|
|
1900
|
-
}
|
|
1901
|
-
committedBytes += record.length;
|
|
1902
|
-
return true;
|
|
1903
|
-
} catch (error) {
|
|
1904
|
-
if (eventsFD != null) {
|
|
1905
|
-
try {
|
|
1906
|
-
fileSystem.closeSync(eventsFD);
|
|
1907
|
-
} catch {
|
|
1908
|
-
// Best-effort fd cleanup after append failure.
|
|
1909
|
-
}
|
|
1910
|
-
eventsFD = null;
|
|
1911
|
-
committedBytes = null;
|
|
1912
|
-
recovered = false;
|
|
1913
|
-
}
|
|
1914
|
-
if (error?.code === 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT') {
|
|
1915
|
-
try {
|
|
1916
|
-
finalizeUnboundFallback(event, error);
|
|
1917
|
-
} catch {
|
|
1918
|
-
// The controller can finish a dead CAS loser from its immutable
|
|
1919
|
-
// writer identity. Preserve the original conflict for fallback.
|
|
1920
|
-
}
|
|
1921
|
-
throw error;
|
|
1922
|
-
}
|
|
1923
|
-
if (error?.code === 'PREVIEW_PHASE_RUN_CONFLICT') {
|
|
1924
|
-
try {
|
|
1925
|
-
finalizeUnpublishedStagingFallback(event, error);
|
|
1926
|
-
} catch {
|
|
1927
|
-
// Preserve the staging writer for controller-owned dead-owner GC.
|
|
1928
|
-
}
|
|
1929
|
-
throw error;
|
|
1930
|
-
}
|
|
1931
|
-
if (
|
|
1932
|
-
isTransientFilesystemError(error) &&
|
|
1933
|
-
attempt < MAX_TRANSIENT_ATTEMPTS
|
|
1934
|
-
) {
|
|
1935
|
-
continue;
|
|
1936
|
-
}
|
|
1937
|
-
latchDegraded(event, error);
|
|
1938
|
-
throw error;
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
throw new Error('preview phase transient retry loop exhausted');
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
function flush() {
|
|
1945
|
-
if (eventsFD == null) return true;
|
|
1946
|
-
for (let attempt = 1; attempt <= MAX_TRANSIENT_ATTEMPTS; attempt += 1) {
|
|
1947
|
-
try {
|
|
1948
|
-
fileSystem.fdatasyncSync(eventsFD);
|
|
1949
|
-
return true;
|
|
1950
|
-
} catch (error) {
|
|
1951
|
-
if (
|
|
1952
|
-
isTransientFilesystemError(error) &&
|
|
1953
|
-
attempt < MAX_TRANSIENT_ATTEMPTS
|
|
1954
|
-
) {
|
|
1955
|
-
continue;
|
|
1956
|
-
}
|
|
1957
|
-
latchDegraded(null, error);
|
|
1958
|
-
throw error;
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
throw new Error('preview phase flush retry loop exhausted');
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
function pollQuiesce() {
|
|
1965
|
-
const state = (localSealed, canonicalAcknowledged) => ({
|
|
1966
|
-
local_sealed: localSealed,
|
|
1967
|
-
canonical_acknowledged: canonicalAcknowledged,
|
|
1968
|
-
});
|
|
1969
|
-
if (closed) {
|
|
1970
|
-
return state(closeResult, everOwnedCanonicalWriter && closeResult);
|
|
1971
|
-
}
|
|
1972
|
-
if (sealRequested) {
|
|
1973
|
-
const localSealed = close();
|
|
1974
|
-
return state(localSealed, everOwnedCanonicalWriter && localSealed);
|
|
1975
|
-
}
|
|
1976
|
-
if (!metadata) return state(false, false);
|
|
1977
|
-
if (!fileSystem.existsSync(quiesceRequestPath)) return state(false, false);
|
|
1978
|
-
const request = readRegularJSON(quiesceRequestPath, 'quiesce request');
|
|
1979
|
-
assertStateGeneration(request, 'quiesce request');
|
|
1980
|
-
if (
|
|
1981
|
-
request.protocol !== 'preview-phase-quiesce-v1' ||
|
|
1982
|
-
request.schema_version !== 1 ||
|
|
1983
|
-
!STATE_ID_PATTERN.test(request.request_id || '') ||
|
|
1984
|
-
!Number.isFinite(request.requested_at_ms) ||
|
|
1985
|
-
request.requested_at_ms <= 0
|
|
1986
|
-
) {
|
|
1987
|
-
throw new Error(
|
|
1988
|
-
`invalid preview phase quiesce request: ${quiesceRequestPath}`
|
|
1989
|
-
);
|
|
1990
|
-
}
|
|
1991
|
-
if (!ownsSelectedWriterLock()) {
|
|
1992
|
-
// Do not acknowledge or mutate the canonical owner lifecycle. The
|
|
1993
|
-
// quiesce boundary still terminalizes this producer's own fallback file.
|
|
1994
|
-
sealRequested = true;
|
|
1995
|
-
closeResult = sealFallback();
|
|
1996
|
-
closed = true;
|
|
1997
|
-
return state(closeResult, false);
|
|
1998
|
-
}
|
|
1999
|
-
const localSealed = close();
|
|
2000
|
-
return state(localSealed, localSealed);
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
function close() {
|
|
2004
|
-
const fallbackCloseResult = sealFallback();
|
|
2005
|
-
// A fallback-only producer never owns the canonical writer lifecycle.
|
|
2006
|
-
// Its open spool fd is sufficient to terminalize after controller rename;
|
|
2007
|
-
// attempting canonical close here could recreate or mutate another owner.
|
|
2008
|
-
if (fallbackStarted && !everOwnedCanonicalWriter) {
|
|
2009
|
-
closed = true;
|
|
2010
|
-
closeResult = fallbackCloseResult;
|
|
2011
|
-
return closeResult;
|
|
2012
|
-
}
|
|
2013
|
-
if (closed) return closeResult && fallbackCloseResult;
|
|
2014
|
-
sealRequested = true;
|
|
2015
|
-
let finalSize = null;
|
|
2016
|
-
try {
|
|
2017
|
-
// A reporter that never attempted persistence has no generation to
|
|
2018
|
-
// close. Persistence failures set degraded/fallbackRequired and retry
|
|
2019
|
-
// this initialization when the filesystem becomes available again.
|
|
2020
|
-
if (!metadata && !degraded && !fallbackRequired) {
|
|
2021
|
-
closed = true;
|
|
2022
|
-
closeResult = fallbackCloseResult;
|
|
2023
|
-
return closeResult;
|
|
2024
|
-
}
|
|
2025
|
-
assertSafeDirectory(fileSystem, root);
|
|
2026
|
-
assertRootDirectoryQuota();
|
|
2027
|
-
ensureCanonicalWriter();
|
|
2028
|
-
|
|
2029
|
-
if (fileSystem.existsSync(sealPath)) {
|
|
2030
|
-
const seal = readAndValidateSeal();
|
|
2031
|
-
fallbackRequired ||= seal.fallback_required === true;
|
|
2032
|
-
if (ownsSelectedWriterLock()) {
|
|
2033
|
-
publishActiveProducerMarker();
|
|
2034
|
-
publishTerminalProducerMarker();
|
|
2035
|
-
removeOwnedWriterLock();
|
|
2036
|
-
} else if (fileSystem.existsSync(writerLockPath)) {
|
|
2037
|
-
closeResult = false;
|
|
2038
|
-
return false;
|
|
2039
|
-
} else {
|
|
2040
|
-
const marker = readAndValidateProducerMarker();
|
|
2041
|
-
if (
|
|
2042
|
-
!markerBelongsToThisWriter(marker) ||
|
|
2043
|
-
marker.state !== 'terminal'
|
|
2044
|
-
) {
|
|
2045
|
-
closeResult = false;
|
|
2046
|
-
return false;
|
|
2047
|
-
}
|
|
2048
|
-
producerStartedAtMs = marker.started_at_ms;
|
|
2049
|
-
producerTerminalAtMs = marker.terminal_at_ms;
|
|
2050
|
-
producerMarkerActive = true;
|
|
2051
|
-
producerMarkerTerminal = true;
|
|
2052
|
-
}
|
|
2053
|
-
closed = true;
|
|
2054
|
-
closeResult = fallbackCloseResult;
|
|
2055
|
-
return closeResult;
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
if (!ownsSelectedWriterLock()) {
|
|
2059
|
-
acquireWriterLock();
|
|
2060
|
-
}
|
|
2061
|
-
publishActiveProducerMarker();
|
|
2062
|
-
observeFallbackIntent();
|
|
2063
|
-
observeDegradedMarker();
|
|
2064
|
-
if (eventsFD != null) {
|
|
2065
|
-
fileSystem.fdatasyncSync(eventsFD);
|
|
2066
|
-
finalSize = fileSystem.fstatSync(eventsFD).size;
|
|
2067
|
-
fileSystem.closeSync(eventsFD);
|
|
2068
|
-
eventsFD = null;
|
|
2069
|
-
committedBytes = null;
|
|
2070
|
-
} else {
|
|
2071
|
-
recoverTornTail();
|
|
2072
|
-
// The first append can fail after the writer lock is acquired but
|
|
2073
|
-
// before events.jsonl is created (for example, aggregate quota). Such
|
|
2074
|
-
// a degraded generation still needs an empty, sealed prefix so the
|
|
2075
|
-
// lock is releasable and the reader can merge stdout_fallback safely.
|
|
2076
|
-
const flags =
|
|
2077
|
-
fs.constants.O_CREAT |
|
|
2078
|
-
fs.constants.O_RDWR |
|
|
2079
|
-
(fs.constants.O_NOFOLLOW || 0);
|
|
2080
|
-
const fd = fileSystem.openSync(eventsPath, flags, 0o600);
|
|
2081
|
-
try {
|
|
2082
|
-
fileSystem.fchmodSync(fd, 0o600);
|
|
2083
|
-
fileSystem.fdatasyncSync(fd);
|
|
2084
|
-
finalSize = fileSystem.fstatSync(fd).size;
|
|
2085
|
-
} finally {
|
|
2086
|
-
fileSystem.closeSync(fd);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
publishImmutableState(sealPath, 'sealed', {
|
|
2090
|
-
protocol: 'preview-phase-seal-v1',
|
|
2091
|
-
schema_version: 1,
|
|
2092
|
-
run_id: options.run_id,
|
|
2093
|
-
generation: metadata.generation,
|
|
2094
|
-
final_size_bytes: finalSize,
|
|
2095
|
-
sealed_at_ms: now(),
|
|
2096
|
-
fallback_required: fallbackRequired,
|
|
2097
|
-
});
|
|
2098
|
-
publishTerminalProducerMarker();
|
|
2099
|
-
removeOwnedWriterLock();
|
|
2100
|
-
closeResult = fallbackCloseResult;
|
|
2101
|
-
closed = true;
|
|
2102
|
-
} catch (error) {
|
|
2103
|
-
closeResult = false;
|
|
2104
|
-
if (!isTransientFilesystemError(error)) {
|
|
2105
|
-
latchDegraded(null, error);
|
|
2106
|
-
}
|
|
2107
|
-
if (eventsFD != null) {
|
|
2108
|
-
try {
|
|
2109
|
-
fileSystem.closeSync(eventsFD);
|
|
2110
|
-
} catch {
|
|
2111
|
-
// Best-effort fd cleanup after close failure.
|
|
2112
|
-
}
|
|
2113
|
-
eventsFD = null;
|
|
2114
|
-
committedBytes = null;
|
|
2115
|
-
}
|
|
2116
|
-
// Keep the unique writer owner and pending close intent for retries. If
|
|
2117
|
-
// the process dies, shell-server is the sole authority that verifies
|
|
2118
|
-
// PID+starttime, repairs the tail, and seals the generation.
|
|
2119
|
-
}
|
|
2120
|
-
return closeResult;
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
return {
|
|
2124
|
-
append,
|
|
2125
|
-
append_fallback: appendFallback,
|
|
2126
|
-
seal_fallback: sealFallback,
|
|
2127
|
-
flush,
|
|
2128
|
-
close,
|
|
2129
|
-
poll_quiesce: pollQuiesce,
|
|
2130
|
-
is_local_sealed: () => closed && closeResult,
|
|
2131
|
-
events_path: eventsPath,
|
|
2132
|
-
meta_path: metaPath,
|
|
2133
|
-
seal_path: sealPath,
|
|
2134
|
-
degraded_path: degradedPath,
|
|
2135
|
-
quiesce_request_path: quiesceRequestPath,
|
|
2136
|
-
producer_epoch_marker_path: producerEpochMarkerPath,
|
|
2137
|
-
fallback_spool_path: fallbackSpoolPath,
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
module.exports = {
|
|
2142
|
-
DEFAULT_OUTBOX_ROOT,
|
|
2143
|
-
DEFAULT_MAX_RUN_BYTES,
|
|
2144
|
-
DEFAULT_MAX_ROOT_BYTES,
|
|
2145
|
-
DEFAULT_MAX_ROOT_DIRECTORIES,
|
|
2146
|
-
DEFAULT_MAX_ROOT_SCAN_DIRECTORIES,
|
|
2147
|
-
DEFAULT_MAX_RUN_SCAN_ENTRIES,
|
|
2148
|
-
DEFAULT_MAX_FALLBACK_FILES,
|
|
2149
|
-
DEFAULT_MAX_FALLBACK_BYTES,
|
|
2150
|
-
DEFAULT_MAX_FALLBACK_EVENTS,
|
|
2151
|
-
MAX_FALLBACK_RECORD_BYTES,
|
|
2152
|
-
MAX_EVENT_BYTES,
|
|
2153
|
-
createFallbackSessionID,
|
|
2154
|
-
createPreviewPhaseOutbox,
|
|
2155
|
-
};
|