Skip to content

WW-5832 Bound the FreeMarker template cache - #2090

Open
g0w6y wants to merge 1 commit into
apache:mainfrom
g0w6y:WW-5832-freemarker-template-cache-bound
Open

g0w6y wants to merge 1 commit into
apache:mainfrom
g0w6y:WW-5832-freemarker-template-cache-bound

Conversation

@g0w6y

@g0w6y g0w6y commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Fixes WW-5832

With struts.freemarker.mru.max.strong.size=0 (the default) FreemarkerManager leaves FreeMarker's template cache on its default SoftCacheStorage, which has no entry limit. The cache key includes the locale. For LocaleProvider actions (e.g. ActionSupport) FreemarkerResult.deduceLocale() returns the action's locale, which by default comes from request.getLocale() and is not validated unless struts.locale.validateRequestLocale is enabled (default false). Distinct request locales therefore add template cache entries without a count bound. Same shape as WW-5668 (text caches) and WW-5716 (Tiles locale caches).

The cost is retention and re-parsing, not an out-of-memory error: entries are soft references and are cleared under heap pressure, which also evicts the templates legitimate requests need, and every locale re-reads and re-parses the template.

Change

  • When mruMaxStrongSize is 0, install a SoftCacheStorage subclass that is emptied once it holds 10000 entries (the same cap as struts.i18n.cacheMaxSize). Entries stay soft references, and an evicted template is just loaded again on next use. The bound is approximate under concurrency.
  • Not soft:10000: that creates MruCacheStorage(0, 10000), which is not a ConcurrentCacheStorage, so TemplateCache would take a lock on every lookup; the default SoftCacheStorage avoids that (see WW-3766).
  • The user-configured path (mruMaxStrongSize > 0 -> strong:N) is unchanged.

10000 entries is templates x locales: core ships 131 theme templates (at most 45 in one theme), so an application would need on the order of a hundred locales across a few hundred templates to reach it.

How verified

FreemarkerManagerTest#testTemplateCacheIsBoundedByDefault loads one template under 10100 distinct locales and asserts the cache stays within the limit and the storage is still concurrent. With the bounded-storage branch removed it fails on the size assertion; with the change it passes. FreemarkerManagerTest (8), FreeMarkerResultTest (6) and I18nInterceptorTest (28) pass, and the full core suite runs 3626 tests with 0 failures and 2 skipped.

Also checked against a running Struts Boot app with an ActionSupport action rendering a FreeMarker template and 12,000 requests with 12,000 distinct Accept-Language headers: on main the template cache grew from 1 to 12,001 entries (DefaultSoftCacheStorage); with this change it is a BoundedSoftCacheStorage, emptied at 10000 and holding 2,002 entries after the same run. All 12,000 responses were correct and the server logged no errors.

Not changed

  • struts.locale.validateRequestLocale and its default, AcceptLanguageLocaleHandler, and the default.properties comment are untouched.
  • POJO actions (not LocaleProvider) use configuration.getLocale() in deduceLocale(); I did not test them.
  • The limit is not configurable; happy to add a constant if you want one.

With struts.freemarker.mru.max.strong.size=0 (default) the FreeMarker template
cache stayed on the default SoftCacheStorage, which has no entry limit. Its key
includes the locale, which for LocaleProvider actions derives from the request,
so distinct request locales grew it without a count bound.

Use a SoftCacheStorage that is emptied once it holds 10000 entries. It stays
concurrent, so lookups remain lock-free (unlike MruCacheStorage, which
TemplateCache synchronizes on). The strong:N path is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant