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  }