Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
6b4e176
feat(durable-messaging): add envelope and routing contracts
ReubenBond Sep 16, 2026
f698562
test(durable-messaging): cover key ordering and null validation
ReubenBond Sep 16, 2026
7466516
docs(durable-messaging): correct segment validation comment
ReubenBond Sep 16, 2026
d8f5c2b
docs(durable-messaging): clarify safe-to-commit staging
ReubenBond Sep 17, 2026
ac7f416
test(durable-messaging): cover composed multi-segment keys
ReubenBond Sep 17, 2026
fb193b7
chore(durable-messaging): record inbox wire contract
ReubenBond Sep 17, 2026
319d8ee
fix(durable-messaging): reject default builder sender
ReubenBond Sep 17, 2026
eb82782
feat(durable-messaging)!: prepare handlers for synchronous apply
ReubenBond Sep 18, 2026
23689d8
feat(durable-messaging)!: require prepared outbox batches
ReubenBond Sep 20, 2026
d852b7b
docs(durable-messaging): clarify prepared batch ownership
ReubenBond Sep 20, 2026
098702b
fix(durable-messaging): preserve external consumer test boundary
ReubenBond Sep 20, 2026
ab0ae36
feat(journaling): add owned persistence operation hooks
ReubenBond Oct 7, 2026
a86f230
feat(durable-messaging): add hook-backed envelope send contracts
ReubenBond Oct 7, 2026
e682950
feat(durable-messaging): handle messages with explicit completion
ReubenBond Oct 8, 2026
0dea9ec
refactor(durable-messaging)!: reduce transport to owned opaque payloads
ReubenBond Oct 9, 2026
0f006dd
chore(durable-messaging): remove retired protocol source files
ReubenBond Oct 9, 2026
4395183
style(durable-messaging): format opaque contract ownership helpers
ReubenBond Oct 9, 2026
58e9638
test(journaling): require format metadata in JSON recovery
ReubenBond Oct 9, 2026
63836a1
test(journaling): assert writes stay fenced after JSON recovery rejec…
ReubenBond Oct 9, 2026
3eb0d39
feat(durable-messaging)!: identify commands by hierarchical keys and …
ReubenBond Oct 9, 2026
5c80957
feat(durable-messaging): expose outbox sender identity
ReubenBond Oct 10, 2026
2715548
fix(runtime): rearm early physical grain timer callbacks
ReubenBond Oct 9, 2026
918ad70
style(tests): format owned RPC lifetime fixtures
ReubenBond Oct 10, 2026
4a296f0
fix(durable-messaging): compose reviewed prerequisites and preserve j…
ReubenBond Oct 10, 2026
2981757
refactor(durable-messaging): carry direct Arc storage and provider-ow…
ReubenBond Oct 10, 2026
bfba49c
fix(durable-messaging): carry validated Arc boundaries and S3 conversion
ReubenBond Oct 10, 2026
3bab408
refactor(durable-messaging)!: use ordinary GC-owned byte payloads
ReubenBond Oct 10, 2026
86980ad
test(serialization): assert materialized pool slice page
ReubenBond Oct 10, 2026
cd1e8c1
refactor(durable-messaging): drop unused storage and ownership prereq…
ReubenBond Oct 10, 2026
ff2996c
chore(api): regenerate narrowed storage and payload surfaces
ReubenBond Oct 10, 2026
389ada8
refactor(durable-messaging): leave independent serialization optimiza…
ReubenBond Oct 10, 2026
4fd1c8f
chore(api): remove independent pool surface from contracts
ReubenBond Oct 11, 2026
de7ba4b
fix(durable-messaging): reconcile narrowed prerequisite scopes
ReubenBond Oct 11, 2026
04e5d3c
chore: preserve merged Arc correctness during contracts rebase
ReubenBond Oct 11, 2026
0d9eede
feat(durable-messaging): align mandatory journal hooks with current main
ReubenBond Oct 11, 2026
ef6a16b
refactor(durable-messaging): carry final journal hook simplification
ReubenBond Oct 11, 2026
683d11d
feat(durable-messaging): process owned Arc payloads in inbox
ReubenBond Oct 9, 2026
7755348
feat(durable-messaging): deduplicate inbox commands by application key
ReubenBond Oct 9, 2026
199751b
test(durable-messaging): preserve payload pins when identity conflicts
ReubenBond Oct 9, 2026
6ef9373
test(durable-messaging): migrate owned identity controls to valid pro…
ReubenBond Oct 9, 2026
99a2de6
test(durable-messaging): correlate retry ACK and fresh recovery
ReubenBond Oct 9, 2026
d970e0e
test(durable-messaging): verify ordinal subject admission and diagnos…
ReubenBond Oct 9, 2026
4d49b91
test(durable-messaging): expose test outbox owner identity
ReubenBond Oct 10, 2026
70592b7
fix(tests): release replay barrier before awaiting inbox snapshot
ReubenBond Oct 10, 2026
0125796
test(durable-messaging): assert callback rejection preserves journal …
ReubenBond Oct 10, 2026
78124ef
test(durable-messaging): adapt journal fixture to Arc storage
ReubenBond Oct 10, 2026
a99265a
refactor(durable-messaging): prepare GC-owned inbox payloads
ReubenBond Oct 10, 2026
73b8e5e
docs(durable-messaging): preserve immutable byte publication contract
ReubenBond Oct 10, 2026
02c9af5
test(durable-messaging): reject null payload before retained completion
ReubenBond Oct 10, 2026
ec2d7e1
test(durable-messaging): preserve direct published array identity
ReubenBond Oct 10, 2026
2689b73
refactor(durable-messaging): preserve main storage and byte publication
ReubenBond Oct 11, 2026
9c47520
test(durable-messaging): compile byte payload protocol controls
ReubenBond Oct 11, 2026
e60f683
test(durable-messaging): preserve retired GC dictionary snapshots
ReubenBond Oct 11, 2026
cda39a2
test(durable-messaging): distinguish entry and snapshot encoding failure
ReubenBond Oct 11, 2026
0425634
chore(durable-messaging): generate byte receiver API
ReubenBond Oct 11, 2026
8f14ee9
test(durable-messaging): use reduced journal hook interface
ReubenBond Oct 11, 2026
e22bede
feat(durable-messaging): deliver commands by application identity
ReubenBond Oct 9, 2026
5282662
feat(durable-messaging): expose outbox sender identity
ReubenBond Oct 10, 2026
db8590d
test(durable-messaging): verify outbox owner sender identity
ReubenBond Oct 10, 2026
16aa425
refactor(durable-messaging): use durable owner for pending equivalence
ReubenBond Oct 10, 2026
a635e53
test(durable-messaging): forward owned journal buffers directly
ReubenBond Oct 10, 2026
20c4ef0
refactor(durable-messaging): deliver GC-owned payloads
ReubenBond Oct 10, 2026
db1a25f
refactor(durable-messaging): simplify ordinary payload staging fixtures
ReubenBond Oct 10, 2026
d0f6143
refactor(durable-messaging): retain ordinary byte payload references
ReubenBond Oct 10, 2026
01b792a
fix(tests): preserve destination initialization in byte migration
ReubenBond Oct 11, 2026
d5712e3
test(durable-messaging): preserve retired GC snapshot after failed de…
ReubenBond Oct 11, 2026
d6b3a05
test(durable-messaging): keep journal hook callbacks internal
ReubenBond Oct 11, 2026
908c6c6
fix(tests): preserve codec imports and malformed payload coverage
ReubenBond Oct 11, 2026
aebeff7
test(durable-messaging): observe retired GC intents before fresh replay
ReubenBond Oct 11, 2026
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
2 changes: 2 additions & 0 deletions Orleans.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Project Path="src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj" />
<Project Path="src/Orleans.Core.Abstractions/Orleans.Core.Abstractions.csproj" />
<Project Path="src/Orleans.Core/Orleans.Core.csproj" />
<Project Path="src/Orleans.DurableMessaging/Orleans.DurableMessaging.csproj" />
<Project Path="src/Orleans.EventSourcing/Orleans.EventSourcing.csproj" />
<Project Path="src/Orleans.Journaling/Orleans.Journaling.csproj" />
<Project Path="src/Orleans.Persistence.Memory/Orleans.Persistence.Memory.csproj" />
Expand Down Expand Up @@ -152,6 +153,7 @@
<Project Path="test/Orleans.GrainDirectory.Tests/Orleans.GrainDirectory.Tests.csproj" />
<Project Path="test/Orleans.Streaming.Tests/Orleans.Streaming.Tests.csproj" />
<Project Path="test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj" />
<Project Path="test/Orleans.DurableMessaging.Tests/Orleans.DurableMessaging.Tests.csproj" />
<Project Path="test/Orleans.EventSourcing.Tests/Orleans.EventSourcing.Tests.csproj" />
<Project Path="test/Orleans.DurableJobs.Tests/Orleans.DurableJobs.Tests.csproj" />
</Folder>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Serializer naming policies affect application payload values. Journal command na

