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:
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cameleer.server.app.alerting.eval;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public record EvalContext(String tenantId, Instant now, TickCache tickCache) {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user