JournaledGrain snapshotting: replay only post-snapshot events (#144) - #166
Merged
Conversation
Replaces the GrainScopeInitializer/IGrainScopeInitializerRegistry/ AddGrainScopeInitializer family with a single compile-time-discovered IGrainUserServiceProviderFactory, addressing per-call DI resolution overhead found via benchmarking.
Replaces the "generator emits a duplicate satellite collection" idea with a marker-capture mechanism (AddQuarkOwnedScoped) that reuses the existing deferred-registration idiom with far less generator surface. Also documents a real correctness bug the naive design would have hit: BehaviorResolver captured its own ambient IServiceProvider, which would silently starve user-owned constructor parameters when resolved from the Quark-only scope — fixed by passing the construction provider explicitly. Narrows v1 scope to exclude persistence patterns that need cross-package services (IStorage<T> etc.), confirmed with the user.
10 TDD tasks covering: new interface/composite-provider/registry types, the BehaviorResolver ambient-scope-capture fix, RuntimeServiceCollection- Extensions wiring, SiloHostedService satellite-provider construction, GrainActivation branch, end-to-end tests, generator changes, and docs.
Design spec and task-by-task implementation plan for #144 — adds an optional ISnapshotStore so JournaledGrain replays only post-snapshot events instead of the entire log on every activation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces ISnapshotStore, SnapshotEnvelope<TState>, and CorruptSnapshotException in Quark.Persistence.Abstractions.Journaling as the foundation for JournaledGrain log snapshotting (#144). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deep-copies state on write and read (via ICopierProvider) to isolate the stored snapshot from the grain's live state, matching InMemoryGrainStorage isolation. Adds AddInMemorySnapshotStore() DI helper. (#144) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an optional ISnapshotStore ctor dependency, a per-type SnapshotInterval (default 100, 0 disables), and a protected WriteSnapshotAsync() hook. ConfirmEventsAsync writes a snapshot once ConfirmedVersion advances a full interval past the last snapshot. (#144) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On activation, reads the latest snapshot and replays only events after its version. A missing snapshot full-replays from 0 (unchanged). A snapshot whose version is ahead of the log throws CorruptSnapshotException (fail-fast); recovery is via ISnapshotStore.ClearSnapshotAsync. (#144) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes LedgerState [GenerateSerializer], registers its deep copier, forwards ISnapshotStore into LedgerBehavior with SnapshotInterval=5, and wires AddInMemorySnapshotStore() in the silo. (#144) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the read/write isolation requirement on ISnapshotStore's XML docs (ReadSnapshotAsync must return an isolated copy since the caller mutates it in place during tail replay; WriteSnapshotAsync must store an isolated copy since the caller keeps mutating its own state after the call returns), move the throwaway `new TState()` in ReloadFromLogAsync so it only allocates on the no-snapshot full-replay path, and add two tests: one exercising 2N/3N auto-snapshot cadence and one exercising read-isolation across repeated reactivations from the same snapshot via the real InMemorySnapshotStore. Co-Authored-By: Claude Opus 4.8 (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
Adds an optional snapshot mechanism to
JournaledGrain<TState,TEvent>so activation replays only the events after the latest snapshot instead of the entire event log from version 0. Previously replay cost was unbounded and grew linearly with a grain's whole history, forever.The event log remains the sole source of truth — a snapshot is only a replay-shortcut and can never change the replayed result.
What's included
ISnapshotStoreabstraction +SnapshotEnvelope<TState>+CorruptSnapshotException(Quark.Persistence.Abstractions.Journaling) — a dedicated snapshot store, separate fromILogStorage/IGrainStorage.InMemorySnapshotStore+AddInMemorySnapshotStore()— deep-copies state on write and read (viaICopierProvider) to isolate the stored snapshot from the grain's live, still-mutating state, matchingInMemoryGrainStorage.JournaledGrainwrite path — auto-snapshots every N confirmed events (protected virtual int SnapshotInterval => 100,0disables) plus a manualWriteSnapshotAsync()hook. New optionalISnapshotStore?ctor param (backward-compatible: unregistered → snapshotting off).JournaledGrainactivation path — seeds state from the snapshot and replays only the tail. Uses aVersion-1boundary probe to confirm the log contains the snapshot's events without adding any length API toILogStorage(relies on the existingversion == indexcontiguity guarantee).CorruptSnapshotException, recoverable viaClearSnapshotAsync.LedgerStategains[GenerateSerializer]; silo registersAddInMemorySnapshotStore()).Design & plan
docs/superpowers/specs/2026-07-10-journaledgrain-snapshotting-design.mddocs/superpowers/plans/2026-07-10-journaledgrain-snapshotting.mdScope
InMemory provider + the abstraction only. A
RedisSnapshotStoreand a durable RedisILogStorageare explicit, documented follow-ups (spec §9).Testing
ClearSnapshotAsyncrecovery, deep-copy isolation on both write and read, and a reactivate-twice read-isolation regression guard against the real store).JournaledGrainTestsunchanged and green (backward compatibility).Quark.Runtime,-r linux-x64 -p:PublishAot=true): clean.Fixes #144
🤖 Generated with Claude Code