@sekiban/dcb-runtime 0.2.0
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/LICENSE +86 -0
- package/README.md +13 -0
- package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
- package/dist/allocator/AllocatorDurableObject.js +658 -0
- package/dist/allocator/IssuanceLedger.d.ts +188 -0
- package/dist/allocator/IssuanceLedger.js +608 -0
- package/dist/allocator/IssuanceReconciler.d.ts +41 -0
- package/dist/allocator/IssuanceReconciler.js +191 -0
- package/dist/allocator/OrderClock.d.ts +41 -0
- package/dist/allocator/OrderClock.js +92 -0
- package/dist/allocator/SortableUniqueId.d.ts +46 -0
- package/dist/allocator/SortableUniqueId.js +156 -0
- package/dist/allocator/types.d.ts +52 -0
- package/dist/allocator/types.js +1 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
- package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
- package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
- package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
- package/dist/bootstrap/OperatorBootstrap.js +104 -0
- package/dist/bootstrap/manifest.d.ts +9 -0
- package/dist/bootstrap/manifest.js +115 -0
- package/dist/bootstrap/types.d.ts +75 -0
- package/dist/bootstrap/types.js +1 -0
- package/dist/cli/OperatorRepairCli.d.ts +11 -0
- package/dist/cli/OperatorRepairCli.js +108 -0
- package/dist/cloudflare.d.ts +164 -0
- package/dist/cloudflare.js +19487 -0
- package/dist/commit/CommitWorker.d.ts +101 -0
- package/dist/commit/CommitWorker.js +921 -0
- package/dist/commit/types.d.ts +25 -0
- package/dist/commit/types.js +13 -0
- package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
- package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
- package/dist/completeness/types.d.ts +85 -0
- package/dist/completeness/types.js +6 -0
- package/dist/composition.d.ts +123 -0
- package/dist/composition.js +237 -0
- package/dist/cosmos.d.ts +11 -0
- package/dist/cosmos.js +1411 -0
- package/dist/d1-mv.d.ts +8 -0
- package/dist/d1-mv.js +2001 -0
- package/dist/d1.d.ts +16 -0
- package/dist/d1.js +3418 -0
- package/dist/diagnostics/G60DurableHop.d.ts +99 -0
- package/dist/diagnostics/G60DurableHop.js +248 -0
- package/dist/diagnostics/G65Admission.d.ts +23 -0
- package/dist/diagnostics/G65Admission.js +43 -0
- package/dist/diagnostics/G65DirectRing.d.ts +33 -0
- package/dist/diagnostics/G65DirectRing.js +85 -0
- package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
- package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
- package/dist/downstream/DeliveryCore.d.ts +127 -0
- package/dist/downstream/DeliveryCore.js +304 -0
- package/dist/downstream/Doorbell.d.ts +43 -0
- package/dist/downstream/Doorbell.js +137 -0
- package/dist/downstream/DownstreamAdapter.d.ts +52 -0
- package/dist/downstream/DownstreamAdapter.js +299 -0
- package/dist/downstream/ExclusionLookup.d.ts +36 -0
- package/dist/downstream/ExclusionLookup.js +50 -0
- package/dist/downstream/InconsistencyDetector.d.ts +27 -0
- package/dist/downstream/InconsistencyDetector.js +82 -0
- package/dist/downstream/OutboxDrain.d.ts +35 -0
- package/dist/downstream/OutboxDrain.js +126 -0
- package/dist/downstream/types.d.ts +77 -0
- package/dist/downstream/types.js +64 -0
- package/dist/eventIdentity.d.ts +30 -0
- package/dist/eventIdentity.js +49 -0
- package/dist/eventRecord.d.ts +26 -0
- package/dist/eventRecord.js +53 -0
- package/dist/g42-probe.d.ts +9 -0
- package/dist/g42-probe.js +9 -0
- package/dist/http/SerializedQueryWorker.d.ts +58 -0
- package/dist/http/SerializedQueryWorker.js +497 -0
- package/dist/http/commitResponse.d.ts +46 -0
- package/dist/http/commitResponse.js +80 -0
- package/dist/http/testServiceId.d.ts +17 -0
- package/dist/http/testServiceId.js +14 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +19926 -0
- package/dist/journal/JournalDurableObject.d.ts +30 -0
- package/dist/journal/JournalDurableObject.js +481 -0
- package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
- package/dist/journal/JournalFirstTouchProbe.js +375 -0
- package/dist/journal/types.d.ts +116 -0
- package/dist/journal/types.js +52 -0
- package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
- package/dist/mv/MaterializedViewCatchUp.js +353 -0
- package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
- package/dist/mv/MaterializedViewRuntime.js +328 -0
- package/dist/mv/MaterializedViewStore.d.ts +257 -0
- package/dist/mv/MaterializedViewStore.js +875 -0
- package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
- package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
- package/dist/mv/index.d.ts +8 -0
- package/dist/mv/index.js +2357 -0
- package/dist/projection/LiveProjectionWorker.d.ts +86 -0
- package/dist/projection/LiveProjectionWorker.js +240 -0
- package/dist/projection/ProjectionRuntime.d.ts +113 -0
- package/dist/projection/ProjectionRuntime.js +346 -0
- package/dist/projection/ProjectorRegistry.d.ts +51 -0
- package/dist/projection/ProjectorRegistry.js +116 -0
- package/dist/query/ProjectionQueryStore.d.ts +102 -0
- package/dist/query/ProjectionQueryStore.js +185 -0
- package/dist/query/QueryRegistry.d.ts +22 -0
- package/dist/query/QueryRegistry.js +36 -0
- package/dist/read/SerializedReadWorker.d.ts +36 -0
- package/dist/read/SerializedReadWorker.js +225 -0
- package/dist/repair/RepairWorker.d.ts +72 -0
- package/dist/repair/RepairWorker.js +461 -0
- package/dist/safeWindow.d.ts +20 -0
- package/dist/safeWindow.js +36 -0
- package/dist/scope/ControlRouteScope.d.ts +22 -0
- package/dist/scope/ControlRouteScope.js +36 -0
- package/dist/scope/ScopeName.d.ts +34 -0
- package/dist/scope/ScopeName.js +67 -0
- package/dist/service/ServiceIdentityProvider.d.ts +35 -0
- package/dist/service/ServiceIdentityProvider.js +56 -0
- package/dist/store/CosmosEventStore.d.ts +126 -0
- package/dist/store/CosmosEventStore.js +950 -0
- package/dist/store/D1EventStore.d.ts +111 -0
- package/dist/store/D1EventStore.js +1111 -0
- package/dist/store/PostgresEventStore.d.ts +35 -0
- package/dist/store/PostgresEventStore.js +811 -0
- package/dist/store/provider.d.ts +25 -0
- package/dist/store/provider.js +32 -0
- package/dist/store/types.d.ts +205 -0
- package/dist/store/types.js +18 -0
- package/dist/tag/EventDigest.d.ts +40 -0
- package/dist/tag/EventDigest.js +158 -0
- package/dist/tag/TagDurableObject.d.ts +286 -0
- package/dist/tag/TagDurableObject.js +4071 -0
- package/dist/tag/TagSqlMeasurement.d.ts +26 -0
- package/dist/tag/TagSqlMeasurement.js +44 -0
- package/dist/tag/TagSqlSchema.d.ts +29 -0
- package/dist/tag/TagSqlSchema.js +211 -0
- package/dist/tag/types.d.ts +173 -0
- package/dist/tag/types.js +3 -0
- package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
- package/dist/tagstate/TagStateDurableObject.js +499 -0
- package/dist/trace/AttributionRatio.d.ts +12 -0
- package/dist/trace/AttributionRatio.js +68 -0
- package/dist/trace/CloudflareTracing.d.ts +7 -0
- package/dist/trace/CloudflareTracing.js +9 -0
- package/dist/trace/CommitTrace.d.ts +405 -0
- package/dist/trace/CommitTrace.js +1008 -0
- package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
- package/dist/trace/CommitTraceConsoleSink.js +114 -0
- package/dist/trace/CommitTraceVerifier.d.ts +22 -0
- package/dist/trace/CommitTraceVerifier.js +295 -0
- package/dist/trace/ObservationStream.d.ts +118 -0
- package/dist/trace/ObservationStream.js +148 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
import commitTraceManifestJson from "../../../../contracts/commit-trace-manifest.json";
|
|
2
|
+
import { calculateUnattributedRatio } from "./AttributionRatio";
|
|
3
|
+
const NOOP_NATIVE_TRACE_SPAN = Object.freeze({
|
|
4
|
+
isTraced: false,
|
|
5
|
+
setAttribute: () => undefined,
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* The portable runtime must remain importable from Node-side provider and
|
|
9
|
+
* contract tooling. Cloudflare-only entrypoints inject the real tracer;
|
|
10
|
+
* this adapter preserves the trace-only, fail-open contract everywhere else.
|
|
11
|
+
*/
|
|
12
|
+
export const noOpNativeTracing = Object.freeze({
|
|
13
|
+
enterSpan(_name, callback, ...args) {
|
|
14
|
+
return callback(NOOP_NATIVE_TRACE_SPAN, ...args);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* A request handler can use the platform context without importing the
|
|
19
|
+
* Cloudflare-only module into the portable Node/provider graph.
|
|
20
|
+
*/
|
|
21
|
+
export function nativeTracingFromContext(context) {
|
|
22
|
+
const native = context?.tracing;
|
|
23
|
+
return native ?? noOpNativeTracing;
|
|
24
|
+
}
|
|
25
|
+
const manifest = commitTraceManifestJson;
|
|
26
|
+
const DEFAULT_CLOCK = { now: () => Date.now() };
|
|
27
|
+
const RAW_TAG_KEY = /(^|[._])tag($|[._])/i;
|
|
28
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
29
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
30
|
+
const NATIVE_ROW_ID_ATTRIBUTE = "sdt.row.id";
|
|
31
|
+
function hasOwn(value, key) {
|
|
32
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
33
|
+
}
|
|
34
|
+
function rowFor(schema, rowId) {
|
|
35
|
+
const schemaRows = manifest.schemas[schema]?.rows;
|
|
36
|
+
const row = schemaRows?.find((entry) => entry.rowId === rowId);
|
|
37
|
+
if (row === undefined) {
|
|
38
|
+
throw new Error(`Commit trace row ${rowId} is not defined by ${schema}`);
|
|
39
|
+
}
|
|
40
|
+
return row;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* G30's emitted-row inventory is deliberately limited to rows emitted from
|
|
44
|
+
* the primary Worker process. It is an observation-side discriminator, not a
|
|
45
|
+
* new trace attribute or a cross-actor propagation mechanism.
|
|
46
|
+
*/
|
|
47
|
+
function isWorkerLocalEmitter(row) {
|
|
48
|
+
return row.emitter === "root-worker" || row.emitter === "caller-worker";
|
|
49
|
+
}
|
|
50
|
+
function faceState(attribute, face) {
|
|
51
|
+
const matrix = manifest.attributeMatrix.attributes[attribute];
|
|
52
|
+
if (matrix === undefined) {
|
|
53
|
+
throw new Error(`Commit trace attribute ${attribute} is not declared by the authority matrix`);
|
|
54
|
+
}
|
|
55
|
+
const state = matrix.faces[face];
|
|
56
|
+
if (state !== "required" && state !== "optional" && state !== "forbidden") {
|
|
57
|
+
throw new Error(`Commit trace matrix omits face ${face} for ${attribute}`);
|
|
58
|
+
}
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
function rowScopedState(declaration, face, rowId) {
|
|
62
|
+
if (declaration.rowScope !== undefined && !declaration.rowScope.includes(rowId))
|
|
63
|
+
return "forbidden";
|
|
64
|
+
return declaration.faces[face];
|
|
65
|
+
}
|
|
66
|
+
function asTraceValue(value, name) {
|
|
67
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
68
|
+
return value;
|
|
69
|
+
throw new Error(`Commit trace attribute ${name} must be scalar`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Keep the callback-boundary emission path subject to the same typed matrix
|
|
73
|
+
* as the in-memory/export verifiers. A malformed observation remains
|
|
74
|
+
* fail-open for the commit itself, but it must never be emitted as a
|
|
75
|
+
* schema-conformant custom span.
|
|
76
|
+
*/
|
|
77
|
+
function assertAttributeValue(value, declaration, key) {
|
|
78
|
+
const type = declaration.type;
|
|
79
|
+
if (type === "hex64") {
|
|
80
|
+
if (typeof value !== "string" || !HEX64.test(value))
|
|
81
|
+
throw new Error(`Commit trace attribute ${key} must be lower-case 64-hex`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (type === "uuid") {
|
|
85
|
+
if (typeof value !== "string" || !UUID.test(value))
|
|
86
|
+
throw new Error(`Commit trace attribute ${key} must be a UUID`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (type === "boolean") {
|
|
90
|
+
if (typeof value !== "boolean")
|
|
91
|
+
throw new Error(`Commit trace attribute ${key} must be boolean`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (type === "integer" || type === "non-negative-integer") {
|
|
95
|
+
if (typeof value !== "number" || !Number.isInteger(value) || (type === "non-negative-integer" && value < 0)) {
|
|
96
|
+
throw new Error(`Commit trace attribute ${key} must be ${type}`);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value !== "string")
|
|
101
|
+
throw new Error(`Commit trace attribute ${key} must be a string`);
|
|
102
|
+
if (type === "enum-from-manifest-recovery") {
|
|
103
|
+
const reconcile = manifest.schemas["sdt.commit.reconcile/v1"];
|
|
104
|
+
const recoveryKinds = Object.keys(reconcile.recoveryDag ?? {});
|
|
105
|
+
const terminalAtEntry = reconcile.terminalAtEntryBoundary?.name;
|
|
106
|
+
if (!recoveryKinds.includes(value) && value !== terminalAtEntry) {
|
|
107
|
+
throw new Error(`Commit trace attribute ${key} is not a manifest recovery kind`);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (declaration.values !== undefined && !declaration.values.includes(value)) {
|
|
112
|
+
throw new Error(`Commit trace attribute ${key} is not manifest-declared`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function assertAttributeTypes(attributes) {
|
|
116
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
117
|
+
const declaration = manifest.attributeMatrix.attributes[key];
|
|
118
|
+
if (declaration === undefined)
|
|
119
|
+
continue;
|
|
120
|
+
assertAttributeValue(value, declaration, key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function assertNoRawTagAttributes(attributes) {
|
|
124
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
125
|
+
if (key === "tag.key_hash")
|
|
126
|
+
continue;
|
|
127
|
+
if (RAW_TAG_KEY.test(key)) {
|
|
128
|
+
throw new Error(`Commit trace forbids raw tag attribute ${key}=${String(value)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Stable, non-authoritative, non-cryptographic observation key. Four
|
|
134
|
+
* independent FNV-1a lanes yield the lower-case 64-hex spelling mandated by
|
|
135
|
+
* the trace schema without making the key a protocol or storage authority.
|
|
136
|
+
*/
|
|
137
|
+
export function stableTraceHash(value) {
|
|
138
|
+
const masks = [
|
|
139
|
+
0xcbf29ce484222325n,
|
|
140
|
+
0x9e3779b185ebca87n,
|
|
141
|
+
0x84222325cbf29ce4n,
|
|
142
|
+
0x100000001b3n,
|
|
143
|
+
];
|
|
144
|
+
const prime = 0x100000001b3n;
|
|
145
|
+
const modulus = (1n << 64n) - 1n;
|
|
146
|
+
return masks.map((seed, index) => {
|
|
147
|
+
let hash = seed;
|
|
148
|
+
for (let offset = 0; offset < value.length; offset += 1) {
|
|
149
|
+
hash ^= BigInt(value.charCodeAt(offset) + index);
|
|
150
|
+
hash = (hash * prime) & modulus;
|
|
151
|
+
}
|
|
152
|
+
return hash.toString(16).padStart(16, "0");
|
|
153
|
+
}).join("");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The DO request bodies already carry attemptId, while G30 cannot add a
|
|
157
|
+
* correlation wire field. This deterministic, distinct observation key lets
|
|
158
|
+
* independently exported caller/callee spans be joined without treating
|
|
159
|
+
* attempt.id and correlation.id as the same attribute.
|
|
160
|
+
*/
|
|
161
|
+
export function correlationIdForAttempt(attemptId) {
|
|
162
|
+
return `corr-${stableTraceHash(`sdt.commit/v1:${attemptId}`)}`;
|
|
163
|
+
}
|
|
164
|
+
export function createTraceCorrelationId() {
|
|
165
|
+
return crypto.randomUUID();
|
|
166
|
+
}
|
|
167
|
+
export function traceManifest() {
|
|
168
|
+
return manifest;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A scope is the test/evidence adapter's reflection of callback nesting. It
|
|
172
|
+
* does not accept a native span context or a parent span ID; Cloudflare owns
|
|
173
|
+
* real parentage through enterSpan's active async context.
|
|
174
|
+
*/
|
|
175
|
+
export class CommitTraceScope {
|
|
176
|
+
trace;
|
|
177
|
+
state;
|
|
178
|
+
constructor(trace, state) {
|
|
179
|
+
this.trace = trace;
|
|
180
|
+
this.state = state;
|
|
181
|
+
}
|
|
182
|
+
accepted(attemptId) {
|
|
183
|
+
return new CommitTraceScope(this.trace, {
|
|
184
|
+
...this.state,
|
|
185
|
+
face: "accepted",
|
|
186
|
+
attemptId,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
withActor(actorClass, actorKey, clockDomain = this.state.clockDomain) {
|
|
190
|
+
return new CommitTraceScope(this.trace, {
|
|
191
|
+
...this.state,
|
|
192
|
+
actorClass,
|
|
193
|
+
actorKey,
|
|
194
|
+
clockDomain,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
fork() {
|
|
198
|
+
return new CommitTraceScope(this.trace, { ...this.state });
|
|
199
|
+
}
|
|
200
|
+
async span(rowId, options, callback) {
|
|
201
|
+
return this.trace.span(this.state, rowId, {
|
|
202
|
+
face: options.face ?? this.state.face,
|
|
203
|
+
actorClass: options.actorClass ?? this.state.actorClass,
|
|
204
|
+
...(options.actorKey === undefined && this.state.actorKey === undefined ? {} : { actorKey: options.actorKey ?? this.state.actorKey }),
|
|
205
|
+
...(options.attemptId === undefined && this.state.attemptId === undefined ? {} : { attemptId: options.attemptId ?? this.state.attemptId }),
|
|
206
|
+
clockDomain: options.clockDomain ?? this.state.clockDomain,
|
|
207
|
+
...(options.outcome === undefined ? {} : { outcome: options.outcome }),
|
|
208
|
+
...(options.httpStatus === undefined ? {} : { httpStatus: options.httpStatus }),
|
|
209
|
+
...(options.phaseOrdinal === undefined ? {} : { phaseOrdinal: options.phaseOrdinal }),
|
|
210
|
+
...(options.retryIndex === undefined ? {} : { retryIndex: options.retryIndex }),
|
|
211
|
+
...(options.memberIndex === undefined ? {} : { memberIndex: options.memberIndex }),
|
|
212
|
+
...(options.tag === undefined ? {} : { tag: options.tag }),
|
|
213
|
+
...(options.attributes === undefined ? {} : { attributes: options.attributes }),
|
|
214
|
+
...(options.zeroDurationPlatformLimited === undefined ? {} : { zeroDurationPlatformLimited: options.zeroDurationPlatformLimited }),
|
|
215
|
+
}, callback);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export class CommitTrace {
|
|
219
|
+
options;
|
|
220
|
+
spans = [];
|
|
221
|
+
emittedWorkerRows = new Set();
|
|
222
|
+
provider;
|
|
223
|
+
diagnostics;
|
|
224
|
+
correlationId;
|
|
225
|
+
activeRootId;
|
|
226
|
+
activeRootRecord;
|
|
227
|
+
activeRootNative;
|
|
228
|
+
nextRootIndex = 0;
|
|
229
|
+
runtimeVerification;
|
|
230
|
+
observationFailureCode;
|
|
231
|
+
constructor(options) {
|
|
232
|
+
this.options = options;
|
|
233
|
+
this.provider = Object.freeze({ ...(options.provider ?? {}) });
|
|
234
|
+
this.diagnostics = Object.freeze({ ...(options.diagnostics ?? {}) });
|
|
235
|
+
this.correlationId = options.correlationId;
|
|
236
|
+
}
|
|
237
|
+
async root(rowId, options, callback) {
|
|
238
|
+
if (this.activeRootId !== undefined) {
|
|
239
|
+
throw new Error("Commit trace root is already active");
|
|
240
|
+
}
|
|
241
|
+
const rootId = `${this.options.correlationId}:${this.nextRootIndex++}`;
|
|
242
|
+
const state = {
|
|
243
|
+
rootId,
|
|
244
|
+
currentRowId: null,
|
|
245
|
+
face: options.face ?? "pre-admission",
|
|
246
|
+
actorClass: options.actorClass ?? "ROOT",
|
|
247
|
+
...(options.actorKey === undefined ? {} : { actorKey: options.actorKey }),
|
|
248
|
+
...(options.attemptId === undefined ? {} : { attemptId: options.attemptId }),
|
|
249
|
+
clockDomain: options.clockDomain ?? "caller",
|
|
250
|
+
};
|
|
251
|
+
this.activeRootId = rootId;
|
|
252
|
+
try {
|
|
253
|
+
return await this.span(state, rowId, {
|
|
254
|
+
face: state.face,
|
|
255
|
+
actorClass: state.actorClass,
|
|
256
|
+
...(state.actorKey === undefined ? {} : { actorKey: state.actorKey }),
|
|
257
|
+
...(state.attemptId === undefined ? {} : { attemptId: state.attemptId }),
|
|
258
|
+
clockDomain: state.clockDomain,
|
|
259
|
+
...(options.outcome === undefined ? {} : { outcome: options.outcome }),
|
|
260
|
+
...(options.httpStatus === undefined ? {} : { httpStatus: options.httpStatus }),
|
|
261
|
+
...(options.phaseOrdinal === undefined ? {} : { phaseOrdinal: options.phaseOrdinal }),
|
|
262
|
+
...(options.retryIndex === undefined ? {} : { retryIndex: options.retryIndex }),
|
|
263
|
+
...(options.memberIndex === undefined ? {} : { memberIndex: options.memberIndex }),
|
|
264
|
+
...(options.tag === undefined ? {} : { tag: options.tag }),
|
|
265
|
+
...(options.attributes === undefined ? {} : { attributes: options.attributes }),
|
|
266
|
+
...(options.zeroDurationPlatformLimited === undefined ? {} : { zeroDurationPlatformLimited: options.zeroDurationPlatformLimited }),
|
|
267
|
+
}, async (scope) => {
|
|
268
|
+
const result = await callback(scope);
|
|
269
|
+
// This has the same containment rule as a Worker waitUntil task: it
|
|
270
|
+
// is spawned while the response is being finalized, but is never
|
|
271
|
+
// awaited by S00. The production entrypoint intentionally supplies
|
|
272
|
+
// no callback here; exporters may use it without touching protocol.
|
|
273
|
+
void this.scheduleDetachedObservation();
|
|
274
|
+
return result;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
this.activeRootId = undefined;
|
|
279
|
+
this.activeRootRecord = undefined;
|
|
280
|
+
this.activeRootNative = undefined;
|
|
281
|
+
// A sink/exporter error is observation-only. It must not replace the
|
|
282
|
+
// response or durable outcome produced by the callback above.
|
|
283
|
+
this.publishSnapshot();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Returns the Worker-local rows for which a native tracing callback was
|
|
288
|
+
* actually entered on this request. This private-in-effect observation is
|
|
289
|
+
* copied only into sdt.observe/v1 by CommitWorker; it never changes the
|
|
290
|
+
* sealed trace schema, verifier, public response, or a control branch.
|
|
291
|
+
*/
|
|
292
|
+
emittedWorkerRowIds() {
|
|
293
|
+
return Object.freeze([...this.emittedWorkerRows]);
|
|
294
|
+
}
|
|
295
|
+
snapshot() {
|
|
296
|
+
const rootId = this.spans.find((span) => span.logicalParent === null)?.rootId ?? `${this.correlationId}:none`;
|
|
297
|
+
const spans = Object.freeze(this.spans.map((span) => Object.freeze({
|
|
298
|
+
...span,
|
|
299
|
+
attributes: Object.freeze({ ...span.attributes }),
|
|
300
|
+
})));
|
|
301
|
+
const accepted = this.options.schema === "sdt.commit/v1" && spans.some((span) => typeof span.attributes["attempt.id"] === "string");
|
|
302
|
+
const base = {
|
|
303
|
+
schema: this.options.schema,
|
|
304
|
+
rootId,
|
|
305
|
+
correlationId: this.correlationId,
|
|
306
|
+
serviceId: this.options.serviceId,
|
|
307
|
+
spans,
|
|
308
|
+
provider: this.provider,
|
|
309
|
+
diagnostics: this.diagnostics,
|
|
310
|
+
...(this.runtimeVerification === undefined ? {} : { runtimeVerification: this.runtimeVerification }),
|
|
311
|
+
};
|
|
312
|
+
if (!accepted)
|
|
313
|
+
return Object.freeze(base);
|
|
314
|
+
try {
|
|
315
|
+
const unattributed = calculateUnattributedRatio(base);
|
|
316
|
+
return Object.freeze({
|
|
317
|
+
...base,
|
|
318
|
+
unattributed,
|
|
319
|
+
attributionGatePassed: unattributed.unattributedRatio <= 0.05,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
// A malformed observation must be visible to evidence consumers, but
|
|
324
|
+
// its calculation cannot become a new commit failure.
|
|
325
|
+
this.noteObservationFailure(error);
|
|
326
|
+
return Object.freeze({
|
|
327
|
+
...base,
|
|
328
|
+
runtimeVerification: this.observationVerification(),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* A request becomes accepted after admission. Updating the still-active root
|
|
334
|
+
* preserves the single root correlation while S01 remains an honest
|
|
335
|
+
* pre-admission record with no attempt.id.
|
|
336
|
+
*/
|
|
337
|
+
markAccepted(attemptId) {
|
|
338
|
+
if (this.activeRootRecord === undefined)
|
|
339
|
+
return;
|
|
340
|
+
const correlationId = correlationIdForAttempt(attemptId);
|
|
341
|
+
// S00 begins before admission, but the accepted trace's root is the
|
|
342
|
+
// accepted face: S01 is the separately retained pre-admission row. Keep
|
|
343
|
+
// the two phases distinct rather than leaking an attempt ID into a
|
|
344
|
+
// pre-admission record (or leaving the accepted root without one).
|
|
345
|
+
const attributes = {
|
|
346
|
+
...this.activeRootRecord.attributes,
|
|
347
|
+
"correlation.id": correlationId,
|
|
348
|
+
"attempt.id": attemptId,
|
|
349
|
+
outcome: "accepted",
|
|
350
|
+
};
|
|
351
|
+
const record = Object.freeze({
|
|
352
|
+
...this.activeRootRecord,
|
|
353
|
+
face: "accepted",
|
|
354
|
+
attributes: Object.freeze(attributes),
|
|
355
|
+
});
|
|
356
|
+
const index = this.spans.indexOf(this.activeRootRecord);
|
|
357
|
+
if (index >= 0)
|
|
358
|
+
this.spans[index] = record;
|
|
359
|
+
this.activeRootRecord = record;
|
|
360
|
+
this.correlationId = correlationId;
|
|
361
|
+
this.setNativeAttributes(this.activeRootNative, {
|
|
362
|
+
"correlation.id": correlationId,
|
|
363
|
+
"attempt.id": attemptId,
|
|
364
|
+
outcome: "accepted",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Observation-only consumers can read the post-admission correlation after
|
|
369
|
+
* it has been written to S00. This exposes no wire field and never controls
|
|
370
|
+
* a commit decision or native-parent relationship.
|
|
371
|
+
*/
|
|
372
|
+
observationCorrelationId() {
|
|
373
|
+
return this.correlationId;
|
|
374
|
+
}
|
|
375
|
+
async span(state, rowId, options, callback) {
|
|
376
|
+
const disabledChild = () => new CommitTraceScope(this, {
|
|
377
|
+
rootId: state.rootId,
|
|
378
|
+
currentRowId: rowId,
|
|
379
|
+
face: options.face,
|
|
380
|
+
actorClass: options.actorClass,
|
|
381
|
+
...(options.actorKey === undefined ? {} : { actorKey: options.actorKey }),
|
|
382
|
+
...(options.attemptId === undefined ? {} : { attemptId: options.attemptId }),
|
|
383
|
+
clockDomain: options.clockDomain ?? state.clockDomain,
|
|
384
|
+
observationDisabled: true,
|
|
385
|
+
});
|
|
386
|
+
if (state.observationDisabled === true)
|
|
387
|
+
return callback(disabledChild());
|
|
388
|
+
let row;
|
|
389
|
+
let attributes;
|
|
390
|
+
try {
|
|
391
|
+
row = rowFor(this.options.schema, rowId);
|
|
392
|
+
if (row.logicalParent !== state.currentRowId) {
|
|
393
|
+
throw new Error(`Commit trace row ${rowId} must run under ${row.logicalParent ?? "a root"}, observed ${state.currentRowId ?? "a root"}`);
|
|
394
|
+
}
|
|
395
|
+
if (row.logicalParent === null && this.activeRootId !== state.rootId) {
|
|
396
|
+
throw new Error(`Commit trace root ${rowId} is not active`);
|
|
397
|
+
}
|
|
398
|
+
attributes = this.attributesFor(row, options);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
if (!this.isFailOpen())
|
|
402
|
+
throw error;
|
|
403
|
+
this.noteObservationFailure(error);
|
|
404
|
+
return callback(disabledChild());
|
|
405
|
+
}
|
|
406
|
+
const execute = async (native) => {
|
|
407
|
+
this.setNativeAttributes(native, nativeAttributesWithRowId(this.options.schema, rowId, attributes));
|
|
408
|
+
this.recordEmittedWorkerRow(row, native);
|
|
409
|
+
const startMs = this.now();
|
|
410
|
+
const record = {
|
|
411
|
+
rowId,
|
|
412
|
+
schema: this.options.schema,
|
|
413
|
+
face: options.face,
|
|
414
|
+
span: row.span,
|
|
415
|
+
emitter: row.emitter,
|
|
416
|
+
logicalParent: row.logicalParent,
|
|
417
|
+
rootId: state.rootId,
|
|
418
|
+
clockDomain: options.clockDomain ?? state.clockDomain,
|
|
419
|
+
startMs,
|
|
420
|
+
endMs: startMs,
|
|
421
|
+
present: true,
|
|
422
|
+
attributes: Object.freeze({ ...attributes }),
|
|
423
|
+
zeroDurationPlatformLimited: options.zeroDurationPlatformLimited ?? false,
|
|
424
|
+
};
|
|
425
|
+
this.spans.push(record);
|
|
426
|
+
if (row.logicalParent === null) {
|
|
427
|
+
this.activeRootRecord = record;
|
|
428
|
+
this.activeRootNative = native;
|
|
429
|
+
}
|
|
430
|
+
const child = new CommitTraceScope(this, {
|
|
431
|
+
rootId: state.rootId,
|
|
432
|
+
currentRowId: rowId,
|
|
433
|
+
face: options.face,
|
|
434
|
+
actorClass: options.actorClass,
|
|
435
|
+
...(options.actorKey === undefined ? {} : { actorKey: options.actorKey }),
|
|
436
|
+
...(options.attemptId === undefined ? {} : { attemptId: options.attemptId }),
|
|
437
|
+
clockDomain: options.clockDomain ?? state.clockDomain,
|
|
438
|
+
});
|
|
439
|
+
try {
|
|
440
|
+
const result = await callback(child);
|
|
441
|
+
const final = this.finish(record, native, this.outcomeFromResult(result, options.outcome, options.httpStatus));
|
|
442
|
+
if (row.logicalParent === null)
|
|
443
|
+
this.activeRootRecord = final;
|
|
444
|
+
return result;
|
|
445
|
+
}
|
|
446
|
+
catch (failure) {
|
|
447
|
+
const final = this.finish(record, native, {
|
|
448
|
+
outcome: options.outcome ?? "exception",
|
|
449
|
+
...(options.httpStatus === undefined ? {} : { httpStatus: options.httpStatus }),
|
|
450
|
+
});
|
|
451
|
+
if (row.logicalParent === null)
|
|
452
|
+
this.activeRootRecord = final;
|
|
453
|
+
throw failure;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const native = this.options.nativeTracing;
|
|
457
|
+
return native === undefined
|
|
458
|
+
? execute(undefined)
|
|
459
|
+
: this.enterNativeSpan(native, row.span, execute);
|
|
460
|
+
}
|
|
461
|
+
finish(existing, native, outcome) {
|
|
462
|
+
// markAccepted replaces the still-open S00 record so it can move from
|
|
463
|
+
// the pre-admission to accepted face. Finish the replacement rather than
|
|
464
|
+
// silently leaving its terminal outcome behind in the in-memory evidence
|
|
465
|
+
// adapter. Normal spans retain identity and follow the fast path.
|
|
466
|
+
const current = this.activeRootRecord !== undefined && existing.logicalParent === null
|
|
467
|
+
? this.activeRootRecord
|
|
468
|
+
: existing;
|
|
469
|
+
const attributes = {
|
|
470
|
+
...current.attributes,
|
|
471
|
+
...(outcome?.outcome === undefined ? {} : { outcome: outcome.outcome }),
|
|
472
|
+
...(outcome?.httpStatus === undefined ? {} : { "http.status": outcome.httpStatus }),
|
|
473
|
+
};
|
|
474
|
+
const endMs = this.now();
|
|
475
|
+
const next = Object.freeze({
|
|
476
|
+
...current,
|
|
477
|
+
endMs,
|
|
478
|
+
// Cloudflare may quantize custom spans to a zero-length interval. The
|
|
479
|
+
// row remains present; duration is not used as a proxy for emission.
|
|
480
|
+
zeroDurationPlatformLimited: current.zeroDurationPlatformLimited || endMs === current.startMs,
|
|
481
|
+
attributes: Object.freeze(attributes),
|
|
482
|
+
});
|
|
483
|
+
const index = this.spans.indexOf(current);
|
|
484
|
+
if (index >= 0)
|
|
485
|
+
this.spans[index] = next;
|
|
486
|
+
this.setNativeAttributes(native, {
|
|
487
|
+
...(outcome?.outcome === undefined ? {} : { outcome: outcome.outcome }),
|
|
488
|
+
...(outcome?.httpStatus === undefined ? {} : { "http.status": outcome.httpStatus }),
|
|
489
|
+
});
|
|
490
|
+
return next;
|
|
491
|
+
}
|
|
492
|
+
recordEmittedWorkerRow(row, native) {
|
|
493
|
+
// `enterNativeSpan` calls this callback only after the platform has
|
|
494
|
+
// accepted the child callback. A declined/untraced span remains absent
|
|
495
|
+
// from the inventory so the exporter can distinguish local emission from
|
|
496
|
+
// a later provider-ingestion loss. DO and provider-subrequest rows are
|
|
497
|
+
// intentionally outside this Worker-local inventory.
|
|
498
|
+
if (native === undefined || native.isTraced === false || !isWorkerLocalEmitter(row))
|
|
499
|
+
return;
|
|
500
|
+
this.emittedWorkerRows.add(row.rowId);
|
|
501
|
+
}
|
|
502
|
+
attributesFor(row, options) {
|
|
503
|
+
const attributes = {
|
|
504
|
+
"schema.version": this.options.schema,
|
|
505
|
+
"correlation.id": this.correlationId,
|
|
506
|
+
"service.id": this.options.serviceId,
|
|
507
|
+
"actor.class": options.actorClass,
|
|
508
|
+
operation: row.span,
|
|
509
|
+
"span.kind": row.kind,
|
|
510
|
+
outcome: options.outcome ?? "in-progress",
|
|
511
|
+
};
|
|
512
|
+
if (options.actorKey !== undefined)
|
|
513
|
+
attributes["actor.key_hash"] = stableTraceHash(options.actorKey);
|
|
514
|
+
if (options.attemptId !== undefined)
|
|
515
|
+
attributes["attempt.id"] = options.attemptId;
|
|
516
|
+
if (options.phaseOrdinal !== undefined)
|
|
517
|
+
attributes["phase.ordinal"] = options.phaseOrdinal;
|
|
518
|
+
if (options.retryIndex !== undefined)
|
|
519
|
+
attributes["retry.index"] = options.retryIndex;
|
|
520
|
+
if (options.memberIndex !== undefined)
|
|
521
|
+
attributes["member.index"] = options.memberIndex;
|
|
522
|
+
if (options.tag !== undefined)
|
|
523
|
+
attributes["tag.key_hash"] = stableTraceHash(options.tag);
|
|
524
|
+
if (options.httpStatus !== undefined)
|
|
525
|
+
attributes["http.status"] = options.httpStatus;
|
|
526
|
+
if (this.provider.scriptVersion !== undefined)
|
|
527
|
+
attributes["script.version"] = this.provider.scriptVersion;
|
|
528
|
+
if (this.provider.colo !== undefined)
|
|
529
|
+
attributes.colo = this.provider.colo;
|
|
530
|
+
if (this.provider.placement !== undefined)
|
|
531
|
+
attributes.placement = this.provider.placement;
|
|
532
|
+
for (const [key, value] of Object.entries(options.attributes ?? {})) {
|
|
533
|
+
attributes[key] = asTraceValue(value, key);
|
|
534
|
+
}
|
|
535
|
+
assertNoRawTagAttributes(attributes);
|
|
536
|
+
for (const [attribute, declaration] of Object.entries(manifest.attributeMatrix.attributes)) {
|
|
537
|
+
const present = hasOwn(attributes, attribute);
|
|
538
|
+
const state = rowScopedState(declaration, options.face, row.rowId);
|
|
539
|
+
if (state === "required" && !present) {
|
|
540
|
+
throw new Error(`Commit trace row ${row.rowId} is missing required ${attribute} on ${options.face}`);
|
|
541
|
+
}
|
|
542
|
+
if (state === "forbidden" && present) {
|
|
543
|
+
throw new Error(`Commit trace row ${row.rowId} must not emit ${attribute} on ${options.face}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Every emitted schema attribute must have an authority-matrix row,
|
|
547
|
+
// except diagnostics which intentionally live out-of-band in snapshots.
|
|
548
|
+
for (const attribute of Object.keys(attributes)) {
|
|
549
|
+
faceState(attribute, options.face);
|
|
550
|
+
}
|
|
551
|
+
assertAttributeTypes(attributes);
|
|
552
|
+
return attributes;
|
|
553
|
+
}
|
|
554
|
+
setNativeAttributes(native, attributes) {
|
|
555
|
+
if (native === undefined || native.isTraced === false)
|
|
556
|
+
return;
|
|
557
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
558
|
+
try {
|
|
559
|
+
native.setAttribute(key, value);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
this.noteObservationFailure(error);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
outcomeFromResult(result, explicit, fallbackHttpStatus) {
|
|
567
|
+
const response = result instanceof Response
|
|
568
|
+
? result
|
|
569
|
+
: typeof result === "object" && result !== null && "response" in result && result.response instanceof Response
|
|
570
|
+
? result.response
|
|
571
|
+
: undefined;
|
|
572
|
+
return {
|
|
573
|
+
outcome: explicit ?? (response === undefined ? "success" : response.ok ? "success" : `http-${response.status}`),
|
|
574
|
+
...(response === undefined ? fallbackHttpStatus === undefined ? {} : { httpStatus: fallbackHttpStatus } : { httpStatus: response.status }),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
clock() {
|
|
578
|
+
return this.options.clock ?? DEFAULT_CLOCK;
|
|
579
|
+
}
|
|
580
|
+
now() {
|
|
581
|
+
try {
|
|
582
|
+
return this.clock().now();
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
this.noteObservationFailure(error);
|
|
586
|
+
// The timestamp is evidence-only. Fall back to the process clock so
|
|
587
|
+
// a broken injected/provider clock cannot change the observed commit.
|
|
588
|
+
return Date.now();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
publishSnapshot() {
|
|
592
|
+
let beforeVerification;
|
|
593
|
+
try {
|
|
594
|
+
beforeVerification = this.snapshot();
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
this.noteObservationFailure(error);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (this.options.runtimeVerifier !== undefined) {
|
|
601
|
+
try {
|
|
602
|
+
if (this.observationFailureCode !== undefined) {
|
|
603
|
+
this.runtimeVerification = this.observationVerification();
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
this.options.runtimeVerifier.verify(beforeVerification);
|
|
607
|
+
this.runtimeVerification = Object.freeze({ passed: true });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
// G30 observes the existing protocol. A trace defect must be retained
|
|
612
|
+
// for the sink/evidence lane but can never turn a successful commit
|
|
613
|
+
// into a new rejection or retry.
|
|
614
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
615
|
+
? error.code
|
|
616
|
+
: "runtime-verifier-error";
|
|
617
|
+
this.runtimeVerification = Object.freeze({ passed: false, code });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
this.options.sink?.record(this.snapshot());
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
// Export/sink loss is recorded in-process when a later observer asks
|
|
625
|
+
// for a snapshot, but it can never replace the application result.
|
|
626
|
+
this.noteObservationFailure(error);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* A telemetry/export failure is retained as an observation failure, but a
|
|
631
|
+
* detached task is never allowed to hold the root open or replace the
|
|
632
|
+
* application's Response. Keeping this as a method makes the exact
|
|
633
|
+
* `void` call at root finalization mutation-testable.
|
|
634
|
+
*/
|
|
635
|
+
async scheduleDetachedObservation() {
|
|
636
|
+
try {
|
|
637
|
+
await this.options.scheduleDetachedObservation?.();
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
this.noteObservationFailure(error);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
isFailOpen() {
|
|
644
|
+
return this.options.failOpen !== false;
|
|
645
|
+
}
|
|
646
|
+
observationVerification() {
|
|
647
|
+
return Object.freeze({ passed: false, code: this.observationFailureCode ?? "trace-observation-error" });
|
|
648
|
+
}
|
|
649
|
+
noteObservationFailure(error) {
|
|
650
|
+
if (this.observationFailureCode !== undefined)
|
|
651
|
+
return;
|
|
652
|
+
this.observationFailureCode = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
653
|
+
? error.code
|
|
654
|
+
: "trace-observation-error";
|
|
655
|
+
this.runtimeVerification = this.observationVerification();
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* If a platform tracer declines a span before invoking its callback, run
|
|
659
|
+
* the application callback once without a span. Once the callback has
|
|
660
|
+
* started, its own error remains authoritative and is never replayed.
|
|
661
|
+
*/
|
|
662
|
+
async enterNativeSpan(native, name, callback) {
|
|
663
|
+
let callbackStarted = false;
|
|
664
|
+
const wrapped = (span) => {
|
|
665
|
+
callbackStarted = true;
|
|
666
|
+
return callback(span);
|
|
667
|
+
};
|
|
668
|
+
try {
|
|
669
|
+
const result = native.enterSpan(name, wrapped);
|
|
670
|
+
if (!callbackStarted) {
|
|
671
|
+
this.noteObservationFailure(new Error("native tracer returned without invoking callback"));
|
|
672
|
+
return callback(undefined);
|
|
673
|
+
}
|
|
674
|
+
return await result;
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (callbackStarted)
|
|
678
|
+
throw error;
|
|
679
|
+
this.noteObservationFailure(error);
|
|
680
|
+
return callback(undefined);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Constructor-only activation observation. It never touches Durable Object
|
|
686
|
+
* storage and callers must invoke beginHandler synchronously before awaiting.
|
|
687
|
+
*/
|
|
688
|
+
export class DurableObjectActivation {
|
|
689
|
+
activationId = crypto.randomUUID();
|
|
690
|
+
constructedAtMs = Date.now();
|
|
691
|
+
first = true;
|
|
692
|
+
beginHandler() {
|
|
693
|
+
const first = this.first;
|
|
694
|
+
this.first = false;
|
|
695
|
+
const handlerStartedAtMs = Date.now();
|
|
696
|
+
return Object.freeze({
|
|
697
|
+
activationId: this.activationId,
|
|
698
|
+
first,
|
|
699
|
+
handlerStartedAtMs,
|
|
700
|
+
constructorToHandlerMs: Math.max(0, handlerStartedAtMs - this.constructedAtMs),
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// Workerd forbids random generation while evaluating a module. Keep the
|
|
705
|
+
// isolate identity module-scoped once observed, but create it at the first
|
|
706
|
+
// request-handler boundary rather than during Worker startup.
|
|
707
|
+
let workerIsolateInstanceId;
|
|
708
|
+
let workerIsolateFirstInvocation = true;
|
|
709
|
+
export function beginWorkerInvocationObservation() {
|
|
710
|
+
workerIsolateInstanceId ??= crypto.randomUUID();
|
|
711
|
+
const firstInvocation = workerIsolateFirstInvocation;
|
|
712
|
+
workerIsolateFirstInvocation = false;
|
|
713
|
+
return Object.freeze({ isolateInstanceId: workerIsolateInstanceId, firstInvocation });
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* This deliberately accepts only an externally collected complete ledger.
|
|
717
|
+
* The DO runtime never records timestamps or idle observations in storage.
|
|
718
|
+
*/
|
|
719
|
+
export function observedIdleGapLowerBoundMs(previous, currentStartMs) {
|
|
720
|
+
if (previous === undefined || !previous.complete || !Number.isFinite(previous.endMs) || !Number.isFinite(currentStartMs))
|
|
721
|
+
return null;
|
|
722
|
+
return Math.max(0, currentStartMs - previous.endMs);
|
|
723
|
+
}
|
|
724
|
+
export const IDLE_EXPERIMENT_SCHEDULE_MS = Object.freeze([2_000, 15_000, 180_000]);
|
|
725
|
+
export function classifyReactivationCause(evidence) {
|
|
726
|
+
if (evidence.deployedVersionChanged)
|
|
727
|
+
return "deployment-correlated";
|
|
728
|
+
if (typeof evidence.platformEvidence === "string" && evidence.platformEvidence.length > 0)
|
|
729
|
+
return "platform-evidenced";
|
|
730
|
+
return "unknown";
|
|
731
|
+
}
|
|
732
|
+
function assertNativeFaceAttributes(schema, face, rowId, attributes, options = {}) {
|
|
733
|
+
const row = rowFor(schema, rowId);
|
|
734
|
+
if (attributes.operation !== row.span) {
|
|
735
|
+
throw new Error(`Native commit trace row ${rowId} operation must equal ${row.span}`);
|
|
736
|
+
}
|
|
737
|
+
if (attributes["span.kind"] !== row.kind) {
|
|
738
|
+
throw new Error(`Native commit trace row ${rowId} span.kind must equal ${row.kind}`);
|
|
739
|
+
}
|
|
740
|
+
assertNoRawTagAttributes(attributes);
|
|
741
|
+
for (const [attribute, declaration] of Object.entries(manifest.attributeMatrix.attributes)) {
|
|
742
|
+
const present = hasOwn(attributes, attribute);
|
|
743
|
+
const state = rowScopedState(declaration, face, rowId);
|
|
744
|
+
if (state === "required" && !present && !(options.allowDeferredFactDerived === true && declaration.factDerived === true)) {
|
|
745
|
+
throw new Error(`Native commit trace row ${rowId} is missing required ${attribute} on ${face}`);
|
|
746
|
+
}
|
|
747
|
+
if (state === "forbidden" && present) {
|
|
748
|
+
throw new Error(`Native commit trace row ${rowId} must not emit ${attribute} on ${face}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
for (const attribute of Object.keys(attributes)) {
|
|
752
|
+
faceState(attribute, face);
|
|
753
|
+
}
|
|
754
|
+
assertAttributeTypes(attributes);
|
|
755
|
+
}
|
|
756
|
+
function nativeAttributesAreValid(schema, face, rowId, attributes, options = {}) {
|
|
757
|
+
try {
|
|
758
|
+
assertNativeFaceAttributes(schema, face, rowId, attributes, options);
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
// The static manifest and mutation lanes make this observable. At a
|
|
763
|
+
// production callback boundary it must remain telemetry loss, never a
|
|
764
|
+
// new application rejection or alarm retry.
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function nativeTracingEnabled(span) {
|
|
769
|
+
return span !== undefined && span.isTraced !== false;
|
|
770
|
+
}
|
|
771
|
+
function setNativeAttribute(span, key, value) {
|
|
772
|
+
if (!nativeTracingEnabled(span))
|
|
773
|
+
return;
|
|
774
|
+
try {
|
|
775
|
+
span.setAttribute(key, value);
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Span export is explicitly non-semantic.
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function setNativeAttributes(span, attributes) {
|
|
782
|
+
for (const [key, value] of Object.entries(attributes))
|
|
783
|
+
setNativeAttribute(span, key, value);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* `sdt.row.id` is an exporter-facing native-span discriminator, not a
|
|
787
|
+
* commit-snapshot attribute. Keep it outside the frozen authority matrix so
|
|
788
|
+
* it cannot alter validation, reservation, fence, response, or the in-memory
|
|
789
|
+
* evidence contract; add it only after that matrix has accepted the normal
|
|
790
|
+
* attributes for a real sdt.commit/v1 S-row.
|
|
791
|
+
*/
|
|
792
|
+
function nativeAttributesWithRowId(schema, rowId, attributes) {
|
|
793
|
+
if (schema !== "sdt.commit/v1" || !/^S\d/.test(rowId))
|
|
794
|
+
return { ...attributes };
|
|
795
|
+
return { ...attributes, [NATIVE_ROW_ID_ATTRIBUTE]: rowId };
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* A tracer that fails before callback invocation simply loses this span. A
|
|
799
|
+
* callback that did begin is never run a second time: its normal result or
|
|
800
|
+
* error remains the protocol authority.
|
|
801
|
+
*/
|
|
802
|
+
async function enterNativeSpanFailOpen(nativeTracing, name, callback) {
|
|
803
|
+
let callbackStarted = false;
|
|
804
|
+
const wrapped = (span) => {
|
|
805
|
+
callbackStarted = true;
|
|
806
|
+
return callback(span);
|
|
807
|
+
};
|
|
808
|
+
try {
|
|
809
|
+
const result = nativeTracing.enterSpan(name, wrapped);
|
|
810
|
+
if (!callbackStarted)
|
|
811
|
+
return callback(undefined);
|
|
812
|
+
return await result;
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
if (callbackStarted)
|
|
816
|
+
throw error;
|
|
817
|
+
return callback(undefined);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Emit a real Cloudflare custom span in the currently active async context.
|
|
822
|
+
* There is intentionally no parent argument: Worker/DO parentage is the
|
|
823
|
+
* platform's concern, and this function only supplies manifest attributes.
|
|
824
|
+
*/
|
|
825
|
+
export async function enterNativeCommitSpan(nativeTracing, name, input, callback) {
|
|
826
|
+
const attributes = {
|
|
827
|
+
"schema.version": input.schema,
|
|
828
|
+
"correlation.id": input.correlationId,
|
|
829
|
+
"service.id": input.serviceId,
|
|
830
|
+
"actor.class": input.actorClass,
|
|
831
|
+
"actor.key_hash": stableTraceHash(input.actorKey),
|
|
832
|
+
"activation.id": input.activation.activationId,
|
|
833
|
+
"activation.first": input.activation.first,
|
|
834
|
+
operation: input.operation,
|
|
835
|
+
"span.kind": input.kind,
|
|
836
|
+
outcome: "in-progress",
|
|
837
|
+
...(input.attributes ?? {}),
|
|
838
|
+
};
|
|
839
|
+
if (input.attemptId !== undefined)
|
|
840
|
+
attributes["attempt.id"] = input.attemptId;
|
|
841
|
+
if (!nativeAttributesAreValid(input.schema, input.face, input.rowId, attributes)) {
|
|
842
|
+
return callback();
|
|
843
|
+
}
|
|
844
|
+
return enterNativeSpanFailOpen(nativeTracing, name, async (span) => {
|
|
845
|
+
setNativeAttributes(span, nativeAttributesWithRowId(input.schema, input.rowId, attributes));
|
|
846
|
+
try {
|
|
847
|
+
const result = await callback();
|
|
848
|
+
const response = result instanceof Response
|
|
849
|
+
? result
|
|
850
|
+
: typeof result === "object" && result !== null && "response" in result && result.response instanceof Response
|
|
851
|
+
? result.response
|
|
852
|
+
: undefined;
|
|
853
|
+
if (nativeTracingEnabled(span)) {
|
|
854
|
+
setNativeAttribute(span, "outcome", response === undefined ? "success" : response.ok ? "success" : `http-${response.status}`);
|
|
855
|
+
// Alarm-root rows have no HTTP response surface, and the manifest
|
|
856
|
+
// forbids http.status for that face even when a test harness happens
|
|
857
|
+
// to represent an internal result as Response.
|
|
858
|
+
if (response !== undefined && input.face !== "reconcile-root")
|
|
859
|
+
setNativeAttribute(span, "http.status", response.status);
|
|
860
|
+
}
|
|
861
|
+
return result;
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
setNativeAttribute(span, "outcome", "exception");
|
|
865
|
+
throw error;
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Emits S16 at the Durable Object handler boundary. Identity may only become
|
|
871
|
+
* available after a clone of the internal request body is decoded, but the
|
|
872
|
+
* custom span itself begins synchronously at handler entry. Failure to derive
|
|
873
|
+
* an internal identity merely leaves that non-commit actor invocation outside
|
|
874
|
+
* the sdt.commit universe; it never changes the request's control path.
|
|
875
|
+
*/
|
|
876
|
+
export async function enterNativeActorHandleSpan(nativeTracing, input, resolveIdentity, callback) {
|
|
877
|
+
return enterNativeSpanFailOpen(nativeTracing, "actor.handle", async (span) => {
|
|
878
|
+
let identity;
|
|
879
|
+
try {
|
|
880
|
+
identity = await resolveIdentity();
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// Observation is deliberately fail-open. Parsing a cloned body must
|
|
884
|
+
// never turn an internal actor request into a new rejection.
|
|
885
|
+
identity = undefined;
|
|
886
|
+
}
|
|
887
|
+
if (identity !== undefined) {
|
|
888
|
+
input.observation?.bindCorrelation(correlationIdForAttempt(identity.attemptId));
|
|
889
|
+
const attributes = {
|
|
890
|
+
"schema.version": "sdt.commit/v1",
|
|
891
|
+
"correlation.id": correlationIdForAttempt(identity.attemptId),
|
|
892
|
+
"attempt.id": identity.attemptId,
|
|
893
|
+
"service.id": identity.serviceId,
|
|
894
|
+
"actor.class": input.actorClass,
|
|
895
|
+
"actor.key_hash": stableTraceHash(identity.actorKey ?? input.actorKey),
|
|
896
|
+
"activation.id": input.activation.activationId,
|
|
897
|
+
"activation.first": input.activation.first,
|
|
898
|
+
operation: "actor.handle",
|
|
899
|
+
"span.kind": "callee",
|
|
900
|
+
outcome: "in-progress",
|
|
901
|
+
};
|
|
902
|
+
if (nativeAttributesAreValid("sdt.commit/v1", "accepted", "S16", attributes)) {
|
|
903
|
+
setNativeAttributes(span, nativeAttributesWithRowId("sdt.commit/v1", "S16", attributes));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
const result = await callback();
|
|
908
|
+
if (identity !== undefined && nativeTracingEnabled(span)) {
|
|
909
|
+
const response = result instanceof Response
|
|
910
|
+
? result
|
|
911
|
+
: typeof result === "object" && result !== null && "response" in result && result.response instanceof Response
|
|
912
|
+
? result.response
|
|
913
|
+
: undefined;
|
|
914
|
+
setNativeAttribute(span, "outcome", response === undefined ? "success" : response.ok ? "success" : `http-${response.status}`);
|
|
915
|
+
if (response !== undefined)
|
|
916
|
+
setNativeAttribute(span, "http.status", response.status);
|
|
917
|
+
}
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
if (identity !== undefined)
|
|
922
|
+
setNativeAttribute(span, "outcome", "exception");
|
|
923
|
+
throw error;
|
|
924
|
+
}
|
|
925
|
+
finally {
|
|
926
|
+
// The structured log is emitted while the actor span is still active.
|
|
927
|
+
// Its existing attempt correlation is bound above; no header or request
|
|
928
|
+
// shape is added merely to label observation telemetry.
|
|
929
|
+
input.observation?.finish();
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Emit R00 at actual alarm-handler entry. The Journal must read the durable
|
|
935
|
+
* record to discover the generation/attempt identity, so resolving it inside
|
|
936
|
+
* the active callback is the only way to keep both the entry boundary and
|
|
937
|
+
* Cloudflare-owned parentage without synthetic parent IDs.
|
|
938
|
+
*/
|
|
939
|
+
export async function enterNativeReconcileRootSpan(nativeTracing, resolveIdentity, callback) {
|
|
940
|
+
return enterNativeSpanFailOpen(nativeTracing, "sdt.commit.reconcile", async (span) => {
|
|
941
|
+
let identity;
|
|
942
|
+
let attributes;
|
|
943
|
+
let recoveryKind;
|
|
944
|
+
const facts = {
|
|
945
|
+
setRecoveryKind: (kind) => {
|
|
946
|
+
recoveryKind = kind;
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
try {
|
|
950
|
+
identity = await resolveIdentity();
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// A missing/corrupt Journal record cannot be turned into a different
|
|
954
|
+
// alarm outcome by attribution. The handler preserves its normal
|
|
955
|
+
// fail-closed path while this non-commit invocation remains unlabelled.
|
|
956
|
+
identity = undefined;
|
|
957
|
+
}
|
|
958
|
+
if (identity !== undefined) {
|
|
959
|
+
attributes = {
|
|
960
|
+
"schema.version": "sdt.commit.reconcile/v1",
|
|
961
|
+
"correlation.id": correlationIdForAttempt(identity.attemptId),
|
|
962
|
+
"attempt.id": identity.attemptId,
|
|
963
|
+
"service.id": identity.serviceId,
|
|
964
|
+
"actor.class": "JOURNAL",
|
|
965
|
+
"actor.key_hash": stableTraceHash(identity.actorKey),
|
|
966
|
+
"activation.id": identity.activation.activationId,
|
|
967
|
+
"activation.first": identity.activation.first,
|
|
968
|
+
operation: "sdt.commit.reconcile",
|
|
969
|
+
"span.kind": "root",
|
|
970
|
+
outcome: "in-progress",
|
|
971
|
+
"alarm.event.id": identity.alarmEventId,
|
|
972
|
+
"alarm.invocation.id": identity.invocationId,
|
|
973
|
+
"alarm.retryCount": identity.retryCount,
|
|
974
|
+
"alarm.isRetry": identity.isRetry,
|
|
975
|
+
"durable.prefix.at_entry": identity.prefixAtEntry,
|
|
976
|
+
};
|
|
977
|
+
// recovery.kind is the one fact-derived R00 attribute. It is checked
|
|
978
|
+
// after the recovery callback establishes the durable branch.
|
|
979
|
+
if (nativeAttributesAreValid("sdt.commit.reconcile/v1", "reconcile-root", "R00", attributes, { allowDeferredFactDerived: true })) {
|
|
980
|
+
setNativeAttributes(span, attributes);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const finish = (outcome) => {
|
|
984
|
+
if (identity === undefined || attributes === undefined || !nativeTracingEnabled(span))
|
|
985
|
+
return;
|
|
986
|
+
if (recoveryKind === undefined) {
|
|
987
|
+
// Trace-only behavior: an incomplete observation cannot alter the
|
|
988
|
+
// alarm result. The export verifier retains the missing required fact.
|
|
989
|
+
setNativeAttribute(span, "outcome", outcome);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const completed = { ...attributes, "recovery.kind": recoveryKind, outcome };
|
|
993
|
+
if (!nativeAttributesAreValid("sdt.commit.reconcile/v1", "reconcile-root", "R00", completed))
|
|
994
|
+
return;
|
|
995
|
+
setNativeAttribute(span, "recovery.kind", recoveryKind);
|
|
996
|
+
setNativeAttribute(span, "outcome", outcome);
|
|
997
|
+
};
|
|
998
|
+
try {
|
|
999
|
+
const result = await callback(identity, facts);
|
|
1000
|
+
finish("success");
|
|
1001
|
+
return result;
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
finish("exception");
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
}
|