pi-braid-telegram 0.1.7 → 0.1.8
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/build-info.json +4 -4
- package/package.json +1 -1
- package/src/lib/poller-lock.ts +67 -1
package/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-braid-telegram",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"sha": "
|
|
5
|
-
"shaShort": "
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"sha": "e1d647e5b1e0c9367bd49bcde72e3a316c908250",
|
|
5
|
+
"shaShort": "e1d647e5",
|
|
6
6
|
"branch": "detached",
|
|
7
7
|
"dirty": false,
|
|
8
|
-
"packedAt": "2026-11-
|
|
8
|
+
"packedAt": "2026-11-04T17:20:23.646Z"
|
|
9
9
|
}
|
package/package.json
CHANGED
package/src/lib/poller-lock.ts
CHANGED
|
@@ -40,6 +40,14 @@ export const POLLER_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
|
40
40
|
*/
|
|
41
41
|
export const POLLER_HEARTBEAT_STALE_MS = 30_000;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Slack between the moment a pre-identity holder wrote its lock and the start
|
|
45
|
+
* time of the process now answering to the same pid. A pid can be recycled, so a
|
|
46
|
+
* live pid alone cannot prove that the recorded holder is still there; a process
|
|
47
|
+
* that started well after the record was written is someone else.
|
|
48
|
+
*/
|
|
49
|
+
const LEGACY_PID_REUSE_SLACK_MS = 60_000;
|
|
50
|
+
|
|
43
51
|
/** Boot identity recorded when neither the kernel id nor sysctl is readable. */
|
|
44
52
|
const BOOT_IDENTITY_FALLBACK = "boot-unknown";
|
|
45
53
|
|
|
@@ -93,7 +101,9 @@ export type PollerLockTakeoverReason =
|
|
|
93
101
|
| "unreadable-file"
|
|
94
102
|
| "same-pid-refresh"
|
|
95
103
|
| "stale-same-instance"
|
|
96
|
-
| "stale-across-namespace"
|
|
104
|
+
| "stale-across-namespace"
|
|
105
|
+
| "stale-legacy"
|
|
106
|
+
| "stale-legacy-recycled";
|
|
97
107
|
|
|
98
108
|
/**
|
|
99
109
|
* Why an acquisition refused.
|
|
@@ -109,6 +119,7 @@ export type PollerLockTakeoverReason =
|
|
|
109
119
|
export type PollerLockRefusalReason =
|
|
110
120
|
| "held-same-instance"
|
|
111
121
|
| "held-across-namespace"
|
|
122
|
+
| "held-legacy"
|
|
112
123
|
| "stale-unknown";
|
|
113
124
|
|
|
114
125
|
export type PollerLockReason =
|
|
@@ -136,6 +147,9 @@ export type AcquirePollerLockResult =
|
|
|
136
147
|
export interface AcquirePollerLockOptions {
|
|
137
148
|
pidAlive?: (pid: number) => boolean;
|
|
138
149
|
now?: () => number;
|
|
150
|
+
/** Start time of a live pid, in epoch ms. Injected so the legacy arm can be
|
|
151
|
+
* tested without depending on the machine's process table. */
|
|
152
|
+
pidStartedAt?: (pid: number) => number | undefined;
|
|
139
153
|
}
|
|
140
154
|
|
|
141
155
|
/** Read the lock file; absent/corrupt/unreadable reads as "no holder". */
|
|
@@ -292,6 +306,12 @@ export function describeLockReason(
|
|
|
292
306
|
return "the holder shares this instance and is no longer polling (dead pid, or a heartbeat past the staleness window)";
|
|
293
307
|
case "stale-across-namespace":
|
|
294
308
|
return "the holder was recorded by another boot and its heartbeat is past the staleness window";
|
|
309
|
+
case "stale-legacy":
|
|
310
|
+
return "the holder was recorded by a build that kept no identity or heartbeat, and its pid is dead, so the channel is free";
|
|
311
|
+
case "stale-legacy-recycled":
|
|
312
|
+
return "the holder was recorded by a build that kept no identity or heartbeat, and the live pid now wearing its number started after that record was written, so it is a recycled pid and not the holder";
|
|
313
|
+
case "held-legacy":
|
|
314
|
+
return `the holder was recorded by a build that kept no identity or heartbeat, so liveness cannot be read from the record, and its pid is still alive; confirm it is not polling, then remove ${path} (a clean disable releases it)`;
|
|
295
315
|
}
|
|
296
316
|
}
|
|
297
317
|
|
|
@@ -303,6 +323,24 @@ function heartbeatIsCurrent(holder: PollerLockInfo, now: number): boolean {
|
|
|
303
323
|
return now - (holder.heartbeatAt as number) <= POLLER_HEARTBEAT_STALE_MS;
|
|
304
324
|
}
|
|
305
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Start time of a live pid in epoch ms, from `ps`. Undefined when ps cannot say:
|
|
328
|
+
* an undated live pid is a refusal, never a takeover.
|
|
329
|
+
*/
|
|
330
|
+
function readPidStartedAt(pid: number): number | undefined {
|
|
331
|
+
try {
|
|
332
|
+
const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
333
|
+
encoding: "utf8",
|
|
334
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
335
|
+
}).trim();
|
|
336
|
+
if (!out) return undefined;
|
|
337
|
+
const parsed = Date.parse(out);
|
|
338
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
339
|
+
} catch {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
306
344
|
/**
|
|
307
345
|
* Take the lock. The rule, in order:
|
|
308
346
|
*
|
|
@@ -319,6 +357,12 @@ function heartbeatIsCurrent(holder: PollerLockInfo, now: number): boolean {
|
|
|
319
357
|
* 7. another boot or no recorded identity → refuse `held-across-namespace`
|
|
320
358
|
* while the heartbeat is current, acquire `stale-across-namespace` once it
|
|
321
359
|
* is not, and refuse `stale-unknown` when no heartbeat can be read at all.
|
|
360
|
+
* 8. a record written before identity existed (`instanceId` absent, so no
|
|
361
|
+
* heartbeat ever): a dead pid is evidence enough, so acquire `stale-legacy`;
|
|
362
|
+
* a live pid whose process started later than the record was written is a
|
|
363
|
+
* recycled pid, so acquire `stale-legacy-recycled`; anything else refuses
|
|
364
|
+
* `held-legacy` and names the file, because a live pid we cannot date is the
|
|
365
|
+
* one case where guessing either steals a channel or wedges one.
|
|
322
366
|
*/
|
|
323
367
|
export function acquirePollerLock(
|
|
324
368
|
path: string,
|
|
@@ -326,6 +370,7 @@ export function acquirePollerLock(
|
|
|
326
370
|
options: AcquirePollerLockOptions = {},
|
|
327
371
|
): AcquirePollerLockResult {
|
|
328
372
|
const pidAlive = options.pidAlive ?? isPidAlive;
|
|
373
|
+
const pidStartedAt = options.pidStartedAt ?? readPidStartedAt;
|
|
329
374
|
const now = options.now ?? Date.now;
|
|
330
375
|
const at = now();
|
|
331
376
|
const instanceId = stamp.instanceId ?? resolveInstanceId();
|
|
@@ -369,6 +414,27 @@ export function acquirePollerLock(
|
|
|
369
414
|
return { ok: false, reason: "held-same-instance", holder: existing, path };
|
|
370
415
|
}
|
|
371
416
|
|
|
417
|
+
// A record from a build that had no instance identity and no heartbeat. Its
|
|
418
|
+
// pid is the only field, and it is the field that used to wedge the channel:
|
|
419
|
+
// the arm below read the absent heartbeat as "cannot tell" and refused
|
|
420
|
+
// forever, even when the holder had died days earlier.
|
|
421
|
+
if (existing.instanceId === undefined) {
|
|
422
|
+
if (existing.pid === stamp.pid) return acquire("same-pid-refresh");
|
|
423
|
+
if (!pidAlive(existing.pid)) return acquire("stale-legacy");
|
|
424
|
+
const startedAt = pidStartedAt(existing.pid);
|
|
425
|
+
const recordedAt = Number.isFinite(existing.startedAt)
|
|
426
|
+
? (existing.startedAt as number)
|
|
427
|
+
: undefined;
|
|
428
|
+
if (
|
|
429
|
+
startedAt !== undefined &&
|
|
430
|
+
recordedAt !== undefined &&
|
|
431
|
+
startedAt - recordedAt > LEGACY_PID_REUSE_SLACK_MS
|
|
432
|
+
) {
|
|
433
|
+
return acquire("stale-legacy-recycled");
|
|
434
|
+
}
|
|
435
|
+
return { ok: false, reason: "held-legacy", holder: existing, path };
|
|
436
|
+
}
|
|
437
|
+
|
|
372
438
|
if (!Number.isFinite(existing.heartbeatAt)) {
|
|
373
439
|
return { ok: false, reason: "stale-unknown", holder: existing, path };
|
|
374
440
|
}
|