## Migrate a journal format

Providers expose the persisted format key as <xref:Orleans.Journaling.IJournalMetadata.FormatKey> and <xref:Orleans.Journaling.JournalMetadata.FormatKey>. Recovery selects the stored reader independently of the configured write format. When they differ, the next write creates a full snapshot using the configured format and updates the metadata. <xref:Orleans.Journaling.Json.JsonLinesJournalFormat.JournalFormatKey> supplies the JSON Lines format key.
Providers expose the persisted format key as <xref:Orleans.Journaling.IJournalMetadata.FormatKey> and <xref:Orleans.Journaling.JournalMetadata.FormatKey>. Recovery selects the stored reader independently of the configured write format. When stored format metadata is absent, recovery uses the configured format. New empty journals use the configured write format. When the stored and configured keys differ, the next write creates a full snapshot using the configured format and updates the metadata. <xref:Orleans.Journaling.Json.JsonLinesJournalFormat.JournalFormatKey> supplies the JSON Lines format key.

Use this deployment sequence:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ During <xref:Orleans.Runtime.GrainLifecycleStage.SetupState>, the manager:
1. Resets and replays each registered durable state.
1. Completes activation setup after replay finishes.

Recovery uses the configured format when stored format metadata is absent. New empty journals use the configured write format.

