feat(alerting): evaluator scaffolding (context, result, tick cache, circuit breaker)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-19 19:32:06 +02:00
parent 891c7f87e3
commit 55f4cab948
7 changed files with 237 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
package com.cameleer.server.app.alerting.eval;
import com.cameleer.server.core.alerting.AlertCondition;
import com.cameleer.server.core.alerting.AlertRule;
import com.cameleer.server.core.alerting.ConditionKind;
public interface ConditionEvaluator<C extends AlertCondition> {
ConditionKind kind();
EvalResult evaluate(C condition, AlertRule rule, EvalContext ctx);
}

View File

@@ -0,0 +1,5 @@
package com.cameleer.server.app.alerting.eval;
import java.time.Instant;
public record EvalContext(String tenantId, Instant now, TickCache tickCache) {}

View File

@@ -0,0 +1,25 @@
package com.cameleer.server.app.alerting.eval;
import java.util.List;
import java.util.Map;
public sealed interface EvalResult {
record Firing(Double currentValue, Double threshold, Map<String, Object> context) implements EvalResult {
public Firing {
context = context == null ? Map.of() : Map.copyOf(context);
}
}
record Clear() implements EvalResult {
public static final Clear INSTANCE = new Clear();
}
record Error(Throwable cause) implements EvalResult {}
record Batch(List<Firing> firings) implements EvalResult {
public Batch {
firings = firings == null ? List.of() : List.copyOf(firings);
}
}
}

View File

@@ -0,0 +1,55 @@
package com.cameleer.server.app.alerting.eval;
import com.cameleer.server.core.alerting.ConditionKind;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.ConcurrentHashMap;
public class PerKindCircuitBreaker {
private record State(Deque<Instant> failures, Instant openUntil) {}
private final int threshold;
private final Duration window;
private final Duration cooldown;
private final Clock clock;
private final ConcurrentHashMap<ConditionKind, State> byKind = new ConcurrentHashMap<>();
/** Production constructor — uses system clock. */
public PerKindCircuitBreaker(int threshold, int windowSeconds, int cooldownSeconds) {
this(threshold, windowSeconds, cooldownSeconds, Clock.systemDefaultZone());
}
/** Test constructor — allows a fixed/controllable clock. */
public PerKindCircuitBreaker(int threshold, int windowSeconds, int cooldownSeconds, Clock clock) {
this.threshold = threshold;
this.window = Duration.ofSeconds(windowSeconds);
this.cooldown = Duration.ofSeconds(cooldownSeconds);
this.clock = clock;
}
public void recordFailure(ConditionKind kind) {
byKind.compute(kind, (k, s) -> {
Deque<Instant> deque = (s == null) ? new ArrayDeque<>() : new ArrayDeque<>(s.failures());
Instant now = Instant.now(clock);
Instant cutoff = now.minus(window);
while (!deque.isEmpty() && deque.peekFirst().isBefore(cutoff)) deque.pollFirst();
deque.addLast(now);
Instant openUntil = (deque.size() >= threshold) ? now.plus(cooldown) : null;
return new State(deque, openUntil);
});
}
public boolean isOpen(ConditionKind kind) {
State s = byKind.get(kind);
return s != null && s.openUntil() != null && Instant.now(clock).isBefore(s.openUntil());
}
public void recordSuccess(ConditionKind kind) {
byKind.compute(kind, (k, s) -> new State(new ArrayDeque<>(), null));
}
}

View File

@@ -0,0 +1,14 @@
package com.cameleer.server.app.alerting.eval;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
public class TickCache {
private final ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
public <T> T getOrCompute(String key, Supplier<T> supplier) {
return (T) map.computeIfAbsent(key, k -> supplier.get());
}
}