Federico Mariani created CAMEL-26669:
----------------------------------------

             Summary: camel-aws2-kinesis - the polling consumer routes the 
whole GetRecords response before a stop or suspend
                 Key: CAMEL-26669
                 URL: https://issues.apache.org/jira/browse/CAMEL-26669
             Project: Camel
          Issue Type: Improvement
          Components: camel-aws2-kinesis
    Affects Versions: 4.18.5, 4.23.0, 4.22.2
            Reporter: Federico Mariani


h3. Problem
The polling {{aws2-kinesis}} consumer ({{useKclConsumers=false}}) routes every 
record of a {{GetRecords}} response before its poll ends, also when the route 
is stopped or suspended meanwhile. Line numbers are on main (7978db3d42dd):
* 
{{components/camel-aws/camel-aws2-kinesis/src/main/java/org/apache/camel/component/aws2/kinesis/Kinesis2Consumer.java}}
 lines 244-256: {{processBatch}} loops {{while (!exchanges.isEmpty())}} and 
never calls {{isBatchAllowed()}}.
* 
{{core/camel-support/src/main/java/org/apache/camel/support/ScheduledBatchPollingConsumer.java}}
 lines 103-118: {{isBatchAllowed()}} returns false once 
{{DefaultShutdownStrategy}} has called {{deferShutdown}} with the 
{{shutdownRunningTask}} of the route, which is {{CompleteCurrentTaskOnly}} by 
default. The other batch consumers (aws2-ddbstream, aws2-sqs, aws2-s3, file, 
...) stop their batch there, as the graceful shutdown documentation describes 
for batch consumers.
* Same file, lines 58-78: while a poll runs, {{getPendingExchangesSize()}} 
returns at least 1 ({{isPolling()}}), so the graceful shutdown waits for the 
whole poll. Suspending the consumer does not stop the poll 
({{ScheduledPollConsumer.doSuspend}} is empty).
* 
{{core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java}}
 line 129: the timeout is 45 seconds by default 
({{camel.main.shutdownTimeout}}); after it the routes are shut down by force.

Since CAMEL-26650 the consumer asks each shard for up to 1000 records per poll 
(it was 1). With a route that takes 50 ms per record, one poll takes about 50 
seconds, more than the default timeout, so a plain stop of the route or of the 
CamelContext becomes a forced shutdown. The records of the poll that are not 
routed when the timeout fires then fail one by one with 
{{RouteStoppingException}}, each reported to the consumer's exception handler.

h3. How to reproduce
Measured on main + PR #29272 with a Mockito {{KinesisClient}}: each shard holds 
5000 records, {{GetRecords}} returns {{min(Limit, remaining)}} records. The 
route is 
{{from("aws2-kinesis://stream?amazonKinesisClient=#kinesisClient&exceptionHandler=#handler"
 + options).delay(20).syncDelayed()}}, the shutdown timeout is set to 5 
seconds, and {{stopRoute}} is called once the first record has been routed.
||options||shards||stopRoute took||timeout / forced||records routed||records 
failed after the stop||
|(default, Limit 1000)|1|5.0 s|yes|209 of 1000|791 (1 {{InterruptedException}}, 
790 {{RouteStoppingException}})|
|(default, Limit 1000)|4|5.0 s|yes|823 of 4000|3177 (1 
{{InterruptedException}}, 3176 {{RouteStoppingException}})|
|{{maxResultsPerRequest=1}} (control, the old default)|1|0 s|no|1 of 1|0|
The log shows "Timeout occurred during graceful shutdown. Forcing the routes to 
be shutdown now." in both default cases. The 45 s / 50 ms case of the review 
was computed, not run. A suspend goes through the same wait of 
{{DefaultShutdownStrategy}}; that was read from the source, not run.

h3. Proposed change
Do what CAMEL-25570 did for {{Ddb2StreamConsumer}} (commit 7024148c09c3):
# {{processBatch}} stops when {{isBatchAllowed()}} is false.
# In {{fetchAndPrepareRecordsForCamel}} (lines 191-204), when fewer records 
were routed than received:
#* do not store {{nextShardIterator}}, and remove the cached iterator, so that 
the next poll requests a new one. A null {{nextShardIterator}} (closed shard) 
must not be stored either, or the shard is marked closed before its last 
records are routed.
#* some were routed: put the sequence number of the last *routed* record in 
{{lastSequenceNumbers}}. The next poll then requests an 
{{AFTER_SEQUENCE_NUMBER}} iterator through the existing path in 
{{getShardIterator}} (lines 282-288, CAMEL-25569).
#* none was routed and the shard has no {{lastSequenceNumbers}} entry yet: 
remember the first record, and request {{AT_SEQUENCE_NUMBER}} for it. 
CAMEL-25570 added the same position to aws2-ddbstream 
({{firstNotRoutedSequenceNumbers}}). This does not rely on using a shard 
iterator twice, which the Kinesis API documentation does not describe.
#* release the exchanges that were not routed ({{releaseExchange(exchange, 
true)}}), for the pooled exchange factory.

Both parts are needed. Only adding the {{isBatchAllowed()}} check would make a 
{{suspendRoute}} / {{resumeRoute}} skip the rest of the response, because the 
same consumer continues from the stored iterator (the bug CAMEL-25570 fixed for 
aws2-ddbstream). After a stop and start the route gets a new consumer 
({{DefaultRoute.gatherRootServices}}), which starts from {{iteratorType}} or 
the resume strategy, as today.

Size and risk (estimated from the ddbstream fix, which changed about 60 lines 
of main code): about 30-50 lines in {{Kinesis2Consumer}}, plus Mockito tests: 
suspend in the middle of a response and resume (no record skipped or routed 
twice), a cut before the first record, a closed shard cut short, an expired 
iterator after a cut, pooled exchanges released. Points to watch:
* the shards of a poll run in parallel, so each shard cuts at its own record. 
The state is already per shard and thread-safe (CAMEL-24080).
* a resume strategy's adapter runs after the {{AFTER_SEQUENCE_NUMBER}} request 
is built (line 305) and can change it, as it already can after an expired 
iterator.
* {{pendingExchanges}} is one field shared by the parallel shards: leave it 
unset. While polling, {{getPendingExchangesSize()}} already returns 1.

h3. Impact / upgrade
* The stop and suspend behaviour changes for routes with the default 
{{CompleteCurrentTaskOnly}}: the consumer stops after the record it is routing, 
and does not route the rest of the response first. This needs an upgrade-guide 
entry. Routes that need the old behaviour set 
{{shutdownRunningTask(CompleteAllTasks)}}, and for them nothing changes.
* Only the polling consumer is affected. The KCL consumer 
({{KclKinesis2Consumer}}) extends {{DefaultConsumer}}.
* The same code is in camel-4.18.5, camel-4.22.2 and camel-4.23.0, where the 
default of 1 record per poll keeps the effect small unless 
{{maxResultsPerRequest}} is raised.

h3. Related
* CAMEL-26650 (PR #29272): the new default of 1000 records per shard per poll. 
Its review raised this point; the upgrade guide of that PR now mentions the 
shutdown timeout.
* CAMEL-25570 (commit 7024148c09c3): the same fix for aws2-ddbstream. 
CAMEL-25684 (camel-slack) is similar.
* CAMEL-25569 (commit 043ac9ec84ab): {{lastSequenceNumbers}} and the 
{{AFTER_SEQUENCE_NUMBER}} iterator after an expired iterator.
* The follow-up about the {{parallelStream()}} on 
{{ForkJoinPool.commonPool()}}, filed at the same time.

_Claude Code on behalf of Federico Mariani_



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to