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 fad67bf [ISSUE #141] fix(remoting): stop delivering the wrong data
silently (#145)
fad67bf is described below
commit fad67bf45d4d5f2f59acf16b22acb9e2d383d6b4
Author: lizhimins <[email protected]>
AuthorDate: Mon Oct 26 14:47:03 2026 +0800
[ISSUE #141] fix(remoting): stop delivering the wrong data silently (#145)
An acceptance run against a commercial 5.5.1 cluster found a family of
defects
with the same shape: the job stays RUNNING, checkpoints succeed, nothing is
logged, and the data is wrong or absent. Every fix below either makes the
behaviour correct or makes the failure loud.
Source. rocketmq.source.filter.sql was accepted, logged as a warning and
then
ignored, so every message was delivered regardless of the predicate; assign
mode
cannot push a SQL92 expression to the broker, so it is now evaluated on the
client with rocketmq-filter, the implementation the broker uses, and an
expression that does not compile fails at reader construction.
RowDeserializationSchema read its format options from keys such as
"fielddelimiter" while the table factory registers and documents
"rocketmq.client.message.field.delimiter". The DDL values never arrived, the
field delimiter stayed at its default, and every record of a multi column
table
was dropped as "field missing" -- a SQL source that returns zero rows
without
failing. Both row deserializers now read the canonical keys, and an
unsupported
message.length.check is rejected instead of ignored.
RocketMQSourceReaderMetrics declared nine metric names and was threaded
through
four classes, but its constructor and both registration methods had empty
bodies, so a running job exposed no RocketMQ metric at all. Offsets, commit
counters and per queue lag are now reported, and the lag is summed into
Flink's
pendingRecords. Lag needs the broker's maximum offset, which the reader
refreshes asynchronously at most once every ten seconds.
RocketMQSourceFunction could not start at all: open() allocates the
subtask's
queues itself and then seeks each one, but the consumer was in subscribe
mode,
where allocation belongs to the client's asynchronous rebalance, so the
assigned
list was still empty and every seek failed with "The message queue is not in
assigned list". The allocated queues are now assigned explicitly, which also
matches the new source.
offset.commit.auto was read only to print its value while the consumer was
hardcoded to setAutoCommit(false); pull.rpc.timeout was declared and never
read.
Both are applied now, and the auto-commit default becomes false to match the
behaviour the code always had. An unknown startup.scan.mode fell through to
latest, making a misspelled mode look like a healthy job reading from the
wrong
position; it is rejected.
Sink. RocketMQSinkBuilder accepted a MessageQueueSelector, null checked it
and
stored it, and both send paths then called send(Message), so records that
had to
stay ordered were spread over every queue while the API said otherwise. The
selector now reaches the send call with the record as its sharding argument.
The committer logged a failed commit without the throwable, so a failure
produced a message with no cause and no stack trace. A record that failed to
emit was reported as "Failed to deserialize message due to" wherever the
failure
actually came from, and named no record; the wrapper now covers only the
deserialization call and the message carries the message id, topic, queue
and
offset.
The async path of the legacy RocketMQSink logged a send failure and carried
on,
so a job whose every record was rejected stayed RUNNING, kept checkpointing
and
wrote nothing. The first failure is now parked and rethrown from invoke()
and
finish().
Table. A validation failure repeated every table option, and a WITH clause
normally carries the credentials, publishing the access key to the
JobManager
log and the REST exception history. Both factories now report the missing
keys
only, before delegating to the helper, which still detects unknown options.
Removed. rocketmq.source.startup.offset.strategy and
rocketmq.sink.serialize.format were read by nothing, and the first was even
registered as a DDL option; an option that does nothing should not be
offered.
The duplicate CollectorOption key sets are gone with the fix above, as is
the
InnerConsumerImpl.wakeup() that paused every assigned queue with no matching
resume anywhere in the codebase, and the message key separator that was an
empty
string instead of MessageConst.KEY_SEPARATOR.
---
.../rocketmq/common/config/RocketMQOptions.java | 1 -
.../rocketmq/common/filter/Sql92MessageFilter.java | 97 ++++++++++
.../connector/rocketmq/sink/InnerProducer.java | 11 ++
.../connector/rocketmq/sink/InnerProducerImpl.java | 109 +++++++++--
.../rocketmq/sink/RocketMQSinkOptions.java | 5 -
.../rocketmq/sink/committer/RocketMQCommitter.java | 3 +-
.../rocketmq/sink/writer/RocketMQWriter.java | 49 +++--
.../rocketmq/source/InnerConsumerImpl.java | 32 ++--
.../rocketmq/source/RocketMQSourceOptions.java | 14 +-
.../metrics/RocketMQSourceReaderMetrics.java | 137 +++++++++++++-
.../rocketmq/source/reader/MessageViewExt.java | 7 +-
.../source/reader/RocketMQRecordEmitter.java | 17 +-
.../source/reader/RocketMQSplitReader.java | 152 ++++++++++-----
.../deserializer/RowDeserializationSchema.java | 86 ++++-----
.../connectors/rocketmq/RocketMQSink.java | 26 +++
.../rocketmq/RocketMQSourceFunction.java | 22 ++-
.../RowKeyValueDeserializationSchema.java | 82 ++++----
.../rocketmq/table/RequiredOptionValidator.java | 56 ++++++
.../table/RocketMQDynamicTableSinkFactory.java | 1 +
.../table/RocketMQDynamicTableSourceFactory.java | 2 +-
.../rocketmq/table/RocketMQScanTableSource.java | 10 +-
.../common/filter/Sql92MessageFilterTest.java | 103 ++++++++++
.../writer/RocketMQWriterQueueSelectorTest.java | 158 ++++++++++++++++
.../metrics/RocketMQSourceReaderMetricsTest.java | 208 +++++++++++++++++++++
.../RowDeserializationSchemaOptionsTest.java | 144 ++++++++++++++
.../RocketMQDynamicTableSourceFactoryTest.java | 65 +++++++
.../rocketmq/table/RocketMQParameterChainTest.java | 6 -
27 files changed, 1386 insertions(+), 217 deletions(-)
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 3df6b41..9e7eef7 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
@@ -64,7 +64,6 @@ public class RocketMQOptions {
.noDefaultValue()
.withDescription("RocketMQ instance namespace");
- /** 这里不知道对轨迹功能有没有影响, 待验证 */
public static final ConfigOption<AccessChannel> OPTIONAL_ACCESS_CHANNEL =
ConfigOptions.key(CLIENT_CONFIG_PREFIX + "channel")
.enumType(AccessChannel.class)
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/common/filter/Sql92MessageFilter.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/common/filter/Sql92MessageFilter.java
new file mode 100644
index 0000000..3020d8f
--- /dev/null
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/common/filter/Sql92MessageFilter.java
@@ -0,0 +1,97 @@
+/*
+ * 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.common.filter;
+
+import org.apache.rocketmq.common.filter.ExpressionType;
+import org.apache.rocketmq.filter.FilterFactory;
+import org.apache.rocketmq.filter.expression.EvaluationContext;
+import org.apache.rocketmq.filter.expression.Expression;
+import org.apache.rocketmq.filter.expression.MQFilterException;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Evaluates a SQL92 expression against the properties of a single message.
+ *
+ * <p>{@link org.apache.rocketmq.client.consumer.DefaultLitePullConsumer} can
only carry a tag
+ * sub-expression while queues are assigned explicitly, and assigning queues
explicitly is what
+ * keeps split assignment under the control of the Flink enumerator. A SQL92
filter therefore has to
+ * be evaluated here rather than on the broker.
+ *
+ * <p>The expression is compiled once by the reader that owns this filter.
Compilation uses the same
+ * {@code rocketmq-filter} implementation the broker uses, so predicate
semantics, including the
+ * coercion between string properties and numeric literals, match server side
SQL92 filtering.
+ */
+public final class Sql92MessageFilter {
+
+ private final String expressionString;
+ private final Expression expression;
+
+ public Sql92MessageFilter(String expressionString) {
+ this.expressionString = expressionString;
+ try {
+ this.expression =
+
FilterFactory.INSTANCE.get(ExpressionType.SQL92).compile(expressionString);
+ } catch (MQFilterException e) {
+ throw new IllegalArgumentException(
+ "Invalid SQL92 filter expression '" + expressionString +
"'", e);
+ }
+ }
+
+ /**
+ * Returns whether the message satisfies the expression.
+ *
+ * <p>A message whose properties cannot be evaluated, for instance because
the expression
+ * dereferences a missing property in an unsupported way, does not satisfy
the predicate. This
+ * mirrors the broker, which drops such messages instead of failing the
pull request.
+ */
+ public boolean matches(Map<String, String> properties) {
+ try {
+ return Boolean.TRUE.equals(expression.evaluate(new
PropertiesContext(properties)));
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public String getExpressionString() {
+ return expressionString;
+ }
+
+ /** Adapts the immutable string properties of a message to the filter
evaluation context. */
+ private static final class PropertiesContext implements EvaluationContext {
+
+ private final Map<String, String> properties;
+
+ private PropertiesContext(Map<String, String> properties) {
+ this.properties = properties == null ? Collections.emptyMap() :
properties;
+ }
+
+ @Override
+ public Object get(String name) {
+ return properties.get(name);
+ }
+
+ @Override
+ public Map<String, Object> keyValues() {
+ return Collections.unmodifiableMap(new HashMap<String,
Object>(properties));
+ }
+ }
+}
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 9d1669b..aef6614 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
@@ -18,6 +18,7 @@
package org.apache.flink.connector.rocketmq.sink;
import org.apache.flink.connector.rocketmq.sink.committer.SendCommittable;
+import
org.apache.flink.streaming.connectors.rocketmq.common.selector.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
@@ -52,6 +53,16 @@ public interface InnerProducer extends AutoCloseable {
*/
CompletableFuture<SendResult> send(Message message);
+ /**
+ * Sends the message to a queue chosen by the given selector.
+ *
+ * @param message the message to be sent
+ * @param selector picks the queue among the queues of the topic
+ * @param arg the sharding argument handed to the selector, usually the
record being written
+ * @return a Future for the send operation that returns a SendResult object
+ */
+ CompletableFuture<SendResult> send(Message message, MessageQueueSelector
selector, Object arg);
+
/**
* Sends the message to the messaging system and returns a Future for the
send operation.
*
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 84581b2..6595f7a 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
@@ -20,6 +20,8 @@ package org.apache.flink.connector.rocketmq.sink;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.rocketmq.common.config.RocketMQOptions;
import org.apache.flink.connector.rocketmq.sink.committer.SendCommittable;
+import
org.apache.flink.streaming.connectors.rocketmq.common.selector.MessageQueueSelector;
+import org.apache.flink.util.FlinkRuntimeException;
import org.apache.flink.util.StringUtils;
import org.apache.rocketmq.acl.common.AclClientRPCHook;
@@ -40,6 +42,7 @@ import
org.apache.rocketmq.remoting.exception.RemotingException;
import org.apache.rocketmq.remoting.protocol.NamespaceUtil;
import org.apache.rocketmq.remoting.protocol.RemotingCommand;
import org.apache.rocketmq.remoting.protocol.RequestCode;
+import org.apache.rocketmq.remoting.protocol.ResponseCode;
import
org.apache.rocketmq.remoting.protocol.header.EndTransactionRequestHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -209,7 +212,7 @@ public class InnerProducerImpl implements InnerProducer {
() -> {
try {
SendResult sendResult = producer.send(message);
- LOG.info(
+ LOG.debug(
"Send message successfully, topic={},
messageId={}",
message.getTopic(),
sendResult.getMsgId());
@@ -222,6 +225,27 @@ public class InnerProducerImpl implements InnerProducer {
Runnable::run);
}
+ @Override
+ public CompletableFuture<SendResult> send(
+ Message message, MessageQueueSelector selector, Object arg) {
+ return CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ SendResult sendResult = producer.send(message,
selector, arg);
+ LOG.debug(
+ "Send message successfully, topic={},
messageId={}, queue={}",
+ message.getTopic(),
+ sendResult.getMsgId(),
+ sendResult.getMessageQueue());
+ return sendResult;
+ } catch (Exception e) {
+ LOG.error("Failed to send message, topic={}",
message.getTopic(), e);
+ throw new RuntimeException(e);
+ }
+ },
+ Runnable::run);
+ }
+
@Override
public CompletableFuture<SendResult> sendMessageInTransaction(Message
message) {
return CompletableFuture.supplyAsync(
@@ -278,8 +302,28 @@ public class InnerProducerImpl implements InnerProducer {
final String brokerName =
this.mqClientInstance.getBrokerNameFromMessageQueue(
producer.queueWithNamespace(sendCommittable.getMessageQueue()));
- final String brokerAddress =
-
this.mqClientInstance.findBrokerAddressInPublish(brokerName);
+ 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);
+ }
+ if (brokerAddress == null) {
+ throw new IllegalStateException(
+ "Cannot resolve the address of broker "
+ + brokerName
+ + " for topic "
+ + sendCommittable.getTopic());
+ }
EndTransactionRequestHeader requestHeader = new
EndTransactionRequestHeader();
requestHeader.setTransactionId(sendCommittable.getTransactionId());
@@ -304,16 +348,38 @@ public class InnerProducerImpl implements InnerProducer {
break;
}
- if (sendCommittable.getMessageOffset() != -1L) {
- this.endTransaction(
- brokerAddress, requestHeader, "",
this.producer.getSendMsgTimeout());
- } else {
- LOG.error(
- "Convert message physical offset error, msgId={}",
- sendCommittable.getMsgId());
+ if (sendCommittable.getMessageOffset() == -1L) {
+ throw new IllegalStateException(
+ "Convert message physical offset error, msgId="
+ + sendCommittable.getMsgId()
+ + ", offsetMsgId="
+ + sendCommittable.getOffsetMsgId());
}
+ LOG.debug(
+ "End transaction, state={}, brokerName={},
brokerAddress={}, transId={}, "
+ + "commitLogOffset={}, tranStateTableOffset={}",
+ transactionResult,
+ brokerName,
+ brokerAddress,
+ sendCommittable.getTransactionId(),
+ sendCommittable.getMessageOffset(),
+ sendCommittable.getQueueOffset());
+ this.endTransaction(
+ brokerAddress, requestHeader, "",
this.producer.getSendMsgTimeout());
} catch (Exception e) {
- LOG.error("Try end transaction error", e);
+ // Swallowing the failure here lets the CompletableFuture complete
normally, so the
+ // Committer reports success and Flink drops the committable: the
half message is
+ // then never committed and the record is lost without any error
anywhere.
+ throw new FlinkRuntimeException(
+ "End transaction failed, state="
+ + transactionResult
+ + ", topic="
+ + sendCommittable.getTopic()
+ + ", msgId="
+ + sendCommittable.getMsgId()
+ + ", transId="
+ + sendCommittable.getTransactionId(),
+ e);
}
}
@@ -326,10 +392,23 @@ public class InnerProducerImpl implements InnerProducer {
RemotingCommand request =
RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION,
requestHeader);
request.setRemark(remark);
- this.mqClientInstance
- .getMQClientAPIImpl()
- .getRemotingClient()
- .invokeSync(address, request, timeoutMillis);
+ RemotingCommand response =
+ this.mqClientInstance
+ .getMQClientAPIImpl()
+ .getRemotingClient()
+ .invokeSync(address, request, timeoutMillis);
+ if (response == null) {
+ throw new RemotingException("END_TRANSACTION got a null response
from " + address);
+ }
+ if (response.getCode() != ResponseCode.SUCCESS) {
+ throw new RemotingException(
+ "END_TRANSACTION rejected by "
+ + address
+ + ", code="
+ + response.getCode()
+ + ", remark="
+ + response.getRemark());
+ }
}
@Override
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/RocketMQSinkOptions.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/RocketMQSinkOptions.java
index a220e50..99bb5d0 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/RocketMQSinkOptions.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/RocketMQSinkOptions.java
@@ -40,11 +40,6 @@ public class RocketMQSinkOptions extends RocketMQOptions {
.noDefaultValue()
.withDescription("The name of the persist topic");
- public static final ConfigOption<String> SERIALIZE_FORMAT =
- ConfigOptions.key(PRODUCER_PREFIX + "serialize.format")
- .stringType()
- .defaultValue("json");
-
public static final ConfigOption<String> DELIVERY_GUARANTEE =
ConfigOptions.key(PRODUCER_PREFIX + "delivery.guarantee")
.stringType()
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/RocketMQCommitter.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/RocketMQCommitter.java
index 4262b7b..77d3327 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/RocketMQCommitter.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/sink/committer/RocketMQCommitter.java
@@ -63,7 +63,8 @@ public class RocketMQCommitter implements
Committer<SendCommittable>, Cloneable
throwable -> {
LOG.error(
"Commit message error,
committable={}",
- committable);
+ committable,
+ throwable);
request.signalFailedWithKnownReason(throwable);
return null;
});
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 ae1b4e6..59ad65c 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
@@ -17,6 +17,7 @@
package org.apache.flink.connector.rocketmq.sink.writer;
+import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.api.common.eventtime.Watermark;
import org.apache.flink.api.connector.sink2.Sink;
import org.apache.flink.api.connector.sink2.TwoPhaseCommittingSink;
@@ -37,12 +38,15 @@ import org.apache.rocketmq.common.message.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.annotation.Nullable;
+
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -69,6 +73,17 @@ public class RocketMQWriter<IN>
MessageQueueSelector messageQueueSelector,
RocketMQSerializationSchema<IN> serializationSchema,
Sink.InitContext initContext) {
+ this(configuration, messageQueueSelector, serializationSchema,
initContext, null);
+ }
+
+ /** @param producer the producer to send with, or null to create and start
a real one */
+ @VisibleForTesting
+ RocketMQWriter(
+ Configuration configuration,
+ MessageQueueSelector messageQueueSelector,
+ RocketMQSerializationSchema<IN> serializationSchema,
+ Sink.InitContext initContext,
+ @Nullable InnerProducer producer) {
this.deliveryGuarantee =
DeliveryGuarantee.valueOf(
@@ -86,8 +101,12 @@ public class RocketMQWriter<IN>
} catch (Exception e) {
throw new FlinkRuntimeException("Failed to open serialization
schema", e);
}
- this.producer = new InnerProducerImpl(configuration);
- this.producer.start();
+ if (producer == null) {
+ this.producer = new InnerProducerImpl(configuration);
+ this.producer.start();
+ } else {
+ this.producer = producer;
+ }
}
@Override
@@ -99,17 +118,23 @@ public class RocketMQWriter<IN>
if (deliveryGuarantee == DeliveryGuarantee.EXACTLY_ONCE) {
writeInTransaction(message);
- } else {
- pendingRecords.incrementAndGet();
- producer.send(message)
- .whenComplete(
- (sendResult, throwable) -> {
- if (throwable != null) {
- asyncSendException.compareAndSet(null,
throwable);
- }
- pendingRecords.decrementAndGet();
- });
+ return;
}
+
+ 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);
+ sendFuture.whenComplete(
+ (sendResult, throwable) -> {
+ if (throwable != null) {
+ asyncSendException.compareAndSet(null, throwable);
+ }
+ pendingRecords.decrementAndGet();
+ });
}
private void writeInTransaction(Message message) throws IOException {
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 1149de6..ef621cc 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
@@ -89,7 +89,6 @@ public class InnerConsumerImpl implements InnerConsumer {
this.consumer = new DefaultLitePullConsumer();
}
- // Enable message trace if configured (P1 fix — previously a ghost
parameter)
this.consumer.setEnableMsgTrace(enableTrace);
if (traceTopic != null) {
this.consumer.setCustomizedTraceTopic(traceTopic);
@@ -100,7 +99,6 @@ public class InnerConsumerImpl implements InnerConsumer {
this.consumer.setNamesrvAddr(endPoints);
this.consumer.setConsumerGroup(groupId);
- this.consumer.setAutoCommit(false);
this.consumer.setVipChannelEnabled(false);
this.consumer.setInstanceName(
String.join(
@@ -109,29 +107,33 @@ public class InnerConsumerImpl implements InnerConsumer {
groupId,
UUID.randomUUID().toString()));
- // Apply pull batch size from config (P1 fix — previously a ghost
parameter)
long pullBatchSize =
configuration.getLong(RocketMQSourceOptions.PULL_BATCH_SIZE);
if (pullBatchSize > 0) {
this.consumer.setPullBatchSize((int) pullBatchSize);
}
+ long pullTimeoutMillis =
configuration.getLong(RocketMQSourceOptions.PULL_TIMEOUT_MILLIS);
+ if (pullTimeoutMillis > 0) {
+ this.consumer.setConsumerPullTimeoutMillis(pullTimeoutMillis);
+ }
- // Apply namespace from config (P2 fix)
String namespace = configuration.getString(RocketMQOptions.NAMESPACE);
if (namespace != null && !namespace.isEmpty()) {
this.consumer.setNamespace(namespace);
}
- // Log auto-commit configuration for observability (P1 fix)
+ // Offsets are driven by Flink checkpoints; the client side
auto-commit below only affects
+ // what broker side consumer progress reports.
boolean autoCommit =
configuration.getBoolean(RocketMQSourceOptions.AUTO_COMMIT_OFFSET);
long commitInterval =
configuration.getLong(RocketMQSourceOptions.AUTO_COMMIT_OFFSET_INTERVAL);
+ this.consumer.setAutoCommit(autoCommit);
+ this.consumer.setAutoCommitIntervalMillis(commitInterval);
LOG.info(
"Consumer offset commit config: autoCommit={},
commitInterval={}ms, checkpointCommit={}",
autoCommit,
commitInterval,
configuration.getBoolean(RocketMQSourceOptions.COMMIT_OFFSETS_ON_CHECKPOINT));
- // Apply common client parameters (P2 fix)
if (configuration.contains(RocketMQOptions.OPTIONAL_ACCESS_CHANNEL)) {
this.consumer.setAccessChannel(
configuration.get(RocketMQOptions.OPTIONAL_ACCESS_CHANNEL));
@@ -285,16 +287,14 @@ public class InnerConsumerImpl implements InnerConsumer {
@Override
public void wakeup() {
- // wakeup long polling
- try {
- Set<MessageQueue> assignment = this.consumer.assignment();
- if (assignment != null) {
- this.consumer.pause(assignment);
- }
- } catch (MQClientException e) {
- LOG.warn("Consume wakeup long polling failed", e);
- throw new RuntimeException(e);
- }
+ // 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.
+ LOG.debug(
+ "Wakeup requested; relying on the bounded poll timeout instead
of pausing queues");
}
@Override
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/RocketMQSourceOptions.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/RocketMQSourceOptions.java
index 4683184..eb5f06a 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/RocketMQSourceOptions.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/RocketMQSourceOptions.java
@@ -73,11 +73,6 @@ public class RocketMQSourceOptions extends RocketMQOptions {
.longType()
.defaultValue(RocketMQConfig.DEFAULT_START_MESSAGE_OFFSET);
- public static final ConfigOption<String> OPTIONAL_STARTUP_OFFSET_STRATEGY =
- ConfigOptions.key(CONSUMER_PREFIX + "startup.offset.strategy")
- .stringType()
- .noDefaultValue();
-
public static final ConfigOption<String> OPTIONAL_STARTUP_OFFSET_DATE =
ConfigOptions.key(CONSUMER_PREFIX + "startup.offset.date")
.stringType()
@@ -152,11 +147,16 @@ public class RocketMQSourceOptions extends
RocketMQOptions {
"The maximum wait time for a response from the
broker "
+ "in long polling by the client");
- /** for auto commit offset to rocketmq server */
+ /**
+ * Lets the RocketMQ client commit the offset of everything it has polled
on its own timer. Off
+ * by default: Flink commits the offsets of checkpointed state instead,
see {@link
+ * #COMMIT_OFFSETS_ON_CHECKPOINT}. Broker side offsets never decide where
a restored job starts
+ * reading, so enabling this only changes what consumer progress
monitoring reports.
+ */
public static final ConfigOption<Boolean> AUTO_COMMIT_OFFSET =
ConfigOptions.key(CONSUMER_PREFIX + "offset.commit.auto")
.booleanType()
- .defaultValue(true)
+ .defaultValue(false)
.withDescription("The setting for automatic commit of
offset");
public static final ConfigOption<Long> AUTO_COMMIT_OFFSET_INTERVAL =
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/metrics/RocketMQSourceReaderMetrics.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/metrics/RocketMQSourceReaderMetrics.java
index f21e12a..7c62f58 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/metrics/RocketMQSourceReaderMetrics.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/metrics/RocketMQSourceReaderMetrics.java
@@ -1,10 +1,11 @@
/*
- * 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
+ * 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
*
@@ -18,10 +19,29 @@
package org.apache.flink.connector.rocketmq.source.metrics;
import org.apache.flink.annotation.PublicEvolving;
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.metrics.Gauge;
+import org.apache.flink.metrics.MetricGroup;
import org.apache.flink.metrics.groups.SourceReaderMetricGroup;
import org.apache.rocketmq.common.message.MessageQueue;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Metrics of the RocketMQ source reader.
+ *
+ * <p>Offsets are exposed per message queue under the {@code
RocketmqSourceReader} group, keyed by
+ * topic and queue id, so a reader that owns several queues can be inspected
queue by queue. The lag
+ * of every queue is additionally summed into {@code pendingRecords}, which is
what Flink's
+ * backpressure and idleness tooling reads.
+ *
+ * <p>Lag needs the maximum offset of a queue, which only the broker knows.
The reader therefore
+ * pushes the values in with {@link #updateMaxOffset} whenever it refreshes
them, and the gauges
+ * here only ever report the last known value; reading a metric never triggers
a remote call.
+ */
@PublicEvolving
public class RocketMQSourceReaderMetrics {
@@ -38,9 +58,108 @@ public class RocketMQSourceReaderMetrics {
public static final String BYTES_CONSUMED_TOTAL = "bytes-consumed-total";
public static final String RECORDS_LAG = "records-lag";
- public RocketMQSourceReaderMetrics(SourceReaderMetricGroup
sourceReaderMetricGroup) {}
+ private final MetricGroup readerGroup;
+ private final Counter commitsSucceeded;
+ private final Counter commitsFailed;
+ private final Counter bytesConsumedTotal;
+ private final Map<MessageQueue, QueueMetrics> queues = new
ConcurrentHashMap<>();
+
+ public RocketMQSourceReaderMetrics(SourceReaderMetricGroup
sourceReaderMetricGroup) {
+ this.readerGroup =
sourceReaderMetricGroup.addGroup(ROCKETMQ_SOURCE_READER_METRIC_GROUP);
+ this.commitsSucceeded =
readerGroup.counter(COMMITS_SUCCEEDED_METRIC_COUNTER);
+ this.commitsFailed =
readerGroup.counter(COMMITS_FAILED_METRIC_COUNTER);
+
+ MetricGroup fetchManagerGroup =
+ sourceReaderMetricGroup
+ .addGroup(ROCKETMQ_CONSUMER_METRIC_GROUP)
+ .addGroup(CONSUMER_FETCH_MANAGER_GROUP);
+ this.bytesConsumedTotal =
fetchManagerGroup.counter(BYTES_CONSUMED_TOTAL);
+ fetchManagerGroup.gauge(RECORDS_LAG, (Gauge<Long>) this::getTotalLag);
+
+ sourceReaderMetricGroup.setPendingRecordsGauge((Gauge<Long>)
this::getTotalLag);
+ }
+
+ /** Starts reporting the offsets of a newly assigned queue. */
+ public void registerNewMessageQueue(MessageQueue messageQueue) {
+ queues.computeIfAbsent(messageQueue, this::createQueueMetrics);
+ }
+
+ /**
+ * Stops aggregating a queue that is no longer assigned to this reader.
+ *
+ * <p>{@link MetricGroup} offers no way to deregister from a connector, so
the gauges of the
+ * queue stay visible and keep reporting their last known values. The
queue count of a topic is
+ * finite, which bounds how many groups a reader can accumulate.
+ */
+ public void unregisterMessageQueue(MessageQueue messageQueue) {
+ queues.remove(messageQueue);
+ }
+
+ /** Records how far the reader has consumed a queue. */
+ public void setCurrentOffset(MessageQueue messageQueue, long offset) {
+ QueueMetrics metrics = queues.get(messageQueue);
+ if (metrics != null) {
+ metrics.currentOffset.set(offset);
+ }
+ }
+
+ /** Records the offset that was successfully committed to the broker for a
queue. */
+ public void setCommittedOffset(MessageQueue messageQueue, long offset) {
+ QueueMetrics metrics = queues.get(messageQueue);
+ if (metrics != null) {
+ metrics.committedOffset.set(offset);
+ }
+ }
+
+ /**
+ * Records the maximum offset the broker holds for a queue and derives its
lag from the offset
+ * the reader has consumed so far.
+ */
+ public void updateMaxOffset(MessageQueue messageQueue, long maxOffset) {
+ QueueMetrics metrics = queues.get(messageQueue);
+ if (metrics != null) {
+ long currentOffset = metrics.currentOffset.get();
+ metrics.lag.set(currentOffset < 0L ? 0L : Math.max(0L, maxOffset -
currentOffset));
+ }
+ }
+
+ public void recordBytesConsumed(long bytes) {
+ bytesConsumedTotal.inc(bytes);
+ }
+
+ public void recordCommitSucceeded() {
+ commitsSucceeded.inc();
+ }
+
+ public void recordCommitFailed() {
+ commitsFailed.inc();
+ }
+
+ private QueueMetrics createQueueMetrics(MessageQueue messageQueue) {
+ MetricGroup queueGroup =
+ readerGroup
+ .addGroup(TOPIC_GROUP, messageQueue.getTopic())
+ .addGroup(QUEUE_GROUP,
String.valueOf(messageQueue.getQueueId()));
+ QueueMetrics metrics = new QueueMetrics();
+ queueGroup.gauge(CURRENT_OFFSET_METRIC_GAUGE, (Gauge<Long>)
metrics.currentOffset::get);
+ queueGroup.gauge(COMMITTED_OFFSET_METRIC_GAUGE, (Gauge<Long>)
metrics.committedOffset::get);
+ queueGroup.gauge(RECORDS_LAG, (Gauge<Long>) metrics.lag::get);
+ return metrics;
+ }
+
+ private long getTotalLag() {
+ long total = 0L;
+ for (QueueMetrics metrics : queues.values()) {
+ total += metrics.lag.get();
+ }
+ return total;
+ }
- public void registerNewMessageQueue(MessageQueue messageQueue) {}
+ /** The mutable state behind the gauges of a single queue. */
+ private static final class QueueMetrics {
- public void unregisterMessageQueue(MessageQueue messageQueue) {}
+ private final AtomicLong currentOffset = new AtomicLong(-1L);
+ private final AtomicLong committedOffset = new AtomicLong(-1L);
+ private final AtomicLong lag = new AtomicLong(0L);
+ }
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/MessageViewExt.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/MessageViewExt.java
index b289f20..e48ca5e 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/MessageViewExt.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/MessageViewExt.java
@@ -17,6 +17,7 @@
package org.apache.flink.connector.rocketmq.source.reader;
+import org.apache.rocketmq.common.message.MessageConst;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.ArrayList;
@@ -26,7 +27,11 @@ import java.util.Map;
public class MessageViewExt implements MessageView {
- private static final String KEY_SEPARATE = "";
+ // Message.getKeys() returns the keys joined by MessageConst.KEY_SEPARATOR
(a single space) --
+ // that is what Message.setKeys(Collection) produces via String.join.
Splitting on anything
+ // else is wrong: splitting on "" yields one element per character, so a
message whose key is
+ // "smoke-11" would report eight keys ("s","m","o","k","e","-","1","1")
instead of one.
+ private static final String KEY_SEPARATE = MessageConst.KEY_SEPARATOR;
private final String messageId;
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 7ba42d6..12afce2 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
@@ -42,14 +42,23 @@ public class RocketMQRecordEmitter<T>
MessageView element, SourceOutput<T> output,
RocketMQSourceSplitState splitState)
throws IOException {
+ sourceOutputWrapper.setSourceOutput(output);
+ sourceOutputWrapper.setTimestamp(element.getEventTime());
try {
- sourceOutputWrapper.setSourceOutput(output);
- sourceOutputWrapper.setTimestamp(element.getEventTime());
deserializationSchema.deserialize(element, sourceOutputWrapper);
- splitState.setCurrentOffset(element.getQueueOffset() + 1);
} catch (Exception e) {
- throw new IOException("Failed to deserialize message due to", 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()),
+ e);
}
+ splitState.setCurrentOffset(element.getQueueOffset() + 1);
}
private static class SourceOutputWrapper<T> implements Collector<T> {
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 b8349e5..17c7e82 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
@@ -28,6 +28,7 @@ import
org.apache.flink.connector.base.source.reader.splitreader.SplitsAddition;
import org.apache.flink.connector.base.source.reader.splitreader.SplitsChange;
import org.apache.flink.connector.rocketmq.common.config.RocketMQOptions;
import org.apache.flink.connector.rocketmq.common.event.SourceInitAssignEvent;
+import org.apache.flink.connector.rocketmq.common.filter.Sql92MessageFilter;
import org.apache.flink.connector.rocketmq.common.lock.SpinLock;
import org.apache.flink.connector.rocketmq.source.InnerConsumer;
import org.apache.flink.connector.rocketmq.source.InnerConsumerImpl;
@@ -48,7 +49,9 @@ import javax.annotation.Nullable;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -59,6 +62,7 @@ import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
/**
* A {@link SplitReader} implementation that reads records from RocketMQ
partitions.
@@ -70,6 +74,10 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
private static final Logger LOG =
LoggerFactory.getLogger(RocketMQSplitReader.class);
+ // The maximum offset of a queue is only known to the broker, so the lag
is refreshed on a
+ // coarse interval instead of on every fetch.
+ private static final long LAG_REFRESH_INTERVAL_MS = 10_000L;
+
private volatile boolean wakeup = false;
private final InnerConsumer consumer;
@@ -87,12 +95,15 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
// Init status : true-finish init; false-not finish init
private boolean initFinish;
+ private long lastLagRefreshTime;
private final RocketMQRecordsWithSplitIds<MessageView> recordsWithSplitIds;
private final SpinLock lock;
- // Client-side filter configuration (P0 fix — tag/sql were dead in new API
path)
- private final String filterTag;
- private final String filterSql;
+ // Tag and SQL92 filters are evaluated here rather than on the broker: the
lite-pull consumer
+ // can only carry a tag sub-expression while queues are assigned
explicitly, and explicit
+ // assignment is what lets the enumerator own split allocation. See
Sql92MessageFilter.
+ private final Set<String> filterTags;
+ private final Sql92MessageFilter sqlFilter;
public RocketMQSplitReader(
Configuration configuration,
@@ -116,14 +127,18 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
this.commitOffsetsOnCheckpoint =
configuration.getBoolean(RocketMQSourceOptions.COMMIT_OFFSETS_ON_CHECKPOINT);
- // Read filter configuration for client-side filtering (P0 fix)
- this.filterTag =
configuration.getString(RocketMQSourceOptions.OPTIONAL_TAG);
- this.filterSql = configuration.get(RocketMQSourceOptions.OPTIONAL_SQL);
- if (filterSql != null) {
- LOG.warn(
- "SQL filter expression '{}' is set but client-side SQL
evaluation "
- + "is not supported with assign() mode. Only
tag-based filtering will be applied.",
- filterSql);
+ this.filterTags =
+
parseFilterTags(configuration.getString(RocketMQSourceOptions.OPTIONAL_TAG));
+ String sqlExpression =
configuration.get(RocketMQSourceOptions.OPTIONAL_SQL);
+ this.sqlFilter =
+ sqlExpression == null || sqlExpression.trim().isEmpty()
+ ? null
+ : new Sql92MessageFilter(sqlExpression);
+ if (sqlFilter != null) {
+ LOG.info(
+ "Evaluating SQL92 filter '{}' on the client side; assign
mode cannot push it "
+ + "down to the broker",
+ sqlFilter.getExpressionString());
}
}
@@ -145,16 +160,20 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
Duration.ofMillis(this.configuration.getLong(RocketMQOptions.POLL_TIMEOUT));
List<MessageView> messageExtList = consumer.poll(duration);
for (MessageView messageView : messageExtList) {
- // Client-side tag filtering (P0 fix)
+ MessageQueue messageQueue =
+ new MessageQueue(
+ messageView.getTopic(),
+ messageView.getBrokerName(),
+ messageView.getQueueId());
+ // Records dropped by a filter still advance the queue, so
they are counted here
+ // rather than after the filter.
+ rocketmqSourceReaderMetrics.setCurrentOffset(
+ messageQueue, messageView.getQueueOffset() + 1);
+
rocketmqSourceReaderMetrics.recordBytesConsumed(messageView.getStoreSize());
if (!matchesFilter(messageView)) {
continue;
}
- String splitId =
- UtilAll.getSplitId(
- new MessageQueue(
- messageView.getTopic(),
- messageView.getBrokerName(),
- messageView.getQueueId()));
+ String splitId = UtilAll.getSplitId(messageQueue);
recordsWithSplitIds.recordsForSplit(splitId).add(messageView);
if
(this.configuration.getBoolean(RocketMQSourceOptions.GLOBAL_DEBUG_MODE)) {
LOG.info(
@@ -164,6 +183,7 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
}
}
recordsWithSplitIds.prepareForRead();
+ refreshLagIfDue();
} catch (Exception e) {
if (wakeup) {
LOG.debug("Reader fetch interrupted by wakeup", e);
@@ -257,37 +277,83 @@ public class RocketMQSplitReader<T> implements
SplitReader<MessageView, RocketMQ
}
}
+ /** Returns whether the message satisfies the configured tag and SQL92
filters. */
+ private boolean matchesFilter(MessageView messageView) {
+ if (!filterTags.isEmpty() &&
!filterTags.contains(messageView.getTag())) {
+ return false;
+ }
+ return sqlFilter == null ||
sqlFilter.matches(messageView.getProperties());
+ }
+
/**
- * Client-side message filtering. Applies tag-based filtering for
consumers using assign() mode
- * where server-side MessageSelector is not available.
+ * Splits a {@code "tagA||tagB"} sub-expression into the set of accepted
tags. An absent or
+ * {@code "*"} expression accepts every tag and yields an empty set.
*/
- private boolean matchesFilter(MessageView messageView) {
- if (filterTag != null && !"*".equals(filterTag) &&
!filterTag.isEmpty()) {
- String messageTag = messageView.getTag();
- if (messageTag == null) {
- return false;
- }
- // Support "||" separated multi-tag filter expression
- String[] filterTags = filterTag.split("\\|\\|");
- boolean matched = false;
- for (String ft : filterTags) {
- if (ft.trim().equals(messageTag)) {
- matched = true;
- break;
- }
- }
- if (!matched) {
- return false;
- }
+ private static Set<String> parseFilterTags(@Nullable String tagExpression)
{
+ if (tagExpression == null) {
+ return Collections.emptySet();
}
- return true;
+ String trimmed = tagExpression.trim();
+ if (trimmed.isEmpty() || "*".equals(trimmed)) {
+ return Collections.emptySet();
+ }
+ return Arrays.stream(trimmed.split("\\|\\|"))
+ .map(String::trim)
+ .filter(tag -> !tag.isEmpty())
+ .collect(Collectors.toSet());
}
public void notifyCheckpointComplete(Map<MessageQueue, Long>
offsetsToCommit) {
- if (offsetsToCommit != null) {
- for (Map.Entry<MessageQueue, Long> entry :
offsetsToCommit.entrySet()) {
- consumer.commitOffset(entry.getKey(), entry.getValue());
- }
+ if (offsetsToCommit == null) {
+ return;
+ }
+ for (Map.Entry<MessageQueue, Long> entry : offsetsToCommit.entrySet())
{
+ MessageQueue messageQueue = entry.getKey();
+ long offset = entry.getValue();
+ consumer.commitOffset(messageQueue, offset)
+ .whenComplete(
+ (ignored, error) -> {
+ if (error == null) {
+
rocketmqSourceReaderMetrics.recordCommitSucceeded();
+
rocketmqSourceReaderMetrics.setCommittedOffset(
+ messageQueue, offset);
+ } else {
+
rocketmqSourceReaderMetrics.recordCommitFailed();
+ LOG.error(
+ "Failed to commit offset {} for mq
{}",
+ offset,
+
UtilAll.getQueueDescription(messageQueue),
+ error);
+ }
+ });
+ }
+ }
+
+ /**
+ * Asks the broker for the maximum offset of every assigned queue, at most
once per {@link
+ * #LAG_REFRESH_INTERVAL_MS}. The lookup is asynchronous so the fetch loop
never blocks on a
+ * remote call; a queue whose lookup fails simply keeps its previous lag.
+ */
+ private void refreshLagIfDue() {
+ long now = System.currentTimeMillis();
+ if (now - lastLagRefreshTime < LAG_REFRESH_INTERVAL_MS) {
+ return;
+ }
+ lastLagRefreshTime = now;
+ for (MessageQueue messageQueue : currentOffsetTable.keySet()) {
+ consumer.seekMaxOffset(messageQueue)
+ .whenComplete(
+ (maxOffset, error) -> {
+ if (error != null) {
+ LOG.debug(
+ "Failed to fetch the max offset of
mq {} for the lag metric",
+
UtilAll.getQueueDescription(messageQueue),
+ error);
+ } else if (maxOffset != null) {
+
rocketmqSourceReaderMetrics.updateMaxOffset(
+ messageQueue, maxOffset);
+ }
+ });
}
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/deserializer/RowDeserializationSchema.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/deserializer/RowDeserializationSchema.java
index 98baef7..24d9c71 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/deserializer/RowDeserializationSchema.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/connector/rocketmq/source/reader/deserializer/RowDeserializationSchema.java
@@ -20,9 +20,9 @@ package
org.apache.flink.connector.rocketmq.source.reader.deserializer;
import
org.apache.flink.api.common.serialization.DeserializationSchema.InitializationContext;
import org.apache.flink.api.common.typeinfo.TypeInformation;
-import org.apache.flink.configuration.ConfigOption;
-import org.apache.flink.configuration.ConfigOptions;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.common.config.RocketMQOptions;
+import org.apache.flink.connector.rocketmq.source.RocketMQSourceOptions;
import org.apache.flink.connector.rocketmq.source.util.ByteSerializer;
import org.apache.flink.connector.rocketmq.source.util.StringSerializer;
import org.apache.flink.table.api.TableSchema;
@@ -528,46 +528,54 @@ public class RowDeserializationSchema implements
QueryableSchema<List<BytesMessa
return this;
}
Configuration configuration = new Configuration();
- for (String key : properties.keySet()) {
- configuration.setString(key, properties.get(key));
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ configuration.setString(entry.getKey(), entry.getValue());
}
- String lengthCheck =
configuration.get(CollectorOption.LENGTH_CHECK);
+
applyLengthCheck(configuration.get(RocketMQOptions.OPTIONAL_LENGTH_CHECK));
+
this.setEncoding(configuration.get(RocketMQOptions.OPTIONAL_ENCODING));
+
this.setFieldDelimiter(configuration.get(RocketMQOptions.OPTIONAL_FIELD_DELIMITER));
+
this.setLineDelimiter(configuration.get(RocketMQOptions.OPTIONAL_LINE_DELIMITER));
+ this.setColumnErrorDebug(
+
configuration.get(RocketMQSourceOptions.OPTIONAL_COLUMN_ERROR_DEBUG));
+ return this;
+ }
+
+ /**
+ * Maps {@code rocketmq.client.message.length.check} onto the dirty
data strategies. An
+ * unknown value is rejected rather than ignored: silently keeping the
defaults is what
+ * turns a misspelled option into a stream of dropped records.
+ */
+ private void applyLengthCheck(String lengthCheck) {
switch (lengthCheck.toUpperCase()) {
+ case "NONE":
+ break;
case "SKIP":
- {
- this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
- this.setFieldMissingStrategy(DirtyDataStrategy.SKIP);
- this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP);
- }
+ this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
+ this.setFieldMissingStrategy(DirtyDataStrategy.SKIP);
+ this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP);
break;
case "PAD":
- {
- this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
- this.setFieldMissingStrategy(DirtyDataStrategy.PAD);
- this.setFieldIncrementStrategy(DirtyDataStrategy.CUT);
- }
- break;
- case "EXCEPTION":
- {
-
this.setFormatErrorStrategy(DirtyDataStrategy.EXCEPTION);
-
this.setFieldMissingStrategy(DirtyDataStrategy.EXCEPTION);
-
this.setFieldIncrementStrategy(DirtyDataStrategy.EXCEPTION);
- }
+ this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
+ this.setFieldMissingStrategy(DirtyDataStrategy.PAD);
+ this.setFieldIncrementStrategy(DirtyDataStrategy.CUT);
break;
case "SKIP_SILENT":
- {
-
this.setFormatErrorStrategy(DirtyDataStrategy.SKIP_SILENT);
-
this.setFieldMissingStrategy(DirtyDataStrategy.SKIP_SILENT);
-
this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP_SILENT);
- }
+ this.setFormatErrorStrategy(DirtyDataStrategy.SKIP_SILENT);
+
this.setFieldMissingStrategy(DirtyDataStrategy.SKIP_SILENT);
+
this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP_SILENT);
+ break;
+ case "EXCEPTION":
+ this.setFormatErrorStrategy(DirtyDataStrategy.EXCEPTION);
+ this.setFieldMissingStrategy(DirtyDataStrategy.EXCEPTION);
+
this.setFieldIncrementStrategy(DirtyDataStrategy.EXCEPTION);
break;
default:
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported value '%s' for %s. Expected
one of NONE, SKIP, "
+ + "SKIP_SILENT, PAD, EXCEPTION.",
+ lengthCheck,
RocketMQOptions.OPTIONAL_LENGTH_CHECK.key()));
}
-
this.setEncoding(configuration.getString(CollectorOption.ENCODING));
-
this.setFieldDelimiter(configuration.getString(CollectorOption.FIELD_DELIMITER));
-
this.setLineDelimiter(configuration.getString(CollectorOption.LINE_DELIMITER));
-
this.setColumnErrorDebug(configuration.getBoolean(CollectorOption.COLUMN_ERROR_DEBUG));
- return this;
}
public RowDeserializationSchema build() {
@@ -586,18 +594,4 @@ public class RowDeserializationSchema implements
QueryableSchema<List<BytesMessa
properties);
}
}
-
- /** Options for {@link RowDeserializationSchema}. */
- public static class CollectorOption {
- public static final ConfigOption<String> ENCODING =
-
ConfigOptions.key("encoding".toLowerCase()).defaultValue("UTF-8");
- public static final ConfigOption<String> FIELD_DELIMITER =
-
ConfigOptions.key("fieldDelimiter".toLowerCase()).defaultValue("\u0001");
- public static final ConfigOption<String> LINE_DELIMITER =
-
ConfigOptions.key("lineDelimiter".toLowerCase()).defaultValue("\n");
- public static final ConfigOption<Boolean> COLUMN_ERROR_DEBUG =
-
ConfigOptions.key("columnErrorDebug".toLowerCase()).defaultValue(true);
- public static final ConfigOption<String> LENGTH_CHECK =
-
ConfigOptions.key("lengthCheck".toLowerCase()).defaultValue("NONE");
- }
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSink.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSink.java
index bc3ef5c..f6cae6e 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSink.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSink.java
@@ -38,10 +38,12 @@ import
org.apache.rocketmq.remoting.exception.RemotingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
/**
* The RocketMQSink provides at-least-once reliability guarantees when
checkpoints are enabled and
@@ -106,8 +108,17 @@ public class RocketMQSink extends
RichSinkFunction<Message> implements Checkpoin
latencyGauge = MetricUtils.registerOutLatency(getRuntimeContext());
}
+ /**
+ * The first asynchronous send failure that has not been reported yet. An
async callback runs on
+ * a client thread, so it cannot fail the task directly; the failure is
parked here and rethrown
+ * from the next {@link #invoke} or from {@link #finish}. Without this the
job keeps running and
+ * checkpointing while every record is being dropped.
+ */
+ private final AtomicReference<Throwable> asyncSendException = new
AtomicReference<>();
+
@Override
public void invoke(Message input, Context context) throws Exception {
+ checkAsyncSendException();
sinkInTps.markEvent();
if (batchFlushOnCheckpoint) {
@@ -138,6 +149,7 @@ public class RocketMQSink extends RichSinkFunction<Message>
implements Checkpoin
public void onException(Throwable throwable) {
if (throwable != null) {
LOG.error("Async send message failure!",
throwable);
+ asyncSendException.compareAndSet(null,
throwable);
}
}
};
@@ -152,6 +164,7 @@ public class RocketMQSink extends RichSinkFunction<Message>
implements Checkpoin
}
} catch (Exception e) {
LOG.error("Async send message failure!", e);
+ asyncSendException.compareAndSet(null, e);
}
} else {
try {
@@ -180,6 +193,19 @@ public class RocketMQSink extends
RichSinkFunction<Message> implements Checkpoin
}
}
+ @Override
+ public void finish() throws Exception {
+ checkAsyncSendException();
+ }
+
+ /** Rethrows the first asynchronous send failure recorded since the last
call. */
+ private void checkAsyncSendException() throws IOException {
+ Throwable throwable = asyncSendException.getAndSet(null);
+ if (throwable != null) {
+ throw new IOException("Async send message failed", throwable);
+ }
+ }
+
public RocketMQSink withAsync(boolean async) {
this.async = async;
return this;
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSourceFunction.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSourceFunction.java
index 42dc415..2fe7f98 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSourceFunction.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/RocketMQSourceFunction.java
@@ -25,6 +25,7 @@ import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.common.filter.Sql92MessageFilter;
import org.apache.flink.metrics.Counter;
import org.apache.flink.metrics.Meter;
import org.apache.flink.metrics.MeterView;
@@ -48,7 +49,6 @@ import org.apache.commons.collections.map.LinkedMap;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
-import org.apache.rocketmq.client.consumer.MessageSelector;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
@@ -88,6 +88,7 @@ public class RocketMQSourceFunction<OUT> extends
RichParallelSourceFunction<OUT>
private RunningChecker runningChecker;
private transient DefaultLitePullConsumer consumer;
+ private transient Sql92MessageFilter sqlFilter;
private KeyValueDeserializationSchema<OUT> schema;
private transient ListState<Tuple2<MessageQueue, Long>> unionOffsetStates;
@@ -227,6 +228,11 @@ public class RocketMQSourceFunction<OUT> extends
RichParallelSourceFunction<OUT>
Collection<MessageQueue> totalQueues =
consumer.fetchMessageQueues(topic);
messageQueues =
RocketMQUtils.allocate(totalQueues, taskNumber,
ctx.getIndexOfThisSubtask());
+ // Assign the queues this subtask was allocated instead of
subscribing. subscribe() leaves
+ // allocation to the client's own rebalance, which runs
asynchronously, so the assigned list
+ // is still empty when initOffsets() seeks below and every seek fails
with "The message
+ // queue is not in assigned list".
+ consumer.assign(messageQueues);
// If the job recovers from the state, the state has already contained
the offsets of last
// commit.
if (restored) {
@@ -249,12 +255,13 @@ public class RocketMQSourceFunction<OUT> extends
RichParallelSourceFunction<OUT>
RocketMQConfig.DEFAULT_CONSUMER_BATCH_SIZE);
// Watermark emission removed: legacy watermark assigners are no
longer available.
// Use WatermarkStrategy with the new Source API instead.
- if (StringUtils.isEmpty(sql)) {
- consumer.subscribe(topic, tag);
- } else {
- // pull with sql do not support block pull.
- consumer.subscribe(topic, MessageSelector.bySql(sql));
+ // The queues are assigned in open(); only the filter is set up here.
A tag sub-expression
+ // is honoured by the broker in assign mode, while SQL92 cannot be
pushed down and is
+ // evaluated against the message properties on the way out.
+ if (!StringUtils.isEmpty(tag) &&
!RocketMQConfig.DEFAULT_CONSUMER_TAG.equals(tag)) {
+ consumer.setSubExpressionForAssign(topic, tag);
}
+ this.sqlFilter = StringUtils.isEmpty(sql) ? null : new
Sql92MessageFilter(sql);
for (MessageQueue mq : messageQueues) {
this.executor.execute(() -> consumeMessages(mq, context,
pullBatchSize));
}
@@ -317,6 +324,9 @@ public class RocketMQSourceFunction<OUT> extends
RichParallelSourceFunction<OUT>
private void emitMessages(List<MessageExt> messages, SourceContext<OUT>
context) {
long fetchTime = System.currentTimeMillis();
for (MessageExt msg : messages) {
+ if (sqlFilter != null && !sqlFilter.matches(msg.getProperties())) {
+ continue;
+ }
byte[] key =
msg.getKeys() != null ?
msg.getKeys().getBytes(StandardCharsets.UTF_8) : null;
byte[] value = msg.getBody();
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/common/serialization/RowKeyValueDeserializationSchema.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/common/serialization/RowKeyValueDeserializationSchema.java
index bf7d58d..69a2fa2 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/common/serialization/RowKeyValueDeserializationSchema.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/common/serialization/RowKeyValueDeserializationSchema.java
@@ -17,9 +17,9 @@
package org.apache.flink.streaming.connectors.rocketmq.common.serialization;
import org.apache.flink.api.common.typeinfo.TypeInformation;
-import org.apache.flink.configuration.ConfigOption;
-import org.apache.flink.configuration.ConfigOptions;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.common.config.RocketMQOptions;
+import org.apache.flink.connector.rocketmq.source.RocketMQSourceOptions;
import
org.apache.flink.connector.rocketmq.source.reader.deserializer.DirtyDataStrategy;
import org.apache.flink.connector.rocketmq.source.util.ByteSerializer;
import org.apache.flink.connector.rocketmq.source.util.StringSerializer;
@@ -337,45 +337,53 @@ public class RowKeyValueDeserializationSchema implements
KeyValueDeserialization
return this;
}
Configuration configuration = new Configuration();
- for (String key : properties.keySet()) {
- configuration.setString(key, properties.get(key));
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ configuration.setString(entry.getKey(), entry.getValue());
}
- String lengthCheck =
configuration.get(CollectorOption.LENGTH_CHECK);
+
applyLengthCheck(configuration.get(RocketMQOptions.OPTIONAL_LENGTH_CHECK));
+
this.setEncoding(configuration.get(RocketMQOptions.OPTIONAL_ENCODING));
+
this.setFieldDelimiter(configuration.get(RocketMQOptions.OPTIONAL_FIELD_DELIMITER));
+ this.setColumnErrorDebug(
+
configuration.get(RocketMQSourceOptions.OPTIONAL_COLUMN_ERROR_DEBUG));
+ return this;
+ }
+
+ /**
+ * Maps {@code rocketmq.client.message.length.check} onto the dirty
data strategies. An
+ * unknown value is rejected rather than ignored: silently keeping the
defaults is what
+ * turns a misspelled option into a stream of dropped records.
+ */
+ private void applyLengthCheck(String lengthCheck) {
switch (lengthCheck.toUpperCase()) {
+ case "NONE":
+ break;
case "SKIP":
- {
- this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
- this.setFieldMissingStrategy(DirtyDataStrategy.SKIP);
- this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP);
- }
+ this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
+ this.setFieldMissingStrategy(DirtyDataStrategy.SKIP);
+ this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP);
break;
case "PAD":
- {
- this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
- this.setFieldMissingStrategy(DirtyDataStrategy.PAD);
- this.setFieldIncrementStrategy(DirtyDataStrategy.CUT);
- }
- break;
- case "EXCEPTION":
- {
-
this.setFormatErrorStrategy(DirtyDataStrategy.EXCEPTION);
-
this.setFieldMissingStrategy(DirtyDataStrategy.EXCEPTION);
-
this.setFieldIncrementStrategy(DirtyDataStrategy.EXCEPTION);
- }
+ this.setFormatErrorStrategy(DirtyDataStrategy.SKIP);
+ this.setFieldMissingStrategy(DirtyDataStrategy.PAD);
+ this.setFieldIncrementStrategy(DirtyDataStrategy.CUT);
break;
case "SKIP_SILENT":
- {
-
this.setFormatErrorStrategy(DirtyDataStrategy.SKIP_SILENT);
-
this.setFieldMissingStrategy(DirtyDataStrategy.SKIP_SILENT);
-
this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP_SILENT);
- }
+ this.setFormatErrorStrategy(DirtyDataStrategy.SKIP_SILENT);
+
this.setFieldMissingStrategy(DirtyDataStrategy.SKIP_SILENT);
+
this.setFieldIncrementStrategy(DirtyDataStrategy.SKIP_SILENT);
+ break;
+ case "EXCEPTION":
+ this.setFormatErrorStrategy(DirtyDataStrategy.EXCEPTION);
+ this.setFieldMissingStrategy(DirtyDataStrategy.EXCEPTION);
+
this.setFieldIncrementStrategy(DirtyDataStrategy.EXCEPTION);
break;
default:
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported value '%s' for %s. Expected
one of NONE, SKIP, "
+ + "SKIP_SILENT, PAD, EXCEPTION.",
+ lengthCheck,
RocketMQOptions.OPTIONAL_LENGTH_CHECK.key()));
}
-
this.setEncoding(configuration.getString(CollectorOption.ENCODING));
-
this.setFieldDelimiter(configuration.getString(CollectorOption.FIELD_DELIMITER));
-
this.setColumnErrorDebug(configuration.getBoolean(CollectorOption.COLUMN_ERROR_DEBUG));
- return this;
}
public RowKeyValueDeserializationSchema build() {
@@ -390,16 +398,4 @@ public class RowKeyValueDeserializationSchema implements
KeyValueDeserialization
properties);
}
}
-
- /** Options for {@link RowKeyValueDeserializationSchema}. */
- public static class CollectorOption {
- public static final ConfigOption<String> ENCODING =
-
ConfigOptions.key("encoding".toLowerCase()).defaultValue("UTF-8");
- public static final ConfigOption<String> FIELD_DELIMITER =
-
ConfigOptions.key("fieldDelimiter".toLowerCase()).defaultValue("\u0001");
- public static final ConfigOption<Boolean> COLUMN_ERROR_DEBUG =
-
ConfigOptions.key("columnErrorDebug".toLowerCase()).defaultValue(true);
- public static final ConfigOption<String> LENGTH_CHECK =
-
ConfigOptions.key("lengthCheck".toLowerCase()).defaultValue("NONE");
- }
}
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RequiredOptionValidator.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RequiredOptionValidator.java
new file mode 100644
index 0000000..01c3398
--- /dev/null
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RequiredOptionValidator.java
@@ -0,0 +1,56 @@
+/*
+ * 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.streaming.connectors.rocketmq.table;
+
+import org.apache.flink.configuration.ConfigOption;
+import org.apache.flink.table.api.ValidationException;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Reports the required table options a DDL does not set, naming the missing
keys only.
+ *
+ * <p>{@link
org.apache.flink.table.factories.FactoryUtil.TableFactoryHelper#validate()}
repeats the
+ * table options in its failure message, and a WITH clause normally carries
{@code accessKey} and
+ * {@code secretKey}. That message ends up in the JobManager log and in the
REST exception history,
+ * so a typo would turn the credentials into plaintext in both. Factories
therefore run this check
+ * first and still call the helper afterwards, which keeps unknown option
detection unchanged.
+ */
+final class RequiredOptionValidator {
+
+ private RequiredOptionValidator() {}
+
+ static void validate(Set<ConfigOption<?>> requiredOptions, Map<String,
String> tableOptions) {
+ List<String> missing =
+ requiredOptions.stream()
+ .map(ConfigOption::key)
+ .filter(key -> !tableOptions.containsKey(key))
+ .sorted()
+ .collect(Collectors.toList());
+ if (!missing.isEmpty()) {
+ throw new ValidationException(
+ String.format(
+ "Missing required option%s for the RocketMQ
connector: %s",
+ missing.size() == 1 ? "" : "s", missing));
+ }
+ }
+}
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 1210268..3c5f240 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
@@ -98,6 +98,7 @@ public class RocketMQDynamicTableSinkFactory implements
DynamicTableSinkFactory
@Override
public DynamicTableSink createDynamicTableSink(Context context) {
FactoryUtil.TableFactoryHelper helper = createTableFactoryHelper(this,
context);
+ RequiredOptionValidator.validate(requiredOptions(),
context.getCatalogTable().getOptions());
helper.validate();
Map<String, String> rawProperties =
context.getCatalogTable().getOptions();
Configuration properties = Configuration.fromMap(rawProperties);
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactory.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactory.java
index c5754c7..cd4f3f9 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactory.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactory.java
@@ -83,7 +83,6 @@ public class RocketMQDynamicTableSourceFactory implements
DynamicTableSourceFact
optionalOptions.add(RocketMQSourceOptions.OPTIONAL_SECRET_KEY);
optionalOptions.add(RocketMQSourceOptions.OPTIONAL_STARTUP_SCAN_MODE);
optionalOptions.add(RocketMQSourceOptions.OPTIONAL_STARTUP_OFFSET_DATE);
-
optionalOptions.add(RocketMQSourceOptions.OPTIONAL_STARTUP_OFFSET_STRATEGY);
optionalOptions.add(RocketMQSourceOptions.OPTIONAL_USE_NEW_API);
optionalOptions.add(RocketMQSourceOptions.PULL_BATCH_SIZE);
optionalOptions.add(RocketMQSourceOptions.ALLOCATE_MESSAGE_QUEUE_STRATEGY);
@@ -99,6 +98,7 @@ public class RocketMQDynamicTableSourceFactory implements
DynamicTableSourceFact
@Override
public DynamicTableSource createDynamicTableSource(Context context) {
FactoryUtil.TableFactoryHelper helper = createTableFactoryHelper(this,
context);
+ RequiredOptionValidator.validate(requiredOptions(),
context.getCatalogTable().getOptions());
helper.validate();
Map<String, String> rawProperties =
context.getCatalogTable().getOptions();
Configuration configuration = Configuration.fromMap(rawProperties);
diff --git
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQScanTableSource.java
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQScanTableSource.java
index 6d12961..43864f9 100644
---
a/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQScanTableSource.java
+++
b/flink-connector-rocketmq/src/main/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQScanTableSource.java
@@ -238,8 +238,16 @@ public class RocketMQScanTableSource implements
ScanTableSource, SupportsReading
case "committed":
return OffsetsSelector.committedOffsets();
case "latest":
- default:
return OffsetsSelector.latest();
+ default:
+ // Silently falling back to a default is what let a misspelled
scan mode look like
+ // a healthy job that started reading from the wrong position.
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported value '%s' for %s. Expected one
of earliest, latest, "
+ + "timestamp, committed.",
+ consumerOffsetMode,
+
RocketMQSourceOptions.OPTIONAL_STARTUP_SCAN_MODE.key()));
}
}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/common/filter/Sql92MessageFilterTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/common/filter/Sql92MessageFilterTest.java
new file mode 100644
index 0000000..b2009ef
--- /dev/null
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/common/filter/Sql92MessageFilterTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.common.filter;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link Sql92MessageFilter}. Message properties always arrive as
strings, so the cases
+ * below also pin down how literals are coerced, which is what decides whether
a filter silently
+ * accepts every message.
+ */
+class Sql92MessageFilterTest {
+
+ @Test
+ void numericComparisonKeepsOnlyMatchingMessagesTest() {
+ Sql92MessageFilter filter = new Sql92MessageFilter("id > 50");
+
+ List<Integer> matched = new ArrayList<>();
+ for (int id = 0; id < 100; id++) {
+ if (filter.matches(properties("id", String.valueOf(id)))) {
+ matched.add(id);
+ }
+ }
+
+ assertThat(matched).hasSize(49).startsWith(51).endsWith(99);
+ }
+
+ @Test
+ void stringEqualityAndLogicalOperatorsTest() {
+ Sql92MessageFilter filter = new Sql92MessageFilter("region =
'hangzhou' AND retries >= 2");
+
+ assertThat(filter.matches(row("hangzhou", "3"))).isTrue();
+ assertThat(filter.matches(row("hangzhou", "1"))).isFalse();
+ assertThat(filter.matches(row("beijing", "3"))).isFalse();
+ }
+
+ @Test
+ void inBetweenAndNotOperatorsTest() {
+ Sql92MessageFilter inFilter = new Sql92MessageFilter("level IN ('a',
'b')");
+ assertThat(inFilter.matches(properties("level", "a"))).isTrue();
+ assertThat(inFilter.matches(properties("level", "c"))).isFalse();
+
+ Sql92MessageFilter betweenFilter = new Sql92MessageFilter("seq BETWEEN
10 AND 20");
+ assertThat(betweenFilter.matches(properties("seq", "10"))).isTrue();
+ assertThat(betweenFilter.matches(properties("seq", "21"))).isFalse();
+
+ Sql92MessageFilter notFilter = new Sql92MessageFilter("NOT (seq > 5)");
+ assertThat(notFilter.matches(properties("seq", "1"))).isTrue();
+ assertThat(notFilter.matches(properties("seq", "9"))).isFalse();
+ }
+
+ @Test
+ void missingPropertyDoesNotMatchTest() {
+ Sql92MessageFilter filter = new Sql92MessageFilter("id > 50");
+
+ assertThat(filter.matches(Collections.emptyMap())).isFalse();
+ assertThat(filter.matches(properties("other", "99"))).isFalse();
+ assertThat(filter.matches(null)).isFalse();
+ }
+
+ @Test
+ void malformedExpressionFailsAtConstructionTest() {
+ assertThatThrownBy(() -> new Sql92MessageFilter("id > > 50"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("id > > 50");
+ }
+
+ private static Map<String, String> row(String region, String retries) {
+ Map<String, String> properties = new HashMap<>();
+ properties.put("region", region);
+ properties.put("retries", retries);
+ return properties;
+ }
+
+ private static Map<String, String> properties(String key, String value) {
+ return Collections.singletonMap(key, value);
+ }
+}
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
new file mode 100644
index 0000000..1828201
--- /dev/null
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/sink/writer/RocketMQWriterQueueSelectorTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.writer;
+
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.base.DeliveryGuarantee;
+import org.apache.flink.connector.rocketmq.sink.InnerProducer;
+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.mockito.Mockito;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+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.
+ */
+class RocketMQWriterQueueSelectorTest {
+
+ private static final String TOPIC = "TopicWriterSelector";
+
+ @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);
+ }
+
+ @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) {
+ Configuration configuration = new Configuration();
+ configuration.setString(
+ RocketMQSinkOptions.DELIVERY_GUARANTEE,
DeliveryGuarantee.AT_LEAST_ONCE.name());
+ return new RocketMQWriter<>(
+ configuration,
+ selector,
+ new StringToMessageSchema(),
+ Mockito.mock(Sink.InitContext.class),
+ producer);
+ }
+
+ /** Wraps the record into a message body so the send call can be
inspected. */
+ private static class StringToMessageSchema implements
RocketMQSerializationSchema<String> {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Message serialize(String element, RocketMQSinkContext context,
Long timestamp) {
+ return new Message(TOPIC,
element.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ /** 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<>();
+
+ @Override
+ public void start() {}
+
+ @Override
+ public String getProducerGroup() {
+ return "recording-producer";
+ }
+
+ @Override
+ public CompletableFuture<SendResult> send(Message message) {
+ plainSends.add(message);
+ return CompletableFuture.completedFuture(new SendResult());
+ }
+
+ @Override
+ public CompletableFuture<SendResult> send(
+ Message message, MessageQueueSelector selector, Object arg) {
+ selectorSends.add(new SelectorSend(message, selector, arg));
+ return CompletableFuture.completedFuture(new SendResult());
+ }
+
+ @Override
+ public CompletableFuture<SendResult> sendMessageInTransaction(Message
message) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CompletableFuture<Void> commit(SendCommittable sendCommittable)
{
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CompletableFuture<Void> rollback(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;
+
+ 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/metrics/RocketMQSourceReaderMetricsTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/metrics/RocketMQSourceReaderMetricsTest.java
new file mode 100644
index 0000000..8ea85f7
--- /dev/null
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/metrics/RocketMQSourceReaderMetricsTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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.metrics;
+
+import org.apache.flink.metrics.Counter;
+import org.apache.flink.metrics.Gauge;
+import org.apache.flink.metrics.MetricGroup;
+import org.apache.flink.metrics.SimpleCounter;
+import org.apache.flink.metrics.groups.OperatorIOMetricGroup;
+import org.apache.flink.metrics.groups.SourceReaderMetricGroup;
+import org.apache.flink.metrics.groups.UnregisteredMetricsGroup;
+
+import org.apache.rocketmq.common.message.MessageQueue;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link RocketMQSourceReaderMetrics}. The metric groups are
replaced by a recording fake
+ * so the gauges and counters can be read back the way a metric reporter would.
+ */
+class RocketMQSourceReaderMetricsTest {
+
+ private static final MessageQueue QUEUE_0 = new MessageQueue("TopicA",
"broker-a", 0);
+ private static final MessageQueue QUEUE_1 = new MessageQueue("TopicA",
"broker-a", 1);
+
+ private final RecordingSourceReaderMetricGroup metricGroup =
+ new RecordingSourceReaderMetricGroup();
+ private final RocketMQSourceReaderMetrics metrics =
+ new RocketMQSourceReaderMetrics(metricGroup);
+
+ @Test
+ void registeredQueueExposesOffsetGaugesTest() {
+ metrics.registerNewMessageQueue(QUEUE_0);
+ metrics.setCurrentOffset(QUEUE_0, 42L);
+ metrics.setCommittedOffset(QUEUE_0, 40L);
+
+ RecordingMetricGroup queueGroup = queueGroup("TopicA", "0");
+ assertThat(gaugeValue(queueGroup, "currentOffset")).isEqualTo(42L);
+ assertThat(gaugeValue(queueGroup, "committedOffset")).isEqualTo(40L);
+ }
+
+ @Test
+ void lagIsDerivedFromTheBrokerMaxOffsetTest() {
+ metrics.registerNewMessageQueue(QUEUE_0);
+ metrics.setCurrentOffset(QUEUE_0, 100L);
+ metrics.updateMaxOffset(QUEUE_0, 250L);
+
+ assertThat(gaugeValue(queueGroup("TopicA", "0"),
"records-lag")).isEqualTo(150L);
+ assertThat(metricGroup.pendingRecords.getValue()).isEqualTo(150L);
+ }
+
+ @Test
+ void lagNeverGoesNegativeAndStartsAtZeroTest() {
+ metrics.registerNewMessageQueue(QUEUE_0);
+
+ assertThat(metricGroup.pendingRecords.getValue()).isZero();
+
+ metrics.setCurrentOffset(QUEUE_0, 300L);
+ metrics.updateMaxOffset(QUEUE_0, 120L);
+
+ assertThat(metricGroup.pendingRecords.getValue()).isZero();
+ }
+
+ @Test
+ void pendingRecordsSumsEveryAssignedQueueTest() {
+ metrics.registerNewMessageQueue(QUEUE_0);
+ metrics.registerNewMessageQueue(QUEUE_1);
+ metrics.setCurrentOffset(QUEUE_0, 10L);
+ metrics.updateMaxOffset(QUEUE_0, 30L);
+ metrics.setCurrentOffset(QUEUE_1, 5L);
+ metrics.updateMaxOffset(QUEUE_1, 100L);
+
+ assertThat(metricGroup.pendingRecords.getValue()).isEqualTo(115L);
+ }
+
+ @Test
+ void unregisteredQueueStopsContributingToLagTest() {
+ metrics.registerNewMessageQueue(QUEUE_0);
+ metrics.registerNewMessageQueue(QUEUE_1);
+ metrics.setCurrentOffset(QUEUE_0, 10L);
+ metrics.updateMaxOffset(QUEUE_0, 30L);
+ metrics.setCurrentOffset(QUEUE_1, 5L);
+ metrics.updateMaxOffset(QUEUE_1, 100L);
+
+ metrics.unregisterMessageQueue(QUEUE_1);
+
+ assertThat(metricGroup.pendingRecords.getValue()).isEqualTo(20L);
+ // Updates for a queue that is no longer assigned must not resurrect
it.
+ metrics.updateMaxOffset(QUEUE_1, 500L);
+ assertThat(metricGroup.pendingRecords.getValue()).isEqualTo(20L);
+ }
+
+ @Test
+ void commitCountersTrackSuccessAndFailureTest() {
+ metrics.recordCommitSucceeded();
+ metrics.recordCommitSucceeded();
+ metrics.recordCommitFailed();
+
+ RecordingMetricGroup readerGroup =
metricGroup.group("RocketmqSourceReader");
+
assertThat(readerGroup.counters.get("commitsSucceeded").getCount()).isEqualTo(2L);
+
assertThat(readerGroup.counters.get("commitsFailed").getCount()).isEqualTo(1L);
+ }
+
+ @Test
+ void bytesConsumedCounterAccumulatesTest() {
+ metrics.recordBytesConsumed(1024L);
+ metrics.recordBytesConsumed(512L);
+
+ RecordingMetricGroup fetchManager =
+
metricGroup.group("RocketMQConsumer").group("consumer-fetch-manager-metrics");
+
assertThat(fetchManager.counters.get("bytes-consumed-total").getCount()).isEqualTo(1536L);
+ }
+
+ private RecordingMetricGroup queueGroup(String topic, String queueId) {
+ return metricGroup
+ .group("RocketmqSourceReader")
+ .group("topic=" + topic)
+ .group("queue=" + queueId);
+ }
+
+ private static long gaugeValue(RecordingMetricGroup group, String name) {
+ Gauge<?> gauge = group.gauges.get(name);
+ assertThat(gauge).as("gauge %s", name).isNotNull();
+ return ((Number) gauge.getValue()).longValue();
+ }
+
+ /** Records every metric registered on it so tests can read the values
back. */
+ private static class RecordingMetricGroup extends UnregisteredMetricsGroup
{
+
+ final Map<String, Gauge<?>> gauges = new HashMap<>();
+ final Map<String, Counter> counters = new HashMap<>();
+ final Map<String, RecordingMetricGroup> groups = new HashMap<>();
+
+ @Override
+ public <T, G extends Gauge<T>> G gauge(String name, G gauge) {
+ gauges.put(name, gauge);
+ return gauge;
+ }
+
+ @Override
+ public Counter counter(String name) {
+ SimpleCounter counter = new SimpleCounter();
+ counters.put(name, counter);
+ return counter;
+ }
+
+ @Override
+ public MetricGroup addGroup(String name) {
+ return groups.computeIfAbsent(name, ignored -> new
RecordingMetricGroup());
+ }
+
+ @Override
+ public MetricGroup addGroup(String key, String value) {
+ return groups.computeIfAbsent(key + "=" + value, ignored -> new
RecordingMetricGroup());
+ }
+
+ RecordingMetricGroup group(String name) {
+ RecordingMetricGroup group = groups.get(name);
+ assertThat(group).as("metric group %s", name).isNotNull();
+ return group;
+ }
+ }
+
+ /** The recording group plus the handful of methods only a source reader
group declares. */
+ private static class RecordingSourceReaderMetricGroup extends
RecordingMetricGroup
+ implements SourceReaderMetricGroup {
+
+ Gauge<Long> pendingRecords;
+
+ @Override
+ public OperatorIOMetricGroup getIOMetricGroup() {
+ return UnregisteredMetricsGroup.createOperatorIOMetricGroup();
+ }
+
+ @Override
+ public Counter getNumRecordsInErrorsCounter() {
+ return new SimpleCounter();
+ }
+
+ @Override
+ public void setPendingBytesGauge(Gauge<Long> pendingBytesGauge) {}
+
+ @Override
+ public void setPendingRecordsGauge(Gauge<Long> pendingRecordsGauge) {
+ this.pendingRecords = pendingRecordsGauge;
+ }
+ }
+}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/deserializer/RowDeserializationSchemaOptionsTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/deserializer/RowDeserializationSchemaOptionsTest.java
new file mode 100644
index 0000000..dcc749d
--- /dev/null
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/connector/rocketmq/source/reader/deserializer/RowDeserializationSchemaOptionsTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.deserializer;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.api.TableSchema;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.descriptors.DescriptorProperties;
+import org.apache.flink.util.Collector;
+
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests that the format options a user writes in the DDL actually reach {@link
+ * RowDeserializationSchema}. Reading them under a different key than the one
the table factory
+ * registers makes every option silently ineffective, and with a multi column
schema the default
+ * field delimiter then drops every record instead of failing.
+ */
+class RowDeserializationSchemaOptionsTest {
+
+ private static final String FIELD_DELIMITER_KEY =
"rocketmq.client.message.field.delimiter";
+ private static final String LENGTH_CHECK_KEY =
"rocketmq.client.message.length.check";
+
+ @Test
+ void fieldDelimiterFromDdlSplitsRowTest() {
+ List<RowData> rows = deserialize(options(FIELD_DELIMITER_KEY, "|"),
"SMOKE1|a|b|c|d|e");
+
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getArity()).isEqualTo(6);
+ assertThat(rows.get(0).getString(0).toString()).isEqualTo("SMOKE1");
+ assertThat(rows.get(0).getString(5).toString()).isEqualTo("e");
+ }
+
+ @Test
+ void defaultDelimiterDoesNotSplitPipeSeparatedBodyTest() {
+ // Documents the trap the option above avoids: with the default
delimiter the body is a
+ // single field, so a six column row has too few fields and is dropped.
+ assertThat(deserialize(ddlOptions(), "SMOKE1|a|b|c|d|e")).isEmpty();
+ }
+
+ @Test
+ void lineDelimiterFromDdlSplitsBodyIntoRowsTest() {
+ Map<String, String> options = options(FIELD_DELIMITER_KEY, "|");
+ options.put("rocketmq.client.message.line.delimiter", "#");
+
+ List<RowData> rows = deserialize(options,
"SMOKE1|a|b|c|d|e#SMOKE1|f|g|h|i|j");
+
+ assertThat(rows).hasSize(2);
+ assertThat(rows.get(1).getString(1).toString()).isEqualTo("f");
+ }
+
+ @Test
+ void unsupportedLengthCheckIsRejectedTest() {
+ assertThatThrownBy(() -> deserialize(options(LENGTH_CHECK_KEY,
"BOGUS"), "SMOKE1|a"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(LENGTH_CHECK_KEY)
+ .hasMessageContaining("BOGUS");
+ }
+
+ @Test
+ void lengthCheckExceptionFailsInsteadOfSkippingTest() {
+ Map<String, String> options = options(FIELD_DELIMITER_KEY, "|");
+ options.put(LENGTH_CHECK_KEY, "EXCEPTION");
+
+ assertThatThrownBy(() -> deserialize(options, "only-one-field"))
+ .isInstanceOf(RuntimeException.class);
+ }
+
+ private static Map<String, String> options(String key, String value) {
+ Map<String, String> options = ddlOptions();
+ options.put(key, value);
+ return options;
+ }
+
+ /** Mirrors what the table factory hands over: the DDL options plus the
physical schema. */
+ private static Map<String, String> ddlOptions() {
+ DescriptorProperties descriptorProperties = new DescriptorProperties();
+ descriptorProperties.putTableSchema("schema", tableSchema());
+ return new HashMap<>(descriptorProperties.asMap());
+ }
+
+ private static TableSchema tableSchema() {
+ return TableSchema.builder()
+ .field("f0", DataTypes.STRING())
+ .field("f1", DataTypes.STRING())
+ .field("f2", DataTypes.STRING())
+ .field("f3", DataTypes.STRING())
+ .field("f4", DataTypes.STRING())
+ .field("f5", DataTypes.STRING())
+ .build();
+ }
+
+ private static List<RowData> deserialize(Map<String, String> options,
String body) {
+ RowDeserializationSchema schema =
+ new RowDeserializationSchema.Builder()
+ .setTableSchema(tableSchema())
+ .setProperties(options)
+ .build();
+ schema.open(null);
+
+ BytesMessage message = new BytesMessage();
+ message.setData(body.getBytes(StandardCharsets.UTF_8));
+
+ List<RowData> rows = new ArrayList<>();
+ schema.deserialize(
+ Collections.singletonList(message),
+ new Collector<RowData>() {
+ @Override
+ public void collect(RowData record) {
+ rows.add(record);
+ }
+
+ @Override
+ public void close() {}
+ });
+ return rows;
+ }
+}
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactoryTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactoryTest.java
index 5f4eff7..ddb18bf 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactoryTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQDynamicTableSourceFactoryTest.java
@@ -28,6 +28,7 @@ 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.source.DynamicTableSource;
+import org.apache.flink.table.factories.DynamicTableFactory;
import org.apache.flink.table.factories.FactoryUtil;
import org.junit.Test;
@@ -39,7 +40,9 @@ import java.util.Map;
import static org.apache.flink.table.api.DataTypes.STRING;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
/** Tests for {@link RocketMQDynamicTableSourceFactory}. */
public class RocketMQDynamicTableSourceFactoryTest {
@@ -55,6 +58,68 @@ public class RocketMQDynamicTableSourceFactoryTest {
private static final String CONSUMER_GROUP = "test_consumer";
private static final String NAME_SERVER_ADDRESS = "127.0.0.1:9876";
+ /**
+ * A missing required option must be reported by key only. Note that this
drives the factory
+ * directly: going through {@link FactoryUtil#createTableSource} lets
Flink run its own required
+ * option check first, and that message repeats every table option,
including the access key in
+ * plaintext.
+ */
+ @Test
+ public void testMissingRequiredOptionDoesNotEchoOptionValues() {
+ final Map<String, String> options = new HashMap<>();
+ options.put("connector", IDENTIFIER);
+ options.put(RocketMQSourceOptions.TOPIC.key(), TOPIC);
+ options.put(RocketMQSourceOptions.CONSUMER_GROUP.key(),
CONSUMER_GROUP);
+ options.put(RocketMQSourceOptions.OPTIONAL_ACCESS_KEY.key(),
"ak-value-must-not-leak");
+ options.put(RocketMQSourceOptions.OPTIONAL_SECRET_KEY.key(),
"sk-value-must-not-leak");
+
+ final ResolvedCatalogTable table =
+ new ResolvedCatalogTable(
+ CatalogTable.of(
+
Schema.newBuilder().fromResolvedSchema(SCHEMA).build(),
+ "mock source",
+ Collections.emptyList(),
+ options),
+ SCHEMA);
+ final DynamicTableFactory.Context context =
+ new DynamicTableFactory.Context() {
+ @Override
+ public ObjectIdentifier getObjectIdentifier() {
+ return ObjectIdentifier.of("default", "default",
IDENTIFIER);
+ }
+
+ @Override
+ public ResolvedCatalogTable getCatalogTable() {
+ return table;
+ }
+
+ @Override
+ public Configuration getConfiguration() {
+ return new Configuration();
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return
RocketMQDynamicTableSourceFactoryTest.class.getClassLoader();
+ }
+
+ @Override
+ public boolean isTemporary() {
+ return false;
+ }
+ };
+
+ try {
+ new
RocketMQDynamicTableSourceFactory().createDynamicTableSource(context);
+ fail("expected the missing endpoints option to be reported");
+ } catch (ValidationException e) {
+ final String message = e.getMessage();
+ assertTrue(message,
message.contains(RocketMQSourceOptions.ENDPOINTS.key()));
+ assertFalse(message, message.contains("ak-value-must-not-leak"));
+ assertFalse(message, message.contains("sk-value-must-not-leak"));
+ }
+ }
+
@Test
public void testRocketMQDynamicTableSourceWithLegalOption() {
final Map<String, String> options = new HashMap<>();
diff --git
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQParameterChainTest.java
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQParameterChainTest.java
index f6759c5..28b4c27 100644
---
a/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQParameterChainTest.java
+++
b/flink-connector-rocketmq/src/test/java/org/apache/flink/streaming/connectors/rocketmq/table/RocketMQParameterChainTest.java
@@ -58,12 +58,6 @@ class RocketMQParameterChainTest {
RocketMQSinkOptions.TRANSACTION_TIMEOUT);
}
- @Test
- void sinkFactoryShouldNotDeclareSerializeFormat() {
- RocketMQDynamicTableSinkFactory factory = new
RocketMQDynamicTableSinkFactory();
-
assertThat(factory.optionalOptions()).doesNotContain(RocketMQSinkOptions.SERIALIZE_FORMAT);
- }
-
@Test
void sourceConfigurationShouldPreserveUserValues() {
Configuration config = new Configuration();