This is an automated email from the ASF dual-hosted git repository.

RongtongJin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-clients.git


The following commit(s) were added to refs/heads/master by this push:
     new 981a507e fix(golang): close the receive hook when long polling finds 
no message (#1378)
981a507e is described below

commit 981a507eda963c92e06c174250f40f68a5eb1a57
Author: guyinyou <[email protected]>
AuthorDate: Wed Sep 16 14:08:52 2026 +0800

    fix(golang): close the receive hook when long polling finds no message 
(#1378)
    
    A push consumer opens the RECEIVE hook before every long polling request
    and closes it once the request completes, but the branch handling
    MESSAGE_NOT_FOUND only logged, so a queue with no traffic leaked one
    inflight reception per poll. The counter never came back down, and
    waitingReceiveRequestFinished always burned its full
    requestTimeout + longPollingTimeout, about 33s by default, before logging
    "Timeout waiting for all inflight receive requests" and giving up. Every
    push consumer shutdown paid that.
    
    Report the completed reception instead: OK for an empty poll, ERROR for a
    real failure, which keeps the current logging. Metrics are unaffected
    because the meter interceptor ignores a RECEIVE hook carrying no messages,
    so idle polling still records nothing.
    
    Adds a regression test that drives a queue whose long polling always
    answers MESSAGE_NOT_FOUND and asserts the inflight count returns to zero
    once the receptions complete. It fails before this change with one leaked
    reception per poll.
    
    Co-authored-by: guyinyou <[email protected]>
---
 golang/process_queue.go      |   6 ++-
 golang/process_queue_test.go | 126 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 131 insertions(+), 1 deletion(-)

diff --git a/golang/process_queue.go b/golang/process_queue.go
index ccd7fee7..a73aff03 100644
--- a/golang/process_queue.go
+++ b/golang/process_queue.go
@@ -466,15 +466,19 @@ func (dpq *defaultProcessQueue) 
receiveMessageImmediatelyWithAttemptId(attemptId
                        }
                        rpcErr, isRpcErr := AsErrRpcStatus(err)
                        isNoNewMessage := isRpcErr && rpcErr.GetCode() == 
int32(v2.Code_MESSAGE_NOT_FOUND)
+                       // An empty long polling result is still a completed 
reception, so it has
+                       // to close the hook opened before the request was sent.
+                       hookStatus := MessageHookPointsStatus_OK
                        if isNoNewMessage {
                                dpq.consumer.cli.log.Debugf("No new message, 
mq=%s, endpoints=%v, clientId=%s",
                                        dpq.mqstr, endpoints, clientId)
                        } else {
-                               
dpq.consumer.cli.doAfter(MessageHookPoints_RECEIVE, make([]*MessageCommon, 0), 
duration, MessageHookPointsStatus_ERROR)
+                               hookStatus = MessageHookPointsStatus_ERROR
                                dpq.consumer.cli.log.Errorf("Exception raised 
during message reception, mq=%s, endpoints=%v, attemptId=%s, "+
                                        "nextAttemptId=%s, clientId=%s, 
err=%v", dpq.mqstr, endpoints, request.GetAttemptId(), nextAttemptId,
                                        clientId, err)
                        }
+                       dpq.consumer.cli.doAfter(MessageHookPoints_RECEIVE, 
make([]*MessageCommon, 0), duration, hookStatus)
                        dpq.onReceiveMessageException(err, nextAttemptId)
                }
        }()
