Skip to content

Fix Cluster Sharding private replicator recovery - #8480

Merged
Aaronontheweb merged 7 commits into
akkadotnet:v1.5from
Aaronontheweb:fix/8471-sharding-replicator-recovery
Aug 26, 2026
Merged

Fix Cluster Sharding private replicator recovery#8480
Aaronontheweb merged 7 commits into
akkadotnet:v1.5from
Aaronontheweb:fix/8471-sharding-replicator-recovery

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

  • Watch each role-scoped Cluster Sharding DData replicator and recreate it in place after unexpected termination.
  • Give sharding DData consumers an ICanTell actor selection for the replicator's stable path, so existing coordinators and remember-entities stores automatically address the replacement actor.
  • Preserve persistence-mode behavior, including configurations that specify DData as the remember-entities store.
  • Add regression coverage for DData recovery, persistence-mode compatibility, and coordinated shutdown in both state-store modes.

Fixes #8471

Compatibility and scope

  • DData and custom state-store modes recreate the private replicator.
  • The replicator remains a direct child at the existing /system/sharding/<role>Replicator path, preserving compatibility with older cluster members.
  • Persistence state-store mode does not create a private replicator. Its remember-entities provider remains event sourced even if remember-entities-store = ddata is configured.
  • The lifecycle message is local-only and implements INoSerializationVerificationNeeded.
  • Replicators are not recreated during cluster shutdown; shutdown detection uses the coordinated-shutdown reason instead of depending on a late cluster event.
  • This PR does not modify Akka.DistributedData.

Validation

  • Akka.Cluster.Sharding.Tests net10.0 build: 0 warnings and 0 errors.
  • Focused recovery, persistence compatibility, and parameterized coordinated-shutdown tests: 4 passed.
  • The shutdown test covers both persistence and DData across the existing three-node leaving/downing flow and asserts that no private-replicator termination error is logged.
  • Slopwatch and whitespace validation passed.

Watch role-scoped sharding DData replicators and recreate them at their stable actor paths when they terminate. Use actor selections so existing coordinators and remember-entities stores continue communicating with the replacement incarnation.

Add regression coverage for DData remember entities and the persistence compatibility configuration.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs some work


public class ClusterShardingReplicatorResiliencySpec : AkkaSpec
{
private sealed record EntityEnvelope(string EntityId);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use ShardEnvelope and the HashCodeMessageExtractor.Create here, no?

}

private static Config SpecConfig =>
ConfigurationFactory.ParseString(@"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - this is the config we want to test

}

[Fact]
public async Task Private_replicator_should_recover_at_the_same_path_without_restarting_consumers()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backwards compat test - make sure that the ActorSelection paths have not diverged between deployments, otherwise the deployment + shard rebalancing it triggers is going to be unable to reconcile until the new version is completely cycled in.

shardWatcher.Watch(await Sys.ActorSelection(shard).ResolveOne(3.Seconds()));

var firstReplicator = await Sys.ActorSelection(replicatorPath).ResolveOne(3.Seconds());
Watch(firstReplicator);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

``WatchAsync`

var coordinatorWatcher = CreateTestProbe();
coordinatorWatcher.Watch(coordinator);
var shardWatcher = CreateTestProbe();
shardWatcher.Watch(await Sys.ActorSelection(shard).ResolveOne(3.Seconds()));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WatchAsync for all of these calls

_replicatorSettingsByRole = _replicatorSettingsByRole.SetItem(role, replicatorSettings);
}

return Context.ActorSelection(Self.Path / name);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sucks - we should just stick with the IActorRef and return that rather than an ICanTell + ActorSelection


private IActorRef CreateReplicator(string role, ReplicatorSettings settings)
{
var replicator = Context.Watch(Context.ActorOf(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use Context.WatchWith and a custom message type so we know exactly which replicator + role died when we receive the termination message? That would avoid some of the messy look-ups inside TryHandleReplicatorTermination

private bool TryHandleReplicatorTermination(IActorRef terminated)
{
string role = null;
foreach (var (candidateRole, replicator) in _replicatorsByRole)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see previous comments about using Context.WatchWith to avoid the lookups here

if (_clusterShuttingDown || _cluster.IsTerminated)
return true;

_log.Warning(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log an error, not a warning.


Receive<Terminated>(msg =>
{
if (TryHandleReplicatorTermination(msg.ActorRef))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we might need here is a flag for when we're shutting down ourselves, so we don't log a bunch of errors when the shard region is being terminated as part of a normal node exit.

Keep IActorRef throughout the sharding DData consumers and publish a local replacement message when the guardian recreates a terminated replicator. Existing coordinators and remember-entities stores swap to the new ref and retry their active operation, while providers use the replacement for newly-created stores.

Use WatchWith role-aware termination messages, preserve the peer-visible actor path, suppress shutdown recreation, and simplify the regression test fixtures.
@Aaronontheweb

Copy link
Copy Markdown
Member Author

Addressed the review in 1ca3718:

  • restored IActorRef throughout the DData consumer APIs
  • switched replicator death-watch to WatchWith using a role-aware local message
  • recreated at the unchanged /system/sharding/Replicator path
  • published a local ReplicatorChanged message so existing consumers swap refs and retry active operations
  • updated remember-entities providers so newly-created stores receive the replacement ref
  • marked both internal lifecycle messages INoSerializationVerificationNeeded
  • changed unexpected termination logging to Error while retaining shutdown suppression
  • simplified the regression fixture with ShardEnvelope, HashCodeMessageExtractor, and WatchAsync

The focused recovery tests passed four consecutive rebuilt runs. The full sharding suite was 193/194 with only the unchanged RememberEntitiesStarterSpec timing failure noted in the PR description.

@Aaronontheweb Aaronontheweb added this to the 1.5.71 milestone Aug 26, 2026

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - I refined this and tried some alternative approaches with the BackoffSupervisor but I think this is probably the best option / least disruptive for Akka.Cluster.Sharding.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

public ClusterShardingSettings Settings { get; }
public int MajorityMinCap { get; }
public IActorRef Replicator { get; }
public ICanTell Replicator { get; }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving to ICanTell allows us to communicate with the replicator based on its ActorPath via ActorSelection, so in the event that it dies / gets replaced this will continue to work - unlike an IActorRef which will get invalidated.

I would've gone with the IActorRef-based approach but it requires A LOT more accounting and moving parts, so I settled for this.

Aaronontheweb added a commit that referenced this pull request Aug 27, 2026
* Fix Cluster Sharding private replicator recovery

Watch role-scoped sharding DData replicators and recreate them at their stable actor paths when they terminate. Use actor selections so existing coordinators and remember-entities stores continue communicating with the replacement incarnation.

Add regression coverage for DData remember entities and the persistence compatibility configuration.

* Address sharding replicator recovery review

Keep IActorRef throughout the sharding DData consumers and publish a local replacement message when the guardian recreates a terminated replicator. Existing coordinators and remember-entities stores swap to the new ref and retry their active operation, while providers use the replacement for newly-created stores.

Use WatchWith role-aware termination messages, preserve the peer-visible actor path, suppress shutdown recreation, and simplify the regression test fixtures.

* Simplify remember entities provider tracking

* Simplify sharding replicator recovery

* Cover sharding replicator coordinated shutdown
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant