[ 
https://issues.apache.org/jira/browse/CAMEL-25741?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen resolved CAMEL-25741.
---------------------------------
    Resolution: Fixed

> camel-aws2-ddb - aws2-ddbstream consumer leaves a closed shard before reading 
> it to its end, and starts again from streamIteratorType when every shard it 
> reads has closed (records lost or routed again)
> ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25741
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25741
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-aws
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>             Fix For: 4.24.0
>
>
> {{ShardIteratorHandler.getShardIterators()}} (camel-aws2-ddb, main 
> 94ca50181230, lines 53-96) decides which shards the {{aws2-ddbstream}} 
> consumer reads at each poll from two signals, and neither means that a shard 
> was read:
> # *A tracked shard that has children in the shard tree is replaced by its 
> children* (lines 71-80, "Inactive shard, move down to its children"). When 
> DynamoDB closes a shard it creates its child shards, which {{DescribeStream}} 
> then lists with the closed shard as {{ParentShardId}}; the closed shard still 
> holds the records the consumer has not read yet, and the [DynamoDB Streams 
> guide|https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html]
>  says "an application must always process a parent shard before it processes 
> a child shard". {{GetRecords}} returns no {{NextShardIterator}} only when a 
> closed shard was read to its end 
> ([GetRecords|https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_GetRecords.html]:
>  "If set to null, the shard has been closed and the requested iterator will 
> not return any more data"). Consequences:
> #* {{streamIteratorType=FROM_START}}: the first poll populates the tree with 
> every shard of the last 24 hours. The roots are closed and already have 
> children, so the second poll leaves every root after one response 
> ({{maxResultsPerRequest}} records, default 100, divided by the number of 
> shards). The rest of the root shards is never routed.
> #* any iterator type, table with several partitions: when one shard is read 
> to its end, the tree is refreshed; every other tracked shard that has closed 
> meanwhile (a shard has a lifespan of up to four hours, [AWS Database 
> Blog|https://aws.amazon.com/blogs/database/build-scalable-event-driven-architectures-with-amazon-dynamodb-and-aws-lambda/])
>  is left with its unread records.
> # *An empty map of shard iterators re-initialises the consumer from 
> {{streamIteratorType}}* (line 58). The map is empty when every shard the 
> consumer was reading has been read to its end: a table with one partition 
> (one open shard) at each shard rotation, or several shards read to their end 
> in the same poll. {{pendingClosedShards}} (CAMEL-24154) is not consulted on 
> that path. Consequences:
> #* {{FROM_START}}: the consumer reads the stream again from its roots and 
> routes their first records again (then signal 1 moves it down the tree one 
> response per shard); when the closed roots are read to their end in one 
> response, the map is empty again at the next poll, so the consumer routes 
> them again at every poll and never reads the child shards.
> #* {{FROM_LATEST}} (the default): the consumer takes {{LATEST}} iterators of 
> the new shards and skips the records that were written to them between the 
> rotation and the poll.
> Example (one partition, default {{FROM_LATEST}}): the consumer reads 
> {{shard-0}}; DynamoDB rotates it into {{shard-1}}; the application writes 
> record 4 (into {{shard-1}}); the consumer reads the last record of 
> {{shard-0}} (no next iterator), the map is empty, the next poll takes 
> {{LATEST}} of {{shard-1}}: record 4 is never routed. CAMEL-24154 described 
> this case ("On a single-partition table the map does empty, but 
> re-initialization uses the configured iterator type") and its fix covers only 
> the case where another shard is still tracked.
> *Fix* (one class): the configured iterator type is used once, for the start 
> position (again only while no shard was found at all); a shard is left only 
> when {{GetRecords}} returned no next shard iterator, and its children are 
> then read from {{TRIM_HORIZON}} (as before); a shard read to its end whose 
> children {{DescribeStream}} does not list yet is looked up again at the next 
> poll. The existing unit test 
> {{ShardIteratorHandlerTest.shouldProgressThroughTreeWhenShardIteratorsAreRetrievedRepeatedly}}
>  expected the move to the children at each call without any record read; it 
> now reads each shard to its end before the next call.
> *Tests*: new {{Ddb2StreamConsumerShardRotationTest}} (5 tests, a real route 
> and an in-memory {{DynamoDbStreamsClient}} with closed and open shards): a 
> closed root with 5 records and {{maxResultsPerRequest=2}} ({{FROM_START}}; 
> main routes 1, 2 and the child's record only); a closed root and its child 
> ({{FROM_START}}; main routes 1, 2, 1, 2, ... and never 3); the child of the 
> only shard ({{FROM_LATEST}}; main never routes record 4); two shards that 
> rotate together ({{FROM_LATEST}}; main skips five records of the longer 
> shard); a child listed only after the consumer read the end of its parent 
> ({{FROM_LATEST}}; main skips its records). All five end with an error on main 
> (an Awaitility {{ConditionTimeoutException}} around the assertion; the second 
> test routed 2906 records instead of 3 in 20 seconds) and pass with the fix; 
> the camel-aws2-ddb suite passes (82 tests).
> *How it was found*: deterministic simulation testing with a fake DynamoDB 
> Streams world written from the DynamoDB Streams API reference (shard history, 
> writes, rotations and splits, faults on every call; the seed also picked 
> {{streamIteratorType}}, {{maxResultsPerRequest}} 1 to 1000, the poll delay, 
> {{greedy}} and the {{DescribeStream}} page size). The invariants "every 
> record the consumer is expected to read is routed" and "no record is routed 
> twice" failed in 1253 of 3000 seeds on main (first failing seed 3, invariant 
> "every expected record is routed", {{FROM_START}}); with the fix, 0 of 3000 
> seeds.
> *Affected*: main, camel-4.23.x and camel-4.22.x (same code, both signals); 
> camel-4.18.x and camel-4.14.x have signal 1 and signal 2 without the 
> CAMEL-24154 refresh (read, not run).
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to