Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/adr/0023-isolate-legacy-startup-synchronization-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# ADR-0023: Isolate the legacy backend's startup synchronization context

- Status: Accepted
- Date: 2026-10-04
- Related: ADR-0008, ADR-0013, ADR-0014, ADR-0017

## Context

Exact-main CI 37188258263 reproduced a net48 completion stall. The actual CLR4
dump showed all four CliWrap process/input/output/error tasks normally completed
while the outer async enumeration and wrapper completion remained pending.
This narrows the captured incident but does not by itself identify which caller
context or continuation mechanism caused the historical stall.

Pinned CliWrap 3.10.5 uses an unconfigured `await foreach` over its internalized
PolyShim Task.WhenEach. Awaiting the returned command with ConfigureAwait(false)
does not prevent the dependency's own awaits from capturing the startup context.
Draft PR #25 baseline CI 37188939390 confirms this separate, deterministic defect:
session and one-shot execution both fail to complete under an occupied queued
context on net48 (two posts each); modern targets pass. The one-shot fixture is
gated until startup returns, preventing a synchronous early-exit false positive.

## Decision

Keep the target matrix, CliWrap 3.10.5 dependency and shell-free execution policy.
Initialize CliWrap's command task synchronously under a null SynchronizationContext
for both finite and redirected execution. Restore the caller's context in finally,
including synchronous startup failures. Continue observing the returned task with
ConfigureAwait(false), mapping results/failures through existing PgCliSharp types.

Do not wrap startup in Task.Run, block on asynchronous startup, suppress the wider
ExecutionContext, or change culture/cancellation/timeout/stream ownership. This
scope only prevents capture of the caller's SynchronizationContext by dependency
initialization; it does not promise arbitrary custom TaskScheduler independence.
No public API or runtime dependency changes are needed.

## Consequences and validation

The compatibility backend's internal completion no longer requires the caller's
startup synchronization context to pump callbacks. The caller context is restored
before Start returns and after start errors. Existing synchronous startup error
translation remains intact.

The deterministic regressions must turn green on net48 and remain green on modern
targets; run the existing full suites, bounded Windows 30/3/3 stress, real PostgreSQL
and candidate audits at the final PR head and exact main commit. Preserve failed
baseline/main runs, and inspect actual diagnostic artifacts. Diagnostic heap filters
also include the dependency's internalized MemberPolyfills types, whose names do
not contain the PolyShim namespace.

The reproduced context defect is confirmed; attributing every historical stall to
it would exceed the evidence. A passing bounded run is regression evidence, not
proof of absence of all intermittent process defects. 1.0 acceptance and postponed
publication remain separate.

CI 37189457631 confirms the limitation: the context regressions pass, but net48
stress round 27 still stalls with all four lower tasks complete and a yielded
WhenEach value awaiting consumer progress. Context isolation is a confirmed
partial fix; final merge gates remain blocked. Nested promise/awaiter diagnostics
are expanded before choosing any additional runtime/dependency change.

## Primary-source evidence

