Skip to content

cocache-core ​

cocache-core implements the CoCache contracts without any Spring or Redis dependency. Its runtime dependencies are cocache-api, Caffeine (default L2), Spring Expression (SpEL keys), kotlin-logging, and CosId (client IDs). Guava is compile-only, used by BloomKeyFilter.

Package Overview ​

PackageKey TypesSource
me.ahoo.cacheTtlPolicy, CacheFactorycache/
me.ahoo.cache.consistencyCoherentCache, DefaultCoherentCache, CoherentCacheConfiguration, DefaultCoherentCacheFactory, InvalidationStamps (internal), LocalCacheEvictedEventBus, NoOpCacheEvictedEventBusconsistency/
me.ahoo.cache.concurrentSingleFlightconcurrent/
me.ahoo.cache.clientCaffeineClientSideCache, MapClientSideCache, DefaultClientSideCacheFactoryclient/
me.ahoo.cache.distributedInMemoryDistributedCache, DistributedCacheFactorydistributed/
me.ahoo.cache.converterToStringKeyConverter, ExpKeyConverter, DefaultKeyConverterFactoryconverter/
me.ahoo.cache.filterBloomKeyFilterfilter/
me.ahoo.cache.annotationCoCacheMetadata(Parser), JoinCacheMetadata(Parser)annotation/
me.ahoo.cache.proxyCacheInvocationHandler, DefaultCacheProxyFactory, CacheDelegated, CacheMetadataCapableproxy/
me.ahoo.cache.joinSimpleJoinCache, ExpJoinKeyExtractor, DefaultJoinCacheProxyFactoryjoin/
me.ahoo.cache.utilClientIdGenerator (UUID / host-based)util/

DefaultCoherentCache ​

mermaid
flowchart TD
    get["getCache(key)"] --> l2{"L2 hit,<br>not expired?"}
    l2 -->|yes| ret["return"]
    l2 -->|no| filter{"keyFilter.notExist?"}
    filter -->|yes| missing["return ttlPolicy.missing()"]
    filter -->|no| flight["SingleFlight.execute(cacheKey)"]
    flight --> stamp["stamp = stamps.current()"]
    stamp --> l1{"L1 hit?"}
    l1 -->|yes| fill["fillClientSide (stamp-guarded)"]
    l1 -->|no| load["cacheSource.load ?: missing"]
    load --> wb["writeBack L1 + L2 (stamp-guarded)"]

    style get fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style l2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ret fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style filter fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style missing fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style flight fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style stamp fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style l1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style fill fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style load fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style wb fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
MechanismWhat it guaranteesSource
SingleFlightOne L1 read + source load per key at a time within an instance. Followers get the leader's value or original exception. Same-key reentrancy fails fast.SingleFlight.kt
InvalidationStampsA write-back that overlaps any invalidation of its key (local evict/setCache, remote onEvicted, onReset) is skipped or undoneInvalidationStamps.kt
onResetClears L2 and invalidates all stamps when the channel (re)subscribesDefaultCoherentCache.kt
close()Idempotent: unregister from the bus, close L1DefaultCoherentCache.kt

CoherentCacheConfiguration ​

FieldDefaultDescription
cacheName--Event channel and logical name
clientId--Identifies self-published events
keyConverter--Business key → storage key
distributedCache--L1
clientSideCacheCaffeineClientSideCache.build()L2
cacheSourceCacheSource.noOp()L0
keyFilterKeyFilter.NO_OPExistence filter
ttlPolicyTtlPolicy() (3600 / 60 / 60)Value and negative-cache TTLs

TTL Policy ​

mermaid
graph LR
    V["set(key, value)"] --> P{"value == null?"}
    P -->|no| T["ttlAt = now + jitter(ttl, ttlAmplitude)"]
    P -->|yes| M["ttlAt = now + missingTtl<br>(MissingValue)"]
    T --> S["CacheStore"]
    M --> S

    style V fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style P fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style T fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style M fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style S fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

The jittered TTL is clamped to stay positive. TtlAt.FOREVER as ttl disables expiry. Time comes from CacheClock, a volatile epoch second refreshed every 100 ms by a daemon thread: System.currentTimeMillis() does not scale across threads on some platforms (e.g. macOS), and the hit path checks expiry on every read.

L2 Implementations ​

ClassNotesSource
CaffeineClientSideCacheDefault. build(maximumSize = 10_000, initialCapacity, expireAfterAccess); expired entries are evicted when read. No per-entry Expiry, which would write metadata on every read and stop hot keys from scalingCaffeineClientSideCache.kt
MapClientSideCacheUnbounded ConcurrentHashMap; tests or small fixed key setsMapClientSideCache.kt

Proxies and Join ​

  • CacheInvocationHandler serves @CoCache and @JoinCacheable proxies. It unwraps InvocationTargetException and implements identity equals/hashCode. See Proxy and Annotations.
  • SimpleJoinCache composes two Caches without owning their lifecycles. evict(key) evicts only the first cache, and a missing first value yields MissingValue.
mermaid
sequenceDiagram
autonumber
    participant App
    participant SJC as SimpleJoinCache
    participant F as firstCache
    participant J as joinCache
    App->>SJC: getCache(k)
    SJC->>F: getCache(k)
    alt MissingValue
        SJC-->>App: MissingValue
    else PresentValue
        SJC->>J: getCache(extract(v1))
        SJC-->>App: JoinValue (min ttlAt)
    end

Released under the Apache License 2.0.