<xref:Orleans.Grain.OnActivateAsync*> and requests observe recovered durable state after setup succeeds, whether the grain derives directly from <xref:Orleans.Grain>, from an application-owned base, or from <xref:Orleans.Journaling.DurableGrain>. A storage read, format, codec, or malformed-data failure fails activation and preserves the stored journal for diagnosis and recovery.

Provider registration makes Journaling services available. Per-grain journal I/O begins only for activations which resolve the manager, directly or through durable-state dependencies. Grains which use other persistence models keep their existing activation behavior.
Expand Down Expand Up @@ -68,6 +70,46 @@ Orleans executes that synchronous block on a single activation thread. Another g
the operation awaits, so keep shared state safe to commit at each await. Any caller's write can include
staged mutations from other calls.

## Journal operation hooks

Activation-scoped features coordinate prerequisites and completion through
<xref:Orleans.Journaling.IJournaledStateManager.Hooks>. The owner exposes a stable, lazily
allocated list of <xref:Orleans.Journaling.IJournaledStateHook> registrations. Inspect and
deduplicate feature registrations on the owner's logical execution context while persistence
is quiescent. Registration survives recovery and deletion; the standard manager rejects hook-list
mutation while persistence is queued or running.
Every journal owner implementation provides this list and runs its registered callbacks at the
operation boundaries below. Delegating owners forward the list to the inner owner.

Each actual append, snapshot, or deletion runs ordinary before callbacks in list order, outside
the manager lock. At most one <xref:Orleans.Journaling.IJournaledStateCaptureHook> supplies the
final prerequisite. Its before callback runs last, and the work loop awaits it directly before
synchronous capture or storage deletion. Prerequisites cover changes staged during asynchronous
preparation, including the final hook's own I/O wait. Preserve operation-local bookkeeping for
the captured batch separately from changes staged later.

Storage acknowledgement and registered-state acknowledgement or reset precede after callbacks.
All after callbacks run in list order, including for successful zero-byte writes. Coalesced callers
share callbacks for the actual operation. Features implement the before and after callbacks on
their own hook, retaining feature identity and operation-local bookkeeping there.

A failed prerequisite reports <xref:Orleans.Journaling.JournaledStatePreCommitException> with pending
state retained for an explicit persistence retry after the prerequisite is restored. A failed after
callback reports <xref:Orleans.Journaling.JournaledStatePostCommitException> with persistence completed.
Remaining after callbacks run, multiple failures are aggregated, and the manager stays usable.
The feature's durable recovery protocol resumes interrupted post-persistence work. Storage and
state-processing failures retain the manager's fencing and fresh-recovery behavior.

Hook callbacks receive the owner's shutdown token. Cancelling a caller's wait leaves the owned
operation running through its actual outcome. Disposal drains owned hooks and storage before releasing
journal resources, including when cancellation callbacks or cleanup fail. Concurrent disposal callers
share this completion. Hook implementations complete without calling initialization, persistence,
or disposal on their own owner. Awaiting an operation serialized behind the current callback creates
a circular dependency. Shutdown closes work admission and cancels queued operations while the current
operation drains to its actual storage and hook outcome. For deletion, the feature owner stops
admission and drains feature operations before
queuing the whole-journal reset.

