Skip to content

Commit b81a46d

Browse files
MNTR: make barrier failures actually fail the barrier (#8431) (#8492)
Backport of dev commit dcd9acb to v1.5, scoped to the ShardedDaemonProcessSpec rewrite. The Player.cs / MultiNodeTestRunner.cs portions of #8431 were already delivered to v1.5 in the MNTR conductor reliability backport (#8488), so only the spec change is taken here. Rewrite the ShardedDaemonProcess multinode spec to be async and resistant to slow-CI barrier timeouts: - task-returning TestKit methods throughout (no sync-over-async) - single cluster-wide collector probe on 'first' so shards reallocated during cluster settle are reported correctly - fish for a complete distinct-ID set instead of assuming 4 messages - raise testconductor barrier-timeout to 60s for slow agents - fix HOCON brace nesting that dropped keep-alive-interval Adapted to v1.5: AwaitClusterUpAsync(CancellationToken.None, ...) to match the v1.5 TestKit signature. (cherry picked from commit dcd9acb)
1 parent a5b9182 commit b81a46d

1 file changed

Lines changed: 102 additions & 51 deletions

File tree

Lines changed: 102 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
//-----------------------------------------------------------------------
1+
//-----------------------------------------------------------------------
22
// <copyright file="ShardedDaemonProcessSpec.cs" company="Akka.NET Project">
33
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
44
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
55
// </copyright>
66
//-----------------------------------------------------------------------
77

8+
#nullable enable
9+
810
using System;
11+
using System.Collections.Generic;
912
using System.Linq;
13+
using System.Threading;
14+
using System.Threading.Tasks;
1015
using Akka.Actor;
1116
using Akka.Cluster.TestKit;
1217
using Akka.Cluster.Tools.Singleton;
1318
using Akka.Configuration;
19+
using Akka.Event;
1420
using Akka.MultiNode.TestAdapter;
1521
using Akka.Remote.TestKit;
22+
using Akka.TestKit;
1623
using FluentAssertions;
1724

1825
namespace Akka.Cluster.Sharding.Tests.MultiNode
@@ -32,14 +39,22 @@ public ShardedDaemonProcessSpecConfig()
3239
CommonConfig = DebugConfig(false)
3340
.WithFallback(ConfigurationFactory.ParseString(@"
3441
akka.loglevel = INFO
35-
akka.cluster.sharded-daemon-process {{
36-
sharding {{
42+
# `first` collects Started events from every node, so it sits in front of the
43+
# trailing barrier for as long as cross-node shard allocation takes. Give the
44+
# barrier more room than the 30s default so a slow allocation cannot turn into
45+
# a barrier timeout on the nodes waiting for `first`.
46+
akka.testconductor.barrier-timeout = 60s
47+
# NB: these braces used to be doubled, which HOCON parsed as nested anonymous
48+
# objects and quietly dropped keep-alive-interval on the floor - the spec ran on
49+
# the 10s default, so a missed initial start waited 10s for the next ping.
50+
akka.cluster.sharded-daemon-process {
51+
sharding {
3752
# First is likely to be ignored as shard coordinator not ready
3853
retry-interval = 0.2s
39-
}}
54+
}
4055
# quick ping to make test swift
4156
keep-alive-interval = 1s
42-
}}
57+
}
4358
"))
4459
.WithFallback(ClusterSharding.DefaultConfig())
4560
.WithFallback(ClusterSingleton.DefaultConfig())
@@ -55,6 +70,14 @@ protected ShardedDaemonProcessMultiNode(ShardedDaemonProcessSpecConfig config) :
5570

5671
public abstract class ShardedDaemonProcessSpec : MultiNodeClusterSpec
5772
{
73+
private const int TotalProcesses = 4;
74+
75+
/// <summary>
76+
/// Deterministic name for the single cluster-wide collector, so the other nodes can address
77+
/// it. TestKit creates probe actors under the system guardian, hence the /system path below.
78+
/// </summary>
79+
private const string CollectorName = "process-event-collector";
80+
5881
private readonly ShardedDaemonProcessSpecConfig _config;
5982

6083
protected ShardedDaemonProcessSpec(ShardedDaemonProcessSpecConfig config, Type type)
@@ -64,59 +87,68 @@ protected ShardedDaemonProcessSpec(ShardedDaemonProcessSpecConfig config, Type t
6487
}
6588

6689
[MultiNodeFact]
67-
public void ShardedDaemonProcess_Specs()
90+
public async Task ShardedDaemonProcess_Specs()
6891
{
69-
ShardedDaemonProcess_Should_Init_Actor_Set();
92+
await ShardedDaemonProcess_Should_Init_Actor_Set();
7093
}
7194

72-
public void ShardedDaemonProcess_Should_Init_Actor_Set()
95+
private async Task ShardedDaemonProcess_Should_Init_Actor_Set()
7396
{
74-
// HACK
75-
RunOn(() => FormCluster(_config.First, _config.Second, _config.Third), _config.First);
97+
await AwaitClusterUpAsync(CancellationToken.None, _config.First, _config.Second, _config.Third);
98+
99+
// One collector for the whole cluster, living on `first`. Every node passes this same
100+
// ref into its entity Props, so a ProcessActor reports in no matter which node ends up
101+
// hosting it. Handing each node its own local probe would only ever prove that a node
102+
// sees its own entities, which says nothing about the set as a whole.
103+
var collectorProbe = IsNode(_config.First) ? CreateTestProbe(CollectorName) : null;
104+
await EnterBarrierAsync("collector-started");
76105

77-
var probe = CreateTestProbe();
78-
ShardedDaemonProcess.Get(Sys).Init("the-fearless", 4, id => ProcessActor.Props(id, probe.Ref));
79-
EnterBarrier("sharded-daemon-process-initialized");
106+
var collector = await Sys
107+
.ActorSelection(await NodeAsync(_config.First) / "system" / CollectorName)
108+
.ResolveOne(TimeSpan.FromSeconds(20));
80109

81-
RunOn(() =>
110+
ShardedDaemonProcess.Get(Sys).Init("the-fearless", TotalProcesses, id => ProcessActor.Props(id, collector));
111+
await EnterBarrierAsync("sharded-daemon-process-initialized");
112+
113+
if (collectorProbe is not null)
82114
{
83-
var startedIds = Enumerable.Range(0, 4).Select(_ =>
84-
{
85-
var evt = probe.ExpectMsg<ProcessActorEvent>(TimeSpan.FromSeconds(5));
86-
evt.Event.Should().Be("Started");
87-
return evt.Id;
88-
}).ToList();
89-
startedIds.Count.Should().Be(4);
90-
}, _config.First);
91-
EnterBarrier("sharded-daemon-process-started");
115+
await AssertAllProcessesStarted(collectorProbe);
116+
}
117+
118+
await EnterBarrierAsync("sharded-daemon-process-started");
92119
}
93120

94-
private void FormCluster(RoleName first, params RoleName[] rest)
121+
private async Task AssertAllProcessesStarted(TestProbe collectorProbe)
95122
{
96-
RunOn(() =>
97-
{
98-
Cluster.Join(GetAddress(first));
99-
AwaitAssert(() =>
123+
var startedIds = new HashSet<int>();
124+
var hostingNodes = new HashSet<string>();
125+
126+
// A shard can be reallocated while the cluster settles, which stops an entity and starts
127+
// it again elsewhere, so the same id may report twice. Fish until every distinct id has
128+
// checked in rather than assuming exactly TotalProcesses messages arrive; this returns
129+
// as soon as the set is complete, so the bound below is a ceiling and not a delay.
130+
await collectorProbe.FishForMessageAsync<ProcessActorEvent>(
131+
isMessage: evt =>
100132
{
101-
Cluster.State.Members.Select(i => i.UniqueAddress).Should().Contain(Cluster.SelfUniqueAddress);
102-
Cluster.State.Members.Select(i => i.Status).Should().OnlyContain(i => i == MemberStatus.Up);
103-
});
104-
}, first);
105-
EnterBarrier(first.Name + "-joined");
106-
107-
foreach (var node in rest)
108-
{
109-
RunOn(() =>
110-
{
111-
Cluster.Join(GetAddress(first));
112-
AwaitAssert(() =>
113-
{
114-
Cluster.State.Members.Select(i => i.UniqueAddress).Should().Contain(Cluster.SelfUniqueAddress);
115-
Cluster.State.Members.Select(i => i.Status).Should().OnlyContain(i => i == MemberStatus.Up);
116-
});
117-
}, node);
118-
}
119-
EnterBarrier("all-joined");
133+
if (evt.Event != ProcessActorEvent.Started)
134+
return false;
135+
136+
startedIds.Add(evt.Id);
137+
hostingNodes.Add(evt.HostAddress);
138+
return startedIds.Count == TotalProcesses;
139+
},
140+
max: TimeSpan.FromSeconds(30),
141+
hint: $"a Started event from each of the {TotalProcesses} sharded daemon processes");
142+
143+
startedIds.Should().BeEquivalentTo(
144+
Enumerable.Range(0, TotalProcesses),
145+
"every sharded daemon process must start somewhere in the cluster");
146+
147+
// Placement is up to the allocation strategy, so this is reported rather than asserted -
148+
// a single-node placement is legal, just not what this spec is here to exercise.
149+
Log.Info(
150+
"[{0}] sharded daemon processes started across [{1}] node(s): [{2}]",
151+
startedIds.Count, hostingNodes.Count, string.Join(", ", hostingNodes.OrderBy(x => x)));
120152
}
121153
}
122154

@@ -145,31 +177,50 @@ public ProcessActor(int id, IActorRef probe)
145177
public IActorRef Probe { get; }
146178
public int Id { get; }
147179

180+
private string SelfAddress => Cluster.Get(Context.System).SelfAddress.ToString();
181+
148182
protected override void PreStart()
149183
{
150184
base.PreStart();
151-
Probe.Tell(new ProcessActorEvent(Id, "Started"));
185+
Probe.Tell(new ProcessActorEvent(Id, ProcessActorEvent.Started, SelfAddress));
152186
}
153187

154188
protected override void OnReceive(object message)
155189
{
156190
if (message is Stop)
157191
{
158-
Probe.Tell(new ProcessActorEvent(Id, "Stopped"));
192+
Probe.Tell(new ProcessActorEvent(Id, ProcessActorEvent.Stopped, SelfAddress));
159193
Context.Stop(Self);
160194
}
161195
}
162196
}
163197

198+
/// <summary>
199+
/// Reported by a <see cref="ProcessActor"/> to the cluster-wide collector. This crosses the wire
200+
/// whenever the entity is hosted somewhere other than <c>first</c>, so every member is a plain
201+
/// serializable type.
202+
/// </summary>
203+
[Serializable]
164204
internal sealed class ProcessActorEvent
165205
{
166-
public ProcessActorEvent(int id, object @event)
206+
public const string Started = "Started";
207+
public const string Stopped = "Stopped";
208+
209+
public ProcessActorEvent(int id, string @event, string hostAddress)
167210
{
168211
Id = id;
169212
Event = @event;
213+
HostAddress = hostAddress;
170214
}
171215

172216
public int Id { get; }
173-
public object Event { get; }
217+
218+
public string Event { get; }
219+
220+
/// <summary>
221+
/// Address of the node that hosted the entity, so the spec can report how the processes were
222+
/// spread across the cluster.
223+
/// </summary>
224+
public string HostAddress { get; }
174225
}
175226
}

0 commit comments

Comments
 (0)