Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
0634e80
feat(messaging): add durable messaging contracts
ReubenBond Oct 12, 2026
0469576
refactor(messaging): rename InboxOptions to DurableMessagingOptions
ReubenBond Oct 12, 2026
9c59988
feat(messaging): create message types from the serializer service
ReubenBond Oct 12, 2026
6c9fc07
docs(messaging): add Microsoft.Orleans.Messaging to the package inven…
ReubenBond Oct 12, 2026
d77bee4
chore(messaging): add generated API surface
ReubenBond Oct 12, 2026
15bebb4
fix(messaging)!: reserve zero as an invalid delivery status
ReubenBond Oct 13, 2026
5451697
fix(messaging): check cancellation before decoding typed inbox bodies
ReubenBond Oct 13, 2026
b61ca4d
build(messaging): set the package readme file
ReubenBond Oct 13, 2026
ff8113e
feat(messaging): add Envelope.DeliverAt
ReubenBond Oct 13, 2026
f94c636
docs(messaging): align capacity and pending-intent contracts with Del…
ReubenBond Oct 13, 2026
8c71cd4
test(messaging): cover DeliverAt wire round trips and HierarchicalKey…
ReubenBond Oct 13, 2026
ab64055
feat(messaging): add outbox message source and inbox message sink con…
ReubenBond Oct 13, 2026
55b239b
feat(messaging): send source-provided messages by ID and let sources …
ReubenBond Oct 13, 2026
671a91e
feat(messaging): identify messages with MessageId wrapping Hierarchic…
ReubenBond Oct 13, 2026
2be6376
feat(messaging): dead-letter outbox messages whose source fails
ReubenBond Oct 13, 2026
3bca153
feat(messaging): offer scheduled messages to inbox sinks and let sink…
ReubenBond Oct 13, 2026
390ab1e
refactor(messaging)!: decode message types from envelopes only
ReubenBond Oct 13, 2026
528a8e2
refactor(messaging): rename IInboxExtension to IMessagingExtension
ReubenBond Oct 13, 2026
1aa0ad3
feat(messaging): run outbox delivery attempts through a Polly resilie…
ReubenBond Oct 13, 2026
3c60582
feat(messaging)!: demote failed deliveries to later batches and retry…
ReubenBond Oct 14, 2026
9652ce0
refactor(messaging)!: track one outbox retry period and drop MaxHandl…
ReubenBond Oct 14, 2026
d243d93
refactor(messaging)!: align outbox retry terminology with Temporal
ReubenBond Oct 14, 2026
6de36ca
fix(messaging): reject ill-formed UTF-16 in identities and header names
ReubenBond Oct 18, 2026
cd6fd25
feat(messaging)!: make inbox handler selection read-only
ReubenBond Oct 18, 2026
3760728
docs(messaging): describe DeliverAt acceptance and capacity in the RE…
ReubenBond Oct 18, 2026
df7983d
build(messaging): enable trim, AOT, and single-file analyzers
ReubenBond Oct 18, 2026
476f459
feat(messaging)!: land the final delivery, retirement, and source con…
ReubenBond Oct 19, 2026
aae8dbb
test(messaging): check identity laws with CsCheck and pin the wire fo…
ReubenBond Oct 19, 2026
0180d6b
feat(messaging): interleave deliveries with the grain's other requests
ReubenBond Oct 19, 2026
8f3f8a2
feat(messaging): carry each delivery's redelivery deadline to the rec…
ReubenBond Oct 20, 2026
fd865f5
fix(messaging): address review feedback
ReubenBond Oct 20, 2026
83df265
docs(messaging): clarify retirement callback recovery
ReubenBond Oct 20, 2026
66584ff
feat(messaging): define clock-independent admission and durable outcomes
ReubenBond Oct 20, 2026
1db69dc
docs(messaging): align rejection tracking with durable fencing
ReubenBond Oct 20, 2026
05bb339
feat(messaging)!: identify sends by SentAt and expire records after t…
ReubenBond Oct 20, 2026
83fdfc9
docs(messaging): describe record extension for later send times
ReubenBond Oct 20, 2026
13a4422
docs(messaging): bound accepted send times by the deduplication window
ReubenBond Oct 20, 2026
92c69fb
docs(messaging): answer known messages before applying the send-time …
ReubenBond Oct 21, 2026
8999537
docs(messaging): note that expiry takes precedence over the send-time…
ReubenBond Oct 21, 2026
29190aa
docs(messaging): state how self-sends relate to the send-time bound
ReubenBond Oct 21, 2026
523e7ee
fix(messaging): report ill-formed subjects like header keys
ReubenBond Oct 21, 2026
0da5cb1
fix(messaging)!: qualify the messaging extension's alias
ReubenBond Oct 21, 2026
875a8d7
docs(messaging): state buffer ownership after withdrawal and pending …
ReubenBond Oct 21, 2026
c13f641
chore(api): regenerate Orleans.Messaging API surface
ReubenBond Oct 31, 2026
e0cc0f2
feat(journaling): add journaled state hooks and write preparation
ReubenBond Oct 12, 2026
f4b2465
feat(messaging): process inbox messages
ReubenBond Oct 12, 2026
0513a3b
refactor(journaling): remove operation hooks
ReubenBond Oct 12, 2026
270e371
fix(messaging): start the pump loop asynchronously
ReubenBond Oct 12, 2026
f765266
test(messaging): remove handler races and simplify timeout reporting
ReubenBond Oct 13, 2026
61a7271
test(messaging): capture pump shutdown token before deactivation
ReubenBond Oct 13, 2026
9e7ff86
test(messaging): hold the active handler through deactivation
ReubenBond Oct 13, 2026
0d3c9a8
fix(messaging): address inbox review feedback
ReubenBond Oct 13, 2026
905b021
fix(messaging): discard wake-ups overtaken by a completed job run
ReubenBond Oct 13, 2026
b8817c3
feat(messaging): process inbox messages at DeliverAt
ReubenBond Oct 13, 2026
34f0e93
test(messaging): fail once per message across reactivations
ReubenBond Oct 13, 2026
c80e199
test(messaging): compare staged test outbox messages by command equiv…
ReubenBond Oct 13, 2026
4bef107
fix(messaging): bind IInbox to the delivered inbox
ReubenBond Oct 13, 2026
377a262
feat(messaging): accept messages into application state through inbox…
ReubenBond Oct 13, 2026
4cbf498
test(messaging): implement the source-provided outbox send in the tes…
ReubenBond Oct 13, 2026
2b193ca
feat(messaging): identify messages with MessageId wrapping Hierarchic…
ReubenBond Oct 13, 2026
4c5d23a
feat(messaging): offer scheduled messages to inbox sinks and let sink…
ReubenBond Oct 13, 2026
a4a71b3
refactor(messaging): rename IInboxExtension to IMessagingExtension
ReubenBond Oct 13, 2026
a290931
feat(messaging)!: demote failed deliveries to later batches and retry…
ReubenBond Oct 14, 2026
32902b2
refactor(messaging)!: track one outbox retry period and drop MaxHandl…
ReubenBond Oct 14, 2026
ab01433
refactor(messaging)!: align outbox retry terminology with Temporal
ReubenBond Oct 14, 2026
7107aa6
feat(messaging): keep activations alive while the pump has inbox work
ReubenBond Oct 14, 2026
1d5cea0
feat(messaging)!: bound inbox handler retries by time instead of atte…
ReubenBond Oct 15, 2026
fdd4203
feat(messaging): wake grains for work held by registered inbox work s…
ReubenBond Oct 15, 2026
f06d5f8
fix(messaging): acknowledge duplicates only after their completion is…
ReubenBond Oct 18, 2026
3e5739e
fix(messaging): count sink dead-letters as received dead_lettered
ReubenBond Oct 18, 2026
ad482fd
fix(messaging): resume pending messages when a handler registers inst…
ReubenBond Oct 18, 2026
978d117
fix(messaging): schedule inbox retries on the Durable Jobs clock
ReubenBond Oct 18, 2026
cdf9400
fix(messaging): schedule an earlier wake-up for work due before the c…
ReubenBond Oct 18, 2026
3de24bd
feat(messaging)!: make inbox handler selection read-only
ReubenBond Oct 18, 2026
88d9a06
fix(messaging): report null delivery pipeline through options validation
ReubenBond Oct 18, 2026
0dae570
chore(messaging): remove unused ObjectPool package reference
ReubenBond Oct 18, 2026
befe604
test(messaging): fix test helper edge cases
ReubenBond Oct 18, 2026
5059f4e
test(messaging): build the messaging test cluster during fixture init…
ReubenBond Oct 18, 2026
953869a
fix(messaging): normalize non-positive work-source delays to a prompt…
ReubenBond Oct 18, 2026
35942ad
fix(messaging): answer duplicates of sink-rejected messages with thei…
ReubenBond Oct 18, 2026
0b7732f
fix(messaging): preserve inbox wakeups across capture and failed passes
ReubenBond Oct 19, 2026
9e59b8a
fix(messaging)!: implement the sink failure and rejection-code contra…
ReubenBond Oct 19, 2026
157bbd4
fix(messaging): count scheduled messages toward inbox capacity
ReubenBond Oct 19, 2026
cc089a0
fix(messaging): keep a wake-up for committed work through any outage
ReubenBond Oct 19, 2026
59db261
feat(journaling): count staged changes for messaging
ReubenBond Oct 19, 2026
40561fe
fix(messaging): discard a failed handler attempt's staged changes
ReubenBond Oct 19, 2026
7011a8a
test(messaging): check inbox retries on an independent job clock and …
ReubenBond Oct 19, 2026
1f03efc
docs(journaling): state that write preparation covers every state on …
ReubenBond Oct 19, 2026
4ee7aae
fix(messaging): start the pump when activation completes
ReubenBond Oct 19, 2026
84d9d25
test(messaging): admit a delivery while a non-reentrant request runs
ReubenBond Oct 19, 2026
1b2dd64
fix(messaging): reject a delivery whose redeliveries outlast the dedu…
ReubenBond Oct 20, 2026
5ae6295
fix(messaging): fence forgotten inbox sends durably
ReubenBond Oct 20, 2026
8df39fc
docs(messaging): describe fenced inbox retention
ReubenBond Oct 20, 2026
6d11e30
test(messaging): prove immutable inbox admission and retained identity
ReubenBond Oct 20, 2026
fed5e73
perf(messaging): compact acknowledged snapshots without completion ti…
ReubenBond Oct 20, 2026
5c2ad95
test(messaging): preserve admission identity without processing times…
ReubenBond Oct 20, 2026
eeeb71a
feat(messaging): retain identities by their latest send time
ReubenBond Oct 20, 2026
8c49020
docs(messaging): describe finite send-time retention
ReubenBond Oct 20, 2026
7866d4e
test(messaging): cover required send times and finite inbox retention
ReubenBond Oct 20, 2026
eab57ec
style(messaging): format send-time identity fixtures
ReubenBond Oct 20, 2026
b465160
test(messaging): adapt inbox-layer sink and scheduling send times
ReubenBond Oct 20, 2026
fe2fbac
fix(messaging): make processed send-time extensions replayable
ReubenBond Oct 20, 2026
9930e6a
test(messaging): record append boundaries and self-contained retentio…
ReubenBond Oct 20, 2026
89921bc
fix(messaging): reject send times beyond the receiver window
ReubenBond Oct 20, 2026
5f4db74
fix(messaging): report receiver and pending extension invariants
ReubenBond Oct 21, 2026
1ed2654
fix(messaging): bound only send times which admission would store
ReubenBond Oct 21, 2026
e3ba765
fix(messaging): assert the self-send admission bound
ReubenBond Oct 21, 2026
e44214c
fix(journaling): report unknown staged changes while a state can't re…
ReubenBond Oct 21, 2026
c808308
test(messaging): compact expired records interleaved with retained ones
ReubenBond Oct 21, 2026
1432c3a
test(messaging): pin the admission floor clamp under the maximum window
ReubenBond Oct 21, 2026
89771ca
fix(messaging): charge attempts which an unclean stop interrupted
ReubenBond Oct 21, 2026
8b09c68
test(messaging): pin the inbox send-time admission order
ReubenBond Oct 31, 2026
9980812
style(messaging): remove trailing blank line from IInboxHandler
ReubenBond Oct 31, 2026
c90a01e
feat(messaging): deliver outbox messages
ReubenBond Oct 19, 2026
42b2c80
test(messaging): check delivery under generated fault schedules
ReubenBond Oct 19, 2026
e2440da
test(messaging): stop reading a deactivating sender's live outbox
ReubenBond Oct 20, 2026
24af403
feat(messaging): stamp each delivery attempt with its redelivery dead…
ReubenBond Oct 20, 2026
b651968
fix(messaging): preserve one admission cutoff for each send
ReubenBond Oct 20, 2026
c0619d9
fix(messaging): validate recovered outbox admission evidence
ReubenBond Oct 20, 2026
77c963e
test(messaging): prove durable terminal work across delivery faults
ReubenBond Oct 20, 2026
3441ad1
test(messaging): reclaim fenced history only on snapshot acknowledgement
ReubenBond Oct 20, 2026
50f7b7d
test(messaging): hold redelivery before advancing the receiver clock
ReubenBond Oct 20, 2026
da182b5
refactor(messaging): journal send time and derive retry expiration
ReubenBond Oct 20, 2026
4dc25c3
test(messaging): prove SentAt redelivery and durable retention extension
ReubenBond Oct 20, 2026
0bb33a2
style(messaging): format send-time recovery fixtures
ReubenBond Oct 20, 2026
95231eb
style(messaging): remove duplicated sink fixture import
ReubenBond Oct 20, 2026
48e1574
refactor(messaging): use expiration and admission-floor terminology
ReubenBond Oct 20, 2026
5fe98bf
test(messaging): recover retention extensions after omitted snapshots
ReubenBond Oct 20, 2026
223afea
test(messaging): pin future-time admission and independent clock retries
ReubenBond Oct 20, 2026
2ceb0cd
test(messaging): pin rollback admission bounds and replay invariants
ReubenBond Oct 21, 2026
e4e57db
test(messaging): preserve known outcomes across receiver clock rollback
ReubenBond Oct 21, 2026
bb3e234
fix(messaging): report the outbox's staged sends
ReubenBond Oct 21, 2026
2fdc01b
test(messaging): show a failed attempt's retry stays covered by the s…
ReubenBond Oct 21, 2026
f449a48
test(messaging): dispose the bootstrap probe's event subscription
ReubenBond Oct 21, 2026
6682ed3
test(messaging): charge one attempt per due message after an unclean …
ReubenBond Oct 21, 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.Messaging/Orleans.Messaging.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.Messaging.Tests/Orleans.Messaging.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
3 changes: 2 additions & 1 deletion docs/site/src/content/docs/resources/nuget-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ms.topic: reference

