Files
nats-server/server/jetstream_sourcing_scaling_test.go
Jean-Noël MoyneandWaldemar Quevedo beaa987edf Improve leadership change signaling (#5504)
Because leadership status can sometimes change very quickly and
raft.updateLeadChange() can 'drop' some lead change signals, especially
with the leadc channel having a capacity of one, which means that
sometimes stream.setLeader(isLeader bool) gets called twice in a row
with the same isLeader value.

This could lead to in some cases the sourcing to 'stall' (not all
messages gets sourced) or 'duplicate messages' (more messages than
expected get sourced).

Changes are increasing the leadc channel's size to 32.

The effects of the change can be demonstrated by running the
TestStreamSourcingScalingSourcingManyBenchmark load test a large number
of times (e.g. 100 times): without these changes the test flaps and
fails between 10 and 20% of the time (most of the time 'stalling' but
also sometimes with 'too many'). With the changes over around a hundred
runs not once did the test fail with 'stalling' and only once did it
fail with 'too many' messages (so there's still a bug/corner case that
could result in 'more than expected' messages to be sourced, although it
seems to be a rare occurence (i.e. 1% of the time)).

This is a replacement for #5496

---------

Signed-off-by: Jean-Noël Moyne <[email protected]>
2024-06-10 10:09:52 -07:00

283 lines
8.1 KiB
Go

// Copyright 2024 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 server
import (
"fmt"
"github.com/nats-io/nats.go"
"strconv"
"testing"
"time"
)
var serverConfig1 = `
server_name: server1
listen: 127.0.0.1:4222
http: 8222
prof_port = 18222
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
cluster {
name: my_cluster
listen: 127.0.0.1:4248
routes: [nats://127.0.0.1:4249,nats://127.0.0.1:4250]
}
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
`
var serverConfig2 = `
server_name: server2
listen: 127.0.0.1:5222
http: 8223
prof_port = 18223
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
cluster {
name: my_cluster
listen: 127.0.0.1:4249
routes: [nats://127.0.0.1:4248,nats://127.0.0.1:4250]
}
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
`
var serverConfig3 = `
server_name: server3
listen: 127.0.0.1:6222
http: 8224
prof_port = 18224
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
cluster {
name: my_cluster
listen: 127.0.0.1:4250
routes: [nats://127.0.0.1:4248,nats://127.0.0.1:4249]
}
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
`
var connectURL = "nats://127.0.0.1:4222,nats://127.0.0.1:4223,nats://127.0.0.1:4224"
func createMyLocalCluster(t *testing.T) *cluster {
c := &cluster{servers: make([]*Server, 0, 3), opts: make([]*Options, 0, 3), name: "C3"}
storeDir1 := t.TempDir()
s1, o := RunServerWithConfig(createConfFile(t, []byte(fmt.Sprintf(serverConfig1, storeDir1))))
c.servers = append(c.servers, s1)
c.opts = append(c.opts, o)
storeDir2 := t.TempDir()
s2, o := RunServerWithConfig(createConfFile(t, []byte(fmt.Sprintf(serverConfig2, storeDir2))))
c.servers = append(c.servers, s2)
c.opts = append(c.opts, o)
storeDir3 := t.TempDir()
s3, o := RunServerWithConfig(createConfFile(t, []byte(fmt.Sprintf(serverConfig3, storeDir3))))
c.servers = append(c.servers, s3)
c.opts = append(c.opts, o)
c.t = t
// Wait til we are formed and have a leader.
c.checkClusterFormed()
c.waitOnClusterReady()
return c
}
// This test is being skipped by CI as it takes too long to run and is meant to test the scalability of sourcing
// rather than being a unit test.
func TestStreamSourcingScalingSourcingManyBenchmark(t *testing.T) {
t.Skip()
var numSourced = 1000
var numMsgPerSource = 10_000
var batchSize = 200
var retries int
var err error
c := createMyLocalCluster(t)
defer c.shutdown()
nc, err := nats.Connect(connectURL)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
js, err := nc.JetStream(nats.MaxWait(10 * time.Second))
if err != nil {
t.Fatalf("Unexpected error getting JetStream context: %v", err)
}
defer nc.Close()
// create n streams to source from
for i := 0; i < numSourced; i++ {
_, err := js.AddStream(&nats.StreamConfig{
Name: fmt.Sprintf("sourced-%d", i),
Subjects: []string{strconv.Itoa(i)},
Retention: nats.LimitsPolicy,
Storage: nats.FileStorage,
Discard: nats.DiscardOld,
Replicas: 1,
AllowDirect: true,
MirrorDirect: false,
DiscardNewPerSubject: false,
})
require_NoError(t, err)
}
fmt.Printf("Streams created\n")
// publish n messages for each sourced stream
for j := 0; j < numMsgPerSource; j++ {
start := time.Now()
var pafs = make([]nats.PubAckFuture, numSourced)
for i := 0; i < numSourced; i++ {
var err error
for {
pafs[i], err = js.PublishAsync(strconv.Itoa(i), []byte(strconv.Itoa(j)))
if err != nil {
fmt.Printf("Error async publishing: %v, retrying\n", err)
retries++
time.Sleep(10 * time.Millisecond)
} else {
break
}
}
if i != 0 && i%batchSize == 0 {
<-js.PublishAsyncComplete()
}
}
<-js.PublishAsyncComplete()
for i := 0; i < numSourced; i++ {
select {
case <-pafs[i].Ok():
case psae := <-pafs[i].Err():
fmt.Printf("Error on PubAckFuture: %v, retrying sync...\n", psae)
retries++
_, err = js.Publish(strconv.Itoa(i), []byte(strconv.Itoa(j)))
require_NoError(t, err)
}
}
end := time.Now()
if j%1000 == 0 {
fmt.Printf("Published round %d, avg pub latency %v\n", j, end.Sub(start)/time.Duration(numSourced))
}
}
fmt.Printf("Messages published\n")
// create the StreamSources
streamSources := make([]*nats.StreamSource, numSourced)
for i := 0; i < numSourced; i++ {
streamSources[i] = &nats.StreamSource{Name: fmt.Sprintf("sourced-%d", i), FilterSubject: strconv.Itoa(i)}
}
// create a stream that sources from them
_, err = js.AddStream(&nats.StreamConfig{
Name: "sourcing",
Subjects: []string{"foo"},
Sources: streamSources,
Retention: nats.LimitsPolicy,
Storage: nats.FileStorage,
Discard: nats.DiscardOld,
Replicas: 3,
AllowDirect: true,
MirrorDirect: false,
DiscardNewPerSubject: false,
})
require_NoError(t, err)
c.waitOnStreamLeader(globalAccountName, "sourcing")
sl := c.streamLeader(globalAccountName, "sourcing")
fmt.Printf("Sourcing stream created leader is *** %s ***\n", sl.Name())
expectedSeq := make([]int, numSourced)
start := time.Now()
var lastMsgs uint64
mset, err := sl.GlobalAccount().lookupStream("sourcing")
require_NoError(t, err)
checkFor(t, 5*time.Minute, 1000*time.Millisecond, func() error {
mset.mu.RLock()
var state StreamState
mset.store.FastState(&state)
mset.mu.RUnlock()
if state.Msgs == uint64(numMsgPerSource*numSourced) {
fmt.Printf("👍 Test passed: expected %d messages, got %d and took %v\n", uint64(numMsgPerSource*numSourced), state.Msgs, time.Since(start))
return nil
} else if state.Msgs < uint64(numMsgPerSource*numSourced) {
fmt.Printf("Current Rate %d per second - Received %d\n", state.Msgs-lastMsgs, state.Msgs)
lastMsgs = state.Msgs
return fmt.Errorf("Expected %d messages, got %d", uint64(numMsgPerSource*numSourced), state.Msgs)
} else {
fmt.Printf("Too many messages! expected %d (retries=%d), got %d\n", uint64(numMsgPerSource*numSourced), retries, state.Msgs)
return fmt.Errorf("Too many messages: expected %d (retries=%d), got %d", uint64(numMsgPerSource*numSourced), retries, state.Msgs)
}
})
// Check that all the messages sourced in the stream are correct
// Note: expects to see exactly increasing matching sequence numbers, so could theoretically fail if some messages
// get recorded 'out of order' (according to the payload value) because asynchronous JS publication is used to
// publish the messages.
// However, that should not happen if the publish 'batch size' is not more than the number of streams being sourced.
// create a consumer on sourcing
_, err = js.AddConsumer("sourcing", &nats.ConsumerConfig{AckPolicy: nats.AckExplicitPolicy})
require_NoError(t, err)
syncSub, err := js.SubscribeSync("", nats.BindStream("sourcing"))
require_NoError(t, err)
start = time.Now()
print("Checking the messages\n")
for i := 0; i < numSourced*numMsgPerSource; i++ {
msg, err := syncSub.NextMsg(30 * time.Second)
require_NoError(t, err)
sId, err := strconv.Atoi(msg.Subject)
require_NoError(t, err)
seq, err := strconv.Atoi(string(msg.Data))
require_NoError(t, err)
if expectedSeq[sId] == seq {
expectedSeq[sId]++
} else {
t.Fatalf("Expected seq number %d got %d for source %d\n", expectedSeq[sId], seq, sId)
}
msg.Ack()
if i%100_000 == 0 {
now := time.Now()
fmt.Printf("[%v] Checked %d messages: %f msgs/sec \n", now, i, 100_000/now.Sub(start).Seconds())
start = now
}
}
print("👍 Done. \n")
}