retrace-sdk 0.16.11 → 0.16.12
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/dist/resume.js +53 -1
- package/dist/transport.js +1 -1
- package/package.json +1 -1
package/dist/resume.js
CHANGED
|
@@ -9,20 +9,60 @@
|
|
|
9
9
|
*/
|
|
10
10
|
// Registry of resumable functions
|
|
11
11
|
const resumableFunctions = new Map();
|
|
12
|
+
import { getConfig } from "./config.js";
|
|
12
13
|
export function registerResumable(name, fn) {
|
|
13
14
|
resumableFunctions.set(name, fn);
|
|
14
15
|
}
|
|
15
16
|
export function getResumable(name) {
|
|
16
17
|
return resumableFunctions.get(name);
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Best-effort execution-start / heartbeat claim. Returns false ONLY on a definitive 409 (a newer
|
|
21
|
+
* replay superseded this one). ANY other outcome — network/timeout, 401/403, deploy-window 404, 429,
|
|
22
|
+
* 5xx — returns true and PROCEEDS: this fence is best-effort by design; the server's completion CAS
|
|
23
|
+
* is the final authority that rejects a stale result. It does NOT, on its own, prevent stale work on
|
|
24
|
+
* a claim-endpoint failure.
|
|
25
|
+
*/
|
|
26
|
+
async function claimExecution(forkId, executionId) {
|
|
27
|
+
try {
|
|
28
|
+
const cfg = getConfig();
|
|
29
|
+
const resp = await fetch(`${cfg.baseUrl}/api/v1/forks/${forkId}/replay/claim`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "x-retrace-key": cfg.apiKey, "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ execution_id: executionId }),
|
|
33
|
+
signal: AbortSignal.timeout(5000),
|
|
34
|
+
});
|
|
35
|
+
return resp.status !== 409;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return true; // ambiguous failure → proceed (best-effort)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
18
41
|
export async function handleResume(command) {
|
|
19
42
|
const fn = getResumable(command.traceName);
|
|
20
43
|
if (!fn)
|
|
21
44
|
return false;
|
|
45
|
+
// Execution-start fence BEFORE creating the recorder, so a superseded replay leaves NO leaked
|
|
46
|
+
// recorder/trace/interceptors/context (the recorder is only started once we're cleared to run).
|
|
47
|
+
if (command.executionId && !(await claimExecution(command.forkId, command.executionId))) {
|
|
48
|
+
console.warn("[retrace] Cascade replay superseded by a newer replay — aborting before execution.");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Keep-alive heartbeat: refresh our generation while a (possibly long) replay runs so a new dispatch
|
|
52
|
+
// can't reclaim it as stale — which is what would otherwise let a second replay run concurrently.
|
|
53
|
+
// Interval is well under the server lease window; stopped in finally.
|
|
54
|
+
let heartbeat = null;
|
|
55
|
+
if (command.executionId) {
|
|
56
|
+
heartbeat = setInterval(() => { void claimExecution(command.forkId, command.executionId); }, 5 * 60 * 1000);
|
|
57
|
+
if (typeof heartbeat.unref === "function")
|
|
58
|
+
heartbeat.unref();
|
|
59
|
+
}
|
|
60
|
+
let recorder = null;
|
|
61
|
+
let markFailed = null;
|
|
22
62
|
try {
|
|
23
63
|
const { TraceRecorder } = await import("./recorder.js");
|
|
24
64
|
const { TraceStatus } = await import("./trace.js");
|
|
25
|
-
|
|
65
|
+
recorder = new TraceRecorder({
|
|
26
66
|
name: `Fork: ${command.traceName}`,
|
|
27
67
|
input: command.modifiedInput,
|
|
28
68
|
metadata: {
|
|
@@ -35,6 +75,10 @@ export async function handleResume(command) {
|
|
|
35
75
|
forkPointSpanId: command.forkPointSpanId,
|
|
36
76
|
forkPointIndex: command.forkPointIndex,
|
|
37
77
|
});
|
|
78
|
+
// Finalizer captured with the right enum type — used to end the trace as FAILED if user code
|
|
79
|
+
// throws, so a thrown replay never leaks a running trace / interceptor routing / trace context /
|
|
80
|
+
// an elevated active-recorder count.
|
|
81
|
+
markFailed = () => recorder.end(undefined, TraceStatus.FAILED);
|
|
38
82
|
recorder.start(`Fork: ${command.traceName}`, command.modifiedInput);
|
|
39
83
|
// Determine args for re-execution
|
|
40
84
|
let args = command.originalArgs || [];
|
|
@@ -50,8 +94,16 @@ export async function handleResume(command) {
|
|
|
50
94
|
}
|
|
51
95
|
catch (err) {
|
|
52
96
|
console.error("[retrace] Cascade replay failed:", err);
|
|
97
|
+
try {
|
|
98
|
+
markFailed?.();
|
|
99
|
+
}
|
|
100
|
+
catch { /* best-effort finalize */ }
|
|
53
101
|
return false;
|
|
54
102
|
}
|
|
103
|
+
finally {
|
|
104
|
+
if (heartbeat)
|
|
105
|
+
clearInterval(heartbeat);
|
|
106
|
+
}
|
|
55
107
|
}
|
|
56
108
|
export function parseResumeMessage(msg) {
|
|
57
109
|
if (msg.type !== "resume" || !msg.data)
|
package/dist/transport.js
CHANGED
|
@@ -87,7 +87,7 @@ export class WSTransport {
|
|
|
87
87
|
// (no-op on runtimes without `_socket`, e.g. the global WebSocket). Graceful shutdown still
|
|
88
88
|
// drains via flush()/beforeExit.
|
|
89
89
|
socket?._socket?.unref?.();
|
|
90
|
-
socket.send(JSON.stringify({ type: "auth", api_key: getConfig().apiKey, resumable: getConfig().allowRemoteReplay }));
|
|
90
|
+
socket.send(JSON.stringify({ type: "auth", api_key: getConfig().apiKey, resumable: getConfig().allowRemoteReplay, replay_generation: 1 }));
|
|
91
91
|
});
|
|
92
92
|
socket.addEventListener("message", (ev) => {
|
|
93
93
|
try {
|