Skip to content

Commit 1f4826a

Browse files
authored
Fix reliability of a couple of caching tests (#359)
These tests relied on some timing for file writes and static property access. This causes a race condition where the change monitor is disposed at some point internally, but at the same time the test tries to dispose it. This causes the list of change monitors to be enumerated/modified at the same time leading to a crash. The following are fixes/mitigations: - Used a TaskCompletionSource for better management of timing in the test to ensure we wait the right amount of time - Added a lock in the dispose method to guard against reentrancy This also changed the IHttpRuntime access to go through HttpContext.Current instead of a static property. While not necessary for the current bug, it has been the source of other issues where a test will set the static property at the same time another test is using it. Switching to HttpContext.Current will prevent similar issues as the IHttpRuntime will be request/test local.
1 parent 267bb3f commit 1f4826a

File tree

4 files changed

+62
-28
lines changed

4 files changed

+62
-28
lines changed

src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ public static void UseSystemWebAdapters(this IApplicationBuilder app)
5555
{
5656
ArgumentNullException.ThrowIfNull(app);
5757

58-
HttpRuntime.Current = app.ApplicationServices.GetRequiredService<IHttpRuntime>();
59-
6058
app.UseSystemWebAdapterFeatures();
6159
app.UseAuthenticationEvents();
6260
app.UseAuthorizationEvents();

src/Microsoft.AspNetCore.SystemWebAdapters/Caching/CacheDependency.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ public CacheDependency(string[] filenames, DateTime start) : this(filenames, nul
3131
public CacheDependency(string[]? filenames, string[]? cachekeys) : this(filenames, cachekeys, null, DateTime.MaxValue) { }
3232

3333
public CacheDependency(string[]? filenames, string[]? cachekeys, DateTime start) :
34-
this(filenames, cachekeys, null, start) { }
34+
this(filenames, cachekeys, null, start)
35+
{ }
3536

3637
public CacheDependency(string[]? filenames, string[]? cachekeys, CacheDependency? dependency) :
37-
this(filenames, cachekeys, dependency, DateTime.MaxValue) { }
38+
this(filenames, cachekeys, dependency, DateTime.MaxValue)
39+
{ }
3840

3941
public CacheDependency(
4042
string[]? filenames,
@@ -70,7 +72,7 @@ public CacheDependency(
7072
protected internal void FinishInit()
7173
{
7274
HasChanged = changeMonitors.Any(cm => cm.HasChanged && (cm.GetLastModifiedUtc() > utcStart));
73-
utcLastModified = changeMonitors.Count==0 ? DateTime.MinValue : changeMonitors.Max(cm => cm.GetLastModifiedUtc());
75+
utcLastModified = changeMonitors.Count == 0 ? DateTime.MinValue : changeMonitors.Max(cm => cm.GetLastModifiedUtc());
7476
if (HasChanged)
7577
{
7678
NotifyDependencyChanged(this, EventArgs.Empty);
@@ -131,19 +133,27 @@ protected virtual void DependencyDispose() { }
131133

132134
protected virtual void Dispose(bool disposing)
133135
{
134-
if (!disposedValue)
136+
if (disposing)
135137
{
136-
if (disposing)
138+
if (!disposedValue)
137139
{
138-
foreach (var changeMonitor in changeMonitors)
140+
// Ensure that if the Dispose is called by different threads (i.e. the wrapping ChangeMonitor) that it won't enumerate the list at the same time
141+
lock (changeMonitors)
139142
{
140-
changeMonitor?.Dispose();
143+
if (!disposedValue)
144+
{
145+
foreach (var changeMonitor in changeMonitors)
146+
{
147+
changeMonitor?.Dispose();
148+
}
149+
changeMonitors.Clear();
150+
151+
DependencyDispose();
152+
153+
disposedValue = true;
154+
}
141155
}
142-
changeMonitors.Clear();
143-
144-
DependencyDispose();
145156
}
146-
disposedValue = true;
147157
}
148158
}
149159

src/Microsoft.AspNetCore.SystemWebAdapters/HttpRuntime.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,17 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.SystemWebAdapters;
5+
using Microsoft.Extensions.DependencyInjection;
56

67
namespace System.Web;
78

89
public sealed class HttpRuntime
910
{
10-
private static IHttpRuntime? _current;
11-
1211
/// <summary>
1312
/// Gets the current <see cref="IHttpRuntime"/>. This should not be used internally besides where is strictly necessary.
1413
/// If this is needed, it should be retrieved through dependency injection.
1514
/// </summary>
16-
internal static IHttpRuntime Current
17-
{
18-
get => _current ?? throw new InvalidOperationException("HttpRuntime is not available in the current environment");
19-
set => _current = value;
20-
}
15+
internal static IHttpRuntime Current => HttpContext.Current.UnwrapAdapter()?.RequestServices.GetRequiredService<IHttpRuntime>() ?? throw new InvalidOperationException("No runtime is currently available");
2116

2217
private HttpRuntime()
2318
{

test/Microsoft.AspNetCore.SystemWebAdapters.Tests/Caching/CacheTests.cs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
using System.Collections;
55
using System.Collections.Generic;
6+
using System.IO;
67
using System.Runtime.Caching;
78
using System.Threading.Tasks;
89
using AutoFixture;
10+
using Microsoft.AspNetCore.Http;
911
using Microsoft.AspNetCore.SystemWebAdapters;
1012
using Moq;
1113
using Xunit;
@@ -340,6 +342,8 @@ public async Task DependentFileCallback()
340342
var slidingExpiration = TimeSpan.FromMilliseconds(1);
341343
CacheItemUpdateReason? updateReason = default;
342344

345+
var tcs = new TaskCompletionSource();
346+
343347
void Callback(string key, CacheItemUpdateReason reason, out object? expensiveObject, out CacheDependency? dependency, out DateTime absoluteExpiration, out TimeSpan slidingExpiration)
344348
{
345349
expensiveObject = null;
@@ -349,23 +353,25 @@ void Callback(string key, CacheItemUpdateReason reason, out object? expensiveObj
349353

350354
updated = true;
351355
updateReason = reason;
356+
357+
tcs.SetResult();
352358
}
353359

354-
var file = System.IO.Path.GetTempFileName();
355-
await System.IO.File.WriteAllTextAsync(file, key);
360+
var file = Path.GetTempFileName();
361+
await File.WriteAllTextAsync(file, key);
356362

357363
using var cd = new CacheDependency(file);
358364
// Act
359365
cache.Insert(key, item, cd, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, Callback);
360366

361367
// Ensure file is updated
362-
await System.IO.File.WriteAllTextAsync(file, DateTime.UtcNow.ToString("O"));
368+
await File.WriteAllTextAsync(file, DateTime.UtcNow.ToString("O"));
363369

364-
// Small delay here to ensure that the file change notification happens (may fail tests if too fast)
365-
await Task.Delay(10);
370+
// Wait for callback to be called
371+
await tcs.Task;
366372

367-
// Force cleanup to initiate callbacks on current thread
368-
memCache.Trim(100);
373+
// Wait for callback to be finalized
374+
await Task.Delay(100);
369375

370376
// Assert
371377
Assert.True(updated);
@@ -382,7 +388,8 @@ public async Task DependentItemCallback()
382388
var cache = new Cache(memCache);
383389
var httpRuntime = new Mock<IHttpRuntime>();
384390
httpRuntime.Setup(s => s.Cache).Returns(cache);
385-
HttpRuntime.Current = httpRuntime.Object;
391+
392+
SetHttpRuntime(httpRuntime.Object);
386393

387394
var item1 = new object();
388395
var item2 = new object();
@@ -425,4 +432,28 @@ void Callback(string key, CacheItemUpdateReason reason, out object? expensiveObj
425432
Assert.Equal(CacheItemUpdateReason.Expired, updateReason[key1]);
426433
Assert.Equal(CacheItemUpdateReason.DependencyChanged, updateReason[key2]);
427434
}
435+
436+
private static void SetHttpRuntime(IHttpRuntime runtime)
437+
{
438+
_ = new HttpContextAccessor
439+
{
440+
HttpContext = new DefaultHttpContext
441+
{
442+
RequestServices = new RuntimeServiceProvider(runtime)
443+
}
444+
};
445+
}
446+
447+
private sealed class RuntimeServiceProvider : IServiceProvider
448+
{
449+
private readonly IHttpRuntime _runtime;
450+
451+
public RuntimeServiceProvider(IHttpRuntime runtime)
452+
{
453+
_runtime = runtime;
454+
}
455+
456+
public object? GetService(Type serviceType)
457+
=> serviceType == typeof(IHttpRuntime) ? _runtime : null;
458+
}
428459
}

0 commit comments

Comments
 (0)