shashank created CAMEL-25781:
--------------------------------
Summary: camel-azure-eventhubs - an event that completes when the
checkpoint timer of its window has already run is not checkpointed until the
next event of the partition (delivered again after a restart if the partition
stays quiet)
Key: CAMEL-25781
URL: https://issues.apache.org/jira/browse/CAMEL-25781
Project: Camel
Issue Type: Bug
Components: camel-azure
Reporter: shashank
Assignee: shashank
{{EventHubsConsumer.processCommit}} (main bbd6f750aa00, lines 225-283) counts
each completed event in the checkpoint window of its partition. The window is
an {{EventHubsCheckpointUpdaterTask}} scheduled for {{checkpointBatchTimeout}};
when its timer runs, it takes the count and checkpoints the last event of the
window. A new window is started when the wall clock has passed the scheduled
time:
{code:java}
// EventHubsConsumer, line 234
if (checkpointTask == null || checkpointTask.isExpired()) {
// EventHubsCheckpointUpdaterTask, line 74
return System.currentTimeMillis() > scheduledTime;
{code}
and the timer takes the count ({{processedEvents.getAndSet(0)}}, line 45 of the
task) without the consumer's lock. So an event can be counted in a window whose
timer has already run:
# the event completes in the millisecond of the scheduled time, after the timer
ran ({{>}}, not {{>=}}; the scheduler fires by {{nanoTime}} and the scheduled
time is a truncated {{currentTimeMillis}}, so the timer often runs in that very
millisecond);
# the timer runs between the {{isExpired()}} check (line 234) and the
{{setEventContext}} / count of the event (lines 244 and 248), because it does
not take the lock;
# a timer that has already started when a checkpoint by batch size cancels it
({{cancel(false)}}, line 255) takes the count of the next window and
checkpoints an older event.
There is also the other side of the same check: when the timer runs late (after
the scheduled time by the wall clock: a GC pause, the single checkpoint thread
busy, a wall clock that runs ahead of {{nanoTime}}), the next event starts a
new window, the old timer takes its count and checkpoints the previous event,
and the new timer finds nothing.
In each case the event is not checkpointed by a timer. It is checkpointed with
the next event of the partition. If the partition stays quiet, its offset is
never checkpointed, and after a restart of the route or a rebalance of the
partition to another consumer, it is delivered again from the last checkpoint:
a duplicate, not a loss (the checkpoint is never moved past an event that was
not completed; only the checkpoint of a completed event is missing). With
{{>=}} alone the first window closes but the late-timer case gets wider, so the
fix does not use the wall clock.
The component docs only describe the two options ("Sets the batch timeout
between each checkpoint update. Works jointly with checkpointBatchSize."): a
completed event is expected to be checkpointed at the latest
{{checkpointBatchTimeout}} after it completed.
*Fix*: a new window starts when the timer of the current window has run
({{EventHubsCheckpointUpdaterTask.isDone()}}), not when the wall clock passed
its time. The timer takes the count and the event context under the consumer's
lock and marks itself done, so an event is either counted before (and
checkpointed by that timer) or after (and starts a new window). A checkpoint by
batch size marks the task done under the lock as well, so a timer that has
already started takes nothing. {{isExpired()}} is kept for the
{{CHECKPOINT_UPDATED_BY}} header.
*Tests*: new {{EventHubsConsumerCheckpointWindowTest}} (3 tests, the style of
the CAMEL-25683 tests: {{processCommit}} called directly, a mocked scheduler
whose captured timer tasks the test runs). All three fail on main with {{Wanted
but not invoked: eventContext.updateCheckpointAsync()}} for the event that
completed last:
* {{eventCompletedAfterTheTimerRanBeforeItsScheduledTimeIsCheckpointed}}: the
timer runs before the wall clock passes its scheduled time (case 1), then an
event completes; on main it is counted in the done window and no timer is
scheduled;
* {{eventCompletedAfterTheScheduledTimeBeforeTheTimerRanIsCheckpointed}}: the
late timer; on main the old timer checkpoints the previous event and the new
one finds 0;
* {{timerStartedBeforeABatchSizeCheckpointDoesNotTakeTheNextWindow}}: case 3.
Case 2 cannot be shown without a hook between the check and the count; it is
closed by the same lock. With the fix: camel-azure-eventhubs unit tests 38 run,
0 failures.
*How it was found*: the camel-test-dst world for Event Hubs (CAMEL-25738), seed
3042 of 5000, then the JUnit test above. On the camel-test-dst-stage1 branch
seed 3042 fails without the fix (trace hash 1137160c..., "1 completed after the
last checkpoint request ... counted for a window whose timer already ran (not
yet expired)"); with the fix it passes, and 5000 of 5000 seeds pass.
*Affected*: main, camel-4.23.x, camel-4.22.x and camel-4.18.x (the same
{{isExpired()}} check and lock-free {{EventHubsCheckpointUpdaterTask}}),
camel-4.14.x (the same {{>}} check against
{{TimerTask.scheduledExecutionTime()}} of a {{java.util.Timer}}, without any
lock). Read, not run.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)