alerting(eval): clamp first-run cursor to deployBacklogCap — flood guard
New property cameleer.server.alerting.perExchangeDeployBacklogCapSeconds (default 86400 = 24h, 0 disables). On first run (no persisted cursor or malformed), clamp cursorTs to max(rule.createdAt, now - cap) so a long-lived PER_EXCHANGE rule doesn't scan from its creation date forward on first post-deploy tick. Normal-advance path unaffected. Follows up final-review I-1 on the PER_EXCHANGE exactly-once phase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.cameleer.server.app.alerting.eval;
|
||||
|
||||
import com.cameleer.server.app.alerting.config.AlertingProperties;
|
||||
import com.cameleer.server.app.search.ClickHouseSearchIndex;
|
||||
import com.cameleer.server.core.alerting.*;
|
||||
import com.cameleer.server.core.runtime.Environment;
|
||||
@@ -36,7 +37,9 @@ class ExchangeMatchEvaluatorTest {
|
||||
void setUp() {
|
||||
searchIndex = mock(ClickHouseSearchIndex.class);
|
||||
envRepo = mock(EnvironmentRepository.class);
|
||||
eval = new ExchangeMatchEvaluator(searchIndex, envRepo);
|
||||
AlertingProperties props = new AlertingProperties(
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
eval = new ExchangeMatchEvaluator(searchIndex, envRepo, props);
|
||||
|
||||
var env = new Environment(ENV_ID, "prod", "Production", false, true, null, null, null);
|
||||
when(envRepo.findById(ENV_ID)).thenReturn(Optional.of(env));
|
||||
@@ -264,6 +267,39 @@ class ExchangeMatchEvaluatorTest {
|
||||
assertThat(((EvalResult.Batch) r).firings()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstRun_clampsCursorToDeployBacklogCap_whenRuleCreatedLongAgo() {
|
||||
// Rule created 7 days ago, cap default is 24h; expect timeFrom to be now - 24h, not rule.createdAt.
|
||||
Instant now = Instant.parse("2026-04-22T12:00:00Z");
|
||||
Instant createdLongAgo = now.minus(Duration.ofDays(7));
|
||||
Instant expectedClampFloor = now.minusSeconds(86_400); // 24h
|
||||
|
||||
ArgumentCaptor<SearchRequest> cap = ArgumentCaptor.forClass(SearchRequest.class);
|
||||
when(searchIndex.search(cap.capture())).thenReturn(new SearchResult<>(List.of(), 0L, 0, 50));
|
||||
|
||||
ExchangeMatchCondition condition = perExchangeCondition();
|
||||
AlertRule rule = ruleWith(condition, Map.of(), createdLongAgo);
|
||||
eval.evaluate(condition, rule, new EvalContext("default", now, new TickCache()));
|
||||
|
||||
SearchRequest req = cap.getValue();
|
||||
assertThat(req.timeFrom()).isEqualTo(expectedClampFloor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void firstRun_usesCreatedAt_whenWithinDeployBacklogCap() {
|
||||
Instant now = Instant.parse("2026-04-22T12:00:00Z");
|
||||
Instant createdRecent = now.minus(Duration.ofHours(1)); // 1h < 24h cap
|
||||
|
||||
ArgumentCaptor<SearchRequest> cap = ArgumentCaptor.forClass(SearchRequest.class);
|
||||
when(searchIndex.search(cap.capture())).thenReturn(new SearchResult<>(List.of(), 0L, 0, 50));
|
||||
|
||||
ExchangeMatchCondition condition = perExchangeCondition();
|
||||
AlertRule rule = ruleWith(condition, Map.of(), createdRecent);
|
||||
eval.evaluate(condition, rule, new EvalContext("default", now, new TickCache()));
|
||||
|
||||
assertThat(cap.getValue().timeFrom()).isEqualTo(createdRecent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void kindIsExchangeMatch() {
|
||||
assertThat(eval.kind()).isEqualTo(ConditionKind.EXCHANGE_MATCH);
|
||||
|
||||
@@ -50,7 +50,7 @@ class WebhookDispatcherIT {
|
||||
new ApacheOutboundHttpClientFactory(props, new SslContextBuilder()),
|
||||
cipher,
|
||||
new MustacheRenderer(),
|
||||
new AlertingProperties(null, null, null, null, null, null, null, null, null, null, null, null, null),
|
||||
new AlertingProperties(null, null, null, null, null, null, null, null, null, null, null, null, null, null),
|
||||
new ObjectMapper()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ class AlertingRetentionJobIT extends AbstractPostgresIT {
|
||||
// effectiveEventRetentionDays = 90, effectiveNotificationRetentionDays = 30
|
||||
new com.cameleer.server.app.alerting.config.AlertingProperties(
|
||||
null, null, null, null, null, null, null, null, null,
|
||||
90, 30, null, null),
|
||||
90, 30, null, null, null),
|
||||
instanceRepo,
|
||||
notificationRepo,
|
||||
fixedClock);
|
||||
|
||||
Reference in New Issue
Block a user