diff --git a/src/contrib/hosting/Akka.Hosting.Tests/Logging/SemanticLoggingSpecs.cs b/src/contrib/hosting/Akka.Hosting.Tests/Logging/SemanticLoggingSpecs.cs
index 23eeffb057a..771297d158a 100644
--- a/src/contrib/hosting/Akka.Hosting.Tests/Logging/SemanticLoggingSpecs.cs
+++ b/src/contrib/hosting/Akka.Hosting.Tests/Logging/SemanticLoggingSpecs.cs
@@ -1,10 +1,11 @@
-// -----------------------------------------------------------------------
+// -----------------------------------------------------------------------
//
// Copyright (C) 2013-2025 .NET Foundation
//
// -----------------------------------------------------------------------
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -241,7 +242,9 @@ private async Task WaitUntilSilent(TimeSpan timeout)
private async Task AwaitAssertAsync(Action assertion, TimeSpan? timeout = null, TimeSpan? interval = null)
{
- var maxWait = timeout ?? TimeSpan.FromSeconds(3);
+ // The logger actor runs on the default dispatcher; on a starved CI agent it has been seen to reach
+ // an event ~3 s after it was published, so 3 s left no slack.
+ var maxWait = timeout ?? TimeSpan.FromSeconds(10);
var checkInterval = interval ?? TimeSpan.FromMilliseconds(100);
var cts = new CancellationTokenSource(maxWait);
@@ -334,8 +337,10 @@ private static object ParseArg(string arg)
public class SemanticTestLogger : ILogger
{
private readonly ITestOutputHelper _helper;
- public readonly List LogEntries = new();
- public int ReceivedLogs { get; private set; }
+ // Written on the logger actor's thread, read on the test's; ConcurrentQueue is safe for both.
+ public readonly ConcurrentQueue LogEntries = new();
+ private int _receivedLogs;
+ public int ReceivedLogs => Volatile.Read(ref _receivedLogs);
public SemanticTestLogger(ITestOutputHelper helper)
{
@@ -345,15 +350,16 @@ public SemanticTestLogger(ITestOutputHelper helper)
public void Clear()
{
LogEntries.Clear();
- ReceivedLogs = 0;
+ Interlocked.Exchange(ref _receivedLogs, 0);
}
public void Log(LogLevel logLevel, EventId eventId, TState state,
Exception? exception, Func formatter)
{
var message = formatter(state, exception);
- _helper.WriteLine($"[{logLevel}] {message}");
- ReceivedLogs++;
+ // the wall-clock print time, not the event's own timestamp: shows when the logger actually got to it
+ _helper.WriteLine($"{DateTime.UtcNow:HH:mm:ss.fff} [{logLevel}] {message}");
+ Interlocked.Increment(ref _receivedLogs);
// Capture state as dictionary for semantic logging assertions
var stateDict = new Dictionary();
@@ -365,7 +371,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state,
}
}
- LogEntries.Add(new LogEntry
+ LogEntries.Enqueue(new LogEntry
{
LogLevel = logLevel,
Message = message,
diff --git a/src/contrib/hosting/Akka.Hosting.Tests/Logging/WithContextSpecs.cs b/src/contrib/hosting/Akka.Hosting.Tests/Logging/WithContextSpecs.cs
index 1dc3bc7fb78..94847448e74 100644
--- a/src/contrib/hosting/Akka.Hosting.Tests/Logging/WithContextSpecs.cs
+++ b/src/contrib/hosting/Akka.Hosting.Tests/Logging/WithContextSpecs.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------
+// -----------------------------------------------------------------------
//
// Copyright (C) 2013-2025 .NET Foundation
//
@@ -173,7 +173,9 @@ private async Task WaitUntilSilent(TimeSpan timeout)
private async Task AwaitAssertAsync(Action assertion, TimeSpan? timeout = null, TimeSpan? interval = null)
{
- var maxWait = timeout ?? TimeSpan.FromSeconds(3);
+ // The logger actor runs on the default dispatcher; on a starved CI agent it has been seen to reach
+ // an event ~3 s after it was published, so 3 s left no slack.
+ var maxWait = timeout ?? TimeSpan.FromSeconds(10);
var checkInterval = interval ?? TimeSpan.FromMilliseconds(100);
var cts = new CancellationTokenSource(maxWait);