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 04f20de [ISSUE #137] feat: add a simple consumer mode for
normal-topic consumption (#138)
04f20de is described below
commit 04f20debcd1549911fb4b507cbfadd74157f52c0
Author: lizhimins <[email protected]>
AuthorDate: Tue Sep 22 16:02:33 2026 +0800
[ISSUE #137] feat: add a simple consumer mode for normal-topic consumption
(#138)
* [ISSUE #137] feat: add a simple consumer mode for normal-topic consumption
Add a SIMPLE consumption mode to the gRPC connector so a normal topic can be
consumed through the SDK SimpleConsumer, next to the existing LITE mode that
binds a main lite topic through the LiteSimpleConsumer.
- rocketmq.source.mode (SQL: source.mode) selects the mode and defaults to
SIMPLE; LITE must now be requested explicitly.
- SIMPLE subscribes one topic with an optional tag / SQL92 filter
expression and
acknowledges messages itself: the receipt handles of emitted records are
grouped per checkpoint by CheckpointAckTracker and acked from
notifyCheckpointComplete through the very consumer that received them, so
no
second gRPC connection is needed. Ack failures are tolerated and logged,
so a
single stale handle can never fail the job; un-acked messages are
redelivered
once their invisible duration expires, which is the at-least-once
guarantee.
- PopConsumer abstracts the two SDK Pop clients, which expose the same
receive / ack / changeInvisibleDuration operations but share no common
interface, keeping the split reader mode-agnostic.
- RocketMQLiteAckClient is renamed to RocketMQAckClient and now serves only
the
LITE downstream-ack path, which genuinely needs credentials and pooling.
- The SQL factory exposes source.mode, source.filter-expression and
source.filter-type; docs/grpc-connector.md documents both modes, the
checkpoint-ack semantics and how to size the invisible duration.
Verified end to end against an open-source cluster and an Alibaba Cloud
commercial instance: 50 messages received, acked after checkpoint
completion and
zero redelivery once the invisible duration expired.
BREAKING CHANGE: the source now consumes a normal topic with a
checkpoint-aligned SimpleConsumer by default. Jobs relying on the main lite
topic with downstream acknowledgement must select LITE explicitly via
setMode(ConsumerMode.LITE) or 'source.mode' = 'lite'.
* [ISSUE #137] chore: apply spotless formatting
The changes of this PR were written on a JDK 21 toolchain, where the parent
pom's
java17 profile disables spotless, so google-java-format never validated
them and
a few statements and javadoc paragraphs kept a manual line break that the
formatter collapses. Run spotless:apply to restore the canonical
formatting; the
diff is line breaks only, no behaviour changes.
* [ISSUE #137] fix: drop the empty protected_branches key from .asf.yaml
ASF Infra rejects the file with "when expecting a mapping, found a blank
string": `protected_branches:` has been present without a value since the
file
was created, and YAML parses it as null. Because the parse fails, the whole
`github` section is discarded, so the description, homepage, labels and the
issues / wiki feature flags never took effect.
Remove the empty key so the section parses again. Filling it in instead
would
change the actual branch-protection rules of the repository, which is a
separate
decision.
* [ISSUE #137] chore: set the connector version to 3.0.0
Bump the reactor version from 1.0.0-SNAPSHOT to 3.0.0 via versions:set, so
the
parent and all four modules stay in sync. The external
flink-connector-parent
version is untouched.
---
.asf.yaml | 3 +-
docs/grpc-connector.md | 173 +++++++++++++++++++--
flink-connector-rocketmq-grpc/pom.xml | 2 +-
...MQLiteAckClient.java => RocketMQAckClient.java} | 32 ++--
.../grpc/ack/RocketMQAckProcessFunction.java | 16 +-
.../rocketmq/grpc/source/ConsumerMode.java | 41 +++++
.../rocketmq/grpc/source/RocketMQGrpcSource.java | 25 ++-
.../grpc/source/RocketMQGrpcSourceBuilder.java | 51 +++++-
.../grpc/source/RocketMQGrpcSourceOptions.java | 59 +++++--
.../grpc/source/reader/CheckpointAckTracker.java | 68 ++++++++
.../source/reader/LiteSimpleConsumerProvider.java | 58 -------
.../rocketmq/grpc/source/reader/PopConsumer.java | 48 ++++++
.../grpc/source/reader/PopConsumerProvider.java | 164 +++++++++++++++++++
.../source/reader/RocketMQGrpcSourceReader.java | 81 +++++++++-
.../reader/RocketMQGrpcSourceRecordEmitter.java | 16 ++
.../reader/RocketMQGrpcSourceSplitReader.java | 82 ++++++++--
.../grpc/table/RocketMQGrpcConnectorOptions.java | 41 ++++-
.../table/RocketMQGrpcDynamicTableFactory.java | 30 ++++
.../grpc/table/RocketMQGrpcDynamicTableSource.java | 15 +-
.../rocketmq/grpc/RocketMQGrpcITCase.java | 137 +++++++++++++++-
.../rocketmq/grpc/RocketMQGrpcLiteE2EVerify.java | 3 +
...kClientTest.java => RocketMQAckClientTest.java} | 40 ++---
.../grpc/source/RocketMQGrpcSourceBuilderTest.java | 63 ++++++++
.../source/reader/CheckpointAckTrackerTest.java | 94 +++++++++++
.../source/reader/PopConsumerProviderTest.java | 100 ++++++++++++
.../reader/RocketMQGrpcSourceSplitReaderTest.java | 127 +++++++++++++++
.../table/RocketMQGrpcDynamicTableFactoryTest.java | 32 ++++
flink-connector-rocketmq/pom.xml | 2 +-
flink-sql-connector-rocketmq-grpc/pom.xml | 2 +-
flink-sql-connector-rocketmq/pom.xml | 2 +-
pom.xml | 2 +-
31 files changed, 1430 insertions(+), 179 deletions(-)
diff --git a/.asf.yaml b/.asf.yaml
index a2d2d75..f2c834d 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -10,8 +10,7 @@ github:
issues: true
# Enable wiki
wiki: true
- protected_branches:
-
+
notifications:
commits: [email protected]
issues: [email protected]
diff --git a/docs/grpc-connector.md b/docs/grpc-connector.md
index 017b176..14ba709 100644
--- a/docs/grpc-connector.md
+++ b/docs/grpc-connector.md
@@ -1,8 +1,11 @@
-# gRPC Connector (LiteSimpleConsumer)
+# gRPC Connector
-The gRPC connector (`flink-connector-rocketmq-grpc`) targets RocketMQ 5.x
**lite topics** using
-`rocketmq-client-java` 5.2.1+. The source binds one main lite topic with a
wildcard subscription,
-never acks by itself, and hands the ack / throttle decision to downstream
operators.
+The gRPC connector (`flink-connector-rocketmq-grpc`) targets RocketMQ 5.x using
+`rocketmq-client-java` 5.2.1+ and offers two consumption modes. In **SIMPLE**
mode — the default —
+the source subscribes to a normal topic (with an optional tag / SQL92 filter)
and acknowledges the
+messages itself once the enclosing checkpoint completes. In **LITE** mode,
which must be selected
+explicitly, the source binds one main lite topic with a wildcard subscription,
never acks by itself,
+and hands the ack / throttle decision to downstream operators. Section 2
details both modes.
This document covers both the design (architecture, semantics, decisions and
trade-offs) and the
usage of the connector.
@@ -43,8 +46,21 @@ This leads to the two defining properties of the connector:
## 2. Consumption model
+**Two modes.** A single `rocketmq.source.mode` option selects how the source
subscribes and who
+acknowledges:
+
+- **SIMPLE** (default) — the source subscribes to one **normal** topic via a
`SimpleConsumer` with
+ an optional tag / SQL92 filter expression. Here the source itself
acknowledges messages: it
+ batches the receipt handles and acks them through that very consumer once
the enclosing
+ checkpoint completes (at-least-once).
+- **LITE** (opt-in, requires `rocketmq.source.mode` = `LITE`) — the source
binds one main lite topic
+ via a `LiteSimpleConsumer` with a wildcard (generalized) subscription, so
the broker funnels the
+ messages of all sub topics through one receive stream. The source never
acks: every emitted record
+ carries a self-contained receipt handle, and the downstream operator takes
the ack / throttle
+ decision — which is what enables fair per-sub-topic throttling.
+
RocketMQ 5.x offers a push client (`PushConsumer`) and pull/Pop clients
-(`SimpleConsumer` / `LiteSimpleConsumer`). The connector uses
**`LiteSimpleConsumer`**
+(`SimpleConsumer` / `LiteSimpleConsumer`). Both connector modes build on the
Pop clients
(available since `rocketmq-client-java` 5.2.1), whose consumption is split
into three explicit
phases that map cleanly onto a Flink pipeline:
@@ -82,9 +98,12 @@ start. Per subtask:
```
subtask
└─ SourceReader (mailbox thread)
+ │ SIMPLE mode only: on notifyCheckpointComplete, ack the handles of that
+ │ checkpoint through the SplitReader's consumer (see below)
└─ SingleThreadFetcherManager → 1 fetcher thread
└─ SplitReader
- ├─ 1 shared LiteSimpleConsumer (thread-safe)
+ ├─ 1 shared Pop consumer (thread-safe)
+ │ SimpleConsumer in SIMPLE mode, LiteSimpleConsumer in LITE
mode
├─ N ReceiveWorker threads (N =
rocketmq.source.fetch-concurrency)
│ loop: consumer.receive(maxMessageNum, invisibleDuration)
│ → put into a bounded in-memory queue (capacity =
maxMessageNum × N)
@@ -96,6 +115,11 @@ subtask
flight — raising a single subtask's throughput without extra clientIds,
heartbeats, connections
or route caches. Total throughput scales with
`parallelism × fetch-concurrency × maxMessageNum / receive-latency`.
+- **SIMPLE-mode acks go through that same consumer.** The reader hands its
completed receipt
+ handles to `RocketMQGrpcSourceSplitReader#ack`, which issues the ack RPC on
the consumer that
+ received the messages. No second client, connection, clientId or credential
resolution is
+ involved, and no LITE-only client is used for normal topics. Pop consumers
are thread-safe, so
+ the mailbox thread may ack while the receive workers block in `receive()` on
the same instance.
- **Backpressure = stop receiving.** When the bounded queue is full, workers
block on `put()`;
the pull model has no explicit pause API and does not need one.
- **Receive failures back off exponentially** (100 ms doubling up to 30 s)
instead of
@@ -124,11 +148,14 @@ it on the source side and rebuilding a minimal message
view on the ack side requ
types. Confining that (experimental) coupling to one `@Internal` adapter keeps
the rest of the
connector SDK-clean.
-### 3.3 Downstream acknowledgement
+### 3.3 Downstream acknowledgement (LITE mode)
-Downstream operators obtain a shared, credential-free ack client:
+This section applies to **LITE mode only**: it is the path where the ack
decision is taken outside
+the source, so the ack RPC cannot reuse the receiving consumer. (SIMPLE mode
acks source-side
+through the consumer that received the messages — see §3.1.) Downstream
operators obtain a shared,
+credential-free ack client:
-- **Pooling.** `RocketMQLiteAckClient` is shared per TaskManager JVM via
reference-counted
+- **Pooling.** `RocketMQAckClient` is shared per TaskManager JVM via
reference-counted
`acquire`/`release` keyed by the client configuration. Internally it lazily
keeps one
same-group `LiteSimpleConsumer` per routing triple `(endpoint, namespace,
consumerGroup)`
carried by the incoming handles. The consumer built for a handle uses the
*handle's* endpoints
@@ -191,6 +218,10 @@ Alternatives that were evaluated and rejected:
| Carrying credentials in the record stream | Security red line. Credentials
stay in operator configuration, resolved locally per TaskManager. |
| Serializing the SDK protobuf receipt blob | Unnecessary — the server-side
handle string self-encodes routing; a handful of strings suffice. |
+(The rejection of source-side checkpoint ack applies to the LITE requirement,
where the ack decision
+is only known downstream. SIMPLE mode has no such decision and intentionally
adopts that very
+approach — see §5.)
+
Other decisions:
- **Commit point is user-driven.** The connector does not align acks with
checkpoints; the user
@@ -202,18 +233,66 @@ Other decisions:
## 5. Delivery semantics
-- **At-least-once.** The source never acks; un-acked messages are redelivered
after
- `invisible-duration` expires. Downstream must be idempotent.
+Both modes provide **at-least-once**: un-acked messages are redelivered by the
broker once their
+`invisible-duration` expires, so downstream processing must be idempotent.
What differs is *who*
+acks and *when*.
+
+### LITE mode
+
+- The source never acks; the ack decision is deferred to downstream operators,
which gives
+ at-least-once with user-controlled granularity.
- `invisible-duration` must cover the full downstream processing time of a
message (including
shuffles and slow operators such as model inference); use the renewal policy
when queueing
time is unpredictable.
- Exactly-once is out of scope for the Pop model: there is no offset the
connector could
checkpoint-align, and acks are user-driven by design.
+### SIMPLE mode
+
+- Once a message is emitted, its receipt handle is registered with a
checkpoint-aligned tracker;
+ the handle is snapshotted together with the enclosing checkpoint and the
source batch-acks all
+ handles of a checkpoint from `notifyCheckpointComplete` after that
checkpoint completes.
+- The acks are issued **through the consumer that received the messages** (the
split reader's
+ `SimpleConsumer`), not through the pooled `RocketMQAckClient` of §3.3. The
reader therefore opens
+ no second gRPC connection, resolves no credentials of its own, and never
routes normal-topic acks
+ through a LITE client.
+- On a failure before completion the pending handles are simply dropped; the
un-acked messages are
+ redelivered by the broker after their `invisible-duration` expires. That
redelivery *is* the
+ at-least-once guarantee, which is why the tracker holds no
failure-survivable state.
+- **Checkpointing is required.** Without it `notifyCheckpointComplete` never
fires, nothing is ever
+ acked, and every message is redelivered forever.
+- **Size `invisible-duration` with headroom — do not compute it as a tight
bound.** The invisible
+ window is consumed by more than one checkpoint cycle:
+
+ | Consumer of the window | Why it counts |
+ | --- | --- |
+ | Queueing before emission | The window starts when the broker serves
`receive()`, not when the record is emitted. Time spent in the reader's
internal queue (`max-message-num` × `fetch-concurrency` slots) and in
deserialization is already gone. |
+ | Waiting for the next barrier | A record emitted just after a barrier waits
almost a full checkpoint interval for the next one. |
+ | Completing that checkpoint | Barrier alignment plus state materialisation,
bounded by the checkpoint timeout. |
+ | **A failed or timed-out checkpoint** | The tracker only clears handles for
checkpoints that actually complete, so handles of a failed checkpoint wait for
the next successful one — roughly **another full interval**, more if failures
repeat. |
+ | Draining the ack batch | `notifyCheckpointComplete` acks the batch one
handle per RPC, so the last handles of a large batch wait for the earlier ones.
|
+
+ A practical starting point is therefore
+
+ ```
+ invisible-duration ≥ 2 × checkpoint interval + checkpoint timeout + headroom
+ ```
+
+ Configuring it as merely `interval + timeout` still satisfies at-least-once,
but a single failed
+ checkpoint is then enough to make the batch visible again and reprocessed.
+- Emitted-but-not-yet-acked handles are **not renewed**. The SDK's
`changeInvisibleDuration` does
+ not return the new receipt handle, so the source cannot refresh a handle it
has already handed to
+ the tracker; sizing `invisible-duration` as above is the substitute. A
renewal policy (§7) only
+ covers the *first* row of the table above — messages still queued inside the
reader — because the
+ reader freezes a handle the moment the record is emitted.
+
---
## 6. Prerequisites
+The default SIMPLE mode only needs an ordinary RocketMQ 5.x topic plus a
consumer group. The
+following applies to the opt-in LITE mode:
+
- RocketMQ 5.x cluster with lite topic support enabled on the broker
(`enableMultiDispatch`, `enableLmq`, etc. — preset by the official Helm
chart).
- A LITE-type main topic and a consumer group with the wildcard attribute:
@@ -229,12 +308,16 @@ wildcard subscription receives nothing.
## 7. Source
+The source runs in **SIMPLE** mode unless another mode is selected, i.e. it
subscribes to a normal
+topic and acks the messages itself once the enclosing checkpoint completes
(checkpointing must be
+enabled):
+
```java
RocketMQGrpcSource<String> source = RocketMQGrpcSource.<String>builder()
.setEndpoints("127.0.0.1:8081")
- .setConsumerGroup("GID-lite")
- .setMainTopic("LiteMainTopic")
- .setFetchConcurrency(4)
+ .setConsumerGroup("GID-example")
+ .setTopic("normal-topic")
+ .setFilterExpression("tagA||tagB")
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
@@ -244,10 +327,31 @@ DataStream<AckableMessage<String>> stream =
The output type is `AckableMessage<OUT>`: the payload plus a serializable
`RocketMQReceiptHandle` (endpoint, namespace, consumerGroup, lite topic,
receipt handle). The
-handle can cross keyBy/shuffle boundaries; credentials never travel with it.
+handle can cross keyBy/shuffle boundaries; credentials never travel with it.
In SIMPLE mode the
+handle is acknowledged automatically by the source once the enclosing
checkpoint completes, so
+downstream neither needs nor should invoke the ack / throttle operators.
+
+### LITE mode
+
+LITE mode is opt-in: select it explicitly and bind a main lite topic instead
of a normal topic. The
+source then never acks by itself and the downstream operators take the ack /
throttle decision.
+
+```java
+RocketMQGrpcSource<String> source = RocketMQGrpcSource.<String>builder()
+ .setEndpoints("127.0.0.1:8081")
+ .setConsumerGroup("GID-lite")
+ .setMode(ConsumerMode.LITE)
+ .setMainTopic("LiteMainTopic")
+ .setFetchConcurrency(4)
+ .setValueOnlyDeserializer(new SimpleStringSchema())
+ .build();
+```
### Downstream ack / throttling
+The ack / throttle wirings below apply to LITE mode only; in SIMPLE mode the
source acks the
+messages itself.
+
Unacked messages are redelivered by the broker after `invisible-duration`
expires
(at-least-once — downstream must be idempotent). Two wirings are provided:
@@ -273,6 +377,12 @@
builder.setRenewalPolicyClass("com.example.MyRenewalPolicy")
.setRenewalAheadTime(Duration.ofSeconds(5));
```
+Renewal only covers records that are **still queued inside the reader**: the
reader freezes a
+handle the moment the record is emitted, because renewing would invalidate the
handle that has
+already travelled downstream. Consequently it does not extend the window for a
record that is
+waiting for its downstream ack (LITE) or for its checkpoint to complete
(SIMPLE) — in SIMPLE mode a
+renewal policy is therefore no substitute for sizing `invisible-duration` as
described in §5.
+
## 8. Sink
```java
@@ -301,11 +411,15 @@ RocketMQGrpcSink<String> sink =
RocketMQGrpcSink.<String>builder()
| Key | Type | Default | Description |
| --- | --- | --- | --- |
-| `rocketmq.source.main-topic` | String | (none) | Main lite topic to bind,
required |
+| `rocketmq.source.mode` | ConsumerMode | `SIMPLE` | Consumption mode:
`SIMPLE` (default; normal topic, checkpoint-aligned source ack) or `LITE` (main
lite topic, downstream ack) |
+| `rocketmq.source.main-topic` | String | (none) | Main lite topic to bind,
required in LITE mode |
+| `rocketmq.source.topic` | String | (none) | Normal topic to subscribe,
required in SIMPLE mode |
+| `rocketmq.source.filter-expression` | String | (none) | SIMPLE-mode tag /
SQL92 filter expression (absent = receive all messages) |
+| `rocketmq.source.filter-type` | String | `TAG` | Type of
`filter-expression`: `TAG` or `SQL92` |
| `rocketmq.source.consumer-group` | String | (none) | Consumer group,
required |
| `rocketmq.source.fetch-concurrency` | Integer | 1 | Concurrent long-polling
receive workers |
| `rocketmq.source.await-duration` | Duration | 20s | Long-polling await time |
-| `rocketmq.source.invisible-duration` | Duration | 60s | Invisible time per
receive (min 10s) |
+| `rocketmq.source.invisible-duration` | Duration | 60s | Invisible time per
receive (min 10s); size it with headroom — see §5 |
| `rocketmq.source.max-message-num` | Integer | 32 | Max messages per receive |
| `rocketmq.source.renewal-policy-class` | String | (none) |
`InvisibleDurationRenewalPolicy` implementation |
| `rocketmq.source.renewal-ahead-time` | Duration | 5s | Renew this long
before invisible expiry |
@@ -325,9 +439,34 @@ The SQL connector identifier is `rocketmq-grpc` (fat-jar
module
`rocketmq.source.fetch-concurrency`. The SQL/Table path consumes message
values only; the
downstream ack / throttling APIs are DataStream-only.
+The SQL source runs in `simple` mode by default (`source.mode` defaults to
`simple`), i.e. the
+configured `topic` is consumed as a normal topic and the source acks on
checkpoint completion, so
+checkpointing must be enabled:
+
+```sql
+CREATE TABLE orders (
+ f0 STRING
+) WITH (
+ 'connector' = 'rocketmq-grpc',
+ 'endpoints' = '127.0.0.1:8081',
+ 'topic' = 'normal-topic',
+ 'source.consumer-group' = 'GID-example',
+ 'source.filter-expression' = 'tagA || tagB',
+ 'format' = 'json'
+);
+```
+
+To consume a main lite topic instead, opt into `lite` mode explicitly with
+`'source.mode' = 'lite'`; the `topic` option is then bound as the main lite
topic and the SQL path
+performs no acknowledgement (the ack / throttling APIs are DataStream-only).
+
## 11. Known limitations
- `LiteSimpleConsumer` is experimental in the SDK (5.2.1+) and offers
synchronous APIs only.
+- SIMPLE mode requires checkpointing; without checkpoints messages are never
acknowledged and are
+ redelivered indefinitely.
+- SIMPLE mode has no way to renew a handle it has already emitted, so an
`invisible-duration` sized
+ as a tight bound turns any failed checkpoint into reprocessed messages (see
§5).
- One main lite topic per source; no startup-offset control in
wildcard-subscription mode
(`OffsetOption` is limited to exact `subscribeLite`).
- Throttling defers messages per message (extra RPC per defer, reordering,
`deliveryAttempt`
diff --git a/flink-connector-rocketmq-grpc/pom.xml
b/flink-connector-rocketmq-grpc/pom.xml
index 512ddce..d035375 100644
--- a/flink-connector-rocketmq-grpc/pom.xml
+++ b/flink-connector-rocketmq-grpc/pom.xml
@@ -26,7 +26,7 @@ under the License.
<parent>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-rocketmq-parent</artifactId>
- <version>1.0.0-SNAPSHOT</version>
+ <version>3.0.0</version>
</parent>
<artifactId>flink-connector-rocketmq-grpc</artifactId>
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQLiteAckClient.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckClient.java
similarity index 92%
rename from
flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQLiteAckClient.java
rename to
flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckClient.java
index f6754c4..0ffe20c 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQLiteAckClient.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckClient.java
@@ -47,10 +47,12 @@ import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
- * A credential-free acknowledgement client that a downstream operator uses to
acknowledge or
- * re-schedule RocketMQ Pop messages described by a {@link
RocketMQReceiptHandle}. The credentials
- * used to talk to the RocketMQ proxy are configured on the operator that owns
the client and are
- * <b>never</b> carried in the data stream.
+ * A credential-free acknowledgement client that acknowledges RocketMQ Pop
messages of both lite and
+ * normal topics. A downstream operator uses it to acknowledge or re-schedule
the messages described
+ * by a {@link RocketMQReceiptHandle}, which is what the LITE mode of the
source relies on: the ack
+ * decision is only known after downstream processing, possibly on the other
side of a shuffle. The
+ * credentials used to talk to the RocketMQ proxy are configured on the
operator that owns the
+ * client and are <b>never</b> carried in the data stream.
*
* <p>The client keeps a lazily populated pool of same-group {@link
LiteSimpleConsumer}s, one per
* {@link ConsumerKey routing triple} carried by the incoming handles, and
issues the {@code ack} /
@@ -59,16 +61,18 @@ import java.util.concurrent.ConcurrentHashMap;
* come from the handle (so the ack RPC is routed to the right proxy and
resource) and whose
* credentials/TLS/timeout come from the operator {@link Configuration}. This
is required because
* the SDK stamps the ack request with the <em>consumer's</em> namespace, so a
pooled consumer must
- * share the handle's namespace.
+ * share the handle's namespace. The pooled consumers are built through {@code
LiteSimpleConsumer},
+ * but the ack RPC itself is protocol-level identical for lite and normal
topic handles, so handles
+ * of normal topics are acknowledged without any semantic difference.
*
* <p>Instances are shared per TaskManager JVM: {@link
#acquire(Configuration)} reference-counts a
* client per distinct client configuration and {@link
#release(Configuration)} closes it once no
* operator instance references it anymore, so callers must not close a client
directly.
*/
@Internal
-public final class RocketMQLiteAckClient {
+public final class RocketMQAckClient {
- private static final Logger LOG =
LoggerFactory.getLogger(RocketMQLiteAckClient.class);
+ private static final Logger LOG =
LoggerFactory.getLogger(RocketMQAckClient.class);
/**
* The await duration is only consulted by {@code receive}, which this
ack-only client never
@@ -90,7 +94,7 @@ public final class RocketMQLiteAckClient {
private volatile boolean closed = false;
- private RocketMQLiteAckClient(Configuration configuration) {
+ private RocketMQAckClient(Configuration configuration) {
this.configuration =
Objects.requireNonNull(configuration, "configuration should
not be null");
this.credentialsResolver =
CredentialsResolvers.createFromConfiguration(configuration);
@@ -103,12 +107,12 @@ public final class RocketMQLiteAckClient {
* @param configuration the operator configuration carrying the RocketMQ
client options.
* @return the shared ack client; callers must not close it directly.
*/
- public static synchronized RocketMQLiteAckClient acquire(Configuration
configuration) {
+ public static synchronized RocketMQAckClient acquire(Configuration
configuration) {
Objects.requireNonNull(configuration, "configuration should not be
null");
final String key = keyOf(configuration);
RefCounted ref = CLIENTS.get(key);
if (ref == null) {
- ref = new RefCounted(new RocketMQLiteAckClient(configuration));
+ ref = new RefCounted(new RocketMQAckClient(configuration));
CLIENTS.put(key, ref);
}
ref.count++;
@@ -128,8 +132,8 @@ public final class RocketMQLiteAckClient {
public static void release(Configuration configuration) {
Objects.requireNonNull(configuration, "configuration should not be
null");
final String key = keyOf(configuration);
- RocketMQLiteAckClient toClose = null;
- synchronized (RocketMQLiteAckClient.class) {
+ RocketMQAckClient toClose = null;
+ synchronized (RocketMQAckClient.class) {
final RefCounted ref = CLIENTS.get(key);
if (ref == null) {
return;
@@ -337,10 +341,10 @@ public final class RocketMQLiteAckClient {
}
private static final class RefCounted {
- private final RocketMQLiteAckClient client;
+ private final RocketMQAckClient client;
private int count;
- private RefCounted(RocketMQLiteAckClient client) {
+ private RefCounted(RocketMQAckClient client) {
this.client = client;
}
}
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckProcessFunction.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckProcessFunction.java
index 29330af..fbe215b 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckProcessFunction.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckProcessFunction.java
@@ -29,13 +29,13 @@ import java.util.Objects;
/**
* An abstract {@link ProcessFunction} that gives subclasses a ready-to-use
{@link
- * RocketMQLiteAckClient} so they can acknowledge or re-schedule RocketMQ Pop
messages directly from
+ * RocketMQAckClient} so they can acknowledge or re-schedule RocketMQ Pop
messages directly from
* their own business logic via {@link #ack} and {@link
#changeInvisibleDuration}.
*
- * <p>The client is shared per TaskManager through {@link
RocketMQLiteAckClient#acquire}: it is
- * acquired in {@link #open(OpenContext)} and released in {@link #close()}.
The RocketMQ client
- * options (endpoints, namespace, credentials, TLS, timeout) are provided
through the {@link
- * Configuration} passed to the constructor and never travel in the data
stream.
+ * <p>The client is shared per TaskManager through {@link
RocketMQAckClient#acquire}: it is acquired
+ * in {@link #open(OpenContext)} and released in {@link #close()}. The
RocketMQ client options
+ * (endpoints, namespace, credentials, TLS, timeout) are provided through the
{@link Configuration}
+ * passed to the constructor and never travel in the data stream.
*
* @param <IN> the input record type, typically {@code AckableMessage<T>}.
* @param <OUT> the output record type.
@@ -47,7 +47,7 @@ public abstract class RocketMQAckProcessFunction<IN, OUT>
extends ProcessFunctio
private final Configuration configuration;
- private transient RocketMQLiteAckClient ackClient;
+ private transient RocketMQAckClient ackClient;
private transient boolean acquired;
private transient Counter numAcksSucceeded;
private transient Counter numAcksFailed;
@@ -63,7 +63,7 @@ public abstract class RocketMQAckProcessFunction<IN, OUT>
extends ProcessFunctio
@Override
public void open(OpenContext openContext) throws Exception {
super.open(openContext);
- this.ackClient = RocketMQLiteAckClient.acquire(configuration);
+ this.ackClient = RocketMQAckClient.acquire(configuration);
this.acquired = true;
this.numAcksSucceeded =
getRuntimeContext().getMetricGroup().counter("numAcksSucceeded");
this.numAcksFailed =
getRuntimeContext().getMetricGroup().counter("numAcksFailed");
@@ -80,7 +80,7 @@ public abstract class RocketMQAckProcessFunction<IN, OUT>
extends ProcessFunctio
// Only release when open() actually acquired the client; Flink calls
close() even after a
// failed open(), and a stray release would decrement another
operator's reference.
if (acquired) {
- RocketMQLiteAckClient.release(configuration);
+ RocketMQAckClient.release(configuration);
acquired = false;
}
this.ackClient = null;
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/ConsumerMode.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/ConsumerMode.java
new file mode 100644
index 0000000..63dcc5a
--- /dev/null
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/ConsumerMode.java
@@ -0,0 +1,41 @@
+/*
+ * 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.grpc.source;
+
+import org.apache.flink.annotation.PublicEvolving;
+
+/** The consumption mode of the RocketMQ gRPC source. */
+@PublicEvolving
+public enum ConsumerMode {
+
+ /**
+ * Bind a main lite topic via a {@code LiteSimpleConsumer}; the broker
delivers messages of all
+ * sub topics over one receive stream. Messages are acknowledged by a
downstream operator using
+ * the receipt handle carried by each emitted record.
+ */
+ LITE,
+
+ /**
+ * Subscribe to a normal topic via a {@code SimpleConsumer} with an
optional filter expression.
+ * Messages are acknowledged by the source itself once the enclosing
checkpoint completes
+ * (at-least-once); checkpointing must be enabled and the invisible
duration must exceed the
+ * checkpoint interval plus timeout.
+ */
+ SIMPLE
+}
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSource.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSource.java
index 1f21b86..a3e48d2 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSource.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSource.java
@@ -39,6 +39,7 @@ import
org.apache.flink.connector.rocketmq.grpc.source.deserialization.RocketMQG
import
org.apache.flink.connector.rocketmq.grpc.source.enumerator.RocketMQGrpcSourceEnumState;
import
org.apache.flink.connector.rocketmq.grpc.source.enumerator.RocketMQGrpcSourceEnumStateSerializer;
import
org.apache.flink.connector.rocketmq.grpc.source.enumerator.RocketMQGrpcSourceEnumerator;
+import
org.apache.flink.connector.rocketmq.grpc.source.reader.CheckpointAckTracker;
import org.apache.flink.connector.rocketmq.grpc.source.reader.MessageViewImpl;
import
org.apache.flink.connector.rocketmq.grpc.source.reader.RocketMQGrpcSourceFetcherManager;
import
org.apache.flink.connector.rocketmq.grpc.source.reader.RocketMQGrpcSourceReader;
@@ -116,17 +117,27 @@ public class RocketMQGrpcSource<OUT>
final RocketMQGrpcSourceFetcherManager fetcherManager =
new RocketMQGrpcSourceFetcherManager(elementsQueue,
splitReaderSupplier);
+ final ConsumerMode mode =
configuration.get(RocketMQGrpcSourceOptions.MODE);
+ final CheckpointAckTracker ackTracker =
+ mode == ConsumerMode.SIMPLE ? new CheckpointAckTracker() :
null;
+
final RocketMQGrpcSourceRecordEmitter<OUT> recordEmitter =
new RocketMQGrpcSourceRecordEmitter<>(
deserializationSchema,
configuration.get(RocketMQGrpcOptions.NAMESPACE),
-
configuration.get(RocketMQGrpcSourceOptions.CONSUMER_GROUP));
-
- final RocketMQGrpcSourceReader<OUT> reader =
- new RocketMQGrpcSourceReader<>(
- elementsQueue, fetcherManager, recordEmitter,
configuration, readerContext);
-
- return reader;
+
configuration.get(RocketMQGrpcSourceOptions.CONSUMER_GROUP),
+ ackTracker == null ? null : ackTracker::add);
+
+ // The same split reader instance is handed to the fetcher manager and
to the source reader:
+ // in SIMPLE mode the reader acks through the very consumer that
received the messages.
+ return new RocketMQGrpcSourceReader<>(
+ elementsQueue,
+ fetcherManager,
+ recordEmitter,
+ configuration,
+ readerContext,
+ ackTracker,
+ splitReader);
}
@Override
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilder.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilder.java
index f38dcf8..ebc0861 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilder.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilder.java
@@ -37,6 +37,8 @@ public class RocketMQGrpcSourceBuilder<OUT> {
private final Configuration configuration;
private String mainTopic;
+ private ConsumerMode mode = ConsumerMode.SIMPLE;
+ private String topic;
private Boundedness boundedness;
private RocketMQGrpcDeserializationSchema<OUT> deserializationSchema;
@@ -55,7 +57,11 @@ public class RocketMQGrpcSourceBuilder<OUT> {
return setConfig(RocketMQGrpcSourceOptions.CONSUMER_GROUP,
consumerGroup);
}
- /** Set the main lite topic bound by the LiteSimpleConsumer. Every subtask
binds this topic. */
+ /**
+ * Set the main lite topic bound by the LiteSimpleConsumer. Every subtask
binds this topic.
+ * Required in {@link ConsumerMode#LITE}, which must be selected
explicitly via {@link
+ * #setMode(ConsumerMode)}; ignored in {@link ConsumerMode#SIMPLE}.
+ */
public RocketMQGrpcSourceBuilder<OUT> setMainTopic(String mainTopic) {
checkArgument(
mainTopic != null && !mainTopic.trim().isEmpty(),
@@ -64,6 +70,32 @@ public class RocketMQGrpcSourceBuilder<OUT> {
return this;
}
+ /** Set the consumption mode. Defaults to {@link ConsumerMode#SIMPLE}. */
+ public RocketMQGrpcSourceBuilder<OUT> setMode(ConsumerMode mode) {
+ this.mode = checkNotNull(mode);
+ return this;
+ }
+
+ /**
+ * Set the normal topic subscribed by the SimpleConsumer. Required in the
default {@link
+ * ConsumerMode#SIMPLE}; ignored in {@link ConsumerMode#LITE}.
+ */
+ public RocketMQGrpcSourceBuilder<OUT> setTopic(String topic) {
+ checkArgument(topic != null && !topic.trim().isEmpty(), "topic must
not be null or blank");
+ this.topic = topic;
+ return this;
+ }
+
+ /** Set the SIMPLE mode filter expression, e.g. {@code "tagA||tagB"} or a
SQL92 expression. */
+ public RocketMQGrpcSourceBuilder<OUT> setFilterExpression(String
filterExpression) {
+ return setConfig(RocketMQGrpcSourceOptions.FILTER_EXPRESSION,
filterExpression);
+ }
+
+ /** Set the SIMPLE mode filter expression type: {@code TAG} (default) or
{@code SQL92}. */
+ public RocketMQGrpcSourceBuilder<OUT> setFilterType(String filterType) {
+ return setConfig(RocketMQGrpcSourceOptions.FILTER_TYPE, filterType);
+ }
+
/**
* Set the fetch concurrency of each subtask, i.e. the number of
concurrent fetch requests it
* issues. The requests are served by worker threads sharing a single
thread-safe {@code
@@ -131,14 +163,23 @@ public class RocketMQGrpcSourceBuilder<OUT> {
checkNotNull(
configuration.get(RocketMQGrpcSourceOptions.CONSUMER_GROUP),
"consumer group must be configured");
- checkArgument(
- mainTopic != null && !mainTopic.trim().isEmpty(),
- "the main topic must be configured");
+ checkNotNull(mode, "the consumer mode must be configured");
+ if (mode == ConsumerMode.LITE) {
+ checkArgument(
+ mainTopic != null && !mainTopic.trim().isEmpty(),
+ "the main topic must be configured");
+ configuration.set(RocketMQGrpcSourceOptions.MAIN_TOPIC, mainTopic);
+ } else {
+ checkArgument(
+ topic != null && !topic.trim().isEmpty(),
+ "the topic must be configured in SIMPLE mode");
+ configuration.set(RocketMQGrpcSourceOptions.TOPIC, topic);
+ }
checkArgument(
configuration.get(RocketMQGrpcSourceOptions.FETCH_CONCURRENCY)
>= 1,
"fetch concurrency must be at least 1");
checkNotNull(deserializationSchema, "deserializer must be configured");
- configuration.set(RocketMQGrpcSourceOptions.MAIN_TOPIC, mainTopic);
+ configuration.set(RocketMQGrpcSourceOptions.MODE, mode);
return new RocketMQGrpcSource<>(configuration, boundedness,
deserializationSchema);
}
}
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceOptions.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceOptions.java
index 94b1e5a..f54d473 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceOptions.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceOptions.java
@@ -47,14 +47,52 @@ public class RocketMQGrpcSourceOptions {
.noDefaultValue()
.withDescription("The consumer group of the
SimpleConsumer.");
+ public static final ConfigOption<ConsumerMode> MODE =
+ ConfigOptions.key(CONSUMER_CONFIG_PREFIX + "mode")
+ .enumType(ConsumerMode.class)
+ .defaultValue(ConsumerMode.SIMPLE)
+ .withDescription(
+ "The consumption mode. SIMPLE (the default)
subscribes to a normal "
+ + "topic via a SimpleConsumer and the
source acknowledges "
+ + "messages when their checkpoint
completes; LITE binds a main "
+ + "lite topic via a LiteSimpleConsumer and
defers "
+ + "acknowledgement to a downstream
operator.");
+
+ public static final ConfigOption<String> TOPIC =
+ ConfigOptions.key(CONSUMER_CONFIG_PREFIX + "topic")
+ .stringType()
+ .noDefaultValue()
+ .withDescription(
+ "The normal topic subscribed by the
SimpleConsumer. Required in "
+ + "SIMPLE mode; ignored in LITE mode.");
+
+ public static final ConfigOption<String> FILTER_EXPRESSION =
+ ConfigOptions.key(CONSUMER_CONFIG_PREFIX + "filter-expression")
+ .stringType()
+ .noDefaultValue()
+ .withDescription(
+ "The filter expression applied to the SIMPLE mode
subscription, e.g. "
+ + "a tag expression 'tagA||tagB' or a
SQL92 expression. When "
+ + "absent, all messages are received. Only
effective in "
+ + "SIMPLE mode.");
+
+ public static final ConfigOption<String> FILTER_TYPE =
+ ConfigOptions.key(CONSUMER_CONFIG_PREFIX + "filter-type")
+ .stringType()
+ .defaultValue("TAG")
+ .withDescription(
+ "The type of 'filter-expression': TAG or SQL92.
Only effective in "
+ + "SIMPLE mode.");
+
public static final ConfigOption<String> MAIN_TOPIC =
ConfigOptions.key(CONSUMER_CONFIG_PREFIX + "main-topic")
.stringType()
.noDefaultValue()
.withDescription(
- "The main lite topic bound by the
LiteSimpleConsumer. Every subtask "
- + "binds this topic; the broker performs
message-level load "
- + "balancing across the consumer group.");
+ "The main lite topic bound by the
LiteSimpleConsumer. Required in "
+ + "LITE mode; ignored in SIMPLE mode.
Every subtask binds this "
+ + "topic; the broker performs
message-level load balancing "
+ + "across the consumer group.");
public static final ConfigOption<Integer> FETCH_CONCURRENCY =
ConfigOptions.key(CONSUMER_CONFIG_PREFIX + "fetch-concurrency")
@@ -79,12 +117,15 @@ public class RocketMQGrpcSourceOptions {
.durationType()
.defaultValue(Duration.ofSeconds(60))
.withDescription(
- "The invisible duration of a received message.
Because messages are "
- + "acknowledged by a downstream operator,
it must be larger "
- + "than the full downstream processing
time of a message so "
- + "that a message is not prematurely
redelivered while still "
- + "in flight; un-acked messages are
redelivered after this "
- + "duration, which provides the
at-least-once guarantee.");
+ "The invisible duration of a received message;
un-acked messages are "
+ + "redelivered after it expires, which
provides the "
+ + "at-least-once guarantee. In LITE mode
it must be larger than "
+ + "the full downstream processing time of
a message, because a "
+ + "downstream operator acknowledges it. In
SIMPLE mode the "
+ + "source acknowledges on checkpoint
completion, so size it "
+ + "with headroom above two checkpoint
intervals plus the "
+ + "checkpoint timeout: a failed checkpoint
defers the ack to "
+ + "the next successful one.");
public static final ConfigOption<Integer> MAX_MESSAGE_NUM =
ConfigOptions.key(CONSUMER_CONFIG_PREFIX + "max-message-num")
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/CheckpointAckTracker.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/CheckpointAckTracker.java
new file mode 100644
index 0000000..d699de0
--- /dev/null
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/CheckpointAckTracker.java
@@ -0,0 +1,68 @@
+/*
+ * 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.grpc.source.reader;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.connector.rocketmq.grpc.ack.RocketMQReceiptHandle;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeMap;
+
+/**
+ * Tracks the receipt handles of emitted-but-unacknowledged messages of the
SIMPLE mode, grouped by
+ * the checkpoint that observed them. Handles snapshotted with checkpoint N
are acknowledged once
+ * checkpoint N completes; a failure before completion simply leaves the
messages un-acked so the
+ * broker redelivers them after their invisible duration (at-least-once).
+ *
+ * <p>The tracker holds no state that must survive failures: losing pending
handles only causes
+ * redelivery, never loss. All methods are synchronized because the record
emitter (reader thread)
+ * and the checkpoint callbacks (mailbox thread) may touch it concurrently.
+ */
+@Internal
+public class CheckpointAckTracker {
+
+ private final List<RocketMQReceiptHandle> pending = new ArrayList<>();
+ private final TreeMap<Long, List<RocketMQReceiptHandle>> snapshotted = new
TreeMap<>();
+
+ /** Track the handle of an emitted message. */
+ public synchronized void add(RocketMQReceiptHandle handle) {
+ pending.add(handle);
+ }
+
+ /** Associate all handles emitted so far with the given checkpoint. */
+ public synchronized void snapshot(long checkpointId) {
+ if (!pending.isEmpty()) {
+ snapshotted.put(checkpointId, new ArrayList<>(pending));
+ pending.clear();
+ }
+ }
+
+ /**
+ * Return every handle whose checkpoint id is at most the completed one,
removing them from the
+ * tracker.
+ */
+ public synchronized List<RocketMQReceiptHandle> completeUpTo(long
checkpointId) {
+ final List<RocketMQReceiptHandle> completed = new ArrayList<>();
+ while (!snapshotted.isEmpty() && snapshotted.firstKey() <=
checkpointId) {
+ completed.addAll(snapshotted.pollFirstEntry().getValue());
+ }
+ return completed;
+ }
+}
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/LiteSimpleConsumerProvider.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/LiteSimpleConsumerProvider.java
deleted file mode 100644
index 82b0c8d..0000000
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/LiteSimpleConsumerProvider.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.grpc.source.reader;
-
-import org.apache.flink.annotation.Internal;
-import org.apache.flink.configuration.Configuration;
-import
org.apache.flink.connector.rocketmq.grpc.common.ClientConfigurationProvider;
-import
org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSourceOptions;
-
-import org.apache.rocketmq.client.apis.ClientException;
-import org.apache.rocketmq.client.apis.ClientServiceProvider;
-import org.apache.rocketmq.client.apis.consumer.LiteSimpleConsumer;
-
-/**
- * A factory that builds a gRPC {@link LiteSimpleConsumer} (Pop model) from a
Flink configuration.
- *
- * <p>The consumer binds the configured main topic and relies on its wildcard
(generalized)
- * subscription to receive messages from all of its sub topics; the consumer
group must carry the
- * {@code lite.sub.wildcard=true} attribute on the broker.
- */
-@Internal
-public class LiteSimpleConsumerProvider {
-
- private LiteSimpleConsumerProvider() {}
-
- /**
- * Build and start a {@link LiteSimpleConsumer} from the given Flink
configuration.
- *
- * @param configuration the Flink configuration.
- * @return a started {@link LiteSimpleConsumer}.
- */
- public static LiteSimpleConsumer create(Configuration configuration)
throws ClientException {
- final ClientServiceProvider provider =
ClientServiceProvider.loadService();
- return provider.newLiteSimpleConsumerBuilder()
- .setClientConfiguration(
-
ClientConfigurationProvider.getClientConfiguration(configuration))
-
.setConsumerGroup(configuration.get(RocketMQGrpcSourceOptions.CONSUMER_GROUP))
-
.setAwaitDuration(configuration.get(RocketMQGrpcSourceOptions.AWAIT_DURATION))
-
.bindTopic(configuration.get(RocketMQGrpcSourceOptions.MAIN_TOPIC))
- .build();
- }
-}
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumer.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumer.java
new file mode 100644
index 0000000..a770aa8
--- /dev/null
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumer.java
@@ -0,0 +1,48 @@
+/*
+ * 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.grpc.source.reader;
+
+import org.apache.flink.annotation.Internal;
+
+import org.apache.rocketmq.client.apis.ClientException;
+import org.apache.rocketmq.client.apis.message.MessageView;
+
+import java.io.Closeable;
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * The minimal consumer surface the split reader needs. Both SDK consumers
({@code
+ * LiteSimpleConsumer} and {@code SimpleConsumer}) expose the same Pop-model
receive / ack /
+ * change-invisible-duration operations but share no common interface, so the
connector adapts them
+ * to this single type.
+ */
+@Internal
+public interface PopConsumer extends Closeable {
+
+ /** Blocking long-polling receive. */
+ List<MessageView> receive(int maxMessageNum, Duration invisibleDuration)
throws ClientException;
+
+ /** Acknowledge a received message, removing it from the Pop invisible
set. */
+ void ack(MessageView messageView) throws ClientException;
+
+ /** Extend the invisible duration of a still-buffered message. */
+ void changeInvisibleDuration(MessageView messageView, Duration
invisibleDuration)
+ throws ClientException;
+}
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumerProvider.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumerProvider.java
new file mode 100644
index 0000000..7111912
--- /dev/null
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumerProvider.java
@@ -0,0 +1,164 @@
+/*
+ * 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.grpc.source.reader;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.configuration.Configuration;
+import
org.apache.flink.connector.rocketmq.grpc.common.ClientConfigurationProvider;
+import org.apache.flink.connector.rocketmq.grpc.common.FilterExpressionParser;
+import org.apache.flink.connector.rocketmq.grpc.source.ConsumerMode;
+import
org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSourceOptions;
+
+import org.apache.rocketmq.client.apis.ClientException;
+import org.apache.rocketmq.client.apis.ClientServiceProvider;
+import org.apache.rocketmq.client.apis.consumer.FilterExpression;
+import org.apache.rocketmq.client.apis.consumer.LiteSimpleConsumer;
+import org.apache.rocketmq.client.apis.consumer.SimpleConsumer;
+import org.apache.rocketmq.client.apis.message.MessageView;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A factory that builds a gRPC Pop consumer from a Flink configuration: a
{@code
+ * LiteSimpleConsumer} bound to the main lite topic in {@link
ConsumerMode#LITE}, or a {@code
+ * SimpleConsumer} subscribed to a normal topic with a filter expression in
{@link
+ * ConsumerMode#SIMPLE}.
+ */
+@Internal
+public class PopConsumerProvider {
+
+ private PopConsumerProvider() {}
+
+ /**
+ * Build and start the {@link PopConsumer} selected by {@link
RocketMQGrpcSourceOptions#MODE}.
+ */
+ public static PopConsumer create(Configuration configuration) throws
ClientException {
+ final ClientServiceProvider provider =
ClientServiceProvider.loadService();
+ if (configuration.get(RocketMQGrpcSourceOptions.MODE) ==
ConsumerMode.SIMPLE) {
+ final FilterExpression filter =
+ FilterExpressionParser.parse(
+
configuration.get(RocketMQGrpcSourceOptions.FILTER_EXPRESSION),
+
configuration.get(RocketMQGrpcSourceOptions.FILTER_TYPE));
+ final SimpleConsumer consumer =
+ provider.newSimpleConsumerBuilder()
+ .setClientConfiguration(
+
ClientConfigurationProvider.getClientConfiguration(
+ configuration))
+ .setConsumerGroup(
+
configuration.get(RocketMQGrpcSourceOptions.CONSUMER_GROUP))
+ .setAwaitDuration(
+
configuration.get(RocketMQGrpcSourceOptions.AWAIT_DURATION))
+ .setSubscriptionExpressions(
+ Collections.singletonMap(
+
configuration.get(RocketMQGrpcSourceOptions.TOPIC),
+ filter))
+ .build();
+ return new SimplePopConsumer(consumer);
+ }
+ final LiteSimpleConsumer consumer =
+ provider.newLiteSimpleConsumerBuilder()
+ .setClientConfiguration(
+
ClientConfigurationProvider.getClientConfiguration(configuration))
+ .setConsumerGroup(
+
configuration.get(RocketMQGrpcSourceOptions.CONSUMER_GROUP))
+ .setAwaitDuration(
+
configuration.get(RocketMQGrpcSourceOptions.AWAIT_DURATION))
+
.bindTopic(configuration.get(RocketMQGrpcSourceOptions.MAIN_TOPIC))
+ .build();
+ return new LitePopConsumer(consumer);
+ }
+
+ /** Wrap an SDK {@code LiteSimpleConsumer} into the connector's {@link
PopConsumer} surface. */
+ @VisibleForTesting
+ static PopConsumer wrap(LiteSimpleConsumer consumer) {
+ return new LitePopConsumer(consumer);
+ }
+
+ /** Wrap an SDK {@code SimpleConsumer} into the connector's {@link
PopConsumer} surface. */
+ @VisibleForTesting
+ static PopConsumer wrap(SimpleConsumer consumer) {
+ return new SimplePopConsumer(consumer);
+ }
+
+ private static final class LitePopConsumer implements PopConsumer {
+
+ private final LiteSimpleConsumer delegate;
+
+ private LitePopConsumer(LiteSimpleConsumer delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public List<MessageView> receive(int maxMessageNum, Duration
invisibleDuration)
+ throws ClientException {
+ return delegate.receive(maxMessageNum, invisibleDuration);
+ }
+
+ @Override
+ public void ack(MessageView messageView) throws ClientException {
+ delegate.ack(messageView);
+ }
+
+ @Override
+ public void changeInvisibleDuration(MessageView messageView, Duration
invisibleDuration)
+ throws ClientException {
+ delegate.changeInvisibleDuration(messageView, invisibleDuration);
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+ }
+
+ private static final class SimplePopConsumer implements PopConsumer {
+
+ private final SimpleConsumer delegate;
+
+ private SimplePopConsumer(SimpleConsumer delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public List<MessageView> receive(int maxMessageNum, Duration
invisibleDuration)
+ throws ClientException {
+ return delegate.receive(maxMessageNum, invisibleDuration);
+ }
+
+ @Override
+ public void ack(MessageView messageView) throws ClientException {
+ delegate.ack(messageView);
+ }
+
+ @Override
+ public void changeInvisibleDuration(MessageView messageView, Duration
invisibleDuration)
+ throws ClientException {
+ delegate.changeInvisibleDuration(messageView, invisibleDuration);
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+ }
+}
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceReader.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceReader.java
index 63fd328..b9c560a 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceReader.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceReader.java
@@ -26,17 +26,35 @@ import
org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
import
org.apache.flink.connector.base.source.reader.SingleThreadMultiplexSourceReaderBase;
import
org.apache.flink.connector.base.source.reader.synchronization.FutureCompletingBlockingQueue;
import org.apache.flink.connector.rocketmq.grpc.ack.AckableMessage;
+import org.apache.flink.connector.rocketmq.grpc.ack.RocketMQReceiptHandle;
import
org.apache.flink.connector.rocketmq.grpc.source.split.RocketMQGrpcSourceSplit;
import
org.apache.flink.connector.rocketmq.grpc.source.split.RocketMQGrpcSourceSplitState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
import java.util.Map;
/**
* The source reader for the RocketMQ gRPC connector. It emits {@link
AckableMessage} records and
- * does not acknowledge messages itself; acknowledgement is performed by a
downstream operator using
- * the self-contained receipt handle carried by each emitted record. Un-acked
messages are
- * redelivered by the broker after their invisible duration expires, providing
the at-least-once
- * guarantee.
+ * its acknowledgement behaviour depends on the consumer mode:
+ *
+ * <ul>
+ * <li>LITE mode: the reader does not acknowledge messages itself;
acknowledgement is performed by
+ * a downstream operator using the self-contained receipt handle carried
by each emitted
+ * record.
+ * <li>SIMPLE mode: the reader acknowledges the emitted messages itself once
the checkpoint that
+ * observed them completes, issuing the acks through the very {@link
+ * RocketMQGrpcSourceSplitReader} consumer that received them — no
separate ack client and no
+ * second connection are involved. This requires checkpointing to be
enabled and the invisible
+ * duration to be larger than the checkpoint interval plus the
checkpoint timeout.
+ * </ul>
+ *
+ * <p>In both modes un-acked messages are redelivered by the broker after
their invisible duration
+ * expires, providing the at-least-once guarantee.
*/
@Internal
public class RocketMQGrpcSourceReader<T>
@@ -46,13 +64,68 @@ public class RocketMQGrpcSourceReader<T>
RocketMQGrpcSourceSplit,
RocketMQGrpcSourceSplitState> {
+ private static final Logger LOG =
LoggerFactory.getLogger(RocketMQGrpcSourceReader.class);
+
+ @Nullable private final CheckpointAckTracker ackTracker;
+
+ /**
+ * The split reader that received the messages; only consulted in SIMPLE
mode, i.e. when {@link
+ * #ackTracker} is non-null. It is not closed here: the fetcher manager
owns its lifecycle and
+ * shuts it down as part of the base reader close.
+ */
+ @Nullable private final RocketMQGrpcSourceSplitReader splitReader;
+
public RocketMQGrpcSourceReader(
FutureCompletingBlockingQueue<RecordsWithSplitIds<MessageViewImpl>>
elementsQueue,
RocketMQGrpcSourceFetcherManager fetcherManager,
RocketMQGrpcSourceRecordEmitter<T> recordEmitter,
Configuration config,
SourceReaderContext context) {
+ this(elementsQueue, fetcherManager, recordEmitter, config, context,
null, null);
+ }
+
+ public RocketMQGrpcSourceReader(
+
FutureCompletingBlockingQueue<RecordsWithSplitIds<MessageViewImpl>>
elementsQueue,
+ RocketMQGrpcSourceFetcherManager fetcherManager,
+ RocketMQGrpcSourceRecordEmitter<T> recordEmitter,
+ Configuration config,
+ SourceReaderContext context,
+ @Nullable CheckpointAckTracker ackTracker,
+ @Nullable RocketMQGrpcSourceSplitReader splitReader) {
super(elementsQueue, fetcherManager, recordEmitter, config, context);
+ this.ackTracker = ackTracker;
+ this.splitReader = splitReader;
+ }
+
+ @Override
+ public List<RocketMQGrpcSourceSplit> snapshotState(long checkpointId) {
+ final List<RocketMQGrpcSourceSplit> splits =
super.snapshotState(checkpointId);
+ if (ackTracker != null) {
+ ackTracker.snapshot(checkpointId);
+ }
+ return splits;
+ }
+
+ @Override
+ public void notifyCheckpointComplete(long checkpointId) {
+ if (ackTracker == null || splitReader == null) {
+ return;
+ }
+ for (RocketMQReceiptHandle handle :
ackTracker.completeUpTo(checkpointId)) {
+ try {
+ splitReader.ack(handle);
+ } catch (Exception e) {
+ // The handle is already removed from the tracker, so
tolerating the failure
+ // redelivers only this one message once its invisible
duration expires, while
+ // failing the job would redeliver everything since the last
checkpoint.
+ LOG.warn(
+ "Failed to acknowledge message {} on checkpoint {}
completion; "
+ + "the broker will redeliver the message.",
+ handle.getMessageId(),
+ checkpointId,
+ e);
+ }
+ }
}
@Override
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceRecordEmitter.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceRecordEmitter.java
index 2dae65e..186d254 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceRecordEmitter.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceRecordEmitter.java
@@ -28,7 +28,10 @@ import
org.apache.flink.connector.rocketmq.grpc.source.deserialization.RocketMQG
import
org.apache.flink.connector.rocketmq.grpc.source.split.RocketMQGrpcSourceSplitState;
import org.apache.flink.util.Collector;
+import javax.annotation.Nullable;
+
import java.io.IOException;
+import java.util.function.Consumer;
/**
* The {@link RecordEmitter} implementation for the RocketMQ gRPC source. It
deserializes the
@@ -43,15 +46,25 @@ public class RocketMQGrpcSourceRecordEmitter<T>
private final RocketMQGrpcDeserializationSchema<T> deserializationSchema;
private final String namespace;
private final String consumerGroup;
+ @Nullable private final Consumer<RocketMQReceiptHandle>
emittedHandleListener;
private final AckableCollector<T> collector = new AckableCollector<>();
public RocketMQGrpcSourceRecordEmitter(
RocketMQGrpcDeserializationSchema<T> deserializationSchema,
String namespace,
String consumerGroup) {
+ this(deserializationSchema, namespace, consumerGroup, null);
+ }
+
+ public RocketMQGrpcSourceRecordEmitter(
+ RocketMQGrpcDeserializationSchema<T> deserializationSchema,
+ String namespace,
+ String consumerGroup,
+ @Nullable Consumer<RocketMQReceiptHandle> emittedHandleListener) {
this.deserializationSchema = deserializationSchema;
this.namespace = namespace;
this.consumerGroup = consumerGroup;
+ this.emittedHandleListener = emittedHandleListener;
}
@Override
@@ -66,6 +79,9 @@ public class RocketMQGrpcSourceRecordEmitter<T>
element.getMessageView(), namespace,
consumerGroup);
collector.reset(output, element.getEventTime(), handle);
deserializationSchema.deserialize(element, collector);
+ if (emittedHandleListener != null) {
+ emittedHandleListener.accept(handle);
+ }
splitState.incrementProcessedRecords();
} catch (Exception e) {
throw new IOException("Failed to deserialize message due to", e);
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceSplitReader.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceSplitReader.java
index 570269a..846639b 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceSplitReader.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceSplitReader.java
@@ -19,11 +19,15 @@
package org.apache.flink.connector.rocketmq.grpc.source.reader;
import org.apache.flink.annotation.Internal;
+import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.base.source.reader.RecordsBySplits;
import org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
import org.apache.flink.connector.base.source.reader.splitreader.SplitsChange;
+import org.apache.flink.connector.rocketmq.grpc.ack.RocketMQReceiptHandle;
+import org.apache.flink.connector.rocketmq.grpc.ack.RocketMQReceiptHandleCodec;
+import org.apache.flink.connector.rocketmq.grpc.source.ConsumerMode;
import
org.apache.flink.connector.rocketmq.grpc.source.InvisibleDurationRenewalPolicies;
import
org.apache.flink.connector.rocketmq.grpc.source.InvisibleDurationRenewalPolicy;
import
org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSourceOptions;
@@ -31,7 +35,6 @@ import
org.apache.flink.connector.rocketmq.grpc.source.split.RocketMQGrpcSourceS
import org.apache.flink.util.FlinkRuntimeException;
import org.apache.rocketmq.client.apis.ClientException;
-import org.apache.rocketmq.client.apis.consumer.LiteSimpleConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -53,15 +56,19 @@ import static
org.apache.flink.util.Preconditions.checkArgument;
* The {@link SplitReader} implementation for the RocketMQ gRPC Pop model.
*
* <p>Because the broker performs message-level load balancing across the
consumer group, this
- * reader does not partition topics. Instead every subtask binds the same main
lite topic. To raise
- * a single subtask's throughput it runs {@code fetch-concurrency} worker
threads that share a
- * single {@link LiteSimpleConsumer}, each issuing a blocking {@code
receive()} long poll. Received
- * messages are handed to the (single) fetcher thread through an internal
queue.
+ * reader does not partition topics. Instead every subtask consumes the same
topic: the main lite
+ * topic in {@link ConsumerMode#LITE}, or the subscribed normal topic in {@link
+ * ConsumerMode#SIMPLE}. To raise a single subtask's throughput it runs {@code
fetch-concurrency}
+ * worker threads that share a single {@link PopConsumer}, each issuing a
blocking {@code receive()}
+ * long poll. Received messages are handed to the (single) fetcher thread
through an internal queue.
*
- * <p>This reader never acknowledges messages: acknowledgement is deferred to
a downstream operator
- * that receives the {@code AckableMessage} records carrying a self-contained
receipt handle.
- * Messages that are never acked become visible again after their invisible
duration and are
- * redelivered by the broker, which provides the at-least-once guarantee.
+ * <p>Acknowledgement depends on the mode. In {@link ConsumerMode#LITE} this
reader never
+ * acknowledges messages: acknowledgement is deferred to a downstream operator
that receives the
+ * {@code AckableMessage} records carrying a self-contained receipt handle. In
{@link
+ * ConsumerMode#SIMPLE} the source reader acknowledges through {@link
#ack(RocketMQReceiptHandle)}
+ * on this very consumer once the checkpoint that observed the messages
completes. Messages that are
+ * never acked become visible again after their invisible duration and are
redelivered by the
+ * broker, which provides the at-least-once guarantee.
*
* <p>When an {@link InvisibleDurationRenewalPolicy} is configured, messages
that are still buffered
* in the internal queue shortly before they would become visible again are
offered to the policy,
@@ -90,7 +97,13 @@ public class RocketMQGrpcSourceSplitReader
private volatile boolean closed = false;
private ReceiveWorker[] workers;
- private LiteSimpleConsumer consumer;
+
+ /**
+ * Written by the fetcher thread in {@link #startWorkers()} and read by
the receive workers, the
+ * renewal thread and the mailbox thread ({@link
#ack(RocketMQReceiptHandle)}), hence volatile.
+ */
+ private volatile PopConsumer consumer;
+
@Nullable private ScheduledThreadPoolExecutor renewalExecutor;
public RocketMQGrpcSourceSplitReader(Configuration configuration) {
@@ -158,10 +171,10 @@ public class RocketMQGrpcSourceSplitReader
return;
}
try {
- consumer = LiteSimpleConsumerProvider.create(configuration);
+ consumer = PopConsumerProvider.create(configuration);
} catch (ClientException e) {
started.set(false);
- throw new FlinkRuntimeException("Failed to create RocketMQ gRPC
LiteSimpleConsumer", e);
+ throw new FlinkRuntimeException("Failed to create the RocketMQ
gRPC consumer", e);
}
if (renewalPolicy != null) {
renewalExecutor =
@@ -180,10 +193,49 @@ public class RocketMQGrpcSourceSplitReader
workers[i] = worker;
worker.start();
}
+ final boolean simple =
+ configuration.get(RocketMQGrpcSourceOptions.MODE) ==
ConsumerMode.SIMPLE;
LOG.info(
- "Started {} receive worker(s) sharing one lite consumer bound
to topic {}",
+ "Started {} receive worker(s) sharing one {} consumer bound to
topic {}",
fetchConcurrency,
- configuration.get(RocketMQGrpcSourceOptions.MAIN_TOPIC));
+ simple ? "simple" : "lite",
+ configuration.get(
+ simple
+ ? RocketMQGrpcSourceOptions.TOPIC
+ : RocketMQGrpcSourceOptions.MAIN_TOPIC));
+ }
+
+ /**
+ * Acknowledge a message that this reader received, removing it from the
Pop invisible set. Used
+ * by the source reader in SIMPLE mode once the checkpoint that observed
the message completes.
+ *
+ * <p>The credential-free {@link RocketMQReceiptHandle} is passed instead
of the SDK {@code
+ * MessageView} on purpose: it keeps the checkpoint ack tracker from
pinning message bodies in
+ * memory, and the minimal view the ack RPC needs is rebuilt here.
+ *
+ * <p>SDK Pop consumers are thread-safe, so acking from the mailbox thread
while the receive
+ * workers block in {@code receive()} on the same instance is safe.
+ *
+ * @param handle the receipt handle of a message emitted by this reader.
+ * @throws ClientException if the ack RPC fails.
+ * @throws IllegalStateException if the consumer has not been started yet
or is already closed.
+ */
+ public void ack(RocketMQReceiptHandle handle) throws ClientException {
+ final PopConsumer currentConsumer = consumer;
+ if (currentConsumer == null || closed) {
+ throw new IllegalStateException(
+ "Cannot acknowledge message "
+ + handle.getMessageId()
+ + ": the RocketMQ gRPC consumer is "
+ + (currentConsumer == null ? "not started yet" :
"already closed"));
+ }
+ currentConsumer.ack(RocketMQReceiptHandleCodec.toAckable(handle));
+ }
+
+ /** Inject a consumer so that ack / renewal behaviour can be tested
without a live cluster. */
+ @VisibleForTesting
+ void setConsumer(PopConsumer consumer) {
+ this.consumer = consumer;
}
@Override
@@ -262,7 +314,7 @@ public class RocketMQGrpcSourceSplitReader
}
/**
- * A single receive loop over the shared {@link LiteSimpleConsumer}.
Because the consumer is
+ * A single receive loop over the shared {@link PopConsumer}. Because the
consumer is
* thread-safe, several workers can call {@code receive()} on it
concurrently; a blocking {@code
* receive()} only occupies its calling thread, which is why several
threads are used to keep
* that many long polls in flight.
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcConnectorOptions.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcConnectorOptions.java
index 03b3d36..a59ad69 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcConnectorOptions.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcConnectorOptions.java
@@ -122,6 +122,37 @@ public class RocketMQGrpcConnectorOptions {
.noDefaultValue()
.withDescription("The consumer group of the
SimpleConsumer.");
+ public static final ConfigOption<String> MODE =
+ ConfigOptions.key("source.mode")
+ .stringType()
+ .defaultValue("simple")
+ .withDescription(
+ "The consumption mode. 'simple' (the default)
subscribes to the "
+ + "configured topic as a normal topic via
a SimpleConsumer and "
+ + "the source acknowledges messages when
their checkpoint "
+ + "completes (checkpointing must be
enabled and "
+ + "'source.invisible-duration' must exceed
the checkpoint "
+ + "interval plus timeout); 'lite' binds
the configured topic "
+ + "as a main lite topic via a
LiteSimpleConsumer and defers "
+ + "acknowledgement to a downstream
operator.");
+
+ public static final ConfigOption<String> FILTER_EXPRESSION =
+ ConfigOptions.key("source.filter-expression")
+ .stringType()
+ .noDefaultValue()
+ .withDescription(
+ "The filter expression of the 'simple' mode
subscription, e.g. a tag "
+ + "expression 'tagA||tagB' or a SQL92
expression. When absent, "
+ + "all messages are received.");
+
+ public static final ConfigOption<String> FILTER_TYPE =
+ ConfigOptions.key("source.filter-type")
+ .stringType()
+ .defaultValue("TAG")
+ .withDescription(
+ "The type of 'source.filter-expression': TAG or
SQL92. Only effective "
+ + "in 'simple' mode.");
+
public static final ConfigOption<Duration> AWAIT_DURATION =
ConfigOptions.key("source.await-duration")
.durationType()
@@ -134,9 +165,13 @@ public class RocketMQGrpcConnectorOptions {
.durationType()
.defaultValue(Duration.ofSeconds(60))
.withDescription(
- "The invisible duration of a received message. It
must be larger than "
- + "the checkpoint interval plus the
checkpoint timeout so that "
- + "un-acked messages are redelivered after
a failure.");
+ "The invisible duration of a received message;
un-acked messages are "
+ + "redelivered after it expires. In
'simple' mode the source "
+ + "acknowledges on checkpoint completion,
so size it with "
+ + "headroom above two checkpoint intervals
plus the checkpoint "
+ + "timeout, because a failed checkpoint
defers the ack to the "
+ + "next successful one. In 'lite' mode it
must cover the full "
+ + "downstream processing time instead.");
public static final ConfigOption<Integer> MAX_MESSAGE_NUM =
ConfigOptions.key("source.max-message-num")
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactory.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactory.java
index d6968f9..8c0d0b9 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactory.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactory.java
@@ -27,7 +27,9 @@ import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.ReadableConfig;
import org.apache.flink.connector.rocketmq.grpc.RocketMQGrpcOptions;
import org.apache.flink.connector.rocketmq.grpc.sink.RocketMQGrpcSinkOptions;
+import org.apache.flink.connector.rocketmq.grpc.source.ConsumerMode;
import
org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSourceOptions;
+import org.apache.flink.table.api.ValidationException;
import org.apache.flink.table.connector.format.DecodingFormat;
import org.apache.flink.table.connector.format.EncodingFormat;
import org.apache.flink.table.connector.sink.DynamicTableSink;
@@ -80,6 +82,9 @@ public class RocketMQGrpcDynamicTableFactory
options.add(RocketMQGrpcConnectorOptions.REQUEST_TIMEOUT);
options.add(RocketMQGrpcConnectorOptions.LITE_TOPIC);
options.add(RocketMQGrpcConnectorOptions.CONSUMER_GROUP);
+ options.add(RocketMQGrpcConnectorOptions.MODE);
+ options.add(RocketMQGrpcConnectorOptions.FILTER_EXPRESSION);
+ options.add(RocketMQGrpcConnectorOptions.FILTER_TYPE);
options.add(RocketMQGrpcConnectorOptions.AWAIT_DURATION);
options.add(RocketMQGrpcConnectorOptions.INVISIBLE_DURATION);
options.add(RocketMQGrpcConnectorOptions.MAX_MESSAGE_NUM);
@@ -194,6 +199,31 @@ public class RocketMQGrpcDynamicTableFactory
configuration,
RocketMQGrpcConnectorOptions.CONSUMER_GROUP,
RocketMQGrpcSourceOptions.CONSUMER_GROUP);
+ final String mode = options.get(RocketMQGrpcConnectorOptions.MODE);
+ if (mode != null) {
+ final ConsumerMode consumerMode;
+ try {
+ consumerMode = ConsumerMode.valueOf(mode.trim().toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new ValidationException(
+ "Unsupported value '"
+ + mode
+ + "' for option '"
+ + RocketMQGrpcConnectorOptions.MODE.key()
+ + "'. Supported values are 'lite' and
'simple'.");
+ }
+ configuration.set(RocketMQGrpcSourceOptions.MODE, consumerMode);
+ }
+ copyMapped(
+ options,
+ configuration,
+ RocketMQGrpcConnectorOptions.FILTER_EXPRESSION,
+ RocketMQGrpcSourceOptions.FILTER_EXPRESSION);
+ copyMapped(
+ options,
+ configuration,
+ RocketMQGrpcConnectorOptions.FILTER_TYPE,
+ RocketMQGrpcSourceOptions.FILTER_TYPE);
copyMapped(
options,
configuration,
diff --git
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableSource.java
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableSource.java
index 5cb7f54..c4d8d50 100644
---
a/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableSource.java
+++
b/flink-connector-rocketmq-grpc/src/main/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableSource.java
@@ -25,7 +25,10 @@ import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.connector.source.Boundedness;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.connector.rocketmq.grpc.ack.AckableMessage;
+import org.apache.flink.connector.rocketmq.grpc.source.ConsumerMode;
import org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSource;
+import
org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSourceBuilder;
+import
org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSourceOptions;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
@@ -119,13 +122,17 @@ public class RocketMQGrpcDynamicTableSource implements
ScanTableSource, Supports
RocketMQGrpcRowDataConverter.forSource(
valueDeserialization, metadataConverters,
producedType);
- final RocketMQGrpcSource<RowData> source =
+ final RocketMQGrpcSourceBuilder<RowData> sourceBuilder =
RocketMQGrpcSource.<RowData>builder()
.setConfig(configuration)
- .setMainTopic(topic)
.setBoundedness(boundedness)
- .setDeserializer(converter)
- .build();
+ .setDeserializer(converter);
+ if (configuration.get(RocketMQGrpcSourceOptions.MODE) ==
ConsumerMode.SIMPLE) {
+ sourceBuilder.setTopic(topic);
+ } else {
+ sourceBuilder.setMainTopic(topic);
+ }
+ final RocketMQGrpcSource<RowData> source = sourceBuilder.build();
// The source produces AckableMessage<RowData>; the SQL/Table path
does not perform
// downstream acknowledgement, so the receipt handle is stripped here
and only the RowData
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcITCase.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcITCase.java
index 9ce6d06..c041f9f 100644
---
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcITCase.java
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcITCase.java
@@ -21,8 +21,10 @@ package org.apache.flink.connector.rocketmq.grpc;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.grpc.ack.AckableMessage;
import
org.apache.flink.connector.rocketmq.grpc.ack.RocketMQThrottleProcessFunction;
import org.apache.flink.connector.rocketmq.grpc.sink.RocketMQGrpcSink;
+import org.apache.flink.connector.rocketmq.grpc.source.ConsumerMode;
import org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSource;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
@@ -31,13 +33,16 @@ import org.apache.flink.test.util.AbstractTestBase;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.StaticSessionCredentialsProvider;
+import org.apache.rocketmq.client.apis.consumer.FilterExpression;
+import org.apache.rocketmq.client.apis.consumer.SimpleConsumer;
import org.apache.rocketmq.client.apis.message.Message;
+import org.apache.rocketmq.client.apis.message.MessageView;
import org.apache.rocketmq.client.apis.producer.Producer;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,14 +60,12 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
/**
- * Integration tests for the RocketMQ gRPC connector. Requires a running
RocketMQ 5.x instance with
- * gRPC proxy enabled. Connection details are read from environment variables,
so this test is
- * {@link Disabled} by default and must be run manually after exporting {@code
- * ROCKETMQ_GRPC_ENDPOINTS}, {@code ROCKETMQ_GRPC_ACCESS_KEY} and {@code
ROCKETMQ_GRPC_SECRET_KEY}.
+ * Integration tests for the RocketMQ gRPC connector. They require a running
RocketMQ 5.x instance
+ * with the gRPC proxy enabled and are therefore skipped unless {@code
ROCKETMQ_GRPC_ENDPOINTS} is
+ * set; export it together with {@code ROCKETMQ_GRPC_ACCESS_KEY} and {@code
+ * ROCKETMQ_GRPC_SECRET_KEY} to run them against a real cluster.
*/
-@Disabled(
- "Requires a running RocketMQ 5.x instance with gRPC proxy. Set the
ROCKETMQ_GRPC_* "
- + "environment variables and run manually.")
+@EnabledIfEnvironmentVariable(named = "ROCKETMQ_GRPC_ENDPOINTS", matches =
".+")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class RocketMQGrpcITCase extends AbstractTestBase {
@@ -80,6 +83,8 @@ class RocketMQGrpcITCase extends AbstractTestBase {
private static final String E2E_SOURCE_TOPIC = "flink-source-1";
private static final String E2E_SINK_TOPIC = "flink-sink-2";
private static final String E2E_SINK_LITE_TOPIC = "flink-sink-2-lite";
+ private static final String SIMPLE_SOURCE_TOPIC =
+ System.getProperty("rocketmq.simple.topic", "flink-source-simple");
private static final String CONSUMER_GROUP = "GID-flink";
private static final int NUM_MESSAGES = 50;
@@ -141,6 +146,7 @@ class RocketMQGrpcITCase extends AbstractTestBase {
RocketMQGrpcSource.<String>builder()
.setEndpoints(ENDPOINTS)
.setConsumerGroup(CONSUMER_GROUP)
+ .setMode(ConsumerMode.LITE)
.setMainTopic(SOURCE_TOPIC)
.setConfig(RocketMQGrpcOptions.ACCESS_KEY, ACCESS_KEY)
.setConfig(RocketMQGrpcOptions.SECRET_KEY, SECRET_KEY)
@@ -186,6 +192,7 @@ class RocketMQGrpcITCase extends AbstractTestBase {
RocketMQGrpcSource.<String>builder()
.setEndpoints(ENDPOINTS)
.setConsumerGroup(CONSUMER_GROUP)
+ .setMode(ConsumerMode.LITE)
.setMainTopic(E2E_SOURCE_TOPIC)
.setConfig(RocketMQGrpcOptions.ACCESS_KEY, ACCESS_KEY)
.setConfig(RocketMQGrpcOptions.SECRET_KEY, SECRET_KEY)
@@ -246,6 +253,120 @@ class RocketMQGrpcITCase extends AbstractTestBase {
}
}
+ // -----------------------------------------------------------------------
+ // Test: Simple mode consumes a normal topic with checkpoint-based ack
+ // -----------------------------------------------------------------------
+
+ @Test
+ void testSimpleModeConsumesNormalTopic() throws Exception {
+ final String prefix = "simple-" + runId + "-";
+
+ seedMessagesViaProducer(SIMPLE_SOURCE_TOPIC, NUM_MESSAGES, prefix);
+
+ final StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
+ env.setParallelism(1);
+ env.enableCheckpointing(5_000);
+
+ final RocketMQGrpcSource<String> source =
+ RocketMQGrpcSource.<String>builder()
+ .setEndpoints(ENDPOINTS)
+ .setConsumerGroup(CONSUMER_GROUP)
+ .setMode(ConsumerMode.SIMPLE)
+ .setTopic(SIMPLE_SOURCE_TOPIC)
+ .setConfig(RocketMQGrpcOptions.ACCESS_KEY, ACCESS_KEY)
+ .setConfig(RocketMQGrpcOptions.SECRET_KEY, SECRET_KEY)
+ .setValueOnlyDeserializer(new SimpleStringSchema())
+ .build();
+
+ final DataStream<String> stream =
+ env.fromSource(source, WatermarkStrategy.noWatermarks(),
"RocketMQ gRPC Source")
+ .map(AckableMessage::getValue)
+ .returns(String.class);
+ stream.addSink(new CollectingSinkFunction());
+
+ // Start the job in a daemon thread (same pattern as startAndCollect),
but keep it
+ // alive after enough records are collected: SIMPLE mode acks only
after a checkpoint
+ // completes, and closing the job immediately would leave nothing
acked.
+ CollectingSinkFunction.QUEUE.clear();
+ final Thread jobThread =
+ new Thread(
+ () -> {
+ try {
+ env.execute("Simple Mode Collecting Job");
+ } catch (Exception e) {
+ LOG.info("Simple mode collecting job ended:
{}", e.getMessage());
+ }
+ });
+ jobThread.setDaemon(true);
+ jobThread.start();
+
+ // Collect messages matching the prefix until we have enough or
timeout.
+ final List<String> results = new ArrayList<>();
+ final long deadline = System.currentTimeMillis() +
COLLECT_TIMEOUT_SECONDS * 1000;
+ while (results.size() < NUM_MESSAGES && System.currentTimeMillis() <
deadline) {
+ final String item = CollectingSinkFunction.QUEUE.poll(500,
TimeUnit.MILLISECONDS);
+ if (item != null && item.startsWith(prefix)) {
+ results.add(item);
+ }
+ }
+ LOG.info("Collected {} messages (prefix={}) in simple mode",
results.size(), prefix);
+
+ assertThat(results).hasSize(NUM_MESSAGES);
+ for (int i = 0; i < NUM_MESSAGES; i++) {
+ assertThat(results).contains(prefix + i);
+ }
+
+ // Keep the job running so that at least one checkpoint completes
(interval is 5s)
+ // and the reader acks the tracked receipt handles.
+ Thread.sleep(20_000);
+
+ env.close();
+ jobThread.interrupt();
+ jobThread.join(10_000);
+
+ // Wait for the default 60s invisible duration to expire so that any
un-acked message
+ // would become visible for redelivery again.
+ Thread.sleep(75_000);
+
+ // Verify no un-acked messages remain: a fresh consumer under the same
group must not
+ // receive any message of this run's prefix.
+ final ClientServiceProvider provider =
ClientServiceProvider.loadService();
+ final ClientConfiguration clientConfig =
+ ClientConfiguration.newBuilder()
+ .setEndpoints(ENDPOINTS)
+ .setCredentialProvider(
+ new
StaticSessionCredentialsProvider(ACCESS_KEY, SECRET_KEY))
+ .build();
+
+ try (SimpleConsumer consumer =
+ provider.newSimpleConsumerBuilder()
+ .setClientConfiguration(clientConfig)
+ .setConsumerGroup(CONSUMER_GROUP)
+ .setAwaitDuration(Duration.ofSeconds(5))
+ .setSubscriptionExpressions(
+ Collections.singletonMap(
+ SIMPLE_SOURCE_TOPIC,
FilterExpression.SUB_ALL))
+ .build()) {
+ int redelivered = 0;
+ final List<MessageView> messages = consumer.receive(32,
Duration.ofSeconds(20));
+ for (MessageView msg : messages) {
+ final ByteBuffer buf = msg.getBody();
+ final byte[] bytes = new byte[buf.remaining()];
+ buf.get(bytes);
+ final String body = new String(bytes, StandardCharsets.UTF_8);
+ if (body.startsWith(prefix)) {
+ redelivered++;
+ }
+ }
+ assertThat(redelivered)
+ .withFailMessage(
+ "Expected no redelivered messages after
checkpoint-aligned ack,"
+ + " but got %d",
+ redelivered)
+ .isEqualTo(0);
+ }
+ }
+
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcLiteE2EVerify.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcLiteE2EVerify.java
index bea6673..070247b 100644
---
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcLiteE2EVerify.java
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/RocketMQGrpcLiteE2EVerify.java
@@ -23,6 +23,7 @@ import
org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.configuration.Configuration;
import
org.apache.flink.connector.rocketmq.grpc.ack.RocketMQThrottleProcessFunction;
import org.apache.flink.connector.rocketmq.grpc.sink.RocketMQGrpcSink;
+import org.apache.flink.connector.rocketmq.grpc.source.ConsumerMode;
import
org.apache.flink.connector.rocketmq.grpc.source.InvisibleDurationRenewalPolicy;
import org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSource;
import
org.apache.flink.connector.rocketmq.grpc.source.RocketMQGrpcSourceOptions;
@@ -111,6 +112,7 @@ public class RocketMQGrpcLiteE2EVerify {
RocketMQGrpcSource.<String>builder()
.setEndpoints(ENDPOINTS)
.setConsumerGroup(CONSUMER_GROUP)
+ .setMode(ConsumerMode.LITE)
.setMainTopic(MAIN_TOPIC)
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
@@ -195,6 +197,7 @@ public class RocketMQGrpcLiteE2EVerify {
RocketMQGrpcSource.<String>builder()
.setEndpoints(ENDPOINTS)
.setConsumerGroup(CONSUMER_GROUP)
+ .setMode(ConsumerMode.LITE)
.setMainTopic(MAIN_TOPIC)
.setConfig(
RocketMQGrpcSourceOptions.INVISIBLE_DURATION,
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQLiteAckClientTest.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckClientTest.java
similarity index 59%
rename from
flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQLiteAckClientTest.java
rename to
flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckClientTest.java
index 5c9dc5e..3acd987 100644
---
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQLiteAckClientTest.java
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/ack/RocketMQAckClientTest.java
@@ -25,8 +25,8 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
-/** Tests for the reference counting behaviour of {@link
RocketMQLiteAckClient#acquire}. */
-class RocketMQLiteAckClientTest {
+/** Tests for the reference counting behaviour of {@link
RocketMQAckClient#acquire}. */
+class RocketMQAckClientTest {
private static Configuration config(String endpoints) {
final Configuration configuration = new Configuration();
@@ -37,20 +37,20 @@ class RocketMQLiteAckClientTest {
@Test
void sameConfigurationSharesASingleReferenceCountedClient() {
final Configuration configuration = config("127.0.0.1:8080");
-
assertThat(RocketMQLiteAckClient.getReferenceCount(configuration)).isZero();
+
assertThat(RocketMQAckClient.getReferenceCount(configuration)).isZero();
- final RocketMQLiteAckClient first =
RocketMQLiteAckClient.acquire(configuration);
- final RocketMQLiteAckClient second =
RocketMQLiteAckClient.acquire(configuration);
+ final RocketMQAckClient first =
RocketMQAckClient.acquire(configuration);
+ final RocketMQAckClient second =
RocketMQAckClient.acquire(configuration);
// The same client is shared and the reference count is incremented
per acquire.
assertThat(second).isSameAs(first);
-
assertThat(RocketMQLiteAckClient.getReferenceCount(configuration)).isEqualTo(2);
+
assertThat(RocketMQAckClient.getReferenceCount(configuration)).isEqualTo(2);
- RocketMQLiteAckClient.release(configuration);
-
assertThat(RocketMQLiteAckClient.getReferenceCount(configuration)).isEqualTo(1);
+ RocketMQAckClient.release(configuration);
+
assertThat(RocketMQAckClient.getReferenceCount(configuration)).isEqualTo(1);
- RocketMQLiteAckClient.release(configuration);
-
assertThat(RocketMQLiteAckClient.getReferenceCount(configuration)).isZero();
+ RocketMQAckClient.release(configuration);
+
assertThat(RocketMQAckClient.getReferenceCount(configuration)).isZero();
}
@Test
@@ -58,25 +58,25 @@ class RocketMQLiteAckClientTest {
final Configuration a = config("host-a:8080");
final Configuration b = config("host-b:8080");
- final RocketMQLiteAckClient clientA = RocketMQLiteAckClient.acquire(a);
- final RocketMQLiteAckClient clientB = RocketMQLiteAckClient.acquire(b);
+ final RocketMQAckClient clientA = RocketMQAckClient.acquire(a);
+ final RocketMQAckClient clientB = RocketMQAckClient.acquire(b);
try {
assertThat(clientA).isNotSameAs(clientB);
-
assertThat(RocketMQLiteAckClient.getReferenceCount(a)).isEqualTo(1);
-
assertThat(RocketMQLiteAckClient.getReferenceCount(b)).isEqualTo(1);
+ assertThat(RocketMQAckClient.getReferenceCount(a)).isEqualTo(1);
+ assertThat(RocketMQAckClient.getReferenceCount(b)).isEqualTo(1);
} finally {
- RocketMQLiteAckClient.release(a);
- RocketMQLiteAckClient.release(b);
+ RocketMQAckClient.release(a);
+ RocketMQAckClient.release(b);
}
- assertThat(RocketMQLiteAckClient.getReferenceCount(a)).isZero();
- assertThat(RocketMQLiteAckClient.getReferenceCount(b)).isZero();
+ assertThat(RocketMQAckClient.getReferenceCount(a)).isZero();
+ assertThat(RocketMQAckClient.getReferenceCount(b)).isZero();
}
@Test
void releaseWithoutAcquireIsANoOp() {
final Configuration configuration = config("no-acquire:8080");
- RocketMQLiteAckClient.release(configuration);
-
assertThat(RocketMQLiteAckClient.getReferenceCount(configuration)).isZero();
+ RocketMQAckClient.release(configuration);
+
assertThat(RocketMQAckClient.getReferenceCount(configuration)).isZero();
}
}
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilderTest.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilderTest.java
index 21a8c6a..1f78bda 100644
---
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilderTest.java
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/RocketMQGrpcSourceBuilderTest.java
@@ -34,6 +34,7 @@ class RocketMQGrpcSourceBuilderTest {
RocketMQGrpcSource.<String>builder()
.setEndpoints("127.0.0.1:8080")
.setConsumerGroup("group")
+ .setMode(ConsumerMode.LITE)
.setMainTopic("topic")
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
@@ -46,6 +47,7 @@ class RocketMQGrpcSourceBuilderTest {
() ->
RocketMQGrpcSource.<String>builder()
.setConsumerGroup("group")
+ .setMode(ConsumerMode.LITE)
.setMainTopic("topic")
.setValueOnlyDeserializer(new
SimpleStringSchema())
.build())
@@ -59,6 +61,7 @@ class RocketMQGrpcSourceBuilderTest {
() ->
RocketMQGrpcSource.<String>builder()
.setEndpoints("127.0.0.1:8080")
+ .setMode(ConsumerMode.LITE)
.setMainTopic("topic")
.setValueOnlyDeserializer(new
SimpleStringSchema())
.build())
@@ -73,6 +76,7 @@ class RocketMQGrpcSourceBuilderTest {
RocketMQGrpcSource.<String>builder()
.setEndpoints("127.0.0.1:8080")
.setConsumerGroup("group")
+ .setMode(ConsumerMode.LITE)
.setValueOnlyDeserializer(new
SimpleStringSchema())
.build())
.isInstanceOf(IllegalArgumentException.class)
@@ -86,9 +90,68 @@ class RocketMQGrpcSourceBuilderTest {
RocketMQGrpcSource.<String>builder()
.setEndpoints("127.0.0.1:8080")
.setConsumerGroup("group")
+ .setMode(ConsumerMode.LITE)
.setMainTopic("topic")
.build())
.isInstanceOf(NullPointerException.class)
.hasMessageContaining("deserializer");
}
+
+ @Test
+ void buildSucceedsWithSimpleModeTest() {
+ final RocketMQGrpcSource<String> source =
+ RocketMQGrpcSource.<String>builder()
+ .setEndpoints("127.0.0.1:8080")
+ .setConsumerGroup("group")
+ .setMode(ConsumerMode.SIMPLE)
+ .setTopic("normal-topic")
+ .setFilterExpression("tagA||tagB")
+ .setValueOnlyDeserializer(new SimpleStringSchema())
+ .build();
+ assertThat(source).isNotNull();
+ }
+
+ @Test
+ void buildFailsWithSimpleModeWithoutTopicTest() {
+ assertThatThrownBy(
+ () ->
+ RocketMQGrpcSource.<String>builder()
+ .setEndpoints("127.0.0.1:8080")
+ .setConsumerGroup("group")
+ .setMode(ConsumerMode.SIMPLE)
+ .setValueOnlyDeserializer(new
SimpleStringSchema())
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("topic");
+ }
+
+ @Test
+ void buildFailsWithLiteModeWhenOnlySimpleTopicIsSetTest() {
+ assertThatThrownBy(
+ () ->
+ RocketMQGrpcSource.<String>builder()
+ .setEndpoints("127.0.0.1:8080")
+ .setConsumerGroup("group")
+ .setMode(ConsumerMode.LITE)
+ .setTopic("normal-topic")
+ .setValueOnlyDeserializer(new
SimpleStringSchema())
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("main topic");
+ }
+
+ @Test
+ void buildDefaultsToSimpleModeTest() {
+ // Without setMode() the builder must run in SIMPLE mode: a LITE-mode
build would reject
+ // the missing main topic.
+ final RocketMQGrpcSource<String> source =
+ RocketMQGrpcSource.<String>builder()
+ .setEndpoints("127.0.0.1:8080")
+ .setConsumerGroup("group")
+ .setTopic("normal-topic")
+ .setValueOnlyDeserializer(new SimpleStringSchema())
+ .build();
+ assertThat(source).isNotNull();
+
assertThat(RocketMQGrpcSourceOptions.MODE.defaultValue()).isEqualTo(ConsumerMode.SIMPLE);
+ }
}
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/CheckpointAckTrackerTest.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/CheckpointAckTrackerTest.java
new file mode 100644
index 0000000..6604a61
--- /dev/null
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/CheckpointAckTrackerTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.grpc.source.reader;
+
+import org.apache.flink.connector.rocketmq.grpc.ack.RocketMQReceiptHandle;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link CheckpointAckTracker}. */
+class CheckpointAckTrackerTest {
+
+ @Test
+ void completeUpToReturnsSnapshottedHandlesTest() {
+ final CheckpointAckTracker tracker = new CheckpointAckTracker();
+ tracker.add(handle(1));
+ tracker.add(handle(2));
+ tracker.snapshot(7L);
+
+ final List<RocketMQReceiptHandle> completed = tracker.completeUpTo(7L);
+ assertThat(completed).containsExactly(handle(1), handle(2));
+ }
+
+ @Test
+ void completeUpToKeepsLaterCheckpointsTest() {
+ final CheckpointAckTracker tracker = new CheckpointAckTracker();
+ tracker.add(handle(1));
+ tracker.snapshot(7L);
+ tracker.add(handle(2));
+ tracker.snapshot(8L);
+
+ assertThat(tracker.completeUpTo(7L)).containsExactly(handle(1));
+ assertThat(tracker.completeUpTo(8L)).containsExactly(handle(2));
+ assertThat(tracker.completeUpTo(9L)).isEmpty();
+ }
+
+ @Test
+ void completeUpToMergesAllCheckpointsUpToIdTest() {
+ final CheckpointAckTracker tracker = new CheckpointAckTracker();
+ tracker.add(handle(1));
+ tracker.snapshot(1L);
+ tracker.add(handle(2));
+ tracker.snapshot(2L);
+
+ assertThat(tracker.completeUpTo(2L)).containsExactly(handle(1),
handle(2));
+ }
+
+ @Test
+ void handlesAddedAfterSnapshotGoToNextCheckpointTest() {
+ final CheckpointAckTracker tracker = new CheckpointAckTracker();
+ tracker.snapshot(1L);
+ tracker.add(handle(1));
+
+ assertThat(tracker.completeUpTo(1L)).isEmpty();
+ tracker.snapshot(2L);
+ assertThat(tracker.completeUpTo(2L)).containsExactly(handle(1));
+ }
+
+ @Test
+ void emptySnapshotIsNotRecordedTest() {
+ final CheckpointAckTracker tracker = new CheckpointAckTracker();
+ tracker.snapshot(1L);
+ tracker.add(handle(1));
+ tracker.snapshot(2L);
+
+ // Checkpoint 1 carried nothing; handle(1) belongs to checkpoint 2
only.
+ assertThat(tracker.completeUpTo(1L)).isEmpty();
+ assertThat(tracker.completeUpTo(2L)).containsExactly(handle(1));
+ }
+
+ private static RocketMQReceiptHandle handle(int id) {
+ return new RocketMQReceiptHandle(
+ "127.0.0.1:8080", "", "group", "topic", null, "msg-" + id,
"receipt-" + id, 1);
+ }
+}
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumerProviderTest.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumerProviderTest.java
new file mode 100644
index 0000000..cfa76ad
--- /dev/null
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/PopConsumerProviderTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.grpc.source.reader;
+
+import org.apache.rocketmq.client.apis.consumer.LiteSimpleConsumer;
+import org.apache.rocketmq.client.apis.consumer.SimpleConsumer;
+import org.apache.rocketmq.client.apis.message.MessageView;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that the {@link PopConsumer} adapters of {@link PopConsumerProvider}
delegate to the
+ * wrapped SDK consumer. Building a real consumer requires a live proxy, so
the SDK interfaces are
+ * stubbed with dynamic proxies that only record the invoked method and its
arguments.
+ */
+class PopConsumerProviderTest {
+
+ @Test
+ void litePopConsumerDelegatesAckTest() throws Exception {
+ final List<Invocation> invocations = new ArrayList<>();
+ final PopConsumer consumer =
+
PopConsumerProvider.wrap(recordingProxy(LiteSimpleConsumer.class, invocations));
+
+ final MessageView messageView = messageView();
+ consumer.ack(messageView);
+
+ assertThat(invocations).hasSize(1);
+ assertThat(invocations.get(0).method).isEqualTo("ack");
+ assertThat(invocations.get(0).argument).isSameAs(messageView);
+ }
+
+ @Test
+ void simplePopConsumerDelegatesAckTest() throws Exception {
+ final List<Invocation> invocations = new ArrayList<>();
+ final PopConsumer consumer =
+ PopConsumerProvider.wrap(recordingProxy(SimpleConsumer.class,
invocations));
+
+ final MessageView messageView = messageView();
+ consumer.ack(messageView);
+
+ assertThat(invocations).hasSize(1);
+ assertThat(invocations.get(0).method).isEqualTo("ack");
+ assertThat(invocations.get(0).argument).isSameAs(messageView);
+ }
+
+ private static <T> T recordingProxy(Class<T> type, List<Invocation>
invocations) {
+ return type.cast(
+ Proxy.newProxyInstance(
+ type.getClassLoader(),
+ new Class<?>[] {type},
+ (proxy, method, args) -> {
+ invocations.add(
+ new Invocation(
+ method.getName(), args == null ?
null : args[0]));
+ return null;
+ }));
+ }
+
+ private static MessageView messageView() {
+ return (MessageView)
+ Proxy.newProxyInstance(
+ MessageView.class.getClassLoader(),
+ new Class<?>[] {MessageView.class},
+ (proxy, method, args) -> {
+ throw new UnsupportedOperationException();
+ });
+ }
+
+ private static final class Invocation {
+
+ private final String method;
+ private final Object argument;
+
+ private Invocation(String method, Object argument) {
+ this.method = method;
+ this.argument = argument;
+ }
+ }
+}
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceSplitReaderTest.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceSplitReaderTest.java
new file mode 100644
index 0000000..ab635c3
--- /dev/null
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/source/reader/RocketMQGrpcSourceSplitReaderTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.grpc.source.reader;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.connector.rocketmq.grpc.ack.RocketMQReceiptHandle;
+
+import org.apache.rocketmq.client.apis.ClientException;
+import org.apache.rocketmq.client.apis.message.MessageView;
+import org.apache.rocketmq.client.java.message.MessageIdCodec;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for the SIMPLE-mode acknowledgement path of {@link
RocketMQGrpcSourceSplitReader}. */
+class RocketMQGrpcSourceSplitReaderTest {
+
+ @Test
+ void ackBeforeStartThrowsTest() throws Exception {
+ final RocketMQGrpcSourceSplitReader reader =
+ new RocketMQGrpcSourceSplitReader(new Configuration());
+ try {
+ assertThatThrownBy(() -> reader.ack(handle()))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("not started yet");
+ } finally {
+ reader.close();
+ }
+ }
+
+ @Test
+ void ackAfterCloseThrowsTest() throws Exception {
+ final RocketMQGrpcSourceSplitReader reader =
+ new RocketMQGrpcSourceSplitReader(new Configuration());
+ final RecordingPopConsumer consumer = new RecordingPopConsumer();
+ reader.setConsumer(consumer);
+ reader.close();
+
+ assertThatThrownBy(() -> reader.ack(handle()))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("already closed");
+ assertThat(consumer.acked).isEmpty();
+ }
+
+ @Test
+ void ackDelegatesRebuiltMessageViewToConsumerTest() throws Exception {
+ final RocketMQGrpcSourceSplitReader reader =
+ new RocketMQGrpcSourceSplitReader(new Configuration());
+ final RecordingPopConsumer consumer = new RecordingPopConsumer();
+ reader.setConsumer(consumer);
+
+ final RocketMQReceiptHandle handle = handle();
+ try {
+ reader.ack(handle);
+ } finally {
+ reader.close();
+ }
+
+ assertThat(consumer.acked).hasSize(1);
+ final org.apache.rocketmq.client.java.message.MessageViewImpl acked =
+ (org.apache.rocketmq.client.java.message.MessageViewImpl)
consumer.acked.get(0);
+ assertThat(acked.getMessageId())
+
.isEqualTo(MessageIdCodec.getInstance().decode(handle.getMessageId()));
+ assertThat(acked.getTopic()).isEqualTo(handle.getTopic());
+
assertThat(acked.getReceiptHandle()).isEqualTo(handle.getReceiptHandle());
+ }
+
+ private static RocketMQReceiptHandle handle() {
+ return new RocketMQReceiptHandle(
+ "127.0.0.1:8081",
+ "ns-1",
+ "GID-test",
+ "normal-topic",
+ null,
+ MessageIdCodec.getInstance().nextMessageId().toString(),
+ "receipt-handle-abc",
+ 1);
+ }
+
+ private static final class RecordingPopConsumer implements PopConsumer {
+
+ private final List<MessageView> acked = new ArrayList<>();
+
+ @Override
+ public List<MessageView> receive(int maxMessageNum, Duration
invisibleDuration)
+ throws ClientException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void ack(MessageView messageView) throws ClientException {
+ acked.add(messageView);
+ }
+
+ @Override
+ public void changeInvisibleDuration(MessageView messageView, Duration
invisibleDuration)
+ throws ClientException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() throws IOException {}
+ }
+}
diff --git
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactoryTest.java
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactoryTest.java
index 67aa3d4..8db4bc6 100644
---
a/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactoryTest.java
+++
b/flink-connector-rocketmq-grpc/src/test/java/org/apache/flink/connector/rocketmq/grpc/table/RocketMQGrpcDynamicTableFactoryTest.java
@@ -53,6 +53,7 @@ class RocketMQGrpcDynamicTableFactoryTest {
@Test
void createTableSourceTest() {
+ // No 'source.mode' is configured, so the source runs in the default
'simple' mode.
final Map<String, String> options = baseOptions();
options.put("source.consumer-group", "test-group");
@@ -60,6 +61,16 @@ class RocketMQGrpcDynamicTableFactoryTest {
assertThat(source).isInstanceOf(RocketMQGrpcDynamicTableSource.class);
}
+ @Test
+ void createLiteModeTableSourceTest() {
+ final Map<String, String> options = baseOptions();
+ options.put("source.consumer-group", "test-group");
+ options.put("source.mode", "lite");
+
+ final DynamicTableSource source =
FactoryMocks.createTableSource(SCHEMA, options);
+ assertThat(source).isInstanceOf(RocketMQGrpcDynamicTableSource.class);
+ }
+
@Test
void createTableSinkTest() {
final DynamicTableSink sink = FactoryMocks.createTableSink(SCHEMA,
baseOptions());
@@ -80,4 +91,25 @@ class RocketMQGrpcDynamicTableFactoryTest {
assertThatThrownBy(() -> FactoryMocks.createTableSink(SCHEMA, options))
.hasStackTraceContaining("lite-topic");
}
+
+ @Test
+ void createSimpleModeTableSourceTest() {
+ final Map<String, String> options = baseOptions();
+ options.put("source.consumer-group", "test-group");
+ options.put("source.mode", "simple");
+ options.put("source.filter-expression", "tagA||tagB");
+
+ final DynamicTableSource source =
FactoryMocks.createTableSource(SCHEMA, options);
+ assertThat(source).isInstanceOf(RocketMQGrpcDynamicTableSource.class);
+ }
+
+ @Test
+ void createTableSourceFailsWithInvalidModeTest() {
+ final Map<String, String> options = baseOptions();
+ options.put("source.consumer-group", "test-group");
+ options.put("source.mode", "bogus");
+
+ assertThatThrownBy(() -> FactoryMocks.createTableSource(SCHEMA,
options))
+ .hasStackTraceContaining("bogus");
+ }
}
diff --git a/flink-connector-rocketmq/pom.xml b/flink-connector-rocketmq/pom.xml
index 8f71191..b022b86 100644
--- a/flink-connector-rocketmq/pom.xml
+++ b/flink-connector-rocketmq/pom.xml
@@ -26,7 +26,7 @@ under the License.
<parent>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-rocketmq-parent</artifactId>
- <version>1.0.0-SNAPSHOT</version>
+ <version>3.0.0</version>
</parent>
<artifactId>flink-connector-rocketmq</artifactId>
diff --git a/flink-sql-connector-rocketmq-grpc/pom.xml
b/flink-sql-connector-rocketmq-grpc/pom.xml
index 963169a..92444ab 100644
--- a/flink-sql-connector-rocketmq-grpc/pom.xml
+++ b/flink-sql-connector-rocketmq-grpc/pom.xml
@@ -26,7 +26,7 @@ under the License.
<parent>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-rocketmq-parent</artifactId>
- <version>1.0.0-SNAPSHOT</version>
+ <version>3.0.0</version>
</parent>
<artifactId>flink-sql-connector-rocketmq-grpc</artifactId>
diff --git a/flink-sql-connector-rocketmq/pom.xml
b/flink-sql-connector-rocketmq/pom.xml
index 1724963..ad3bc8f 100644
--- a/flink-sql-connector-rocketmq/pom.xml
+++ b/flink-sql-connector-rocketmq/pom.xml
@@ -26,7 +26,7 @@ under the License.
<parent>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-rocketmq-parent</artifactId>
- <version>1.0.0-SNAPSHOT</version>
+ <version>3.0.0</version>
</parent>
<artifactId>flink-sql-connector-rocketmq</artifactId>
diff --git a/pom.xml b/pom.xml
index 7570a25..377451f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@ under the License.
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-rocketmq-parent</artifactId>
- <version>1.0.0-SNAPSHOT</version>
+ <version>3.0.0</version>
<name>Flink : Connectors : RocketMQ : Parent</name>
<packaging>pom</packaging>
<url>https://flink.apache.org</url>