Harden inline script cache lifecycle coordination - #1735
Conversation
|
🔒 Automated review in progress — Heejae Chang (@heejaechang) is auto-reviewing this PR. |
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
Heejae Chang (heejaechang)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-key preservation Reduce this PR's net diff versus its merge-base to only three logical changes: - #3 atomic canonical lock retirement on release (src/common/lockfile.apis.ts) - #6 serialized persistent-state clears (src/common/persistentState.ts) - #7 generic Clear Cache preserves the inline association key (constants.ts, envCommands.ts, extension.ts, and an import-only change in the inline-script envManager) Revert the other seven changes to the merge-base content: - #1 root-generation nonce, #2 root/entry admission decoupling, #9 create-counting order (inline-script envManager) - #4/#5 reclaim-side lock retirement + generation-specific inspection (keep only the minimal release-side companions inspect/reclaim need to stay correct: .release-* recognition and ENOENT-on-readdir tolerance) - #8 workspace-root protection (settingHelpers) - #10 typed ClearCacheNotSupported (envManagers, NotSupportedError) Also revert the associated test changes for the removed items, keeping the new persistentState suite (#6), the new Clear Environment Caches suite (#7), and the release-side lock-retirement tests (#3). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…iew feedback) release() could wedge a lock handle when both the canonical-directory retirement AND the ownership-restoration rename failed: it threw with the handle still 'held' and the owner marker already renamed away, so every later release() hit ENOENT -> ECOMPROMISED forever. Introduce a 'releasing' LockState. release() now transitions the owner marker to the '.release-*' marker, sets state 'releasing', then retires the canonical directory. If retirement fails: - restore succeeds -> revert to 'held' and throw ELOCKRELEASEFAILED (unchanged behavior), or - restore fails -> stay 'releasing' and throw ELOCKRELEASEFAILED so a later release() on the same handle resumes retirement instead of wedging. Retired-directory scavenging is intentionally unchanged (a leaked '.retired-*' artifact still persists, per the existing interrupted-artifact test). Add a unit test that fails both the retirement and the restoration rename, asserts the handle reports 'held' with a live '.release-*' marker, then resumes and completes release() on the same handle. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ng clear queue (review feedback) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
c3544ce to
bddd428
Compare
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
…review feedback) - persistentState.set() now joins the clear queue as the new tail so a later clear() cannot be resurrected by a lagging write (review r3858573315 / r3858651583). - Inline lock release() is serialized through a shared in-flight promise so two concurrent releases no longer race and spuriously report ECOMPROMISED; the handle stays retryable after a failed release (review r3858573008 / r3858434379). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
Merge latest upstream microsoft/main (9e44ce1) into the PR branch to resolve the conflict GitHub reported (mergeable=false / dirty). Only one file conflicted: src/test/features/envCommands.unit.test.ts, where both sides inserted different imports at the same location. Resolved by keeping both sets of imports (ours: shellProviders + ShellStartupScriptProvider for the Clear Environment Caches suite; upstream: terminalRunner + TerminalManager for the new terminal tests), ordered alphabetically. All four imports are used in the file. src/features/envCommands.ts and src/managers/builtin/inlineScript/envManager.ts auto-merged cleanly and were verified semantically: our clearEnvironmentCachesCommand and INLINE_SCRIPT_ENVS_KEY re-export are preserved alongside upstream's terminal return-fix and uv interpreter/version handling. Net PR diff vs upstream/main remains the intended 9-file scope plus review fixes: atomic lock retirement with resumable/concurrent release, persistent-state clear and set serialization, and generic Clear Cache preserving inline associations. No new files were introduced into the PR diff by conflict resolution. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
Revert the broad PersistentState mutation queue so non-inline consumers keep their pre-PR get/set/clear behavior. The general concurrency hardening is replaced by the pre-PR clearing gate; only a no-op `preserveKeys` filter and `clearPersistentState(options)` remain so the generic Clear Cache command can preserve the inline association key without changing any other caller. Move the dedicated inline-cache deletion off the shared `PersistentState.clear([key])` (which coalesces onto an in-flight generic clear and could be silently dropped) and onto the inline-owned persistence queue via `set(key, undefined)`. Generic-preserve and inline-delete stay key-disjoint and the dedicated deletion cannot be lost or resurrected. The atomic-rename lock release hardening and its tests are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
| const _keys = (keys ?? this.momento.keys()).filter((key) => !preservedKeys.has(key)); | ||
| await Promise.all(_keys.map((key) => this.momento.update(key, undefined))); | ||
| this.clearing.resolve(); | ||
| } |
There was a problem hiding this comment.
Issue · Please address or respond
This is still a coalescing gate rather than a failure-isolated queue: concurrent callers await the first request and their own keys/preservation options are never applied. More importantly, if any momento.update rejects, this.clearing.resolve() is skipped, leaving the deferred pending so every later clear() waits forever. Queue each captured request independently and ensure a failed request cannot block later clears.
There was a problem hiding this comment.
You're right on both counts: the shared PersistentState.clear() is a coalescing gate, and a rejected Memento.update skips this.clearing.resolve(), leaving the deferred pending so every later clear() waits forever. Rather than reshape that shared implementation in this PR, I've taken the PR out of it entirely.
src/common/persistentState.tsis now reverted to base — this PR has no production diff in that file. Its general coalescing/wedge defect is unchanged and explicitly out of scope for this inline-only PR (happy to track it separately).- Inline PEP 723 associations moved to a new inline-owned,
Memento-backedInlineScriptAssociationStorethat owns onlyINLINE_SCRIPT_ENVS_KEY. It has its own failure-isolated FIFO queue: each caller awaits its own enqueued operation and receives that operation's success/failure, and a rejected op still advances the tail (this.tail = run.then(ok, ignore)) so a failed read/write/delete can't block later inline operations. The store never awaits the sharedclear()deferred, so a wedged generic clear cannot stall inline work. - Generic "Clear Cache" no longer needs any preserve option in shared code: at the command it reads the current workspace keys from the
Memento, excludesINLINE_SCRIPT_ENVS_KEY, and calls the existingPersistentState.clear(explicitKeys)plus the existing globalclear(). Generic clear and the inline store are key-disjoint (generic clears every key except the inline key; the store touches only the inline key), so neither can drop the other, and the dedicated inline delete is a queued direct update of that one key.
Net: this PR leaves the shared clear() behavior untouched, and inline association reliability no longer depends on it.
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. Result: Summary: No meaningful test execution was possible because the verification container could not start and local execution was not authorized. The PR adds 14 targeted unit tests covering lock retirement, persistent-state clearing, generic cache clearing, and inline persistence coordination. Their pass/fail status remains unverified. Test runs: 2 not run
|
Stop this PR from modifying or depending on the shared PersistentState implementation. src/common/persistentState.ts is reverted to base (no production diff); its general coalescing/failure defect is left unchanged and out of scope for this inline-only PR. Inline PEP 723 script-to-environment associations now live behind a new inline-owned InlineScriptAssociationStore (Memento-backed). It owns only INLINE_SCRIPT_ENVS_KEY, exposes no arbitrary keys, and serializes every read/mutation/deletion on its own failure-isolated FIFO queue: each caller gets its own operation's result or rejection, and a failed operation still advances the tail so later operations run. Verified writes mirror the prior PersistentState.set read-back semantics. All inline association paths (activation load, reads/rehydration, invalid-entry removal, updates, snapshots, and dedicated deletion) route through the store; the manager no longer depends on PersistentState. The dedicated clear is a queued direct update of only INLINE_SCRIPT_ENVS_KEY to undefined, so it cannot be coalesced onto (and dropped by) an overlapping generic clear. Generic "Clear Cache" preserves the inline key without changing PersistentState: it reads the current workspace keys from the injected Memento, excludes INLINE_SCRIPT_ENVS_KEY, and calls the existing PersistentState.clear(explicitKeys) plus the existing global clear() in parallel, keeping the prior command ordering (persistent state, then managers, then shell profile cache). Generic clear and the inline store are therefore key-disjoint and deterministic, and a wedged shared clear cannot block inline association work. Lock-release hardening and non-inline environment managers are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. Result: Summary: No tests could execute because the verification container failed to start: no trusted sandbox image is configured for this repository, and local execution was not authorized. The PR adds 19 targeted unit tests covering lock retirement, cache preservation, and inline association serialization. Because no meaningful test command ran, the verdict is **could-not-verify**. Test runs: 1 not run
|
d0d52d1
into
microsoft:main
Context
Inline-script environments live in a shared extension cache that may be accessed by more than one extension host. Clearing that cache is destructive and must coordinate with lock recovery, persistent-state clears, and inline association persistence.
The dedicated inline clear command remains hidden behind the undeclared, default-off inline-script flag and is not contributed to the Command Palette.
What changed
This PR is scoped to three coordinated hardening changes:
Atomic canonical lock retirement on release (
src/common/lockfile.apis.ts) —release()renames the owner marker to a transient.release-*marker and atomically renames the lock directory to a.retired-*sibling before cleaning it up out of band. TransientEPERM/EBUSY/EACCESretirement failures retry while ownership is retained; a terminal failure restores ownership and throwsELOCKRELEASEFAILED. Lock inspection recognizes the transient.release-*marker and tolerates the directory being retired betweenlstatandreaddir.Serialized persistent-state clears (
src/common/persistentState.ts) — every clear request is serialized through a failure-isolated queue tail, each request's key options are preserved, andclearPersistentStategainspreserveWorkspaceKeys/preserveGlobalKeys(ClearPersistentStateOptions).Generic Clear Cache preserves the inline association key (
src/common/constants.ts,src/features/envCommands.ts,src/extension.ts, and an import-only move ofINLINE_SCRIPT_ENVS_KEYin the inline-script manager) —python-envs.clearCachenow routes throughclearEnvironmentCachesCommand, which preserves the inline association workspace-state key (built on the serialized-clearpreserveKeysplumbing from change 2) while retaining existing behavior for all other state and managers.Behavior and compatibility
Reviewer guide
lockfile.apis.ts.persistentState.ts.envCommands.ts,extension.ts,constants.ts).Validation
npm run compile-testsnpm run compilenpm run lint