Commits
Tarun Sukhani authored cde04398dcb
feat: typed Cache contract + memcached removal (PF-88, release 1.12.39)
Replaces the 2009-era memcached-centric play.cache facade with a typed,
dependency-inverted Cache architecture loaded via Java's ServiceLoader.
Framework code now references only abstractions; concrete implementations
live in their own packages and ship via SPI.
Architecture
* play.cache (framework, abstractions only, zero compile dependency on
Caffeine or any specific backend):
- Cache<K, V> — typed runtime contract: getIfPresent, get-with-loader,
put, invalidate, invalidateAll, estimatedSize, stats.
- CacheConfig — immutable builder: expireAfterWrite, expireAfterAccess,
maximumSize, recordStats.
- CacheStats — record (hitCount, missCount, loadCount, evictionCount,
hitRate).
- CacheProvider — SPI: name, create(name, config), bindMetricsToRegistry,
stop. Default bindMetricsToRegistry is no-op for providers without
per-cache metrics.
- Caches — public registry: ServiceLoader resolution at boot, named()
is computeIfAbsent per name, installProvider() for tests, lifecycle
init/stop, invalidateAll for dev-mode hot reload.
* play.cache.caffeine (separate package, ships with the framework):
- CaffeineCacheProvider implements CacheProvider, name="caffeine".
- CaffeineCache<K, V> wraps com.github.benmanes.caffeine.cache.Cache.
- META-INF/services/play.cache.CacheProvider registers via SPI.
Provider selection (Caches.selectProvider, package-private for test):
* Zero providers → ConfigurationException at boot.
* One provider → use it.
* Multiple → require cache.provider=<name> in application.conf; mismatch
fails fast with diagnostic listing all registered names.
Re-implemented user-surface features on the new contract
* @CacheFor — ActionInvoker.actionCache(ttl) → Caches.named("play.actions."
+ ttl, CacheConfig with matching expireAfterWrite). One backing cache
per distinct TTL string so each cache's expireAfterWrite matches the
declared duration (Caffeine eviction is per-cache, not per-entry). Drop
the Serializable constraint inherited from memcached. Annotation
surface unchanged.
* #{cache for:'10m'} — FastTags.fragmentCache(ttl) → Caches.named(
"play.fragments." + ttl, ...). Same pattern. Tag surface unchanged.
Deletions
* play.cache.MemcachedImpl (entire ~270-line class).
* play.cache.CaffeineImpl (replaced by CaffeineCache + Provider).
* play.cache.CacheImpl (replaced by CacheProvider returning typed Cache).
* play.cache.Cache static facade (set/get/add/replace/incr/decr/safe*/
Object overloads — all gone). Hard break per ticket; no compatibility
shim.
* play.cache.checkSerializable.
* spymemcached-2.12.3.jar from framework/lib.
* net.spy -> spymemcached entry from framework/dependencies.yml.
* memcached=/memcached.host= keys from application-skel application.conf.
* testrunner /@tests/cache HTTP debug action (only legacy facade
consumer outside the framework itself).
* Old CacheTest + CaffeineImplTest (replaced by new typed-contract tests).
Migrated framework consumers
* Play.start/stop — Cache.init/stop → Caches.init/stop.
* ApplicationClassloader hot-reload — Cache.clear → Caches.invalidateAll.
* MetricsPlugin — bindFrameworkCacheMetrics → bindCacheMetrics; dispatches
to provider.bindMetricsToRegistry so cache binding is now provider-
agnostic at the plugin layer. CaffeineCacheProvider attaches Micrometer's
CaffeineCacheMetrics for every created cache, tagged with the registered
name.
New tests
* CachesTest — registry idempotency (named returns same instance per
name), invalidateAll, named-before-init fails loudly, installProvider
stops the previous provider.
* CachesProviderResolutionTest — drives selectProvider directly with
fabricated candidate lists: zero / single / multiple-without-config /
multiple-with-matching-config / multiple-with-mismatched-config.
* CaffeineCacheTest — putAndGetIfPresent, get-with-loader (loader runs
once on miss), expireAfterWrite TTL, maximumSize bounds (with
cleanUp() to drain Caffeine's async maintenance), recordStats hit/miss/
load tracking, zero-stats snapshot when recording disabled,
bindMetricsToRegistry attaches every created cache.
Docs
* documentation/manual/cache.textile — rewritten end-to-end on the new
API. Documents Caches.named, CacheConfig, the SPI extension point for
alternate backends, the test-override path, and the migrated @CacheFor
/ #{cache} surfaces.
* documentation/manual/configuration.textile — memcached section
replaced with cache.provider config key + cross-link to the cache page.
* documentation/manual/releases/release1.12.x/releasenotes-1.12.0.textile
— adds h4 1.12.39 PF-88 subsection summarizing the contract change,
what's deleted, and the SPI extension story.
Build
* framework/build.xml — copy fileset extended to include
META-INF/services/** so the SPI registration ships in the jar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>