Repository: nats-io/nats-server

Issue:
Unexpected number of `Redelivered Messages` for consumer
### Observed behavior

Hi,

We noticed a NATS behavior we cannot easily explain, and i'm hoping to find some insights here.

Given a consumer with `Maximum Deliveries: 1` we keep seeing a state with `Redelivered Messages: 10`.

This is likely caused by some missed acks on our side for whatever reason (eg. killed worker process), however the message was never redelivered to the consumer (which is intended on our side).

So i'm wondering if the state is legit? Perhaps related to #5214 still.

### Expected behavior

Consumer having 0 `Redelivered Messages`

### Server and client version

```
$ nats-server --version
nats-server: v2.10.26

$ nats --version
v0.1.6
```

### Host environment

_No response_

### Steps to reproduce

_No response_

Discussion:
@MauriceVanVeen: > This is likely caused by some missed acks on our side for whatever reason (eg. killed worker process), however the message was never redelivered to the consumer (which is intended on our side).

This is expected behavior, although those messages are not technically redelivered, they have been delivered once and didn't get acked within the `AckWait`. (and they can be redelivered if you'd up the max deliveries, and would then also contain the correct delivery count)

That was changed in https://github.com/nats-io/nats-server/pull/6575 to fix issues where people are using max deliveries and the advisories for DLQ-like setups, but that state was not properly preserved before.

The information that the message was delivered is stored in the pending/outstanding acks section. When `AckWait` is reached, they go from pending into redelivered state. That delivery counter needs to be stored somewhere, and in this case that's redelivered.@ro0NL: > those messages are not technically redelivered

hence i expected `Redelivered Messages: 0` (given `Maximum Deliveries: 1`)

IIUC i can ignore the `Redelivered Messages` number in this case then@ro0NL: we also get the `Redelivered Messages` count for `stream.info.state.messages` in our application.

We expected this to be the pending acks really, which is in fact 0:

```
        Outstanding Acks: 0
    Redelivered Messages: 10
```@ro0NL: in other words, we're trying to tell NATS we only expect a message once, and we dont care about a missed ack (since a cronjob will re-offer the message next time anyway).

In that sense we're trying to avoid unnessecary redeliveries for our specific case.

Which works, except NATS keeps saying `Redelivered Messages: 10`, which is not true.@MauriceVanVeen: Opened a PR that makes an exception when using `MaxDeliver: 1`. The state is still tracked (needed for correctness with an Interest-based stream), but it's not reported.@MauriceVanVeen: https://github.com/nats-io/nats-server/pull/6877#issuecomment-2858949462
> I dont understand the difference between NumPending and NumAckPending quite yet

`Unprocessed Messages: 2`, means there are two messages available that have not yet been delivered. If you fetch one message you will see `Outstanding Acks: 1`, and `Unprocessed Messages: 1`. If you acknowledge that message it will show `Outstanding Acks: 0`. If you then fetch and ack again, `Unprocessed Messages: 0`.

Solution patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 4aab1908..83f728e1 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -21,7 +21,7 @@ builds:
     env:
       # This is the toolchain version we use for releases. To override, set the env var, e.g.:
       # GORELEASER_TOOLCHAIN="go1.22.8" TARGET='linux_amd64' goreleaser build --snapshot --clean --single-target
-      - GOTOOLCHAIN={{ envOrDefault "GORELEASER_TOOLCHAIN" "go1.24.2" }}
+      - GOTOOLCHAIN={{ envOrDefault "GORELEASER_TOOLCHAIN" "go1.24.3" }}
       - GO111MODULE=on
       - CGO_ENABLED=0
     goos:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index bd8672a5..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-os: linux
