From d3ce5e861b3253c671938c5f33127308cdffb07e Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:58:49 +0200 Subject: [PATCH] feat(diagrams): add findLatestContentHashForAppRoute with app-route cache Agent-scoped lookups miss diagrams from routes whose publishing agents have been redeployed or removed. The new method resolves by (applicationId, environment, routeId) + created_at DESC, independent of the agent registry. An in-memory cache mirrors the existing hashCache pattern, warm-loaded at startup via argMax. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/storage/ClickHouseDiagramStore.java | 60 +++++++++++++++++++ .../server/core/storage/DiagramStore.java | 14 +++++ .../core/ingestion/ChunkAccumulatorTest.java | 1 + 3 files changed, 75 insertions(+) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseDiagramStore.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseDiagramStore.java index c5340f1a..dc2b879f 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseDiagramStore.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseDiagramStore.java @@ -57,6 +57,12 @@ public class ClickHouseDiagramStore implements DiagramStore { ORDER BY created_at DESC LIMIT 1 """; + private static final String SELECT_HASH_FOR_APP_ROUTE = """ + SELECT content_hash FROM route_diagrams + WHERE tenant_id = ? AND application_id = ? AND environment = ? AND route_id = ? + ORDER BY created_at DESC LIMIT 1 + """; + private static final String SELECT_DEFINITIONS_FOR_APP = """ SELECT DISTINCT route_id, definition FROM route_diagrams WHERE tenant_id = ? AND application_id = ? AND environment = ? @@ -68,6 +74,8 @@ public class ClickHouseDiagramStore implements DiagramStore { // (routeId + "\0" + instanceId) → contentHash private final ConcurrentHashMap hashCache = new ConcurrentHashMap<>(); + // (applicationId + "\0" + environment + "\0" + routeId) → most recent contentHash + private final ConcurrentHashMap appRouteHashCache = new ConcurrentHashMap<>(); // contentHash → deserialized RouteGraph private final ConcurrentHashMap graphCache = new ConcurrentHashMap<>(); @@ -92,12 +100,37 @@ public class ClickHouseDiagramStore implements DiagramStore { } catch (Exception e) { log.warn("Failed to warm diagram hash cache — lookups will fall back to ClickHouse: {}", e.getMessage()); } + + try { + jdbc.query( + "SELECT application_id, environment, route_id, " + + "argMax(content_hash, created_at) AS content_hash " + + "FROM route_diagrams WHERE tenant_id = ? " + + "GROUP BY application_id, environment, route_id", + rs -> { + String key = appRouteCacheKey( + rs.getString("application_id"), + rs.getString("environment"), + rs.getString("route_id")); + appRouteHashCache.put(key, rs.getString("content_hash")); + }, + tenantId); + log.info("Diagram app-route cache warmed: {} entries", appRouteHashCache.size()); + } catch (Exception e) { + log.warn("Failed to warm diagram app-route cache — lookups will fall back to ClickHouse: {}", e.getMessage()); + } } private static String cacheKey(String routeId, String instanceId) { return routeId + "\0" + instanceId; } + private static String appRouteCacheKey(String applicationId, String environment, String routeId) { + return (applicationId != null ? applicationId : "") + "\0" + + (environment != null ? environment : "") + "\0" + + (routeId != null ? routeId : ""); + } + @Override public void store(TaggedDiagram diagram) { try { @@ -122,6 +155,7 @@ public class ClickHouseDiagramStore implements DiagramStore { // Update caches hashCache.put(cacheKey(routeId, agentId), contentHash); + appRouteHashCache.put(appRouteCacheKey(applicationId, environment, routeId), contentHash); graphCache.put(contentHash, graph); log.debug("Stored diagram for route={} agent={} with hash={}", routeId, agentId, contentHash); @@ -199,6 +233,32 @@ public class ClickHouseDiagramStore implements DiagramStore { return Optional.of((String) rows.get(0).get("content_hash")); } + @Override + public Optional findLatestContentHashForAppRoute(String applicationId, + String routeId, + String environment) { + if (applicationId == null || applicationId.isBlank() + || routeId == null || routeId.isBlank() + || environment == null || environment.isBlank()) { + return Optional.empty(); + } + + String key = appRouteCacheKey(applicationId, environment, routeId); + String cached = appRouteHashCache.get(key); + if (cached != null) { + return Optional.of(cached); + } + + List> rows = jdbc.queryForList( + SELECT_HASH_FOR_APP_ROUTE, tenantId, applicationId, environment, routeId); + if (rows.isEmpty()) { + return Optional.empty(); + } + String hash = (String) rows.get(0).get("content_hash"); + appRouteHashCache.put(key, hash); + return Optional.of(hash); + } + @Override public Map findProcessorRouteMapping(String applicationId, String environment) { Map mapping = new HashMap<>(); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/DiagramStore.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/DiagramStore.java index 21b9419d..5b2a007a 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/DiagramStore.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/DiagramStore.java @@ -17,5 +17,19 @@ public interface DiagramStore { Optional findContentHashForRouteByAgents(String routeId, List instanceIds); + /** + * Return the most recently stored {@code content_hash} for the given + * {@code (applicationId, environment, routeId)} triple, regardless of the + * agent instance that produced it. + * + *

Unlike {@link #findContentHashForRoute(String, String)} and + * {@link #findContentHashForRouteByAgents(String, List)}, this lookup is + * independent of the agent registry — so it keeps working for routes + * whose publishing agents have since been redeployed or removed. + */ + Optional findLatestContentHashForAppRoute(String applicationId, + String routeId, + String environment); + Map findProcessorRouteMapping(String applicationId, String environment); } diff --git a/cameleer-server-core/src/test/java/com/cameleer/server/core/ingestion/ChunkAccumulatorTest.java b/cameleer-server-core/src/test/java/com/cameleer/server/core/ingestion/ChunkAccumulatorTest.java index 2f1089d5..ecbea1f1 100644 --- a/cameleer-server-core/src/test/java/com/cameleer/server/core/ingestion/ChunkAccumulatorTest.java +++ b/cameleer-server-core/src/test/java/com/cameleer/server/core/ingestion/ChunkAccumulatorTest.java @@ -23,6 +23,7 @@ class ChunkAccumulatorTest { public Optional findByContentHash(String h) { return Optional.empty(); } public Optional findContentHashForRoute(String r, String a) { return Optional.empty(); } public Optional findContentHashForRouteByAgents(String r, List a) { return Optional.empty(); } + public Optional findLatestContentHashForAppRoute(String app, String r, String env) { return Optional.empty(); } public Map findProcessorRouteMapping(String app, String env) { return Map.of(); } };