From d5ac709986139fb4e4ad1b3fb0e33b3511a93486 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Wed, 19 Aug 2026 17:20:15 +0200 Subject: [PATCH] Skip SSE config updated events if they arrive too fast --- .../agent_api/background/RealtimeSSETask.java | 21 ++++++++++++++++++- .../java/background/RealtimeSSETaskTest.java | 16 ++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/background/RealtimeSSETask.java b/agent_api/src/main/java/dev/aikido/agent_api/background/RealtimeSSETask.java index 30c67618..a82e841d 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/background/RealtimeSSETask.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/background/RealtimeSSETask.java @@ -12,12 +12,14 @@ import dev.aikido.agent_api.storage.ServiceConfigStore; import java.util.Optional; +import java.util.concurrent.TimeUnit; public class RealtimeSSETask extends Thread { private static final Logger logger = LogManager.getLogger(RealtimeSSETask.class); private final RealtimeSSEAPI realtimeSSEApi; private final ReportingApiHTTP reportingApi; private Optional configLastUpdatedAt; + private Optional lastConfigRefreshStartedAt = Optional.empty(); public RealtimeSSETask(RealtimeSSEAPI realtimeSSEApi, ReportingApiHTTP reportingApi) { super("RealtimeSSETask"); @@ -32,7 +34,7 @@ public void run() { realtimeSSEApi.listen(this::onEvent); } - public void onEvent(SSEParser.Event event) { + public synchronized void onEvent(SSEParser.Event event) { logger.trace("SSE event received: %s", event.event()); if (!"config-updated".equals(event.event())) { return; @@ -51,6 +53,11 @@ public void onEvent(SSEParser.Event event) { return; } + if (configUpdateArrivedTooFast()) { + logger.debug("Ignoring SSE config-updated event during refresh throttle"); + return; + } + Optional newConfig = reportingApi.fetchNewConfig(); if (newConfig.isEmpty()) { logger.debug("Failed to fetch config after SSE event"); @@ -66,4 +73,16 @@ public void onEvent(SSEParser.Event event) { logger.debug("Config updated via SSE"); } + + private boolean configUpdateArrivedTooFast() { + long now = System.nanoTime(); + if (lastConfigRefreshStartedAt.isPresent()) { + if (now - lastConfigRefreshStartedAt.get() < TimeUnit.SECONDS.toNanos(9)) { + return true; + } + } + + lastConfigRefreshStartedAt = Optional.of(now); + return false; + } } diff --git a/agent_api/src/test/java/background/RealtimeSSETaskTest.java b/agent_api/src/test/java/background/RealtimeSSETaskTest.java index a0375e0e..8ad84972 100644 --- a/agent_api/src/test/java/background/RealtimeSSETaskTest.java +++ b/agent_api/src/test/java/background/RealtimeSSETaskTest.java @@ -84,7 +84,7 @@ public void testHandlesFetchFailureGracefully() { } @Test - public void testRepeatedEventAfterFetchFailure() { + public void testThrottlesRepeatedEventAfterFetchFailure() { when(reportingApi.fetchNewConfig()) .thenReturn(Optional.empty()) .thenReturn(Optional.of(sampleConfig(200))); @@ -94,7 +94,19 @@ public void testRepeatedEventAfterFetchFailure() { task.onEvent(event); task.onEvent(event); - verify(reportingApi, times(2)).fetchNewConfig(); + verify(reportingApi, times(1)).fetchNewConfig(); + verify(reportingApi, never()).fetchBlockedLists(); + } + + @Test + public void testThrottlesNewerEvents() { + when(reportingApi.fetchNewConfig()).thenReturn(Optional.of(sampleConfig(200))); + when(reportingApi.fetchBlockedLists()).thenReturn(Optional.empty()); + + task.onEvent(new SSEParser.Event("config-updated", "{\"configUpdatedAt\":200}")); + task.onEvent(new SSEParser.Event("config-updated", "{\"configUpdatedAt\":300}")); + + verify(reportingApi, times(1)).fetchNewConfig(); verify(reportingApi, times(1)).fetchBlockedLists(); }