github.com/weaviate/weaviate@v1.24.6/usecases/cluster/transactions_broadcast_test.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 package cluster 13 14 import ( 15 "context" 16 "fmt" 17 "strings" 18 "sync" 19 "testing" 20 "time" 21 22 "github.com/sirupsen/logrus/hooks/test" 23 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/require" 26 ) 27 28 var logger, _ = test.NewNullLogger() 29 30 func TestBroadcastOpenTransaction(t *testing.T) { 31 client := &fakeClient{} 32 state := &fakeState{hosts: []string{"host1", "host2", "host3"}} 33 34 bc := NewTxBroadcaster(state, client, logger) 35 36 tx := &Transaction{ID: "foo"} 37 38 err := bc.BroadcastTransaction(context.Background(), tx) 39 require.Nil(t, err) 40 41 assert.ElementsMatch(t, []string{"host1", "host2", "host3"}, client.openCalled) 42 } 43 44 func TestBroadcastOpenTransactionWithReturnPayload(t *testing.T) { 45 client := &fakeClient{} 46 state := &fakeState{hosts: []string{"host1", "host2", "host3"}} 47 48 bc := NewTxBroadcaster(state, client, logger) 49 bc.SetConsensusFunction(func(ctx context.Context, 50 in []*Transaction, 51 ) (*Transaction, error) { 52 // instead of actually reaching a consensus this test mock simply merged 53 // all the individual results. For testing purposes this is even better 54 // because now we can be sure that every element was considered. 55 merged := "" 56 for _, tx := range in { 57 if len(merged) > 0 { 58 merged += "," 59 } 60 merged += tx.Payload.(string) 61 } 62 63 return &Transaction{ 64 Payload: merged, 65 }, nil 66 }) 67 68 tx := &Transaction{ID: "foo"} 69 70 err := bc.BroadcastTransaction(context.Background(), tx) 71 require.Nil(t, err) 72 73 assert.ElementsMatch(t, []string{"host1", "host2", "host3"}, client.openCalled) 74 75 results := strings.Split(tx.Payload.(string), ",") 76 assert.ElementsMatch(t, []string{ 77 "hello_from_host1", 78 "hello_from_host2", 79 "hello_from_host3", 80 }, results) 81 } 82 83 func TestBroadcastOpenTransactionAfterNodeHasDied(t *testing.T) { 84 client := &fakeClient{} 85 state := &fakeState{hosts: []string{"host1", "host2", "host3"}} 86 bc := NewTxBroadcaster(state, client, logger) 87 88 waitUntilIdealStateHasReached(t, bc, 3, 4*time.Second) 89 90 // host2 is dead 91 state.updateHosts([]string{"host1", "host3"}) 92 93 tx := &Transaction{ID: "foo"} 94 95 err := bc.BroadcastTransaction(context.Background(), tx) 96 require.NotNil(t, err) 97 assert.Contains(t, err.Error(), "host2") 98 99 // no node is should have received an open 100 assert.ElementsMatch(t, []string{}, client.openCalled) 101 } 102 103 func waitUntilIdealStateHasReached(t *testing.T, bc *TxBroadcaster, goal int, 104 max time.Duration, 105 ) { 106 ctx, cancel := context.WithTimeout(context.Background(), max) 107 defer cancel() 108 109 interval := time.NewTicker(250 * time.Millisecond) 110 defer interval.Stop() 111 112 for { 113 select { 114 case <-ctx.Done(): 115 t.Error(fmt.Errorf("waiting to reach state goal %d: %w", goal, ctx.Err())) 116 return 117 case <-interval.C: 118 if len(bc.ideal.Members()) == goal { 119 return 120 } 121 } 122 } 123 } 124 125 func TestBroadcastAbortTransaction(t *testing.T) { 126 client := &fakeClient{} 127 state := &fakeState{hosts: []string{"host1", "host2", "host3"}} 128 129 bc := NewTxBroadcaster(state, client, logger) 130 131 tx := &Transaction{ID: "foo"} 132 133 err := bc.BroadcastAbortTransaction(context.Background(), tx) 134 require.Nil(t, err) 135 136 assert.ElementsMatch(t, []string{"host1", "host2", "host3"}, client.abortCalled) 137 } 138 139 func TestBroadcastCommitTransaction(t *testing.T) { 140 client := &fakeClient{} 141 state := &fakeState{hosts: []string{"host1", "host2", "host3"}} 142 143 bc := NewTxBroadcaster(state, client, logger) 144 145 tx := &Transaction{ID: "foo"} 146 147 err := bc.BroadcastCommitTransaction(context.Background(), tx) 148 require.Nil(t, err) 149 150 assert.ElementsMatch(t, []string{"host1", "host2", "host3"}, client.commitCalled) 151 } 152 153 func TestBroadcastCommitTransactionAfterNodeHasDied(t *testing.T) { 154 client := &fakeClient{} 155 state := &fakeState{hosts: []string{"host1", "host2", "host3"}} 156 bc := NewTxBroadcaster(state, client, logger) 157 158 waitUntilIdealStateHasReached(t, bc, 3, 4*time.Second) 159 160 state.updateHosts([]string{"host1", "host3"}) 161 162 tx := &Transaction{ID: "foo"} 163 164 err := bc.BroadcastCommitTransaction(context.Background(), tx) 165 require.NotNil(t, err) 166 assert.Contains(t, err.Error(), "host2") 167 168 // no node should have received the commit 169 assert.ElementsMatch(t, []string{}, client.commitCalled) 170 } 171 172 type fakeState struct { 173 hosts []string 174 sync.Mutex 175 } 176 177 func (f *fakeState) updateHosts(newHosts []string) { 178 f.Lock() 179 defer f.Unlock() 180 181 f.hosts = newHosts 182 } 183 184 func (f *fakeState) Hostnames() []string { 185 f.Lock() 186 defer f.Unlock() 187 188 return f.hosts 189 } 190 191 func (f *fakeState) AllNames() []string { 192 f.Lock() 193 defer f.Unlock() 194 195 return f.hosts 196 } 197 198 type fakeClient struct { 199 sync.Mutex 200 openCalled []string 201 abortCalled []string 202 commitCalled []string 203 } 204 205 func (f *fakeClient) OpenTransaction(ctx context.Context, host string, tx *Transaction) error { 206 f.Lock() 207 defer f.Unlock() 208 209 f.openCalled = append(f.openCalled, host) 210 tx.Payload = "hello_from_" + host 211 return nil 212 } 213 214 func (f *fakeClient) AbortTransaction(ctx context.Context, host string, tx *Transaction) error { 215 f.Lock() 216 defer f.Unlock() 217 218 f.abortCalled = append(f.abortCalled, host) 219 return nil 220 } 221 222 func (f *fakeClient) CommitTransaction(ctx context.Context, host string, tx *Transaction) error { 223 f.Lock() 224 defer f.Unlock() 225 226 f.commitCalled = append(f.commitCalled, host) 227 return nil 228 }