Skip to content

Commit 468546c

Browse files
chrisjhoareChris HoareAaronontheweb
authored
Add support for optional snapshots (akkadotnet#7444)
* Add support for optional snapshots * Fix markdown title formatting * Fix markdown consecutive blank lines --------- Co-authored-by: Chris Hoare <[email protected]> Co-authored-by: Aaron Stannard <[email protected]>
1 parent 26383ec commit 468546c

File tree

5 files changed

+90
-6
lines changed

5 files changed

+90
-6
lines changed

docs/articles/persistence/snapshots.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ persistent actors should use the `deleteSnapshots` method. Depending on the jour
5151
best practice to do specific deletes with `deleteSnapshot` or to include a `minSequenceNr` as well as a `maxSequenceNr`
5252
for the `SnapshotSelectionCriteria`.
5353

54+
## Optional Snapshots
55+
56+
By default, the persistent actor will unconditionally be stopped if the snapshot can't be loaded in the recovery.
57+
It is possible to make snapshot loading optional. This can be useful when it is alright to ignore snapshot in case
58+
of for example deserialization errors. When snapshot loading fails it will instead recover by replaying all events.
59+
60+
Enable this feature by setting `snapshot-is-optional = true` in the snapshot store configuration.
61+
62+
> [!WARNING]
63+
>Don't set `snapshot-is-optional = true` if events have been deleted because that would result in wrong recovered state if snapshot load fails.
64+
5465
## Snapshot Status Handling
5566

5667
Saving or deleting snapshots can either succeed or fail – this information is reported back to the persistent actor via status messages as illustrated in the following table.

src/core/Akka.Persistence.Tests/SnapshotFailureRobustnessSpec.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,41 @@ public void PersistentActor_with_a_failing_snapshot_should_receive_failure_messa
292292
m.Cause.Message.Contains("Failed to delete"));
293293
}
294294
}
295+
296+
public class SnapshotIsOptionalSpec : PersistenceSpec
297+
{
298+
public SnapshotIsOptionalSpec() : base(Configuration("SnapshotIsOptionalSpec", serialization: "off",
299+
extraConfig: @"
300+
akka.persistence.snapshot-store.local.snapshot-is-optional = true
301+
akka.persistence.snapshot-store.local.class = ""Akka.Persistence.Tests.SnapshotFailureRobustnessSpec+FailingLocalSnapshotStore, Akka.Persistence.Tests""
302+
"))
303+
{
304+
}
305+
306+
[Fact]
307+
public void PersistentActor_with_a_failing_snapshot_with_snapshot_is_optional_true_falls_back_to_events()
308+
{
309+
var spref = Sys.ActorOf(Props.Create(() => new SnapshotFailureRobustnessSpec.SaveSnapshotTestActor(Name, TestActor)));
310+
311+
ExpectMsg<RecoveryCompleted>();
312+
spref.Tell(new SnapshotFailureRobustnessSpec.Cmd("boom"));
313+
ExpectMsg(1L);
314+
315+
Sys.EventStream.Subscribe(TestActor, typeof(Error));
316+
try
317+
{
318+
319+
var lpref = Sys.ActorOf(Props.Create(() => new SnapshotFailureRobustnessSpec.LoadSnapshotTestActor(Name, TestActor)));
320+
ExpectMsg<Error>(m => m.Message.ToString().StartsWith("Error loading snapshot"));
321+
ExpectMsg("boom-1");
322+
ExpectMsg<RecoveryCompleted>();
323+
324+
}
325+
finally
326+
{
327+
Sys.EventStream.Unsubscribe(TestActor, typeof(Error));
328+
}
329+
330+
}
331+
}
295332
}

src/core/Akka.Persistence/Eventsourced.Recovery.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
using System;
99
using Akka.Actor;
10+
using Akka.Event;
1011
using Akka.Persistence.Internal;
1112

1213
namespace Akka.Persistence
@@ -61,7 +62,9 @@ private EventsourcedState RecoveryStarted(long maxReplays)
6162
// protect against snapshot stalling forever because journal overloaded and such
6263
var timeout = Extension.JournalConfigFor(JournalPluginId).GetTimeSpan("recovery-event-timeout", null, false);
6364
var timeoutCancelable = Context.System.Scheduler.ScheduleTellOnceCancelable(timeout, Self, new RecoveryTick(true), Self);
64-
65+
66+
var snapshotIsOptional = Extension.SnapshotStoreConfigFor(SnapshotPluginId).GetBoolean("snapshot-is-optional", false);
67+
6568
bool RecoveryBehavior(object message)
6669
{
6770
Receive receiveRecover = ReceiveRecover;
@@ -120,15 +123,24 @@ bool RecoveryBehavior(object message)
120123
}
121124
case LoadSnapshotFailed failed:
122125
timeoutCancelable.Cancel();
123-
try
126+
if (snapshotIsOptional)
124127
{
125-
OnRecoveryFailure(failed.Cause);
128+
Log.Info("Snapshot load error for persistenceId [{0}]. Replaying all events since snapshot-is-optional=true", PersistenceId);
129+
ChangeState(Recovering(RecoveryBehavior, timeout));
130+
Journal.Tell(new ReplayMessages(LastSequenceNr +1L, long.MaxValue, maxReplays, PersistenceId, Self));
126131
}
127-
finally
132+
else
128133
{
129-
Context.Stop(Self);
134+
try
135+
{
136+
OnRecoveryFailure(failed.Cause);
137+
}
138+
finally
139+
{
140+
Context.Stop(Self);
141+
}
142+
ReturnRecoveryPermit();
130143
}
131-
ReturnRecoveryPermit();
132144
break;
133145
case RecoveryTick { Snapshot: true }:
134146
try

src/core/Akka.Persistence/Persistence.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,22 @@ internal Config JournalConfigFor(string journalPluginId)
198198
var configPath = string.IsNullOrEmpty(journalPluginId) ? _defaultJournalPluginId.Value : journalPluginId;
199199
return PluginHolderFor(configPath, JournalFallbackConfigPath).Config;
200200
}
201+
202+
/// <summary>
203+
/// Returns the plugin config identified by <paramref name="snapshotPluginId"/>.
204+
/// When empty, looks in `akka.persistence.snapshot-store.plugin` to find configuration entry path.
205+
/// When configured, uses <paramref name="snapshotPluginId"/> as absolute path to the journal configuration entry.
206+
/// </summary>
207+
/// <param name="snapshotPluginId">TBD</param>
208+
/// <exception cref="ArgumentException">
209+
/// This exception is thrown when either the plugin class name is undefined or the configuration path is missing.
210+
/// </exception>
211+
/// <returns>TBD</returns>
212+
internal Config SnapshotStoreConfigFor(string snapshotPluginId)
213+
{
214+
var configPath = string.IsNullOrEmpty(snapshotPluginId) ? _defaultSnapshotPluginId.Value : snapshotPluginId;
215+
return PluginHolderFor(configPath, SnapshotStoreFallbackConfigPath).Config;
216+
}
201217

202218
/// <summary>
203219
/// Looks up the plugin config by plugin's ActorRef.

src/core/Akka.Persistence/persistence.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@ akka.persistence {
181181
call-timeout = 20s
182182
reset-timeout = 60s
183183
}
184+
185+
# Set this to true if successful loading of snapshot is not necessary.
186+
# This can be useful when it is alright to ignore snapshot in case of
187+
# for example deserialization errors. When snapshot loading fails it will instead
188+
# recover by replaying all events.
189+
# Don't set to true if events are deleted because that would
190+
# result in wrong recovered state if snapshot load fails.
191+
snapshot-is-optional = false
184192
}
185193

186194
fsm {

0 commit comments

Comments
 (0)