Skip to content

SLING-13170 Replace Timer with injectable ScheduledExecutorService#55

Open
daniancu wants to merge 5 commits into
apache:masterfrom
daniancu:feature/SLING-13170-injectable-scheduler
Open

SLING-13170 Replace Timer with injectable ScheduledExecutorService#55
daniancu wants to merge 5 commits into
apache:masterfrom
daniancu:feature/SLING-13170-injectable-scheduler

Conversation

@daniancu
Copy link
Copy Markdown

Summary

  • Replace fire-and-forget java.util.Timer in startProcessing() with a ScheduledExecutorService that is properly shut down on deactivate(), preventing thread leaks
  • Add package-private setScheduler() so tests can inject a controllable scheduler
  • Rewrite testTopologyChange to use a ManualScheduler that captures tasks for on-demand execution, eliminating the flaky Thread.sleep(4000) and making the test deterministic and ~8x faster

Test plan

  • mvn test -Dtest=JobManagerConfigurationTest#testTopologyChange — passes in ~1s (was ~9s)
  • mvn test — all 109 tests pass

🤖 Generated with Claude Code

Daniel Iancu added 3 commits April 14, 2026 15:48
Replace the fire-and-forget Timer in startProcessing() with a
ScheduledExecutorService that is properly shut down on deactivate
and can be replaced in tests via setScheduler().

This eliminates the flaky Thread.sleep(4000) in testTopologyChange
by using a ManualScheduler that captures tasks for on-demand execution,
making the test deterministic and ~8x faster.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Ai-Assisted-By: claude
- Use AtomicReference for thread-safe scheduler access
- Guard startProcessing() against null scheduler and
  RejectedExecutionException when shutdown races with task submission
- Model realistic shutdown semantics in ManualScheduler test fake
- Add testDeactivatePreventsDelayedNotification covering deactivation
  clearing pending tasks and schedule/shutdown race safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Ai-Assisted-By: claude
…ivate

- testStartProcessingWithNullScheduler: covers the scheduler.get()==null
  branch when deactivate() has already cleared the AtomicReference
- testDeactivateWithNullScheduler: covers deactivate() when scheduler
  was never initialized or already cleared (including double-deactivate)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Ai-Assisted-By: claude
* and clears pending tasks; {@link #schedule} after shutdown throws {@link RejectedExecutionException};
* {@link #runAll()} skips execution after shutdown.
*/
private static class ManualScheduler implements ScheduledExecutorService {
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.

I would externalize this internal class into a dedicated class.


/**
* Replace the scheduler used for delayed topology change processing.
* Package-private for testability.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is an OSGi component, why would this setScheduler be needed, if simply another could be injected (if it was made so that it would be an OSGi service as well)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

it's just a backdoor for testing since tests don't use OSGi. in production we do not need to inject, it runs with the default one

Daniel Iancu and others added 2 commits April 15, 2026 16:24
Move ManualScheduler from a private inner class in
JobManagerConfigurationTest to a standalone package-private class
for better readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the package-private setScheduler() setter with reflection-based
injection in tests, consistent with how startupDelayListener is already
injected. The scheduler AtomicReference is now only accessed directly
by production code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Ai-Assisted-By: claude
@daniancu
Copy link
Copy Markdown
Author

Changes since initial PR

Commit 2 — Harden scheduler lifecycle

  • Use AtomicReference<ScheduledExecutorService> instead of volatile for thread-safe access
  • startProcessing() reads the scheduler into a local variable, handles null (already deactivated), and catches RejectedExecutionException (shutdown race)
  • ManualScheduler test fake now models realistic shutdown semantics
  • Added testDeactivatePreventsDelayedNotification covering deactivation clearing pending tasks and schedule/shutdown race safety

Commit 3 — Null scheduler tests

  • testStartProcessingWithNullScheduler: covers scheduler.get() == null branch
  • testDeactivateWithNullScheduler: covers deactivate() when scheduler was never initialized, including double-deactivate

Commit 4 — Extract ManualScheduler

  • Moved ManualScheduler from inner class to standalone package-private file

Commit 5 — Remove setScheduler from production code

  • Removed the package-private setScheduler() setter from JobManagerConfiguration
  • Tests now inject the scheduler via reflection (same pattern as startupDelayListener)

@joerghoh
Copy link
Copy Markdown
Contributor

@stefanseifert IIRC you have a Windows system, can you please check this PR? I get constant failures in the CI for Windows machines, and I would like to understand if this is a Windows problem or rather a CI issue.

@sonarqubecloud
Copy link
Copy Markdown

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.

3 participants