Skip to content

Commit def9bf9

Browse files
committed
[Spanner Change Streams] Fix potential data loss issue by ensuring to only claim timestamps that have been fully processed from the restriction tracker.
1 parent 4dcc27d commit def9bf9

2 files changed

Lines changed: 65 additions & 5 deletions

File tree

sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamAction.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,14 +303,24 @@ public ProcessContinuation run(
303303
throw e;
304304
}
305305

306-
LOG.debug("[{}] change stream completed successfully", token);
307-
if (tracker.tryClaim(endTimestamp)) {
306+
LOG.debug(
307+
"[{}] change stream completed successfully up to {}", token, changeStreamQueryEndTimestamp);
308+
if (!tracker.tryClaim(changeStreamQueryEndTimestamp)) {
309+
return ProcessContinuation.stop();
310+
}
311+
312+
if (changeStreamQueryEndTimestamp.equals(endTimestamp)) {
308313
LOG.debug("[{}] Finishing partition", token);
314+
// TODO: This should be performed after the commit succeeds. Since bundle finalizers are not
315+
// guaranteed to be called, this needs to be performed in a subsequent fused stage.
309316
partitionMetadataDao.updateToFinished(token);
310317
metrics.decActivePartitionReadCounter();
311318
LOG.info("[{}] After attempting to finish the partition", token);
319+
return ProcessContinuation.stop();
312320
}
313-
return ProcessContinuation.stop();
321+
322+
LOG.info("[{}] Rescheduling partition where query completed due to not being finished", token);
323+
return ProcessContinuation.resume();
314324
}
315325

316326
private BundleFinalizer.Callback updateWatermarkCallback(
@@ -339,8 +349,8 @@ private boolean isTimestampOutOfRange(SpannerException e) {
339349
}
340350

341351
// Return (now + 2 mins) as the end timestamp for reading change streams. This is only used if
342-
// users want to run the connector forever. This approach works because Google Dataflow
343-
// checkpoints every 5s or 5MB output provided and the change stream query has deadline for 1 min.
352+
// users want to run the connector forever. If the end timestamp is reached, we will resume
353+
// processing from that timestamp on a subsequent DoFn execution.
344354
private Timestamp getNextReadChangeStreamEndTimestamp() {
345355
final Timestamp current = Timestamp.now();
346356
return Timestamp.ofTimeSecondsAndNanos(current.getSeconds() + 2 * 60, current.getNanos());

sdks/java/io/google-cloud-platform/src/test/java/org/apache/beam/sdk/io/gcp/spanner/changestreams/action/QueryChangeStreamActionTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
*/
1818
package org.apache.beam.sdk.io.gcp.spanner.changestreams.action;
1919

20+
import static org.apache.beam.sdk.io.gcp.spanner.changestreams.ChangeStreamsConstants.MAX_INCLUSIVE_END_AT;
2021
import static org.apache.beam.sdk.io.gcp.spanner.changestreams.model.PartitionMetadata.State.SCHEDULED;
2122
import static org.junit.Assert.assertEquals;
23+
import static org.junit.Assert.assertNotEquals;
2224
import static org.mockito.ArgumentMatchers.any;
2325
import static org.mockito.ArgumentMatchers.eq;
2426
import static org.mockito.Mockito.mock;
@@ -55,6 +57,7 @@
5557
import org.joda.time.Instant;
5658
import org.junit.Before;
5759
import org.junit.Test;
60+
import org.mockito.ArgumentCaptor;
5861

5962
public class QueryChangeStreamActionTest {
6063
private static final String PARTITION_TOKEN = "partitionToken";
@@ -613,6 +616,53 @@ public void testQueryChangeStreamWithStreamFinished() {
613616
assertEquals(ProcessContinuation.stop(), result);
614617
verify(partitionMetadataDao).updateWatermark(PARTITION_TOKEN, WATERMARK_TIMESTAMP);
615618
verify(partitionMetadataDao).updateToFinished(PARTITION_TOKEN);
619+
verify(metrics).decActivePartitionReadCounter();
620+
621+
verify(dataChangeRecordAction, never()).run(any(), any(), any(), any(), any(), any());
622+
verify(heartbeatRecordAction, never()).run(any(), any(), any(), any(), any());
623+
verify(childPartitionsRecordAction, never()).run(any(), any(), any(), any(), any());
624+
verify(partitionStartRecordAction, never()).run(any(), any(), any(), any(), any());
625+
verify(partitionEndRecordAction, never()).run(any(), any(), any(), any(), any());
626+
verify(partitionEventRecordAction, never()).run(any(), any(), any(), any(), any());
627+
}
628+
629+
@Test
630+
public void testQueryChangeStreamFinishedWithResume() {
631+
partition =
632+
PartitionMetadata.newBuilder()
633+
.setPartitionToken(PARTITION_TOKEN)
634+
.setParentTokens(Sets.newHashSet("parentToken"))
635+
.setStartTimestamp(PARTITION_START_TIMESTAMP)
636+
.setEndTimestamp(MAX_INCLUSIVE_END_AT)
637+
.setHeartbeatMillis(PARTITION_HEARTBEAT_MILLIS)
638+
.setState(SCHEDULED)
639+
.setWatermark(WATERMARK_TIMESTAMP)
640+
.setScheduledAt(Timestamp.now())
641+
.build();
642+
when(partitionMetadataMapper.from(any())).thenReturn(partition);
643+
644+
final ChangeStreamResultSet changeStreamResultSet = mock(ChangeStreamResultSet.class);
645+
final ArgumentCaptor<Timestamp> timestampCaptor = ArgumentCaptor.forClass(Timestamp.class);
646+
when(changeStreamDao.changeStreamQuery(
647+
eq(PARTITION_TOKEN),
648+
eq(PARTITION_START_TIMESTAMP),
649+
timestampCaptor.capture(),
650+
eq(PARTITION_HEARTBEAT_MILLIS)))
651+
.thenReturn(changeStreamResultSet);
652+
when(changeStreamResultSet.next()).thenReturn(false);
653+
when(watermarkEstimator.currentWatermark()).thenReturn(WATERMARK);
654+
when(restrictionTracker.tryClaim(any(Timestamp.class))).thenReturn(true);
655+
656+
final ProcessContinuation result =
657+
action.run(
658+
partition, restrictionTracker, outputReceiver, watermarkEstimator, bundleFinalizer);
659+
assertEquals(ProcessContinuation.resume(), result);
660+
assertNotEquals(MAX_INCLUSIVE_END_AT, timestampCaptor.getValue());
661+
662+
verify(restrictionTracker).tryClaim(timestampCaptor.getValue());
663+
verify(partitionMetadataDao).updateWatermark(PARTITION_TOKEN, WATERMARK_TIMESTAMP);
664+
verify(partitionMetadataDao, never()).updateToFinished(PARTITION_TOKEN);
665+
verify(metrics, never()).decActivePartitionReadCounter();
616666

617667
verify(dataChangeRecordAction, never()).run(any(), any(), any(), any(), any(), any());
618668
verify(heartbeatRecordAction, never()).run(any(), any(), any(), any(), any());

0 commit comments

Comments
 (0)