@lark-apaas/fullstack-cli 1.1.59-alpha.20260720153322 → 1.1.59-alpha.20260720184331
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/package.json
CHANGED
|
@@ -9,9 +9,11 @@ const DEFAULT_MAX_RUN_BYTES = 2 * 1024 * 1024;
|
|
|
9
9
|
const DEFAULT_MAX_ROOT_BYTES = 64 * 1024 * 1024;
|
|
10
10
|
const DEFAULT_MAX_ROOT_DIRECTORIES = 128;
|
|
11
11
|
const DEFAULT_MAX_ROOT_SCAN_DIRECTORIES = 256;
|
|
12
|
+
const DEFAULT_MAX_RUN_SCAN_ENTRIES = 64;
|
|
12
13
|
const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,60}$/;
|
|
13
14
|
const STATE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
14
15
|
const PROCESS_START_TICKS_PATTERN = /^\d+$/;
|
|
16
|
+
const PRODUCER_EPOCH_PATTERN = /^ppe_[a-f0-9]{32}$/;
|
|
15
17
|
|
|
16
18
|
function readLinuxProcessStartTicks(fileSystem, pid) {
|
|
17
19
|
try {
|
|
@@ -51,6 +53,7 @@ function createPreviewPhaseOutbox(options) {
|
|
|
51
53
|
const randomUUID = options.random_uuid || require('crypto').randomUUID;
|
|
52
54
|
const now = options.now || Date.now;
|
|
53
55
|
const writerInstanceID = options.writer_instance_id || randomUUID();
|
|
56
|
+
const producerEpoch = options.producer_epoch;
|
|
54
57
|
const getProcessStartTicks =
|
|
55
58
|
options.get_process_start_ticks ||
|
|
56
59
|
(pid => readLinuxProcessStartTicks(fileSystem, pid));
|
|
@@ -65,16 +68,18 @@ function createPreviewPhaseOutbox(options) {
|
|
|
65
68
|
options.max_root_directories ?? DEFAULT_MAX_ROOT_DIRECTORIES;
|
|
66
69
|
const maxRootScanDirectories =
|
|
67
70
|
options.max_root_scan_directories ?? DEFAULT_MAX_ROOT_SCAN_DIRECTORIES;
|
|
71
|
+
const maxRunScanEntries =
|
|
72
|
+
options.max_run_scan_entries ?? DEFAULT_MAX_RUN_SCAN_ENTRIES;
|
|
68
73
|
if (!RUN_ID_PATTERN.test(options.run_id || '')) {
|
|
69
74
|
throw new Error('invalid preview run_id for phase outbox');
|
|
70
75
|
}
|
|
76
|
+
if (!PRODUCER_EPOCH_PATTERN.test(producerEpoch || '')) {
|
|
77
|
+
throw new Error('invalid preview phase producer_epoch');
|
|
78
|
+
}
|
|
71
79
|
if (!STATE_ID_PATTERN.test(writerInstanceID)) {
|
|
72
80
|
throw new Error('invalid preview phase writer_instance_id');
|
|
73
81
|
}
|
|
74
|
-
if (
|
|
75
|
-
currentProcessStartTicks != null &&
|
|
76
|
-
!PROCESS_START_TICKS_PATTERN.test(currentProcessStartTicks)
|
|
77
|
-
) {
|
|
82
|
+
if (!PROCESS_START_TICKS_PATTERN.test(currentProcessStartTicks || '')) {
|
|
78
83
|
throw new Error('invalid preview phase process_start_ticks');
|
|
79
84
|
}
|
|
80
85
|
if (!Number.isSafeInteger(maxRunBytes) || maxRunBytes <= 0) {
|
|
@@ -92,6 +97,9 @@ function createPreviewPhaseOutbox(options) {
|
|
|
92
97
|
) {
|
|
93
98
|
throw new Error('invalid preview phase max_root_scan_directories');
|
|
94
99
|
}
|
|
100
|
+
if (!Number.isSafeInteger(maxRunScanEntries) || maxRunScanEntries <= 0) {
|
|
101
|
+
throw new Error('invalid preview phase max_run_scan_entries');
|
|
102
|
+
}
|
|
95
103
|
const runDir = path.join(root, options.run_id);
|
|
96
104
|
const eventsPath = path.join(runDir, 'events.jsonl');
|
|
97
105
|
const metaPath = path.join(runDir, 'meta.json');
|
|
@@ -99,6 +107,10 @@ function createPreviewPhaseOutbox(options) {
|
|
|
99
107
|
const sealPath = path.join(runDir, 'sealed.json');
|
|
100
108
|
const degradedPath = path.join(runDir, 'degraded.json');
|
|
101
109
|
const quiesceRequestPath = path.join(runDir, 'quiesce.request.json');
|
|
110
|
+
const producerEpochMarkerPath = path.join(
|
|
111
|
+
root,
|
|
112
|
+
`producer-epoch-${producerEpoch}.json`
|
|
113
|
+
);
|
|
102
114
|
let recovered = false;
|
|
103
115
|
let metadata = null;
|
|
104
116
|
let writerLockOwned = false;
|
|
@@ -109,6 +121,10 @@ function createPreviewPhaseOutbox(options) {
|
|
|
109
121
|
let sealRequested = false;
|
|
110
122
|
let closed = false;
|
|
111
123
|
let closeResult = false;
|
|
124
|
+
let producerMarkerActive = false;
|
|
125
|
+
let producerMarkerTerminal = false;
|
|
126
|
+
let producerStartedAtMs = null;
|
|
127
|
+
let producerTerminalAtMs = null;
|
|
112
128
|
|
|
113
129
|
function rootQuotaError(message) {
|
|
114
130
|
const error = new Error(message);
|
|
@@ -116,13 +132,30 @@ function createPreviewPhaseOutbox(options) {
|
|
|
116
132
|
return error;
|
|
117
133
|
}
|
|
118
134
|
|
|
119
|
-
function
|
|
120
|
-
const entries =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
function readDirectoryEntriesBounded(directoryPath, limit, quotaMessage) {
|
|
136
|
+
const entries = [];
|
|
137
|
+
const directory = fileSystem.opendirSync(directoryPath);
|
|
138
|
+
try {
|
|
139
|
+
while (entries.length <= limit) {
|
|
140
|
+
const entry = directory.readSync();
|
|
141
|
+
if (!entry) break;
|
|
142
|
+
entries.push(entry);
|
|
143
|
+
if (entries.length > limit) {
|
|
144
|
+
throw rootQuotaError(quotaMessage);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
directory.closeSync();
|
|
125
149
|
}
|
|
150
|
+
return entries;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function inspectRootUsage() {
|
|
154
|
+
const entries = readDirectoryEntriesBounded(
|
|
155
|
+
root,
|
|
156
|
+
maxRootScanDirectories,
|
|
157
|
+
`preview phase root scan exceeds ${maxRootScanDirectories} entries`
|
|
158
|
+
);
|
|
126
159
|
let directories = 0;
|
|
127
160
|
let bytes = 0;
|
|
128
161
|
const disappearingEntry = error =>
|
|
@@ -152,9 +185,11 @@ function createPreviewPhaseOutbox(options) {
|
|
|
152
185
|
directories += 1;
|
|
153
186
|
let runEntries;
|
|
154
187
|
try {
|
|
155
|
-
runEntries =
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
runEntries = readDirectoryEntriesBounded(
|
|
189
|
+
entryPath,
|
|
190
|
+
maxRunScanEntries,
|
|
191
|
+
`preview phase run scan exceeds ${maxRunScanEntries} entries: ${entryPath}`
|
|
192
|
+
);
|
|
158
193
|
} catch (error) {
|
|
159
194
|
if (disappearingEntry(error)) continue;
|
|
160
195
|
throw error;
|
|
@@ -223,6 +258,19 @@ function createPreviewPhaseOutbox(options) {
|
|
|
223
258
|
}
|
|
224
259
|
}
|
|
225
260
|
|
|
261
|
+
function syncRootDirectory() {
|
|
262
|
+
const flags =
|
|
263
|
+
fs.constants.O_RDONLY |
|
|
264
|
+
(fs.constants.O_DIRECTORY || 0) |
|
|
265
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
266
|
+
const fd = fileSystem.openSync(root, flags);
|
|
267
|
+
try {
|
|
268
|
+
fileSystem.fsyncSync(fd);
|
|
269
|
+
} finally {
|
|
270
|
+
fileSystem.closeSync(fd);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
226
274
|
function writeAll(fd, payload) {
|
|
227
275
|
let offset = 0;
|
|
228
276
|
while (offset < payload.length) {
|
|
@@ -239,6 +287,24 @@ function createPreviewPhaseOutbox(options) {
|
|
|
239
287
|
}
|
|
240
288
|
}
|
|
241
289
|
|
|
290
|
+
function writeDurableTempFile(tempPath, payload) {
|
|
291
|
+
let fd = null;
|
|
292
|
+
try {
|
|
293
|
+
const flags =
|
|
294
|
+
fs.constants.O_CREAT |
|
|
295
|
+
fs.constants.O_EXCL |
|
|
296
|
+
fs.constants.O_WRONLY |
|
|
297
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
298
|
+
fd = fileSystem.openSync(tempPath, flags, 0o600);
|
|
299
|
+
writeAll(fd, payload);
|
|
300
|
+
fileSystem.fsyncSync(fd);
|
|
301
|
+
} finally {
|
|
302
|
+
if (fd != null) {
|
|
303
|
+
fileSystem.closeSync(fd);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
242
308
|
function publishImmutableState(filePath, label, value) {
|
|
243
309
|
const tempPath = path.join(
|
|
244
310
|
runDir,
|
|
@@ -272,6 +338,194 @@ function createPreviewPhaseOutbox(options) {
|
|
|
272
338
|
}
|
|
273
339
|
}
|
|
274
340
|
|
|
341
|
+
function producerMarkerConflict() {
|
|
342
|
+
const error = new Error(
|
|
343
|
+
`preview phase producer epoch already belongs to another writer: ${producerEpoch}`
|
|
344
|
+
);
|
|
345
|
+
error.code = 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT';
|
|
346
|
+
return error;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function validateProducerMarker(marker) {
|
|
350
|
+
const terminal = marker?.state === 'terminal';
|
|
351
|
+
if (
|
|
352
|
+
marker?.protocol !== 'preview-phase-producer-epoch-v1' ||
|
|
353
|
+
marker.schema_version !== 1 ||
|
|
354
|
+
marker.epoch !== producerEpoch ||
|
|
355
|
+
!RUN_ID_PATTERN.test(marker.run_id || '') ||
|
|
356
|
+
!STATE_ID_PATTERN.test(marker.generation || '') ||
|
|
357
|
+
!STATE_ID_PATTERN.test(marker.writer_instance_id || '') ||
|
|
358
|
+
!Number.isSafeInteger(marker.pid) ||
|
|
359
|
+
marker.pid <= 0 ||
|
|
360
|
+
!PROCESS_START_TICKS_PATTERN.test(marker.process_start_ticks || '') ||
|
|
361
|
+
!Number.isFinite(marker.started_at_ms) ||
|
|
362
|
+
marker.started_at_ms <= 0 ||
|
|
363
|
+
(marker.state !== 'active' && !terminal) ||
|
|
364
|
+
(terminal &&
|
|
365
|
+
(!Number.isFinite(marker.terminal_at_ms) ||
|
|
366
|
+
marker.terminal_at_ms < marker.started_at_ms)) ||
|
|
367
|
+
(!terminal && marker.terminal_at_ms !== undefined)
|
|
368
|
+
) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`invalid preview phase producer epoch marker: ${producerEpochMarkerPath}`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return marker;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function markerBelongsToThisWriter(marker) {
|
|
377
|
+
return (
|
|
378
|
+
marker.epoch === producerEpoch &&
|
|
379
|
+
marker.run_id === options.run_id &&
|
|
380
|
+
marker.generation === metadata.generation &&
|
|
381
|
+
marker.writer_instance_id === writerInstanceID &&
|
|
382
|
+
marker.pid === process.pid &&
|
|
383
|
+
(marker.process_start_ticks || '') === (currentProcessStartTicks || '')
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function readAndValidateProducerMarker() {
|
|
388
|
+
return validateProducerMarker(
|
|
389
|
+
readRegularJSON(producerEpochMarkerPath, 'producer epoch marker')
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildProducerMarker(state) {
|
|
394
|
+
return {
|
|
395
|
+
protocol: 'preview-phase-producer-epoch-v1',
|
|
396
|
+
schema_version: 1,
|
|
397
|
+
epoch: producerEpoch,
|
|
398
|
+
run_id: options.run_id,
|
|
399
|
+
generation: metadata.generation,
|
|
400
|
+
writer_instance_id: writerInstanceID,
|
|
401
|
+
pid: process.pid,
|
|
402
|
+
process_start_ticks: currentProcessStartTicks,
|
|
403
|
+
started_at_ms: producerStartedAtMs,
|
|
404
|
+
state,
|
|
405
|
+
...(state === 'terminal' ? { terminal_at_ms: producerTerminalAtMs } : {}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function publishActiveProducerMarker() {
|
|
410
|
+
if (producerMarkerActive) return;
|
|
411
|
+
if (!metadata || !writerLockOwned) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
'preview phase producer marker requires writer ownership'
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
if (fileSystem.existsSync(producerEpochMarkerPath)) {
|
|
417
|
+
const existing = readAndValidateProducerMarker();
|
|
418
|
+
if (!markerBelongsToThisWriter(existing) || existing.state !== 'active') {
|
|
419
|
+
throw producerMarkerConflict();
|
|
420
|
+
}
|
|
421
|
+
producerStartedAtMs = existing.started_at_ms;
|
|
422
|
+
producerMarkerActive = true;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
producerStartedAtMs = producerStartedAtMs ?? now();
|
|
427
|
+
const marker = buildProducerMarker('active');
|
|
428
|
+
const tempPath = path.join(
|
|
429
|
+
root,
|
|
430
|
+
`.producer-epoch.${producerEpoch}.${process.pid}.${writerInstanceID}.tmp`
|
|
431
|
+
);
|
|
432
|
+
let fd = null;
|
|
433
|
+
try {
|
|
434
|
+
const flags =
|
|
435
|
+
fs.constants.O_CREAT |
|
|
436
|
+
fs.constants.O_EXCL |
|
|
437
|
+
fs.constants.O_WRONLY |
|
|
438
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
439
|
+
fd = fileSystem.openSync(tempPath, flags, 0o600);
|
|
440
|
+
writeAll(fd, Buffer.from(`${JSON.stringify(marker)}\n`, 'utf8'));
|
|
441
|
+
fileSystem.fsyncSync(fd);
|
|
442
|
+
fileSystem.closeSync(fd);
|
|
443
|
+
fd = null;
|
|
444
|
+
try {
|
|
445
|
+
fileSystem.linkSync(tempPath, producerEpochMarkerPath);
|
|
446
|
+
syncRootDirectory();
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
449
|
+
const existing = readAndValidateProducerMarker();
|
|
450
|
+
if (
|
|
451
|
+
!markerBelongsToThisWriter(existing) ||
|
|
452
|
+
existing.state !== 'active'
|
|
453
|
+
) {
|
|
454
|
+
throw producerMarkerConflict();
|
|
455
|
+
}
|
|
456
|
+
producerStartedAtMs = existing.started_at_ms;
|
|
457
|
+
}
|
|
458
|
+
producerMarkerActive = true;
|
|
459
|
+
} finally {
|
|
460
|
+
if (fd != null) {
|
|
461
|
+
try {
|
|
462
|
+
fileSystem.closeSync(fd);
|
|
463
|
+
} catch {}
|
|
464
|
+
}
|
|
465
|
+
let tempRemoved = false;
|
|
466
|
+
try {
|
|
467
|
+
fileSystem.rmSync(tempPath, { force: true });
|
|
468
|
+
tempRemoved = true;
|
|
469
|
+
} catch {
|
|
470
|
+
// A stale temp file is never accepted as lifecycle proof.
|
|
471
|
+
}
|
|
472
|
+
if (tempRemoved) syncRootDirectory();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function publishTerminalProducerMarker() {
|
|
477
|
+
if (producerMarkerTerminal) return;
|
|
478
|
+
const existing = readAndValidateProducerMarker();
|
|
479
|
+
if (!markerBelongsToThisWriter(existing)) {
|
|
480
|
+
throw producerMarkerConflict();
|
|
481
|
+
}
|
|
482
|
+
producerStartedAtMs = existing.started_at_ms;
|
|
483
|
+
if (existing.state === 'terminal') {
|
|
484
|
+
producerTerminalAtMs = existing.terminal_at_ms;
|
|
485
|
+
producerMarkerActive = true;
|
|
486
|
+
producerMarkerTerminal = true;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
producerTerminalAtMs =
|
|
490
|
+
producerTerminalAtMs ?? Math.max(now(), producerStartedAtMs);
|
|
491
|
+
const marker = buildProducerMarker('terminal');
|
|
492
|
+
const tempPath = path.join(
|
|
493
|
+
root,
|
|
494
|
+
`.producer-terminal.${producerEpoch}.${process.pid}.${writerInstanceID}.tmp`
|
|
495
|
+
);
|
|
496
|
+
let fd = null;
|
|
497
|
+
try {
|
|
498
|
+
const flags =
|
|
499
|
+
fs.constants.O_CREAT |
|
|
500
|
+
fs.constants.O_EXCL |
|
|
501
|
+
fs.constants.O_WRONLY |
|
|
502
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
503
|
+
fd = fileSystem.openSync(tempPath, flags, 0o600);
|
|
504
|
+
writeAll(fd, Buffer.from(`${JSON.stringify(marker)}\n`, 'utf8'));
|
|
505
|
+
fileSystem.fsyncSync(fd);
|
|
506
|
+
fileSystem.closeSync(fd);
|
|
507
|
+
fd = null;
|
|
508
|
+
fileSystem.renameSync(tempPath, producerEpochMarkerPath);
|
|
509
|
+
syncRootDirectory();
|
|
510
|
+
producerMarkerActive = true;
|
|
511
|
+
producerMarkerTerminal = true;
|
|
512
|
+
} finally {
|
|
513
|
+
if (fd != null) {
|
|
514
|
+
try {
|
|
515
|
+
fileSystem.closeSync(fd);
|
|
516
|
+
} catch {}
|
|
517
|
+
}
|
|
518
|
+
let tempRemoved = false;
|
|
519
|
+
try {
|
|
520
|
+
fileSystem.rmSync(tempPath, { force: true });
|
|
521
|
+
tempRemoved = true;
|
|
522
|
+
} catch {
|
|
523
|
+
// A stale temp file is never accepted as lifecycle proof.
|
|
524
|
+
}
|
|
525
|
+
if (tempRemoved) syncRootDirectory();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
275
529
|
function readAndValidateMetadata() {
|
|
276
530
|
const stat = fileSystem.lstatSync(metaPath);
|
|
277
531
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
@@ -282,6 +536,7 @@ function createPreviewPhaseOutbox(options) {
|
|
|
282
536
|
if (
|
|
283
537
|
metadata.protocol !== 'preview-phase-outbox-v1' ||
|
|
284
538
|
metadata.schema_version !== 1 ||
|
|
539
|
+
metadata.producer_epoch !== producerEpoch ||
|
|
285
540
|
metadata.run_id !== options.run_id ||
|
|
286
541
|
!/^[A-Za-z0-9_-]{1,128}$/.test(metadata.generation || '') ||
|
|
287
542
|
!Number.isFinite(metadata.created_at_ms) ||
|
|
@@ -303,6 +558,7 @@ function createPreviewPhaseOutbox(options) {
|
|
|
303
558
|
const candidate = {
|
|
304
559
|
protocol: 'preview-phase-outbox-v1',
|
|
305
560
|
schema_version: 1,
|
|
561
|
+
producer_epoch: producerEpoch,
|
|
306
562
|
generation: randomUUID(),
|
|
307
563
|
run_id: options.run_id,
|
|
308
564
|
created_at_ms: now(),
|
|
@@ -313,13 +569,13 @@ function createPreviewPhaseOutbox(options) {
|
|
|
313
569
|
`.meta.${process.pid}.${candidate.generation}.tmp`
|
|
314
570
|
);
|
|
315
571
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
});
|
|
572
|
+
writeDurableTempFile(
|
|
573
|
+
tempPath,
|
|
574
|
+
Buffer.from(`${JSON.stringify(candidate)}\n`, 'utf8')
|
|
575
|
+
);
|
|
321
576
|
try {
|
|
322
577
|
fileSystem.linkSync(tempPath, metaPath);
|
|
578
|
+
syncRunDirectory();
|
|
323
579
|
} catch (error) {
|
|
324
580
|
if (error?.code !== 'EEXIST') throw error;
|
|
325
581
|
}
|
|
@@ -386,11 +642,10 @@ function createPreviewPhaseOutbox(options) {
|
|
|
386
642
|
`.writer.${process.pid}.${writerInstanceID}.tmp`
|
|
387
643
|
);
|
|
388
644
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
});
|
|
645
|
+
writeDurableTempFile(
|
|
646
|
+
tempPath,
|
|
647
|
+
Buffer.from(`${JSON.stringify(lock)}\n`, 'utf8')
|
|
648
|
+
);
|
|
394
649
|
try {
|
|
395
650
|
fileSystem.linkSync(tempPath, writerLockPath);
|
|
396
651
|
writerLockOwned = true;
|
|
@@ -586,6 +841,7 @@ function createPreviewPhaseOutbox(options) {
|
|
|
586
841
|
ensureMetadata();
|
|
587
842
|
assertGenerationIsOpen();
|
|
588
843
|
acquireWriterLock();
|
|
844
|
+
publishActiveProducerMarker();
|
|
589
845
|
// Any existing writer identity is authoritative to JS. Dead-owner
|
|
590
846
|
// recovery and torn-tail finalization belong exclusively to shell-server.
|
|
591
847
|
assertGenerationIsNotDegraded();
|
|
@@ -699,10 +955,25 @@ function createPreviewPhaseOutbox(options) {
|
|
|
699
955
|
const seal = readAndValidateSeal();
|
|
700
956
|
fallbackRequired ||= seal.fallback_required === true;
|
|
701
957
|
if (writerLockOwned) {
|
|
958
|
+
publishActiveProducerMarker();
|
|
959
|
+
publishTerminalProducerMarker();
|
|
702
960
|
removeOwnedWriterLock();
|
|
703
961
|
} else if (fileSystem.existsSync(writerLockPath)) {
|
|
704
962
|
closeResult = false;
|
|
705
963
|
return false;
|
|
964
|
+
} else {
|
|
965
|
+
const marker = readAndValidateProducerMarker();
|
|
966
|
+
if (
|
|
967
|
+
!markerBelongsToThisWriter(marker) ||
|
|
968
|
+
marker.state !== 'terminal'
|
|
969
|
+
) {
|
|
970
|
+
closeResult = false;
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
producerStartedAtMs = marker.started_at_ms;
|
|
974
|
+
producerTerminalAtMs = marker.terminal_at_ms;
|
|
975
|
+
producerMarkerActive = true;
|
|
976
|
+
producerMarkerTerminal = true;
|
|
706
977
|
}
|
|
707
978
|
closed = true;
|
|
708
979
|
closeResult = true;
|
|
@@ -712,6 +983,7 @@ function createPreviewPhaseOutbox(options) {
|
|
|
712
983
|
if (!writerLockOwned) {
|
|
713
984
|
acquireWriterLock();
|
|
714
985
|
}
|
|
986
|
+
publishActiveProducerMarker();
|
|
715
987
|
observeDegradedMarker();
|
|
716
988
|
if (eventsFD != null) {
|
|
717
989
|
fileSystem.fdatasyncSync(eventsFD);
|
|
@@ -747,6 +1019,7 @@ function createPreviewPhaseOutbox(options) {
|
|
|
747
1019
|
sealed_at_ms: now(),
|
|
748
1020
|
fallback_required: fallbackRequired,
|
|
749
1021
|
});
|
|
1022
|
+
publishTerminalProducerMarker();
|
|
750
1023
|
removeOwnedWriterLock();
|
|
751
1024
|
closeResult = true;
|
|
752
1025
|
closed = true;
|
|
@@ -777,6 +1050,7 @@ function createPreviewPhaseOutbox(options) {
|
|
|
777
1050
|
seal_path: sealPath,
|
|
778
1051
|
degraded_path: degradedPath,
|
|
779
1052
|
quiesce_request_path: quiesceRequestPath,
|
|
1053
|
+
producer_epoch_marker_path: producerEpochMarkerPath,
|
|
780
1054
|
};
|
|
781
1055
|
}
|
|
782
1056
|
|
|
@@ -786,6 +1060,7 @@ module.exports = {
|
|
|
786
1060
|
DEFAULT_MAX_ROOT_BYTES,
|
|
787
1061
|
DEFAULT_MAX_ROOT_DIRECTORIES,
|
|
788
1062
|
DEFAULT_MAX_ROOT_SCAN_DIRECTORIES,
|
|
1063
|
+
DEFAULT_MAX_RUN_SCAN_ENTRIES,
|
|
789
1064
|
MAX_EVENT_BYTES,
|
|
790
1065
|
createPreviewPhaseOutbox,
|
|
791
1066
|
};
|
|
@@ -4,7 +4,10 @@ const fs = require('fs');
|
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const net = require('net');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
MAX_EVENT_BYTES,
|
|
9
|
+
createPreviewPhaseOutbox,
|
|
10
|
+
} = require('./preview-phase-outbox.cjs');
|
|
8
11
|
|
|
9
12
|
const PREFIX = '[MiaodaPreviewPhase] ';
|
|
10
13
|
const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,60}$/;
|
|
@@ -86,18 +89,28 @@ function createPreviewPhaseReporter(options = {}) {
|
|
|
86
89
|
});
|
|
87
90
|
const producerInstanceID = randomUUID();
|
|
88
91
|
const runId = env.MIAODA_PREVIEW_RUN_ID || '';
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
let outbox = null;
|
|
93
|
+
let outboxInitializationError = null;
|
|
94
|
+
if (RUN_ID_PATTERN.test(runId)) {
|
|
95
|
+
try {
|
|
96
|
+
outbox = createPreviewPhaseOutbox({
|
|
91
97
|
root: options.outbox_root || env.MIAODA_PREVIEW_PHASE_OUTBOX_ROOT,
|
|
92
98
|
run_id: runId,
|
|
99
|
+
producer_epoch: env.MIAODA_PREVIEW_PHASE_PRODUCER_EPOCH,
|
|
100
|
+
process_start_ticks: options.process_start_ticks,
|
|
101
|
+
get_process_start_ticks: options.get_process_start_ticks,
|
|
93
102
|
fs: options.fs,
|
|
94
103
|
random_uuid: randomUUID,
|
|
95
104
|
now,
|
|
96
|
-
})
|
|
97
|
-
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
outboxInitializationError = error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
98
110
|
const producerSequences = new Map();
|
|
99
111
|
const startupTerminalPhases = new Set();
|
|
100
112
|
let reporterSealed = false;
|
|
113
|
+
let terminalCloseScheduled = false;
|
|
101
114
|
let quiescePollTimer = null;
|
|
102
115
|
|
|
103
116
|
function reportDiagnostic(diagnostic) {
|
|
@@ -108,6 +121,33 @@ function createPreviewPhaseReporter(options = {}) {
|
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
123
|
|
|
124
|
+
function scheduleTerminalClose() {
|
|
125
|
+
if (terminalCloseScheduled || reporterSealed) return;
|
|
126
|
+
terminalCloseScheduled = true;
|
|
127
|
+
queueMicrotask(() => {
|
|
128
|
+
terminalCloseScheduled = false;
|
|
129
|
+
if (reporterSealed) return;
|
|
130
|
+
// backend_tcp_ready's synchronous onReady callback emits the initial
|
|
131
|
+
// deferred-typecheck decision immediately after the readiness event.
|
|
132
|
+
// Closing in a microtask keeps that terminal startup decision inside the
|
|
133
|
+
// same durable generation without extending the outbox into later source
|
|
134
|
+
// changes or daemon restarts.
|
|
135
|
+
flush();
|
|
136
|
+
close();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (outboxInitializationError) {
|
|
141
|
+
reportDiagnostic({
|
|
142
|
+
code: 'outbox_initialization_failed',
|
|
143
|
+
run_id: runId,
|
|
144
|
+
error:
|
|
145
|
+
outboxInitializationError instanceof Error
|
|
146
|
+
? outboxInitializationError.message
|
|
147
|
+
: String(outboxInitializationError),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
111
151
|
function emitForProducer(eventProducer, phase, status, detail = {}) {
|
|
112
152
|
if (!RUN_ID_PATTERN.test(runId) || reporterSealed) return false;
|
|
113
153
|
const atMs = detail.at_ms == null ? now() : detail.at_ms;
|
|
@@ -132,34 +172,60 @@ function createPreviewPhaseReporter(options = {}) {
|
|
|
132
172
|
recorded_at_ms: now(),
|
|
133
173
|
transport: 'outbox',
|
|
134
174
|
};
|
|
175
|
+
let serializedEvent = JSON.stringify(event);
|
|
176
|
+
if (Buffer.byteLength(serializedEvent, 'utf8') > MAX_EVENT_BYTES) {
|
|
177
|
+
reportDiagnostic({
|
|
178
|
+
code: 'event_rejected',
|
|
179
|
+
reason: 'event_too_large',
|
|
180
|
+
run_id: runId,
|
|
181
|
+
phase,
|
|
182
|
+
event_id: event.event_id,
|
|
183
|
+
});
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
135
186
|
let persisted = false;
|
|
136
|
-
|
|
137
|
-
persisted = outbox.append(event);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (error?.code === 'PREVIEW_PHASE_EVENT_TOO_LARGE') {
|
|
140
|
-
reportDiagnostic({
|
|
141
|
-
code: 'event_rejected',
|
|
142
|
-
reason: 'event_too_large',
|
|
143
|
-
run_id: runId,
|
|
144
|
-
phase,
|
|
145
|
-
event_id: event.event_id,
|
|
146
|
-
});
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
187
|
+
if (!outbox) {
|
|
149
188
|
event.transport = 'stdout_fallback';
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
} else {
|
|
190
|
+
try {
|
|
191
|
+
persisted = outbox.append(event);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error?.code === 'PREVIEW_PHASE_EVENT_TOO_LARGE') {
|
|
194
|
+
reportDiagnostic({
|
|
195
|
+
code: 'event_rejected',
|
|
196
|
+
reason: 'event_too_large',
|
|
197
|
+
run_id: runId,
|
|
198
|
+
phase,
|
|
199
|
+
event_id: event.event_id,
|
|
200
|
+
});
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
event.transport = 'stdout_fallback';
|
|
204
|
+
if (error?.code !== 'PREVIEW_PHASE_OUTBOX_DEGRADED') {
|
|
205
|
+
reportDiagnostic({
|
|
206
|
+
code: 'outbox_append_failed',
|
|
207
|
+
run_id: runId,
|
|
208
|
+
phase,
|
|
209
|
+
event_id: event.event_id,
|
|
210
|
+
error: error instanceof Error ? error.message : String(error),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
158
213
|
}
|
|
159
214
|
}
|
|
215
|
+
serializedEvent = JSON.stringify(event);
|
|
216
|
+
if (Buffer.byteLength(serializedEvent, 'utf8') > MAX_EVENT_BYTES) {
|
|
217
|
+
reportDiagnostic({
|
|
218
|
+
code: 'event_rejected',
|
|
219
|
+
reason: 'event_too_large',
|
|
220
|
+
run_id: runId,
|
|
221
|
+
phase,
|
|
222
|
+
event_id: event.event_id,
|
|
223
|
+
});
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
160
226
|
let mirrored = false;
|
|
161
227
|
try {
|
|
162
|
-
write(`${PREFIX}${
|
|
228
|
+
write(`${PREFIX}${serializedEvent}`);
|
|
163
229
|
mirrored = true;
|
|
164
230
|
} catch (error) {
|
|
165
231
|
reportDiagnostic({
|
|
@@ -178,8 +244,7 @@ function createPreviewPhaseReporter(options = {}) {
|
|
|
178
244
|
) {
|
|
179
245
|
startupTerminalPhases.add(phase);
|
|
180
246
|
if (startupTerminalPhases.size === 2) {
|
|
181
|
-
|
|
182
|
-
close();
|
|
247
|
+
scheduleTerminalClose();
|
|
183
248
|
}
|
|
184
249
|
}
|
|
185
250
|
return delivered;
|
|
@@ -247,7 +312,7 @@ function createPreviewPhaseReporter(options = {}) {
|
|
|
247
312
|
|
|
248
313
|
function flush() {
|
|
249
314
|
if (reporterSealed) return true;
|
|
250
|
-
if (!outbox) return
|
|
315
|
+
if (!outbox) return outboxInitializationError == null;
|
|
251
316
|
try {
|
|
252
317
|
return outbox.flush();
|
|
253
318
|
} catch (error) {
|
|
@@ -288,7 +353,7 @@ function createPreviewPhaseReporter(options = {}) {
|
|
|
288
353
|
|
|
289
354
|
function close() {
|
|
290
355
|
if (reporterSealed) return true;
|
|
291
|
-
if (!outbox) return
|
|
356
|
+
if (!outbox) return outboxInitializationError == null;
|
|
292
357
|
try {
|
|
293
358
|
const sealed = outbox.close();
|
|
294
359
|
if (sealed) {
|