You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by cr...@apache.org on 2026/10/20 14:59:10 UTC

(pulsar-client-go) branch master updated: [Issue 1298][consumer] Fix Seek to earliest or latest on non-partitioned topics (#1551)

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

crossoverJie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-client-go.git


The following commit(s) were added to refs/heads/master by this push:
     new 28c34534 [Issue 1298][consumer] Fix Seek to earliest or latest on non-partitioned topics (#1551)
28c34534 is described below

commit 28c34534b4b9371a50e85666a3e7bb8bcfadaa5c
Author: Yashvardhan Thanvi <ya...@gmail.com>
AuthorDate: Tue Oct 20 20:29:04 2026 +0530

    [Issue 1298][consumer] Fix Seek to earliest or latest on non-partitioned topics (#1551)
    
    Signed-off-by: Yashvardhan Thanvi <ya...@gmail.com>
---
 pulsar/consumer_impl.go | 13 +++++--
 pulsar/consumer_test.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 104 insertions(+), 3 deletions(-)

diff --git a/pulsar/consumer_impl.go b/pulsar/consumer_impl.go
index 909cb595..236bb35f 100644
--- a/pulsar/consumer_impl.go
+++ b/pulsar/consumer_impl.go
@@ -749,9 +749,16 @@ func (c *consumer) Seek(msgID MessageID) error {
 		return newError(SeekFailed, "for partition topic, seek command should perform on the individual partitions")
 	}
 
-	consumer, err := findPartitionConsumer(consumers, msgID)
-	if err != nil {
-		return err
+	var consumer *partitionConsumer
+	if len(consumers) == 1 && msgID.PartitionIdx() == -1 {
+		// EarliestMessageID() and LatestMessageID() use -1 as the partition index.
+		// There is only one partition consumer here, so that is the one to seek.
+		consumer = consumers[0]
+	} else {
+		var err error
+		if consumer, err = findPartitionConsumer(consumers, msgID); err != nil {
+			return err
+		}
 	}
 	consumer.pauseDispatchMessage()
 	// clear messageCh
diff --git a/pulsar/consumer_test.go b/pulsar/consumer_test.go
index 4fd73f49..cdd3adb9 100644
--- a/pulsar/consumer_test.go
+++ b/pulsar/consumer_test.go
@@ -1799,6 +1799,52 @@ func TestConsumerSeek(t *testing.T) {
 	assert.Equal(t, fmt.Sprintf("hello-%d", N-50), string(msg.Payload()))
 }
 
+func TestConsumerSeekEarliestOnNonPartitionedTopic(t *testing.T) {
+	client, err := NewClient(ClientOptions{
+		URL: lookupURL,
+	})
+	require.NoError(t, err)
+	defer client.Close()
+
+	topicName := newTopicName()
+	ctx := context.Background()
+
+	producer, err := client.CreateProducer(ProducerOptions{
+		Topic: topicName,
+	})
+	require.NoError(t, err)
+	defer producer.Close()
+
+	consumer, err := client.Subscribe(ConsumerOptions{
+		Topic:            topicName,
+		SubscriptionName: "sub-1",
+	})
+	require.NoError(t, err)
+	defer consumer.Close()
+
+	const N = 10
+	for i := 0; i < N; i++ {
+		_, err := producer.Send(ctx, &ProducerMessage{
+			Payload: []byte(fmt.Sprintf("hello-%d", i)),
+		})
+		require.NoError(t, err)
+	}
+
+	for i := 0; i < N; i++ {
+		msg, err := consumer.Receive(ctx)
+		require.NoError(t, err)
+		require.NoError(t, consumer.Ack(msg))
+	}
+
+	require.NoError(t, consumer.Seek(EarliestMessageID()))
+
+	receiveCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+	defer cancel()
+	msg, err := consumer.Receive(receiveCtx)
+	require.NoError(t, err)
+	assert.Equal(t, "hello-0", string(msg.Payload()))
+}
+
 func TestConsumerSeekByTime(t *testing.T) {
 	client, err := NewClient(ClientOptions{
 		URL: lookupURL,
@@ -6646,3 +6692,51 @@ func TestConsumerWithDLQRetryTopicNoGetPartitionedTopicMetadata(t *testing.T) {
 			"GetPartitionedTopicMetadata should not be called with old Retry topic when custom Retry topic is provided")
 	}
 }
+
+func TestConsumerSeekWithoutPartitionIndexOnSinglePartitionConsumer(t *testing.T) {
+	eventsCh := make(chan interface{}, 1)
+	pc := &partitionConsumer{
+		topic:                      "persistent://public/default/test-topic",
+		eventsCh:                   eventsCh,
+		dispatcherSeekingControlCh: make(chan struct{}, 1),
+		log:                        plog.DefaultNopLogger(),
+	}
+	pc.ackGroupingTracker = newAckGroupingTracker(&AckGroupingOptions{MaxSize: 0}, nil, nil, nil)
+
+	c := &consumer{
+		topic:     "persistent://public/default/test-topic",
+		messageCh: make(chan ConsumerMessage, 1),
+	}
+	c.consumers.Store([]*partitionConsumer{pc})
+
+	for _, msgID := range []MessageID{EarliestMessageID(), LatestMessageID()} {
+		errCh := make(chan error, 1)
+		go func() {
+			errCh <- c.Seek(msgID)
+		}()
+
+		select {
+		case event := <-eventsCh:
+			req, ok := event.(*seekRequest)
+			require.True(t, ok, "expected a seek request, got %T", event)
+			assert.Equal(t, msgID.LedgerID(), req.msgID.LedgerID())
+			assert.Equal(t, msgID.EntryID(), req.msgID.EntryID())
+			close(req.doneCh)
+		case err := <-errCh:
+			t.Fatalf("Seek(%v) did not reach the partition consumer: %v", msgID, err)
+		case <-time.After(3 * time.Second):
+			t.Fatalf("timed out waiting for the seek request for %v", msgID)
+		}
+
+		require.NoError(t, <-errCh)
+		// Drain the pause signal sent by Seek so that the next Seek does not block.
+		<-pc.dispatcherSeekingControlCh
+	}
+
+	// A partition index that does not exist is still rejected.
+	assert.Error(t, c.Seek(NewMessageID(1, 2, -1, 3)))
+
+	// Ack paths keep rejecting IDs without a partition index.
+	assert.Error(t, c.AckID(EarliestMessageID()))
+	assert.Error(t, c.AckIDCumulative(LatestMessageID()))
+}