Skip to content

debezium/dbz#2523 Fix possible infinite retry loop on bounded change stream partitions - #175

Merged
Naros merged 2 commits into
debezium:mainfrom
nancyxu825:fix/bounded-stream-completion-dbz-2523
Sep 1, 2026
Merged

debezium/dbz#2523 Fix possible infinite retry loop on bounded change stream partitions#175
Naros merged 2 commits into
debezium:mainfrom
nancyxu825:fix/bounded-stream-completion-dbz-2523

Conversation

@nancyxu825

Copy link
Copy Markdown
Contributor

When a bounded change stream partition query (endTimestamp != null) completes (resultSet.next() == false), the backend query has scanned all rows up to endTimestamp.

Requiring lastReceivedTimestamp >= endTimestamp caused bounded queries ending with a DataChangeRecord at T < endTimestamp to throw ChangeStreamException and retry from T indefinitely because start_timestamp is inclusive.

This fix:

  1. Updates reachedEnd in SpannerChangeStreamService to allow bounded queries (partition.getEndTimestamp() != null) to complete normally on EOF without requiring lastReceivedTimestamp >= endTimestamp.
  2. Preserves the validation that unbounded queries (endTimestamp == null) must receive ChildPartitionsEvent before completing, continuing to protect against silent stream dropouts.
  3. Updates unit tests in SpannerChangeStreamServiceTest.

…rtitions

When a bounded change stream partition query (endTimestamp != null)
completes (resultSet.next() == false), the backend query has scanned all
rows up to endTimestamp.

In Cloud Spanner's Change Stream TVF (AwaitChangeIterator), an OuterApply
is performed against the change data table for each time slice:
1. If no mutations exist in the slice, Spanner emits a HeartbeatRecord with
   timestamp = endTimestamp.
2. If a mutation exists in the slice, Spanner emits DataChangeRecord and does
   not emit a trailing HeartbeatRecord at endTimestamp.

Requiring lastReceivedTimestamp >= endTimestamp caused bounded queries ending
with a DataChangeRecord at T < endTimestamp to throw ChangeStreamException and
retry from T indefinitely because start_timestamp is inclusive.

This fix:
1. Updates reachedEnd in SpannerChangeStreamService to allow bounded queries
   (partition.getEndTimestamp() != null) to complete normally on EOF without
   requiring lastReceivedTimestamp >= endTimestamp.
2. Preserves the validation that unbounded queries (endTimestamp == null) must
   receive ChildPartitionsEvent before completing, continuing to protect against
   silent stream dropouts.
3. Updates unit tests in SpannerChangeStreamServiceTest.

Signed-off-by: Nancy Xu <nancyxu@google.com>

@Naros Naros left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi @nancyxu825 this LGTM, do you want to cleanup the one local variable below or are you okay leaving it as-is?

…eStreamService

Remove unused lastReceivedTimestamp variable and timestamp tracking logic
following the simplification of stream completion checks.

Signed-off-by: Nancy Xu <nancyxu@google.com>
@Naros
Naros merged commit 32797b1 into debezium:main Sep 1, 2026
5 checks passed
@Naros

Naros commented Sep 1, 2026

Copy link
Copy Markdown
Member

Applied, thanks @nancyxu825 and @jiangzzhu 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants