fix(adt): skip redundant mutation gate after lock to keep stateful session alive#125
Open
dme007 wants to merge 1 commit into
Open
fix(adt): skip redundant mutation gate after lock to keep stateful session alive#125dme007 wants to merge 1 commit into
dme007 wants to merge 1 commit into
Conversation
…ssion alive
EditSourceWithOptions and the other write workflows ran the unified
mutation gate up front, then called LockObject, then called UpdateSource
(or UpdateClassInclude / DeleteObject / CreateTestInclude) — and those
inner mutators ran the gate AGAIN. Under SAP_ALLOWED_PACKAGES, the
inner gate's getObjectPackage path issues a STATELESS hop:
GET /sap/bc/adt/repository/informationsystem/search?…&query=…
When that stateless hop is interleaved between a stateful Lock and the
stateful PUT/DELETE/POST, SAP's ICM retires the stateful session
server-side (Sap-Err-Id: ICMENOSESSION). The lock handle bound to the
dead session is then rejected on the write with HTTP 423
ExceptionResourceInvalidLockHandle. Reproduced live in
/tmp/vsp-mcp-trace-dsd201.log lines 1167 (LOCK) → 1192
(quickSearch) → 1216 (failing PUT).
This is the exact same bug class as commit 8cb45a5 (SyntaxCheck before
Lock), just at a different call site that the prior fix did not cover.
The Stateful flags on the per-request RequestOptions are necessary but
not sufficient — call ordering matters too, and any stateless hop
between Lock and Write breaks the session.
Fix introduces an unexported context-key (mutationGateSkipKey) plus
two helpers — withMutationGateAlreadyRan and mutationGateAlreadyRan —
in pkg/adt/mutation_gate.go. checkMutation short-circuits when the
flag is set. Outer workflows that already ran the gate before
LockObject mark the context so the inner mutators skip their
redundant gate.
Workflows updated to mark the context after their own gate call:
EditSourceWithOptions workflows_edit.go
WriteProgram workflows.go
WriteClass workflows.go
CreateAndActivateProgram workflows.go
CreateClassWithTests workflows.go
CreateFromFile workflows_deploy.go (also adds full gate)
UpdateFromFile workflows_deploy.go (also adds full gate)
RenameObject workflows_fileio.go
ExecuteABAP workflows_execute.go (also adds full gate)
writeSourceUpdate (INTF, DDLS/BDEF/SRVD) workflows_source.go
writeSourceCreate (INTF, DDLS/BDEF/SRVD) workflows_source.go
writeClassMethodUpdate workflows_source.go
Activate is not touched: it does not resolve the package and never
issues a SearchObject hop, so the inner gate already wouldn't break
the session there. WriteMessageClassTexts / WriteDataElementLabels
and the UI5 mutators are left alone — their inner gate is reachable
only from external lock-acquiring callers (MCP tool handlers), so
deciding when to skip is a caller concern.
Tests:
TestMutationGateSkip_EditSourceNoSearchBetweenLockAndPut
— full happy-path EditSourceWithOptions under AllowedPackages,
asserts no informationsystem/search request appears between
the Lock POST and the source PUT.
TestMutationGateSkip_FlagSkipsInnerCheck
— unit-tests the skip plumbing: a deliberately-invalid
MutationContext (no ObjectURL, no Package) that would normally
fail closed under AllowedPackages must short-circuit when the
context is marked.
TestMutationGateSkip_FlagDoesNotLeakAcrossContexts
— the flag is scoped to its derived context only; sibling
contexts derived from the same parent stay clean.
go test ./... is green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sister bug to commit 8cb45a5 (SyntaxCheck before Lock). When
SAP_ALLOWED_PACKAGESis configured, the inner mutation gate insideUpdateSource/UpdateClassInclude/DeleteObject/CreateTestIncluderesolves the package viagetObjectPackage—which issues a STATELESS
informationsystem/searchhop. When thathop is interleaved between a stateful
LockObjectand the statefulwrite, SAP's ICM retires the stateful session and the lock handle is
invalidated on the write with HTTP 423
ExceptionResourceInvalidLockHandle.Captured live against an on-prem S/4HANA via SAP Cloud Connector +
Principal Propagation. The trace below is from the working session
that finally produced the diagnosis. Full
go test ./...is green.What & why
pkg/adt/workflows_edit.go:153correctly runscheckMutationoncebefore
LockObject(line 363). The bug is on the inside: after thelock is acquired,
EditSourceWithOptionscallsUpdateSource(
pkg/adt/crud.go:138) orUpdateClassInclude(crud.go:1027),and both of those call
checkMutationAGAIN. UnderAllowedPackages, the second invocation has to resolve the packagefrom
ObjectURL, which fans out toSearchObject— and that hoprides the transport's stateless default.
Reproduced on
EditSourceWithOptionsvia live HTTP trace:The lock handle is bound to the stateful session that the search hop
killed. Refetching CSRF gets a new token but the lock handle is gone,
and the retry hits 423.
Fix
Unexported context-key
mutationGateSkipKeyplus two helpers inpkg/adt/mutation_gate.go:checkMutationshort-circuits immediately when the context is marked.Outer workflows that have already run the gate before
LockObjectmark the context and the inner mutators skip their redundant gate.
This keeps the precise package check intact (it still runs once, at
the outer entry point) while removing the second resolution from
between
LockandWrite. The Stateful flags from 22517d4 remainnecessary; ordering matters too, and any stateless hop in the middle
breaks the session.
Activateis intentionally not touched — it runs onlycheckSafety(no
getObjectPackage), so it never injects a SearchObject hop.WriteMessageClassTexts/WriteDataElementLabels/ UI5 mutatorsare leaf APIs reachable only from external lock-acquiring callers
(MCP tool handlers); deciding when to skip is a caller concern.
Workflows updated to mark the context
EditSourceWithOptions— the smoking gun from the traceWriteProgram,WriteClass— same Lock→UpdateSource patternCreateAndActivateProgram— Create→Lock→UpdateSource patternCreateClassWithTests— Create→Lock→UpdateSource→CreateTestInclude→UpdateClassIncludeCreateFromFile— added an upfront unified gate (was onlycheckSafety)UpdateFromFile— added an upfront unified gate (was onlycheckSafety)RenameObject— already gates twice; mark after bothExecuteABAP— added an upfront unified gate ($TMP); avoids the bug for the temp-program Lock→PUT path under any AllowedPackages whitelist that excludes $TMP it now fails up front instead of mid-writewriteSourceUpdate(INTF, DDLS/BDEF/SRVD branches) — direct Lock→UpdateSourcewriteSourceCreate(INTF, DDLS/BDEF/SRVD branches) — direct Create→Lock→UpdateSourcewriteClassMethodUpdate— direct Lock→UpdateSourceWriteSourceitself stays as-is: it intentionally delegates packageresolution to its inner workflow methods, which now do the gate-and-mark.
Files changed
Tests
TestMutationGateSkip_EditSourceNoSearchBetweenLockAndPut— fullEditSourceWithOptions happy path under
WithAllowedPackages("$TMP"),asserts no
informationsystem/searchrequest appears between theLock POST and the source PUT. Verified to FAIL when the
withMutationGateAlreadyRan(ctx)line inEditSourceWithOptionsisremoved (reproduces the exact bug pattern from the trace).
TestMutationGateSkip_FlagSkipsInnerCheck— unit-tests the skipplumbing: a deliberately invalid
MutationContext(noObjectURL,no
Package) that would normally fail closed underAllowedPackagesmust short-circuit when the context is marked.
TestMutationGateSkip_FlagDoesNotLeakAcrossContexts— flag is scopedto its derivation chain only; sibling contexts derived from the same
parent stay clean.
go test ./pkg/adt/...andgo test ./...both green.Related
Commit 8cb45a5 —
fix(adt): move SyntaxCheck before Lock in deploy workflows. Same bug class (stateless hop between statefulLock and stateful Write retires the ICM session), different
injection point. That fix moved a stateless
SyntaxCheck; thisfix removes a stateless
SearchObjectfrom the same window. Thetwo together close the class for all currently known mutation
workflows.
Issues ExceptionResourceInvalidLockHandle, EditSource, WriteSource, CloneObject, ImportFromFile .. #88 / Bug Report: Invalid Lock Handle (Status 423) on Windows when Writing/Updating ABAP Objects #92 / [Bug] 423 "Invalid Lock Handle" when editing DDLS objects — LOCK and UPDATE_SOURCE executed in separate HTTP sessions #98 — session affinity. The Stateful flags
on individual
RequestOptionsfrom 22517d4 are necessary but notsufficient; call ordering matters too. With both this PR and 8cb45a5
in place, the
Lock → Write → Unlock → Activateblock is a singleuninterrupted stateful run for every workflow that holds a lock.
Non-goals
UpdateSource,UpdateClassInclude,DeleteObject,CreateTestInclude,WriteMessageClassTexts,WriteDataElementLabels) keep their inner gate. Callers thatacquire their own lock and bypass the workflows still get
policy enforcement; if they hit the same session-affinity bug,
the fix is the same — gate-and-mark before the lock.