Skip to content

Fix flaky Quartz tests: widen timing windows and fix latch timeout#24819

Merged
davsclaus merged 1 commit into
apache:mainfrom
gnodet:fix/flaky-quartz-tests
Jul 18, 2026
Merged

Fix flaky Quartz tests: widen timing windows and fix latch timeout#24819
davsclaus merged 1 commit into
apache:mainfrom
gnodet:fix/flaky-quartz-tests

Conversation

@gnodet

@gnodet gnodet commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Claude Code on behalf of gnodet

Fix 4 flaky tests in camel-quartz identified by Develocity analytics:

Test Flaky rate Root cause Fix
QuartzAddRoutesAfterCamelContextStartedTest 3.2% (48/1483) repeatInterval=100ms too tight for loaded CI Increased to 500ms, added timed assertion
QuartzCronRouteWithStartDateEndDateTest 2.5% (37/1483) Narrow 2s trigger window with only 3s setup headroom Widened to 4s window with 5s headroom, proper timed assertion
QuartzAddDynamicRouteTest 1.7% (25/1483) repeatInterval=2ms — absurdly tight for any scheduler Increased to 200ms, added timed assertion
CronScheduledRoutePolicyTest$CronTest5 0.3% (5/1483) await(5000, TimeUnit.SECONDS) — 83 minutes! (typo) Fixed to await(30, TimeUnit.SECONDS), assert result

Details

QuartzAddRoutesAfterCamelContextStartedTest: The Quartz scheduler thread pool can be starved on loaded CI machines. A 100ms repeat interval with repeatCount=1 means two firings must happen within ~200ms — too tight. Increased to 500ms.

QuartzCronRouteWithStartDateEndDateTest: The cron trigger starts 3 seconds in the future and fires for only 2 seconds. On loaded CI, Quartz scheduler initialization can take >1 second, eating into the headroom. Increased start offset to 5s and window to 4s. Also replaced the redundant mock.await() + assertIsSatisfied() chain with a single timed assertIsSatisfied.

QuartzAddDynamicRouteTest: A 2ms repeat interval is far below Quartz's scheduling granularity. Both firings can happen before the mock is ready, or the scheduler may miss them entirely. Increased to 200ms.

CronScheduledRoutePolicyTest$CronTest5: startedLatch.await(5000, TimeUnit.SECONDS) is clearly a typo — 5000 seconds = 83 minutes. Fixed to 30 seconds with proper assertion of the latch result for fail-fast behavior.

Test plan

  • All 4 fixed tests pass locally (5 consecutive runs)
  • CI green on this PR

🤖 Generated with Claude Code

…out bug

- QuartzAddRoutesAfterCamelContextStartedTest: increase repeatInterval
  from 100ms to 500ms so Quartz can reliably fire both triggers on
  loaded CI machines; add timed assertIsSatisfied
- QuartzAddDynamicRouteTest: increase repeatInterval from 2ms to 200ms
  (2ms was far too tight for any scheduler to handle reliably); add
  timed assertIsSatisfied
- QuartzCronRouteWithStartDateEndDateTest: increase startAt offset from
  3s to 5s and widen trigger window from 2s to 4s so Quartz scheduler
  initialization doesn't eat into the firing window; replace redundant
  mock.await()+assertIsSatisfied with single timed assertIsSatisfied
- CronScheduledRoutePolicyTest$CronTest5: fix CountDownLatch.await()
  timeout from 5000 SECONDS (83 minutes!) to 30 SECONDS — an obvious
  typo; also assert the latch result to fail fast on timeout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet
gnodet requested review from apupier and davsclaus July 16, 2026 22:37

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Clean, well-targeted fixes. All four changes address genuine timing issues in tests.

Review

1. QuartzAddDynamicRouteTest (repeatInterval=2200) ✅
A 2ms repeat interval is far below Quartz's practical scheduling granularity — the scheduler thread pool can't reliably fire two triggers within 4ms. 200ms is reasonable. The timed assertIsSatisfied(context, 30, TimeUnit.SECONDS) is the correct pattern per project guidelines.

2. QuartzAddRoutesAfterCamelContextStartedTest (repeatInterval=100500) ✅
100ms was tight for CI. Two firings need to complete within ~200ms total, which fails when Quartz's thread pool is starved under CI load. 500ms gives ample room. Good switch to timed assertion.

3. QuartzCronRouteWithStartDateEndDateTest (widened window + headroom) ✅
Start offset 3s→5s and window 2s→4s give the Quartz scheduler time to initialize before the trigger window opens. The upper bound assertion widened from <= 3 to <= 5 matches the 4-second window correctly (a per-second cron in a 4s window fires up to 5 times at 0, 1, 2, 3, 4s). Good cleanup removing the redundant mock.await() + assertIsSatisfied() chain — MockEndpoint.assertIsSatisfied(context, 30, TimeUnit.SECONDS) already waits internally via its latch.

4. CronScheduledRoutePolicyTest$CronTest5 (latch await typo fix) ✅
This is the highest-value fix in the batch. await(5000, TimeUnit.SECONDS) = 83 minutes — a clear typo that would cause an 83-minute CI hang on failure. The fix to assertTrue(startedLatch.await(30, TimeUnit.SECONDS), ...) is correct on two fronts:

  • Fixes the timeout to a sensible value (30s)
  • Wraps in assertTrue with a message for fail-fast behavior — the previous code silently ignored the await() return value

Checklist

Check Status
Tests ✅ This IS the test fix (4 flaky tests)
Thread.sleep() ✅ None introduced
MockEndpoint usage ✅ Timed assertions per project guidelines
Production code ✅ No production changes
Commit convention

Reviewed with Claude Code on behalf of gnodet. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-quartz

