ferenc-csaky commented on code in PR #258:
URL:
https://github.com/apache/flink-connector-aws/pull/258#discussion_r4323266739
##########
flink-connector-aws/flink-connector-aws-kinesis-firehose/src/test/java/org/apache/flink/connector/firehose/sink/KinesisFirehoseSinkWriterTest.java:
##########
@@ -50,7 +50,7 @@ public class KinesisFirehoseSinkWriterTest {
@BeforeEach
void setup() throws IOException {
TestSinkInitContext sinkInitContext = new TestSinkInitContext();
- Properties sinkProperties =
AWSServicesTestUtils.createConfig("https://fake_aws_endpoint");
+ Properties sinkProperties =
AWSServicesTestUtils.createConfig("https://fake-aws-endpoint");
Review Comment:
Why is the `fake_aws_endpoint` → `fake-aws-endpoint` change needed? These
tests only build the writer and never send anything to that endpoint, and
nothing else in the PR seems to depend on it. If it isn't needed, please revert
it to keep the diff to the feature. (Same in `KinesisStreamsSinkWriterTest` and
`SqsSinkWriterTest`.)
##########
flink-connector-aws/flink-connector-dynamodb/src/main/java/org/apache/flink/connector/dynamodb/source/enumerator/DynamoDbStreamsSourceEnumerator.java:
##########
@@ -93,7 +94,10 @@ public DynamoDbStreamsSourceEnumerator(
this.shardAssignerContext = new ShardAssignerContext(splitAssignment,
context);
InitialPosition initialPosition =
sourceConfig.get(STREAM_INITIAL_POSITION);
if (state == null) {
- this.startTimestamp = Instant.now();
+ this.startTimestamp =
+ initialPosition == InitialPosition.AT_TIMESTAMP
+ ?
DynamodbStreamsSourceConfigUtil.parseInitialTimestamp(sourceConfig)
Review Comment:
The timestamp is parsed twice. Once in the source constructor, on the client
building the job graph, for validation. Then again here on the JobManager,
which gives the T that's actually used.
With a custom `flink.stream.initpos.timestamp.format` that has no zone (e.g.
`yyyy-MM-dd HH:mm:ss`), `SimpleDateFormat` uses the JVM default time zone, so
the two parses can give different instants. Example: a client on UTC+2 submits
`2026-10-20 10:00:00` at 10:30 local time. Validation reads it as 08:00Z, which
is in the past, so it passes. A JobManager on UTC then reads it as 10:00Z, 1.5
hours in the future. That's exactly the restart loop the validation is meant to
prevent, and in milder cases the job just starts from a different point than
was checked.
Could we resolve the `Instant` once in the `DynamoDbStreamsSource`
constructor, keep it as a field, and pass it to the enumerator? Then the
validated value and the value used are the same.
##########
flink-connector-aws/flink-connector-dynamodb/src/main/java/org/apache/flink/connector/dynamodb/source/enumerator/tracker/SplitTracker.java:
##########
@@ -179,13 +196,68 @@ private void addSplitToMapping(DynamoDbStreamsShardSplit
split) {
.add(split.splitId());
}
+ /**
+ * Adds shards discovered under the AT_TIMESTAMP initial position. Each
newly discovered shard
+ * (one that is not already tracked and is not an ancestor of an
already-tracked shard) is added
+ * at AT_TIMESTAMP: the service resolves the correct per-record position
via
+ * GetShardIterator(AT_TIMESTAMP), so a shard that straddles the timestamp
never emits
+ * pre-timestamp records and the position is independent of the discovery
path or restore
+ * timing. Ancestors of already-tracked shards are skipped because they
have already been
Review Comment:
The Javadoc says ancestors of tracked shards are skipped "because they have
already been processed". That isn't always true. In
`testAtTimestampMissedShardAddedByPeriodicDiscovery`, shard 0 was never tracked
or read. It's skipped only to keep parent-before-child order for 5 and 6, so
any records it has at or after T are dropped. That's a reasonable trade-off,
but could the Javadoc say so, and could we log a WARN when an untracked
ancestor gets skipped, since it means possible data loss?
Smaller things in the same method:
- The inline comment at lines 215–218 repeats the Javadoc.
- The `(a, b) -> a` merge function and the `shardId() != null` filter are
defensive checks the LATEST path doesn't have. Keep them consistent one way or
the other.
##########
flink-connector-aws/flink-connector-dynamodb/src/test/java/org/apache/flink/connector/dynamodb/source/config/DynamodbStreamsSourceConfigUtilTest.java:
##########
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.connector.dynamodb.source.config;
+
+import org.apache.flink.configuration.Configuration;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+
+import static
org.apache.flink.connector.dynamodb.source.config.DynamodbStreamsSourceConfigConstants.STREAM_INITIAL_POSITION;
+import static
org.apache.flink.connector.dynamodb.source.config.DynamodbStreamsSourceConfigConstants.STREAM_INITIAL_TIMESTAMP;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link DynamodbStreamsSourceConfigUtil}. */
+class DynamodbStreamsSourceConfigUtilTest {
+
+ @Test
+ void testParseInitialTimestampWithDefaultFormat() {
+ Configuration sourceConfig = new Configuration();
+ sourceConfig.set(STREAM_INITIAL_TIMESTAMP, "2024-05-01T12:00:00.000Z");
+
+ Instant parsed =
DynamodbStreamsSourceConfigUtil.parseInitialTimestamp(sourceConfig);
+
+ assertThat(parsed).isEqualTo(Instant.parse("2024-05-01T12:00:00Z"));
+ }
+
+ @Test
+ void testParseInitialTimestampFallsBackToEpochSeconds() {
+ Configuration sourceConfig = new Configuration();
+ // Not in the configured date format -> interpreted as epoch seconds.
+ sourceConfig.set(STREAM_INITIAL_TIMESTAMP, "1704067200");
+
+ Instant parsed =
DynamodbStreamsSourceConfigUtil.parseInitialTimestamp(sourceConfig);
+
+ assertThat(parsed).isEqualTo(Instant.ofEpochMilli(1704067200000L));
+ }
+
+ @Test
+ void testParseInitialTimestampThrowsWhenMissing() {
Review Comment:
Nit: the PR description says unparseable values are covered, but I can't
find a test for something like `"not-a-timestamp"` hitting the "Unable to
parse" path. There's also no test for a custom
`STREAM_INITIAL_TIMESTAMP_FORMAT`. Could you add both?
##########
pom.xml:
##########
@@ -53,7 +53,7 @@ under the License.
</scm>
<properties>
- <aws.sdkv2.version>2.40.3</aws.sdkv2.version>
+ <aws.sdkv2.version>2.56.0</aws.sdkv2.version>
Review Comment:
Bumping `aws.sdkv2.version` to 2.56.0 leaves the bundled NOTICE files out of
date. These three still list every `software.amazon.awssdk:*` artifact at
`2.40.3`:
- `flink-sql-connector-dynamodb/src/main/resources/META-INF/NOTICE`
-
`flink-sql-connector-aws-kinesis-streams/src/main/resources/META-INF/NOTICE`
-
`flink-sql-connector-aws-kinesis-firehose/src/main/resources/META-INF/NOTICE`
They need updating, including any new transitive SDK artifacts pulled in
between 2.40.3 and 2.56.0. FLINK-35819 did this for the previous bump.
Also, the PR description says "(see note above)" for the dependency, but
there's no note. What's the minimum SDK version that has `AT_TIMESTAMP` for
DynamoDB Streams?
##########
flink-connector-aws/flink-connector-dynamodb/src/main/java/org/apache/flink/connector/dynamodb/source/config/DynamodbStreamsSourceConfigUtil.java:
##########
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.connector.dynamodb.source.config;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.util.Preconditions;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+
+import static
org.apache.flink.connector.dynamodb.source.config.DynamodbStreamsSourceConfigConstants.STREAM_INITIAL_POSITION;
+import static
org.apache.flink.connector.dynamodb.source.config.DynamodbStreamsSourceConfigConstants.STREAM_INITIAL_TIMESTAMP;
+import static
org.apache.flink.connector.dynamodb.source.config.DynamodbStreamsSourceConfigConstants.STREAM_INITIAL_TIMESTAMP_FORMAT;
+
+/** Utility functions to use with {@link
DynamodbStreamsSourceConfigConstants}. */
+@Internal
+public class DynamodbStreamsSourceConfigUtil {
+
+ private DynamodbStreamsSourceConfigUtil() {
+ // private constructor to prevent initialization of utility class.
+ }
+
+ /**
+ * Parses the timestamp at which to start reading, used for the
AT_TIMESTAMP initial position.
+ * The value is parsed using the configured {@link
+ * DynamodbStreamsSourceConfigConstants#STREAM_INITIAL_TIMESTAMP_FORMAT};
if that fails it is
+ * interpreted as a (possibly fractional) epoch-second value.
+ *
+ * @param sourceConfig the configuration to parse the timestamp from
+ * @return the parsed initial timestamp
+ */
+ public static Instant parseInitialTimestamp(Configuration sourceConfig) {
+ String timestampString = sourceConfig.get(STREAM_INITIAL_TIMESTAMP);
+ if (timestampString == null) {
+ throw new IllegalArgumentException(
+ STREAM_INITIAL_TIMESTAMP.key()
+ + " must be set when "
+ + STREAM_INITIAL_POSITION.key()
+ + " is AT_TIMESTAMP.");
+ }
+ String format = sourceConfig.get(STREAM_INITIAL_TIMESTAMP_FORMAT);
+ try {
+ return new
SimpleDateFormat(format).parse(timestampString).toInstant();
+ } catch (ParseException | IllegalArgumentException e) {
+ // Fall back to interpreting the value as epoch seconds (may be
fractional).
+ try {
+ return Instant.ofEpochMilli((long)
(Double.parseDouble(timestampString) * 1000));
Review Comment:
Nit: the epoch-seconds fallback accepts some odd inputs without complaint:
- `"NaN"` becomes epoch 0 and silently behaves like TRIM_HORIZON.
- Negative values are accepted. The Kinesis source's validation message asks
for a "non-negative double value".
Maybe reject non-finite and negative values here.
Also, `SimpleDateFormat` is lenient by default and only parses a prefix of
the string. So `2024-02-30T…` rolls over to March 1, and trailing garbage is
ignored. That matches the Kinesis behaviour, but calling `setLenient(false)`
would cost nothing.
##########
flink-connector-aws/flink-connector-dynamodb/src/main/java/org/apache/flink/connector/dynamodb/source/enumerator/tracker/SplitTracker.java:
##########
@@ -298,6 +370,10 @@ public Map<String, DynamoDbStreamsShardSplit>
getKnownSplits() {
return knownSplits;
}
+ public Set<String> getFinishedSplits() {
Review Comment:
`getFinishedSplits()` has no callers in either main or test code, and it
returns the mutable internal set. Please remove it, or if it's needed, mark it
`@VisibleForTesting` and return an unmodifiable view.
##########
flink-connector-aws/flink-connector-dynamodb/src/test/java/org/apache/flink/connector/dynamodb/source/enumerator/tracker/SplitTrackerTest.java:
##########
@@ -539,4 +539,190 @@ private DynamoDbStreamsShardSplit getSplitFromShard(Shard
shard) {
return new DynamoDbStreamsShardSplit(
STREAM_ARN, shard.shardId(), StartingPosition.fromStart(),
shard.parentShardId());
}
+
+ @Test
+ public void testAtTimestampBootstrapAddsAllShardsWithTimestampPosition() {
+ Instant startTime = Instant.now();
+ SplitTracker splitTracker =
+ new SplitTracker(STREAM_ARN, InitialPosition.AT_TIMESTAMP,
startTime);
+
+ List<Shard> shards =
+ Arrays.asList(
+ generateShard(5, "5000", null, null), generateShard(6,
"6000", null, null));
+
+ splitTracker.addSplits(shards);
+
+ assertThat(splitTracker.getKnownSplits()).hasSize(2);
+ splitTracker
+ .getKnownSplits()
+ .values()
+ .forEach(
+ split ->
+ assertThat(split.getStartingPosition())
+
.isEqualTo(StartingPosition.atTimestamp(startTime)));
+ }
+
+ @Test
+ public void testAtTimestampRootShardGetsAtTimestamp() {
+ Instant startTime = Instant.now();
+ SplitTracker splitTracker =
+ new SplitTracker(STREAM_ARN, InitialPosition.AT_TIMESTAMP,
startTime);
+
+ // A shard with no tracked parent starts at AT_TIMESTAMP.
+ splitTracker.addSplits(Collections.singletonList(generateShard(0,
"100", null, null)));
+
+
assertThat(splitTracker.getKnownSplits().get(generateShardId(0)).getStartingPosition())
+ .isEqualTo(StartingPosition.atTimestamp(startTime));
+ }
+
+ @Test
+ public void testAtTimestampChildOfPreExistingParentGetsAtTimestamp() {
+ Instant startTime = Instant.now();
+ SplitTracker splitTracker =
+ new SplitTracker(STREAM_ARN, InitialPosition.AT_TIMESTAMP,
startTime);
+
+ // Cycle 1: parent (root) is tracked.
+ splitTracker.addSplits(Collections.singletonList(generateShard(0,
"100", null, null)));
+
+ // Cycle 2: parent has closed and a child appears. The child starts at
+ // AT_TIMESTAMP (not TRIM_HORIZON) regardless of the parent being
tracked before this cycle.
+ splitTracker.addSplits(
+ Arrays.asList(
+ generateShard(0, "100", "200", null),
+ generateShard(1, "200", null, generateShardId(0))));
+
+
assertThat(splitTracker.getKnownSplits().get(generateShardId(1)).getStartingPosition())
+ .isEqualTo(StartingPosition.atTimestamp(startTime));
+ }
+
+ @Test
+ public void testAtTimestampChildInheritsNewlyAddedParentPosition() {
+ Instant startTime = Instant.now();
+ SplitTracker splitTracker =
+ new SplitTracker(STREAM_ARN, InitialPosition.AT_TIMESTAMP,
startTime);
+
+ // Cycle 1: grandparent (root) is tracked.
+ splitTracker.addSplits(Collections.singletonList(generateShard(0,
"100", null, null)));
+
+ // Cycle 2: grandparent(0) closed; parent(1) and child(2) both appear
new in this cycle.
+ splitTracker.addSplits(
+ Arrays.asList(
+ generateShard(0, "100", "200", null),
+ generateShard(1, "200", "300", generateShardId(0)),
+ generateShard(2, "300", null, generateShardId(1))));
+
+ // parent(1): every shard starts at AT_TIMESTAMP.
+
assertThat(splitTracker.getKnownSplits().get(generateShardId(1)).getStartingPosition())
+ .isEqualTo(StartingPosition.atTimestamp(startTime));
+ // child(2): its parent(1) was added in THIS cycle at AT_TIMESTAMP, so
it inherits
Review Comment:
Nit: some test comments look left over from an earlier design where
positions were inherited:
- "so it inherits AT_TIMESTAMP too" (SplitTrackerTest:617), "inherits it"
(:688), and the test name
`testAtTimestampChildInheritsNewlyAddedParentPosition`. Every shard now gets
AT_TIMESTAMP unconditionally.
- "the child-handoff TRIM_HORIZON does not override…" (EnumeratorTest:277).
Under AT_TIMESTAMP the handoff now uses AT_TIMESTAMP.
- EnumeratorTest:224–226 has a broken line wrap ("dedup / keeps sync /
idempotent").
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]