[
https://issues.apache.org/jira/browse/CAMEL-25570?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen updated CAMEL-25570:
--------------------------------
Fix Version/s: 4.24.0
> camel-aws2-ddb - aws2-ddbstream consumer skips the records of a poll it did
> not route when the route is suspended during the poll
> ---------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-25570
> URL: https://issues.apache.org/jira/browse/CAMEL-25570
> Project: Camel
> Issue Type: Bug
> Components: camel-aws
> Reporter: shashank
> Assignee: shashank
> Priority: Major
> Fix For: 4.24.0
>
>
> {{Ddb2StreamConsumer.poll}} (main 5987a263107b) gets up to
> {{maxResultsPerRequest}} records (default 100) of a shard, creates one
> exchange per record and calls {{processBatch}} (line 91). After that it
> stores the next shard iterator of the response and the sequence number of the
> last record of the response (lines 93-96), whatever {{processBatch}} has done.
> {{processBatch}} stops routing when {{isBatchAllowed()}} returns false (line
> 106). {{ScheduledBatchPollingConsumer.isBatchAllowed}} (lines 104-118) does
> that when the consumer is no longer allowed to run (stopping or stopped), or
> when {{deferShutdown}} gave it a {{shutdownRunningTask}} other than
> {{CompleteAllTasks}}. Only {{DefaultShutdownStrategy}} calls
> {{deferShutdown}} (lines 614-616), when it suspends or shuts down routes,
> with the {{shutdownRunningTask}} of the route ({{CompleteCurrentTaskOnly}} by
> default). So the batch ends after the exchange that is being routed when the
> route is suspended at route level:
> * {{RouteController.suspendRoute}} ({{AbstractCamelContext}} line 1651), also
> when a route policy calls it ({{RoutePolicySupport.suspendRoute}}, line 156);
> * {{CamelContext.suspend()}} ({{AbstractCamelContext}} line 2325).
> It does not end when {{consumer.suspend()}} is called directly:
> {{isRunAllowed()}} stays true for a suspended service ({{BaseService}} lines
> 349-351) and no {{shutdownRunningTask}} is set, so the poll that is running
> routes all its records. That is what
> {{RoutePolicySupport.suspendOrStopConsumer}} does (line 110), which the route
> policies shipped with Camel use ({{ThrottlingInflightRoutePolicy}},
> {{ThrottlingExceptionRoutePolicy}}, the quartz {{ScheduledRoutePolicy}}):
> they are not affected. A route with {{shutdownRunningTask(CompleteAllTasks)}}
> is not affected either.
> When the route is resumed ({{resumeRoute}}, or {{CamelContext.resume()}}
> after {{suspend()}}), the same consumer continues with the stored iterator,
> which is after the whole response: the records that were not routed are
> skipped. Nothing is logged. With one shard and the defaults that is up to 99
> records per suspend; the following shards of that poll are requested as well
> and lose their whole response. If the stored iterator has expired meanwhile,
> the consumer continues after {{lastSeenSequenceNumbers}}, which is also the
> last record of the response.
> When the route or the context is stopped, the batch is cut in the same way
> (the same {{deferShutdown}} call), but that consumer is not used again: a
> route that is started again gets a new consumer
> ({{DefaultRoute.gatherRootServices}}, line 711), which starts from the
> configured {{streamIteratorType}}. Nothing is skipped there because of the
> stored iterator.
> h3. Reproduction
> New {{Ddb2StreamConsumerSuspendedBatchTest}} (3 tests) with a real route
> ({{aws2-ddbstream://table?streamIteratorType=FROM_START}}) and an in-memory
> {{DynamoDbStreamsClient}} (one shard, iterators are positions). The processor
> of one record, or the {{GetRecords}} call, suspends the route from another
> thread ({{RouteController.suspendRoute}}) and waits until
> {{isBatchAllowed()}} is false; then the route is resumed. On main all 3 end
> with an error, not a failure ({{Tests run: 3, Failures: 0, Errors: 3}}): the
> assertion is inside an Awaitility {{untilAsserted}}, so the result is a
> {{ConditionTimeoutException}} that wraps it, for example {{ConditionTimeout
> Assertion condition defined as a Lambda expression in
> org.apache.camel.component.aws2.ddbstream.Ddb2StreamConsumerSuspendedBatchTest
> records routed after the resume ==> expected: <[1, 2, 3, 4]> but was: <[1,
> 2]> within 20 seconds.}} The assertion messages:
> * {{recordsNotRoutedByASuspendingConsumerAreRoutedAfterTheResume}} (records
> 1-4, suspend while record 2 is routed): {{records routed after the resume ==>
> expected: <[1, 2, 3, 4]> but was: <[1, 2]>}}.
> * {{recordsOfAResponseThatArrivesWhileSuspendingAreRoutedAfterTheResume}}
> (suspend while the request is in flight): {{records routed after the resume
> ==> expected: <[1, 2]> but was: <[]>}}.
> * {{laterRecordsAreRoutedOnceAfterABatchWasCutShort}} (suspend at record 1 of
> 3, record 4 stored while suspended): {{records routed after the resume ==>
> expected: <[1, 2, 3, 4]> but was: <[1, 4]>}}.
> h3. Proposed fix
> {{poll}} compares the number of exchanges {{processBatch}} routed with the
> number of records of the response. When the batch was cut short:
> * it does not take the next shard iterator of the response;
> * when some records were routed, it stores the sequence number of the last
> routed record, and the next poll requests an iterator after it
> ({{ShardIteratorHandler.requestFreshShardIterator}},
> {{AFTER_SEQUENCE_NUMBER}}, the call the expired-iterator path already uses).
> The request is made by the next poll and not at once, because a stop
> interrupts the polling thread;
> * when nothing was routed, the iterator of the request stays and is used
> again;
> * the following shards are not requested by that poll.
> The exchanges created for the records that are not routed are given back to
> the exchange factory (on main they are dropped); the next poll creates new
> ones. A pooled exchange is given back with {{done()}}, as it was created with
> {{createExchange(true)}};
> {{Ddb2StreamConsumerSuspendedBatchPooledExchangeTest}} runs the tests with
> {{PooledExchangeFactory}} and checks the pool statistics (every exchange
> taken is given back once).
> 4.24 upgrade-guide entry (the skipped records are now routed after the
> resume). No new option. camel-aws2-ddb unit tests with the fix: 75, 0
> failures (integration tests need LocalStack and were not run).
> Other consumers with the same shape (a cursor that is moved for the whole
> poll, and a {{processBatch}} loop on {{isBatchAllowed()}}), read on main and
> not run: {{SlackConsumer}} (the timestamp is set in {{createExchanges}},
> lines 152 and 161, before {{processBatch}}, line 175) and
> {{GoogleCalendarStreamConsumer}} ({{selectUndeliveredAndMoveCursor}} is
> called at line 147, before {{processBatch}}, line 220). They are in other
> modules and not part of this change. {{Kinesis2Consumer.processBatch}} has no
> {{isBatchAllowed()}} check and routes every record of the response.
> Found with a TLA+ model of a poll (GetRecords, route one record at a time
> while the batch is allowed, end of the batch) with a suspend and a resume as
> environment steps: on main TLC violates {{NoSkip}} ("between two polls the
> consumer has not moved past a record it did not route") in 4 states
> (GetRecords returns 3 records, the route is suspended, the batch ends, the
> iterator is after record 3); it holds with the fix (also with an expired
> iterator after the suspend). Then confirmed with the real consumer and a real
> route as described.
> Affected: main; camel-4.22.x, camel-4.18.x and camel-4.14.x have the same
> {{processBatch}} loop and the same update after it (read, not run).
> Duplicate check (2026-10-12): JIRA text "ddbstream suspend" (none),
> "ddbstream isBatchAllowed" (none), "Ddb2StreamConsumer" (1: CAMEL-24156,
> other items), "ddbstream" created since 2025-06-01 (2: CAMEL-24156 and
> CAMEL-24154, resharding); GitHub pull requests: #24861 (merged 2026-07-17,
> CAMEL-24154) is the pull request that carried the ddbstream work of that
> review (it changes {{ShardIteratorHandler}} and its tests), and
> "Ddb2StreamConsumer" in title or body finds #6151 (closed, CAMEL-16594);
> neither is about a suspended route. #24863 (merged 2026-07-18, CAMEL-24156)
> changes no ddbstream file (in camel-aws2-ddb only
> {{Ddb2JsonDataTypeTransformer}}); no open pull request touches
> {{Ddb2StreamConsumer}}.
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)