From d2f95889b4852251ba5c526ef790ea8735b42100 Mon Sep 17 00:00:00 2001 From: imzs Date: Tue, 10 Dec 2024 11:04:06 +0800 Subject: [PATCH] [ISSUE #878] The java implementation of recalling API (#879) --- .github/workflows/java_coverage.yml | 1 + README-CN.md | 19 ++++---- README.md | 1 + .../client/apis/producer/Producer.java | 25 +++++++++++ .../client/apis/producer/RecallReceipt.java | 24 ++++++++++ .../client/apis/producer/SendReceipt.java | 3 ++ .../client/java/impl/ClientManager.java | 12 +++++ .../client/java/impl/ClientManagerImpl.java | 17 +++++++ .../java/impl/producer/ProducerImpl.java | 45 +++++++++++++++++++ .../java/impl/producer/RecallReceiptImpl.java | 43 ++++++++++++++++++ .../java/impl/producer/SendReceiptImpl.java | 14 +++++- .../rocketmq/client/java/rpc/RpcClient.java | 14 ++++++ .../client/java/rpc/RpcClientImpl.java | 10 +++++ .../java/impl/ClientManagerImplTest.java | 9 ++++ .../java/impl/producer/ProducerImplTest.java | 41 +++++++++++++++++ 15 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/RecallReceipt.java create mode 100644 java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/RecallReceiptImpl.java diff --git a/.github/workflows/java_coverage.yml b/.github/workflows/java_coverage.yml index 0ad1e25ed..cd9289d77 100644 --- a/.github/workflows/java_coverage.yml +++ b/.github/workflows/java_coverage.yml @@ -30,4 +30,5 @@ jobs: with: flags: java fail_ci_if_error: true + token: e7eb01be-398b-4f7f-a73e-dc35c428cb50 verbose: true diff --git a/README-CN.md b/README-CN.md index 25131b36d..5ae40d3ed 100644 --- a/README-CN.md +++ b/README-CN.md @@ -17,15 +17,16 @@ * 可用 - ✅ * 进行中 - 🚧 -| 特性 | Java | C/C++ | C# | Golang | Rust | Python | Node.js | PHP | -| ---------------------------------------------- | :---: | :---: | :---: | :----: | :---: | :----: | :-----: | :---: | -| Producer with standard messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| Producer with FIFO messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| Producer with timed/delay messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| Producer with transactional messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| Simple consumer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| Push consumer with concurrent message listener | ✅ | ✅ | 🚧 | 🚧 | ✅ | 🚧 | 🚧 | 🚧 | -| Push consumer with FIFO message listener | ✅ | ✅ | 🚧 | 🚧 | ✅ | 🚧 | 🚧 | 🚧 | +| 特性 | Java | C/C++ | C# | Golang | Rust | Python | Node.js | PHP | +|------------------------------------------------| :---: |:------:|:-----:|:------:|:----:|:------:|:-------:| :---: | +| Producer with standard messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| Producer with FIFO messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| Producer with timed/delay messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| Producer with transactional messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| Producer with recalling timed/delay messages | ✅ | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 | +| Simple consumer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| Push consumer with concurrent message listener | ✅ | ✅ | 🚧 | 🚧 | ✅ | 🚧 | 🚧 | 🚧 | +| Push consumer with FIFO message listener | ✅ | ✅ | 🚧 | 🚧 | ✅ | 🚧 | 🚧 | 🚧 | ## 先决条件和构建 diff --git a/README.md b/README.md index 0772a1726..937d9c314 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Provide cloud-native and robust solutions for Java, C++, C#, Golang, Rust and al | Producer with FIFO messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | | Producer with timed/delay messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | | Producer with transactional messages | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| Producer with recalling timed/delay messages | ✅ | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 | | Simple consumer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | | Push consumer with concurrent message listener | ✅ | ✅ | 🚧 | 🚧 | ✅ | 🚧 | 🚧 | 🚧 | | Push consumer with FIFO message listener | ✅ | ✅ | 🚧 | 🚧 | ✅ | 🚧 | 🚧 | 🚧 | diff --git a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/Producer.java b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/Producer.java index 71a3a57bb..37d127254 100644 --- a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/Producer.java +++ b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/Producer.java @@ -76,6 +76,31 @@ public interface Producer extends Closeable { */ Transaction beginTransaction() throws ClientException; + /** + * Recall message synchronously, only delay message is supported for now. + * + *
{@code
+     * SendReceipt receipt = producer.send(message);
+     * String recallHandle = receipt.getRecallHandle();
+     * }
+ * + * @param topic the topic of the operation + * @param recallHandle the handle to identify a message to recall + * @return the returned receipt, or throw exception if response status is not OK. + */ + RecallReceipt recallMessage(String topic, String recallHandle) throws ClientException; + + /** + * Recall message asynchronously. + * + *

This method returns immediately, the result is included in the {@link CompletableFuture}; + * + * @param topic the topic of the operation + * @param recallHandle the handle to identify a message to recall + * @return a future that indicates the receipt + */ + CompletableFuture recallMessageAsync(String topic, String recallHandle); + /** * Closes the producer and releases all related resources. * diff --git a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/RecallReceipt.java b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/RecallReceipt.java new file mode 100644 index 000000000..7ebeb54b4 --- /dev/null +++ b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/RecallReceipt.java @@ -0,0 +1,24 @@ +/* + * 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.rocketmq.client.apis.producer; + +import org.apache.rocketmq.client.apis.message.MessageId; + +public interface RecallReceipt { + MessageId getMessageId(); +} diff --git a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/SendReceipt.java b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/SendReceipt.java index dc96f55fa..794da0872 100644 --- a/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/SendReceipt.java +++ b/java/client-apis/src/main/java/org/apache/rocketmq/client/apis/producer/SendReceipt.java @@ -24,4 +24,7 @@ */ public interface SendReceipt { MessageId getMessageId(); + + // Unique handle to identify a message to recall, only delay message is supported for now + String getRecallHandle(); } diff --git a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManager.java b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManager.java index ce7d2e1b0..e9ff20763 100644 --- a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManager.java +++ b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManager.java @@ -33,6 +33,8 @@ import apache.rocketmq.v2.QueryAssignmentResponse; import apache.rocketmq.v2.QueryRouteRequest; import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; import apache.rocketmq.v2.ReceiveMessageRequest; import apache.rocketmq.v2.ReceiveMessageResponse; import apache.rocketmq.v2.SendMessageRequest; @@ -170,6 +172,16 @@ public abstract RpcFuture endTran public abstract RpcFuture notifyClientTermination(Endpoints endpoints, NotifyClientTerminationRequest request, Duration duration); + /** + * recall message asynchronously, the method ensures no throwable. + * @param endpoints request endpoints. + * @param request recall message request. + * @param duration request max duration. + * @return invocation of response future. + */ + public abstract RpcFuture recallMessage(Endpoints endpoints, + RecallMessageRequest request, Duration duration); + /** * Establish telemetry session stream to server. * diff --git a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java index 7630fefba..76cfb617b 100644 --- a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java +++ b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/ClientManagerImpl.java @@ -33,6 +33,8 @@ import apache.rocketmq.v2.QueryAssignmentResponse; import apache.rocketmq.v2.QueryRouteRequest; import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; import apache.rocketmq.v2.ReceiveMessageRequest; import apache.rocketmq.v2.ReceiveMessageResponse; import apache.rocketmq.v2.SendMessageRequest; @@ -344,6 +346,21 @@ public RpcFuture endTransaction(E } } + @Override + public RpcFuture recallMessage(Endpoints endpoints, + RecallMessageRequest request, Duration duration) { + try { + final Metadata metadata = client.sign(); + final Context context = new Context(endpoints, metadata); + final RpcClient rpcClient = getRpcClient(endpoints); + final ListenableFuture future = + rpcClient.recallMessage(metadata, request, asyncWorker, duration); + return new RpcFuture<>(context, request, future); + } catch (Throwable t) { + return new RpcFuture<>(t); + } + } + @Override public StreamObserver telemetry(Endpoints endpoints, Duration duration, StreamObserver responseObserver) throws ClientException { diff --git a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/ProducerImpl.java b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/ProducerImpl.java index a8021770b..1945639b3 100644 --- a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/ProducerImpl.java +++ b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/ProducerImpl.java @@ -23,6 +23,8 @@ import apache.rocketmq.v2.EndTransactionResponse; import apache.rocketmq.v2.HeartbeatRequest; import apache.rocketmq.v2.NotifyClientTerminationRequest; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; import apache.rocketmq.v2.RecoverOrphanedTransactionCommand; import apache.rocketmq.v2.SendMessageRequest; import apache.rocketmq.v2.SendMessageResponse; @@ -50,16 +52,19 @@ import java.util.function.Function; import java.util.stream.Collectors; import net.javacrumbs.futureconverter.java8guava.FutureConverter; +import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.apis.ClientConfiguration; import org.apache.rocketmq.client.apis.ClientException; import org.apache.rocketmq.client.apis.message.Message; import org.apache.rocketmq.client.apis.message.MessageId; import org.apache.rocketmq.client.apis.producer.Producer; +import org.apache.rocketmq.client.apis.producer.RecallReceipt; import org.apache.rocketmq.client.apis.producer.SendReceipt; import org.apache.rocketmq.client.apis.producer.Transaction; import org.apache.rocketmq.client.apis.producer.TransactionChecker; import org.apache.rocketmq.client.apis.producer.TransactionResolution; import org.apache.rocketmq.client.java.exception.InternalErrorException; +import org.apache.rocketmq.client.java.exception.StatusChecker; import org.apache.rocketmq.client.java.exception.TooManyRequestsException; import org.apache.rocketmq.client.java.hook.MessageHookPoints; import org.apache.rocketmq.client.java.hook.MessageHookPointsStatus; @@ -250,6 +255,18 @@ public Transaction beginTransaction() { return new TransactionImpl(this); } + @Override + public RecallReceipt recallMessage(String topic, String recallHandle) throws ClientException { + final ListenableFuture future = recallMessage0(topic, recallHandle); + return handleClientFuture(future); + } + + @Override + public CompletableFuture recallMessageAsync(String topic, String recallHandle) { + final ListenableFuture future = recallMessage0(topic, recallHandle); + return FutureConverter.toCompletableFuture(future); + } + @Override public void close() { this.stopAsync().awaitTerminated(); @@ -561,4 +578,32 @@ private ListenableFuture getPublishingLoadBalancer(final return Futures.transform(getRouteData(topic), topicRouteData -> updatePublishingLoadBalancer(topic, topicRouteData), MoreExecutors.directExecutor()); } + + ListenableFuture recallMessage0(String topic, String recallHandle) { + if (!this.isRunning()) { + final IllegalStateException e = new IllegalStateException("Producer is not running now"); + log.error("Unable to recall message because producer is not running, state={}, clientId={}", + this.state(), clientId); + return Futures.immediateFailedFuture(e); + } + if (StringUtils.isEmpty(recallHandle)) { + return Futures.immediateFailedFuture(new IllegalArgumentException("recall handle is invalid")); + } + final RecallMessageRequest request = RecallMessageRequest.newBuilder() + .setTopic(apache.rocketmq.v2.Resource.newBuilder() + .setResourceNamespace(clientConfiguration.getNamespace()) + .setName(topic) + .build()) + .setRecallHandle(recallHandle) + .build(); + final Duration requestTimeout = clientConfiguration.getRequestTimeout(); + final RpcFuture future = + this.getClientManager().recallMessage(endpoints, request, requestTimeout); + + return Futures.transformAsync(future, response -> { + final Status status = response.getStatus(); + StatusChecker.check(status, future); + return Futures.immediateFuture(new RecallReceiptImpl(response.getMessageId())); + }, MoreExecutors.directExecutor()); + } } diff --git a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/RecallReceiptImpl.java b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/RecallReceiptImpl.java new file mode 100644 index 000000000..b6ba0ed6b --- /dev/null +++ b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/RecallReceiptImpl.java @@ -0,0 +1,43 @@ +/* + * 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.rocketmq.client.java.impl.producer; + +import com.google.common.base.MoreObjects; +import org.apache.rocketmq.client.apis.message.MessageId; +import org.apache.rocketmq.client.apis.producer.RecallReceipt; +import org.apache.rocketmq.client.java.message.MessageIdCodec; + +public class RecallReceiptImpl implements RecallReceipt { + private final MessageId messageId; + + public RecallReceiptImpl(String messageIdStr) { + messageId = MessageIdCodec.getInstance().decode(messageIdStr); + } + + @Override + public MessageId getMessageId() { + return messageId; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("messageId", messageId) + .toString(); + } +} diff --git a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/SendReceiptImpl.java b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/SendReceiptImpl.java index 5f9f403f7..ed2fbf79c 100644 --- a/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/SendReceiptImpl.java +++ b/java/client/src/main/java/org/apache/rocketmq/client/java/impl/producer/SendReceiptImpl.java @@ -39,14 +39,17 @@ public class SendReceiptImpl implements SendReceipt { private final MessageId messageId; private final String transactionId; + private final String recallHandle; private final MessageQueueImpl messageQueue; private final long offset; - private SendReceiptImpl(MessageId messageId, String transactionId, MessageQueueImpl messageQueue, long offset) { + private SendReceiptImpl(MessageId messageId, String transactionId, + MessageQueueImpl messageQueue, long offset, String recallHandle) { this.messageId = messageId; this.transactionId = transactionId; this.messageQueue = messageQueue; this.offset = offset; + this.recallHandle = recallHandle; } @Override @@ -54,6 +57,11 @@ public MessageId getMessageId() { return messageId; } + @Override + public String getRecallHandle() { + return recallHandle; + } + public MessageQueueImpl getMessageQueue() { return messageQueue; } @@ -87,7 +95,8 @@ public static List processResponseInvocation(MessageQueueImpl m final MessageId messageId = MessageIdCodec.getInstance().decode(entry.getMessageId()); final String transactionId = entry.getTransactionId(); final long offset = entry.getOffset(); - final SendReceiptImpl impl = new SendReceiptImpl(messageId, transactionId, mq, offset); + final String recallHandle = entry.getRecallHandle(); + final SendReceiptImpl impl = new SendReceiptImpl(messageId, transactionId, mq, offset, recallHandle); sendReceipts.add(impl); } return sendReceipts; @@ -97,6 +106,7 @@ public static List processResponseInvocation(MessageQueueImpl m public String toString() { return MoreObjects.toStringHelper(this) .add("messageId", messageId) + .add("recallHandle", recallHandle) .toString(); } } diff --git a/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClient.java b/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClient.java index 4c5e8b76b..c555082f8 100644 --- a/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClient.java +++ b/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClient.java @@ -33,6 +33,8 @@ import apache.rocketmq.v2.QueryAssignmentResponse; import apache.rocketmq.v2.QueryRouteRequest; import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; import apache.rocketmq.v2.ReceiveMessageRequest; import apache.rocketmq.v2.ReceiveMessageResponse; import apache.rocketmq.v2.SendMessageRequest; @@ -186,6 +188,18 @@ ListenableFuture endTransaction(Metadata metadata, ListenableFuture notifyClientTermination(Metadata metadata, NotifyClientTerminationRequest request, Executor executor, Duration duration); + /** + * Recall message asynchronously. + * + * @param metadata gRPC request header metadata. + * @param request recall message request + * @param executor gRPC asynchronous executor. + * @param duration request max duration. + * @return invocation of response future. + */ + ListenableFuture recallMessage(Metadata metadata, + RecallMessageRequest request, Executor executor, Duration duration); + /** * Start a streaming request and get the request observer. * diff --git a/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClientImpl.java b/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClientImpl.java index a40cd3989..77c2184ed 100644 --- a/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClientImpl.java +++ b/java/client/src/main/java/org/apache/rocketmq/client/java/rpc/RpcClientImpl.java @@ -34,6 +34,8 @@ import apache.rocketmq.v2.QueryAssignmentResponse; import apache.rocketmq.v2.QueryRouteRequest; import apache.rocketmq.v2.QueryRouteResponse; +import apache.rocketmq.v2.RecallMessageRequest; +import apache.rocketmq.v2.RecallMessageResponse; import apache.rocketmq.v2.ReceiveMessageRequest; import apache.rocketmq.v2.ReceiveMessageResponse; import apache.rocketmq.v2.SendMessageRequest; @@ -212,6 +214,14 @@ public ListenableFuture notifyClientTermination .withDeadlineAfter(duration.toNanos(), TimeUnit.NANOSECONDS).notifyClientTermination(request); } + @Override + public ListenableFuture recallMessage(Metadata metadata, + RecallMessageRequest request, Executor executor, Duration duration) { + this.activityNanoTime = System.nanoTime(); + return futureStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)).withExecutor(executor) + .withDeadlineAfter(duration.toNanos(), TimeUnit.NANOSECONDS).recallMessage(request); + } + @Override public StreamObserver telemetry(Metadata metadata, Executor executor, Duration duration, StreamObserver responseObserver) { diff --git a/java/client/src/test/java/org/apache/rocketmq/client/java/impl/ClientManagerImplTest.java b/java/client/src/test/java/org/apache/rocketmq/client/java/impl/ClientManagerImplTest.java index 46d82339a..b6ac2ec36 100644 --- a/java/client/src/test/java/org/apache/rocketmq/client/java/impl/ClientManagerImplTest.java +++ b/java/client/src/test/java/org/apache/rocketmq/client/java/impl/ClientManagerImplTest.java @@ -25,6 +25,7 @@ import apache.rocketmq.v2.NotifyClientTerminationRequest; import apache.rocketmq.v2.QueryAssignmentRequest; import apache.rocketmq.v2.QueryRouteRequest; +import apache.rocketmq.v2.RecallMessageRequest; import apache.rocketmq.v2.ReceiveMessageRequest; import apache.rocketmq.v2.SendMessageRequest; import io.grpc.Metadata; @@ -135,4 +136,12 @@ public void testNotifyClientTermination() { CLIENT_MANAGER.notifyClientTermination(null, request, Duration.ofSeconds(1)); // Expect no exception thrown. } + + @Test + public void testRecallMessage() { + RecallMessageRequest request = RecallMessageRequest.newBuilder().build(); + CLIENT_MANAGER.recallMessage(fakeEndpoints(), request, Duration.ofSeconds(1)); + CLIENT_MANAGER.recallMessage(null, request, Duration.ofSeconds(1)); + // Expect no exception thrown. + } } \ No newline at end of file diff --git a/java/client/src/test/java/org/apache/rocketmq/client/java/impl/producer/ProducerImplTest.java b/java/client/src/test/java/org/apache/rocketmq/client/java/impl/producer/ProducerImplTest.java index 60bb0fefd..20533a8c7 100644 --- a/java/client/src/test/java/org/apache/rocketmq/client/java/impl/producer/ProducerImplTest.java +++ b/java/client/src/test/java/org/apache/rocketmq/client/java/impl/producer/ProducerImplTest.java @@ -24,10 +24,13 @@ import static org.mockito.Mockito.verify; import apache.rocketmq.v2.Broker; +import apache.rocketmq.v2.Code; import apache.rocketmq.v2.MessageQueue; import apache.rocketmq.v2.MessageType; import apache.rocketmq.v2.Permission; +import apache.rocketmq.v2.RecallMessageResponse; import apache.rocketmq.v2.Resource; +import apache.rocketmq.v2.Status; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Service; import java.util.ArrayList; @@ -38,10 +41,15 @@ import org.apache.rocketmq.client.apis.ClientConfiguration; import org.apache.rocketmq.client.apis.ClientException; import org.apache.rocketmq.client.apis.message.Message; +import org.apache.rocketmq.client.java.exception.InternalErrorException; +import org.apache.rocketmq.client.java.impl.ClientManagerImpl; +import org.apache.rocketmq.client.java.message.MessageIdCodec; import org.apache.rocketmq.client.java.route.Endpoints; import org.apache.rocketmq.client.java.route.MessageQueueImpl; import org.apache.rocketmq.client.java.route.TopicRouteData; +import org.apache.rocketmq.client.java.rpc.RpcFuture; import org.apache.rocketmq.client.java.tool.TestBase; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -109,4 +117,37 @@ public void testSendFailureWithTopic() throws ClientException { verify(producer, times(maxAttempts)).send0(any(Endpoints.class), anyList(), any(MessageQueueImpl.class)); producer.close(); } + + @Test + public void testRecall() throws Exception { + final ProducerImpl producer = createProducerWithTopic(FAKE_TOPIC_0); + final String messageId = MessageIdCodec.getInstance().nextMessageId().toString(); + final RecallReceiptImpl recallReceiptImpl = new RecallReceiptImpl(messageId); + Mockito.doReturn(Futures.immediateFuture(recallReceiptImpl)).when(producer).recallMessage0(any(), any()); + producer.recallMessage(FAKE_TOPIC_0, "handle"); + verify(producer, times(1)).recallMessage0(any(), any()); + producer.close(); + } + + @Test + public void testRecallFailure() { + RecallMessageResponse response = RecallMessageResponse.newBuilder() + .setMessageId("") + .setStatus(Status.newBuilder().setCode(Code.INTERNAL_ERROR).build()) + .build(); + final ClientManagerImpl clientManager = mock(ClientManagerImpl.class); + final ProducerImpl producer = createProducerWithTopic(FAKE_TOPIC_0); + Mockito.doReturn(clientManager).when(producer).getClientManager(); + Mockito.doReturn(new RpcFuture<>(fakeRpcContext(), null, Futures.immediateFuture(response))) + .when(clientManager).recallMessage(any(), any(), any()); + + Assert.assertThrows(InternalErrorException.class, () -> { + producer.recallMessage(FAKE_TOPIC_0, "handle"); + }); + producer.recallMessageAsync(FAKE_TOPIC_0, "handle").whenComplete((r, t) -> { + Assert.assertTrue(t instanceof InternalErrorException); + }); + verify(producer, times(2)).recallMessage0(any(), any()); + producer.close(); + } } \ No newline at end of file