CapacityEvictionWarning.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.util;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.RemovalListener;
import org.slf4j.Logger;
import java.time.Duration;
import java.util.Objects;
/**
* Reports capacity eviction on a size- or weight-capped Caffeine cache whose entries are minted on an
* anonymous path. {@code maximumSize}, {@code maximumWeight} or an {@link OldestFirstBoundedCache} bounds the
* heap; this class makes the bound observable: a {@link RemovalCause#SIZE} eviction emits one WARN through the
* owning cache's logger, at most once per minute via {@link LogThrottle}, because under a flood an unthrottled
* line per eviction would trade the heap bound for log amplification. Every other cause
* — ordinary expiry above all — stays silent, or normal traffic (abandoned logins timing out) would
* raise false flood alarms.
*
* <p>Owners pass their own logger so the warning is attributed to the cache it concerns and so a
* log-capturing test can attach to the owner class.
*/
public final class CapacityEvictionWarning {
private static final Duration WARN_INTERVAL = Duration.ofMinutes(1);
private final Logger log;
private final String cacheDescription;
private final String capacity;
private final LogThrottle throttle;
/** {@code capacity} describes the cap in the WARN, e.g. {@code "50000 entries"} or {@code "100 MB"}. */
public CapacityEvictionWarning(Logger log, String cacheDescription, String capacity) {
this(log, cacheDescription, capacity, new LogThrottle(WARN_INTERVAL));
}
CapacityEvictionWarning(Logger log, String cacheDescription, String capacity, LogThrottle throttle) {
this.log = Objects.requireNonNull(log, "log");
this.cacheDescription = Objects.requireNonNull(cacheDescription, "cacheDescription");
this.capacity = Objects.requireNonNull(capacity, "capacity");
this.throttle = Objects.requireNonNull(throttle, "throttle");
}
/** Listener for {@code Caffeine.evictionListener(...)}; only the removal cause is inspected. */
public <K, V> RemovalListener<K, V> listener() {
return (key, value, cause) -> onEviction(cause);
}
/** Called by the listener, or by a bound that evicts on its own, such as {@link OldestFirstBoundedCache}. */
public void onEviction(RemovalCause cause) {
// Guard order matters: tryAcquire consumes the interval token (see its Javadoc).
if (cause == RemovalCause.SIZE && throttle.tryAcquire()) {
log.warn("{} cache is evicting at capacity ({}) — possible flood on its write path",
cacheDescription, capacity);
}
}
}