Files
07b5d2b282 chore: sync upstream 2026-03-27 (#1)
* Release v1.49.0 (#2025)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [FIXED] Remove conn status listener on Consume and Messages cleanup (#2027)

Signed-off-by: txuna <[email protected]>

* Add agent reviews (#2035)

Signed-off-by: Tomasz Pietrek <[email protected]>

* [IMPROVED] Bump CI Go versions to 1.25/1.26 and simplify version management (#2039)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [IMPROVED] Reject control characters in stream and consumer names (#2038)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [IMPROVED] Deduplicate keys in KeyValue.Keys() and document ListKeys behavior (#2029)

Un-deprecate Keys() and add deduplication using slices.Sort + slices.Compact
to handle cases where the server returns duplicate keys during concurrent
writes. Add documentation notes to ListKeys and ListKeysFiltered warning
about possible duplicates on large buckets with frequent writes.

Signed-off-by: Piotr Piotrowski <[email protected]>

* Add missing AccountLimits fields in jetstream package (#2041)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [IMPROVED] Bump dependencies and go version (#2044)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [FIXED] Fix race condition in orderedSubscription.Drain() (#2030)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [FIXED] Fix flaky TestConsumerPrioritized/messages test (#2033)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [IMPROVED] De-flake TestAlwaysReconnectOnAccountMaxConnectionsExceededErr (#2042)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [FIXED] Fix flaky TestKeyValueWithSources test (#2036)

* [FIXED] Fix flaky TestKeyValueWithSources test

Signed-off-by: Piotr Piotrowski <[email protected]>

* Adjust timeout in test

Signed-off-by: Piotr Piotrowski <[email protected]>

* trigger ci

Signed-off-by: Piotr Piotrowski <[email protected]>

---------

Signed-off-by: Piotr Piotrowski <[email protected]>

* [FIXED] Fixed OrderedConsumer.Consume race in handler (#2043)

Signed-off-by: Piotr Piotrowski <[email protected]>

* [FIXED] WebSocket close frame discarding buffered data frames (#2032)

When a NATS server rejects a WebSocket connection (e.g., due to an
expired JWT), it sends a -ERR protocol message followed by a WebSocket
close frame. If both frames arrive in the same TCP read, the
websocketReader.Read() method would process the close frame and
immediately return io.EOF, discarding the -ERR data that had already
been buffered in the pending list.

This caused nats.Connect to return a generic EOF error instead of the
proper authentication error (ErrAuthorization, ErrAuthExpired, etc.),
making it difficult for users to diagnose connection failures.

The fix defers the close frame's io.EOF when there is already pending
data, storing it in a new closeErr field. The pending data is returned
first, and the deferred error is returned on the subsequent Read() call.

Signed-off-by: Waldemar Quevedo <[email protected]>

* [IMPROVED] Wrap EOF/connection reset errors with TLS context after handshake (#2031)

* Wrap EOF/connection reset errors with TLS context after handshake

When a TLS proxy (e.g. nginx with mTLS) completes the TLS handshake but
then closes the connection due to an untrusted client certificate, the
client would receive a bare "EOF" or "connection reset by peer" with no
indication that TLS was involved. This wraps such errors with context:

  "nats: connection closed by remote after TLS handshake: EOF"

The wrapped error preserves the original via %w for backwards
compatibility (errors.Is(err, io.EOF) still returns true).

Signed-off-by: Waldemar Quevedo <[email protected]>

* Wrap TLS errors in ErrTLS

Signed-off-by: Piotr Piotrowski <[email protected]>

---------

Signed-off-by: Waldemar Quevedo <[email protected]>
Signed-off-by: Piotr Piotrowski <[email protected]>
Co-authored-by: Piotr Piotrowski <[email protected]>

* Release v1.50.0 (#2045)

Signed-off-by: Piotr Piotrowski <[email protected]>

---------

Signed-off-by: Piotr Piotrowski <[email protected]>
Signed-off-by: txuna <[email protected]>
Signed-off-by: Tomasz Pietrek <[email protected]>
Signed-off-by: Waldemar Quevedo <[email protected]>
Co-authored-by: Piotr Piotrowski <[email protected]>
Co-authored-by: tuuna <[email protected]>
Co-authored-by: Tomasz Pietrek <[email protected]>
Co-authored-by: Waldemar Quevedo <[email protected]>
2026-05-29 22:07:25 -07:00

622 lines
15 KiB
Go

// Copyright 2022-2023 The NATS Authors
// Licensed 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 jetstream
import (
"errors"
"fmt"
"testing"
"time"
"github.com/hanzoai/pubsub-go"
)
func TestMessageMetadata(t *testing.T) {
tests := []struct {
name string
givenReply string
expectedMetadata MsgMetadata
withError error
}{
{
name: "valid metadata",
givenReply: "$JS.ACK.domain.hash-123.stream.cons.5.10.20.123456789.1.token",
expectedMetadata: MsgMetadata{
Sequence: SequencePair{
Consumer: 20,
Stream: 10,
},
NumDelivered: 5,
NumPending: 1,
Timestamp: time.Unix(0, 123456789),
Stream: "stream",
Consumer: "cons",
Domain: "domain",
},
},
{
name: "no reply subject",
givenReply: "",
withError: ErrMsgNoReply,
},
{
name: "not a JetStream message",
givenReply: "ABC",
withError: ErrNotJSMessage,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
msg := &jetStreamMsg{
msg: &nats.Msg{
Reply: test.givenReply,
Sub: &nats.Subscription{},
},
}
res, err := msg.Metadata()
if test.withError != nil {
if err == nil || !errors.Is(err, test.withError) {
t.Fatalf("Expected error: %v; got: %v", test.withError, err)
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if *res != test.expectedMetadata {
t.Fatalf("Invalid metadata; want: %v; got: %v", test.expectedMetadata, res)
}
})
}
}
func TestValidateSubject(t *testing.T) {
tests := []struct {
subject string
withError bool
}{
{"test.A", false},
{"test.*", false},
{"*", false},
{"*.*", false},
{"test.*.A", false},
{"test.>", false},
{">", false},
{">.", true},
{"test.>.A", true},
{"", true},
{"test A", true},
}
for _, test := range tests {
tName := fmt.Sprintf("subj=%s,err=%t", test.subject, test.withError)
t.Run(tName, func(t *testing.T) {
err := validateSubject(test.subject)
if test.withError {
if err == nil {
t.Fatal("Expected error; got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
}
}
func TestValidateStreamName(t *testing.T) {
tests := []struct {
name string
withError bool
}{
{"test", false},
{"test-stream_1", false},
{"", true},
{"stream.name", true},
{"stream name", true},
{"stream>", true},
{"stream*", true},
{"stream/name", true},
{"stream\\name", true},
{"stream\tname", true},
{"stream\rname", true},
{"stream\nname", true},
}
for _, test := range tests {
t.Run(fmt.Sprintf("name=%q,err=%t", test.name, test.withError), func(t *testing.T) {
err := validateStreamName(test.name)
if test.withError {
if err == nil {
t.Fatal("Expected error; got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
}
}
func TestValidateConsumerName(t *testing.T) {
tests := []struct {
name string
withError bool
}{
{"test", false},
{"test-consumer_1", false},
{"", true},
{"consumer.name", true},
{"consumer name", true},
{"consumer>", true},
{"consumer*", true},
{"consumer/name", true},
{"consumer\\name", true},
{"consumer\tname", true},
{"consumer\rname", true},
{"consumer\nname", true},
}
for _, test := range tests {
t.Run(fmt.Sprintf("name=%q,err=%t", test.name, test.withError), func(t *testing.T) {
err := validateConsumerName(test.name)
if test.withError {
if err == nil {
t.Fatal("Expected error; got nil")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
}
}
func TestRetryWithBackoff(t *testing.T) {
tests := []struct {
name string
givenOpts backoffOpts
withError bool
timeout time.Duration
cancelAfter time.Duration
successfulAttemptNum int
expectedAttemptsCount int
}{
{
name: "infinite attempts, 5 tries before success",
givenOpts: backoffOpts{
attempts: -1,
initialInterval: 10 * time.Millisecond,
maxInterval: 60 * time.Millisecond,
},
withError: false,
successfulAttemptNum: 5,
// 0ms + 10ms + 20ms + 40ms + 60ms = 130ms
timeout: 200 * time.Millisecond,
expectedAttemptsCount: 5,
},
{
name: "infinite attempts, 5 tries before success, without initial execution",
givenOpts: backoffOpts{
attempts: -1,
initialInterval: 10 * time.Millisecond,
disableInitialExecution: true,
factor: 2,
maxInterval: 60 * time.Millisecond,
},
withError: false,
successfulAttemptNum: 5,
// 10ms + 20ms + 40ms + 60ms + 60 = 190ms
timeout: 250 * time.Millisecond,
expectedAttemptsCount: 5,
},
{
name: "5 attempts, unsuccessful",
givenOpts: backoffOpts{
attempts: 5,
initialInterval: 10 * time.Millisecond,
factor: 2,
maxInterval: 60 * time.Millisecond,
},
withError: true,
// 0ms + 10ms + 20ms + 40ms + 60ms = 130ms
timeout: 200 * time.Millisecond,
expectedAttemptsCount: 5,
},
{
name: "custom backoff values, should override other settings",
givenOpts: backoffOpts{
initialInterval: 2 * time.Second,
factor: 2,
maxInterval: 100 * time.Millisecond,
customBackoff: []time.Duration{10 * time.Millisecond, 20 * time.Millisecond, 30 * time.Millisecond, 40 * time.Millisecond, 50 * time.Millisecond},
},
withError: false,
successfulAttemptNum: 4,
// 10ms + 20ms + 30ms + 40ms = 100ms
timeout: 150 * time.Millisecond,
expectedAttemptsCount: 4,
},
{
name: "no custom backoff, with cancel",
givenOpts: backoffOpts{
attempts: -1,
initialInterval: 100 * time.Millisecond,
factor: 1,
},
withError: false,
cancelAfter: 150 * time.Millisecond,
timeout: 200 * time.Millisecond,
expectedAttemptsCount: 2,
},
{
name: "custom backoff, with cancel",
givenOpts: backoffOpts{
customBackoff: []time.Duration{100 * time.Millisecond, 100 * time.Millisecond, 100 * time.Millisecond},
},
cancelAfter: 150 * time.Millisecond,
expectedAttemptsCount: 1,
timeout: 200 * time.Millisecond,
},
{
name: "attempts num not provided",
givenOpts: backoffOpts{
initialInterval: 100 * time.Millisecond,
factor: 1,
},
withError: true,
timeout: 1 * time.Second,
expectedAttemptsCount: 1,
},
{
name: "custom backoff, but attempts num provided",
givenOpts: backoffOpts{
customBackoff: []time.Duration{100 * time.Millisecond, 100 * time.Millisecond, 100 * time.Millisecond},
attempts: 5,
},
withError: true,
timeout: 1 * time.Second,
expectedAttemptsCount: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ok := make(chan struct{})
errs := make(chan error, 1)
var cancelChan chan struct{}
if test.cancelAfter != 0 {
cancelChan = make(chan struct{})
test.givenOpts.cancel = cancelChan
}
var count int
go func() {
err := retryWithBackoff(func(attempt int) (bool, error) {
count = attempt
if test.successfulAttemptNum != 0 && attempt == test.successfulAttemptNum-1 {
return false, nil
}
return true, fmt.Errorf("error %d", attempt)
}, test.givenOpts)
if err != nil {
errs <- err
return
}
close(ok)
}()
if test.cancelAfter > 0 {
go func() {
time.Sleep(test.cancelAfter)
close(cancelChan)
}()
}
select {
case <-ok:
if test.withError {
t.Fatal("Expected error; got nil")
}
case err := <-errs:
if !test.withError {
t.Fatalf("Unexpected error: %v", err)
}
case <-time.After(test.timeout):
t.Fatalf("Timeout after %v", test.timeout)
}
if count != test.expectedAttemptsCount-1 {
t.Fatalf("Invalid count; got: %d, want: %d", count, test.expectedAttemptsCount-1)
}
})
}
}
func TestPullConsumer_checkPending(t *testing.T) {
tests := []struct {
name string
givenSub *pullSubscription
fetchInProgress bool
shouldSend bool
expectedPullRequest *pullRequest
}{
{
name: "msgs threshold not reached, bytes not set, no pull request",
givenSub: &pullSubscription{
pending: pendingMsgs{
msgCount: 10,
},
consumeOpts: &consumeOpts{
ThresholdMessages: 5,
MaxMessages: 10,
},
},
shouldSend: false,
},
{
name: "pending msgs below threshold, send pull request",
givenSub: &pullSubscription{
pending: pendingMsgs{
msgCount: 4,
byteCount: 400, // byte count should be ignored
},
consumeOpts: &consumeOpts{
ThresholdMessages: 5,
MaxMessages: 10,
},
},
shouldSend: true,
expectedPullRequest: &pullRequest{
Batch: 6,
MaxBytes: 0,
},
},
{
name: "pending msgs below threshold but PR in progress",
givenSub: &pullSubscription{
pending: pendingMsgs{
msgCount: 4,
},
consumeOpts: &consumeOpts{
ThresholdMessages: 5,
MaxMessages: 10,
},
},
fetchInProgress: true,
shouldSend: false,
},
{
name: "pending bytes below threshold, send pull request",
givenSub: &pullSubscription{
pending: pendingMsgs{
byteCount: 400,
msgCount: 1000000, // msgs count should be ignored
},
consumeOpts: &consumeOpts{
MaxMessages: 1000000,
ThresholdBytes: 500,
MaxBytes: 1000,
},
},
shouldSend: true,
expectedPullRequest: &pullRequest{
Batch: 1000000,
MaxBytes: 600,
},
},
{
name: "pending bytes above threshold, no pull request",
givenSub: &pullSubscription{
pending: pendingMsgs{
byteCount: 600,
},
consumeOpts: &consumeOpts{
ThresholdBytes: 500,
MaxBytes: 1000,
},
},
shouldSend: false,
},
{
name: "pending bytes below threshold, fetch in progress, no pull request",
givenSub: &pullSubscription{
pending: pendingMsgs{
byteCount: 400,
},
consumeOpts: &consumeOpts{
ThresholdBytes: 500,
MaxBytes: 1000,
},
},
fetchInProgress: true,
shouldSend: false,
},
{
name: "StopAfter set, pending msgs below StopAfter, send pull request",
givenSub: &pullSubscription{
pending: pendingMsgs{
msgCount: 4,
},
consumeOpts: &consumeOpts{
ThresholdMessages: 5,
MaxMessages: 10,
StopAfter: 8,
},
delivered: 2,
},
shouldSend: true,
expectedPullRequest: &pullRequest{
Batch: 2, // StopAfter (8) - delivered (2) - pending (4)
MaxBytes: 0,
},
},
{
name: "StopAfter set, pending msgs equal to StopAfter, no pull request",
givenSub: &pullSubscription{
pending: pendingMsgs{
msgCount: 6,
},
consumeOpts: &consumeOpts{
ThresholdMessages: 5,
MaxMessages: 10,
StopAfter: 6,
},
delivered: 0,
},
shouldSend: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
prChan := make(chan *pullRequest, 1)
test.givenSub.fetchNext = prChan
if test.fetchInProgress {
test.givenSub.fetchInProgress.Store(1)
}
errs := make(chan error, 1)
ok := make(chan struct{}, 1)
go func() {
if test.shouldSend {
select {
case pr := <-prChan:
if *pr != *test.expectedPullRequest {
errs <- fmt.Errorf("Invalid pull request; want: %#v; got: %#v", test.expectedPullRequest, pr)
return
}
ok <- struct{}{}
case <-time.After(1 * time.Second):
errs <- errors.New("Timeout")
return
}
} else {
select {
case <-prChan:
errs <- errors.New("Unexpected pull request")
case <-time.After(100 * time.Millisecond):
ok <- struct{}{}
return
}
}
}()
test.givenSub.checkPending()
select {
case <-ok:
// ok
case err := <-errs:
t.Fatal(err)
}
})
}
}
func TestKV_keyValid(t *testing.T) {
tests := []struct {
key string
ok bool
}{
{key: "foo123", ok: true},
{key: "foo.bar", ok: true},
{key: "Foo.123=bar_baz-abc", ok: true},
{key: "foo.*.bar", ok: false},
{key: "foo.>", ok: false},
{key: ">", ok: false},
{key: "*", ok: false},
{key: "foo!", ok: false},
{key: "foo bar", ok: false},
{key: "", ok: false},
{key: " ", ok: false},
{key: ".", ok: false},
{key: ".foo", ok: false},
{key: "foo.", ok: false},
}
for _, test := range tests {
t.Run(test.key, func(t *testing.T) {
res := keyValid(test.key)
if res != test.ok {
t.Fatalf("Invalid result; want: %v; got: %v", test.ok, res)
}
})
}
}
func TestKV_searchKeyValid(t *testing.T) {
tests := []struct {
key string
ok bool
}{
{key: "foo123", ok: true},
{key: "foo.bar", ok: true},
{key: "Foo.123=bar_baz-abc", ok: true},
{key: "foo.*.bar", ok: true},
{key: "foo.>", ok: true},
{key: ">", ok: true},
{key: "*", ok: true},
{key: "foo!", ok: false},
{key: "foo bar", ok: false},
{key: "", ok: false},
{key: " ", ok: false},
{key: ".", ok: false},
{key: ".foo", ok: false},
{key: "foo.", ok: false},
}
for _, test := range tests {
t.Run(test.key, func(t *testing.T) {
res := searchKeyValid(test.key)
if res != test.ok {
t.Fatalf("Invalid result; want: %v; got: %v", test.ok, res)
}
})
}
}
func TestKV_bucketValid(t *testing.T) {
tests := []struct {
key string
ok bool
}{
{key: "foo123", ok: true},
{key: "Foo123-bar_baz", ok: true},
{key: "foo.bar", ok: false},
{key: "foo.*.bar", ok: false},
{key: "foo.>", ok: false},
{key: ">", ok: false},
{key: "*", ok: false},
{key: "foo!", ok: false},
{key: "foo bar", ok: false},
{key: "", ok: false},
{key: " ", ok: false},
{key: ".", ok: false},
{key: ".foo", ok: false},
{key: "foo.", ok: false},
}
for _, test := range tests {
t.Run(test.key, func(t *testing.T) {
res := bucketValid(test.key)
if res != test.ok {
t.Fatalf("Invalid result; want: %v; got: %v", test.ok, res)
}
})
}
}