# Orleans NuGet packages

All official packages use the `Microsoft.Orleans` prefix and are published on [NuGet.org](https://www.nuget.org/profiles/Orleans). Keep Orleans package versions aligned within an application.
Official packages use the `Microsoft.Orleans` prefix. Find released packages on [NuGet.org](https://www.nuget.org/profiles/Orleans). Keep Orleans package versions aligned within an application.

## Start with a metapackage

Expand Down Expand Up @@ -76,6 +76,7 @@ Memory persistence distributes records across cluster storage grains but isn't d
| [Microsoft.Orleans.Reminders.Redis](https://www.nuget.org/packages/Microsoft.Orleans.Reminders.Redis) | Redis reminders. |
| [Microsoft.Orleans.DurableJobs](https://www.nuget.org/packages/Microsoft.Orleans.DurableJobs) | Distributed scheduling for durable one-time jobs. |
| [Microsoft.Orleans.DurableJobs.AzureStorage](https://www.nuget.org/packages/Microsoft.Orleans.DurableJobs.AzureStorage) | Azure Blob Storage for durable jobs. |
| `Microsoft.Orleans.Messaging` | Experimental grain-scoped inbox and outbox messaging contracts, awaiting their first NuGet publication. |

Use reminders for recurring durable callbacks and durable jobs for scheduled one-time work. Grain timers are activation-scoped and use the core runtime rather than a provider package.

Expand Down
4 changes: 3 additions & 1 deletion docs/site/src/data/unpublished-api-packages.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"description": "Generated API assemblies which are not currently published as standalone NuGet packages.",
"packages": {}
"packages": {
"Microsoft.Orleans.Messaging": "The new package is awaiting its first NuGet publication."
}
}
1 change: 1 addition & 0 deletions src/Orleans.Core/Orleans.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<InternalsVisibleTo Include="Orleans.Persistence.Firestore.Tests" />
<InternalsVisibleTo Include="Orleans.GrainDirectory.Tests" />
<InternalsVisibleTo Include="Orleans.Journaling" />
<InternalsVisibleTo Include="Orleans.Messaging" />
<InternalsVisibleTo Include="Orleans.NativeAotSmoke" />
<InternalsVisibleTo Include="Orleans.Placement.Tests" />
<InternalsVisibleTo Include="Orleans.Redis.Tests" />
Expand Down
2 changes: 1 addition & 1 deletion src/Orleans.Journaling/DurableDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface IDurableDictionary<TKey, TValue> : IDictionary<TKey, TValue> wh

[DebuggerTypeProxy(typeof(IDurableDictionaryDebugView<,>))]
[DebuggerDisplay("Count = {Count}")]
internal class DurableDictionary<TKey, TValue> : IDurableDictionary<TKey, TValue>, IStateMachine, IDurableDictionaryCommandHandler<TKey, TValue> where TKey : notnull
internal class DurableDictionary<TKey, TValue> : IDurableDictionary<TKey, TValue>, IStateMachine, IReportsStagedChanges, IDurableDictionaryCommandHandler<TKey, TValue> where TKey : notnull
{
private readonly IDurableDictionaryCommandCodec<TKey, TValue> _codec;
private readonly Dictionary<TKey, TValue> _items = [];
Expand Down
2 changes: 1 addition & 1 deletion src/Orleans.Journaling/DurableList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface IDurableList<T> : IList<T>

[DebuggerTypeProxy(typeof(IDurableCollectionDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
internal sealed class DurableList<T> : IDurableList<T>, IStateMachine, IDurableListCommandHandler<T>
internal sealed class DurableList<T> : IDurableList<T>, IStateMachine, IReportsStagedChanges, IDurableListCommandHandler<T>
{
private readonly IDurableListCommandCodec<T> _codec;
private readonly List<T> _items = [];
Expand Down
2 changes: 1 addition & 1 deletion src/Orleans.Journaling/DurableQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public interface IDurableQueue<T> : IEnumerable<T>, IReadOnlyCollection<T>

[DebuggerTypeProxy(typeof(DurableQueueDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
internal sealed class DurableQueue<T> : IDurableQueue<T>, IStateMachine, IDurableQueueCommandHandler<T>
internal sealed class DurableQueue<T> : IDurableQueue<T>, IStateMachine, IReportsStagedChanges, IDurableQueueCommandHandler<T>
{
private readonly IDurableQueueCommandCodec<T> _codec;
private readonly Queue<T> _items = new();
Expand Down
2 changes: 1 addition & 1 deletion src/Orleans.Journaling/DurableSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public interface IDurableSet<T> : ISet<T>, IReadOnlyCollection<T>, IReadOnlySet<

[DebuggerTypeProxy(typeof(IDurableCollectionDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
internal sealed class DurableSet<T> : IDurableSet<T>, IStateMachine, IDurableSetCommandHandler<T>
internal sealed class DurableSet<T> : IDurableSet<T>, IStateMachine, IReportsStagedChanges, IDurableSetCommandHandler<T>
{
private readonly IDurableSetCommandCodec<T> _codec;
private readonly HashSet<T> _items = [];
Expand Down
9 changes: 8 additions & 1 deletion src/Orleans.Journaling/DurableTaskCompletionSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public interface IDurableTaskCompletionSource<T>
}

[DebuggerDisplay("Status = {Status}")]
internal sealed class DurableTaskCompletionSource<T> : IDurableTaskCompletionSource<T>, IStateMachine, IDurableTaskCompletionSourceCommandHandler<T>
internal sealed class DurableTaskCompletionSource<T> : IDurableTaskCompletionSource<T>, IStateMachine, IReportsStagedChanges, IDurableTaskCompletionSourceCommandHandler<T>
{
private readonly IDurableTaskCompletionSourceCommandCodec<T> _codec;
private readonly DeepCopier<T> _copier;
Expand All @@ -52,6 +52,7 @@ internal sealed class DurableTaskCompletionSource<T> : IDurableTaskCompletionSou
private DurableTaskCompletionSourceStatus _status;
private T? _value;
private Exception? _exception;
private JournalStreamWriter _writer;

public DurableTaskCompletionSource(
[ServiceKey] string key,
Expand Down Expand Up @@ -90,6 +91,7 @@ public bool TrySetResult(T value)
}

_status = DurableTaskCompletionSourceStatus.Completed;
_writer.MarkModified();
_value = _copier.Copy(value);
return true;
}
Expand All @@ -102,6 +104,7 @@ public bool TrySetException(Exception exception)
}

_status = DurableTaskCompletionSourceStatus.Faulted;
_writer.MarkModified();
_exception = _exceptionCopier.Copy(exception);
return true;
}
Expand All @@ -114,6 +117,7 @@ public bool TrySetCanceled()
}

_status = DurableTaskCompletionSourceStatus.Canceled;
_writer.MarkModified();
return true;
}

Expand Down Expand Up @@ -154,6 +158,7 @@ private void OnValuePersisted()

void IStateMachine.Reset(JournalStreamWriter writer)
{
_writer = writer;
_status = DurableTaskCompletionSourceStatus.Pending;
_value = default;
_exception = null;
Expand Down Expand Up @@ -198,12 +203,14 @@ private void WriteState(JournalStreamWriter writer)
void IDurableTaskCompletionSourceCommandHandler<T>.ApplyCompleted(T value)
{
_status = DurableTaskCompletionSourceStatus.Completed;
_writer.MarkModified();
_value = value;
}

void IDurableTaskCompletionSourceCommandHandler<T>.ApplyFaulted(Exception exception)
{
_status = DurableTaskCompletionSourceStatus.Faulted;
_writer.MarkModified();
_exception = exception;
}

Expand Down
10 changes: 8 additions & 2 deletions src/Orleans.Journaling/DurableValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ public interface IDurableValue<T>
}

[DebuggerDisplay("{Value}")]
internal sealed class DurableValue<T> : IDurableValue<T>, IStateMachine, IDurableValueCommandHandler<T>
internal sealed class DurableValue<T> : IDurableValue<T>, IStateMachine, IReportsStagedChanges, IDurableValueCommandHandler<T>
{
private readonly IDurableValueCommandCodec<T> _codec;
private T? _value;
private bool _isDirty;
private JournalStreamWriter _writer;

public DurableValue(
[ServiceKey] string key,
Expand Down Expand Up @@ -54,7 +55,11 @@ public T? Value

private void OnValuePersisted() => OnPersisted?.Invoke();

public void OnModified() => _isDirty = true;
public void OnModified()
{
_isDirty = true;
_writer.MarkModified();
}

void IStateMachine.ReplayEntry(JournalEntry entry, JournalReplayContext context) =>
context.GetRequiredCommandCodec(entry.FormatKey, _codec).Apply(entry.Reader, this);
Expand All @@ -64,6 +69,7 @@ void IStateMachine.ReplayEntry(JournalEntry entry, JournalReplayContext context)

void IStateMachine.Reset(JournalStreamWriter writer)
{
_writer = writer;
_value = default;
_isDirty = false;
}
Expand Down
4 changes: 4 additions & 0 deletions src/Orleans.Journaling/IJournaledStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public interface IJournaledStateManager : IAsyncDisposable
/// Stage mutations only after establishing that they are safe to commit. Pending changes are shared
/// by all callers using this manager. Storage acknowledgement establishes durability.
/// A failed journal operation fences the manager; recovery requires a new manager and state machine instances.
/// Before capture, the manager prepares each registered state through <see cref="IStateMachine.PrepareWriteAsync"/>.
/// A preparation failure reports <see cref="JournaledStatePreCommitException"/> and retains pending changes
/// for an explicit retry.
/// Cancellation stops the caller's wait; an already queued write continues to its storage outcome.
/// </remarks>
/// <param name="cancellationToken">The token used to cancel the caller's wait.</param>
Expand Down Expand Up @@ -81,4 +84,5 @@ public interface IJournaledStateManager : IAsyncDisposable
/// concurrent writers and should not be used for correctness decisions.
/// </remarks>
long PendingWriteByteCount => -1;

}
12 changes: 12 additions & 0 deletions src/Orleans.Journaling/IReportsStagedChanges.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Orleans.Journaling;

/// <summary>
/// Marks a state whose every change advances <see cref="JournaledStateManager.StagedChangeCount"/>: the state commits a
/// journal entry when the change is made, or calls <see cref="JournalStreamWriter.MarkModified"/>.
/// </summary>
/// <remarks>
/// A state which journals changes it can't observe, such as an object mutated in place and serialized whole when a write
/// captures it, doesn't implement this interface. While one is registered, the manager can't tell whether a state staged
/// a change, so <see cref="JournaledStateManager.StagedChangeCount"/> is negative.
/// </remarks>
internal interface IReportsStagedChanges;
20 changes: 20 additions & 0 deletions src/Orleans.Journaling/IStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Orleans.Journaling;
/// </item>
/// <item>
/// When the application requests a write, the journaled state manager calls
/// <see cref="PrepareWriteAsync"/> to establish the state's persistence prerequisites, then
/// <see cref="WritePendingEntries"/> (and occasionally <see cref="WriteSnapshot"/>) to materialize
/// the pending changes, then flushes the journal to durable storage.
/// </item>
Expand Down Expand Up @@ -68,6 +69,25 @@ public interface IStateMachine
/// </remarks>
void OnRecoveryCompleted() { }

/// <summary>
/// Establishes the persistence prerequisites for this state's pending changes.
/// </summary>
/// <param name="cancellationToken">The token which cancels preparation when the journal owner stops.</param>
/// <returns>A completion representing readiness to capture this state.</returns>
/// <remarks>
/// The manager prepares every registered state before capturing each append or snapshot, whether or not the state
/// has pending changes, so a state can establish prerequisites for changes which other states hold, or fail a write
/// whose pending changes must not commit. Preparation is idempotent. After asynchronous
/// preparation completes, the manager repeats preparation for all states to include changes
/// staged on interleaved turns. Prepared prerequisites complete synchronously on subsequent calls
/// and remain valid through capture. A complete synchronous pass flows directly into capture.
/// Preparation completes without awaiting another operation on its own journal manager,
/// since that operation is serialized behind the current write.
/// A failure reports <see cref="JournaledStatePreCommitException"/> and retains pending changes
/// for an explicit write retry.
/// </remarks>
ValueTask PrepareWriteAsync(CancellationToken cancellationToken = default) => ValueTask.CompletedTask;

/// <summary>
/// Writes pending state changes to the journal.
/// </summary>
Expand Down
24 changes: 24 additions & 0 deletions src/Orleans.Journaling/JournalBufferWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public abstract class JournalBufferWriter : IDisposable, IBufferWriter<byte>
#endif
private readonly ArcBufferWriter _buffer = new();
private int _committedLength;
private long _stagedChangeCount;
private bool _hasActiveEntry;
private bool _disposed;

Expand Down Expand Up @@ -269,6 +270,28 @@ internal void AppendPreservedEntry(JournalStreamId streamId, IPreservedJournalEn
}
}

/// <summary>
/// Gets or sets a count which advances whenever a state stages a change and never decreases between writes.
/// </summary>
/// <remarks>The manager restores it after a write captures pending changes, which stages nothing new.</remarks>
internal long StagedChangeCount
{
get
{
lock (_lock) return _stagedChangeCount;
}
set
{
lock (_lock) _stagedChangeCount = value;
}
}

/// <summary>Records a change which a state stages in memory and journals when a write captures it.</summary>
internal void MarkModified()
{
lock (_lock) _stagedChangeCount++;
}

internal void CommitActiveEntry(JournalStreamId streamId)
{
lock (_lock)
Expand All @@ -279,6 +302,7 @@ internal void CommitActiveEntry(JournalStreamId streamId)
{
FinishEntry(streamId);
_committedLength = _buffer.Length;
_stagedChangeCount++;
}
catch
{
Expand Down
3 changes: 3 additions & 0 deletions src/Orleans.Journaling/JournalStreamWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal JournalStreamWriter(JournalStreamId id, JournalBufferWriter writer)

internal bool IsInitialized => _writer is not null;

/// <summary>Records a change which the state journals when a write captures it.</summary>
internal void MarkModified() => _writer?.MarkModified();

/// <summary>
/// Appends a format-owned entry for retired or unknown state preservation.
/// </summary>
Expand Down
Loading
Loading