|
| 1 | +//----------------------------------------------------------------------- |
| 2 | +// <copyright file="ClusterShardingGracefulShutdownOldestSpec.cs" company="Akka.NET Project"> |
| 3 | +// Copyright (C) 2009-2023 Lightbend Inc. <http://www.lightbend.com> |
| 4 | +// Copyright (C) 2013-2023 .NET Foundation <https://github.com/akkadotnet/akka.net> |
| 5 | +// </copyright> |
| 6 | +//----------------------------------------------------------------------- |
| 7 | + |
| 8 | +using System; |
| 9 | +using System.Collections.Immutable; |
| 10 | +using System.Linq; |
| 11 | +using Akka.Actor; |
| 12 | +using Akka.Cluster.Sharding.Delivery; |
| 13 | +using Akka.Configuration; |
| 14 | +using Akka.MultiNode.TestAdapter; |
| 15 | +using Akka.Remote.TestKit; |
| 16 | +using Akka.Util; |
| 17 | +using FluentAssertions; |
| 18 | + |
| 19 | +namespace Akka.Cluster.Sharding.Tests.MultiNode.Delivery |
| 20 | +{ |
| 21 | + public class ClusterShardingDeliveryGracefulShutdownSpecConfig : MultiNodeClusterShardingConfig |
| 22 | + { |
| 23 | + public RoleName First { get; } |
| 24 | + public RoleName Second { get; } |
| 25 | + |
| 26 | + public ClusterShardingDeliveryGracefulShutdownSpecConfig(StateStoreMode mode) |
| 27 | + : base(mode: mode, loglevel: "DEBUG", additionalConfig: @" |
| 28 | +# don't leak ddata state across runs |
| 29 | +akka.cluster.sharding.distributed-data.durable.keys = [] |
| 30 | +akka.reliable-delivery.sharding.consumer-controller.allow-bypass = true |
| 31 | +") |
| 32 | + { |
| 33 | + First = Role("first"); |
| 34 | + Second = Role("second"); |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + public class PersistentClusterShardingDeliveryGracefulShutdownSpecConfig : ClusterShardingDeliveryGracefulShutdownSpecConfig |
| 39 | + { |
| 40 | + public PersistentClusterShardingDeliveryGracefulShutdownSpecConfig() |
| 41 | + : base(StateStoreMode.Persistence) |
| 42 | + { |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + public class DDataClusterShardingDeliveryGracefulShutdownSpecConfig : ClusterShardingDeliveryGracefulShutdownSpecConfig |
| 47 | + { |
| 48 | + public DDataClusterShardingDeliveryGracefulShutdownSpecConfig() |
| 49 | + : base(StateStoreMode.DData) |
| 50 | + { |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + public class PersistentClusterShardingDeliveryGracefulShutdownSpec : ClusterShardingDeliveryGracefulShutdownSpec |
| 55 | + { |
| 56 | + public PersistentClusterShardingDeliveryGracefulShutdownSpec() |
| 57 | + : base(new PersistentClusterShardingDeliveryGracefulShutdownSpecConfig(), typeof(PersistentClusterShardingDeliveryGracefulShutdownSpec)) |
| 58 | + { |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + public class DDataClusterShardingDeliveryGracefulShutdownSpec : ClusterShardingDeliveryGracefulShutdownSpec |
| 63 | + { |
| 64 | + public DDataClusterShardingDeliveryGracefulShutdownSpec() |
| 65 | + : base(new DDataClusterShardingDeliveryGracefulShutdownSpecConfig(), typeof(DDataClusterShardingDeliveryGracefulShutdownSpec)) |
| 66 | + { |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + public abstract class ClusterShardingDeliveryGracefulShutdownSpec : MultiNodeClusterShardingSpec<ClusterShardingDeliveryGracefulShutdownSpecConfig> |
| 71 | + { |
| 72 | + #region setup |
| 73 | + |
| 74 | + public class TerminationOrderActor : ActorBase |
| 75 | + { |
| 76 | + public class RegionTerminated |
| 77 | + { |
| 78 | + public static RegionTerminated Instance = new(); |
| 79 | + |
| 80 | + private RegionTerminated() |
| 81 | + { |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + public class CoordinatorTerminated |
| 86 | + { |
| 87 | + public static CoordinatorTerminated Instance = new(); |
| 88 | + |
| 89 | + private CoordinatorTerminated() |
| 90 | + { |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + public static Props Props(IActorRef probe, IActorRef coordinator, IActorRef region) |
| 95 | + { |
| 96 | + return Actor.Props.Create(() => new TerminationOrderActor(probe, coordinator, region)); |
| 97 | + } |
| 98 | + |
| 99 | + private readonly IActorRef _probe; |
| 100 | + private readonly IActorRef _coordinator; |
| 101 | + private readonly IActorRef _region; |
| 102 | + |
| 103 | + public TerminationOrderActor(IActorRef probe, IActorRef coordinator, IActorRef region) |
| 104 | + { |
| 105 | + _probe = probe; |
| 106 | + _coordinator = coordinator; |
| 107 | + _region = region; |
| 108 | + |
| 109 | + Context.Watch(coordinator); |
| 110 | + Context.Watch(region); |
| 111 | + } |
| 112 | + |
| 113 | + protected override bool Receive(object message) |
| 114 | + { |
| 115 | + switch (message) |
| 116 | + { |
| 117 | + case Terminated t when t.ActorRef.Equals(_coordinator): |
| 118 | + _probe.Tell(CoordinatorTerminated.Instance); |
| 119 | + return true; |
| 120 | + |
| 121 | + case Terminated t when t.ActorRef.Equals(_region): |
| 122 | + _probe.Tell(RegionTerminated.Instance); |
| 123 | + return true; |
| 124 | + } |
| 125 | + return false; |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + private sealed class MessageExtractor: IMessageExtractor |
| 130 | + { |
| 131 | + public string EntityId(object message) |
| 132 | + => message switch |
| 133 | + { |
| 134 | + SlowStopConsumerEntity.Job j => j.Payload.ToString(), |
| 135 | + _ => null |
| 136 | + }; |
| 137 | + |
| 138 | + public object EntityMessage(object message) |
| 139 | + => message; |
| 140 | + |
| 141 | + public string ShardId(object message) |
| 142 | + => message switch |
| 143 | + { |
| 144 | + SlowStopConsumerEntity.Job j => j.Payload.ToString(), |
| 145 | + _ => null |
| 146 | + }; |
| 147 | + |
| 148 | + public string ShardId(string entityId, object messageHint = null) |
| 149 | + => entityId; |
| 150 | + } |
| 151 | + |
| 152 | + private const string TypeName = "SlowStopEntity"; |
| 153 | + private IActorRef _producer; |
| 154 | + private IActorRef _producerController; |
| 155 | + |
| 156 | + protected ClusterShardingDeliveryGracefulShutdownSpec(ClusterShardingDeliveryGracefulShutdownSpecConfig config, Type type) |
| 157 | + : base(config, type) |
| 158 | + { |
| 159 | + } |
| 160 | + |
| 161 | + private IActorRef CreateProducer(string producerId) |
| 162 | + { |
| 163 | + _producerController = |
| 164 | + Sys.ActorOf( |
| 165 | + ShardingProducerController.Create<SlowStopConsumerEntity.Job>( |
| 166 | + producerId: producerId, |
| 167 | + shardRegion: ClusterSharding.Get(Sys).ShardRegion(TypeName), |
| 168 | + durableQueue: Option<Props>.None, |
| 169 | + settings: ShardingProducerController.Settings.Create(Sys)), |
| 170 | + "shardingProducerController"); |
| 171 | + _producer = Sys.ActorOf(Props.Create(() => new TestShardingProducer(_producerController, TestActor)), |
| 172 | + "producer"); |
| 173 | + return _producer; |
| 174 | + } |
| 175 | + |
| 176 | + private IActorRef StartSharding() |
| 177 | + { |
| 178 | + return ClusterSharding.Get(Sys).Start( |
| 179 | + typeName: TypeName, |
| 180 | + entityPropsFactory: e => ShardingConsumerController.Create<SlowStopConsumerEntity.Job>( |
| 181 | + c => Props.Create(() => new SlowStopConsumerEntity(e, c)), |
| 182 | + ShardingConsumerController.Settings.Create(Sys)), |
| 183 | + settings: Settings.Value.WithRole(null), |
| 184 | + messageExtractor: new MessageExtractor(), |
| 185 | + allocationStrategy: ShardAllocationStrategy.LeastShardAllocationStrategy(absoluteLimit: 2, relativeLimit: 1.0), |
| 186 | + handOffStopMessage: SlowStopConsumerEntity.Stop.Instance); |
| 187 | + } |
| 188 | + |
| 189 | + #endregion |
| 190 | + |
| 191 | + [MultiNodeFact] |
| 192 | + public void ClusterShardingGracefulShutdownSpecs() |
| 193 | + { |
| 194 | + Cluster_sharding_must_join_cluster(); |
| 195 | + Cluster_sharding_must_start_some_shards_in_both_regions(); |
| 196 | + Cluster_sharding_must_gracefully_shutdown_the_oldest_region(); |
| 197 | + } |
| 198 | + |
| 199 | + private void Cluster_sharding_must_join_cluster() |
| 200 | + { |
| 201 | + StartPersistenceIfNeeded(startOn: Config.First, Config.First, Config.Second); |
| 202 | + |
| 203 | + Join(Config.First, Config.First); |
| 204 | + Join(Config.Second, Config.First); |
| 205 | + |
| 206 | + // make sure all nodes are up |
| 207 | + AwaitAssert(() => |
| 208 | + { |
| 209 | + Cluster.Get(Sys).SendCurrentClusterState(TestActor); |
| 210 | + ExpectMsg<ClusterEvent.CurrentClusterState>().Members.Count.Should().Be(2); |
| 211 | + }); |
| 212 | + |
| 213 | + RunOn(() => |
| 214 | + { |
| 215 | + StartSharding(); |
| 216 | + }, Config.First); |
| 217 | + |
| 218 | + RunOn(() => |
| 219 | + { |
| 220 | + StartSharding(); |
| 221 | + }, Config.Second); |
| 222 | + |
| 223 | + EnterBarrier("sharding started"); |
| 224 | + } |
| 225 | + |
| 226 | + private void Cluster_sharding_must_start_some_shards_in_both_regions() |
| 227 | + { |
| 228 | + RunOn(() => |
| 229 | + { |
| 230 | + var producer = CreateProducer("p-1"); |
| 231 | + Within(TimeSpan.FromSeconds(30), () => |
| 232 | + { |
| 233 | + var regionAddresses = Enumerable.Range(1, 20).Select(n => |
| 234 | + { |
| 235 | + producer.Tell(n, TestActor); |
| 236 | + ExpectMsg(n, TimeSpan.FromSeconds(1)); |
| 237 | + return LastSender.Path.Address; |
| 238 | + }).ToImmutableHashSet(); |
| 239 | + |
| 240 | + regionAddresses.Count.Should().Be(2); |
| 241 | + }); |
| 242 | + }, Config.First); |
| 243 | + |
| 244 | + EnterBarrier("after-2"); |
| 245 | + } |
| 246 | + |
| 247 | + private void Cluster_sharding_must_gracefully_shutdown_the_oldest_region() |
| 248 | + { |
| 249 | + Within(TimeSpan.FromSeconds(30), () => |
| 250 | + { |
| 251 | + RunOn(() => |
| 252 | + { |
| 253 | + IActorRef coordinator = null; |
| 254 | + AwaitAssert(() => |
| 255 | + { |
| 256 | + coordinator = Sys |
| 257 | + .ActorSelection($"/system/sharding/{TypeName}Coordinator/singleton/coordinator") |
| 258 | + .ResolveOne(RemainingOrDefault).Result; |
| 259 | + }); |
| 260 | + var terminationProbe = CreateTestProbe(); |
| 261 | + var region = ClusterSharding.Get(Sys).ShardRegion(TypeName); |
| 262 | + Sys.ActorOf(TerminationOrderActor.Props(terminationProbe.Ref, coordinator, region)); |
| 263 | + |
| 264 | + // trigger graceful shutdown |
| 265 | + Cluster.Leave(GetAddress(Config.First)); |
| 266 | + |
| 267 | + // region first |
| 268 | + terminationProbe.ExpectMsg<TerminationOrderActor.RegionTerminated>(); |
| 269 | + terminationProbe.ExpectMsg<TerminationOrderActor.CoordinatorTerminated>(); |
| 270 | + }, Config.First); |
| 271 | + |
| 272 | + EnterBarrier("terminated"); |
| 273 | + |
| 274 | + RunOn(() => |
| 275 | + { |
| 276 | + var producer = CreateProducer("p-2"); |
| 277 | + AwaitAssert(() => |
| 278 | + { |
| 279 | + var responses = Enumerable.Range(1, 20).Select(n => |
| 280 | + { |
| 281 | + producer.Tell(n, TestActor); |
| 282 | + return ExpectMsg(n, TimeSpan.FromSeconds(1)); |
| 283 | + }).ToImmutableHashSet(); |
| 284 | + |
| 285 | + responses.Count.Should().Be(20); |
| 286 | + }); |
| 287 | + }, Config.Second); |
| 288 | + EnterBarrier("done-o"); |
| 289 | + }); |
| 290 | + } |
| 291 | + } |
| 292 | +} |
0 commit comments