From 85ceafee371fb8631b3f79556c4c175b3c3315fb Mon Sep 17 00:00:00 2001 From: shivanshmishra54 Date: Thu, 1 Oct 2026 21:05:34 +0530 Subject: [PATCH 1/3] fix: prevent concurrent status updates Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../admin/server/services/StatusUpdater.java | 11 +++++++++++ .../server/services/StatusUpdaterTest.java | 17 +++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java index 30cfcd7e6dc..337a3784598 100644 --- a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.BiPredicate; import java.util.logging.Level; @@ -67,6 +69,8 @@ public class StatusUpdater { private final HealthGroupsCache healthGroupsCache; + private final ConcurrentMap> updates = new ConcurrentHashMap<>(); + private Duration timeout = Duration.ofSeconds(10); public StatusUpdater(InstanceRepository repository, InstanceWebClient instanceWebClient, @@ -81,6 +85,13 @@ public StatusUpdater timeout(Duration timeout) { } public Mono updateStatus(InstanceId id) { + return this.updates.computeIfAbsent(id, + (instanceId) -> Mono.defer(() -> this.updateStatusInternal(instanceId)) + .doFinally((signal) -> this.updates.remove(instanceId)) + .cache()); + } + + private Mono updateStatusInternal(InstanceId id) { return this.repository.computeIfPresent(id, (key, instance) -> this.doUpdateStatus(instance)).then(); } diff --git a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java index acde0549167..75685fdcb46 100644 --- a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java +++ b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java @@ -45,11 +45,14 @@ import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.okForContentType; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.retry; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.rewriteEndpointUrl; @@ -181,6 +184,20 @@ void should_change_status_to_up() { .verifyComplete(); } + @Test + void should_not_run_concurrent_status_updates_for_same_instance() { + this.wireMock.stubFor(get("/health").willReturn(ok().withFixedDelay(100))); + + StepVerifier + .create(Mono.when(this.updater.updateStatus(this.instanceId), this.updater.updateStatus(this.instanceId))) + .verifyComplete(); + + this.wireMock.verify(exactly(1), getRequestedFor(urlEqualTo("/health"))); + StepVerifier.create(this.repository.find(this.instanceId)) + .assertNext((app) -> assertThat(app.getStatusInfo().getStatus()).isEqualTo("UP")) + .verifyComplete(); + } + @Test void should_change_status_to_down_with_details() { String body = "{ \"foo\" : \"bar\" }"; From 637213ae2c156603148695bda1505f3a02bd8fa2 Mon Sep 17 00:00:00 2001 From: shivanshmishra54 Date: Thu, 1 Oct 2026 21:38:58 +0530 Subject: [PATCH 2/3] Revert "fix: prevent concurrent status updates" This reverts commit 85ceafee371fb8631b3f79556c4c175b3c3315fb. --- .../admin/server/services/StatusUpdater.java | 11 ----------- .../server/services/StatusUpdaterTest.java | 17 ----------------- 2 files changed, 28 deletions(-) diff --git a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java index 337a3784598..30cfcd7e6dc 100644 --- a/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java +++ b/spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/services/StatusUpdater.java @@ -22,8 +22,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.BiPredicate; import java.util.logging.Level; @@ -69,8 +67,6 @@ public class StatusUpdater { private final HealthGroupsCache healthGroupsCache; - private final ConcurrentMap> updates = new ConcurrentHashMap<>(); - private Duration timeout = Duration.ofSeconds(10); public StatusUpdater(InstanceRepository repository, InstanceWebClient instanceWebClient, @@ -85,13 +81,6 @@ public StatusUpdater timeout(Duration timeout) { } public Mono updateStatus(InstanceId id) { - return this.updates.computeIfAbsent(id, - (instanceId) -> Mono.defer(() -> this.updateStatusInternal(instanceId)) - .doFinally((signal) -> this.updates.remove(instanceId)) - .cache()); - } - - private Mono updateStatusInternal(InstanceId id) { return this.repository.computeIfPresent(id, (key, instance) -> this.doUpdateStatus(instance)).then(); } diff --git a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java index 75685fdcb46..acde0549167 100644 --- a/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java +++ b/spring-boot-admin-server/src/test/java/de/codecentric/boot/admin/server/services/StatusUpdaterTest.java @@ -45,14 +45,11 @@ import de.codecentric.boot.admin.server.web.client.InstanceWebClient; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.exactly; import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.ok; import static com.github.tomakehurst.wiremock.client.WireMock.okForContentType; import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.status; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.retry; import static de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunctions.rewriteEndpointUrl; @@ -184,20 +181,6 @@ void should_change_status_to_up() { .verifyComplete(); } - @Test - void should_not_run_concurrent_status_updates_for_same_instance() { - this.wireMock.stubFor(get("/health").willReturn(ok().withFixedDelay(100))); - - StepVerifier - .create(Mono.when(this.updater.updateStatus(this.instanceId), this.updater.updateStatus(this.instanceId))) - .verifyComplete(); - - this.wireMock.verify(exactly(1), getRequestedFor(urlEqualTo("/health"))); - StepVerifier.create(this.repository.find(this.instanceId)) - .assertNext((app) -> assertThat(app.getStatusInfo().getStatus()).isEqualTo("UP")) - .verifyComplete(); - } - @Test void should_change_status_to_down_with_details() { String body = "{ \"foo\" : \"bar\" }"; From 774a9030e92b283bc7dd00a27b37730d32c22e21 Mon Sep 17 00:00:00 2001 From: shivanshmishra54 Date: Thu, 1 Oct 2026 21:41:26 +0530 Subject: [PATCH 3/3] fix: ignore stale application status updates Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/main/frontend/store.spec.ts | 21 +++++++++++++++++++ .../src/main/frontend/store.ts | 9 ++++++++ 2 files changed, 30 insertions(+) diff --git a/spring-boot-admin-server-ui/src/main/frontend/store.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/store.spec.ts index a51f389f46a..95f7de25b29 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/store.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/store.spec.ts @@ -79,6 +79,27 @@ describe('store', () => { expect(removedListener).not.toHaveBeenCalled(); }); + it('does not replace an application with an older status update', async () => { + mockSubject.next({ + data: { + ...registerWithOneInstance, + status: 'DOWN', + statusTimestamp: '2022-12-19T10:07:21.646905Z', + }, + }); + mockSubject.next({ + data: { + ...registerWithOneInstance, + status: 'UP', + statusTimestamp: '2022-12-19T10:07:20.646905Z', + }, + }); + + await waitFor(() => { + expect(applicationStore.applications[0].status).toBe('DOWN'); + }); + }); + it('deregisters an instance', async () => { mockSubject.next({ data: registerWithTwoInstances }); mockSubject.next({ data: registerWithOneInstance }); diff --git a/spring-boot-admin-server-ui/src/main/frontend/store.ts b/spring-boot-admin-server-ui/src/main/frontend/store.ts index 3d44804ec86..6d7f23d16f2 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/store.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/store.ts @@ -122,6 +122,15 @@ export default class ApplicationStore { updateApplication(application: Application) { const oldApplication = this._applications.get(application.name); + if ( + oldApplication && + oldApplication.statusTimestamp && + application.statusTimestamp && + new Date(application.statusTimestamp) < + new Date(oldApplication.statusTimestamp) + ) { + return; + } if (!oldApplication && application.instances.length > 0) { this._applications.set(application.name, application); this._dispatchEvent('added', application);