@telorun/kernel 0.31.0 → 0.33.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/dist/dependency-injection.d.ts +1 -1
- package/dist/dependency-injection.d.ts.map +1 -1
- package/dist/dependency-injection.js +18 -4
- package/dist/dependency-injection.js.map +1 -1
- package/dist/evaluation-context.d.ts +58 -1
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +205 -30
- package/dist/evaluation-context.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +6 -0
- package/dist/events.js.map +1 -1
- package/dist/kernel.d.ts +1 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +16 -4
- package/dist/kernel.js.map +1 -1
- package/dist/module-context.d.ts +4 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +77 -15
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +16 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +52 -1
- package/dist/resource-context.js.map +1 -1
- package/dist/tracing.d.ts +3 -0
- package/dist/tracing.d.ts.map +1 -1
- package/dist/tracing.js +6 -0
- package/dist/tracing.js.map +1 -1
- package/package.json +2 -2
- package/src/dependency-injection.ts +21 -3
- package/src/evaluation-context.ts +308 -45
- package/src/events.ts +5 -0
- package/src/kernel.ts +20 -4
- package/src/module-context.ts +100 -17
- package/src/resource-context.ts +58 -1
- package/src/tracing.ts +7 -0
package/src/module-context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { executeInvokeStep, RuntimeError } from "@telorun/sdk";
|
|
1
|
+
import { executeInvokeStep, getRefIdentity, RuntimeError } from "@telorun/sdk";
|
|
2
2
|
import type {
|
|
3
3
|
BootTarget,
|
|
4
4
|
ControllerPolicy,
|
|
@@ -432,6 +432,29 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
432
432
|
return `${realModule}.${suffix}`;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
protected override resolveKindSafe(kind: string): string {
|
|
436
|
+
// `resolveKind` throws for unqualified / ungated kinds — an expected signal,
|
|
437
|
+
// not a failure: a capability probe that can't resolve falls back to the raw
|
|
438
|
+
// kind (lookup misses → treated as non-service → keeps its ALS scope).
|
|
439
|
+
try {
|
|
440
|
+
return this.resolveKind(kind);
|
|
441
|
+
} catch {
|
|
442
|
+
return kind;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
protected override traceRootScope(): Record<string, unknown> {
|
|
447
|
+
// Mask secret values (keep keys so availability is visible, never the value).
|
|
448
|
+
const secrets: Record<string, unknown> = {};
|
|
449
|
+
for (const key of Object.keys(this._secrets)) secrets[key] = "[secret]";
|
|
450
|
+
return {
|
|
451
|
+
variables: this._variables,
|
|
452
|
+
secrets,
|
|
453
|
+
resources: this._resources,
|
|
454
|
+
ports: this._ports,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
435
458
|
private _rebuildContext(): void {
|
|
436
459
|
this._context = {
|
|
437
460
|
variables: this._variables,
|
|
@@ -473,26 +496,84 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
473
496
|
await super.run(name, ctx);
|
|
474
497
|
}
|
|
475
498
|
|
|
476
|
-
async runTargets(ctx?: InvokeContext) {
|
|
499
|
+
async runTargets(ctx?: InvokeContext, appName?: string) {
|
|
500
|
+
// Open a trace span for the application's boot run so the app itself is a
|
|
501
|
+
// trace participant: every target dispatches under `targetCtx`, parenting to
|
|
502
|
+
// this span instead of becoming a detached root. The span is gated on tracing
|
|
503
|
+
// (zero cost otherwise) and rides the boot cancellation token unchanged.
|
|
504
|
+
const tracing = this.tracer?.enabled === true;
|
|
505
|
+
const appSpanId = tracing ? this.tracer!.next() : undefined;
|
|
506
|
+
// The boot run roots a fresh trace; targets inherit it via `targetCtx`.
|
|
507
|
+
const appTraceId = tracing ? this.tracer!.newTraceId() : undefined;
|
|
508
|
+
const appRootScope = tracing ? this.traceRootScope() : undefined;
|
|
509
|
+
const appLabel = appName ?? "application";
|
|
510
|
+
const appSpan = (
|
|
511
|
+
phase: "start" | "end",
|
|
512
|
+
outcome: "ok" | "failed" | "cancelled" | undefined,
|
|
513
|
+
) =>
|
|
514
|
+
this.tracePayload(
|
|
515
|
+
"Telo.Application",
|
|
516
|
+
appLabel,
|
|
517
|
+
appSpanId,
|
|
518
|
+
undefined,
|
|
519
|
+
appTraceId,
|
|
520
|
+
"run",
|
|
521
|
+
phase,
|
|
522
|
+
outcome,
|
|
523
|
+
phase === "end" && appRootScope ? { context: appRootScope } : {},
|
|
524
|
+
);
|
|
525
|
+
const targetCtx: InvokeContext | undefined =
|
|
526
|
+
tracing && ctx
|
|
527
|
+
? {
|
|
528
|
+
cancellation: ctx.cancellation,
|
|
529
|
+
invocationId: appSpanId,
|
|
530
|
+
parentInvocationId: undefined,
|
|
531
|
+
traceId: appTraceId,
|
|
532
|
+
}
|
|
533
|
+
: ctx;
|
|
534
|
+
|
|
477
535
|
const steps: Record<string, unknown> = {};
|
|
478
536
|
const stepCtx: InvokeStepContext = {
|
|
479
537
|
expandValue: (value, context) => this.expandWith(value, context),
|
|
480
|
-
invoke: (kind, name, inputs) => this.invoke(kind, name, inputs,
|
|
538
|
+
invoke: (kind, name, inputs) => this.invoke(kind, name, inputs, targetCtx),
|
|
481
539
|
invokeResolved: (kind, name, instance, inputs) =>
|
|
482
|
-
this.invokeResolved(kind, name, instance, inputs,
|
|
540
|
+
this.invokeResolved(kind, name, instance, inputs, targetCtx),
|
|
483
541
|
resolveImportedInstance: (alias, name) => this.resolveImportedInstance(alias, name),
|
|
484
542
|
};
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
543
|
+
// Route a pre-resolved boot target through the instrumented chokepoint
|
|
544
|
+
// (`runResolved`) instead of calling `instance.run()` directly, so it emits a
|
|
545
|
+
// run span nested under the app. Kind/name come from the `!ref` identity the
|
|
546
|
+
// kernel stamped at injection, falling back to a positional label.
|
|
547
|
+
const runResolvedInstance = async (inst: ResourceInstance, kind: string, name: string) =>
|
|
548
|
+
this.runResolved(kind, name, inst, targetCtx);
|
|
549
|
+
|
|
550
|
+
if (tracing) await this.emit(`${appLabel}.Running`, appSpan("start", undefined));
|
|
551
|
+
try {
|
|
552
|
+
await this.dispatchTargets(stepCtx, steps, runResolvedInstance, targetCtx);
|
|
553
|
+
if (tracing) await this.emit(`${appLabel}.Run`, appSpan("end", "ok"));
|
|
554
|
+
} catch (err) {
|
|
555
|
+
if (tracing) {
|
|
556
|
+
const cancelled = (err as { code?: unknown })?.code === "ERR_INVOKE_CANCELLED";
|
|
557
|
+
await this.emit(
|
|
558
|
+
`${appLabel}.${cancelled ? "RunCancelled" : "RunFailed"}`,
|
|
559
|
+
appSpan("end", cancelled ? "cancelled" : "failed"),
|
|
493
560
|
);
|
|
494
561
|
}
|
|
495
|
-
|
|
562
|
+
throw err;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private async dispatchTargets(
|
|
567
|
+
stepCtx: InvokeStepContext,
|
|
568
|
+
steps: Record<string, unknown>,
|
|
569
|
+
runResolvedInstance: (inst: ResourceInstance, kind: string, name: string) => Promise<void>,
|
|
570
|
+
ctx?: InvokeContext,
|
|
571
|
+
) {
|
|
572
|
+
// Recover the kind+name the kernel stamped onto a `!ref`-injected instance,
|
|
573
|
+
// so the run span is properly labelled; fall back to a positional name.
|
|
574
|
+
const idOf = (inst: ResourceInstance, fallbackName: string) => {
|
|
575
|
+
const id = getRefIdentity(inst as object);
|
|
576
|
+
return { kind: id?.kind ?? "", name: id?.name ?? fallbackName };
|
|
496
577
|
};
|
|
497
578
|
for (let i = 0; i < this.targets.length; i++) {
|
|
498
579
|
const target = this.targets[i]!;
|
|
@@ -524,7 +605,8 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
524
605
|
// Phase 5 injection may have replaced the ref slot with the live
|
|
525
606
|
// instance; run it directly.
|
|
526
607
|
if (isRunnableInstance(ref)) {
|
|
527
|
-
|
|
608
|
+
const id = idOf(ref, `target[${i}]`);
|
|
609
|
+
await runResolvedInstance(ref, id.kind, id.name);
|
|
528
610
|
} else {
|
|
529
611
|
const r =
|
|
530
612
|
ref && typeof ref === "object"
|
|
@@ -537,7 +619,7 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
537
619
|
`Boot target '${r.alias}.${r.name}' is not a runnable exported instance`,
|
|
538
620
|
);
|
|
539
621
|
}
|
|
540
|
-
await runResolvedInstance(inst,
|
|
622
|
+
await runResolvedInstance(inst, getRefIdentity(inst as object)?.kind ?? "", r.name);
|
|
541
623
|
} else {
|
|
542
624
|
await this.run(typeof ref === "string" ? ref : r!.name, ctx);
|
|
543
625
|
}
|
|
@@ -550,7 +632,8 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
550
632
|
// so the common runtime shape here is a pre-resolved ResourceInstance;
|
|
551
633
|
// fall back to the structural ref forms when injection left them in place.
|
|
552
634
|
if (isRunnableInstance(target)) {
|
|
553
|
-
|
|
635
|
+
const id = idOf(target, `target[${i}]`);
|
|
636
|
+
await runResolvedInstance(target, id.kind, id.name);
|
|
554
637
|
continue;
|
|
555
638
|
}
|
|
556
639
|
const bare = target as { name?: unknown; alias?: unknown };
|
|
@@ -561,7 +644,7 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
561
644
|
`Boot target '${bare.alias}.${bare.name}' is not a runnable exported instance`,
|
|
562
645
|
);
|
|
563
646
|
}
|
|
564
|
-
await runResolvedInstance(inst,
|
|
647
|
+
await runResolvedInstance(inst, getRefIdentity(inst as object)?.kind ?? "", bare.name);
|
|
565
648
|
continue;
|
|
566
649
|
}
|
|
567
650
|
if (typeof bare.name === "string") {
|
package/src/resource-context.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
type InvokeContext,
|
|
13
13
|
type LoadOptions,
|
|
14
14
|
type ModuleContext,
|
|
15
|
+
type OpenSpan,
|
|
16
|
+
type OpenSpanOptions,
|
|
15
17
|
type ParsedArgs,
|
|
16
18
|
type TypeRule,
|
|
17
19
|
} from "@telorun/sdk";
|
|
@@ -27,6 +29,10 @@ import { SchemaValidator } from "./schema-validator.js";
|
|
|
27
29
|
|
|
28
30
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
29
31
|
|
|
32
|
+
/** How long a resource's teardown waits for its in-flight detached tasks before
|
|
33
|
+
* abandoning them (with a logged event). */
|
|
34
|
+
const DETACHED_DRAIN_TIMEOUT_MS = 5000;
|
|
35
|
+
|
|
30
36
|
export class ResourceContextImpl implements ResourceContext {
|
|
31
37
|
readonly env: Record<string, string | undefined>;
|
|
32
38
|
readonly stdin: NodeJS.ReadableStream;
|
|
@@ -155,6 +161,57 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
155
161
|
return createCancellationSource();
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
/** In-flight fire-and-forget tasks this resource spawned. Owned here, not by
|
|
165
|
+
* the kernel: the resource drains them in its own teardown (see
|
|
166
|
+
* `drainDetached`), so background work is bounded by the resource's lifetime. */
|
|
167
|
+
private readonly pendingDetached = new Set<Promise<unknown>>();
|
|
168
|
+
|
|
169
|
+
runDetached(fn: () => Promise<unknown>): void {
|
|
170
|
+
// Fire-and-forget: a detached task has no caller to throw to, so route a
|
|
171
|
+
// failure to the EventBus rather than letting it go unhandled. We track the
|
|
172
|
+
// error-handled chain (not the raw promise) so teardown drains a task whose
|
|
173
|
+
// settlement is already observed here.
|
|
174
|
+
const tracked = this.moduleContext
|
|
175
|
+
.runDetached(fn) // bare scope-detach primitive
|
|
176
|
+
.catch(async (err: unknown) => {
|
|
177
|
+
const detail =
|
|
178
|
+
err instanceof Error ? { name: err.name, message: err.message } : { message: String(err) };
|
|
179
|
+
await this.emitEvent("background.task.error", { resource: this.metadata.name, error: detail });
|
|
180
|
+
})
|
|
181
|
+
.finally(() => {
|
|
182
|
+
this.pendingDetached.delete(tracked);
|
|
183
|
+
});
|
|
184
|
+
this.pendingDetached.add(tracked);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Await this resource's in-flight detached tasks, bounded by
|
|
189
|
+
* `DETACHED_DRAIN_TIMEOUT_MS`. Folded into the resource's teardown by the
|
|
190
|
+
* kernel, so tearing the resource down drains its background work (and its
|
|
191
|
+
* dependencies, torn down later in reverse order, are still alive meanwhile).
|
|
192
|
+
* Past the bound, remaining tasks are abandoned with a logged event rather
|
|
193
|
+
* than blocking shutdown.
|
|
194
|
+
*/
|
|
195
|
+
async drainDetached(): Promise<void> {
|
|
196
|
+
if (this.pendingDetached.size === 0) return;
|
|
197
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
198
|
+
const timeout = new Promise<void>((resolve) => {
|
|
199
|
+
timer = setTimeout(resolve, DETACHED_DRAIN_TIMEOUT_MS);
|
|
200
|
+
});
|
|
201
|
+
await Promise.race([Promise.allSettled([...this.pendingDetached]), timeout]);
|
|
202
|
+
if (timer) clearTimeout(timer);
|
|
203
|
+
if (this.pendingDetached.size > 0) {
|
|
204
|
+
await this.emitEvent("background.task.abandoned", {
|
|
205
|
+
resource: this.metadata.name,
|
|
206
|
+
count: this.pendingDetached.size,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
openSpan(base: InvokeContext | undefined, opts: OpenSpanOptions): Promise<OpenSpan> {
|
|
212
|
+
return this.moduleContext.openSpan(base, opts);
|
|
213
|
+
}
|
|
214
|
+
|
|
158
215
|
invoke<TInputs>(kind: string, name: string, inputs: TInputs): Promise<any> {
|
|
159
216
|
return this.moduleContext.invoke(kind, name, inputs);
|
|
160
217
|
}
|
|
@@ -223,7 +280,7 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
223
280
|
// analyzer's field-map walker descends `oneOf`/`anyOf` variant
|
|
224
281
|
// properties but intentionally early-returns on `$ref` (see
|
|
225
282
|
// `analyzer/nodejs/src/reference-field-map.ts`). Enabling the `$ref`
|
|
226
|
-
// descent regresses the kernel's `<
|
|
283
|
+
// descent regresses the kernel's `<name>.Invoked` event
|
|
227
284
|
// emission for kinds (notably `Run.Sequence`) whose controllers
|
|
228
285
|
// call `instance.invoke()` directly on Phase-5-injected instances;
|
|
229
286
|
// the walker fix needs to land together with routing those callers
|
package/src/tracing.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import type { Tracer } from "@telorun/sdk";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -17,4 +18,10 @@ export class KernelTracer implements Tracer {
|
|
|
17
18
|
this.#next += 1;
|
|
18
19
|
return this.#next;
|
|
19
20
|
}
|
|
21
|
+
|
|
22
|
+
/** A fresh OTel-compatible 16-byte hex trace id. Globally unique, so a trace
|
|
23
|
+
* stays identifiable once it crosses process boundaries. */
|
|
24
|
+
newTraceId(): string {
|
|
25
|
+
return randomBytes(16).toString("hex");
|
|
26
|
+
}
|
|
20
27
|
}
|