-dist: focal
-
-vm:
-  size: x-large
-
-language: go
-go:
-  # This should be quoted or use .x, but should not be unquoted.
-  # Remember that a YAML bare float drops trailing zeroes.
-  # These toolchain versions define the test environment.
-  # When updating, also update the GOTOOLCHAIN value in .goreleaser.yml.
-  - "1.24.1"
-  - "1.23.7"
-
-go_import_path: github.com/nats-io/nats-server
-
-addons:
-  apt:
-    packages:
-      - rpm
-
-jobs:
-  include:
-    - name: "Compile and various other checks"
-      env: TEST_SUITE=compile
-    - name: "Run TestNoRace tests"
-      env: TEST_SUITE=no_race_tests
-    - name: "Run Store tests"
-      env: TEST_SUITE=store_tests
-    - name: "Run JetStream tests"
-      env: TEST_SUITE=js_tests
-    - name: "Run JetStream cluster tests (1)"
-      env: TEST_SUITE=js_cluster_tests_1
-    - name: "Run JetStream cluster tests (2)"
-      env: TEST_SUITE=js_cluster_tests_2
-    - name: "Run JetStream cluster tests (3)"
-      env: TEST_SUITE=js_cluster_tests_3
-    - name: "Run JetStream cluster tests (4)"
-      env: TEST_SUITE=js_cluster_tests_4
-    - name: "Run JetStream super cluster tests"
-      env: TEST_SUITE=js_super_cluster_tests
-    - name: "Run MQTT tests"
-      env: TEST_SUITE=mqtt_tests
-    - name: "Run Message Tracing tests"
-      env: TEST_SUITE=msgtrace_tests
-    - name: "Run all other tests from the server package"
-      env: TEST_SUITE=srv_pkg_non_js_tests
-    - name: "Run all tests from all other packages"
-      env: TEST_SUITE=non_srv_pkg_tests
-    - name: "Compile with older Go release"
-      go: "1.23.x"
-      env: TEST_SUITE=build_only
-
-script: ./scripts/runTestsOnTravis.sh $TEST_SUITE
diff --git a/go.mod b/go.mod
index b2e8a2fa..f2c3fb7b 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,7 @@ module github.com/nats-io/nats-server/v2
 
 go 1.23.0
 
