Nageshwari Elango created SLING-13298:
-----------------------------------------

             Summary: Summary: OakMockSlingRepository.deactivate() shuts down 
executors before repository.shutdown(), causing RejectedExecutionException from 
Oak's BackgroundObserver during test teardown
                 Key: SLING-13298
                 URL: https://issues.apache.org/jira/browse/SLING-13298
             Project: Sling
          Issue Type: Bug
          Components: Apache Sling Testing Clients
            Reporter: Nageshwari Elango


The source confirms everything in the bug report. Now I have all the details 
needed to give you a precise, ready-to-file JIRA ticket.
----
Here's the complete JIRA ticket you can file at 
https://issues.apache.org/jira/projects/SLING/issues:
----
Project: Sling Component: Testing Issue Type: Bug Priority: Minor _(tests still 
pass — annotation noise only)_ Affects Version: sling-mock-oak (current master) 
Labels: _(optional)_ {{{}sling-mock{}}}, {{{}oak{}}}, {{test-infrastructure}}
----
Summary: {{OakMockSlingRepository.deactivate()}} shuts down executors before 
{{{}repository.shutdown(){}}}, causing {{RejectedExecutionException}} from 
Oak's {{BackgroundObserver}} during test teardown
----
Description:

Problem

In {{{}OakMockSlingRepository.deactivate(){}}}, the two {{ExecutorService}} 
instances are terminated _before_ the Oak JCR repository is shut down:
java
 
 {{// Current order — 
brokenexecutor.shutdownNow();scheduledExecutor.shutdownNow();shutdownExecutorService(repository,
 "scheduledExecutor");((JackrabbitRepository) repository).shutdown();   // ← 
executor already dead}}
When {{repository.shutdown()}} is called, it triggers 
{{BackgroundObserver.close()}} on all registered Oak observers. 
{{BackgroundObserver}} uses a chained task pattern: each step that drains its 
internal queue calls {{executor.execute(nextTask)}} to schedule the next step. 
Because the executor is already terminated, every such submission throws 
{{{}RejectedExecutionException{}}}:
 
 
 {{Task 
org.apache.jackrabbit.oak.commons.concurrent.NotifyingFutureTask@1748c62[Not 
completed, task = 
org.apache.jackrabbit.oak.spi.commit.BackgroundObserver$1$1@54b0813]rejected 
from java.util.concurrent.ThreadPoolExecutor@2545113e[Shutting down, pool size 
= 1, active threads = 1, queued tasks = 0, completed tasks = 0]}}
Impact
 * All unit tests using {{ResourceResolverType.JCR_OAK}} are affected (not just 
acs-aem-commons).
 * All tests still pass — this is exclusively teardown noise.
 * GitHub Actions surfaces these exceptions as {{{}failure{}}}-level CI 
annotations, making them visually indistinguishable from real test failures. 
Reviewers must manually inspect every annotation to confirm none are genuine.
 * Observed consistently on Windows JDK 17 and JDK 21; intermittently on 
macOS/Linux (timing-dependent).
 * Confirmed pre-existing across multiple CI runs in acs-aem-commons PR #3760 
(merged June 2026), unrelated to any recent code change in that repo.

Root Cause

The dependency direction is: {{OakMockSlingRepository}} creates the executor 
and passes it _into_ Oak. The owner should outlive its dependents during 
shutdown. Currently it does not — the executors are killed first, leaving Oak's 
{{BackgroundObserver}} with a dead executor when it tries to drain its queue 
during {{{}repository.shutdown(){}}}.

Proposed Fix

Reverse the shutdown order — call {{repository.shutdown()}} first while the 
executors are still alive, then terminate the executors:
java
 
 {{// Proposed order — correct// Shut down Oak JCR repository first so 
BackgroundObserver can complete// its shutdown sequence while the executor is 
still alive((JackrabbitRepository) 
repository).shutdown();executor.shutdownNow();scheduledExecutor.shutdownNow();shutdownExecutorService(repository,
 "scheduledExecutor");}}
The existing intent (force-kill executors without waiting, since this is a unit 
test) is fully preserved — only the ordering changes.

Notes for maintainers to evaluate:
 * Whether reversing the order alone is sufficient, or whether an 
{{awaitTermination()}} call between {{repository.shutdown()}} and 
{{executor.shutdownNow()}} is needed to let the STOP sentinel complete cleanly 
(with the trade-off of added wait time).
 * Whether any other Oak internal components beyond {{BackgroundObserver}} 
depend on the executor during {{{}repository.shutdown(){}}}.

Steps to Reproduce
 # Add a dependency on {{sling-mock-oak}} with 
{{{}ResourceResolverType.JCR_OAK{}}}.
 # Run any unit test that uses a Sling mock Oak-backed resource resolver.
 # Observe {{RejectedExecutionException}} from {{BackgroundObserver}} in 
teardown output / CI annotations.

Example project where observed: acs-aem-commons — CI run #30953046297.

References
 * {{OakMockSlingRepository.java}} — {{deactivate()}} method
 * {{BackgroundObserver.java}} — Oak trunk
 * Related (different issue): SLING-12250



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

Reply via email to