Skip to main content

Release v0.0.87

· 8 min read
vNext Team
Burgan Tech Engineering

Overview

This release spans the transition pipeline end to end, and then removes the redundancy that filling in the trace tree exposed. Every time-consuming mechanism is now a span — validation, context load, instance read, task input/invoke/output compilation, subflow mapping, locks, component reads, the write side and the stretch after the pipeline — all always on, not gated behind Verbose, with the rule that a step which did no work emits no span at all (#920). With the tree filled in, a single business request turned out to resolve the same workflow definition three or more times per hop, load the same instance twice, validate the same payload schema twice, and build a fully populated transition context only to throw it away. The second half of the release removes exactly that: one workflow resolution per hop, snapshot-based admission, a single owner for payload-schema validation, and the orphaned IWorkflowContext deleted outright (#920). The measured effect on one traced business request: time inside SyncTransitionStrategy that no child span accounted for drops from 976 ms to 14 ms. This release runs on component schema 0.0.53.


Features

End-to-end transition span tree, always on (#920)

Before this, a transition showed as a transaction with a handful of spans under it, and everything that actually cost time happened inside the gaps. A reader could see that a hop took 400 ms and had no way to learn where it went. Spans are named so the tree reads without opening anything — the subject is in the span name, details are tags:

SpanCovers
Step.{Name}Every pipeline step, with vnext.step.order / vnext.step.outcome
Transition.LoadContext, Instance.LoadContext construction and the instance read behind it
Transition.Validate, Transition.ValidatePolicySchema validation and the execution policy, separately
Task.Invoke, Invoke.{taskType}/{taskKey}, CacheAside.{Read,Write}/{key}The Execution service, which previously emitted no spans of its own at all
Script.Compile, Script.Execute, Script.ResolveHelpersRoslyn compile and execution, told apart
Cache.Get/{key}, Cache.GenerationGet/{key}The component body read and the generation-token round trip in front of it
Lock.Acquire/{lockKey}, Lock.Release/{lockKey}With vnext.lock.kind = status | chain
Instance.AppendData, Uow.Commit, Events.PublishDeferredThe write side
Transition.Continuation/{mode}, Transition.SettleThe stretch after the pipeline that used to be invisible
FanOut.Item[i]Per item, with batch summary tags on the parent

Three refinements change what a reader sees:

  • A step that did no work emits no span. StepOutcome.ContinueNoWork() clears the recorded flag, so a profile-excluded or short-circuited step leaves no row. A 20-row tree of mostly no-ops is worse than a 6-row tree of real ones.
  • The transaction is named after its transitionTransitionJob.Execute/{key} — which made the transition/{key} child node redundant, so it is gone.
  • Cache and lock spans carry their subject in the name, not only in tags.

All of this is always on. That works without touching Aether because the business span filter suppresses only [-prefixed display names at export — verified rather than assumed.

Measured from one business request traced end to end (39 transactions, warm runtime):

  • Unattributed time inside SyncTransitionStrategy: 976 ms → 14 ms. Residual unattributed time across all transaction roots is 266 ms (9.7%), most of it the HTTP entry and job dispatch edges.
  • Component reads: 74 reads for 14 distinct components across 12 distinct generation tokens — 148 Redis round trips, all L1 misses. This is what motivated the GenerationMemoSeconds default change below.
  • Wall-clock share with parallelism accounted for: an external downstream service 45.5%, component cache reads 21.1%, database (commit plus instance load) 9.4%, locks 0.9%. The lock number is the one to notice: 0.9% of wall clock across 28 transitions is Busy-as-mutex behaving as intended, and it is now measurable rather than argued.

Reference: PR #920 — see also Observability and Telemetry configuration.

Redundancy removal (#920)

Four changes, each found because the tree made the repetition visible. These are the parts that are not purely additive.

1. IWorkflowContext is gone. It was added for a schema-validation aspect that was later deleted, and the context has been orphaned since — two writers, three readers, roughly fifteen test doubles. Its three readers now take the workflow from their caller: PostCommitParentMutationService from PostCommitParentSnapshot, InstanceDataWriteService from an explicit Workflow? parameter, and InstanceCommandAppService's own memo is deleted outright — it compared only Key, so a pinned-version request could silently get a different version back.

2. A resolved workflow is carried instead of re-resolved. WorkflowExecutionContext.ResolvedWorkflow is [JsonIgnore] transport-only state, so one hop resolves the definition once. No version guard is needed, and the reason is worth understanding: the carried definition was resolved from the same context object's Domain / WorkflowKey / WorkflowVersion, and the consumer asks with exactly those fields. The deleted memo was wrong precisely because it lacked that property.

3. Intake admits from the projection, not the aggregate. The full aggregate load in InstanceCommandAppService existed only to build a context for a validation that AsyncTransitionStrategy already performs before enqueue. Both are gone; admission now reads InstanceExecutionSnapshot. Two guards keep this safe and both are pinned by tests: InstanceExecutionSnapshot.IsTerminal was added because Instance.IsCompleted counts Faulted and Passive too while the snapshot's IsCompleted did not — a faulted instance would otherwise have been admitted — and the InstanceNotFound error is reproduced verbatim (Instance:100017, not Instance:100013).

4. Payload-schema validation has a single owner. The sync path gained the schema check it never had, conditioned on !context.IsPreReserved — the same "validated at accept" invariant already documented in the transition pipeline.

Reference: PR #920 — see also Transition pipeline.


Behavior Changes

Two items change observable behavior. ComponentCache:GenerationMemoSeconds now defaults to 5 rather than 0 — this is a policy decision, not a tuning change, and it is the only item here that trades correctness for latency: the memo caches the generation token, so a bump written by another pod stays invisible on this pod for up to five seconds. L1 does not share this exposure, because an L1 key embeds the token and therefore cannot go stale. Set it to 0 where a publish must be cluster-visible immediately. And Telemetry:Tracing:AdditionalSources must list the new activity sources — without them the new spans are produced and never exported, which is a silent failure rather than an error. Each item, its impact direction and the migration steps are documented in the v0.0.87 breaking changes announcement.


Fixes

  • A pinned-version request could silently get a different version back — the deleted workflow memo in InstanceCommandAppService compared only Key, ignoring the requested version (#920).
  • A faulted instance could be admitted — snapshot-based admission required a new IsTerminal flag, because Instance.IsCompleted counts Faulted and Passive while the snapshot's IsCompleted did not (#920).
  • Task execution retries skipped the journal idempotency probe — the probe was suppressed for a fresh transition record, but the suppression also applied on retries after the first attempt, so duplicate execution-key records could be written. The probe is now skipped only on the first attempt and runs again on every retry (#924).
  • Elapsed-time measurement is unified across script compilation, task coordination, task execution, fan-out, remote invocation and every task invoker, removing duplicated stopwatch bookkeeping while preserving the reported durations and telemetry (#924).

Configuration Updates

Configuration for v0.0.87:

{
"runtimeVersion": "0.0.87",
"schemaVersion": "0.0.53"
}

Note: Schema version is unchanged at 0.0.53.

ComponentCache:GenerationMemoSeconds now carries the intended value as its code default of 5, and the orchestration host's explicit 0 override is removed. Set it back to 0 in any environment where a publish must be visible on every pod immediately.

Telemetry:Tracing:AdditionalSources must be extended across both hosts and both workers, or the new spans are never exported:

{
"Telemetry": {
"Tracing": {
"DetailLevel": "Business",
"AdditionalSources": [
"BBT.Workflow.Pipeline",
"BBT.Workflow.BackgroundJobs",
"BBT.Workflow.SubFlow",
"BBT.Workflow.Tasks",
"BBT.Workflow.Cache",
"BBT.Workflow.Scripting",
"BBT.Workflow.Authorization",
"BBT.Workflow.Instances.Read",
"BBT.Workflow.Functions",
"BBT.Workflow.Extensions",
"BBT.Workflow.Execution",
"BBT.Workflow.Execution.Invokers"
]
}
}
}

Container images: published at tag 0.0.87 under ghcr.io/burgan-tech/vnext/*, Cosign-signed (keyless OIDC) with SBOM + provenance. Immutable digests are listed in the GitHub release.


Issues Referenced

  • vnext #920 — Span the transition pipeline end to end, and remove the redundancy it exposed.
  • vnext #924 — Re-probe the task journal on retries and unify elapsed-time measurement.

Summary

  • The full transition span tree is always on: pipeline steps, context load, validation, task invoke, script compile/execute, cache and lock spans with their subject in the name, the write side, and continuation/settle.
  • A step that did no work emits no span; the transaction is named TransitionJob.Execute/{key} and the redundant transition/{key} child is gone.
  • One workflow resolution per hop via WorkflowExecutionContext.ResolvedWorkflow; IWorkflowContext removed.
  • Admission reads InstanceExecutionSnapshot, not the full aggregate, guarded by a new IsTerminal flag and a verbatim Instance:100017.
  • Payload-schema validation has one owner — the sync path gained the check it never had.
  • GenerationMemoSeconds defaults to 5: a cross-pod publish is invisible for up to 5 s; set 0 for immediate cluster visibility.
  • AdditionalSources must list the new activity sources, or the new spans are never exported.
  • Measured: unattributed time inside SyncTransitionStrategy 976 ms → 14 ms; locks are 0.9% of wall clock.
  • Schema stays at 0.0.53.

vNext Runtime Platform Team Released August 31, 2026