[
https://issues.apache.org/jira/browse/JCS-248?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18108844#comment-18108844
]
Thomas Buffagni commented on JCS-248:
-------------------------------------
Thanks for the follow-up work on JCS-248. I reviewed the second lifecycle
change and I think it addresses the main ownership issue much more cleanly,
especially for {{ElementEventQueue}} and shared pools.
While looking at the new reference-counting logic, I noticed a few edge cases
that may be worth checking. I may of course be missing some intended lifecycle
constraints.
*1. Possible reference-count underflow*
In {{{}disposeExecutorService(){}}}, {{poolUseCounts.computeIfAbsent()}}
creates a counter with value {{0}} even if no corresponding acquisition exists,
and it is then immediately decremented.
So a sequence such as:
{{dispose("X") -> count becomes -1
get("X") -> count becomes 0, executor is created
dispose("X") -> count becomes -1}}
would appear to leave the executor alive rather than shutting it down.
A similar situation might occur if a component performs its local dispose after
the global {{ThreadPoolManager.dispose()}} has already removed the counters.
Would it be safer for the release path to use {{poolUseCounts.get()}} and
simply ignore/log an unmatched release instead of creating a new counter?
*2. Reference count is incremented before executor creation succeeds*
{{getExecutorService()}} increments the use count before {{computeIfAbsent()}}
creates the executor.
If pool creation throws for any reason, the caller never receives an executor,
but the reference count has already been incremented. A subsequent successful
acquisition could therefore inherit a phantom reference and require an
additional release before the pool is actually stopped.
It might be safer to increment the reference count only after the executor has
been successfully obtained.
*3. {{getExecutorService()}} now effectively has acquire/release semantics*
With reference counting, every call to {{getExecutorService()}} now represents
an acquisition that must eventually be balanced by
{{{}disposeExecutorService(){}}}.
That makes sense for shared pools, but it is slightly different from the
previous "getter" semantics and may be worth documenting explicitly, or perhaps
even expressing through acquire/release terminology, to avoid accidental
unbalanced calls in future code.
One additional concurrency detail: {{disposeExecutorService()}} is synchronized
and performs {{awaitTermination()}} while holding the {{ThreadPoolManager}}
monitor. If a terminating worker needs to obtain another pool, it could block
waiting for the same monitor while the disposing thread is waiting for that
worker to terminate. The timeout prevents a permanent deadlock, but it could
still block the whole pool manager until the timeout expires.
None of these points changes the fact that the second patch looks like a
significant improvement over the first lifecycle implementation. I just wanted
to highlight these scenarios in case they are useful for additional tests or
hardening.
> ElementEventQueue.dispose() does not shut down its owned executor, leaking
> threads across web application redeployments
> -----------------------------------------------------------------------------------------------------------------------
>
> Key: JCS-248
> URL: https://issues.apache.org/jira/browse/JCS-248
> Project: Commons JCS
> Issue Type: Bug
> Components: Composite Cache
> Affects Versions: jcs-3.2.1, jcs-4.0
> Environment: Apache Commons JCS 3.2.1; Apache Tomcat 11.0.24; Eclipse
> Temurin JDK 25; Spring web application packaged as a WAR; local
> non-distributed cache.
> Reporter: Thomas Buffagni
> Assignee: Thomas Vandahl
> Priority: Major
> Labels: thread-leak,, tomcat,, webapp-lifecycle
> Fix For: jcs-4.0
>
>
> *How the issue was discovered*
> The issue was discovered while running a Tomcat web-application lifecycle
> benchmark. The benchmark repeatedly performs the following sequence:
> 1. Deploy the Spring WAR.
> 2. Initialize and exercise the local JCS cache.
> 3. Stop the Spring application context and invoke JCS.shutdown().
> 4. Undeploy the WAR from Tomcat.
> 5. Deploy it again and repeat the sequence.
> During WAR undeployment, Tomcat reported that threads created by the web
> application had not been stopped. The threads named in the warnings were
> JCS-ElementEventQueue-* workers.
> Inspection of the JVM after undeployment confirmed that two
> JCS-ElementEventQueue-* worker threads remained alive even though the
> application had been stopped and JCS.shutdown() had been invoked.
> Repeating the deploy/undeploy cycle caused additional worker threads to
> accumulate and produced further Tomcat thread-leak warnings. After five
> lifecycle cycles, ten warnings had been recorded and the number of live
> threads showed an estimated growth of 2.3 threads per cycle.
> This undeployment behavior led to the inspection of
> ElementEventQueue.dispose() and to the identification of the executor
> lifecycle problem described below.
> *Problem*
> ElementEventQueue creates its own executor by calling:
> ThreadPoolManager.getInstance().createPool(...)
> The returned executor is not registered in the ThreadPoolManager internal
> pool maps. Consequently, ThreadPoolManager.dispose() cannot shut it down.
> ElementEventQueue.dispose() sets the destroyed flag, but the
> queueProcessor.shutdownNow() call is commented out. When JCS is used inside a
> Tomcat web application, the executor threads survive application undeployment.
> The issue was reproduced at runtime with JCS 3.2.1. Code inspection confirms
> that the same lifecycle problem is present in the current JCS 4.0.0-SNAPSHOT
> source.
> *Steps to reproduce*
> 1. Deploy a Spring WAR that configures and uses a local JCS cache.
> 2. Execute a workload that creates the ElementEventQueue workers.
> 3. Invoke JCS.shutdown() while stopping the Spring application context.
> 4. Undeploy the WAR from Tomcat.
> 5. Deploy the WAR again and repeat the lifecycle cycle.
> 6. Inspect the Tomcat logs and live JVM threads after each undeployment.
> *Actual result*
> Two additional JCS-ElementEventQueue-* threads remain alive after each
> application lifecycle cycle.
> In a five-cycle deploy/workload/undeploy test with JCS 3.2.1, the unpatched
> implementation produced:
> - 10 Tomcat thread-leak warnings
> - final thread counts of 39, 41, 43, 46, and 48
> - an estimated thread-count slope of +2.3 threads per cycle
> *Expected result*
> ElementEventQueue.dispose() should terminate the executor owned by the queue.
> No JCS-ElementEventQueue-* worker should remain alive after JCS shutdown and
> WAR undeployment.
> *Root cause*
> ElementEventQueue obtains a newly created and unregistered executor from
> ThreadPoolManager.createPool(). Because the queue owns this executor, it must
> also terminate it explicitly.
> *Proposed fix*
> Call queueProcessor.shutdownNow() during the first execution of
> ElementEventQueue.dispose().
> A regression test verifies that:
> - the worker thread is running before disposal
> - the worker terminates after disposal
> - repeated calls to dispose() remain safe
> *Validation*
> The patched JCS 3.2.1 JAR was built from source, packaged inside the test
> WAR, and tested through five complete Tomcat deploy/workload/undeploy cycles.
> *Results after the patch:*
> - 0 Tomcat thread-leak warnings
> - final thread counts of 38, 38, 37, 37, and 38
> - an estimated thread-count slope of -0.1 threads per cycle
> The corresponding focused regression test also passes against the JCS 4
> source tree.
> A pull request containing the fix and regression test will be submitted after
> this issue provides the JCS issue identifier.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)