## Consistency and competing writers

Orleans grain placement normally supplies a single active writer for a grain identity. Journal storage providers also use optimistic concurrency to protect the journal when a stale or competing writer reaches storage.
Expand Down
14 changes: 10 additions & 4 deletions docs/site/src/content/docs/grains/timers.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ Register timers with <xref:Orleans.GrainBaseExtensions.RegisterGrainTimer*>. <xr

### Callback scheduling

A timer callback never overlaps itself. Orleans waits for the callback task to complete and then measures the period before scheduling the next callback. Callback duration therefore adds to the interval between callback starts.
Orleans completes each callback task before scheduling the next callback for that timer. It then measures the period, so callback duration adds to the interval between callback starts.

Timer callbacks are local-only messages addressed to their activation. They participate in normal turn scheduling and stay on the activation which registered them.
Timer callbacks are local-only messages addressed to their activation. They participate in normal turn scheduling and start a fresh call chain. Each callback receives a fresh request context, while the state passed at registration remains available to the callback.

A zero due time queues a callback directly on the activation. A zero period queues the next callback after the current callback completes. Both use the same activation scheduling, interleaving, and idle-lifetime rules as delayed ticks.

The configured <xref:System.TimeProvider> controls delayed tick timing and resolution; Orleans queues the provider's tick notifications on the activation.

### Interleaving

Expand All @@ -50,9 +54,11 @@ With <xref:Orleans.Runtime.GrainTimerCreationOptions.KeepAlive> set to `true`, e

## Change or stop a timer

Call <xref:Orleans.Runtime.IGrainTimer.Change*> to replace the due time and period. The new due time schedules the next callback, and the new period applies after that callback completes. A change made inside a running callback takes effect after the callback completes.
Call <xref:Orleans.Runtime.IGrainTimer.Change*> to replace the due time and period. When a callback is already queued or running, it keeps its turn and the change takes effect after it completes. Repeated changes use the latest due time and period for the following schedule.

A physical tick already dispatched by the provider can arrive after a change to another delayed schedule. Orleans admits that tick according to the activation's scheduling rules. After its callback completes, the configured period determines the next tick, or a change made during the callback supplies the next due time.

Dispose <xref:Orleans.Runtime.IGrainTimer> to cancel its callback token and stop future callbacks. Orleans also cancels the token and disposes the timer when the activation begins deactivating.
Dispose <xref:Orleans.Runtime.IGrainTimer> to invalidate queued ticks, cancel the token of an admitted callback, and stop further scheduling. Queued messages drain through activation scheduling. Orleans also cancels the token and disposes the timer when the activation begins deactivating.

## Handle callback failures

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@

namespace Documentation.Grains.Journaling;

// <journal_operation_hooks>
internal static class JournalHookRegistration
{
internal static void Register(IJournaledStateManager owner, IJournaledStateHook featureHook)
{
var hooks = owner.Hooks;
if (!hooks.Contains(featureHook))
{
hooks.Add(featureHook);
}
}
}
// </journal_operation_hooks>

// <composed_shopping_cart>
public interface IShoppingCartGrain : IGrainWithStringKey
{
Expand Down
7 changes: 5 additions & 2 deletions src/Orleans.Core/Messaging/MessageFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ public MessageFactory(DeepCopier deepCopier, ILogger<MessageFactory> logger, Mes
_seed = unchecked((ulong)Random.Shared.NextInt64());
}

public Message CreateMessage(object? body, InvokeMethodOptions options)
public Message CreateMessage(object? body, InvokeMethodOptions options) =>
CreateMessage(body, options, RequestContextExtensions.Export(_deepCopier));

public Message CreateMessage(object? body, InvokeMethodOptions options, Dictionary<string, object>? requestContextData)
{
var message = new Message
{
Expand All @@ -37,7 +40,7 @@ public Message CreateMessage(object? body, InvokeMethodOptions options)
IsUnordered = (options & InvokeMethodOptions.Unordered) != 0,
IsAlwaysInterleave = (options & InvokeMethodOptions.AlwaysInterleave) != 0,
BodyObject = body,
RequestContextData = RequestContextExtensions.Export(_deepCopier),
RequestContextData = requestContextData,
};

return message;
Expand Down
Loading
Loading