You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(journaling)!: require operation prerequisite and completion hooks - #11465
Journal-backed features need prerequisites before capture and completion work after storage ACK. They also need to install or remove hooks from state lifecycle callbacks while keeping each persistence operation's callback set stable.
Solution
Every IJournaledStateManager provides AddHookAsync(hook, index, cancellationToken) and RemoveHookAsync(hook, cancellationToken). Registrations are private to the manager; features retain their own hook references. The backing collection is a lazily allocated list.
Registration completes synchronously before initialization, supporting constructors. After initialization begins, idle changes apply synchronously; busy changes enter the work queue and apply after the current operation's after callbacks finish. Admission uses the manager's existing lock because standalone managers also run outside grain schedulers. Once dequeued, the work loop owns the hook set and applies queued changes directly. Failed recovery publishes idle state and queued failures together under the admission lock.
Queued changes separate write-coalescing groups. Caller cancellation ends its wait while the manager owns admitted changes. Shutdown cancels queued changes; persistence failures preserve their original cause. Invalid queued changes are logged and fault their own completion while the owner stays usable.
Before callbacks run in reverse registration order; after callbacks run forward. Register the final prerequisite with index: 0. Every before callback is awaited. Prerequisites remain valid through capture/deletion, including changes staged during asynchronous preparation or resumed turns. The completion-to-capture boundary retains its documented non-atomic behavior.
Synchronous state callbacks can request a change and observe its queued completion outside the callback. Features await registration before staging changes which depend on its effect. Constructor registration uses the guaranteed synchronous setup path; features created after initialization observe completion during asynchronous setup.
Append and snapshot requests use one WriteStateWorkItem. Operation selection consumes _snapshotRequired, retaining the decision locally; newer requests survive earlier ACKs. Before-hook failure restores the requirement. Snapshot writers validate preserved-entry formats before storage I/O. A tuple-returning helper gathers storage diagnostics, with status and disposal owned by the work loop.
JournaledStatePreCommitException reports retained pending state; JournaledStatePostCommitException reports completed persistence. Every after hook runs despite cleanup failures. Storage failures retain fencing and original-error precedence. Disposal drains owned hooks and I/O before releasing resources, and concurrent or reentrant disposal shares one completion. Callback dependencies between journal owners remain acyclic.
Consumer adoption
Custom owners implement the two registration methods; delegating owners forward them. Replace direct collection mutation with these methods and retain hook references for removal. Remove forwarding or inspection of the former Hooks property. Final prerequisites register using AddHookAsync(hook, index: 0) with completion established before dependent work.
This verified successor is adopted by the coordinated Messaging chain through an explicit pin. Parent and sibling worktrees retain their current pins until that integration is sequenced.
Package validation remains enabled. Hook-specific compatibility suppressions cover the two required registration methods on net8.0/net10.0; obsolete property suppressions are removed. Normal Release pack passes, and the public surface is regenerated using the official GenAPI target.
Rebase and scope
Based on main 79873eb26a59742bc520fcae87c19fe7dc23c7df, preserving merged timer, caching, Arc-lifetime and volatile-snapshot work. The extraction originated from 48f3aaa47fc18123db4109063af591f315e6f9cb..9c1ef0f2feb258c1764de5f408d89c53e7e63f9f.
Storage borrowing contracts, V0/V1 framing/readers, retained storage and dictionary value ownership remain intact. Composition preserves the shared shutdown cancellation/drain correction and dequeue gate once.
Reject hook-list mutations while a persistence operation is queued as well as while it is executing. On the owner's single-threaded context, WriteStateAsync can enqueue and return an incomplete task before the work loop gets to set _hookOperationRunning; a subsequent Hooks.Add/Clear therefore succeeds and changes the hook set of an already-admitted operation, contradicting the operation-stable/in-flight mutation contract. Checking the queue under this same lock closes that admission-to-dequeue gap.
The current-main baseline is commit 79873eb26a and uses the same reviewed coverage matrix.
Coverage combines every CI test matrix job, including providers, CodeGen, .NET 8/10, Linux, Windows, and macOS, using canonical physical source and branch identities.
The comparison remains report-only while normal line and branch variance is calibrated.
ReubenBond
changed the title
feat(journaling): add operation prerequisite and completion hooks
feat(journaling)!: require operation prerequisite and completion hooks
Oct 11, 2026
This message says all journal registrations are frozen after initialization, but the newly added hook-registration contract explicitly permits idle or queued changes after initialization. This guard only protects state-machine/durable-state registration, so the broader wording gives callers contradictory guidance; name state registration specifically and update the matching assertion.
🧠 Review effort: Balanced
This branch has not been deployed
No deployments
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
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.
Problem
Journal-backed features need prerequisites before capture and completion work after storage ACK. They also need to install or remove hooks from state lifecycle callbacks while keeping each persistence operation's callback set stable.
Solution
Every
IJournaledStateManagerprovidesAddHookAsync(hook, index, cancellationToken)andRemoveHookAsync(hook, cancellationToken). Registrations are private to the manager; features retain their own hook references. The backing collection is a lazily allocated list.Registration completes synchronously before initialization, supporting constructors. After initialization begins, idle changes apply synchronously; busy changes enter the work queue and apply after the current operation's after callbacks finish. Admission uses the manager's existing lock because standalone managers also run outside grain schedulers. Once dequeued, the work loop owns the hook set and applies queued changes directly. Failed recovery publishes idle state and queued failures together under the admission lock.
Queued changes separate write-coalescing groups. Caller cancellation ends its wait while the manager owns admitted changes. Shutdown cancels queued changes; persistence failures preserve their original cause. Invalid queued changes are logged and fault their own completion while the owner stays usable.
Before callbacks run in reverse registration order; after callbacks run forward. Register the final prerequisite with
index: 0. Every before callback is awaited. Prerequisites remain valid through capture/deletion, including changes staged during asynchronous preparation or resumed turns. The completion-to-capture boundary retains its documented non-atomic behavior.Synchronous state callbacks can request a change and observe its queued completion outside the callback. Features await registration before staging changes which depend on its effect. Constructor registration uses the guaranteed synchronous setup path; features created after initialization observe completion during asynchronous setup.
Append and snapshot requests use one
WriteStateWorkItem. Operation selection consumes_snapshotRequired, retaining the decision locally; newer requests survive earlier ACKs. Before-hook failure restores the requirement. Snapshot writers validate preserved-entry formats before storage I/O. A tuple-returning helper gathers storage diagnostics, with status and disposal owned by the work loop.JournaledStatePreCommitExceptionreports retained pending state;JournaledStatePostCommitExceptionreports completed persistence. Every after hook runs despite cleanup failures. Storage failures retain fencing and original-error precedence. Disposal drains owned hooks and I/O before releasing resources, and concurrent or reentrant disposal shares one completion. Callback dependencies between journal owners remain acyclic.Consumer adoption
Custom owners implement the two registration methods; delegating owners forward them. Replace direct collection mutation with these methods and retain hook references for removal. Remove forwarding or inspection of the former
Hooksproperty. Final prerequisites register usingAddHookAsync(hook, index: 0)with completion established before dependent work.This verified successor is adopted by the coordinated Messaging chain through an explicit pin. Parent and sibling worktrees retain their current pins until that integration is sequenced.
Package validation remains enabled. Hook-specific compatibility suppressions cover the two required registration methods on net8.0/net10.0; obsolete property suppressions are removed. Normal Release pack passes, and the public surface is regenerated using the official GenAPI target.
Rebase and scope
Based on main
79873eb26a59742bc520fcae87c19fe7dc23c7df, preserving merged timer, caching, Arc-lifetime and volatile-snapshot work. The extraction originated from48f3aaa47fc18123db4109063af591f315e6f9cb..9c1ef0f2feb258c1764de5f408d89c53e7e63f9f.Storage borrowing contracts, V0/V1 framing/readers, retained storage and dictionary value ownership remain intact. Composition preserves the shared shutdown cancellation/drain correction and dequeue gate once.
Microsoft Reviewers: Open in CodeFlow