Skip to content

Migrate concurrent linked map cache to Caffeine - #15961

Open
jamesfredley wants to merge 1 commit into
8.0.xfrom
feat/caffeine-cache-migration-seed
Open

Migrate concurrent linked map cache to Caffeine#15961
jamesfredley wants to merge 1 commit into
8.0.xfrom
feat/caffeine-cache-migration-seed

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

Description

What was found

Problem Impact
Cache module still uses concurrentlinkedhashmap-lru Older dependency, less maintained
Codebase 3 recommended Caffeine migration No current PR
Public cache API must stay stable Internals can move first

What changed

Area Change
Implementation GrailsConcurrentLinkedMapCache internals use Caffeine
Manager Keep public manager API
Dependency Prefer Caffeine over concurrentlinkedhashmap-lru path
Docs / tests Cache config note + behavioral Spock coverage

Out of scope / follow-up

Topic Status
Renaming public classes away from ConcurrentLinkedMap Later
Full Spring Cache abstraction rewrite Not this PR
Benchmark suite vs old LRU Follow-up

Related MD topics

Source Topic
Codebase 3 Caffeine migration from concurrentlinkedhashmap-lru

Contributor Checklist

Issue and Scope

  • Dependency modernization starter.
  • Public cache API preserved.
  • Single focused migration seed.
  • Targets 8.0.x.

Code Quality

  • Tests updated.
  • Focused cache tests run.
  • No mass reformatting.
  • AI starting point labeled.

Licensing and Attribution

  • Apache License 2.0.
  • Contributor rights confirmed.
  • ai-generated-starting-point label applied.

Documentation

  • Cache docs updated.
  • Scope explained.

Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]

Replace concurrentlinkedhashmap-lru internals with Caffeine while keeping public cache API.

Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request migrates the GrailsConcurrentLinkedMapCache implementation from concurrentlinkedhashmap-lru to Caffeine, aiming to modernize the cache internals while keeping the existing public cache manager API stable for Grails applications.

Changes:

  • Replaced the cache’s internal store with a Caffeine Cache and exposed its asMap() view as the ConcurrentMap native cache.
  • Switched the grails-cache module dependency from concurrentlinkedhashmap-lru to com.github.ben-manes.caffeine:caffeine.
  • Updated cache documentation and migrated the cache unit test from JUnit-style assertions to Spock.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
grails-doc/src/en/guide/cache/cacheUsage/cacheConfiguration.adoc Updates cache configuration documentation to describe the Caffeine-backed bounded cache manager.
grails-cache/src/test/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheTests.groovy Rewrites tests as Spock specs and adjusts expectations for the new eviction behavior.
grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCacheManager.groovy Updates class-level documentation to reflect the new bounded-cache intent.
grails-cache/src/main/groovy/grails/plugin/cache/GrailsConcurrentLinkedMapCache.java Implements Caffeine-backed storage and updates capacity/size/hottest-keys behavior accordingly.
grails-cache/build.gradle Replaces the old LRU cache dependency with Caffeine.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

grails.cache.enabled,`true`,Whether to enable the plugin
grails.cache.clearAtStartup,`false`,Whether to clear all caches at startup
grails.cache.cacheManager,GrailsConcurrentMapCacheManager,Cache Manager to use. Default cache manager uses Spring Frameworks ConcurrentMapCache which might grow limitless. If you cannot predict how many cache entries you are going to generate use "GrailsConcurrentLinkedMapCacheManager" instead which uses com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap and limits by default to 10000 entries per cache.
grails.cache.cacheManager,GrailsConcurrentMapCacheManager,Cache Manager to use. Default cache manager uses Spring Frameworks ConcurrentMapCache which might grow limitless. If you cannot predict how many cache entries you are going to generate use "GrailsConcurrentLinkedMapCacheManager" instead. In Grails 8.1 this manager keeps the public Grails cache API while using Caffeine-backed bounded caches; the old concurrentlinkedhashmap-lru dependency path is deprecated.
Comment on lines +58 to +60
then:
cache.nativeCache.get('key') == 'value'
cache.nativeCache.getClass().name.startsWith('com.github.benmanes.caffeine.cache.')
Comment on lines +123 to 126
when:
cache.get(1)
cache.get(2)

