Skip to content
File

Blob: chord/overview_handler_test.go

go103 lines
1package chord
2 
3import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8 "time"
9 
10 "go.miragespace.co/specter/spec/chord"
11 
12 "github.com/stretchr/testify/require"
13)
14 
15// The nil embedded provider panics if the overview attempts any storage access.
16type unavailableOverviewStore struct{ chord.KVProvider }
17 
18func TestOverviewUsesLocalObservationsWithoutStorageOrRPC(t *testing.T) {
19 as := require.New(t)
20 conf := devConfig(t, as)
21 // Unexpected calls fail the test, including key enumeration or export.
22 conf.KVProvider = &unavailableOverviewStore{}
23 conf.Identity.Address = "local.example:443"
24 root := NewLocalNode(conf)
25 root.state.Set(chord.Active)
26 root.chordRate.IncrementBy(4)
27 root.kvRate.IncrementBy(5)
28 root.rpcErrorCount.Store(2)
29 root.kvStaleCount.Store(3)
30 stabilized := time.Now().UTC().Add(-time.Minute)
31 root.lastStabilized.Store(stabilized)
32 
33 otherConf := devConfig(t, as)
34 otherConf.KVProvider = &unavailableOverviewStore{}
35 other := NewLocalNode(otherConf)
36 other.state.Set(chord.Joining)
37 root.predecessor = other
38 root.successors = []chord.VNode{nil, other}
39 
40 w := httptest.NewRecorder()
41 before := time.Now().UTC()
42 OverviewHandler(root, []*LocalNode{root, other}, "sqlite").ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/_internal/overview.json", nil))
43 as.Equal(http.StatusOK, w.Code)
44 as.Equal("no-store", w.Header().Get("Cache-Control"))
45 as.Contains(w.Header().Get("Content-Type"), "application/json")
46 var info localOverview
47 as.NoError(json.Unmarshal(w.Body.Bytes(), &info))
48 as.WithinRange(info.ObservedAt, before, time.Now().UTC())
49 as.Equal("local.example:443", info.Identity.Address)
50 as.Equal("sqlite", info.Provider)
51 as.Len(info.VNodes, 2)
52 vnode := info.VNodes[0]
53 as.Equal("Active", vnode.State)
54 as.True(vnode.Root)
55 as.Equal(stabilized, *vnode.LastStabilized)
56 as.EqualValues(4, vnode.ChordRequests)
57 as.EqualValues(5, vnode.KVRequests)
58 as.EqualValues(2, vnode.RPCErrors)
59 as.EqualValues(3, vnode.KVStale)
60 as.True(vnode.PredecessorAvailable)
61 as.True(vnode.SuccessorsAvailable)
62 as.NotNil(vnode.Predecessor)
63 as.Len(vnode.Successors, 1)
64 as.Equal(vnode.Predecessor.ID, vnode.Successors[0].ID)
65 as.Equal("Joining", info.VNodes[1].State)
66 as.False(info.VNodes[1].Root)
67 as.Nil(info.VNodes[1].LastStabilized)
68 as.True(info.VNodes[1].PredecessorAvailable)
69 as.Nil(info.VNodes[1].Predecessor)
70 as.NotNil(info.VNodes[1].Successors, "empty successors must serialize as an array")
71 as.Empty(info.VNodes[1].Successors)
72}
73 
74func TestOverviewPreservesCountersWhileMembershipIsLocked(t *testing.T) {
75 as := require.New(t)
76 root := NewLocalNode(devConfig(t, as))
77 root.state.Set(chord.Transferring)
78 root.rpcErrorCount.Store(9)
79 root.predecessorMu.Lock()
80 defer root.predecessorMu.Unlock()
81 root.successorsMu.Lock()
82 defer root.successorsMu.Unlock()
83 
84 result := make(chan *httptest.ResponseRecorder, 1)
85 go func() {
86 w := httptest.NewRecorder()
87 OverviewHandler(root, []*LocalNode{root}, "aof").ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/_internal/overview.json", nil))
88 result <- w
89 }()
90 select {
91 case w := <-result:
92 as.Equal(http.StatusOK, w.Code)
93 var info localOverview
94 as.NoError(json.Unmarshal(w.Body.Bytes(), &info))
95 as.Equal("Transferring", info.VNodes[0].State)
96 as.EqualValues(9, info.VNodes[0].RPCErrors)
97 as.False(info.VNodes[0].PredecessorAvailable)
98 as.False(info.VNodes[0].SuccessorsAvailable)
99 case <-time.After(time.Second):
100 t.Fatal("overview blocked on a membership lock")
101 }
102}