Visit the github wiki for more information.
A lib that regulates the cache-based requests an application receives in terms of request-collapsing.
- Java 8 or later if you are a user.
- Kotlin 1.8 or later if you are a user.
- (If link this lib with Spring) Spring Boot 2.7 (Spring Framework 5.3) or later if you are a user.
Choose a module based on your platform (there is a detailed explanation in the wiki):
implementation("com.linecorp.cse.reqshield:core:{version}")
implementation("com.linecorp.cse.reqshield:core-reactor:{version}")
implementation("com.linecorp.cse.reqshield:core-kotlin-coroutine:{version}")
implementation("com.linecorp.cse.reqshield:core-spring:{version}")
implementation("com.linecorp.cse.reqshield:core-spring-webflux:{version}")
implementation("com.linecorp.cse.reqshield:core-spring-webflux-kotlin-coroutine:{version}")
Release versions are available from Maven Central. Add Maven Central and use a release version such as 1.0.0:
repositories {
mavenCentral()
}
dependencies {
implementation("com.linecorp.cse.reqshield:core:1.0.0")
}Dev versions are published as Maven snapshots and require the Maven Central Snapshot repository. Use a snapshot version
such as 1.0.0-SNAPSHOT:
repositories {
mavenCentral()
maven {
name = "MavenCentralSnapshots"
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
mavenContent {
snapshotsOnly()
}
}
}
dependencies {
implementation("com.linecorp.cse.reqshield:core:1.0.0-SNAPSHOT")
}Snapshot versions can be updated without changing the version string. If Gradle continues to use a cached snapshot,
refresh dependencies with ./gradlew build --refresh-dependencies.
- Redis-backed integration tests using Testcontainers always run as part of module test tasks.
- Requirements:
- A working local Docker daemon with network access to pull
redis:6.2.7-alpineon first run. - Sufficient permissions to start containers from tests.
- A working local Docker daemon with network access to pull
- If you need to temporarily bypass Redis ITs locally (e.g., no Docker), run specific unit-test-only tasks or exclude the example modules when invoking Gradle.
@ReqShieldCacheable(nullHandling = ...)controls hownullvalues are emitted in WebFlux:EMIT_EMPTY(default): mapnulltoMono.empty().ERROR: throw anIllegalStateExceptionif anullvalue is produced.
- The Spring adapters store every entry under
"{cacheName}::{key}"(the same convention as Spring'sRedisCacheManager), wherekeyis the SpEL result or theKeyGeneratoroutput.@ReqShieldCacheEvictapplies the same rule, so an evict with the samecacheNameandkeyalways targets the entry written by@ReqShieldCacheable. - Lock keys are derived from that namespaced key and prefixed with
reqshield:lock:, so two caches that happen to use the same raw key never share a lock or an entry. - If you call
ReqShielddirectly (core / core-reactor / core-kotlin-coroutine), the key you pass is used as is.
- When
isLocalLock = false, your cache bean must also implement the module'sGlobalLockSupportinterface (com.linecorp.cse.reqshield.spring.cache.GlobalLockSupport,...spring.webflux.cache.GlobalLockSupport,...spring.webflux.kotlin.coroutine.cache.GlobalLockSupport). If it does not, the first call to the annotated method fails with anIllegalArgumentExceptioninstead of silently running without request collapsing. - Every lock acquisition carries an ownership token. Only the holder that acquired the lock can release it, so a slow holder whose lock already expired can no longer release the lock of the next holder.
- Recommended Redis implementation:
- Lock:
SET {lockKey} {token} NX PX {ttlMillis}(atomic; neverSETNXfollowed by a separatePEXPIRE) - Unlock: compare-and-delete in a Lua script, e.g.
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
- Lock:
- The example modules contain working implementations for
RedisTemplate,ReactiveRedisTemplateand the coroutine extensions.
-
The local lock (
isLocalLock = true, the default) keeps one entry per(cache key, lock type)it is currently locking, in a map shared by the whole JVM. A cache key that is both created and refreshed therefore uses up to two entries. -
An entry is dropped as soon as the lock is released; an entry whose holder never released it is dropped by the cleanup monitor once
lockTimeoutMillishas passed, which runs on a 1 s interval and so can lag by up to that much. The map's size therefore tracks how many keys are being locked at the same time, not how many distinct keys the application sees. A service with 50 ms of supplier latency at 10k rps holds roughly 500 locks at once. -
The map is uncapped by default. To bound it, set
req-shield.lock.max-entries;0(the default) means uncapped. Size it against concurrent lock ownership, not key cardinality.# Spring modules: read from the Environment, so application.yml works req-shield: lock: max-entries: 2000
# core / core-reactor / core-kotlin-coroutine used directly: system property java -Dreq-shield.lock.max-entries=2000 -jar app.jarThe Spring modules read the same key from the
Environment, so a-Doverride still outranks the yml entry there, andREQ_SHIELD_LOCK_MAX_ENTRIESworks as an environment variable. -
Once the map is full, a request for a new key is handed a permit that no map entry backs. It then runs exactly as if it had taken the lock - it calls the supplier and writes the cache - so what the cap costs is request collapsing for that key, and nothing else: no added latency, and the cache still gets populated. Keys whose entry is already in the map are not subject to the cap at all.
-
The cap is a soft one. The size check is an estimate and is not atomic with the insertion it guards, so concurrent callers can push the map slightly past the configured number.
-
The first refusal and every 1000th after it are logged at WARN.
-
A value that cannot be read as a non-negative number is logged at WARN and ignored, leaving the current cap unchanged. This is deliberate in both directions: a class initializer that throws would poison the library for the whole JVM, and an application context should not fail to start over a tuning knob typo. Setting the cap programmatically (
LocalLockLimit.maxEntries = -1) still fails fast, because there the stack trace is actionable.
@ReqShieldCacheEvictevicts after the annotated method completes successfully (Spring's@CacheEvictdefault). If the method throws, nothing is evicted; if the eviction itself fails, that failure propagates to the caller.- In WebFlux the eviction also runs when the method completes empty (for example a
Mono<Void>handler).
- A request that loses the lock polls the cache every 50 ms, up to
maxAttemptGetCachetimes (default 60). - A cache read failure while polling is logged and counted as a failed attempt. Three consecutive failures are treated as a cache outage and the request falls back to calling the supplier itself right away.
- When the attempts are exhausted the request calls the supplier once. A supplier failure is propagated as a
ClientException(SUPPLIER_ERROR)with the original exception ascause; it is never turned into a cachednull.
coreaccepts anyjava.util.concurrent.Executorfor its background cache writes; onlyexecuteis called and the library never shuts a caller-supplied pool down. The default is a shared daemon pool; the Spring adapter exposes it as thereqShieldExecutorbean (anExecutorServicethe context shuts down), which you can override.core-reactoraccepts aScheduler(defaultboundedElastic). The Spring WebFlux adapter exposes it as thereqShieldSchedulerbean.core-kotlin-coroutineaccepts aCoroutineScopefor background cache writes (default: a shared supervisor scope onDispatchers.IO). The coroutine Spring adapter exposes it as thereqShieldCoroutineScopebean and cancels it on context shutdown.
@ReqShieldCacheable/@ReqShieldCacheEvictfromcore-spring-webflux-kotlin-coroutinerequiresuspendfunctions. Annotating a regular function fails fast with anIllegalArgumentException; usecore-springorcore-spring-webfluxfor blocking orMono-returning methods.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Please see CONTRIBUTING.md for contributing to Req-Shield.
Apache License 2.0
- File an issue in the issue tracker to report a bug or suggest an idea.