179 lines
11 KiB
Markdown
179 lines
11 KiB
Markdown
|
|
# Concurrency audit
|
|||
|
|
|
|||
|
|
Status: three relevant P2 findings: one new cancellation defect and two
|
|||
|
|
reconfirmed findings from the functional correctness audit. Reviewed source:
|
|||
|
|
`2ea26afcc5ae691152e80d2fa5ee4d572d390a95`.
|
|||
|
|
|
|||
|
|
## Architecture, contracts, and scope
|
|||
|
|
|
|||
|
|
The [internal overview](../../internal/overview.md) inventories the implemented
|
|||
|
|
owners. Core captures declarations and routing metadata, builds fresh backend
|
|||
|
|
parse state per invocation, prepares typed inputs, and coordinates invocation
|
|||
|
|
reporting and outcomes. ckconfig owns configuration scope/comparison coordination;
|
|||
|
|
ckprompt owns PromptKit request/inspection coordination and sequential aggregate
|
|||
|
|
smoke tests. Applications own loaders, builders, runtime dependencies, contexts,
|
|||
|
|
and executable lifecycle. Upstream libraries own their domain operations.
|
|||
|
|
|
|||
|
|
The [reporting contract](../../consumers/guide.md#invocation-reporting) and
|
|||
|
|
[Reporter GoDoc](../../../reporter.go) explicitly permit concurrent operations on
|
|||
|
|
one invocation's reporter. One reporting-state mutex protects visibility,
|
|||
|
|
deferred messages, destination observations, and writes. Managed output writes
|
|||
|
|
acquire it; rendering callbacks use a coordinated writer while the rendering
|
|||
|
|
operation holds it. Rendering callbacks must not reenter that reporter, and
|
|||
|
|
their supplied writer is valid only while the callback runs. Concurrent message
|
|||
|
|
submission order is unspecified. Reporter handles close before Execute returns;
|
|||
|
|
caller-owned writers are not closed.
|
|||
|
|
|
|||
|
|
Concurrent Execute/Register access to one App is not established by the current
|
|||
|
|
contracts; [concurrent dispatch remains separate future work](../future.md#other-future-proposals).
|
|||
|
|
The audit did not infer that guarantee from independent invocation state or
|
|||
|
|
declaration reuse. Application callback objects and factory results retain their
|
|||
|
|
own ownership and synchronization responsibilities under the
|
|||
|
|
[architecture policy](../../policy/architecture.md#handler-inputs-and-invocation-state).
|
|||
|
|
|
|||
|
|
Static review covered locks and state access, reporter acceptance/finalization,
|
|||
|
|
callback and writer lifetimes, declaration/snapshot ownership, integration input
|
|||
|
|
and shared-state lifetimes, cancellation checkpoints, comparison/aggregate
|
|||
|
|
sequencing, and maintained examples and release launchers. CommandKit's runtime
|
|||
|
|
code starts no goroutines and owns no channels or worker pools. Integration
|
|||
|
|
callbacks run synchronously; aggregate scopes, comparison sides, and smoke kinds
|
|||
|
|
are coordinated sequentially. Release launchers use exec to delegate to ReleaseKit.
|
|||
|
|
Application/upstream workers and subprocess internals remain with their owners.
|
|||
|
|
|
|||
|
|
## Validation evidence
|
|||
|
|
|
|||
|
|
Codebase-memory-mcp symbol/source searches and call tracing were supplemented
|
|||
|
|
with repository-wide searches for synchronization, goroutines, channels, and
|
|||
|
|
cancellation. The indexed Go source matches this revision. Validation used Go
|
|||
|
|
1.27.0, cached dependencies, and no live services or credentials:
|
|||
|
|
|
|||
|
|
- The aligned current-source workspace suite passed with race detection and
|
|||
|
|
fresh test execution:
|
|||
|
|
`GOTOOLCHAIN=local go test -race -count=1 -mod=readonly ./... ./integrations/ckconfig/... ./integrations/ckprompt/...`.
|
|||
|
|
This includes all three modules and their maintained examples. The optional
|
|||
|
|
interactive shell checks remain opt-in and were not enabled in this pass.
|
|||
|
|
- A temporary external consumer run with `go run -race` reproduced N1 through
|
|||
|
|
the public ckprompt/core APIs. It canceled the context from the progress
|
|||
|
|
destination's writer without reentering the reporter.
|
|||
|
|
- Existing temporary external probes using local replacements for all three
|
|||
|
|
CommandKit modules reproduced N2 and N3 in three consecutive runs each with
|
|||
|
|
`go test -race -count=3`. Their functional assertions failed as described
|
|||
|
|
below. No data-race report was emitted.
|
|||
|
|
|
|||
|
|
No production files or repository tests were changed. These failures concern
|
|||
|
|
ordering and cancellation accounting; memory-race detection alone does not
|
|||
|
|
establish that an accepted queue item is delivered or a stopped aggregate fails.
|
|||
|
|
|
|||
|
|
## N1 — P2: Cancellation can stop aggregate smoke testing with a success outcome
|
|||
|
|
|
|||
|
|
**Code:** [smoke_command.go](../../../integrations/ckprompt/smoke_command.go),
|
|||
|
|
runSmokeAggregate lines 159–176 and final outcome selection at 183–187;
|
|||
|
|
queuePromptDiagnostics in [command.go](../../../integrations/ckprompt/command.go),
|
|||
|
|
lines 586–603; core's Progress delivery in [reporter.go](../../../reporter.go).
|
|||
|
|
|
|||
|
|
**Circumstances:** a smoke callback returns a report without error. The first
|
|||
|
|
post-callback context check sees no cancellation. Cancellation becomes visible
|
|||
|
|
while diagnostics are presented, or asynchronously before the later checkpoint
|
|||
|
|
at line 175. That checkpoint stops the loop, but does not retain its error.
|
|||
|
|
Progress diagnostics are delivered immediately, so this window can be exercised
|
|||
|
|
deterministically with a valid progress writer that cancels the context.
|
|||
|
|
|
|||
|
|
**Observed behavior:** a temporary consumer invoked
|
|||
|
|
`promptkit test all --progress --format=json`. Its first callback returned a
|
|||
|
|
completed reachability report and a progress diagnostic. The diagnostic writer
|
|||
|
|
canceled the context and accepted all bytes. Only one callback ran; the JSON
|
|||
|
|
report contained one successful entry with no error. Execute nevertheless
|
|||
|
|
returned Kind success, status 0, and nil Cause. Rendering and writing errors
|
|||
|
|
were nil. The other three requested kinds were never attempted.
|
|||
|
|
A second probe returned a provider error from that callback: status was 1, but
|
|||
|
|
errors.Is(outcome.Cause, context.Canceled) was still false after the same stop.
|
|||
|
|
|
|||
|
|
**Expected behavior and consequences:** the
|
|||
|
|
[aggregate smoke contract](../../cli.md#promptkit-smoke-testing) requires all four
|
|||
|
|
callbacks to return without error for status 0 and preserves cancellation causes
|
|||
|
|
when cancellation stops the sequence. A partial aggregate stopped by cancellation
|
|||
|
|
must not report successful completion. Automation can otherwise accept an
|
|||
|
|
incomplete aggregate, and callers cannot detect the cancellation through
|
|||
|
|
errors.Is on the returned cause. If an operation error already exists, the same
|
|||
|
|
window can omit cancellation from its joined causes.
|
|||
|
|
|
|||
|
|
**Potential remediation:** when the later checkpoint uses cancellation to omit
|
|||
|
|
remaining kinds, retain that error in the aggregate cause before leaving the
|
|||
|
|
loop. Deduplicate an already retained cancellation as appropriate. Preserve
|
|||
|
|
completed reports, prior operation errors/explanations, and attempted-kind order;
|
|||
|
|
do not invent a report for an unattempted kind. Moving only the earlier check
|
|||
|
|
leaves another interval in which cancellation can be observed and discarded.
|
|||
|
|
This does not require treating every cancellation after completed report delivery
|
|||
|
|
as a new failure. Coverage should distinguish cancellation in the provider,
|
|||
|
|
during immediate progress presentation, and between aggregate operations.
|
|||
|
|
|
|||
|
|
## N2 — P2: Deferred-message acceptance remains open after the final batch detaches
|
|||
|
|
|
|||
|
|
This is the same issue as
|
|||
|
|
[C2 in the correctness audit](01-correctness.md#c2--p2-finalization-can-discard-a-successfully-queued-deferred-message),
|
|||
|
|
reconfirmed here rather than a separate remediation item.
|
|||
|
|
|
|||
|
|
**Code:** [reporter.go](../../../reporter.go), DeferredReporter.enqueue lines
|
|||
|
|
254–279 and Reporter.finalize lines 282–325.
|
|||
|
|
|
|||
|
|
**Circumstances:** finalization detaches state.deferred and unlocks between
|
|||
|
|
deliveries. Another goroutine can acquire the mutex during that flush, append a
|
|||
|
|
visible message to the now-empty queue, and receive nil from its submission.
|
|||
|
|
The reporter has not yet set closed. The finalizer drains only the detached
|
|||
|
|
batch, then clears the new queue when closing.
|
|||
|
|
|
|||
|
|
**Consequences and evidence:** the accepted message silently disappears, while
|
|||
|
|
Execute can return success and mark diagnostic delivery complete. The probe
|
|||
|
|
used one invocation, a briefly pausing diagnostic destination, and a goroutine
|
|||
|
|
submitting AtEnd().Warn during the flush. All three race-enabled runs accepted
|
|||
|
|
and lost the message. There was no unsynchronized memory access or callback
|
|||
|
|
reentry; the defect is the queue's acceptance/finalization ordering.
|
|||
|
|
|
|||
|
|
**Potential remediation:** atomically close deferred acceptance when detaching
|
|||
|
|
the final batch, rejecting later submissions explicitly, or drain every accepted
|
|||
|
|
item before closure. Keep finalizer-owned delivery possible after acceptance
|
|||
|
|
closes; setting the reporter's existing closed flag too early would also reject
|
|||
|
|
its internal delivery. Preserve message visibility, ordering, and error evidence.
|
|||
|
|
Goroutines created by an application remain application-owned; this finding
|
|||
|
|
concerns an operation that already returned successful acceptance.
|
|||
|
|
|
|||
|
|
## N3 — P2: Buffered configuration-show cancellation becomes a presentation failure
|
|||
|
|
|
|||
|
|
This is the same issue as
|
|||
|
|
[C4 in the correctness audit](01-correctness.md#c4--p2-show-preparation-labels-ordinary-cancellation-as-a-rendering-error),
|
|||
|
|
reconfirmed here rather than a separate remediation item.
|
|||
|
|
|
|||
|
|
**Code:** [show.go](../../../integrations/ckconfig/show.go), runConfigShow lines
|
|||
|
|
69–95, interacting with core Reporter.RenderOutput in
|
|||
|
|
[reporter.go](../../../reporter.go).
|
|||
|
|
|
|||
|
|
**Circumstances:** cancellation becomes visible between buffered effective
|
|||
|
|
document conversions. The checkpoint inside RenderOutput returns ctx.Err as
|
|||
|
|
the rendering callback's error. Core records it as Output.RenderError before
|
|||
|
|
ckconfig returns cancellation as the operation cause.
|
|||
|
|
|
|||
|
|
**Consequences and evidence:** normal cancellation is reported as both an
|
|||
|
|
operation failure and an incomplete output/rendering failure. A two-scope probe
|
|||
|
|
acquired both views; the first native writer emitted valid safe YAML, canceled
|
|||
|
|
the context, and returned nil. The second writer was skipped and no report
|
|||
|
|
bytes were delivered. All three race-enabled runs retained context.Canceled as
|
|||
|
|
Cause but also incorrectly recorded it as Output.RenderError, with no writer
|
|||
|
|
error. This can mislead callers distinguishing interrupted preparation from
|
|||
|
|
encoder or output failures.
|
|||
|
|
|
|||
|
|
**Potential remediation:** retain preparation cancellation separately from the
|
|||
|
|
rendering callback's error, following renderPreparedConfigReport in
|
|||
|
|
[operations.go](../../../integrations/ckconfig/operations.go). Preserve real
|
|||
|
|
native-writing, conversion, and destination errors independently, and keep an
|
|||
|
|
incomplete aggregate report from being delivered. Retain the selected distinction
|
|||
|
|
between cancellation before delivery and cancellation after delivery has started.
|
|||
|
|
|
|||
|
|
## Assessment
|
|||
|
|
|
|||
|
|
N1 is new. N2 and N3 remain open in the correctness audit and should be tracked
|
|||
|
|
as the same underlying issues. No data race, supported-path deadlock,
|
|||
|
|
goroutine leak, or channel-lifecycle defect was established in the reviewed
|
|||
|
|
CommandKit code. The passing race suite supports the exercised synchronization
|
|||
|
|
paths; the three findings demonstrate why lifecycle and cancellation reasoning
|
|||
|
|
are also necessary.
|