diff --git a/golang/process_queue_test.go b/golang/process_queue_test.go
new file mode 100644
index 00000000..c6c4576d
--- /dev/null
+++ b/golang/process_queue_test.go
@@ -0,0 +1,126 @@
+/*
+ * 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 golang
+
+import (
+       "context"
+       "io"
+       "sync/atomic"
+       "testing"
+       "time"
+
+       "github.com/apache/rocketmq-clients/golang/v5/credentials"
+       "github.com/apache/rocketmq-clients/golang/v5/pkg/utils"
+       v2 "github.com/apache/rocketmq-clients/golang/v5/protocol/v2"
+       "github.com/golang/mock/gomock"
+       "go.uber.org/zap"
+       "google.golang.org/grpc"
+)
+
+// noNewMessageStream answers a long polling receive the way an idle queue 
does:
+// a MESSAGE_NOT_FOUND status followed by EOF.
+type noNewMessageStream struct {
+       grpc.ClientStream
+       statusSent bool
+}
+
+func (stream *noNewMessageStream) Recv() (*v2.ReceiveMessageResponse, error) {
+       if !stream.statusSent {
+               stream.statusSent = true
+               return &v2.ReceiveMessageResponse{
+                       Content: &v2.ReceiveMessageResponse_Status{
+                               Status: &v2.Status{Code: 
v2.Code_MESSAGE_NOT_FOUND, Message: v2.Code_MESSAGE_NOT_FOUND.String()},
+                       },
+               }, nil
+       }
+       return nil, io.EOF
+}
+
+// An empty long polling result is a completed reception, so it has to close 
the
+// receive hook it opened. Otherwise the inflight count grows for as long as 
the
+// topic stays idle and GracefulStop waits out its whole timeout for nothing.
+func TestProcessQueueEmptyLongPollingBalancesInflightReceiveCount(t 
*testing.T) {
+       consumer, err := NewPushConsumer(&Config{
+               Endpoint:      fakeAddress,
+               ConsumerGroup: MOCK_GROUP,
+               Credentials:   &credentials.SessionCredentials{},
+       },
+               
WithPushSubscriptionExpressions(map[string]*FilterExpression{MOCK_TOPIC: 
SUB_ALL}),
+               WithPushMessageListener(&FuncMessageListener{
+                       Consume: func(*MessageView) ConsumerResult { return 
SUCCESS },
+               }),
+       )
+       if err != nil {
+               t.Fatal(err)
+       }
+       pc := consumer.(*defaultPushConsumer)
+       manager := NewMockClientManager(gomock.NewController(t))
+       pc.cli.clientManager = manager
+       pc.cli.log = zap.NewNop().Sugar()
+       pc.cli.inited.Store(true)
+
+       var receptions atomic.Int32
+       manager.EXPECT().ReceiveMessage(gomock.Any(), gomock.Any(), 
gomock.Any()).
+               DoAndReturn(func(context.Context, *v2.Endpoints, 
*v2.ReceiveMessageRequest) (v2.MessagingService_ReceiveMessageClient, error) {
+                       receptions.Add(1)
+                       return &noNewMessageStream{}, nil
+               }).AnyTimes()
+
+       mq := &v2.MessageQueue{
+               Topic:              &v2.Resource{Name: MOCK_TOPIC},
+               Id:                 0,
+               Broker:             &v2.Broker{Name: "broker", Endpoints: 
fakeEndpoints()},
+               AcceptMessageTypes: []v2.MessageType{v2.MessageType_NORMAL},
+       }
+       // Registering the queue the way the route scan does keeps the per 
queue cache
+       // threshold above zero, so an idle queue keeps polling instead of 
backing off
+       // as if its cache were full.
+       dpq, ok := pc.createProcessQueue(utils.ParseMessageQueue2Str(mq), mq, 
SUB_ALL).(*defaultProcessQueue)
+       if !ok {
+               t.Fatal("process queue was not created")
+       }
+       defer dpq.drop()
+
+       dpq.receiveMessageImmediately()
+
+       const wanted = int32(5)
+       deadline := time.Now().Add(10 * time.Second)
+       for receptions.Load() < wanted && time.Now().Before(deadline) {
+               time.Sleep(10 * time.Millisecond)
+       }
+       if got := receptions.Load(); got < wanted {
+               t.Fatalf("expected at least %d receptions of an idle queue, got 
%d", wanted, got)
+       }
+
+       // Stop the retry loop, then let the receptions in flight report back.
+       dpq.drop()
+       count := int64(-1)
+       deadline = time.Now().Add(5 * time.Second)
+       for time.Now().Before(deadline) {
+               count = 
pc.inflightRequestCountInterceptor.getInflightReceiveRequestCount()
+               if count == 0 {
+                       break
+               }
+               time.Sleep(10 * time.Millisecond)
+       }
+       if count != 0 {
+               t.Fatalf("inflight receive count is %d after %d completed 
receptions of an idle queue; "+
+                       "an empty long polling result never closed the hook it 
opened, so GracefulStop would "+
+                       "wait out requestTimeout + longPollingTimeout before 
giving up", count, receptions.Load())
+       }
+}

Reply via email to