Skip to content

Commit c7c84a4

Browse files
authored
Fix crash on shutdown: trying to unregister app that never registered (#1705)
Snapshot from sample logs before the fix (press Ctrl+C on running FortuneTellerWeb): ```text dbug: Steeltoe.Discovery.Eureka.EurekaClient[868536339] HTTP DELETE request to 'http://localhost:8761/eureka/apps/STEELTOE.SAMPLES.FORTUNETELLERWEB/JVBD4M3%3ASteeltoe.Samples.FortuneTellerWeb%3A7233' returned status 404 in attempt 1. info: Steeltoe.Discovery.Eureka.EurekaClient[601900377] HTTP DELETE request to 'http://localhost:8761/eureka/apps/STEELTOE.SAMPLES.FORTUNETELLERWEB/JVBD4M3%3ASteeltoe.Samples.FortuneTellerWeb%3A7233' failed with status 404: '{"timestamp":"2026-05-28T11:15:52.686+00:00","status":404,"error":"Not Found","path":"/eureka/apps/STEELTOE.SAMPLES.FORTUNETELLERWEB/JVBD4M3%3ASteeltoe.Samples.FortuneTellerWeb%3A7233"}'. warn: Steeltoe.Discovery.Eureka.EurekaDiscoveryClient[1003466929] Deregister failed during shutdown. Steeltoe.Discovery.Eureka.Transport.EurekaTransportException: Failed to execute request on all known Eureka servers. ```
1 parent e2ed3a3 commit c7c84a4

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

src/Discovery/src/Eureka/EurekaDiscoveryClient.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public sealed partial class EurekaDiscoveryClient : IDiscoveryClient
3737
private readonly Timer? _cacheRefreshTimer;
3838
private readonly SemaphoreSlim _registerUnregisterAsyncLock = new(1);
3939
private readonly SemaphoreSlim _registryFetchAsyncLock = new(1);
40+
private volatile bool _hasRegistered;
4041
private volatile bool _hasFirstHeartbeatCompleted;
4142

4243
private volatile ApplicationInfoCollection _remoteApps;
@@ -195,7 +196,7 @@ public async Task ShutdownAsync(CancellationToken cancellationToken)
195196

196197
try
197198
{
198-
if (!ReferenceEquals(_appInfoManager.Instance, InstanceInfo.Disabled))
199+
if (_hasRegistered && !ReferenceEquals(_appInfoManager.Instance, InstanceInfo.Disabled))
199200
{
200201
await DeregisterAsync(cancellationToken);
201202
}
@@ -268,6 +269,7 @@ internal async Task RegisterAsync(bool requireDirtyInstance, CancellationToken c
268269
LogRegistrationSucceeded(snapshot.AppName, snapshot.InstanceId);
269270

270271
snapshot.IsDirty = false;
272+
_hasRegistered = true;
271273
}
272274
}
273275
finally

src/Discovery/test/Eureka.Test/EurekaDiscoveryClientTest.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,63 @@ public async Task UnRegisterAsync_Succeeds_WhenOKStatusReturned()
417417
handler.Mock.VerifyNoOutstandingExpectation();
418418
}
419419

420+
[Fact]
421+
public async Task ShutdownAsync_Unregisters_WhenRegistered()
422+
{
423+
var appSettings = new Dictionary<string, string?>
424+
{
425+
["Eureka:Client:ShouldFetchRegistry"] = "false",
426+
["Eureka:Client:ShouldRegisterWithEureka"] = "true",
427+
["Eureka:Instance:AppName"] = "FOO",
428+
["Eureka:Instance:InstanceId"] = "localhost:foo"
429+
};
430+
431+
WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create();
432+
builder.Configuration.AddInMemoryCollection(appSettings);
433+
builder.Services.AddEurekaDiscoveryClient();
434+
435+
var handler = new DelegateToMockHttpClientHandler();
436+
handler.Mock.Expect(HttpMethod.Post, "http://localhost:8761/eureka/apps/FOO").Respond(HttpStatusCode.OK);
437+
handler.Mock.Expect(HttpMethod.Delete, "http://localhost:8761/eureka/apps/FOO/localhost%3Afoo").Respond(HttpStatusCode.OK);
438+
439+
await using WebApplication webApplication = builder.Build();
440+
webApplication.Services.GetRequiredService<HttpClientHandlerFactory>().Using(handler);
441+
442+
var discoveryClient = webApplication.Services.GetRequiredService<EurekaDiscoveryClient>();
443+
444+
await discoveryClient.ShutdownAsync(TestContext.Current.CancellationToken);
445+
446+
handler.Mock.VerifyNoOutstandingExpectation();
447+
}
448+
449+
[Fact]
450+
public async Task ShutdownAsync_DoesNotUnregister_WhenNotRegistered()
451+
{
452+
var appSettings = new Dictionary<string, string?>
453+
{
454+
["Eureka:Client:ShouldFetchRegistry"] = "false",
455+
["Eureka:Client:ShouldRegisterWithEureka"] = "true",
456+
["Eureka:Instance:AppName"] = "FOO",
457+
["Eureka:Instance:InstanceId"] = "localhost:foo"
458+
};
459+
460+
WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create();
461+
builder.Configuration.AddInMemoryCollection(appSettings);
462+
builder.Services.AddEurekaDiscoveryClient();
463+
464+
var handler = new DelegateToMockHttpClientHandler();
465+
handler.Mock.Expect(HttpMethod.Post, "http://localhost:8761/eureka/apps/FOO").Respond(HttpStatusCode.NotFound);
466+
467+
await using WebApplication webApplication = builder.Build();
468+
webApplication.Services.GetRequiredService<HttpClientHandlerFactory>().Using(handler);
469+
470+
var discoveryClient = webApplication.Services.GetRequiredService<EurekaDiscoveryClient>();
471+
472+
await discoveryClient.ShutdownAsync(TestContext.Current.CancellationToken);
473+
474+
handler.Mock.VerifyNoOutstandingExpectation();
475+
}
476+
420477
[Fact]
421478
public async Task GetInstancesAsync_ReturnsExpected()
422479
{

0 commit comments

Comments
 (0)