diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonRestart2Spec.cs b/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonRestart2Spec.cs index 74c6c3032a5..c45e77912ae 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonRestart2Spec.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests/Singleton/ClusterSingletonRestart2Spec.cs @@ -31,7 +31,7 @@ public ClusterSingletonRestart2Spec(ITestOutputHelper output) : base(""" akka.loglevel = INFO akka.actor.provider = "cluster" akka.cluster.roles = [singleton] - #akka.cluster.auto-down-unreachable-after = 2s + akka.cluster.split-brain-resolver.stable-after = 2s akka.cluster.singleton.min-number-of-hand-over-retries = 5 akka.remote { dot-netty.tcp { @@ -120,7 +120,7 @@ await WithinAsync(TimeSpan.FromSeconds(5), () => await Join(_sys4, _sys3); // let it stabilize - //Task.Delay(TimeSpan.FromSeconds(5)).Wait(); + await Task.Delay(TimeSpan.FromSeconds(5)); await WithinAsync(TimeSpan.FromSeconds(10), () => { diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs index 8da493c3fb1..197f5cc429e 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingletonManager.cs @@ -4,9 +4,8 @@ // Copyright (C) 2013-2023 .NET Foundation // //----------------------------------------------------------------------- - +#nullable enable using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Runtime.Serialization; @@ -16,7 +15,6 @@ using Akka.Coordination; using Akka.Dispatch; using Akka.Event; -using Akka.Pattern; using Akka.Remote; using Akka.Util.Internal; using static Akka.Cluster.ClusterEvent; @@ -74,7 +72,6 @@ private HandOverDone() { } } /// - /// TBD /// Sent from from previous oldest to new oldest to /// initiate the normal hand-over process. /// Especially useful when new node joins and becomes @@ -84,59 +81,36 @@ private HandOverDone() { } [Serializable] internal sealed class TakeOverFromMe : IClusterSingletonMessage, IDeadLetterSuppression { - /// - /// TBD - /// public static TakeOverFromMe Instance { get; } = new(); private TakeOverFromMe() { } } /// - /// TBD + /// Scheduled task to cleanup overdue members that have been removed /// [Serializable] internal sealed class Cleanup { - /// - /// TBD - /// public static Cleanup Instance { get; } = new(); private Cleanup() { } } /// - /// TBD + /// Initialize the oldest changed buffer actor. /// [Serializable] internal sealed class StartOldestChangedBuffer { - /// - /// TBD - /// public static StartOldestChangedBuffer Instance { get; } = new(); private StartOldestChangedBuffer() { } } /// - /// TBD + /// Retry a failed cluster singleton handover. /// + /// The number of retries [Serializable] - internal sealed class HandOverRetry - { - /// - /// TBD - /// - public int Count { get; } - - /// - /// TBD - /// - /// TBD - public HandOverRetry(int count) - { - Count = count; - } - } + internal sealed record HandOverRetry(int Count); /// /// TBD @@ -199,82 +173,82 @@ internal sealed class YoungerData : IClusterSingletonData /// /// TBD /// - public List Oldest { get; } + public ImmutableList Oldest { get; } /// /// TBD /// /// TBD - public YoungerData(List oldest) + public YoungerData(ImmutableList oldest) { Oldest = oldest; } } /// - /// TBD + /// State when we're transitioning to becoming the oldest singleton manager. /// [Serializable] internal sealed class BecomingOldestData : IClusterSingletonData { /// - /// TBD + /// The previous oldest nodes - can be empty /// - public List PreviousOldest { get; } - - /// - /// TBD - /// - /// TBD - public BecomingOldestData(List previousOldest) + public ImmutableList PreviousOldest { get; } + + public BecomingOldestData(ImmutableList previousOldest) { PreviousOldest = previousOldest; } } /// - /// TBD + /// State for after we've successfully transitioned to oldest, so we're hosting + /// the singleton actor. /// [Serializable] internal sealed class OldestData : IClusterSingletonData { /// - /// TBD + /// The reference to the current singleton running on this node. /// - public IActorRef Singleton { get; } - - /// - /// TBD - /// - /// TBD - public OldestData(IActorRef singleton) + /// + /// Cam be explicitly set to null when we are leaving the cluster + /// and the singleton has to be terminated. + /// + public IActorRef? Singleton { get; } + + public OldestData(IActorRef? singleton) { Singleton = singleton; } } /// - /// TBD + /// State we're transitioning into once we know we've started the hand-over process. /// [Serializable] internal sealed class WasOldestData : IClusterSingletonData { /// - /// TBD + /// The reference to the singleton. /// - public IActorRef Singleton { get; } + /// + /// Can be null in edge cases where the node became the oldest but was already shutting down. + /// Shouldn't happen very often, but it's not impossible. + /// + public IActorRef? Singleton { get; } /// - /// TBD + /// The address of the new oldest node. /// - public UniqueAddress NewOldest { get; } - - /// - /// TBD - /// - /// TBD - /// TBD - public WasOldestData(IActorRef singleton, UniqueAddress newOldest) + /// + /// Can be null if we don't know who the new oldest is - for instance, during a full cluster + /// shutdown (in which case, there won't be any hand-over.) + /// + public UniqueAddress? NewOldest { get; } + + public WasOldestData(IActorRef? singleton, UniqueAddress? newOldest) { Singleton = singleton; NewOldest = newOldest; @@ -282,28 +256,26 @@ public WasOldestData(IActorRef singleton, UniqueAddress newOldest) } /// - /// TBD + /// State when we're handing over control of the singleton to another node. /// [Serializable] internal sealed class HandingOverData : IClusterSingletonData { /// - /// TBD + /// The current singleton reference /// public IActorRef Singleton { get; } /// - /// TBD + /// The actor we're handing over to. /// - public IActorRef HandOverTo { get; } - - /// - /// TBD - /// - /// TBD - /// TBD - public HandingOverData(IActorRef singleton, IActorRef handOverTo) + /// + /// Can be null if they haven't contacted us yet and some other edge conditions. + /// + public IActorRef? HandOverTo { get; } + + public HandingOverData(IActorRef singleton, IActorRef? handOverTo) { Singleton = singleton; HandOverTo = handOverTo; @@ -311,7 +283,7 @@ public HandingOverData(IActorRef singleton, IActorRef handOverTo) } /// - /// TBD + /// For when we are transitioning to a stopping state. /// [Serializable] internal sealed class StoppingData : IClusterSingletonData @@ -345,27 +317,22 @@ private EndData() { } } /// - /// TBD + /// When we are moving into the "acquiring lease" state /// [Serializable] internal sealed class AcquiringLeaseData : IClusterSingletonData { /// - /// TBD + /// Is there already a lease request in-progress? /// public bool LeaseRequestInProgress { get; } /// - /// TBD + /// A reference to the current singleton, if it exists. /// - public IActorRef Singleton { get; } - - /// - /// TBD - /// - /// TBD - /// TBD - public AcquiringLeaseData(bool leaseRequestInProgress, IActorRef singleton) + public IActorRef? Singleton { get; } + + public AcquiringLeaseData(bool leaseRequestInProgress, IActorRef? singleton) { LeaseRequestInProgress = leaseRequestInProgress; Singleton = singleton; @@ -398,9 +365,9 @@ public ReleaseLeaseResult(bool released) [Serializable] internal sealed class AcquireLeaseFailure : IDeadLetterSuppression, INoSerializationVerificationNeeded { - public Exception Failure { get; } + public Exception? Failure { get; } - public AcquireLeaseFailure(Exception failure) + public AcquireLeaseFailure(Exception? failure) { Failure = failure; } @@ -429,20 +396,18 @@ public LeaseLost(Exception reason) } /// - /// TBD + /// We delay notifications in order to tolerate + /// downed nodes removed by the SBR, as the singleton may still be running there + /// until the node shuts itself down. /// [Serializable] internal sealed class DelayedMemberRemoved { /// - /// TBD + /// The removed member. /// public Member Member { get; } - - /// - /// TBD - /// - /// TBD + public DelayedMemberRemoved(Member member) { Member = member; @@ -457,45 +422,34 @@ public DelayedMemberRemoved(Member member) internal sealed class SelfExiting { private SelfExiting() { } - - /// - /// Singleton instance - /// + public static SelfExiting Instance { get; } = new(); } /// - /// TBD + /// The current FSM state of the cluster singleton manager. /// [Serializable] public enum ClusterSingletonState { - /// - /// TBD - /// Start, - /// - /// TBD - /// AcquiringLease, /// - /// TBD + /// Oldest is the state where we run the singleton. /// Oldest, - /// - /// TBD - /// Younger, /// - /// TBD + /// In the BecomingOldest state we start the hand-off process + /// with the WasOldest node, which is exiting the cluster. /// BecomingOldest, /// - /// TBD + /// We were the oldest node, but now we're exiting the cluster. /// WasOldest, /// - /// TBD + /// We are c /// HandingOver, /// @@ -635,9 +589,9 @@ public static Props Props(Props singletonProps, object terminationMessage, Clust private readonly CoordinatedShutdown _coordShutdown = CoordinatedShutdown.Get(Context.System); private readonly TaskCompletionSource _memberExitingProgress = new(); - private readonly string singletonLeaseName; - private readonly Lease lease; - private readonly TimeSpan leaseRetryInterval = TimeSpan.FromSeconds(5); // won't be used + private readonly string _singletonLeaseName; + private readonly Lease? _lease; + private readonly TimeSpan _leaseRetryInterval = TimeSpan.FromSeconds(5); // won't be used /// /// TBD @@ -657,13 +611,13 @@ public ClusterSingletonManager(Props singletonProps, object terminationMessage, _singletonProps = singletonProps; _terminationMessage = terminationMessage; _settings = settings; - singletonLeaseName = $"{Context.System.Name}-singleton-{Self.Path}"; + _singletonLeaseName = $"{Context.System.Name}-singleton-{Self.Path}"; if (settings.LeaseSettings != null) { - lease = LeaseProvider.Get(Context.System) - .GetLease(singletonLeaseName, settings.LeaseSettings.LeaseImplementation, _cluster.SelfAddress.HostPort()); - leaseRetryInterval = settings.LeaseSettings.LeaseRetryInterval; + _lease = LeaseProvider.Get(Context.System) + .GetLease(_singletonLeaseName, settings.LeaseSettings.LeaseImplementation, _cluster.SelfAddress.HostPort()); + _leaseRetryInterval = settings.LeaseSettings.LeaseRetryInterval; } _removalMargin = (settings.RemovalMargin <= TimeSpan.Zero) ? _cluster.DowningProvider.DownRemovalMargin : settings.RemovalMargin; @@ -769,7 +723,12 @@ private void GetNextOldestChanged() private State TryAcquireLease() { var self = Self; - lease.Acquire(reason => + + if (_lease == null) + throw new ArgumentNullException(nameof(_lease), + "Lease must be initialized before trying to acquire it"); + + _lease.Acquire(reason => { self.Tell(new LeaseLost(reason)); }).ContinueWith(r => @@ -777,7 +736,7 @@ private State TryAcquireLease() if (r.IsFaulted || r.IsCanceled) return (object)new AcquireLeaseFailure(r.Exception); return new AcquireLeaseResult(r.Result); - }).PipeTo(Self); + }).PipeTo(self); return GoTo(ClusterSingletonState.AcquiringLease).Using(new AcquiringLeaseData(true, null)); } @@ -786,13 +745,11 @@ private State TryAcquireLease() private State TryGotoOldest() { // check if lease - if (lease == null) + if (_lease == null) return GoToOldest(); - else - { - Log.Info("Trying to acquire lease before starting singleton"); - return TryAcquireLease(); - } + + Log.Info("Trying to acquire lease before starting singleton"); + return TryAcquireLease(); } private State GoToOldest() @@ -803,26 +760,26 @@ private State GoToOldest() GoTo(ClusterSingletonState.Oldest).Using(new OldestData(singleton)); } - private State HandleOldestChanged(IActorRef singleton, UniqueAddress oldest) + private State HandleOldestChanged(IActorRef? singleton, UniqueAddress? oldest) { _oldestChangedReceived = true; Log.Info("{0} observed OldestChanged: [{1} -> {2}]", StateName, _cluster.SelfAddress, oldest?.Address); switch (oldest) { - case UniqueAddress a when a.Equals(_cluster.SelfUniqueAddress): + case not null when oldest.Equals(_cluster.SelfUniqueAddress): // already oldest return Stay(); - case UniqueAddress a when !_selfExited && _removed.ContainsKey(a): + case not null when !_selfExited && _removed.ContainsKey(oldest): // The member removal was not completed and the old removed node is considered // oldest again. Safest is to terminate the singleton instance and goto Younger. // This node will become oldest again when the other is removed again. return GoToHandingOver(singleton, null); - case UniqueAddress a: + case not null: // send TakeOver request in case the new oldest doesn't know previous oldest - Peer(a.Address).Tell(TakeOverFromMe.Instance); + Peer(oldest.Address).Tell(TakeOverFromMe.Instance); SetTimer(TakeOverRetryTimer, new TakeOverRetry(1), _settings.HandOverRetryInterval, repeat: false); return GoTo(ClusterSingletonState.WasOldest) - .Using(new WasOldestData(singleton, a)); + .Using(new WasOldestData(singleton, oldest)); case null: // new oldest will initiate the hand-over SetTimer(TakeOverRetryTimer, new TakeOverRetry(1), _settings.HandOverRetryInterval, repeat: false); @@ -831,7 +788,7 @@ private State HandleOldestChanged( } } - private State HandleHandOverDone(IActorRef handOverTo) + private State HandleHandOverDone(IActorRef? handOverTo) { var newOldest = handOverTo?.Path.Address; Log.Info("Singleton terminated, hand-over done [{0} -> {1}]", _cluster.SelfAddress, newOldest); @@ -842,17 +799,16 @@ private State HandleHandOverDone(I Log.Info("Self removed, stopping ClusterSingletonManager"); return Stop(); } - else if (handOverTo == null) - { - return GoTo(ClusterSingletonState.Younger).Using(new YoungerData(null)); - } - else + + if (handOverTo == null) { - return GoTo(ClusterSingletonState.End).Using(EndData.Instance); + return GoTo(ClusterSingletonState.Younger).Using(new YoungerData(ImmutableList.Empty)); } + + return GoTo(ClusterSingletonState.End).Using(EndData.Instance); } - private State GoToHandingOver(IActorRef singleton, IActorRef handOverTo) + private State GoToHandingOver(IActorRef? singleton, IActorRef? handOverTo) { if (singleton == null) { @@ -911,24 +867,38 @@ private void InitializeFSM() case OldestChangedBuffer.OldestChanged oldestChanged when e.StateData is YoungerData youngerData: { _oldestChangedReceived = true; - if (oldestChanged.Oldest != null && oldestChanged.Oldest.Equals(_selfUniqueAddress)) + if (oldestChanged.NewOldest != null && oldestChanged.NewOldest.Equals(_selfUniqueAddress)) { Log.Info("Younger observed OldestChanged: [{0} -> myself]", youngerData.Oldest.Head()?.Address); - if (youngerData.Oldest.All(m => _removed.ContainsKey(m))) { return TryGotoOldest(); } - Peer(youngerData.Oldest.Head().Address).Tell(HandOverToMe.Instance); - return GoTo(ClusterSingletonState.BecomingOldest).Using(new BecomingOldestData(youngerData.Oldest)); + // explicitly re-order the list to make sure that the oldest, as indicated to us by the OldestChangedBuffer, + // is the first element - resolves bug https://github.com/akkadotnet/akka.net/issues/6973 + var newOldestState = oldestChanged.PreviousOldest switch + { + not null => ImmutableList.Empty.Add(oldestChanged.PreviousOldest) + .AddRange(youngerData.Oldest.Where(c => c != oldestChanged.PreviousOldest)), + _ => youngerData.Oldest + }; + + Peer(newOldestState.Head().Address).Tell(HandOverToMe.Instance); + return GoTo(ClusterSingletonState.BecomingOldest).Using(new BecomingOldestData(newOldestState)); } - Log.Info("Younger observed OldestChanged: [{0} -> {1}]", youngerData.Oldest.Head()?.Address, oldestChanged.Oldest?.Address); + Log.Info("Younger observed OldestChanged: [{0} -> {1}]", youngerData.Oldest.Head()?.Address, oldestChanged.NewOldest?.Address); GetNextOldestChanged(); - if (oldestChanged.Oldest != null && !youngerData.Oldest.Contains(oldestChanged.Oldest)) - youngerData.Oldest.Insert(0, oldestChanged.Oldest); - return Stay().Using(new YoungerData(youngerData.Oldest)); + + var newOldest = oldestChanged.NewOldest switch + { + not null when !youngerData.Oldest.Contains(oldestChanged.NewOldest) => ImmutableList< + UniqueAddress>.Empty.Add(oldestChanged.NewOldest).AddRange(youngerData.Oldest), + _ => youngerData.Oldest + }; + + return Stay().Using(new YoungerData(newOldest)); } case MemberDowned memberDowned when memberDowned.Member.UniqueAddress.Equals(_cluster.SelfUniqueAddress): Log.Info("Self downed, stopping ClusterSingletonManager"); @@ -1052,13 +1022,10 @@ private void InitializeFSM() return Stay(); case null: Sender.Tell(HandOverToMe.Instance); - becomingOldestData.PreviousOldest.Insert(0, senderUniqueAddress); - return Stay().Using(new BecomingOldestData(becomingOldestData.PreviousOldest)); + return Stay().Using(new BecomingOldestData(ImmutableList.Empty.Add(senderUniqueAddress).AddRange(becomingOldestData.PreviousOldest))); } } } - - break; } case HandOverRetry handOverRetry when e.StateData is BecomingOldestData becomingOldest: { @@ -1071,7 +1038,7 @@ private void InitializeFSM() return Stay(); } - if (becomingOldest.PreviousOldest != null && becomingOldest.PreviousOldest.All(m => _removed.ContainsKey(m))) + if (becomingOldest.PreviousOldest.Count > 0 && becomingOldest.PreviousOldest.All(m => _removed.ContainsKey(m))) { // can't send HandOverToMe, previousOldest unknown for new node (or restart) // previous oldest might be down or removed, so no TakeOverFromMe message is received @@ -1102,11 +1069,9 @@ private void InitializeFSM() { return GoToOldest(); } - else - { - SetTimer(LeaseRetryTimer, LeaseRetry.Instance, leaseRetryInterval, repeat: false); - return Stay().Using(new AcquiringLeaseData(false, null)); - } + + SetTimer(LeaseRetryTimer, LeaseRetry.Instance, _leaseRetryInterval, repeat: false); + return Stay().Using(new AcquiringLeaseData(false, null)); } case Terminated t when e.StateData is AcquiringLeaseData ald && t.ActorRef.Equals(ald.Singleton): Log.Info( @@ -1115,7 +1080,7 @@ private void InitializeFSM() return TryAcquireLease(); case AcquireLeaseFailure alf: Log.Error(alf.Failure, "Failed to get lease (will be retried)"); - SetTimer(LeaseRetryTimer, LeaseRetry.Instance, leaseRetryInterval, repeat: false); + SetTimer(LeaseRetryTimer, LeaseRetry.Instance, _leaseRetryInterval, repeat: false); return Stay().Using(new AcquiringLeaseData(false, null)); case LeaseRetry: // If lease was lost (so previous state was oldest) then we don't try and get the lease @@ -1123,7 +1088,7 @@ private void InitializeFSM() // instance in this case return TryAcquireLease(); case OldestChangedBuffer.OldestChanged oldestChanged when e.StateData is AcquiringLeaseData ald2: - return HandleOldestChanged(ald2.Singleton, oldestChanged.Oldest); + return HandleOldestChanged(ald2.Singleton, oldestChanged.NewOldest); case HandOverToMe when e.StateData is AcquiringLeaseData ald3: return GoToHandingOver(ald3.Singleton, Sender); case TakeOverFromMe: @@ -1148,7 +1113,7 @@ private void InitializeFSM() switch (e.FsmEvent) { case OldestChangedBuffer.OldestChanged oldestChanged when e.StateData is OldestData oldestData: - return HandleOldestChanged(oldestData.Singleton, oldestChanged.Oldest); + return HandleOldestChanged(oldestData.Singleton, oldestChanged.NewOldest); case HandOverToMe when e.StateData is OldestData oldest: return GoToHandingOver(oldest.Singleton, Sender); @@ -1257,11 +1222,9 @@ when e.StateData is WasOldestData oldestData Log.Info("Self downed, stopping ClusterSingletonManager"); return Stop(); } - else - { - Log.Info("Self downed, stopping"); - return GoToStopping(od.Singleton); - } + + Log.Info("Self downed, stopping"); + return GoToStopping(od.Singleton); } default: return null; @@ -1278,7 +1241,7 @@ when e.StateData is HandingOverData handingOverData return HandleHandOverDone(handingOverData.HandOverTo); case HandOverToMe when e.StateData is HandingOverData d - && d.HandOverTo.Equals(Sender): + && Sender.Equals(d.HandOverTo): // retry Sender.Tell(HandOverInProgress.Instance); return Stay(); @@ -1294,8 +1257,7 @@ when e.StateData is HandingOverData d When(ClusterSingletonState.Stopping, e => { - if (e.FsmEvent is Terminated terminated - && e.StateData is StoppingData stoppingData + if (e is { FsmEvent: Terminated terminated, StateData: StoppingData stoppingData } && terminated.ActorRef.Equals(stoppingData.Singleton)) { Log.Info("Singleton actor [{0}] was terminated", stoppingData.Singleton.Path); @@ -1399,9 +1361,9 @@ when removed.Member.UniqueAddress.Equals(_cluster.SelfUniqueAddress): if (StateData is AcquiringLeaseData ald && ald.LeaseRequestInProgress) { Log.Info("Releasing lease as leaving AcquiringLease going to [{0}]", to); - if (lease != null) + if (_lease != null) { - lease.Release().ContinueWith(r => + _lease.Release().ContinueWith(r => { if (r.IsCanceled || r.IsFaulted) return (object)new ReleaseLeaseFailure(r.Exception); @@ -1411,10 +1373,10 @@ when removed.Member.UniqueAddress.Equals(_cluster.SelfUniqueAddress): } } - if (from == ClusterSingletonState.Oldest && lease != null) + if (from == ClusterSingletonState.Oldest && _lease != null) { Log.Info("Releasing lease as leaving Oldest"); - lease.Release().ContinueWith(r => new ReleaseLeaseResult(r.Result)).PipeTo(Self); + _lease.Release().ContinueWith(r => new ReleaseLeaseResult(r.Result)).PipeTo(Self); } if (to is ClusterSingletonState.Younger or ClusterSingletonState.Oldest) GetNextOldestChanged(); @@ -1442,7 +1404,7 @@ private void ScheduleDelayedMemberRemoved(Member member) if (_removalMargin > TimeSpan.Zero) { Log.Debug("Schedule DelayedMemberRemoved for {0}", member.Address); - Context.System.Scheduler.ScheduleTellOnce(_removalMargin, Self, new DelayedMemberRemoved(member), Self); + SetTimer("delayed-member-removed-" + member.UniqueAddress, new DelayedMemberRemoved(member), _removalMargin, repeat: false); } else Self.Tell(new DelayedMemberRemoved(member)); } diff --git a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs index 7a787e9aa46..e76770589c5 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs +++ b/src/contrib/cluster/Akka.Cluster.Tools/Singleton/OldestChangedBuffer.cs @@ -4,14 +4,13 @@ // Copyright (C) 2013-2023 .NET Foundation // //----------------------------------------------------------------------- - +#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Akka.Actor; -using Akka.Util.Internal; namespace Akka.Cluster.Tools.Singleton { @@ -50,7 +49,7 @@ public sealed class InitialOldestState /// /// The first event, corresponding to CurrentClusterState. /// - public List Oldest { get; } + public ImmutableList Oldest { get; } /// /// TBD @@ -62,7 +61,7 @@ public sealed class InitialOldestState /// /// TBD /// TBD - public InitialOldestState(List oldest, bool safeToBeOldest) + public InitialOldestState(ImmutableList oldest, bool safeToBeOldest) { Oldest = oldest; SafeToBeOldest = safeToBeOldest; @@ -70,23 +69,28 @@ public InitialOldestState(List oldest, bool safeToBeOldest) } /// - /// TBD + /// Message propagated once the previous oldest member is exiting / downed / removed. /// [Serializable] public sealed class OldestChanged { /// - /// TBD + /// The new "oldest" - this node will become the new singleton manager. /// - public UniqueAddress Oldest { get; } + /// + /// Can be null if we're the last node in the cluster. + /// + public UniqueAddress? NewOldest { get; } /// - /// TBD + /// The previous oldest - will be `null` if this is the first oldest. /// - /// TBD - public OldestChanged(UniqueAddress oldest) + public UniqueAddress? PreviousOldest { get; } + + public OldestChanged(UniqueAddress? newOldest, UniqueAddress? previousOldest) { - Oldest = oldest; + NewOldest = newOldest; + PreviousOldest = previousOldest; } } @@ -147,7 +151,7 @@ private void TrackChanges(Action block) var after = _membersByAge.FirstOrDefault(); if (!Equals(before, after)) - _changes = _changes.Enqueue(new OldestChanged(after?.UniqueAddress)); + _changes = _changes.Enqueue(new OldestChanged(after?.UniqueAddress, before?.UniqueAddress)); } private bool MatchingRole(Member member) @@ -172,7 +176,7 @@ private void HandleInitial(ClusterEvent.CurrentClusterState state) var oldest = _membersByAge.TakeWhile(m => m.UpNumber <= selfUpNumber).ToList(); var safeToBeOldest = !oldest.Any(m => m.Status is MemberStatus.Down or MemberStatus.Exiting or MemberStatus.Leaving); - var initial = new InitialOldestState(oldest.Select(m => m.UniqueAddress).ToList(), safeToBeOldest); + var initial = new InitialOldestState(oldest.Select(m => m.UniqueAddress).ToImmutableList(), safeToBeOldest); _changes = _changes.Enqueue(initial); } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt index be072a9fe12..5b219595503 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.DotNet.verified.txt @@ -395,6 +395,9 @@ namespace Akka.Cluster.Tools.Singleton public static Akka.Cluster.Tools.Singleton.ClusterSingleton Get(Akka.Actor.ActorSystem system) { } public Akka.Actor.IActorRef Init(Akka.Cluster.Tools.Singleton.SingletonActor singleton) { } } + [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 0, + 1})] public sealed class ClusterSingletonManager : Akka.Actor.FSM { public ClusterSingletonManager(Akka.Actor.Props singletonProps, object terminationMessage, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings settings) { } @@ -404,6 +407,7 @@ namespace Akka.Cluster.Tools.Singleton public static Akka.Actor.Props Props(Akka.Actor.Props singletonProps, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings settings) { } public static Akka.Actor.Props Props(Akka.Actor.Props singletonProps, object terminationMessage, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings settings) { } } + [System.Runtime.CompilerServices.NullableAttribute(0)] public sealed class ClusterSingletonManagerIsStuckException : Akka.Actor.AkkaException { public ClusterSingletonManagerIsStuckException(string message) { } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt index 957381d1a55..8c151712151 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveClusterTools.Net.verified.txt @@ -395,6 +395,9 @@ namespace Akka.Cluster.Tools.Singleton public static Akka.Cluster.Tools.Singleton.ClusterSingleton Get(Akka.Actor.ActorSystem system) { } public Akka.Actor.IActorRef Init(Akka.Cluster.Tools.Singleton.SingletonActor singleton) { } } + [System.Runtime.CompilerServices.NullableAttribute(new byte[] { + 0, + 1})] public sealed class ClusterSingletonManager : Akka.Actor.FSM { public ClusterSingletonManager(Akka.Actor.Props singletonProps, object terminationMessage, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings settings) { } @@ -404,6 +407,7 @@ namespace Akka.Cluster.Tools.Singleton public static Akka.Actor.Props Props(Akka.Actor.Props singletonProps, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings settings) { } public static Akka.Actor.Props Props(Akka.Actor.Props singletonProps, object terminationMessage, Akka.Cluster.Tools.Singleton.ClusterSingletonManagerSettings settings) { } } + [System.Runtime.CompilerServices.NullableAttribute(0)] public sealed class ClusterSingletonManagerIsStuckException : Akka.Actor.AkkaException { public ClusterSingletonManagerIsStuckException(string message) { }