This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/rocketmq-flink.git
The following commit(s) were added to refs/heads/main by this push:
new 7170770 [ISSUE #141] fix: commit transactions on the node holding the
context and close the remaining silent paths (#149)
7170770 is described below
commit 7170770e263688f403ff6e3e6cd1abe32117e836
Author: lizhimins <[email protected]>
AuthorDate: Tue Oct 27 14:28:17 2026 +0800
[ISSUE #141] fix: commit transactions on the node holding the context and
close the remaining silent paths (#149)
---
docs/remoting-connector.md | 8 ++
.../rocketmq/common/config/RocketMQOptions.java | 10 +-
.../connector/rocketmq/sink/InnerProducer.java | 14 ++
.../connector/rocketmq/sink/InnerProducerImpl.java | 76 +++++++---
.../rocketmq/sink/committer/SendCommittable.java | 19 +++
.../sink/committer/SendCommittableSerializer.java | 8 +-
.../rocketmq/sink/writer/RocketMQWriter.java | 51 +++++--
.../rocketmq/source/InnerConsumerImpl.java | 23 +++-
.../source/reader/RocketMQRecordEmitter.java | 56 ++++++--
.../source/reader/RocketMQSplitReader.java | 18 ++-
.../rocketmq/table/RocketMQDynamicTableSink.java | 8 --
.../table/RocketMQDynamicTableSinkFactory.java | 17 ++-
.../rocketmq/sink/InnerProducerImplTest.java | 153 +++++++++++++++++++++
.../committer/SendCommittableSerializerTest.java | 90 ++++++++++++
.../writer/RocketMQWriterQueueSelectorTest.java | 5 +
....java => RocketMQWriterSendPendingMaxTest.java} | 106 +++++++-------
... RocketMQWriterTransactionCommittableTest.java} | 101 ++++++--------
.../source/reader/RocketMQRecordEmitterTest.java | 90 +++++++++++-
.../reader/RocketMQSplitReaderPollTimeoutTest.java | 81 +++++++++++
.../table/RocketMQDynamicTableSinkFactoryTest.java | 60 +++++++-
.../RocketMQDynamicTableSinkMigrationTest.java | 2 -
21 files changed, 823 insertions(+), 173 deletions(-)
diff --git a/docs/remoting-connector.md b/docs/remoting-connector.md
index b31415a..c4c960a 100644
--- a/docs/remoting-connector.md
+++ b/docs/remoting-connector.md
@@ -50,6 +50,13 @@ RocketMQSink<String> sink = RocketMQSink.<String>builder()
- `AT_LEAST_ONCE` uses async sends with a pending counter and a flush barrier
on checkpoint.
- `EXACTLY_ONCE` uses `TransactionMQProducer` two-phase commit driven by Flink
checkpoints;
set `rocketmq.sink.transaction.timeout` larger than the checkpoint interval.
+- `EXACTLY_ONCE` also requires the **checkpoint interval to stay well below
the server-side
+ transaction state TTL**. A proxy keeps the transaction state of a half
message in its own memory
+ and drops it after `transactionDataExpireMillis` (30 seconds by default).
Flink only commits at
+ the end of a checkpoint, so the interval plus the checkpoint duration is the
delay that state has
+ to survive; a commit arriving after it is discarded while still being
answered with a success
+ code, so those records never appear and no error is raised. Raise the
server-side TTL if the job
+ needs longer checkpoints.
## Client options (`rocketmq.client.*`)
@@ -65,6 +72,7 @@ Shared by source and sink.
| `rocketmq.client.network.timeout.ms` | Long | 30000 | Client API timeout |
| `rocketmq.client.callback.threads` | Integer | CPU cores | Client callback
executor threads |
| `rocketmq.client.partition.discovery.interval.ms` | Long | 10000 |
Route/partition discovery interval |
+| `rocketmq.client.poll.timeout` | Long | 1000 | How long a source fetch
blocks while no record is available. Returns as soon as records arrive, so it
costs no data-path latency; it also bounds how long a fetch can outlive a close
request |
| `rocketmq.client.unitMode` / `rocketmq.client.unitName` | Boolean / String |
false / (none) | Unit mode routing |
| `rocketmq.client.debug` | Boolean | false | Verbose client logs |
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/common/config/RocketMQOptions.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/common/config/RocketMQOptions.java
index 9e7eef7..98d4de8 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/common/config/RocketMQOptions.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/common/config/RocketMQOptions.java
@@ -137,6 +137,12 @@ public class RocketMQOptions {
public static final ConfigOption<Long> POLL_TIMEOUT =
ConfigOptions.key(CLIENT_CONFIG_PREFIX + "poll.timeout")
.longType()
- .defaultValue(10L)
- .withDescription("how long to wait before giving up, the
unit is milliseconds");
+ .defaultValue(1000L)
+ .withDescription(
+ "How long a fetch blocks while no record is
available, in milliseconds."
+ + " The poll returns as soon as records
arrive, so this only"
+ + " bounds the idle wait and how long the
fetcher thread takes to"
+ + " notice a wakeup or a close request.
Keep it small enough for"
+ + " a timely shutdown and large enough to
stop an idle job from"
+ + " spinning the fetch loop.");
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducer.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducer.java
index aef6614..84f6614 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducer.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducer.java
@@ -73,6 +73,20 @@ public interface InnerProducer extends AutoCloseable {
*/
CompletableFuture<SendResult> sendMessageInTransaction(Message message);
+ /**
+ * Resolves the address this producer uses to reach the broker that
accepted the given send
+ * result, or null when the publish route does not know that broker.
+ *
+ * <p>The writer must call this right after a transactional send and hand
the address to the
+ * committer along with the {@link SendCommittable}: the node that
accepted the half message is
+ * the only one that can end the transaction, and a committer resolving
the address from its own
+ * route table may well pick a different node.
+ *
+ * @param sendResult the result of a send performed by this producer
+ * @return the address the send went to, or null if it cannot be resolved
+ */
+ String resolvePublishAddress(SendResult sendResult);
+
/**
* Commits the send operation identified by the specified SendCommittable
object.
*
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImpl.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImpl.java
index 6595f7a..156e259 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImpl.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImpl.java
@@ -295,6 +295,31 @@ public class InnerProducerImpl implements InnerProducer {
Runnable::run);
}
+ @Override
+ public String resolvePublishAddress(final SendResult sendResult) {
+ return this.mqClientInstance.findBrokerAddressInPublish(
+ this.mqClientInstance.getBrokerNameFromMessageQueue(
+
producer.queueWithNamespace(sendResult.getMessageQueue())));
+ }
+
+ private String resolvePublishAddress(final String brokerName, final String
topic) {
+ String brokerAddress =
this.mqClientInstance.findBrokerAddressInPublish(brokerName);
+ if (brokerAddress == null) {
+ // The Committer owns a producer that never sends anything, so its
publish route table
+ // stays empty and findBrokerAddressInPublish() returns null.
invokeSync() then falls
+ // back to a name server channel, which cannot commit the half
message, and the reply is
+ // not a SUCCESS the caller would notice -- every record of the
checkpoint is lost
+ // without a single error. Refresh the route for the topic the
half message belongs to
+ // before resolving again.
+ LOG.warn(
+ "Publish route for {} is not loaded yet, refreshing before
ending transaction",
+ brokerName);
+ this.mqClientInstance.updateTopicRouteInfoFromNameServer(topic);
+ brokerAddress =
this.mqClientInstance.findBrokerAddressInPublish(brokerName);
+ }
+ return brokerAddress;
+ }
+
public void endTransaction(
final SendCommittable sendCommittable, final TransactionResult
transactionResult) {
@@ -302,20 +327,14 @@ public class InnerProducerImpl implements InnerProducer {
final String brokerName =
this.mqClientInstance.getBrokerNameFromMessageQueue(
producer.queueWithNamespace(sendCommittable.getMessageQueue()));
- String brokerAddress =
this.mqClientInstance.findBrokerAddressInPublish(brokerName);
- if (brokerAddress == null) {
- // The Committer owns a producer that never sends anything, so
its publish route
- // table stays empty and findBrokerAddressInPublish() returns
null. invokeSync()
- // then falls back to a name server channel, which cannot
commit the half message,
- // and the reply is not a SUCCESS the caller would notice --
every record of the
- // checkpoint is lost without a single error. Refresh the
route for the topic the
- // half message belongs to before resolving again.
- LOG.warn(
- "Publish route for {} is not loaded yet, refreshing
before ending transaction",
- brokerName);
- this.mqClientInstance.updateTopicRouteInfoFromNameServer(
- sendCommittable.getTopic());
- brokerAddress =
this.mqClientInstance.findBrokerAddressInPublish(brokerName);
+ // The node that accepted the half message is the only one holding
its transaction
+ // context, so prefer the address the writer recorded at send
time. Resolving it here
+ // from the committer's own route table can pick a different node,
and a commit sent to
+ // a node without the context is discarded while still being
answered with a success
+ // code -- the record is lost with nothing to show for it.
+ String brokerAddress = sendCommittable.getBrokerAddress();
+ if (StringUtils.isNullOrWhitespaceOnly(brokerAddress)) {
+ brokerAddress = resolvePublishAddress(brokerName,
sendCommittable.getTopic());
}
if (brokerAddress == null) {
throw new IllegalStateException(
@@ -413,16 +432,33 @@ public class InnerProducerImpl implements InnerProducer {
@Override
public CompletableFuture<Void> commit(SendCommittable sendCommittable) {
- return CompletableFuture.runAsync(
- () -> endTransaction(sendCommittable,
TransactionResult.COMMIT),
- producer.getExecutorService());
+ return endTransactionAsync(sendCommittable, TransactionResult.COMMIT);
}
@Override
public CompletableFuture<Void> rollback(SendCommittable sendCommittable) {
- return CompletableFuture.runAsync(
- () -> endTransaction(sendCommittable,
TransactionResult.ROLLBACK),
- producer.getExecutorService());
+ return endTransactionAsync(sendCommittable,
TransactionResult.ROLLBACK);
+ }
+
+ /**
+ * Ends the transaction on the calling thread. Submitting to the
producer's callback executor
+ * instead looked parallel but was lossy: that executor is bounded (4
threads, 2000 queued
+ * tasks, abort policy), so a checkpoint carrying more committables than
the queue holds
+ * rejected the surplus, and a rejected commit retried after the
server-side transaction context
+ * expired is silently dropped. The committer blocks until every request
of the checkpoint is
+ * done anyway, so running here turns a burst into visible checkpoint
duration instead of lost
+ * half messages.
+ */
+ private CompletableFuture<Void> endTransactionAsync(
+ final SendCommittable sendCommittable, final TransactionResult
transactionResult) {
+ try {
+ endTransaction(sendCommittable, transactionResult);
+ return CompletableFuture.completedFuture(null);
+ } catch (Exception e) {
+ CompletableFuture<Void> failed = new CompletableFuture<>();
+ failed.completeExceptionally(e);
+ return failed;
+ }
}
@Override
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittable.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittable.java
index a69e737..76de00a 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittable.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittable.java
@@ -42,6 +42,14 @@ public class SendCommittable {
private String transactionId;
+ /**
+ * Address of the node that accepted the half message, resolved from the
writer's own publish
+ * route right after the send. A proxy keeps the transaction context of a
half message in its
+ * own memory, so ending the transaction on any other node is discarded
while still being
+ * answered with a success code.
+ */
+ private String brokerAddress;
+
public SendCommittable() {}
public SendCommittable(SendResult sendResult) {
@@ -110,6 +118,14 @@ public class SendCommittable {
this.transactionId = transactionId;
}
+ public String getBrokerAddress() {
+ return brokerAddress;
+ }
+
+ public void setBrokerAddress(String brokerAddress) {
+ this.brokerAddress = brokerAddress;
+ }
+
public MessageQueue getMessageQueue() {
return new MessageQueue(topic, brokerName, queueId);
}
@@ -148,6 +164,9 @@ public class SendCommittable {
+ ", transactionId='"
+ transactionId
+ '\''
+ + ", brokerAddress='"
+ + brokerAddress
+ + '\''
+ '}';
}
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittableSerializer.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittableSerializer.java
index 4b64873..a44277f 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittableSerializer.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittableSerializer.java
@@ -28,7 +28,9 @@ import java.io.IOException;
/** A serializer used to serialize {@link SendCommittable}. */
public class SendCommittableSerializer implements
SimpleVersionedSerializer<SendCommittable> {
- private static final int CURRENT_VERSION = 1;
+ // Version 2 adds the address of the node that accepted the half message.
Committables written
+ // by version 1 are still readable: they simply carry no address and fall
back to route lookup.
+ private static final int CURRENT_VERSION = 2;
@Override
public int getVersion() {
@@ -46,6 +48,7 @@ public class SendCommittableSerializer implements
SimpleVersionedSerializer<Send
out.writeUTF(obj.getMsgId());
out.writeUTF(obj.getOffsetMsgId());
out.writeUTF(obj.getTransactionId());
+ out.writeUTF(obj.getBrokerAddress() != null ?
obj.getBrokerAddress() : "");
out.flush();
return stream.toByteArray();
}
@@ -63,6 +66,9 @@ public class SendCommittableSerializer implements
SimpleVersionedSerializer<Send
sendCommittable.setMsgId(in.readUTF());
sendCommittable.setOffsetMsgId(in.readUTF());
sendCommittable.setTransactionId(in.readUTF());
+ if (version >= 2) {
+ sendCommittable.setBrokerAddress(in.readUTF());
+ }
return sendCommittable;
}
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriter.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriter.java
index 59ad65c..6916a3d 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriter.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriter.java
@@ -48,6 +48,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -62,10 +63,16 @@ public class RocketMQWriter<IN>
private final MessageQueueSelector messageQueueSelector;
private final RocketMQSinkContext rocketmqSinkContext;
private final RocketMQSerializationSchema<IN> serializationSchema;
- private final Map<String, SendResult> sendResultMap;
+ private final Map<String, SendCommittable> committableMap;
private final int sendRetryTimes;
private final long sendSleepTimeMs;
private final AtomicLong pendingRecords = new AtomicLong(0);
+ /**
+ * Bounds the in-flight async sends. {@code send.pending.max} was declared
with exactly this
+ * description but never read, so the send buffer grew without limit under
a slow broker.
+ */
+ private final Semaphore sendPermits;
+
private final AtomicReference<Throwable> asyncSendException = new
AtomicReference<>();
public RocketMQWriter(
@@ -91,10 +98,12 @@ public class RocketMQWriter<IN>
this.messageQueueSelector = messageQueueSelector;
this.serializationSchema = serializationSchema;
this.rocketmqSinkContext = new RocketMQSinkContextImpl(initContext,
configuration);
- this.sendResultMap = new ConcurrentHashMap<>();
+ this.committableMap = new ConcurrentHashMap<>();
this.sendRetryTimes =
configuration.getInteger(RocketMQSinkOptions.SEND_RETRY_TIMES);
this.sendSleepTimeMs =
configuration.getLong(RocketMQSinkOptions.OPTIONAL_WRITE_SLEEP_TIME_MS);
+ this.sendPermits =
+ new
Semaphore(configuration.getInteger(RocketMQSinkOptions.SEND_PENDING_MAX));
try {
serializationSchema.open(
initContext.asSerializationSchemaInitializationContext(),
rocketmqSinkContext);
@@ -121,19 +130,35 @@ public class RocketMQWriter<IN>
return;
}
+ try {
+ sendPermits.acquire();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Interrupted while waiting for send
capacity", e);
+ }
pendingRecords.incrementAndGet();
// The element is handed to the selector as its sharding argument,
which is what lets a
// HashMessageQueueSelector route records that share a key to the same
queue.
- CompletableFuture<SendResult> sendFuture =
- messageQueueSelector == null
- ? producer.send(message)
- : producer.send(message, messageQueueSelector,
element);
+ CompletableFuture<SendResult> sendFuture;
+ try {
+ sendFuture =
+ messageQueueSelector == null
+ ? producer.send(message)
+ : producer.send(message, messageQueueSelector,
element);
+ } catch (RuntimeException | Error e) {
+ // A synchronously throwing send never reaches whenComplete, so
the capacity and the
+ // pending count have to be given back here or the writer would
wedge at the limit.
+ pendingRecords.decrementAndGet();
+ sendPermits.release();
+ throw e;
+ }
sendFuture.whenComplete(
(sendResult, throwable) -> {
if (throwable != null) {
asyncSendException.compareAndSet(null, throwable);
}
pendingRecords.decrementAndGet();
+ sendPermits.release();
});
}
@@ -143,7 +168,12 @@ public class RocketMQWriter<IN>
while (attempt <= sendRetryTimes) {
try {
SendResult sendResult =
producer.sendMessageInTransaction(message).get();
- sendResultMap.put(sendResult.getTransactionId(), sendResult);
+ SendCommittable committable = new SendCommittable(sendResult);
+ // Record the address the half message was actually accepted
by. Only that node
+ // holds the transaction context needed to end it, and the
committer owns a
+ // producer of its own whose route table may well resolve to a
different node.
+
committable.setBrokerAddress(producer.resolvePublishAddress(sendResult));
+ committableMap.put(sendResult.getTransactionId(), committable);
return; // success
} catch (Exception e) {
lastException = e;
@@ -192,12 +222,9 @@ public class RocketMQWriter<IN>
public Collection<SendCommittable> prepareCommit() throws IOException,
InterruptedException {
LOG.info("Prepare commit");
if (deliveryGuarantee == DeliveryGuarantee.EXACTLY_ONCE) {
- final List<SendCommittable> committables = new ArrayList<>();
- for (SendResult sendResult : sendResultMap.values()) {
- committables.add(new SendCommittable(sendResult));
- }
+ final List<SendCommittable> committables = new
ArrayList<>(committableMap.values());
LOG.info("Committable size={}.", committables.size());
- sendResultMap.clear();
+ committableMap.clear();
return committables;
}
return Collections.emptyList();
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/InnerConsumerImpl.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/InnerConsumerImpl.java
index ef621cc..61d13f4 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/InnerConsumerImpl.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/InnerConsumerImpl.java
@@ -288,11 +288,12 @@ public class InnerConsumerImpl implements InnerConsumer {
@Override
public void wakeup() {
// Deliberately does not pause the assigned queues.
DefaultLitePullConsumer offers no
- // interruptible poll, but poll(Duration) is already bounded by a
short timeout, so the
- // fetch loop never blocks long enough to need forcing. Pausing here
without a matching
- // resume() would permanently stop delivering records while the client
keeps pulling and
- // advancing offsets in the background -- silent message loss with
nothing logged.
- // Use pause(Collection)/resume(Collection) as a pair if queue pausing
is ever needed.
+ // interruptible poll, but poll(Duration) is bounded by
rocketmq.client.poll.timeout, so the
+ // fetch loop comes back on its own within that bound instead of
needing to be forced.
+ // Pausing here without a matching resume() would permanently stop
delivering records while
+ // the client keeps pulling and advancing offsets in the background --
silent message loss
+ // with nothing logged. Use pause(Collection)/resume(Collection) as a
pair if queue pausing
+ // is ever needed.
LOG.debug(
"Wakeup requested; relying on the bounded poll timeout instead
of pausing queues");
}
@@ -463,7 +464,17 @@ public class InnerConsumerImpl implements InnerConsumer {
@Override
public CompletableFuture<Void> commitOffset(MessageQueue messageQueue,
long offset) {
return CompletableFuture.runAsync(
- () -> consumer.getOffsetStore().updateOffset(messageQueue,
offset, true),
+ () -> {
+ // The broker copy of the offset is display-only:
positioning always comes from
+ // Flink state. Publishing the checkpointed offset --
rollbacks included, hence
+ // increaseOnly=false -- keeps consumer progress and lag
monitoring truthful
+ // instead of leaving a stale high-water mark; the
previous code updated only
+ // the
+ // local offset store with increaseOnly=true and never
persisted, so it had no
+ // effect at all.
+ consumer.getOffsetStore().updateOffset(messageQueue,
offset, false);
+ consumer.getOffsetStore().persist(messageQueue);
+ },
commonExecutorService);
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitter.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitter.java
index 12afce2..f72151f 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitter.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitter.java
@@ -24,12 +24,17 @@ import
org.apache.flink.connector.rocketmq.source.reader.deserializer.RocketMQDe
import
org.apache.flink.connector.rocketmq.source.split.RocketMQSourceSplitState;
import org.apache.flink.util.Collector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.io.IOException;
/** The {@link RecordEmitter} implementation for {@link RocketMQSourceReader}.
*/
public class RocketMQRecordEmitter<T>
implements RecordEmitter<MessageView, T, RocketMQSourceSplitState> {
+ private static final Logger LOG =
LoggerFactory.getLogger(RocketMQRecordEmitter.class);
+
private final RocketMQDeserializationSchema<T> deserializationSchema;
private final SourceOutputWrapper<T> sourceOutputWrapper = new
SourceOutputWrapper<>();
@@ -44,18 +49,34 @@ public class RocketMQRecordEmitter<T>
sourceOutputWrapper.setSourceOutput(output);
sourceOutputWrapper.setTimestamp(element.getEventTime());
+ sourceOutputWrapper.resetCollectError();
try {
deserializationSchema.deserialize(element, sourceOutputWrapper);
} catch (Exception e) {
- // Naming the record makes a poison message identifiable from the
exception alone;
- // otherwise the only way to find it is to replay the whole queue.
- throw new IOException(
- String.format(
- "Failed to deserialize message %s from topic %s,
queue %d, offset %d",
- element.getMessageId(),
- element.getTopic(),
- element.getQueueId(),
- element.getQueueOffset()),
+ RuntimeException collectError =
sourceOutputWrapper.getCollectError();
+ if (collectError != null) {
+ // The record deserialized fine; the failure came from the
downstream sink while the
+ // already-deserialized record was being emitted (collect runs
the writer inline).
+ // Dropping it here would hide a real write failure behind a
"deserialization" warn.
+ throw new IOException(
+ String.format(
+ "Failed to emit message %s from topic %s,
queue %d, offset %d",
+ element.getMessageId(),
+ element.getTopic(),
+ element.getQueueId(),
+ element.getQueueOffset()),
+ collectError);
+ }
+ // A record that cannot be deserialized is dropped instead of
failing the task: with a
+ // throwing schema the job would otherwise loop "fail, restore,
re-read the same record"
+ // forever and rewrite everything it had already emitted. Naming
the record keeps the
+ // poison message identifiable from the log line alone.
+ LOG.warn(
+ "Dropped message {} from topic {}, queue {}, offset {}
because deserialization failed",
+ element.getMessageId(),
+ element.getTopic(),
+ element.getQueueId(),
+ element.getQueueOffset(),
e);
}
splitState.setCurrentOffset(element.getQueueOffset() + 1);
@@ -67,9 +88,16 @@ public class RocketMQRecordEmitter<T>
private long timestamp;
+ private RuntimeException collectError;
+
@Override
public void collect(T record) {
- sourceOutput.collect(record, timestamp);
+ try {
+ sourceOutput.collect(record, timestamp);
+ } catch (RuntimeException e) {
+ collectError = e;
+ throw e;
+ }
}
@Override
@@ -82,5 +110,13 @@ public class RocketMQRecordEmitter<T>
private void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
+
+ private void resetCollectError() {
+ collectError = null;
+ }
+
+ private RuntimeException getCollectError() {
+ return collectError;
+ }
}
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQSplitReader.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQSplitReader.java
index 17c7e82..294b2d4 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQSplitReader.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQSplitReader.java
@@ -19,6 +19,7 @@
package org.apache.flink.connector.rocketmq.source.reader;
import org.apache.flink.annotation.Internal;
+import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.api.connector.source.SourceReaderContext;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
@@ -110,6 +111,21 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
SourceReaderContext sourceReaderContext,
RocketMQDeserializationSchema<T> deserializationSchema,
RocketMQSourceReaderMetrics rocketmqSourceReaderMetrics) {
+ this(
+ configuration,
+ sourceReaderContext,
+ deserializationSchema,
+ rocketmqSourceReaderMetrics,
+ new InnerConsumerImpl(configuration));
+ }
+
+ @VisibleForTesting
+ RocketMQSplitReader(
+ Configuration configuration,
+ SourceReaderContext sourceReaderContext,
+ RocketMQDeserializationSchema<T> deserializationSchema,
+ RocketMQSourceReaderMetrics rocketmqSourceReaderMetrics,
+ InnerConsumer consumer) {
this.configuration = configuration;
this.sourceReaderContext = sourceReaderContext;
@@ -120,7 +136,7 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
this.initFinish = false;
this.lock = new SpinLock();
- this.consumer = new InnerConsumerImpl(configuration);
+ this.consumer = consumer;
this.consumer.start();
this.rocketmqSourceReaderMetrics = rocketmqSourceReaderMetrics;
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSink.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSink.java
index 1a4bb04..a13146d 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSink.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSink.java
@@ -59,7 +59,6 @@ public class RocketMQDynamicTableSink implements
DynamicTableSink, SupportsWriti
private final String accessKey;
private final String secretKey;
- private final long retryTimes;
private final long sleepTime;
private final boolean isDynamicTag;
@@ -86,7 +85,6 @@ public class RocketMQDynamicTableSink implements
DynamicTableSink, SupportsWriti
String dynamicColumn,
String fieldDelimiter,
String encoding,
- long retryTimes,
long sleepTime,
boolean isDynamicTag,
boolean isDynamicTagIncluded,
@@ -110,7 +108,6 @@ public class RocketMQDynamicTableSink implements
DynamicTableSink, SupportsWriti
dynamicColumn,
fieldDelimiter,
encoding,
- retryTimes,
sleepTime,
isDynamicTag,
isDynamicTagIncluded,
@@ -135,7 +132,6 @@ public class RocketMQDynamicTableSink implements
DynamicTableSink, SupportsWriti
String dynamicColumn,
String fieldDelimiter,
String encoding,
- long retryTimes,
long sleepTime,
boolean isDynamicTag,
boolean isDynamicTagIncluded,
@@ -157,7 +153,6 @@ public class RocketMQDynamicTableSink implements
DynamicTableSink, SupportsWriti
this.dynamicColumn = dynamicColumn;
this.fieldDelimiter = fieldDelimiter;
this.encoding = encoding;
- this.retryTimes = retryTimes;
this.sleepTime = sleepTime;
this.isDynamicTag = isDynamicTag;
this.isDynamicTagIncluded = isDynamicTagIncluded;
@@ -208,8 +203,6 @@ public class RocketMQDynamicTableSink implements
DynamicTableSink, SupportsWriti
sinkConfig.setLong(RocketMQSinkOptions.SEND_TIMEOUT, sendTimeout);
sinkConfig.setInteger(RocketMQSinkOptions.SEND_PENDING_MAX,
sendPendingMax);
sinkConfig.setInteger(RocketMQSinkOptions.EXECUTOR_NUM, executorNum);
- // Pass legacy retry config that was previously stored but never
forwarded (P1 fix)
- sinkConfig.setInteger(RocketMQSinkOptions.OPTIONAL_WRITE_RETRY_TIMES,
(int) retryTimes);
sinkConfig.setLong(RocketMQSinkOptions.OPTIONAL_WRITE_SLEEP_TIME_MS,
sleepTime);
sinkBuilder.setConfig(sinkConfig);
@@ -244,7 +237,6 @@ public class RocketMQDynamicTableSink implements
DynamicTableSink, SupportsWriti
dynamicColumn,
fieldDelimiter,
encoding,
- retryTimes,
sleepTime,
isDynamicTag,
isDynamicTagIncluded,
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactory.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactory.java
index 3c5f240..2c11e92 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactory.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactory.java
@@ -111,7 +111,6 @@ public class RocketMQDynamicTableSinkFactory implements
DynamicTableSinkFactory
String dynamicColumn =
properties.getString(OPTIONAL_WRITE_DYNAMIC_TAG_COLUMN);
String encoding = properties.getString(OPTIONAL_ENCODING);
String fieldDelimiter = properties.getString(OPTIONAL_FIELD_DELIMITER);
- int retryTimes = properties.getInteger(OPTIONAL_WRITE_RETRY_TIMES);
long sleepTimeMs = properties.getLong(OPTIONAL_WRITE_SLEEP_TIME_MS);
boolean isDynamicTag =
properties.getBoolean(OPTIONAL_WRITE_IS_DYNAMIC_TAG);
boolean isDynamicTagIncluded =
@@ -123,7 +122,20 @@ public class RocketMQDynamicTableSinkFactory implements
DynamicTableSinkFactory
keyColumns = keyColumnsConfig.split(",");
}
String deliveryGuarantee =
properties.getString(RocketMQSinkOptions.DELIVERY_GUARANTEE);
- int sendRetryTimes =
properties.getInteger(RocketMQSinkOptions.SEND_RETRY_TIMES);
+ // `retry.times` is the legacy DDL name of the send retry count. The
writer only reads
+ // SEND_RETRY_TIMES, so honour the legacy key when the new one is
absent instead of
+ // silently dropping it; with neither set the documented default (3)
applies.
+ int sendRetryTimes =
+ properties
+ .getOptional(RocketMQSinkOptions.SEND_RETRY_TIMES)
+ .orElseGet(
+ () ->
+ properties
+
.getOptional(OPTIONAL_WRITE_RETRY_TIMES)
+ .orElseGet(
+ () ->
+
RocketMQSinkOptions.SEND_RETRY_TIMES
+
.defaultValue()));
long sendTimeout =
properties.getLong(RocketMQSinkOptions.SEND_TIMEOUT);
int sendPendingMax =
properties.getInteger(RocketMQSinkOptions.SEND_PENDING_MAX);
int executorNum =
properties.getInteger(RocketMQSinkOptions.EXECUTOR_NUM);
@@ -144,7 +156,6 @@ public class RocketMQDynamicTableSinkFactory implements
DynamicTableSinkFactory
dynamicColumn,
fieldDelimiter,
encoding,
- retryTimes,
sleepTimeMs,
isDynamicTag,
isDynamicTagIncluded,
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImplTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImplTest.java
index 84a216e..7c0b016 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImplTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/InnerProducerImplTest.java
@@ -18,11 +18,23 @@
package org.apache.flink.connector.rocketmq.sink;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.sink.committer.SendCommittable;
+import org.apache.rocketmq.client.impl.MQClientAPIImpl;
+import org.apache.rocketmq.client.impl.factory.MQClientInstance;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
+import org.apache.rocketmq.remoting.exception.RemotingSendRequestException;
+import org.apache.rocketmq.remoting.netty.NettyRemotingClient;
+import org.apache.rocketmq.remoting.protocol.RemotingCommand;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@@ -87,4 +99,145 @@ class InnerProducerImplTest {
assertThat(producer.getRetryTimesWhenSendAsyncFailed()).isEqualTo(3);
}
}
+
+ /**
+ * The node that accepted the half message is the only one holding its
transaction context, so
+ * the commit must go to the address the writer recorded. Resolving it
from the committer's own
+ * route table can name another node, which discards the commit and still
answers with a success
+ * code.
+ */
+ @Test
+ void commitGoesToTheAddressRecordedByTheWriterTest() throws Exception {
+ MQClientInstance clientInstance = Mockito.mock(MQClientInstance.class);
+ NettyRemotingClient remotingClient = stubRemoting(clientInstance);
+
+ try (InnerProducerImpl impl = new InnerProducerImpl(configuration())) {
+ setClientInstance(impl, clientInstance);
+
+ impl.endTransaction(committable("10.0.38.29:8080"),
TransactionResult.COMMIT);
+
+ Mockito.verify(remotingClient)
+ .invokeSync(
+ Mockito.eq("10.0.38.29:8080"),
+ Mockito.any(RemotingCommand.class),
+ Mockito.anyLong());
+ Mockito.verify(clientInstance, Mockito.never())
+ .findBrokerAddressInPublish(Mockito.anyString());
+ }
+ }
+
+ /** Committables restored from state written before the address was
recorded have no address. */
+ @Test
+ void commitFallsBackToThePublishRouteWithoutRecordedAddressTest() throws
Exception {
+ MQClientInstance clientInstance = Mockito.mock(MQClientInstance.class);
+
Mockito.when(clientInstance.getBrokerNameFromMessageQueue(Mockito.any()))
+ .thenReturn("broker-a");
+ Mockito.when(clientInstance.findBrokerAddressInPublish("broker-a"))
+ .thenReturn("10.0.26.60:8080");
+ NettyRemotingClient remotingClient = stubRemoting(clientInstance);
+
+ try (InnerProducerImpl impl = new InnerProducerImpl(configuration())) {
+ setClientInstance(impl, clientInstance);
+
+ impl.endTransaction(committable(null), TransactionResult.COMMIT);
+
+ Mockito.verify(remotingClient)
+ .invokeSync(
+ Mockito.eq("10.0.26.60:8080"),
+ Mockito.any(RemotingCommand.class),
+ Mockito.anyLong());
+ }
+ }
+
+ /**
+ * A checkpoint can carry thousands of committables; committing them must
not depend on a
+ * bounded executor queue that rejects the surplus (a rejected commit is a
lost commit once the
+ * server-side transaction context expires).
+ */
+ @Test
+ void commitBurstShouldNotRejectTest() throws Exception {
+ MQClientInstance clientInstance = Mockito.mock(MQClientInstance.class);
+ NettyRemotingClient remotingClient = stubRemoting(clientInstance);
+
+ try (InnerProducerImpl impl = new InnerProducerImpl(configuration())) {
+ setClientInstance(impl, clientInstance);
+
+ List<CompletableFuture<Void>> futures = new ArrayList<>();
+ for (int i = 0; i < 3000; i++) {
+ futures.add(impl.commit(committable("10.0.26.60:8080")));
+ }
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
+ .get(30, TimeUnit.SECONDS);
+ Mockito.verify(remotingClient, Mockito.times(3000))
+ .invokeSync(
+ Mockito.eq("10.0.26.60:8080"),
+ Mockito.any(RemotingCommand.class),
+ Mockito.anyLong());
+ }
+ }
+
+ @Test
+ void commitFailureShouldCompleteTheFutureExceptionallyTest() throws
Exception {
+ MQClientInstance clientInstance = Mockito.mock(MQClientInstance.class);
+ MQClientAPIImpl api = Mockito.mock(MQClientAPIImpl.class);
+ NettyRemotingClient remotingClient =
Mockito.mock(NettyRemotingClient.class);
+ Mockito.when(clientInstance.getMQClientAPIImpl()).thenReturn(api);
+ Mockito.when(api.getRemotingClient()).thenReturn(remotingClient);
+ Mockito.when(
+ remotingClient.invokeSync(
+ Mockito.anyString(),
+ Mockito.any(RemotingCommand.class),
+ Mockito.anyLong()))
+ .thenThrow(new
RemotingSendRequestException("10.0.26.60:8080"));
+
+ try (InnerProducerImpl impl = new InnerProducerImpl(configuration())) {
+ setClientInstance(impl, clientInstance);
+
+ CompletableFuture<Void> future =
impl.commit(committable("10.0.26.60:8080"));
+ assertThat(future).isCompletedExceptionally();
+ }
+ }
+
+ private static Configuration configuration() {
+ Configuration config = new Configuration();
+ config.setString(RocketMQSinkOptions.ENDPOINTS, "127.0.0.1:9876");
+ config.setString(RocketMQSinkOptions.PRODUCER_GROUP, "test-group");
+ return config;
+ }
+
+ private static NettyRemotingClient stubRemoting(MQClientInstance
clientInstance)
+ throws Exception {
+ MQClientAPIImpl api = Mockito.mock(MQClientAPIImpl.class);
+ NettyRemotingClient remotingClient =
Mockito.mock(NettyRemotingClient.class);
+ Mockito.when(clientInstance.getMQClientAPIImpl()).thenReturn(api);
+ Mockito.when(api.getRemotingClient()).thenReturn(remotingClient);
+ Mockito.when(
+ remotingClient.invokeSync(
+ Mockito.anyString(),
+ Mockito.any(RemotingCommand.class),
+ Mockito.anyLong()))
+
.thenReturn(RemotingCommand.createResponseCommand(ResponseCode.SUCCESS, ""));
+ return remotingClient;
+ }
+
+ private static void setClientInstance(InnerProducerImpl impl,
MQClientInstance instance)
+ throws Exception {
+ Field field =
InnerProducerImpl.class.getDeclaredField("mqClientInstance");
+ field.setAccessible(true);
+ field.set(impl, instance);
+ }
+
+ private static SendCommittable committable(String brokerAddress) {
+ SendCommittable committable = new SendCommittable();
+ committable.setTopic("TopicTest");
+ committable.setBrokerName("broker-a");
+ committable.setQueueId(3);
+ committable.setQueueOffset(42L);
+ committable.setMsgId("msg-1");
+ // 32 hex chars -- 8 ip, 8 port, 16 commit log offset -- as decoded by
MessageDecoder.
+ committable.setOffsetMsgId("0A00000100002382000000000000002A");
+ committable.setTransactionId("trans-1");
+ committable.setBrokerAddress(brokerAddress);
+ return committable;
+ }
}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittableSerializerTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittableSerializerTest.java
new file mode 100644
index 0000000..a4171e9
--- /dev/null
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/committer/SendCommittableSerializerTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.rocketmq.sink.committer;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link SendCommittableSerializer}. The address of the node that
accepted the half
+ * message travels with the committable because it survives a checkpoint;
losing it on the way would
+ * silently send every commit to whatever node the committer's route table
happens to name.
+ */
+class SendCommittableSerializerTest {
+
+ private final SendCommittableSerializer serializer = new
SendCommittableSerializer();
+
+ @Test
+ void brokerAddressSurvivesRoundTripTest() throws Exception {
+ SendCommittable committable = sendCommittable();
+ committable.setBrokerAddress("10.0.38.29:8080");
+
+ SendCommittable restored =
+ serializer.deserialize(serializer.getVersion(),
serializer.serialize(committable));
+
+ assertThat(restored.getBrokerAddress()).isEqualTo("10.0.38.29:8080");
+ assertThat(restored.getTopic()).isEqualTo(committable.getTopic());
+
assertThat(restored.getBrokerName()).isEqualTo(committable.getBrokerName());
+ assertThat(restored.getQueueId()).isEqualTo(committable.getQueueId());
+
assertThat(restored.getQueueOffset()).isEqualTo(committable.getQueueOffset());
+ assertThat(restored.getMsgId()).isEqualTo(committable.getMsgId());
+
assertThat(restored.getOffsetMsgId()).isEqualTo(committable.getOffsetMsgId());
+
assertThat(restored.getTransactionId()).isEqualTo(committable.getTransactionId());
+ }
+
+ @Test
+ void versionOnePayloadIsStillReadableTest() throws Exception {
+ SendCommittable restored = serializer.deserialize(1,
versionOneBytes());
+
+ assertThat(restored.getTransactionId()).isEqualTo("trans-1");
+ assertThat(restored.getBrokerAddress()).isNull();
+ }
+
+ /** The version 1 layout: the same fields, without the trailing address. */
+ private static byte[] versionOneBytes() throws Exception {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(stream)) {
+ out.writeUTF("TopicTest");
+ out.writeUTF("broker-a");
+ out.writeInt(3);
+ out.writeLong(42L);
+ out.writeUTF("msg-1");
+ out.writeUTF("offset-msg-1");
+ out.writeUTF("trans-1");
+ out.flush();
+ }
+ return stream.toByteArray();
+ }
+
+ private static SendCommittable sendCommittable() {
+ SendCommittable committable = new SendCommittable();
+ committable.setTopic("TopicTest");
+ committable.setBrokerName("broker-a");
+ committable.setQueueId(3);
+ committable.setQueueOffset(42L);
+ committable.setMsgId("msg-1");
+ committable.setOffsetMsgId("offset-msg-1");
+ committable.setTransactionId("trans-1");
+ return committable;
+ }
+}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
index 1828201..2bce6db 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
@@ -130,6 +130,11 @@ class RocketMQWriterQueueSelectorTest {
throw new UnsupportedOperationException();
}
+ @Override
+ public String resolvePublishAddress(SendResult sendResult) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public CompletableFuture<Void> commit(SendCommittable sendCommittable)
{
throw new UnsupportedOperationException();
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterSendPendingMaxTest.java
similarity index 58%
copy from
flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
copy to
flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterSendPendingMaxTest.java
index 1828201..9eac3bc 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterSendPendingMaxTest.java
@@ -26,12 +26,12 @@ import
org.apache.flink.connector.rocketmq.sink.RocketMQSinkOptions;
import org.apache.flink.connector.rocketmq.sink.committer.SendCommittable;
import
org.apache.flink.connector.rocketmq.sink.writer.context.RocketMQSinkContext;
import
org.apache.flink.connector.rocketmq.sink.writer.serializer.RocketMQSerializationSchema;
-import
org.apache.flink.streaming.connectors.rocketmq.common.selector.HashMessageQueueSelector;
import
org.apache.flink.streaming.connectors.rocketmq.common.selector.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
import org.mockito.Mockito;
import java.nio.charset.StandardCharsets;
@@ -42,46 +42,54 @@ import java.util.concurrent.CompletableFuture;
import static org.assertj.core.api.Assertions.assertThat;
/**
- * Tests that a configured {@link MessageQueueSelector} actually reaches the
producer. Accepting the
- * selector on the builder without ever passing it to the send call silently
turns ordered writes
- * into round robin writes, which no amount of configuration can reveal at
runtime.
+ * Tests that {@code send.pending.max} bounds the in-flight async sends. The
option ships a
+ * description promising exactly that ("Maximum number of pending messages in
the send buffer") but
+ * was never read, so a slow broker let the buffer grow without limit.
*/
-class RocketMQWriterQueueSelectorTest {
+class RocketMQWriterSendPendingMaxTest {
- private static final String TOPIC = "TopicWriterSelector";
+ private static final String TOPIC = "TopicWriterPending";
@Test
- void configuredSelectorReachesTheProducerTest() throws Exception {
- RecordingProducer producer = new RecordingProducer();
- MessageQueueSelector selector = new HashMessageQueueSelector();
-
- newWriter(selector, producer).write("sharding-key", null);
-
- assertThat(producer.plainSends).isEmpty();
- assertThat(producer.selectorSends).hasSize(1);
- assertThat(producer.selectorSends.get(0).selector).isSameAs(selector);
-
assertThat(producer.selectorSends.get(0).arg).isEqualTo("sharding-key");
-
assertThat(producer.selectorSends.get(0).message.getTopic()).isEqualTo(TOPIC);
+ @Timeout(30)
+ void writesBlockOnceThePendingLimitIsReachedTest() throws Exception {
+ GatedProducer producer = new GatedProducer();
+ RocketMQWriter<String> writer = newWriter(producer, 1);
+
+ writer.write("first", null);
+ assertThat(producer.sends).hasSize(1);
+
+ Thread second = new Thread(() -> uncheckedWrite(writer, "second"));
+ second.start();
+ second.join(1000);
+ assertThat(second.isAlive())
+ .as("the second write must block while the first send is still
in flight")
+ .isTrue();
+ assertThat(producer.sends).hasSize(1);
+
+ producer.completeAll();
+ second.join(5000);
+ assertThat(second.isAlive()).as("releasing the send must unblock the
writer").isFalse();
+ assertThat(producer.sends).hasSize(2);
+ writer.close();
}
- @Test
- void withoutSelectorThePlainSendIsUsedTest() throws Exception {
- RecordingProducer producer = new RecordingProducer();
-
- newWriter(null, producer).write("sharding-key", null);
-
- assertThat(producer.plainSends).hasSize(1);
- assertThat(producer.selectorSends).isEmpty();
+ private static void uncheckedWrite(RocketMQWriter<String> writer, String
record) {
+ try {
+ writer.write(record, null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
- private static RocketMQWriter<String> newWriter(
- MessageQueueSelector selector, InnerProducer producer) {
+ private static RocketMQWriter<String> newWriter(GatedProducer producer,
int pendingMax) {
Configuration configuration = new Configuration();
configuration.setString(
RocketMQSinkOptions.DELIVERY_GUARANTEE,
DeliveryGuarantee.AT_LEAST_ONCE.name());
+ configuration.setInteger(RocketMQSinkOptions.SEND_PENDING_MAX,
pendingMax);
return new RocketMQWriter<>(
configuration,
- selector,
+ null,
new StringToMessageSchema(),
Mockito.mock(Sink.InitContext.class),
producer);
@@ -98,31 +106,30 @@ class RocketMQWriterQueueSelectorTest {
}
}
- /** Records which send overload the writer chose. */
- private static class RecordingProducer implements InnerProducer {
+ /** Keeps every send future open until the test releases them. */
+ private static class GatedProducer implements InnerProducer {
- private final List<Message> plainSends = new ArrayList<>();
- private final List<SelectorSend> selectorSends = new ArrayList<>();
+ private final List<CompletableFuture<SendResult>> sends = new
ArrayList<>();
@Override
public void start() {}
@Override
public String getProducerGroup() {
- return "recording-producer";
+ return "gated-producer";
}
@Override
- public CompletableFuture<SendResult> send(Message message) {
- plainSends.add(message);
- return CompletableFuture.completedFuture(new SendResult());
+ public synchronized CompletableFuture<SendResult> send(Message
message) {
+ CompletableFuture<SendResult> future = new CompletableFuture<>();
+ sends.add(future);
+ return future;
}
@Override
public CompletableFuture<SendResult> send(
Message message, MessageQueueSelector selector, Object arg) {
- selectorSends.add(new SelectorSend(message, selector, arg));
- return CompletableFuture.completedFuture(new SendResult());
+ return send(message);
}
@Override
@@ -131,28 +138,27 @@ class RocketMQWriterQueueSelectorTest {
}
@Override
- public CompletableFuture<Void> commit(SendCommittable sendCommittable)
{
+ public String resolvePublishAddress(SendResult sendResult) {
throw new UnsupportedOperationException();
}
@Override
- public CompletableFuture<Void> rollback(SendCommittable
sendCommittable) {
+ public CompletableFuture<Void> commit(SendCommittable sendCommittable)
{
throw new UnsupportedOperationException();
}
@Override
- public void close() {}
-
- private static class SelectorSend {
- private final Message message;
- private final MessageQueueSelector selector;
- private final Object arg;
+ public CompletableFuture<Void> rollback(SendCommittable
sendCommittable) {
+ throw new UnsupportedOperationException();
+ }
- private SelectorSend(Message message, MessageQueueSelector
selector, Object arg) {
- this.message = message;
- this.selector = selector;
- this.arg = arg;
+ synchronized void completeAll() {
+ for (CompletableFuture<SendResult> future : sends) {
+ future.complete(new SendResult());
}
}
+
+ @Override
+ public void close() {}
}
}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterTransactionCommittableTest.java
similarity index 57%
copy from
flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
copy to
flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterTransactionCommittableTest.java
index 1828201..0923131 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterTransactionCommittableTest.java
@@ -26,62 +26,56 @@ import
org.apache.flink.connector.rocketmq.sink.RocketMQSinkOptions;
import org.apache.flink.connector.rocketmq.sink.committer.SendCommittable;
import
org.apache.flink.connector.rocketmq.sink.writer.context.RocketMQSinkContext;
import
org.apache.flink.connector.rocketmq.sink.writer.serializer.RocketMQSerializationSchema;
-import
org.apache.flink.streaming.connectors.rocketmq.common.selector.HashMessageQueueSelector;
import
org.apache.flink.streaming.connectors.rocketmq.common.selector.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.common.message.Message;
+import org.apache.rocketmq.common.message.MessageQueue;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import static org.assertj.core.api.Assertions.assertThat;
/**
- * Tests that a configured {@link MessageQueueSelector} actually reaches the
producer. Accepting the
- * selector on the builder without ever passing it to the send call silently
turns ordered writes
- * into round robin writes, which no amount of configuration can reveal at
runtime.
+ * Tests that a transactional write records the address the half message was
accepted by. The
+ * committer runs on a producer of its own, whose publish route is resolved
independently and may
+ * name a different node; a commit sent there is discarded while still
answered with a success code.
*/
-class RocketMQWriterQueueSelectorTest {
+class RocketMQWriterTransactionCommittableTest {
- private static final String TOPIC = "TopicWriterSelector";
+ private static final String TOPIC = "TopicWriterTransaction";
+ private static final String SEND_ADDRESS = "10.0.38.29:8080";
@Test
- void configuredSelectorReachesTheProducerTest() throws Exception {
- RecordingProducer producer = new RecordingProducer();
- MessageQueueSelector selector = new HashMessageQueueSelector();
-
- newWriter(selector, producer).write("sharding-key", null);
-
- assertThat(producer.plainSends).isEmpty();
- assertThat(producer.selectorSends).hasSize(1);
- assertThat(producer.selectorSends.get(0).selector).isSameAs(selector);
-
assertThat(producer.selectorSends.get(0).arg).isEqualTo("sharding-key");
-
assertThat(producer.selectorSends.get(0).message.getTopic()).isEqualTo(TOPIC);
+ void prepareCommitCarriesTheAddressOfTheSendingNodeTest() throws Exception
{
+ TransactionalProducer producer = new TransactionalProducer();
+ RocketMQWriter<String> writer = newWriter(producer);
+
+ writer.write("record", null);
+ Collection<SendCommittable> committables = writer.prepareCommit();
+
+ assertThat(committables).hasSize(1);
+ SendCommittable committable = committables.iterator().next();
+ assertThat(committable.getBrokerAddress()).isEqualTo(SEND_ADDRESS);
+ assertThat(committable.getTransactionId()).isEqualTo("trans-1");
+ assertThat(committable.getBrokerName()).isEqualTo("broker-a");
+ // The map must be drained, otherwise the next checkpoint commits the
same half message
+ // twice.
+ assertThat(writer.prepareCommit()).isEmpty();
}
- @Test
- void withoutSelectorThePlainSendIsUsedTest() throws Exception {
- RecordingProducer producer = new RecordingProducer();
-
- newWriter(null, producer).write("sharding-key", null);
-
- assertThat(producer.plainSends).hasSize(1);
- assertThat(producer.selectorSends).isEmpty();
- }
-
- private static RocketMQWriter<String> newWriter(
- MessageQueueSelector selector, InnerProducer producer) {
+ private static RocketMQWriter<String> newWriter(InnerProducer producer) {
Configuration configuration = new Configuration();
configuration.setString(
- RocketMQSinkOptions.DELIVERY_GUARANTEE,
DeliveryGuarantee.AT_LEAST_ONCE.name());
+ RocketMQSinkOptions.DELIVERY_GUARANTEE,
DeliveryGuarantee.EXACTLY_ONCE.name());
return new RocketMQWriter<>(
configuration,
- selector,
+ null,
new StringToMessageSchema(),
Mockito.mock(Sink.InitContext.class),
producer);
@@ -98,36 +92,43 @@ class RocketMQWriterQueueSelectorTest {
}
}
- /** Records which send overload the writer chose. */
- private static class RecordingProducer implements InnerProducer {
-
- private final List<Message> plainSends = new ArrayList<>();
- private final List<SelectorSend> selectorSends = new ArrayList<>();
+ /** Returns a send result plus the address the writer is expected to
record for it. */
+ private static class TransactionalProducer implements InnerProducer {
@Override
public void start() {}
@Override
public String getProducerGroup() {
- return "recording-producer";
+ return "transactional-producer";
}
@Override
public CompletableFuture<SendResult> send(Message message) {
- plainSends.add(message);
- return CompletableFuture.completedFuture(new SendResult());
+ throw new UnsupportedOperationException();
}
@Override
public CompletableFuture<SendResult> send(
Message message, MessageQueueSelector selector, Object arg) {
- selectorSends.add(new SelectorSend(message, selector, arg));
- return CompletableFuture.completedFuture(new SendResult());
+ throw new UnsupportedOperationException();
}
@Override
public CompletableFuture<SendResult> sendMessageInTransaction(Message
message) {
- throw new UnsupportedOperationException();
+ SendResult sendResult = new SendResult();
+ sendResult.setSendStatus(SendStatus.SEND_OK);
+ sendResult.setMessageQueue(new MessageQueue(TOPIC, "broker-a", 3));
+ sendResult.setQueueOffset(42L);
+ sendResult.setMsgId("msg-1");
+ sendResult.setOffsetMsgId("0A00000100002382000000000000002A");
+ sendResult.setTransactionId("trans-1");
+ return CompletableFuture.completedFuture(sendResult);
+ }
+
+ @Override
+ public String resolvePublishAddress(SendResult sendResult) {
+ return SEND_ADDRESS;
}
@Override
@@ -142,17 +143,5 @@ class RocketMQWriterQueueSelectorTest {
@Override
public void close() {}
-
- private static class SelectorSend {
- private final Message message;
- private final MessageQueueSelector selector;
- private final Object arg;
-
- private SelectorSend(Message message, MessageQueueSelector
selector, Object arg) {
- this.message = message;
- this.selector = selector;
- this.arg = arg;
- }
- }
}
}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitterTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitterTest.java
index 6a357e3..a9b08de 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitterTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQRecordEmitterTest.java
@@ -26,6 +26,7 @@ import
org.apache.flink.connector.rocketmq.source.split.RocketMQSourceSplit;
import
org.apache.flink.connector.rocketmq.source.split.RocketMQSourceSplitState;
import org.apache.flink.table.data.GenericRowData;
import org.apache.flink.table.data.RowData;
+import org.apache.flink.util.Collector;
import org.apache.rocketmq.common.message.MessageExt;
import org.junit.Test;
@@ -33,6 +34,10 @@ import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
/** Test for {@link RocketMQRecordEmitter}. */
public class RocketMQRecordEmitterTest {
@@ -65,7 +70,90 @@ public class RocketMQRecordEmitterTest {
recordEmitter.emitRecord(messageView, new TestingEmitterOutput<>(),
partitionSplitState);
}
- private static final class TestingEmitterOutput<E> implements
ReaderOutput<E> {
+ /**
+ * A record whose deserialization throws must be dropped with a warning
instead of failing the
+ * task: otherwise the job loops "fail, restore, re-read the same record"
and rewrites
+ * everything it already emitted. The split offset still has to advance
past the poison record.
+ */
+ @Test
+ public void testUndeserializableRecordIsDroppedAndOffsetAdvances() throws
IOException {
+ RocketMQRecordEmitter<RowData> recordEmitter =
+ new RocketMQRecordEmitter<>(new ThrowingSchemaWrapper<>());
+ MessageExt message =
+ new MessageExt(
+ 1,
+ System.currentTimeMillis(),
+ InetSocketAddress.createUnresolved("127.0.0.1", 8080),
+ System.currentTimeMillis(),
+ InetSocketAddress.createUnresolved("127.0.0.1", 8088),
+ "184019388");
+ message.setBody("poison".getBytes());
+ message.setQueueOffset(100L);
+
+ RocketMQSourceSplitState splitState =
+ new RocketMQSourceSplitState(new RocketMQSourceSplit("t", "b",
0, 100L, -1L));
+
+ recordEmitter.emitRecord(
+ new MessageViewExt(message), new TestingEmitterOutput<>(),
splitState);
+
+ assertEquals(101L, splitState.getCurrentOffset());
+ }
+
+ /**
+ * A failure raised while emitting an already-deserialized record (the
downstream sink runs
+ * inline inside collect) must still fail the task: dropping it would hide
a real write failure.
+ */
+ @Test
+ public void testSinkFailureDuringCollectIsNotSwallowed() {
+ RocketMQRecordEmitter<RowData> recordEmitter =
+ new RocketMQRecordEmitter<>(new CollectingSchemaWrapper<>());
+ MessageExt message =
+ new MessageExt(
+ 1,
+ System.currentTimeMillis(),
+ InetSocketAddress.createUnresolved("127.0.0.1", 8080),
+ System.currentTimeMillis(),
+ InetSocketAddress.createUnresolved("127.0.0.1", 8088),
+ "184019389");
+ message.setBody("fine".getBytes());
+ message.setQueueOffset(7L);
+
+ RocketMQSourceSplitState splitState =
+ new RocketMQSourceSplitState(new RocketMQSourceSplit("t", "b",
0, 7L, -1L));
+
+ try {
+ recordEmitter.emitRecord(
+ new MessageViewExt(message), new FailingEmitterOutput<>(),
splitState);
+ fail("a sink failure during collect must propagate");
+ } catch (IOException expected) {
+ assertTrue(expected.getMessage().contains("Failed to emit
message"));
+ }
+ }
+
+ /** Emits exactly one record, so the output's failure happens inside
collect. */
+ private static final class CollectingSchemaWrapper<T> extends
RocketMQSchemaWrapper<T> {
+ @Override
+ public void deserialize(MessageView messageView, Collector<T> out) {
+ out.collect(null);
+ }
+ }
+
+ private static final class FailingEmitterOutput<E> extends
TestingEmitterOutput<E> {
+ @Override
+ public void collect(E record) {
+ throw new RuntimeException("sink rejected the record");
+ }
+ }
+
+ /** Deserialization always fails, as a schema rejecting an unparsable body
would. */
+ private static final class ThrowingSchemaWrapper<T> extends
RocketMQSchemaWrapper<T> {
+ @Override
+ public void deserialize(MessageView messageView, Collector<T> out)
throws IOException {
+ throw new IOException("deliberately unparsable body");
+ }
+ }
+
+ private static class TestingEmitterOutput<E> implements ReaderOutput<E> {
private TestingEmitterOutput() {}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQSplitReaderPollTimeoutTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQSplitReaderPollTimeoutTest.java
new file mode 100644
index 0000000..f8c38ca
--- /dev/null
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/RocketMQSplitReaderPollTimeoutTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.rocketmq.source.reader;
+
+import org.apache.flink.api.connector.source.SourceReaderContext;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.common.config.RocketMQOptions;
+import org.apache.flink.connector.rocketmq.source.InnerConsumer;
+import
org.apache.flink.connector.rocketmq.source.metrics.RocketMQSourceReaderMetrics;
+import
org.apache.flink.connector.rocketmq.source.reader.deserializer.RocketMQDeserializationSchema;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that {@code rocketmq.client.poll.timeout} reaches the consumer poll
and that its default no
+ * longer spins the fetch loop of an idle job a hundred times a second. The
bound matters because
+ * {@link RocketMQSplitReader#wakeUp()} only sets a flag and the consumer
deliberately has no
+ * interruptible poll, so the timeout is what limits how long a fetch can
outlive a wakeup.
+ */
+class RocketMQSplitReaderPollTimeoutTest {
+
+ @Test
+ void idleFetchBlocksForTheDefaultTimeoutTest() {
+ assertThat(pollTimeoutOf(new
Configuration())).isEqualTo(Duration.ofSeconds(1));
+ }
+
+ @Test
+ void idleFetchBlocksForTheConfiguredTimeoutTest() {
+ Configuration configuration = new Configuration();
+ configuration.set(RocketMQOptions.POLL_TIMEOUT, 250L);
+
+
assertThat(pollTimeoutOf(configuration)).isEqualTo(Duration.ofMillis(250));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Duration pollTimeoutOf(Configuration configuration) {
+ InnerConsumer consumer = Mockito.mock(InnerConsumer.class);
+
Mockito.when(consumer.poll(Mockito.any())).thenReturn(Collections.emptyList());
+
+ RocketMQSplitReader<String> reader =
+ new RocketMQSplitReader<>(
+ configuration,
+ Mockito.mock(SourceReaderContext.class),
+ Mockito.mock(RocketMQDeserializationSchema.class),
+ Mockito.mock(RocketMQSourceReaderMetrics.class),
+ consumer);
+ try {
+ reader.fetch();
+ } catch (IOException e) {
+ throw new AssertionError("fetching without records must not fail",
e);
+ }
+
+ ArgumentCaptor<Duration> timeout =
ArgumentCaptor.forClass(Duration.class);
+ Mockito.verify(consumer).poll(timeout.capture());
+ return timeout.getValue();
+ }
+}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactoryTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactoryTest.java
index a06c8af..767b47a 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactoryTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkFactoryTest.java
@@ -19,6 +19,9 @@
package org.apache.flink.streaming.connectors.rocketmq.table;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.common.config.RocketMQOptions;
+import org.apache.flink.connector.rocketmq.sink.RocketMQSink;
+import org.apache.flink.connector.rocketmq.sink.RocketMQSinkOptions;
import org.apache.flink.connector.rocketmq.source.RocketMQSourceOptions;
import org.apache.flink.table.api.Schema;
import org.apache.flink.table.api.ValidationException;
@@ -28,17 +31,21 @@ import org.apache.flink.table.catalog.ObjectIdentifier;
import org.apache.flink.table.catalog.ResolvedCatalogTable;
import org.apache.flink.table.catalog.ResolvedSchema;
import org.apache.flink.table.connector.sink.DynamicTableSink;
+import org.apache.flink.table.connector.sink.SinkV2Provider;
import org.apache.flink.table.factories.FactoryUtil;
+import
org.apache.flink.table.runtime.connector.sink.SinkRuntimeProviderContext;
import org.junit.Ignore;
import org.junit.Test;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.apache.flink.table.api.DataTypes.STRING;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -62,7 +69,7 @@ public class RocketMQDynamicTableSinkFactoryTest {
public void testRocketMQDynamicTableSinkWithLegalOption() {
final Map<String, String> options = new HashMap<>();
options.put("connector", IDENTIFIER);
- options.put(RocketMQSourceOptions.TOPIC.key(), TOPIC);
+ options.put(RocketMQSinkOptions.TOPIC.key(), TOPIC);
options.put(
RocketMQSourceOptions.OPTIONAL_STARTUP_OFFSET_TIMESTAMP.key(),
String.valueOf(System.currentTimeMillis()));
@@ -94,6 +101,57 @@ public class RocketMQDynamicTableSinkFactoryTest {
createDynamicTableSink(options);
}
+ /**
+ * {@code retry.times} is the legacy DDL name of the send retry count. It
used to be accepted
+ * and then silently dropped, so a SQL job that configured it retried with
the default instead.
+ */
+ @Test
+ public void testLegacyRetryTimesReachesTheWriter() throws Exception {
+ Map<String, String> options = sinkOptions();
+ options.put(RocketMQSinkOptions.OPTIONAL_WRITE_RETRY_TIMES.key(), "7");
+
+
assertThat(sinkConfiguration(options).getInteger(RocketMQSinkOptions.SEND_RETRY_TIMES))
+ .isEqualTo(7);
+ }
+
+ @Test
+ public void testSendRetryTimesWinsOverTheLegacyAlias() throws Exception {
+ Map<String, String> options = sinkOptions();
+ options.put(RocketMQSinkOptions.OPTIONAL_WRITE_RETRY_TIMES.key(), "7");
+ options.put(RocketMQSinkOptions.SEND_RETRY_TIMES.key(), "5");
+
+
assertThat(sinkConfiguration(options).getInteger(RocketMQSinkOptions.SEND_RETRY_TIMES))
+ .isEqualTo(5);
+ }
+
+ @Test
+ public void testRetryDefaultsToThreeWhenNeitherKeyIsSet() throws Exception
{
+ assertThat(
+ sinkConfiguration(sinkOptions())
+
.getInteger(RocketMQSinkOptions.SEND_RETRY_TIMES))
+ .isEqualTo(3);
+ }
+
+ private static Map<String, String> sinkOptions() {
+ Map<String, String> options = new HashMap<>();
+ options.put("connector", IDENTIFIER);
+ options.put(RocketMQSinkOptions.TOPIC.key(), TOPIC);
+ options.put(RocketMQSinkOptions.PRODUCER_GROUP.key(), PRODUCER_GROUP);
+ options.put(RocketMQOptions.ENDPOINTS.key(), NAME_SERVER_ADDRESS);
+ return options;
+ }
+
+ private static Configuration sinkConfiguration(Map<String, String>
options) throws Exception {
+ DynamicTableSink tableSink = createDynamicTableSink(options);
+ SinkV2Provider provider =
+ (SinkV2Provider)
+ tableSink.getSinkRuntimeProvider(new
SinkRuntimeProviderContext(false));
+ RocketMQSink<?> sink = (RocketMQSink<?>) provider.createSink();
+ Field configuration =
RocketMQSink.class.getDeclaredField("configuration");
+ configuration.setAccessible(true);
+ return (Configuration) configuration.get(sink);
+ }
+
private static DynamicTableSink createDynamicTableSink(Map<String, String>
options) {
return FactoryUtil.createTableSink(
null,
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkMigrationTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkMigrationTest.java
index ad09869..2707f50 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkMigrationTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSinkMigrationTest.java
@@ -53,7 +53,6 @@ class RocketMQDynamicTableSinkMigrationTest {
null, // dynamicColumn
String.valueOf((char) 1), // fieldDelimiter (default
SOH)
"UTF-8", // encoding
- 10, // retryTimes
5000L, // sleepTime
false, // isDynamicTag
true, // isDynamicTagIncluded
@@ -91,7 +90,6 @@ class RocketMQDynamicTableSinkMigrationTest {
null, // dynamicColumn
String.valueOf((char) 1), // fieldDelimiter
"UTF-8", // encoding
- 10, // retryTimes
5000L, // sleepTime
false, // isDynamicTag
true, // isDynamicTagIncluded