**First, a correction: the previous commit went in on a false check.** Its message says the suite passed; it did not. The check piped `go test` through a filter that swallowed the failures and then printed "green" regardless. Two tests were failing when4de10e3landed. What was failing was my own doing. Purging the streams instead of deleting them (4de10e3) left the *consumers* behind, because deleting a stream takes its consumers with it and purging does not. A durable push consumer surviving between tests keeps pushing to a delivery subject the previous test's subscription has gone from: the messages count as delivered, go nowhere, and the next test waits out its timeout for an announcement the server believes it already sent. Consumers are now removed with the purge. Five consecutive clean runs. `-p 1` stays, because two packages asserting and deleting the same fixed-name objects on one bus is a real race — but its comment said the cause I had guessed and not the one I found, so it now says the right thing. **And delivery was not finished when I said it was.** Nothing filled `Rendering.BusUsers`, so the composed file would never have reached a node. `composeBusUsers` closes it: composed per push for the machine holding `mesh-broker`, never kept, because the list is a function of the mesh's records and a stored copy could disagree with them while both looked consistent. A user with no credential is left out and named rather than written as a user without a password — an ordinary situation with an obvious remedy — but a file with no users at all is refused, because that bus would refuse every connection in the mesh. **Minting, on both halves.** A node at enrolment and a module at `module issue`. Three things differ from a management call and each is the point of the move: the credential is minted into the mesh's records and becomes usable at the next composition, so no server need be reachable; the password travels beside the address rather than inside it, because a credential embedded in a URL leaks into every log line that prints a connection; and a module's durable consumer is derived from what it declared rather than named, so it cannot ask for delivery of something it did not say it consumes. A node reconnecting may be refused until that composition reaches the machine running the bus. That is what the host's reconnect backoff is for and it is survivable by design; waiting for the push would hold an enrolment open for as long as a declaration takes to apply. Tested that the switch is a switch: a node enrolling on one bus comes away with a credential for that bus and none for the other, because one that held both could be half-moved and nothing would say which half.
102 lines
3.9 KiB
Go
102 lines
3.9 KiB
Go
package broker
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// Raising the bus's objects against a real server.
|
|
//
|
|
// The pure tests above say what is asked for and in what order. Only a server can say whether it
|
|
// accepts them — and two of these are claims about the server's own behaviour that nothing else
|
|
// could answer: that asserting twice changes nothing, and that a consumer really is bound to the one
|
|
// subject its node is allowed to read.
|
|
//
|
|
// docker run -d --rm --name t -p 14227:4222 nats:2.10-alpine -js
|
|
// MESH_TEST_NATS=nats://127.0.0.1:14227 go test ./internal/broker/ -run TestRaising
|
|
|
|
func aLiveBus(t *testing.T) *JetStream {
|
|
t.Helper()
|
|
url := os.Getenv("MESH_TEST_NATS")
|
|
if url == "" {
|
|
t.Skip("MESH_TEST_NATS unset")
|
|
}
|
|
js, err := Dial(url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(js.Close)
|
|
// **Nothing is deleted here, deliberately.** These objects are the mesh's own and every live
|
|
// test in every package shares one server: a test that deleted a stream to get a clean slate
|
|
// took it out from under whatever was running beside it, and the failure landed in the other
|
|
// test as "stream not found" — which reads as a bug in the code under test. Raise is idempotent
|
|
// by requirement, so asserting against whatever is already there is both safe and the realistic
|
|
// case.
|
|
return js
|
|
}
|
|
|
|
// Every object the mesh's own traffic needs, accepted by a real server, and asserting again changes
|
|
// nothing — which is the whole requirement, because this runs on every start.
|
|
func TestRaisingTheBusIsAcceptedAndIdempotent(t *testing.T) {
|
|
js := aLiveBus(t)
|
|
|
|
if err := Raise(js, []string{"anchor", "laptop"}); err != nil {
|
|
t.Fatalf("a real server refused the mesh's own objects: %v", err)
|
|
}
|
|
// Twice, with nothing in between. A start that failed the second time is a controller that
|
|
// cannot restart.
|
|
if err := Raise(js, []string{"anchor", "laptop"}); err != nil {
|
|
t.Fatalf("asserting the bus's objects a second time failed, so a restart would: %v", err)
|
|
}
|
|
// And again with a machine that was not there before, which is what enrolling one is.
|
|
if err := Raise(js, []string{"anchor", "laptop", "workstation"}); err != nil {
|
|
t.Fatalf("a machine joining an already-raised bus was refused: %v", err)
|
|
}
|
|
|
|
for _, s := range MeshStreams() {
|
|
if _, err := js.Context().StreamInfo(s.Name); err != nil {
|
|
t.Errorf("stream %s is not there: %v", s.Name, err)
|
|
}
|
|
}
|
|
for _, c := range MeshConsumers() {
|
|
if _, err := js.Context().ConsumerInfo(c.Stream, c.Name); err != nil {
|
|
t.Errorf("the controller's consumer on %s is not there: %v", c.Stream, err)
|
|
}
|
|
}
|
|
for _, node := range []string{"anchor", "laptop", "workstation"} {
|
|
info, err := js.Context().ConsumerInfo("NODES", node)
|
|
if err != nil {
|
|
t.Errorf("%s has no way to hear its declaration: %v", node, err)
|
|
continue
|
|
}
|
|
// **Its own subject and no other node's.** A consumer filtered on anything wider is a node
|
|
// reading another machine's declaration, and its own ack grant would not cover it either.
|
|
if info.Config.FilterSubject != "mesh.node."+node+".declare" {
|
|
t.Errorf("%s's consumer reads %q", node, info.Config.FilterSubject)
|
|
}
|
|
if info.Config.AckPolicy != nats.AckExplicitPolicy {
|
|
t.Errorf("%s's consumer acknowledges on delivery, so a declaration it died applying is "+
|
|
"never sent again", node)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The store window needs unlimited redelivery on CONTROL: the bound belongs to the controller, and a
|
|
// server that dead-lettered first would discard the push the stream exists to protect.
|
|
func TestTheControlConsumerDoesNotDeadLetterBeforeTheControllerGivesUp(t *testing.T) {
|
|
js := aLiveBus(t)
|
|
if err := Raise(js, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := js.Context().ConsumerInfo("CONTROL", ControllerName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Config.MaxDeliver > 0 {
|
|
t.Fatalf("max-deliver is %d: a push held through a store restart would be dead-lettered "+
|
|
"before the controller finished deciding about it", info.Config.MaxDeliver)
|
|
}
|
|
}
|