- [Pinned CliWrap execution](https://github.com/Tyrrrz/CliWrap/blob/3.10.5/CliWrap/Command.Execution.cs)
- [Pinned PolyShim WhenEach](https://github.com/Tyrrrz/PolyShim/blob/2.12.2/PolyShim/Net90/Task.cs)
- [Framework Task state flags](https://github.com/microsoft/referencesource/blob/main/mscorlib/system/threading/Tasks/Task.cs)
- [Investigation and preserved receipts](../phase-8-net48-investigation.md)
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Do not silently edit an old Accepted ADR to make history appear different.
| [0020](0020-select-unpublished-full-wrapper-preview.md) | Select an unpublished full-wrapper preview | Superseded |
| [0021](0021-finish-preview-engineering-without-publication.md) | Finish preview engineering without publication | Accepted |
| [0022](0022-require-three-os-native-cli-checkpoint.md) | Require a three-OS native CLI checkpoint for 1.0 | Accepted |
| [0023](0023-isolate-legacy-startup-synchronization-context.md) | Isolate the legacy backend's startup synchronization context | Accepted |

## Template

Expand Down
6 changes: 6 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ Requirements:

The `netstandard2.0` compatibility backend must map execution results, cancellation, timeout, and failures back into PgCliSharp's own result/exception model. Public behavior should remain consistent across target frameworks.

Under ADR-0023, both legacy finite and session backends initialize CliWrap
synchronously with SynchronizationContext temporarily cleared, restoring the
caller context in finally. Internal completion therefore does not require the
startup context to pump callbacks. Start errors remain synchronous; no Task.Run,
ExecutionContext suppression, public API or dependency change is introduced.

Phase 6 additionally separates finite one-shot execution from long-lived redirected process sessions under ADR-0013. A redirected `psql` session exposes programmatic duplex pipe I/O but is not a TTY/PTY and does not promise Readline, command-history, or terminal-emulation behavior. Long-running rich-I/O tools may stream both stdout and stderr to caller-owned destinations so memory usage does not have to grow with process duration. ADR-0014 makes modern session output faults part of lifecycle supervision and bounds the `netstandard2.0` CliWrap input bridge to approximately 1 MiB. Legacy `WriteAsync` therefore applies cancellation-aware backpressure; successful write completion means acceptance into the bounded delivery buffer, while `CompleteInput` drains already accepted bytes before EOF.

## 9. Output model
Expand Down
78 changes: 74 additions & 4 deletions docs/phase-8-net48-investigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,63 @@ in the five-byte case during stress round 2. The latter parent trace records
`drained`. These prove neither child process exit nor the identity of the pending
task. Later successful runs do not establish a root-cause fix.

PR #24 passed all 18 PR CI jobs (37187722869), but exact-main CI 37188258263
at `1e794140c80b2d0092f2d1a4ae0ceb8d85d790fc` reproduced the zero-byte stall
in net48 stress round 3. The failed run is retained without retry.
Artifact 11297931986, SHA-256
`fb7e8b8a9a479e52035240cbda75dae890df0bfa2712a02a314c7874d299613b`,
was independently downloaded and checked. The actual CLR4 dump reader succeeded;
all four CliWrap stdIn/stdOut/stdErr/process tasks had flags `0x7000400`
(RanToCompletion set, Faulted/Canceled unset), while its ExecuteAsync state machine
remained at state 2 and wrapper completion flags were `0x2000400` (pending).
Thus, for this capture, unfinished process/pipe tasks do not explain the stall.
Reference-source Task flag definitions and pinned CliWrap/PolyShim source inform
the next investigation; they do not alone identify a historical root cause.

A follow-up adds deterministic session/one-shot regressions: start under a context
that queues callbacks without pumping, restore the caller's original context,
then require completion within five seconds without any posts to the occupied
startup context. Cleanup releases queued baseline continuations. Start-failure
coverage separately requires caller-context restoration. Before changing runtime
behavior, Windows CI must demonstrate the failing baseline. Baseline CI 37188939390
at PR #25 head `b18a9a7b8187408a232ad4d4d6753a04b00820bd` confirms two net48
failures: both controlled-context completions time out with two posts each.
Modern net8/net10 suites each pass 554 tests, and the start-error restoration test
passes on all targets. This is a deterministic context-dependence defect.

ADR-0023 fixes it by initializing the existing CliWrap command task synchronously
under a null SynchronizationContext and restoring the caller's context in finally.
Both finite and session runners use the same internal adapter. No Task.Run or
wider ExecutionContext suppression is used. Final exact PR/main CI and artifact
inspection remain the acceptance gate for this change. The known defect and the
historical captured stall are distinguished; no claim covers every past timeout.
The current [PR #25 validation receipt](https://github.com/ShutenOishi/pgcli-sharp/pull/25)
records the exact final PR/main commits, CI runs and independently inspected
diagnostic artifacts, preserving failures and superseded revisions. This avoids
an untested follow-up source change solely to embed the CI's own source SHA.

The context-isolation revision `c3d53b4bb23f3f7ee00638ce677028d8d5531f4e`
passed the full Windows suites and 26 net48 stress rounds in CI 37189457631,
then stalled in round 27 on the 262144-byte case. Its controlled-context tests
pass, so the confirmed startup-context fix is not sufficient for the intermittent
defect. The PR remains Draft; this failed run is not retried or merged.
Artifact 11297554452, SHA-256
`a4d56daa20274368b2f22c25f7fa4c246095ef6f0d2767ea4c176d543d07fded`,
was independently checked. Again, all four lower tasks are normally completed;
the outer ExecuteAsync is at state 2, while the internalized WhenEach iterator
is at state -4 with a completed stdout task as its current yielded value.

Further diagnosis reads bounded nested value-type fields (awaiters and the
ManualResetValueTaskSourceCore promise), including short tokens, completion flags
and captured-context type references, with three-level nesting and a global
10,000-field limit. ExecutionContext fields are decoded only as scalar flags and
type/address references; string/environment contents are still never read.
Iterator state -2 is retained rather than treated as an ordinary completed async
method. This is an evidence improvement, not another claimed runtime fix.
The owned CLR4 smoke fixture now keeps a nested struct alive and requires its
actual int/short/bool values (1729/37/true) from the dump, so a decoder that cannot
read interior field addresses cannot silently pass the readiness gate.

The prepared ClrMD reader had not previously compiled or read a dump in CI.
Windows now first captures an owned net48 `wait` helper using `MiniDumpWriteDump`
and requires CLR4, thread and fixture-managed frames from the actual dump.
Expand All @@ -34,8 +91,10 @@ PR/main CI and inspection of actual smoke/stress artifacts are required before
this diagnostic change is accepted. A successful bounded run is preparedness and
regression evidence, not proof that the intermittent defect is resolved.

No production execution behavior, public API, frozen preview source or publication
settings change. 1.0 acceptance and external publication remain pending.
The initial PR #24 diagnostics did not change production execution. The PR #25
follow-up deliberately changes only legacy startup context capture under ADR-0023.
Public API, runtime dependencies, frozen preview source and publication settings
remain unchanged. 1.0 acceptance and external publication remain pending.

## 日本語

Expand All @@ -50,5 +109,16 @@ CLR4・スレッド・fixtureの管理フレームを必須とし、子は後片
net48反復は30回を上限に最初の失敗で止め、既存のタイムアウトは維持します。
生ダンプは削除しアップロードからも除外します。文字列や環境値を出力せず、
スタック・型・アドレス・Task状態のテキストを保持します。
成功した場合も原因解決とは扱いません。実行実装・公開API・固定候補・公開設定は
変更せず、1.0判定と外部公開は保留します。
PR #24のmainで再現した停止の実ダンプを解析し、下位4Taskの正常完了と上位処理の
保留を確認しました。PR #25の修正前CI 37188939390では、処理しない同期コンテキストを
使う回帰テストがnet48の通常実行・セッション双方で失敗し、それぞれ2回のPostを
観測しました。modern対象と起動失敗時のコンテキスト保持は成功しました。
ADR-0023に従い、CliWrap起動だけを同期コンテキストnullで初期化し、finallyで
呼び出し元の状態を戻します。同期的な起動エラー・実行コンテキスト・公開API・
依存・固定候補・公開設定は保持します。確認できた依存の欠陥を修正しますが、
過去の全停止の原因とは断定しません。最終PR/main CIと成果物確認を必須とし、
正確なcommit・run・成果物をPR #25の検証記録に保持します。
1.0判定と外部公開は保留します。
同期コンテキスト修正後も反復27回目で停止が再現し、PRはDraftのままです。
awaiter/promiseの構造体内部と継続先の型を上限付きで採取し、専用CLR4子の
既知のint/short/boolを実ダンプから読めることも必須にします。
14 changes: 14 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,22 @@ and CI 37115096538 (18 jobs, attempt 1), with independent artifact inspection.
The remaining [Windows net48 completion investigation](phase-8-net48-investigation.md)
verifies the diagnostic reader against an owned CLR4 dump and bounds stress to
30 rounds without claiming the historical intermittent stall is fixed.
Exact-main CI 37188258263 reproduced a stall and successfully retained actual
CLR4 state. PR #25 baseline CI 37188939390 confirms a deterministic legacy
startup SynchronizationContext defect; ADR-0023 isolates dependency startup while
preserving synchronous errors and caller context. The current
[PR #25 validation receipt](https://github.com/ShutenOishi/pgcli-sharp/pull/25)
records final PR/main all-18 CI and independent diagnostic artifact inspection;
these are required before the fix is considered complete.
The context regressions now pass, but CI 37189457631 stalls in net48 round 27;
the Draft PR's merge gate remains blocked while nested async diagnostics continue.
日本語: CP-05はPR #23と正確なmain CIの全18ジョブ成功・成果物確認で完了しました。
残るnet48停止は実ダンプでの解析器検証と上限付き反復で調査し、原因解決とは断定しません。
mainでの再現と修正前CIから同期コンテキスト依存の欠陥を確認し、ADR-0023で
内部起動処理を修正します。最終PR/mainの全18CIと成果物確認を完了条件とし、
PR #25の検証記録に保持します。
同期コンテキストの回帰は成功しましたが、net48反復27回目で再び停止し、
PRはDraftのまま追加診断を続けます。

CP-04 bounded preview preparation completed through PR #22, main
`846943fb739efdc30b4d5a6da469eef0d8855a98`, exact-main CI 37105452621 (all 16 jobs,
Expand Down
Loading
Loading