Skip to content

Commit 194f1ee

Browse files
committed
Port HttpClient Factory test fixes
Porting some improvements to flaky tests that were made in master \n\nCommit migrated from dotnet/extensions@4985850
1 parent 68c1432 commit 194f1ee

File tree

1 file changed

+85
-28
lines changed

1 file changed

+85
-28
lines changed

src/HttpClientFactory/Http/test/DefaultHttpClientFactoryTest.cs

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Net.Http;
7+
using System.Runtime.CompilerServices;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Microsoft.Extensions.DependencyInjection;
@@ -328,6 +329,40 @@ public async Task Factory_CleanupCycle_DisposesEligibleHandler()
328329
EnableCleanupTimer = true,
329330
};
330331

332+
var cleanupEntry = await SimulateClientUse_Factory_CleanupCycle_DisposesEligibleHandler(factory);
333+
334+
// Being pretty conservative here because we want this test to be reliable,
335+
// and it depends on the GC and timing.
336+
for (var i = 0; i < 3; i++)
337+
{
338+
GC.Collect();
339+
GC.WaitForPendingFinalizers();
340+
GC.Collect();
341+
342+
if (cleanupEntry.CanDispose)
343+
{
344+
break;
345+
}
346+
347+
await Task.Delay(TimeSpan.FromSeconds(1));
348+
}
349+
350+
Assert.True(cleanupEntry.CanDispose, "Cleanup entry disposable");
351+
352+
// Act
353+
factory.CleanupTimer_Tick(state: null);
354+
355+
// Assert
356+
Assert.Empty(factory._expiredHandlers);
357+
Assert.Equal(1, disposeHandler.DisposeCount);
358+
Assert.False(factory.CleanupTimerStarted.IsSet, "Cleanup timer not started");
359+
}
360+
361+
// Seprate to avoid the HttpClient getting its lifetime extended by
362+
// the state machine of the test.
363+
[MethodImpl(MethodImplOptions.NoInlining)]
364+
private async Task<ExpiredHandlerTrackingEntry> SimulateClientUse_Factory_CleanupCycle_DisposesEligibleHandler(TestHttpClientFactory factory)
365+
{
331366
// Create a handler and move it to the expired state
332367
var client1 = factory.CreateClient("github");
333368

@@ -344,19 +379,8 @@ public async Task Factory_CleanupCycle_DisposesEligibleHandler()
344379
// the factory isn't keeping any references.
345380
kvp = default;
346381
client1 = null;
347-
GC.Collect();
348-
GC.WaitForPendingFinalizers();
349-
GC.Collect();
350-
351-
Assert.True(cleanupEntry.CanDispose, "Cleanup entry disposable");
352-
353-
// Act
354-
factory.CleanupTimer_Tick(null);
355382

356-
// Assert
357-
Assert.Empty(factory._expiredHandlers);
358-
Assert.Equal(1, disposeHandler.DisposeCount);
359-
Assert.False(factory.CleanupTimerStarted.IsSet, "Cleanup timer not started");
383+
return cleanupEntry;
360384
}
361385

362386
[Fact]
@@ -375,6 +399,42 @@ public async Task Factory_CleanupCycle_DisposesLiveHandler()
375399
EnableCleanupTimer = true,
376400
};
377401

402+
var cleanupEntry = await SimulateClientUse_Factory_CleanupCycle_DisposesLiveHandler(factory, disposeHandler);
403+
404+
// Being pretty conservative here because we want this test to be reliable,
405+
// and it depends on the GC and timing.
406+
for (var i = 0; i < 3; i++)
407+
{
408+
GC.Collect();
409+
GC.WaitForPendingFinalizers();
410+
GC.Collect();
411+
412+
if (cleanupEntry.CanDispose)
413+
{
414+
break;
415+
}
416+
417+
await Task.Delay(TimeSpan.FromSeconds(1));
418+
}
419+
420+
Assert.True(cleanupEntry.CanDispose, "Cleanup entry disposable");
421+
422+
// Act - 2
423+
factory.CleanupTimer_Tick(state: null);
424+
425+
// Assert
426+
Assert.Empty(factory._expiredHandlers);
427+
Assert.Equal(1, disposeHandler.DisposeCount);
428+
Assert.False(factory.CleanupTimerStarted.IsSet, "Cleanup timer not started");
429+
}
430+
431+
// Seprate to avoid the HttpClient getting its lifetime extended by
432+
// the state machine of the test.
433+
[MethodImpl(MethodImplOptions.NoInlining)]
434+
private async Task<ExpiredHandlerTrackingEntry> SimulateClientUse_Factory_CleanupCycle_DisposesLiveHandler(
435+
TestHttpClientFactory factory,
436+
DisposeTrackingHandler disposeHandler)
437+
{
378438
// Create a handler and move it to the expired state
379439
var client1 = factory.CreateClient("github");
380440

@@ -386,12 +446,16 @@ public async Task Factory_CleanupCycle_DisposesLiveHandler()
386446
var cleanupEntry = Assert.Single(factory._expiredHandlers);
387447
Assert.True(factory.CleanupTimerStarted.IsSet, "Cleanup timer started");
388448

389-
// Nulling out the refernces to the internal state of the factory since they wouldn't exist in the non-test
449+
// Nulling out the references to the internal state of the factory since they wouldn't exist in the non-test
390450
// scenario. We're holding on the client to prevent disposal - like a real use case.
391-
kvp = default;
451+
lock (this)
452+
{
453+
// Prevent reordering
454+
kvp = default;
455+
}
392456

393457
// Act - 1
394-
factory.CleanupTimer_Tick(null);
458+
factory.CleanupTimer_Tick(state: null);
395459

396460
// Assert
397461
Assert.Same(cleanupEntry, Assert.Single(factory._expiredHandlers));
@@ -401,20 +465,13 @@ public async Task Factory_CleanupCycle_DisposesLiveHandler()
401465
// We need to make sure that the outer handler actually gets GCed, so drop our references to it.
402466
// This is important because the factory relies on this possibility for correctness. We need to ensure that
403467
// the factory isn't keeping any references.
404-
client1 = null;
405-
GC.Collect();
406-
GC.WaitForPendingFinalizers();
407-
GC.Collect();
408-
409-
Assert.True(cleanupEntry.CanDispose, "Cleanup entry disposable");
410-
411-
// Act - 2
412-
factory.CleanupTimer_Tick(null);
468+
lock (this)
469+
{
470+
// Prevent reordering
471+
client1 = null;
472+
}
413473

414-
// Assert
415-
Assert.Empty(factory._expiredHandlers);
416-
Assert.Equal(1, disposeHandler.DisposeCount);
417-
Assert.False(factory.CleanupTimerStarted.IsSet, "Cleanup timer not started");
474+
return cleanupEntry;
418475
}
419476

420477
private class TestHttpClientFactory : DefaultHttpClientFactory

0 commit comments

Comments
 (0)