@testlens-app

testlens-app Bot commented Jul 10, 2026

Copy link
Copy Markdown

🚨 TestLens detected 1 failed test 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

CI - Groovy Joint Validation Build / build_grails > :grails-cache:test

Test Runs Flakiness
GrailsConcurrentLinkedMapCacheTests > returns hottest keys from cache eviction policy 0% 🟢

🏷️ Commit: e01d6d8
▶️ Tests: 6736 executed
⚪️ Checks: 60/60 completed

Test Failures

GrailsConcurrentLinkedMapCacheTests > returns hottest keys from cache eviction policy (:grails-cache:test in CI - Groovy Joint Validation Build / build_grails)
Condition not satisfied:

cache.hottestKeys.contains(2)
|     |           |
|     |           false
|     [12, 11, 10, 9, 8, 7, 6, 5, 4, 18]
<grails.plugin.cache.GrailsConcurrentLinkedMapCache@5715f434 name=inaccessible capacity=inaccessible caffeineStore=inaccessible store=inaccessible allowNullValues=inaccessible>

	at grails.plugin.cache.GrailsConcurrentLinkedMapCacheTests.returns hottest keys from cache eviction policy(GrailsConcurrentLinkedMapCacheTests.groovy:136)

Muted Tests

Select tests to mute in this pull request:

  • GrailsConcurrentLinkedMapCacheTests > returns hottest keys from cache eviction policy

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

Learn more about TestLens at testlens.app.

@jdaugherty

Copy link
Copy Markdown
Contributor

@jamesfredley have you discussed this with @matrei? I thought I remember him doing something similar here (#15091)

@jdaugherty

Copy link
Copy Markdown
Contributor

This PR represents an architectural shift that was not discussed before being submitted. I do not think it should proceed until we've discussed it as a team.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 19.5852%. Comparing base (b00e83e) to head (e01d6d8).
⚠️ Report is 8 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                 Coverage Diff                 @@
##                8.0.x     #15961         +/-   ##
===================================================
- Coverage     49.5403%   19.5852%   -29.9550%     
+ Complexity      16922        308      -16614     
===================================================
  Files            1999         61       -1938     
  Lines           93754       3472      -90282     
  Branches        16420        601      -15819     
===================================================
- Hits            46446        680      -45766     
+ Misses          40145       2662      -37483     
+ Partials         7163        130       -7033     

see 1939 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Good call to check - I looked at #15091, and it's complementary rather than overlapping.

#15091 (@matrei, "fix(deps): clarify and update usage of caffeine") is dependency cleanup: it removes unused direct Caffeine deps from grails-core / grails-data-simple and adds explanatory comments in the modules that do use Caffeine (grails-web-common, grails-web-url-mappings). It changes no files in grails-cache and doesn't touch GrailsConcurrentLinkedMapCache, concurrentlinkedhashmap-lru, or the cache implementation. So it did not perform this migration - #15961 is the functional swap of the cache internals. The two just need dependency-resolution coordination once both are in flight.

On the "architectural shift, not discussed as a team" point - fair, and I'll take it to the dev list / weekly before proceeding rather than merging unilaterally. For grounding, the migration is a documented codebase-improvement recommendation (pre-release review, Codebase 3):

  • concurrentlinkedhashmap-lru is 1.4.2 (2015, archived), and its own author built Caffeine as the successor.
  • It's used only by grails-cache's GrailsConcurrentLinkedMapCache.
  • Caffeine is Spring's preferred cache provider, so migrating aligns grails-cache with the Boot 4 ecosystem, and lets us deprecate/remove the archived dependency.

The plan I'd bring to the discussion: swap the internals of GrailsConcurrentLinkedMapCache to Caffeine behind the existing cache API, add behavioral tests, and stage the old dependency for removal - keeping public behavior stable. I'll hold merge until we've talked it through as a team.

@jamesfredley jamesfredley moved this to Todo in Apache Grails Jul 24, 2026
@borinquenkid borinquenkid added this to the grails:9.0.0-M1 milestone Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants