[ 
https://issues.apache.org/jira/browse/WW-5687?focusedWorklogId=1041209&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1041209
 ]

ASF GitHub Bot logged work on WW-5687:
--------------------------------------

                Author: ASF GitHub Bot
            Created on: 13/Sep/26 15:06
            Start Date: 13/Sep/26 15:06
    Worklog Time Spent: 10m 
      Work Description: lukaszlenart opened a new pull request, #1930:
URL: https://github.com/apache/struts/pull/1930

   Fixes [WW-5687](https://issues.apache.org/jira/browse/WW-5687)
   
   Follow-on of WW-5537, defence-in-depth: `StrutsTypeConverterHolder` 
(`defaultMappings`, `mappings`, `unknownMappings`) and 
`DefaultActionValidatorManager` (`validatorCache`, `validatorFileCache`) hold 
application `Class` objects and converter/validator instances. They are now 
cleared during `Dispatcher.cleanup()` together with the eight existing 
`InternalDestroyable` beans.
   
   ## Why adapters instead of a second `<bean>` line
   
   The container keys singletons by `(type, name)`, so registering the holder 
class a second time under `InternalDestroyable` builds a second, empty instance 
— `destroyInternalBeans()` would clear nothing (see the ticket for the identity 
probe). The existing dual-registered destroyables get away with it only because 
what they clear is `static`.
   
   So this adds:
   
   - `default void clearCache() {}` on `TypeConverterHolder` and 
`ActionValidatorManager` — no-op for third-party implementations, overridden in 
`StrutsTypeConverterHolder` and `DefaultActionValidatorManager` 
(`AnnotationActionValidatorManager`, the default binding, inherits it).
   - `TypeConverterHolderDestroyable` / `ActionValidatorManagerDestroyable` — 
`@Inject` the bare type, which the `StrutsBeanSelectionProvider` alias resolves 
to the singleton actually in use (a custom `struts.actionValidatorManager` / 
`struts.converter.holder` binding is covered), and call `clearCache()`.
   
   ## Tests
   
   `DispatcherCleanupTest` gains two tests that prime the caches on 
`container.getInstance(...)` — the same instance the framework injects — and 
assert they are empty after `cleanup()`. Both fail under the naive dual 
registration, which is the property the ticket asks for. Discovery test 
extended with the two new bean names. Full `core` suite: 3322 tests, 0 failures.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)




Issue Time Tracking
-------------------

            Worklog Id:     (was: 1041209)
    Remaining Estimate: 0h
            Time Spent: 10m

> Clear the conversion and validator caches on Dispatcher.cleanup() (WW-5537 
> defence-in-depth follow-on)
> ------------------------------------------------------------------------------------------------------
>
>                 Key: WW-5687
>                 URL: https://issues.apache.org/jira/browse/WW-5687
>             Project: Struts 2
>          Issue Type: Improvement
>          Components: Core
>            Reporter: Lukasz Lenart
>            Priority: Minor
>             Fix For: 7.4.0
>
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> Follow-on from WW-5537 (Closed/Fixed, shipped 6.9.0 and 7.2.1), which built 
> the {{InternalDestroyable}} cleanup framework. This item was identified 
> during that work but never filed, and is still not on {{main}} as of 
> 2026-08-23.
> h3. What it is, and what it is not
> {{StrutsTypeConverterHolder}} (fields {{mappings}}, {{defaultMappings}}, 
> {{unknownMappingsInternal}}) and the {{ActionValidatorManager}} 
> implementations (fields {{validatorCache}}, {{validatorFileCache}}) hold 
> strong references to application {{Class}} objects and to converter/validator 
> instances.
> These are container-scoped singletons with no static, thread, or otherwise 
> external references — they are reachable only through the container and are 
> collected together with it. They therefore do _not_ independently pin the 
> webapp classloader; that is determined by whatever retains the container, 
> which WW-5537 already addressed.
> _This is not a leak fix._ It is defence-in-depth, consistent with the other 
> destroyables: clearing these caches during {{Dispatcher.cleanup()}} bounds 
> retained heap in the window before the container graph is collected, and 
> costs nothing if it already has been.
> h3. The registration trap
> The obvious implementation — make the two classes implement 
> {{InternalDestroyable}} and add a second {{<bean 
> type="...InternalDestroyable" .../>}} line for each — _does not work_, and 
> this is the reason the item is worth a ticket rather than a five-line patch.
> The Struts container keys factories by (type, name), so declaring the same 
> class under two bean types produces two independent singletons. Verified 
> empirically against {{main}}:
> {code}FileManager(system)   = 1910936570 | 
> InternalDestroyable(defaultFileManager)   = 807322507 | SAME? false
> RootAccessor(struts)  = 1495608502 | 
> InternalDestroyable(compoundRootAccessor) = 835773569 | SAME? false
> {code}
> {{Dispatcher.destroyInternalBeans()}} discovers destroyables with 
> {{container.getInstanceNames(InternalDestroyable.class)}} and then 
> {{container.getInstance(InternalDestroyable.class, name)}} — so it would 
> receive a freshly built holder whose caches are empty, and clear nothing.
> The existing dual-registered destroyables are unaffected by this, because 
> what they clear is static: {{DefaultFileManager.clearCache()}} and 
> {{CompoundRootAccessor.clearCache()}} are both {{static}} and clear 
> {{static}} fields, so any instance will do. The caches in this ticket are 
> per-instance, which is what breaks the pattern.
> h3. Suggested implementation
> Follow the adapter pattern already used for the other six destroyables 
> ({{ComponentCacheDestroyable}}, {{OgnlCacheDestroyable}}, 
> {{ScopeInterceptorCacheDestroyable}}, {{FreemarkerCacheDestroyable}}, 
> {{DebugUtilsCacheDestroyable}}, {{FinalizableReferenceQueueDestroyable}}): 
> add small {{InternalDestroyable}} beans that resolve the _real_ singleton 
> from the container and clear it, rather than making the holders themselves 
> the destroyable.
> Note that {{AnnotationActionValidatorManager extends 
> DefaultActionValidatorManager}} and is the default binding 
> ({{name="struts"}}); {{DefaultActionValidatorManager}} is bound separately as 
> {{name="no-annotations"}}. Whatever is done must cover the instance actually 
> in use, not just the base class.
> h3. Verification
> A test that asserts the caches are non-empty before {{Dispatcher.cleanup()}} 
> and empty afterwards — and, importantly, that fails if the destroyable clears 
> a different instance than the one the framework uses. A naive registration 
> passes any test that only checks "destroy() was called".
> h3. Provenance
> The original plan text lived only in unpushed local commit {{8019e6994}} 
> ({{docs/superpowers/plans/2026-03-23-WW-5537-classloader-leak-fixes.md}}, 
> Task 5b), on a branch deleted 2026-08-23. Its Step 3 prescribed the naive 
> dual registration described above, which the identity probe shows would be a 
> no-op — that correction is the main thing this ticket adds over the original 
> note.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to