|
|
|
@ -12,6 +12,98 @@ import (
|
|
|
|
|
"github.com/rqlite/rqlite/v8/command/proto"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Test_MultiNodeSimple tests that a the core operation of a multi-node
|
|
|
|
|
// cluster works as expected. That is, with a two node cluster, writes
|
|
|
|
|
// actually replicate, and reads are consistent.
|
|
|
|
|
func Test_MultiNodeSimple(t *testing.T) {
|
|
|
|
|
s0, ln := mustNewStore(t)
|
|
|
|
|
defer ln.Close()
|
|
|
|
|
|
|
|
|
|
if err := s0.Open(); err != nil {
|
|
|
|
|
t.Fatalf("failed to open single-node store: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
defer s0.Close(true)
|
|
|
|
|
if err := s0.Bootstrap(NewServer(s0.ID(), s0.Addr(), true)); err != nil {
|
|
|
|
|
t.Fatalf("failed to bootstrap single-node store: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
if _, err := s0.WaitForLeader(10 * time.Second); err != nil {
|
|
|
|
|
t.Fatalf("Error waiting for leader: %s", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s1, ln1 := mustNewStore(t)
|
|
|
|
|
defer ln1.Close()
|
|
|
|
|
|
|
|
|
|
if err := s1.Open(); err != nil {
|
|
|
|
|
t.Fatalf("failed to open single-node store: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
defer s1.Close(true)
|
|
|
|
|
|
|
|
|
|
if err := s0.Join(joinRequest(s1.ID(), s1.Addr(), true)); err != nil {
|
|
|
|
|
t.Fatalf("failed to join single-node store: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
if _, err := s1.WaitForLeader(10 * time.Second); err != nil {
|
|
|
|
|
t.Fatalf("Error waiting for leader: %s", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write some data.
|
|
|
|
|
er := executeRequestFromStrings([]string{
|
|
|
|
|
`CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)`,
|
|
|
|
|
`INSERT INTO foo(id, name) VALUES(1, "fiona")`,
|
|
|
|
|
}, false, false)
|
|
|
|
|
_, err := s0.Execute(er)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to execute on single node: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
if _, err := s0.WaitForAppliedFSM(5 * time.Second); err != nil {
|
|
|
|
|
t.Fatalf("failed to wait for FSM to apply on leader")
|
|
|
|
|
}
|
|
|
|
|
testPoll(t, func() bool {
|
|
|
|
|
return s0.DBAppliedIndex() == s1.DBAppliedIndex()
|
|
|
|
|
}, 250*time.Millisecond, 3*time.Second)
|
|
|
|
|
|
|
|
|
|
// Now, do a NONE consistency query on each node, to actually confirm the data
|
|
|
|
|
// has been replicated.
|
|
|
|
|
testFn1 := func(t *testing.T, s *Store) {
|
|
|
|
|
qr := queryRequestFromString("SELECT * FROM foo", false, false)
|
|
|
|
|
qr.Level = proto.QueryRequest_QUERY_REQUEST_LEVEL_NONE
|
|
|
|
|
r, err := s.Query(qr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to query single node: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
if exp, got := `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"fiona"]]}]`, asJSON(r); exp != got {
|
|
|
|
|
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
testFn1(t, s0)
|
|
|
|
|
testFn1(t, s1)
|
|
|
|
|
|
|
|
|
|
// Write another row using Request
|
|
|
|
|
rr := executeQueryRequestFromString("INSERT INTO foo(id, name) VALUES(2, 'fiona')", proto.QueryRequest_QUERY_REQUEST_LEVEL_STRONG, false, false)
|
|
|
|
|
_, err = s0.Request(rr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to execute on single node: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
testPoll(t, func() bool {
|
|
|
|
|
return s0.DBAppliedIndex() == s1.DBAppliedIndex()
|
|
|
|
|
}, 250*time.Millisecond, 3*time.Second)
|
|
|
|
|
|
|
|
|
|
// Now, do a NONE consistency query on each node, to actually confirm the data
|
|
|
|
|
// has been replicated.
|
|
|
|
|
testFn2 := func(t *testing.T, s *Store) {
|
|
|
|
|
qr := queryRequestFromString("SELECT COUNT(*) FROM foo", false, false)
|
|
|
|
|
qr.Level = proto.QueryRequest_QUERY_REQUEST_LEVEL_NONE
|
|
|
|
|
r, err := s.Query(qr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to query single node: %s", err.Error())
|
|
|
|
|
}
|
|
|
|
|
if exp, got := `[{"columns":["COUNT(*)"],"types":["integer"],"values":[[2]]}]`, asJSON(r); exp != got {
|
|
|
|
|
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
testFn2(t, s0)
|
|
|
|
|
testFn2(t, s1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test_MultiNodeSnapshot_ErrorMessage tests that a snapshot fails with a specific
|
|
|
|
|
// error message when the snapshot is attempted too soon after joining a cluster.
|
|
|
|
|
// Hashicorp Raft doesn't expose a typed error, so we have to check the error
|
|
|
|
|