Repository navigation
Expand file tree
/
Copy pathfailover_test.go
More file actions
131 lines (117 loc) · 3.14 KB
/
Copy pathfailover_test.go
File metadata and controls
131 lines (117 loc) · 3.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package desync
import (
"context"
"crypto/rand"
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestFailoverMissingChunk(t *testing.T) {
s := &TestStore{}
g := NewFailoverGroup(s)
_, err := g.GetChunk(ChunkID{0})
require.IsType(t, ChunkMissing{}, err)
}
func TestFailoverAllError(t *testing.T) {
var failed = errors.New("failed")
storeFail := &TestStore{
GetChunkFunc: func(ChunkID) (*Chunk, error) { return nil, failed },
}
g := NewFailoverGroup(storeFail, storeFail)
_, err := g.GetChunk(ChunkID{0})
require.ErrorIs(t, err, failed)
}
func TestFailoverSimple(t *testing.T) {
// Create two stores, one that always fails and one that works
storeFail := &TestStore{
GetChunkFunc: func(ChunkID) (*Chunk, error) { return nil, errors.New("failed") },
}
storeSucc := &TestStore{
GetChunkFunc: func(ChunkID) (*Chunk, error) { return nil, nil },
}
// Group the two stores together, the failing ones first
g := NewFailoverGroup(storeFail, storeFail, storeSucc)
// Request a chunk, should succeed
_, err := g.GetChunk(ChunkID{0})
require.NoError(t, err)
// Look inside the group to confirm we failed over to the last one
require.Equal(t, 2, g.active)
}
func TestFailoverMutliple(t *testing.T) {
// Create two stores, one that fails when x is 1 and the other fails when x is 0.
//
// mu holds x still for the duration of a whole GetChunk call. The group
// tries each store at most once per call, so a switch landing between
// those two attempts leaves the call having seen both stores fail, and it
// returns an error - which is what the group documents it will do when
// every store fails, not a bug. Without this the test asks for something
// the implementation never promised, and fails whenever a goroutine is
// descheduled mid-call: rarely on an idle machine, readily on a loaded or
// emulated one.
var (
mu sync.RWMutex
x int
)
storeA := &TestStore{
GetChunkFunc: func(id ChunkID) (*Chunk, error) {
if x == 0 {
return nil, nil
}
return nil, errors.New("failed")
},
}
storeB := &TestStore{
GetChunkFunc: func(id ChunkID) (*Chunk, error) {
if x == 1 {
return nil, nil
}
return nil, errors.New("failed")
},
}
// Group the two stores together, the failing ones first
g := NewFailoverGroup(storeA, storeB)
var (
ctx, cancel = context.WithTimeout(t.Context(), time.Second)
eg, gCtx = errgroup.WithContext(ctx)
failOver = time.Tick(10 * time.Millisecond)
)
defer cancel()
// Run several goroutines querying the group in a tight loop
for range 16 {
eg.Go(func() error {
var id ChunkID
for {
time.Sleep(time.Millisecond)
select {
case <-gCtx.Done():
return nil
default:
rand.Read(id[:])
mu.RLock()
_, err := g.GetChunk(id)
mu.RUnlock()
if err != nil {
return err
}
}
}
})
}
// Make the stores fail over every 10 ms
eg.Go(func() error {
for {
select {
case <-gCtx.Done(): // done running
return nil
case <-failOver: // switch over to the other store
mu.Lock()
x = (x + 1) % 2
mu.Unlock()
}
}
})
require.NoError(t, eg.Wait())
}