Skip to content

Commit 44e9c13

Browse files
gdziadkiewiczCopilotCopilot
authored
Fix race in Hierarchy.TryCreateLogger (#294)
* Add the repro project from #292 * Add the repro for #292 as test * Add dotted logers tests and GetCurrentLoggers tests Co-authored-by: Copilot <copilot@github.com> * Use one lock approach and add marker interface * Implement TODOs * Fix mistaken in if -> switch refactor and adjust test for scenario with locking * PR upgrades * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * PR upgrades 2 --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent fcb0f05 commit 44e9c13

5 files changed

Lines changed: 426 additions & 44 deletions

File tree

src/log4net.Tests/Hierarchy/HierarchyTest.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using System.Threading.Tasks;
2424
using System.Xml;
2525
using log4net.Config;
26+
using log4net.Core;
2627
using log4net.Repository;
2728
using log4net.Tests.Appender;
2829
using NUnit.Framework;
@@ -221,4 +222,43 @@ public void CreateChildLoggersMultiThreaded()
221222

222223
Parallel.For(0, 100, i => Assert.That(rep.GetLogger($"A.{i}").Name, Is.EqualTo($"A.{i}")));
223224
}
225+
226+
[Test]
227+
public void GetCurrentLoggers_EmptyHierarchy_ReturnsEmptyArray()
228+
{
229+
ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString());
230+
231+
Assert.That(rep.GetCurrentLoggers(), Is.Empty);
232+
}
233+
234+
[Test]
235+
public void GetCurrentLoggers_ReturnsCreatedLoggers()
236+
{
237+
ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString());
238+
rep.GetLogger("Foo");
239+
rep.GetLogger("Bar");
240+
241+
string[] names = Array.ConvertAll(rep.GetCurrentLoggers(), l => l.Name);
242+
Assert.That(names, Is.EquivalentTo(new[] { "Foo", "Bar" }));
243+
}
244+
245+
[Test]
246+
public void GetCurrentLoggers_DoesNotIncludeRoot()
247+
{
248+
ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString());
249+
rep.GetLogger("Foo");
250+
251+
Assert.That(rep.GetCurrentLoggers(), Has.None.Property(nameof(ILogger.Name)).EqualTo("root"));
252+
}
253+
254+
[Test]
255+
public void GetCurrentLoggers_DoesNotIncludeProvisionNodes()
256+
{
257+
// Creating "A.B.C" before "A" and "A.B" causes provision nodes to be inserted for the ancestors.
258+
ILoggerRepository rep = LogManager.CreateRepository(Guid.NewGuid().ToString());
259+
rep.GetLogger("A.B.C");
260+
261+
string[] names = Array.ConvertAll(rep.GetCurrentLoggers(), l => l.Name);
262+
Assert.That(names, Is.EquivalentTo(new[] { "A.B.C" }));
263+
}
224264
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
#region Apache License
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one or more
4+
// contributor license agreements. See the NOTICE file distributed with
5+
// this work for additional information regarding copyright ownership.
6+
// The ASF licenses this file to you under the Apache License, Version 2.0
7+
// (the "License"); you may not use this file except in compliance with
8+
// the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
#endregion
19+
20+
using System;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
using log4net.Appender;
24+
using log4net.Core;
25+
using log4net.Repository;
26+
using log4net.Repository.Hierarchy;
27+
using NUnit.Framework;
28+
29+
namespace log4net.Tests.Hierarchy;
30+
31+
/// <summary>
32+
/// Concurrency tests for <see cref="LogManager.GetLogger(string, string)"/>.
33+
/// Regression test for LOG4NET-292.
34+
/// </summary>
35+
[TestFixture]
36+
public class LoggingConcurrencyTest
37+
{
38+
private MemoryAppender _memoryAppender = null!;
39+
private ILoggerRepository _repository = null!;
40+
41+
[SetUp]
42+
public void SetUp()
43+
{
44+
_repository = LogManager.CreateRepository(Guid.NewGuid().ToString());
45+
46+
_memoryAppender = new MemoryAppender();
47+
_memoryAppender.ActivateOptions();
48+
49+
var hierarchy = (log4net.Repository.Hierarchy.Hierarchy)_repository;
50+
hierarchy.Root.AddAppender(_memoryAppender);
51+
hierarchy.Root.Level = Level.All;
52+
hierarchy.Configured = true;
53+
}
54+
55+
[TearDown]
56+
public void TearDown()
57+
{
58+
_memoryAppender.Clear();
59+
_repository.Shutdown();
60+
}
61+
62+
/// <summary>
63+
/// Verifies that no messages are lost when multiple threads call
64+
/// <see cref="LogManager.GetLogger(string, string)"/> with the same logger
65+
/// name concurrently (LOG4NET-292).
66+
/// </summary>
67+
[Test]
68+
public void GetLogger_UnderHighConcurrency_ShouldNotLoseMessages()
69+
{
70+
const int messageCount = 400;
71+
const int loggerCount = 20;
72+
const int repetitions = 20;
73+
74+
for (int run = 0; run < repetitions; run++)
75+
{
76+
_memoryAppender.Clear();
77+
78+
Parallel.For(0, messageCount, i =>
79+
{
80+
var logger = LogManager.GetLogger(_repository.Name, "ConcurrencyTest-" + run + "-" + (i % loggerCount));
81+
logger.Info("High concurrency message " + i);
82+
});
83+
84+
var events = _memoryAppender.GetEvents();
85+
Assert.That(events, Has.Length.EqualTo(messageCount), $"Run {run}: expected {messageCount} messages");
86+
87+
for (var i = 0; i < messageCount; i++)
88+
{
89+
var expectedMessage = "High concurrency message " + i;
90+
Assert.That(events, Has.Some.Matches<LoggingEvent>(e => e.RenderedMessage == expectedMessage),
91+
$"Run {run}: Missing message: {expectedMessage}");
92+
}
93+
}
94+
}
95+
96+
[Test]
97+
public void GetLogger_WithDottedNamesUnderHighConcurrency_ShouldNotLoseMessages()
98+
{
99+
const int messageCount = 400;
100+
const int loggerCount = 40;
101+
const int repetitions = 20;
102+
103+
for (var run = 0; run < repetitions; run++)
104+
{
105+
_memoryAppender.Clear();
106+
string loggerPrefix = "DottedConcurrencyTest-" + run;
107+
108+
Parallel.For(0, messageCount, i =>
109+
{
110+
var logger = LogManager.GetLogger(_repository.Name, GetDottedLoggerName(loggerPrefix, i % loggerCount));
111+
logger.Info("Dotted concurrency message " + i);
112+
});
113+
114+
var events = _memoryAppender.GetEvents();
115+
Assert.That(events, Has.Length.EqualTo(messageCount), $"Run {run}: expected {messageCount} messages");
116+
117+
for (var i = 0; i < messageCount; i++)
118+
{
119+
var expectedMessage = "Dotted concurrency message " + i;
120+
Assert.That(events, Has.Some.Matches<LoggingEvent>(e => e.RenderedMessage == expectedMessage),
121+
$"Run {run}: Missing message: {expectedMessage}");
122+
}
123+
}
124+
}
125+
126+
[Test]
127+
public void GetLogger_WhenCreationEventThrows_ShouldNotBlockSubsequentCalls()
128+
{
129+
var hierarchy = (log4net.Repository.Hierarchy.Hierarchy)_repository;
130+
var expectedException = new InvalidOperationException("Logger creation event failed");
131+
string loggerName = "ThrowingEventLogger-" + Guid.NewGuid();
132+
133+
void ThrowingHandler(object sender, LoggerCreationEventArgs args) => throw expectedException;
134+
135+
hierarchy.LoggerCreatedEvent += ThrowingHandler;
136+
try
137+
{
138+
var actualException = Assert.Throws<InvalidOperationException>(() => hierarchy.GetLogger(loggerName));
139+
Assert.That(actualException, Is.SameAs(expectedException));
140+
}
141+
finally
142+
{
143+
hierarchy.LoggerCreatedEvent -= ThrowingHandler;
144+
}
145+
146+
Task<Logger> getLoggerTask = Task.Run(() => (Logger)hierarchy.GetLogger(loggerName));
147+
Assert.That(getLoggerTask.Wait(TimeSpan.FromSeconds(5)), Is.True,
148+
"A failed logger creation event left the logger permanently not ready.");
149+
150+
Logger logger = getLoggerTask.GetAwaiter().GetResult();
151+
logger.Log(Level.Info, "Message after failed creation event", null);
152+
153+
Assert.That(_memoryAppender.GetEvents(), Has.Some.Matches<LoggingEvent>(
154+
e => e.RenderedMessage == "Message after failed creation event"));
155+
}
156+
157+
[Test]
158+
public void GetLogger_WhenParentLoggerIsStillRegistering_ShouldWaitBeforeReturningChild()
159+
{
160+
string testId = Guid.NewGuid().ToString("N");
161+
string parentName = "ConcurrentParent-" + testId;
162+
string firstChildName = parentName + ".FirstChild";
163+
string secondChildName = parentName + ".SecondChild";
164+
165+
using ManualResetEventSlim parentAssignmentStarted = new();
166+
using ManualResetEventSlim allowParentAssignment = new();
167+
168+
using BlockingLoggerFactory factory = new(parentName, firstChildName, secondChildName,
169+
parentAssignmentStarted, allowParentAssignment);
170+
log4net.Repository.Hierarchy.Hierarchy hierarchy = new(factory) { Name = "Hierarchy-" + testId };
171+
MemoryAppender memoryAppender = new();
172+
memoryAppender.ActivateOptions();
173+
hierarchy.Root.AddAppender(memoryAppender);
174+
hierarchy.Root.Level = Level.All;
175+
hierarchy.Configured = true;
176+
177+
try
178+
{
179+
hierarchy.GetLogger(firstChildName);
180+
181+
Task<Logger> parentTask = Task.Run(() => (Logger)hierarchy.GetLogger(parentName));
182+
Assert.That(parentAssignmentStarted.Wait(TimeSpan.FromSeconds(5)), Is.True,
183+
"The parent logger did not reach child relinking.");
184+
185+
Task childLogTask = Task.Run(() =>
186+
{
187+
Logger childLogger = (Logger)hierarchy.GetLogger(secondChildName);
188+
childLogger.Log(Level.Info, "Message from child while parent registers", null);
189+
});
190+
191+
Assert.That(childLogTask.Wait(TimeSpan.FromMilliseconds(100)), Is.False,
192+
"The child logger was returned before its parent logger was ready.");
193+
194+
allowParentAssignment.Set();
195+
196+
Assert.That(parentTask.Wait(TimeSpan.FromSeconds(5)), Is.True);
197+
Assert.That(factory.SecondChildCreated.Wait(TimeSpan.FromSeconds(5)), Is.True,
198+
"The second child logger was not created.");
199+
Assert.That(childLogTask.Wait(TimeSpan.FromSeconds(5)), Is.True);
200+
parentTask.GetAwaiter().GetResult();
201+
childLogTask.GetAwaiter().GetResult();
202+
203+
Assert.That(memoryAppender.GetEvents(), Has.Some.Matches<LoggingEvent>(
204+
e => e.RenderedMessage == "Message from child while parent registers"));
205+
}
206+
finally
207+
{
208+
allowParentAssignment.Set();
209+
memoryAppender.Clear();
210+
hierarchy.Shutdown();
211+
}
212+
}
213+
214+
private static string GetDottedLoggerName(string prefix, int index)
215+
{
216+
int group = index / 4;
217+
string groupName = prefix + ".Group" + group;
218+
219+
return (index % 4) switch
220+
{
221+
0 => groupName,
222+
1 => groupName + ".Child",
223+
2 => groupName + ".Child.Grandchild",
224+
_ => groupName + ".Sibling"
225+
};
226+
}
227+
228+
private sealed class BlockingLoggerFactory(
229+
string parentName,
230+
string firstChildName,
231+
string secondChildName,
232+
ManualResetEventSlim parentAssignmentStarted,
233+
ManualResetEventSlim allowParentAssignment) : ILoggerFactory, IDisposable
234+
{
235+
public ManualResetEventSlim SecondChildCreated { get; } = new();
236+
237+
public Logger CreateLogger(ILoggerRepository repository, string? name)
238+
{
239+
if (name is null)
240+
{
241+
return new RootLogger(repository.LevelMap.LookupWithDefault(Level.Debug));
242+
}
243+
244+
if (name == secondChildName)
245+
{
246+
SecondChildCreated.Set();
247+
}
248+
249+
return new BlockingLogger(name, parentName, firstChildName, parentAssignmentStarted, allowParentAssignment);
250+
}
251+
252+
public void Dispose() => SecondChildCreated.Dispose();
253+
}
254+
255+
private sealed class BlockingLogger(
256+
string name,
257+
string parentName,
258+
string blockedChildName,
259+
ManualResetEventSlim parentAssignmentStarted,
260+
ManualResetEventSlim allowParentAssignment) : Logger(name)
261+
{
262+
public override Logger? Parent
263+
{
264+
get => base.Parent;
265+
set
266+
{
267+
if (Name == blockedChildName && value?.Name == parentName)
268+
{
269+
parentAssignmentStarted.Set();
270+
allowParentAssignment.Wait(TimeSpan.FromSeconds(5));
271+
}
272+
273+
base.Parent = value;
274+
}
275+
}
276+
}
277+
}

0 commit comments

Comments
 (0)