-toolchain go1.23.7
+toolchain go1.23.9
 
 require (
 	github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op
diff --git a/server/consumer.go b/server/consumer.go
index 302e1cba..d101d54c 100644
--- a/server/consumer.go
+++ b/server/consumer.go
@@ -2976,6 +2976,11 @@ func (o *consumer) infoWithSnapAndReply(snap bool, reply string) *ConsumerInfo {
 		TimeStamp:      time.Now().UTC(),
 		PriorityGroups: priorityGroups,
 	}
+	// Reset redelivered for MaxDeliver 1. Redeliveries are disabled so must not report it (is confusing otherwise).
+	// The state does still keep track of these messages.
+	if o.cfg.MaxDeliver == 1 {
+		info.NumRedelivered = 0
+	}
 	if o.cfg.PauseUntil != nil {
 		p := *o.cfg.PauseUntil
 		if info.Paused = time.Now().Before(p); info.Paused {



Test patch:
diff --git a/server/jetstream_cluster_1_test.go b/server/jetstream_cluster_1_test.go
index 3a0b11ed..7cba7fa5 100644
--- a/server/jetstream_cluster_1_test.go
+++ b/server/jetstream_cluster_1_test.go
@@ -6599,7 +6599,7 @@ func TestJetStreamClusterMaxDeliveriesOnInterestStreams(t *testing.T) {
 		require_Equal(t, ci.AckFloor.Consumer, 0)
 		require_Equal(t, ci.AckFloor.Stream, 0)
 		require_Equal(t, ci.NumAckPending, 0)
-		require_Equal(t, ci.NumRedelivered, 1)
+		require_Equal(t, ci.NumRedelivered, 0)
 		require_Equal(t, ci.NumPending, 0)
 	}
 }
diff --git a/server/jetstream_cluster_3_test.go b/server/jetstream_cluster_3_test.go
index 07d1bbc5..899ce0c2 100644
--- a/server/jetstream_cluster_3_test.go
+++ b/server/jetstream_cluster_3_test.go
@@ -5478,7 +5478,7 @@ func TestJetStreamClusterConsumerMaxDeliveryNumAckPendingBug(t *testing.T) {
 		require_Equal(t, a.AckFloor.Consumer, 0)
 		require_Equal(t, a.AckFloor.Stream, 0)
 		require_Equal(t, a.NumPending, 40)
-		require_Equal(t, a.NumRedelivered, 10)
+		require_Equal(t, a.NumRedelivered, 0)
 		a.Cluster, b.Cluster = nil, nil
 		a.Delivered.Last, b.Delivered.Last = nil, nil
 		if !reflect.DeepEqual(a, b) {
diff --git a/server/jetstream_test.go b/server/jetstream_test.go
index 2d113aa9..52b197f0 100644
--- a/server/jetstream_test.go
+++ b/server/jetstream_test.go
@@ -19118,6 +19118,85 @@ func TestJetStreamInterestMaxDeliveryReached(t *testing.T) {
 	}
 }
 
+// https://github.com/nats-io/nats-server/issues/6874
+func TestJetStreamMaxDeliveryRedeliveredReporting(t *testing.T) {
+	s := RunBasicJetStreamServer(t)
+	defer s.Shutdown()
+
+	nc, js := jsClientConnect(t, s)
+	defer nc.Close()
+
+	_, err := js.AddStream(&nats.StreamConfig{
+		Name:      "TEST",
+		Storage:   nats.FileStorage,
+		Subjects:  []string{"foo"},
+		Replicas:  1,
+		Retention: nats.LimitsPolicy,
+	})
+	require_NoError(t, err)
+
+	_, err = js.Publish("foo", nil)
+	require_NoError(t, err)
+
+	maxWait := 250 * time.Millisecond
+	cfg := &nats.ConsumerConfig{
+		Durable:    "CONSUMER",
+		AckPolicy:  nats.AckExplicitPolicy,
+		AckWait:    maxWait,
+		MaxDeliver: 1,
+	}
+	_, err = js.AddConsumer("TEST", cfg)
+	require_NoError(t, err)
+
+	sub, err := js.PullSubscribe(_EMPTY_, "CONSUMER", nats.BindStream("TEST"))
+	require_NoError(t, err)
+
+	nfo, err := js.StreamInfo("TEST")
+	require_NoError(t, err)
+	require_Equal(t, nfo.State.Msgs, uint64(1))
+
+	msgs, err := sub.Fetch(1, nats.MaxWait(maxWait))
+	require_NoError(t, err)
+	require_Len(t, 1, len(msgs))
+
+	cnfo, err := js.ConsumerInfo("TEST", "CONSUMER")
+	require_NoError(t, err)
+	require_Equal(t, cnfo.NumAckPending, 1)
+	require_Equal(t, cnfo.NumRedelivered, 0)
+
+	time.Sleep(2 * maxWait)
+
+	// Max deliver 1 so this will fail.
+	_, err = sub.Fetch(1, nats.MaxWait(maxWait))
+	require_Error(t, err)
+
+	// Redelivered should remain 0, as it doesn't get redelivered with MaxDeliver 1.
+	cnfo, err = js.ConsumerInfo("TEST", "CONSUMER")
+	require_NoError(t, err)
+	require_Equal(t, cnfo.NumAckPending, 0)
+	require_Equal(t, cnfo.NumRedelivered, 0)
+
+	// With a higher MaxDeliver we should report it.
+	cfg.MaxDeliver = 2
+	_, err = js.UpdateConsumer("TEST", cfg)
+	require_NoError(t, err)
+
+	cnfo, err = js.ConsumerInfo("TEST", "CONSUMER")
+	require_NoError(t, err)
+	require_Equal(t, cnfo.NumAckPending, 0)
+	require_Equal(t, cnfo.NumRedelivered, 1)
+
+	// Unset should also report.
+	cfg.MaxDeliver = -1
+	_, err = js.UpdateConsumer("TEST", cfg)
+	require_NoError(t, err)
+
+	cnfo, err = js.ConsumerInfo("TEST", "CONSUMER")
+	require_NoError(t, err)
+	require_Equal(t, cnfo.NumAckPending, 0)
+	require_Equal(t, cnfo.NumRedelivered, 1)
+}
+
 func TestJetStreamRecoversStreamFirstSeqWhenNotEmpty(t *testing.T) {
 	s := RunBasicJetStreamServer(t)
 	defer s.Shutdown()