Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/core/Akka.Tests/Util/ThreadLocalRandomSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//-----------------------------------------------------------------------
// <copyright file="ThreadLocalRandomSpec.cs" company="Akka.NET Project">
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using System.Linq;
using Xunit;

namespace Akka.Tests.Util;

/// <summary>
/// Sanity checks for <see cref="Akka.Util.ThreadLocalRandom"/>.
/// </summary>
public class ThreadLocalRandomSpec
{
[Fact]
public void Current_should_return_a_usable_Random()
{
var random = Akka.Util.ThreadLocalRandom.Current;
Assert.NotNull(random);

// should not throw and should produce values in the expected range
var value = random.Next(0, 100);
Assert.InRange(value, 0, 99);
}

[Fact]
public void Seed_base_should_come_from_a_nondeterministic_source()
{
// Environment.TickCount returns one stable value throughout a tight loop and therefore
// gives simultaneously-started processes the same base. Multiple draws from the
// cryptographic source must not collapse to that deterministic behavior. We avoid
// asserting that every draw is unique: collisions are valid for a finite random domain.
var seeds = Enumerable.Range(0, 32)
.Select(_ => Akka.Util.ThreadLocalRandom.CreateSeed())
.ToArray();

Assert.True(seeds.Distinct().Skip(1).Any(),
"the seed source returned the same value for all 32 draws");
}
}
23 changes: 19 additions & 4 deletions src/core/Akka/Util/ThreadLocalRandom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,36 @@
//-----------------------------------------------------------------------

using System;
using System.Security.Cryptography;
using System.Threading;

namespace Akka.Util
{
/// <summary>
/// Create random numbers with Thread-specific seeds.
///
///
/// Borrowed form Jon Skeet's brilliant C# in Depth: http://csharpindepth.com/Articles/Chapter12/Random.aspx
///
/// The per-thread seed base is drawn from a cryptographically random source rather than
/// <see cref="Environment.TickCount"/>. Processes launched at nearly the same moment (e.g.
/// orchestrator-started cluster nodes, or the node processes spun up by the multi-node test
/// runner) previously shared the same <see cref="Environment.TickCount"/>-derived base, which
/// meant corresponding threads across those processes drew identical random streams from
/// <see cref="Current"/>. A cryptographically random base removes that deterministic
/// cross-process correlation.
/// </summary>
public static class ThreadLocalRandom
{
private static int _seed = Environment.TickCount;
private static int _seed = CreateSeed();

private static ThreadLocal<Random> _rng = new(() => new Random(Interlocked.Increment(ref _seed)));
private static readonly ThreadLocal<Random> _rng = new(() => new Random(Interlocked.Increment(ref _seed)));

/// <summary>
/// Draws the per-process seed base from the operating system's cryptographic random source.
/// Kept as a separate method so the entropy source can be covered without depending on
/// thread scheduling or comparing two already-distinct per-thread increments.
/// </summary>
internal static int CreateSeed() => RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);

/// <summary>
/// The current random number seed available to this thread
Expand All @@ -33,4 +49,3 @@ public static Random Current
}
}
}

Loading