Skip to main content

Release v0.0.86

Β· 11 min read
vNext Team
Burgan Tech Engineering

Overview​

This release is trace-driven performance work on the paths a busy instance actually spends its time in. updateData admission becomes lock-free: the kind is status-neutral, so the async accept no longer takes the status lock or runs the duplicate-active-job guard, and N parallel updateData requests are all admitted β€” measured as zero 409s under five-way parallel load where the baseline trace showed 84% dead air (#917). Script compilation moves to async single-flight, so waiters await one shared compile instead of blocking thread-pool threads while Roslyn emits, taking cold-window Dapr p99 from 177.7 ms to 81.1 ms; the same change adds compile-cost tags to the task span and Trigger.Local.* spans for in-process trigger invocations (#917). Hot-path allocation drops across the board β€” response bodies are no longer buffered to measure their size, the EF command interceptor stops copying the full SQL text about six times per command, and raw request bodies are no longer eagerly decoded to UTF-16 β€” alongside a partial GIN index on InstancesData.Data that finally serves the @> containment predicates every attributes.* filter already emitted, compiled poll-path fingerprint queries, Server GC on every host, and InstanceWriteGate at 256 stripes (#911). On the release side, prerelease publishing moves from manual dispatch to PR labels (#912, #915). This release runs on component schema 0.0.53.


Features​

Lock-free parallel updateData admission (#917)​

TransitionAdmissionService.AcceptAsync no longer acquires the status lock for AdmissionKind.Unconditional β€” that is, for updateData. The kind is status-neutral (AcceptFlip.None, so there is nothing to serialize), and the sync path never locked it either: the async accept was the lone asymmetry.

AsyncTransitionStrategy also skips the duplicate-active-job guard for the same kind. N parallel updateData requests share one logical job identity (instance, source state, transition key) yet are all legitimate β€” each carries its own payload, and deduplicating would lose a caller's data. Physical collision is impossible, because the job id and name are unique per enqueue.

The correctness argument is narrow and worth stating: the accept lock existed to serialize the status flip with the duplicate-job guard, and updateData has neither. Instance-data writes stay serialized by the per-instance write funnel. Every other admission kind is unchanged.

Measured on the KYC flow family:

StepEvidence
Baseline22.1 s wall, 84% dead air, notify-parent self 5.0 s with a 2.6 s busy-wait hole
After enqueue + fast-lane boundaryNotify self 1.2 s; remaining 409 backoffs 95–272 ms
After lock-free updateDataZero 409s under five-way parallel load, every notify single-attempt
Final warm trace9.6 s wall, 0.2% dead air

Alongside it, DirectTriggerTask / StartTask sync now defaults to false: trigger-family notifications enqueue instead of blocking on the target instance's pipeline. This is a behavior change for task definitions that do not set it explicitly.

Async single-flight script compile and compile telemetry (#917)​

CSharpEvaluator._typeCache and ScriptHelperRegistry._cache move from a blocking Lazy<T> to Lazy<Task<T>> + Task.Run. Waiters now await one shared compile rather than blocking pool threads while Roslyn emits, and the emit runs once per key on a dedicated work item β€” which is what removed the cold-start thread-pool starvation visible as a lockstep signature in traces (cold-window Dapr p99 177.7 β†’ 81.1 ms).

  • The fast path requires IsCompletedSuccessfully: a running task is awaited, a faulted one is never served, and a poisoned task is evicted by any awaiter through an idempotent remove.
  • Registry Dispose unloads in-flight builds via a continuation.
  • IScriptHelperRegistry.GetOrBuildHelpers becomes GetOrBuildHelpersAsync.
  • Cache keys stay content-addressed (SHA-256 of the decoded code + target type + profile), so a domain package publish changes the content hash and always compiles fresh.

Three previously invisible costs now read directly in APM:

  • Compile cost on the task span β€” vnext.script.compile.count, vnext.script.compile.miss.count and vnext.script.compile.total_ms cumulative tags, plus a script.compile event on miss or failure. Deliberately attributes on the existing span, not a dedicated span. compiledHere / FromCache semantics are preserved: only the creator reports a miss, and waiters report a hit plus their real wait time.
  • Trigger.Local.* spans β€” trigger-family in-process invocations get an always-on child span and open a child trace lane, so triggered transition jobs nest under the task that caused them instead of detaching to the instance lane (the same mechanism as the subflow handoff).
  • result.retry events β€” Polly retries mark the active span, making busy-wait backoff windows visible.

Reference: PR #917 β€” see also Async and sync execution and Transition pipeline.

Hot-path allocation, a GIN index and compiled poll queries (#911)​

Four independent pieces of work on the request and polling paths.

Allocation. The largest per-request allocation sources are gone: the metrics middleware measures response size with a counting stream instead of buffering the body; WorkflowDatabaseInterceptor classifies SQL span-based with a cached result rather than copying the full command text roughly six times per command; path normalization uses compiled regexes; and raw request bodies are captured lazily instead of being eagerly decoded to UTF-16 strings.

A partial GIN index on InstancesData.Data. Attribute equality filters already emitted @> containment predicates, but no index served them β€” so every attributes.* filter was a sequential scan. Migration AddInstancesDataGinIndex adds:

CREATE INDEX ... USING gin ("Data" jsonb_path_ops) WHERE "IsLatest" = true

The index is schema-rewritten per tenant by the migrations SQL generator. On very large InstancesData tables the CREATE INDEX briefly blocks writes per schema, bounded by the partial filter scanning only latest rows β€” plan the migration window accordingly.

Compiled poll queries. The state and data fingerprint queries on the poll path are compiled and cached per schema model, so schema-per-flow tenants never share a compiled plan. Fingerprint ETag byte-compatibility is preserved: the compiled projections mirror the uncompiled reference term for term, guarded by a parity test run against real PostgreSQL.

Host defaults corrected. All hosts now run Server GC β€” the workers were silently running Workstation GC β€” and the trace and metric console exporters are turned off in the execution and orchestration hosts, where they were on with no production override. Logging console output stays on, matching the workers. InstanceWriteGate goes from 64 to 256 stripes to reduce contention under concurrency.

ClickHouse is removed. The integration was disabled everywhere, deployed nowhere, and carried async-void flush timers. The generic DataSink extension point (IDataSinkManager, the registry and the hosted service) intentionally survives, so a future analytics sink can plug in without touching the write paths.

Reference: PR #911 β€” see also Instance filtering and Telemetry configuration.

PR-label prerelease publishing (#912, #915)​

The manual prerelease channel (prerelease_type / prerelease_number dispatch inputs) is removed; the release pipeline ships stable versions only, keeping the pinned-version + force_publish path for completing a failed release.

In its place, labeling a same-repo PR with publish-alpha, publish-beta or publish-rc builds and publishes all seven Docker images to GHCR on every push to that PR β€” highest channel wins (rc > beta > alpha):

  • Version scheme {base}-{channel}.pr{PR#}.{run#} (e.g. 0.0.86-alpha.pr123.7), plus a moving pr-{N} convenience tag. The base is one patch above the highest stable v-tag, so previews semver-sort below the release they precede.
  • Each labeled PR surfaces as one prerelease entry on the Releases page (tag pr-{N}), updated in place on every push, with a sticky PR comment carrying the pull commands. A companion cleanup workflow deletes the release and tag when the PR closes.
  • PR images get cosign keyless signatures by digest, SPDX SBOM artifacts (14-day retention) and Anchore vulnerability scans with SARIF upload.
  • Deliberately narrow otherwise: Docker images only β€” no NuGet or npm β€” and linux/amd64 only.

Two things to know: prerelease NuGet/npm packages can no longer be published from any channel, since the manual dispatch channel was the only source; and anyone with triage permission can trigger image publishing from unreviewed PR code by adding the label, with the prerelease tag namespace as the mitigation.

The same work fixes a silently-broken vulnerability scan: anchore/scan-action moves v3 β†’ v7 in both the release and PR workflows, because v3's bundled grype 0.74 can no longer load its vulnerability database ("db was built 24 weeks ago") and scans had been returning empty.

Reference: PRs #912, #915.


Behavior Changes​

Three items change observable behavior. DirectTriggerTask / StartTask sync now defaults to false β€” a task definition that does not set it explicitly switches from blocking on the target instance's pipeline to enqueuing (#917). The ClickHouse data sink is removed entirely; any ClickHouse configuration section is now inert and should be deleted, though the generic DataSink extension point remains (#911). And the trace and metric console exporters are off by default in the execution and orchestration hosts, so anything that scraped host stdout for spans or metrics must move to the OTLP exporter (#911). Each item, its impact direction and the migration steps are documented in the v0.0.86 breaking changes announcement.


Fixes​

  • Failed script and helper compilations no longer poison their caches β€” a faulted task is never served and is evicted by any awaiter (#917).
  • Workers ran Workstation GC β€” all hosts now use Server GC via common.props (#911).
  • Trace and metric console exporters were on with no production override in the execution and orchestration hosts (#911).
  • Vulnerability scans were silently empty β€” anchore/scan-action v3's bundled grype could no longer load its database; upgraded to v7 in both workflows (#915).
  • A re-published npm version turned the whole run red even though the package was already present; the version is now checked first and the step skipped (#912).

Configuration Updates​

Configuration for v0.0.86:

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

Note: Schema version is unchanged at 0.0.53.

Migration AddInstancesDataGinIndex adds a partial GIN index (jsonb_path_ops, WHERE "IsLatest" = true) on InstancesData.Data, rewritten per tenant schema. It adds an index only, but on very large tables the CREATE INDEX briefly blocks writes per schema β€” run it in a maintenance window if your InstancesData table is large.

Remove any ClickHouse configuration section from host appsettings.json and environment overrides; the integration is gone and the section is inert.

Container images: published at tag 0.0.86 under ghcr.io/burgan-tech/vnext/*, Cosign-signed (keyless OIDC) with SBOM + provenance. Immutable digests are listed in the GitHub release. Prerelease images built from a labeled PR are published under the same registry as {base}-{channel}.pr{N}.{run} plus a moving pr-{N} tag, and are removed when the PR closes.


Issues Referenced​

  • vnext #917 β€” Lock-free updateData accept, async single-flight script compile, trace observability.
  • vnext #911 β€” Reduce hot-path allocations, add a jsonb GIN index and compiled poll queries.
  • vnext #912 β€” Move prerelease publishing from manual dispatch to PR labels.
  • vnext #915 β€” Land the remaining PR-prerelease features and fix the broken vulnerability scan.

Summary​

  • updateData admission is lock-free: no status lock, no duplicate-job guard for a status-neutral kind β€” zero 409s under parallel load, 84% β†’ 0.2% dead air on the measured flow.
  • DirectTriggerTask / StartTask sync defaults to false β€” trigger notifications enqueue instead of blocking.
  • Async single-flight compile β€” waiters await one shared emit; cold-window Dapr p99 177.7 β†’ 81.1 ms.
  • Compile telemetry (vnext.script.compile.* tags plus a script.compile event), Trigger.Local.* spans and result.retry events.
  • Hot-path allocation cuts: counting-stream response metrics, span-based SQL classification, lazy raw-body capture; InstanceWriteGate 64 β†’ 256 stripes; Server GC everywhere.
  • Partial GIN index on InstancesData.Data (migration AddInstancesDataGinIndex) and compiled per-schema poll fingerprint queries with byte-identical ETags.
  • ClickHouse removed; the generic DataSink extension point stays. Console trace/metric exporters off by default.
  • PR-label prerelease publishing (publish-alpha/beta/rc, pr-{N} tags) with cosign, SBOM and a working Anchore scan.
  • Schema stays at 0.0.53.

vNext Runtime Platform Team Released August 25, 2026