Cache Invalidation
The Query Engine supports multiple invalidation strategies to ensure cached results remain consistent with the underlying data. Invalidation can be triggered by specific query, data dependency, tenant-wide flush, or schema change.
Invalidation Strategies
| Strategy | Endpoint | Scope | Use Case |
|---|---|---|---|
| Query-specific | POST /v1/cache/invalidate/query/{queryHash} | Single cached result | Known stale result |
| Dependency-based | POST /v1/cache/invalidate/dependency/{dependency} | All queries using a table | Table data updated |
| Tenant-wide | POST /v1/cache/invalidate/tenant | All tenant cache entries | Full cache reset |
| Schema change | POST /v1/cache/invalidate/schema-change | Queries touching affected tables | DDL changes |
Query-Specific Invalidation
Invalidate a single cached query result by its hash:
curl -X POST "http://query-engine:8080/v1/cache/invalidate/query/a3f2b9c1d4e5f6?reason=Data%20refreshed" \
-H "Authorization: Bearer $JWT_TOKEN"{
"status": "invalidated",
"queryHash": "a3f2b9c1d4e5f6",
"tenantId": "550e8400-e29b-41d4-a716-446655440000"
}Dependency-Based Invalidation
When a table's data changes, all cached queries that read from that table should be invalidated:
curl -X POST "http://query-engine:8080/v1/cache/invalidate/dependency/analytics.orders?reason=ETL%20completed" \
-H "Authorization: Bearer $JWT_TOKEN"The implementation tracks query-to-table dependencies in Redis sets:
public void invalidateByDependency(UUID tenantId, String dependency) {
String dependencyKey = buildDependencyKey(tenantId, dependency);
Set<String> cacheKeys = redisTemplate.opsForSet().members(dependencyKey);
if (cacheKeys != null && !cacheKeys.isEmpty()) {
for (String cacheKey : cacheKeys) {
invalidateByKey(cacheKey);
}
redisTemplate.delete(dependencyKey);
}
}When a query result is cached, its table dependencies are recorded:
private void trackDependencies(UUID tenantId, String cacheKey, Set<String> dependencies) {
for (String dependency : dependencies) {
String dependencyKey = buildDependencyKey(tenantId, dependency);
redisTemplate.opsForSet().add(dependencyKey, cacheKey);
redisTemplate.expire(dependencyKey, cacheConfig.getL2().getTtl().multipliedBy(2));
}
}Tenant-Wide Invalidation
Flush all cache entries for a tenant (requires ADMIN or QUERY_ADMIN role):
curl -X POST "http://query-engine:8080/v1/cache/invalidate/tenant?reason=Configuration%20change" \
-H "Authorization: Bearer $JWT_TOKEN"The implementation scans for all Redis keys matching the tenant pattern:
public void invalidateTenant(UUID tenantId) {
String pattern = cacheConfig.getL2().getKeyPrefix() + tenantId + ":*";
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
// Also clear L1 entries for this tenant
if (cacheConfig.getL1().isEnabled()) {
l1Cache.asMap().keySet().stream()
.filter(k -> k.contains(tenantId.toString()))
.forEach(l1Cache::invalidate);
}
}Schema Change Invalidation
When DDL operations change table schemas, all cached queries touching those tables must be invalidated:
curl -X POST http://query-engine:8080/v1/cache/invalidate/schema-change \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JWT_TOKEN" \
-d '{"affectedTables": ["analytics.orders", "analytics.order_items"]}'{
"status": "invalidated",
"affectedTables": ["analytics.orders", "analytics.order_items"],
"tenantId": "550e8400-e29b-41d4-a716-446655440000"
}Event-Based Invalidation
The cache supports automatic invalidation via Kafka events. When the CacheInvalidationListener receives events on the query-cache-invalidation topic, it triggers the appropriate invalidation:
query:
cache:
invalidation:
dependency-based-enabled: true
event-based-enabled: true
event-topic: "query-cache-invalidation"
batch-size: 100Invalidation Across Both Levels
Every invalidation operation removes entries from both L1 and L2:
private void invalidateByKey(String cacheKey) {
if (cacheConfig.getL1().isEnabled()) {
l1Cache.invalidate(cacheKey);
}
if (cacheConfig.getL2().isEnabled()) {
redisTemplate.delete(cacheKey);
}
meterRegistry.counter("query.cache.invalidate").increment();
}