🔬 Scalpel shadow comparison — Scalpel: 1 tested, 0 compile-only — current: 12 all tested

Maveniverse Scalpel detected 1 affected modules (current approach: 12).

Modules only in current approach (11)
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-launcher-container
  • camel-mail
  • camel-mail-microsoft-oauth
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
  • camel-zookeeper-master

Skip-tests mode would test 1 modules (1 direct + 0 downstream), skip tests for 0 (generated code, meta-modules)

Modules Scalpel would test (1)
  • camel-quartz

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (12 modules)
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Plugin :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: Launcher :: Container
  • Camel :: Mail
  • Camel :: Mail :: Microsoft OAuth
  • Camel :: Quartz
  • Camel :: YAML DSL :: Validator
  • Camel :: YAML DSL :: Validator Maven Plugin
  • Camel :: Zookeeper Master

⚙️ View full build and test results

@oscerd oscerd left a comment

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.

LGTM — these are genuine fixes, not masking. The repeatInterval widenings correct absurd values (2ms → 200ms, 100ms → 500ms) and then rely on the latch-based MockEndpoint.assertIsSatisfied(context, 30, SECONDS) upper bound rather than a sleep, and the best one is the CronScheduledRoutePolicyTest latch typo — await(5000, SECONDS) (~83 minutes!) → assertTrue(latch.await(30, SECONDS), ...), which also fixes a silently-ignored return value, so it's strictly stronger. No Thread.sleep added and the original assertions are preserved. CI green.

Reviewed with Claude Code on behalf of Andrea Cosentino. This review was generated by an AI agent and may contain inaccuracies; please verify all suggestions before applying.

Comment on lines +44 to +52
// use a reasonable interval (200ms) so that the Quartz
// scheduler can reliably fire both triggers on loaded CI
// machines (2ms was far too tight for reliable scheduling)
from("quartz://myGroup/myTimerName?trigger.repeatInterval=200&trigger.repeatCount=1").routeId("myRoute")
.to("direct:foo");
}
});

resultEndpoint.assertIsSatisfied();
MockEndpoint.assertIsSatisfied(context, 30, TimeUnit.SECONDS);

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 think that the point of having a 2ms scheduling was too ensure that the repeatCount i well respected. By increasing to 200ms, we need to add some wait of at least 200ms to ensure that no other route is triggered and so that we have only 1 and then 2 messages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude Code on behalf of gnodet

Thanks for the careful review! The 2ms interval was not originally chosen to validate repeatCount enforcement — the original commit (a056a93, 2011) says "Added test based on user forum issue" and the test's purpose is verifying that dynamically adding a Quartz route to a running CamelContext works.

Neither interval validates "exactly N, no more" because MockEndpoint.expectedMessageCount(2) returns as soon as 2 messages arrive without waiting to detect extras — you would need setAssertPeriod() for that, which neither the old nor new test uses.

The flakiness root cause is Quartz's misfire handling: triggerStartDelay (default 500ms, see QuartzEndpoint line 96) delays fires to T+500ms and T+502ms. On loaded CI, if the Quartz scheduler thread is delayed by just 3ms, both fire times are in the past when the scheduler catches up. MISFIRE_INSTRUCTION_FIRE_NOW (line 558) consolidates them into a single fire — yielding 1 message instead of 2. Widening to 200ms gives the scheduler a 200ms window between fires, making it resilient to brief scheduler delays.

That said, if you think it would be valuable, we could add resultEndpoint.setAssertPeriod(500) to verify no extra messages arrive after the expected 2. This would address the repeatCount concern independently of the interval change.

Comment on lines +41 to +44
// use a generous interval (500ms) so that the Quartz scheduler
// can reliably fire both the initial and repeat triggers even
// on heavily-loaded CI machines (100ms was too tight)
from("quartz://myGroup/myTimerName?trigger.repeatInterval=500&trigger.repeatCount=1").to("mock:result");

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.

If the message is not sent when the repeat interval is low, I guess this is a bug and we should not change the test but fix the runtime. Isn't it?
or we consider it normal to drop a message?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Claude Code on behalf of gnodet

Good question! No message is actually dropped by Camel — the "lost" message is caused by Quartz's built-in misfire handling, not the Camel runtime.

Here is the mechanism: triggerStartDelay (default 500ms, see QuartzEndpoint line 96) ensures the endpoint is fully started before any fire. With repeatInterval=100ms and repeatCount=1, fires are at T+500ms and T+600ms. On loaded CI, if the Quartz scheduler thread is delayed by ~110ms (CPU contention, GC), both fire times are in the past when the scheduler catches up. Quartz's MISFIRE_INSTRUCTION_FIRE_NOW (QuartzEndpoint line 558) consolidates these misfires into a single fire — by design. This is standard Quartz behavior, not a Camel bug.

This test's interval has been adjusted before:

  • 2010 (CAMEL-3203, original): 2ms
  • 2013 (CAMEL-4075, camel-quartz2 port): increased to 1000ms
  • 2021 (parallel tests refactor): reduced to 100ms for speed — which introduced the flakiness
  • This PR: increased to 500ms

The test's purpose (CAMEL-3203) is verifying that adding routes after CamelContext started works, not stress-testing Quartz's sub-second scheduling. The interval just needs to be large enough that the scheduler can reliably fire both triggers without misfire consolidation.

@apupier apupier left a comment

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 have serious doubt that it will change anything (apart from increasing the time of timeout when the test is still flaky) but let's try

@davsclaus davsclaus added this to the 4.22.0 milestone Jul 18, 2026
@davsclaus davsclaus added the bug Something isn't working label Jul 18, 2026
@davsclaus
davsclaus merged commit deef5a5 into apache:main Jul 18, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants