mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
We had this fail in CI once, and failing took 30 minutes because we didn't have a timeout on this. This shouldn't ever fail, but the fact that it did indicates that the fabled etcd failures we've seen a couple of times were still capable of happening. This will make that failure happen sooner and more clearly. Also, log the cluster states (and possibly node states) while waiting. But add a delay -- otherwise we can do this quite a few times per millisecond. We use Logf so that, if you didn't use -v, you see these reported only if the test fails, but if the test fails, we'll say what happened. It would probably be better to have a passive thing that can wait for updates, because we're waiting on heartbeats. Missing: A way to detect what's actually happening in the failure cases, which we see only quite rarely.
148 lines
4 KiB
Go
148 lines
4 KiB
Go
package etcd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/disco"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// fakeCluster is a cluster of just our local etcd wrappers, without the
|
|
// rest of featurebase present. This code is expected to migrate into a
|
|
// _test.go file later, but for now it's here so we can see code coverage.
|
|
type fakeCluster struct {
|
|
tb testing.TB
|
|
nodes []*Etcd
|
|
}
|
|
|
|
func (f *fakeCluster) Start() error {
|
|
eg, ctx := errgroup.WithContext(context.Background())
|
|
for _, node := range f.nodes {
|
|
node := node
|
|
eg.Go(func() error {
|
|
_, err := node.Start(ctx)
|
|
return err
|
|
})
|
|
}
|
|
return eg.Wait()
|
|
}
|
|
|
|
func (f *fakeCluster) BringUp() error {
|
|
eg, ctx := errgroup.WithContext(context.Background())
|
|
for _, node := range f.nodes {
|
|
node := node
|
|
eg.Go(func() error {
|
|
return node.SetState(ctx, disco.NodeStateStarted)
|
|
})
|
|
}
|
|
return eg.Wait()
|
|
}
|
|
|
|
// AwaitClusterState verifies that node 0 thinks the cluster is in the
|
|
// requested state, or tells you why it failed.
|
|
func (f *fakeCluster) AwaitClusterState(expected disco.ClusterState, timeout time.Duration) (err error) {
|
|
// You might reasonably ask why we don't use context.WithTimeout for this.
|
|
// The answer is that "context deadline exceeded" would be ambiguous as to
|
|
// whether it was an internal error produced by the etcd stuff, or an error
|
|
// introduced by this function.
|
|
now := time.Now()
|
|
state, err := f.nodes[0].ClusterState(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for state != expected {
|
|
// wait a bit to see if it recovered
|
|
time.Sleep(50 * time.Millisecond)
|
|
state, err = f.nodes[0].ClusterState(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if state != expected {
|
|
nodes := f.nodes[0].Nodes()
|
|
for _, n := range nodes {
|
|
f.tb.Logf(" %s: %s", n.ID, n.State)
|
|
}
|
|
}
|
|
elapsed := time.Since(now)
|
|
if elapsed >= timeout {
|
|
return fmt.Errorf("cluster did not reach state %q after %v", expected, timeout)
|
|
}
|
|
f.tb.Logf("cluster state after %v: %q", elapsed, state)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MustAwaitClusterState verifies that node 0 thinks the cluster is in the
|
|
// requested state, or fails a test.
|
|
func (f *fakeCluster) MustAwaitClusterState(expected disco.ClusterState, timeout time.Duration) {
|
|
err := f.AwaitClusterState(expected, timeout)
|
|
if err != nil {
|
|
f.tb.Fatalf("awaiting cluster state %s: %v", expected, err)
|
|
}
|
|
}
|
|
|
|
func (f *fakeCluster) Stop() error {
|
|
for i, node := range f.nodes {
|
|
err := node.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("failure closing node %d: %v", i, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeCluster) MustNodeStates() []disco.NodeState {
|
|
nodes := f.nodes[0].Nodes()
|
|
states := make([]disco.NodeState, len(nodes))
|
|
for i, node := range nodes {
|
|
states[i] = node.State
|
|
}
|
|
return states
|
|
}
|
|
|
|
func (f *fakeCluster) MustClusterState() disco.ClusterState {
|
|
state, err := f.nodes[0].ClusterState(context.TODO())
|
|
if err != nil {
|
|
f.tb.Fatalf("getting cluster state: %v", err)
|
|
}
|
|
return state
|
|
}
|
|
|
|
// Elect tries to force a leader election by identifying a leader
|
|
// and then stopping it. The cluster, if it has at least 3 members,
|
|
// should still stay running.
|
|
func (f *fakeCluster) Elect() (oldleader *Etcd, err error) {
|
|
var leader *Etcd
|
|
for i, node := range f.nodes {
|
|
if node.IsLeader() {
|
|
f.tb.Logf("leader is node %d, stopping it", i)
|
|
leader = node
|
|
copy(f.nodes[i:], f.nodes[i+1:])
|
|
f.nodes = f.nodes[:len(f.nodes)-1]
|
|
}
|
|
}
|
|
// close the leader
|
|
err = leader.Close()
|
|
if err != nil {
|
|
return leader, err
|
|
}
|
|
_, err = f.nodes[0].ClusterState(context.TODO())
|
|
return leader, err
|
|
}
|
|
|
|
// NewFakeCluster creates a fakeCluster of n nodes, providing only the
|
|
// etcd objects, not the rest of a featurebase install.
|
|
func NewFakeCluster(tb testing.TB, n int, replicas int) *fakeCluster {
|
|
logger := logger.NewLogfLogger(tb)
|
|
_, opts := GenEtcdConfigs(tb, n)
|
|
fc := &fakeCluster{nodes: make([]*Etcd, n)}
|
|
for i := range fc.nodes {
|
|
fc.nodes[i] = NewEtcd(opts[i], logger, replicas, "foo")
|
|
}
|
|
fc.tb = tb
|
|
return fc
|
|
}
|