github.com/weaviate/weaviate@v1.24.6/usecases/cluster/transactions_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  	"sync"
    18  	"testing"
    19  	"time"
    20  
    21  	"github.com/sirupsen/logrus/hooks/test"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  func TestSuccessfulOutgoingWriteTransaction(t *testing.T) {
    27  	payload := "my-payload"
    28  	trType := TransactionType("my-type")
    29  	ctx := context.Background()
    30  
    31  	man := newTestTxManager()
    32  
    33  	tx, err := man.BeginTransaction(ctx, trType, payload, 0)
    34  	require.Nil(t, err)
    35  
    36  	err = man.CommitWriteTransaction(ctx, tx)
    37  	require.Nil(t, err)
    38  }
    39  
    40  func TestTryingToOpenTwoTransactions(t *testing.T) {
    41  	payload := "my-payload"
    42  	trType := TransactionType("my-type")
    43  	ctx := context.Background()
    44  
    45  	man := newTestTxManager()
    46  
    47  	tx1, err := man.BeginTransaction(ctx, trType, payload, 0)
    48  	require.Nil(t, err)
    49  
    50  	tx2, err := man.BeginTransaction(ctx, trType, payload, 0)
    51  	assert.Nil(t, tx2)
    52  	require.NotNil(t, err)
    53  	assert.Equal(t, "concurrent transaction", err.Error())
    54  
    55  	err = man.CommitWriteTransaction(ctx, tx1)
    56  	assert.Nil(t, err, "original transaction can still be committed")
    57  }
    58  
    59  func TestTryingToCommitInvalidTransaction(t *testing.T) {
    60  	payload := "my-payload"
    61  	trType := TransactionType("my-type")
    62  	ctx := context.Background()
    63  
    64  	man := newTestTxManager()
    65  
    66  	tx1, err := man.BeginTransaction(ctx, trType, payload, 0)
    67  	require.Nil(t, err)
    68  
    69  	invalidTx := &Transaction{ID: "invalid"}
    70  
    71  	err = man.CommitWriteTransaction(ctx, invalidTx)
    72  	require.NotNil(t, err)
    73  	assert.Equal(t, "invalid transaction", err.Error())
    74  
    75  	err = man.CommitWriteTransaction(ctx, tx1)
    76  	assert.Nil(t, err, "original transaction can still be committed")
    77  }
    78  
    79  func TestTryingToCommitTransactionPastTTL(t *testing.T) {
    80  	payload := "my-payload"
    81  	trType := TransactionType("my-type")
    82  	ctx := context.Background()
    83  
    84  	man := newTestTxManager()
    85  
    86  	tx1, err := man.BeginTransaction(ctx, trType, payload, time.Microsecond)
    87  	require.Nil(t, err)
    88  
    89  	expiredTx := &Transaction{ID: tx1.ID}
    90  
    91  	// give the cancel handler some time to run
    92  	time.Sleep(50 * time.Millisecond)
    93  
    94  	err = man.CommitWriteTransaction(ctx, expiredTx)
    95  	require.NotNil(t, err)
    96  	assert.Contains(t, err.Error(), "transaction TTL")
    97  
    98  	// make sure it is possible to open future transactions
    99  	_, err = man.BeginTransaction(context.Background(), trType, payload, 0)
   100  	require.Nil(t, err)
   101  }
   102  
   103  func TestTryingToCommitIncomingTransactionPastTTL(t *testing.T) {
   104  	payload := "my-payload"
   105  	trType := TransactionType("my-type")
   106  	ctx := context.Background()
   107  
   108  	man := newTestTxManager()
   109  
   110  	dl := time.Now().Add(1 * time.Microsecond)
   111  
   112  	tx := &Transaction{
   113  		ID:       "123456",
   114  		Type:     trType,
   115  		Payload:  payload,
   116  		Deadline: dl,
   117  	}
   118  
   119  	man.IncomingBeginTransaction(context.Background(), tx)
   120  
   121  	// give the cancel handler some time to run
   122  	time.Sleep(50 * time.Millisecond)
   123  
   124  	err := man.IncomingCommitTransaction(ctx, tx)
   125  	require.NotNil(t, err)
   126  	assert.Contains(t, err.Error(), "transaction TTL")
   127  
   128  	// make sure it is possible to open future transactions
   129  	_, err = man.BeginTransaction(context.Background(), trType, payload, 0)
   130  	require.Nil(t, err)
   131  }
   132  
   133  func TestLettingATransactionExpire(t *testing.T) {
   134  	payload := "my-payload"
   135  	trType := TransactionType("my-type")
   136  	ctx := context.Background()
   137  
   138  	man := newTestTxManager()
   139  
   140  	tx1, err := man.BeginTransaction(ctx, trType, payload, time.Microsecond)
   141  	require.Nil(t, err)
   142  
   143  	// give the cancel handler some time to run
   144  	time.Sleep(50 * time.Millisecond)
   145  
   146  	// try to open a new one
   147  	_, err = man.BeginTransaction(context.Background(), trType, payload, 0)
   148  	require.Nil(t, err)
   149  
   150  	// since the old one expired, we now expect a TTL error instead of a
   151  	// concurrent tx error when trying to refer to the old one
   152  	err = man.CommitWriteTransaction(context.Background(), tx1)
   153  	require.NotNil(t, err)
   154  	assert.Contains(t, err.Error(), "transaction TTL")
   155  }
   156  
   157  func TestRemoteDoesntAllowOpeningTransaction(t *testing.T) {
   158  	payload := "my-payload"
   159  	trType := TransactionType("my-type")
   160  	ctx := context.Background()
   161  	broadcaster := &fakeBroadcaster{
   162  		openErr: ErrConcurrentTransaction,
   163  	}
   164  
   165  	man := newTestTxManagerWithRemote(broadcaster)
   166  
   167  	tx1, err := man.BeginTransaction(ctx, trType, payload, 0)
   168  	require.Nil(t, tx1)
   169  	require.NotNil(t, err)
   170  	assert.Contains(t, err.Error(), "open transaction")
   171  
   172  	assert.Len(t, broadcaster.abortCalledId, 36, "a valid uuid was aborted")
   173  }
   174  
   175  func TestRemoteDoesntAllowOpeningTransactionAbortFails(t *testing.T) {
   176  	payload := "my-payload"
   177  	trType := TransactionType("my-type")
   178  	ctx := context.Background()
   179  	broadcaster := &fakeBroadcaster{
   180  		openErr:  ErrConcurrentTransaction,
   181  		abortErr: fmt.Errorf("cannot abort"),
   182  	}
   183  
   184  	man, hook := newTestTxManagerWithRemoteLoggerHook(broadcaster)
   185  
   186  	tx1, err := man.BeginTransaction(ctx, trType, payload, 0)
   187  	require.Nil(t, tx1)
   188  	require.NotNil(t, err)
   189  	assert.Contains(t, err.Error(), "open transaction")
   190  
   191  	assert.Len(t, broadcaster.abortCalledId, 36, "a valid uuid was aborted")
   192  
   193  	require.Len(t, hook.Entries, 1)
   194  	assert.Equal(t, "broadcast tx abort failed", hook.Entries[0].Message)
   195  }
   196  
   197  type fakeBroadcaster struct {
   198  	openErr       error
   199  	commitErr     error
   200  	abortErr      error
   201  	abortCalledId string
   202  }
   203  
   204  func (f *fakeBroadcaster) BroadcastTransaction(ctx context.Context,
   205  	tx *Transaction,
   206  ) error {
   207  	return f.openErr
   208  }
   209  
   210  func (f *fakeBroadcaster) BroadcastAbortTransaction(ctx context.Context,
   211  	tx *Transaction,
   212  ) error {
   213  	f.abortCalledId = tx.ID
   214  	return f.abortErr
   215  }
   216  
   217  func (f *fakeBroadcaster) BroadcastCommitTransaction(ctx context.Context,
   218  	tx *Transaction,
   219  ) error {
   220  	return f.commitErr
   221  }
   222  
   223  func TestSuccessfulDistributedWriteTransaction(t *testing.T) {
   224  	ctx := context.Background()
   225  
   226  	var remoteState interface{}
   227  	remote := newTestTxManager()
   228  	remote.SetCommitFn(func(ctx context.Context, tx *Transaction) error {
   229  		remoteState = tx.Payload
   230  		return nil
   231  	})
   232  	local := NewTxManager(&wrapTxManagerAsBroadcaster{remote},
   233  		&fakeTxPersistence{}, remote.logger)
   234  	local.StartAcceptIncoming()
   235  
   236  	payload := "my-payload"
   237  	trType := TransactionType("my-type")
   238  
   239  	tx, err := local.BeginTransaction(ctx, trType, payload, 0)
   240  	require.Nil(t, err)
   241  
   242  	err = local.CommitWriteTransaction(ctx, tx)
   243  	require.Nil(t, err)
   244  
   245  	assert.Equal(t, "my-payload", remoteState)
   246  }
   247  
   248  func TestConcurrentDistributedTransaction(t *testing.T) {
   249  	ctx := context.Background()
   250  
   251  	var remoteState interface{}
   252  	remote := newTestTxManager()
   253  	remote.SetCommitFn(func(ctx context.Context, tx *Transaction) error {
   254  		remoteState = tx.Payload
   255  		return nil
   256  	})
   257  	local := NewTxManager(&wrapTxManagerAsBroadcaster{remote},
   258  		&fakeTxPersistence{}, remote.logger)
   259  
   260  	payload := "my-payload"
   261  	trType := TransactionType("my-type")
   262  
   263  	// open a transaction on the remote to simulate a concurrent transaction.
   264  	// Since it uses the fakeBroadcaster it does not tell anyone about it, this
   265  	// way we can be sure that the reason for failure is actually a concurrent
   266  	// transaction on the remote side, not on the local side. Compare this to a
   267  	// situation where broadcasting was bi-directional: Then this transaction
   268  	// would have been opened successfully and already be replicated to the
   269  	// "local" tx manager. So the next call on "local" would also fail, but for
   270  	// the wrong reason: It would fail because another transaction is already in
   271  	// place. We, however want to simulate a situation where due to network
   272  	// delays, etc. both sides try to open a transaction more or less in
   273  	// parallel.
   274  	_, err := remote.BeginTransaction(ctx, trType, "wrong payload", 0)
   275  	require.Nil(t, err)
   276  
   277  	tx, err := local.BeginTransaction(ctx, trType, payload, 0)
   278  	require.Nil(t, tx)
   279  	require.NotNil(t, err)
   280  	assert.Contains(t, err.Error(), "concurrent transaction")
   281  
   282  	assert.Equal(t, nil, remoteState, "remote state should not have been updated")
   283  }
   284  
   285  // This test simulates three nodes trying to open a tx at basically the same
   286  // time with the simulated network being so slow that other nodes will try to
   287  // open their own transactions before they receive the incoming tx. This is a
   288  // situation where everyone thinks they were the first to open the tx and there
   289  // is no clear winner. All attempts must fail!
   290  func TestConcurrentOpenAttemptsOnSlowNetwork(t *testing.T) {
   291  	ctx := context.Background()
   292  
   293  	broadcaster := &slowMultiBroadcaster{delay: 100 * time.Millisecond}
   294  	node1 := newTestTxManagerWithRemote(broadcaster)
   295  	node2 := newTestTxManagerWithRemote(broadcaster)
   296  	node3 := newTestTxManagerWithRemote(broadcaster)
   297  
   298  	broadcaster.nodes = []*TxManager{node1, node2, node3}
   299  
   300  	trType := TransactionType("my-type")
   301  
   302  	wg := &sync.WaitGroup{}
   303  	wg.Add(1)
   304  	go func() {
   305  		defer wg.Done()
   306  		_, err := node1.BeginTransaction(ctx, trType, "payload-from-node-1", 0)
   307  		assert.NotNil(t, err, "open tx 1 must fail")
   308  	}()
   309  
   310  	wg.Add(1)
   311  	go func() {
   312  		defer wg.Done()
   313  		_, err := node2.BeginTransaction(ctx, trType, "payload-from-node-2", 0)
   314  		assert.NotNil(t, err, "open tx 2 must fail")
   315  	}()
   316  
   317  	wg.Add(1)
   318  	go func() {
   319  		defer wg.Done()
   320  		_, err := node3.BeginTransaction(ctx, trType, "payload-from-node-3", 0)
   321  		assert.NotNil(t, err, "open tx 3 must fail")
   322  	}()
   323  
   324  	wg.Wait()
   325  }
   326  
   327  type wrapTxManagerAsBroadcaster struct {
   328  	txManager *TxManager
   329  }
   330  
   331  func (w *wrapTxManagerAsBroadcaster) BroadcastTransaction(ctx context.Context,
   332  	tx *Transaction,
   333  ) error {
   334  	_, err := w.txManager.IncomingBeginTransaction(ctx, tx)
   335  	return err
   336  }
   337  
   338  func (w *wrapTxManagerAsBroadcaster) BroadcastAbortTransaction(ctx context.Context,
   339  	tx *Transaction,
   340  ) error {
   341  	w.txManager.IncomingAbortTransaction(ctx, tx)
   342  	return nil
   343  }
   344  
   345  func (w *wrapTxManagerAsBroadcaster) BroadcastCommitTransaction(ctx context.Context,
   346  	tx *Transaction,
   347  ) error {
   348  	return w.txManager.IncomingCommitTransaction(ctx, tx)
   349  }
   350  
   351  type slowMultiBroadcaster struct {
   352  	delay time.Duration
   353  	nodes []*TxManager
   354  }
   355  
   356  func (b *slowMultiBroadcaster) BroadcastTransaction(ctx context.Context,
   357  	tx *Transaction,
   358  ) error {
   359  	time.Sleep(b.delay)
   360  	for _, node := range b.nodes {
   361  		if _, err := node.IncomingBeginTransaction(ctx, tx); err != nil {
   362  			return err
   363  		}
   364  	}
   365  	return nil
   366  }
   367  
   368  func (b *slowMultiBroadcaster) BroadcastAbortTransaction(ctx context.Context,
   369  	tx *Transaction,
   370  ) error {
   371  	time.Sleep(b.delay)
   372  	for _, node := range b.nodes {
   373  		node.IncomingAbortTransaction(ctx, tx)
   374  	}
   375  
   376  	return nil
   377  }
   378  
   379  func (b *slowMultiBroadcaster) BroadcastCommitTransaction(ctx context.Context,
   380  	tx *Transaction,
   381  ) error {
   382  	time.Sleep(b.delay)
   383  	for _, node := range b.nodes {
   384  		if err := node.IncomingCommitTransaction(ctx, tx); err != nil {
   385  			return err
   386  		}
   387  	}
   388  
   389  	return nil
   390  }
   391  
   392  func TestSuccessfulDistributedReadTransaction(t *testing.T) {
   393  	ctx := context.Background()
   394  	payload := "my-payload"
   395  
   396  	remote := newTestTxManager()
   397  	remote.SetResponseFn(func(ctx context.Context, tx *Transaction) ([]byte, error) {
   398  		tx.Payload = payload
   399  		return nil, nil
   400  	})
   401  	local := NewTxManager(&wrapTxManagerAsBroadcaster{remote},
   402  		&fakeTxPersistence{}, remote.logger)
   403  	// TODO local.SetConsensusFn
   404  
   405  	trType := TransactionType("my-read-tx")
   406  
   407  	tx, err := local.BeginTransaction(ctx, trType, nil, 0)
   408  	require.Nil(t, err)
   409  
   410  	local.CloseReadTransaction(ctx, tx)
   411  
   412  	assert.Equal(t, "my-payload", tx.Payload)
   413  }
   414  
   415  func TestSuccessfulDistributedTransactionSetAllowUnready(t *testing.T) {
   416  	ctx := context.Background()
   417  	payload := "my-payload"
   418  
   419  	types := []TransactionType{"type0", "type1"}
   420  	remote := newTestTxManagerAllowUnready(types)
   421  	remote.SetResponseFn(func(ctx context.Context, tx *Transaction) ([]byte, error) {
   422  		tx.Payload = payload
   423  		return nil, nil
   424  	})
   425  	local := NewTxManager(&wrapTxManagerAsBroadcaster{remote},
   426  		&fakeTxPersistence{}, remote.logger)
   427  	local.SetAllowUnready(types)
   428  
   429  	trType := TransactionType("my-read-tx")
   430  
   431  	tx, err := local.BeginTransaction(ctx, trType, nil, 0)
   432  	require.Nil(t, err)
   433  
   434  	local.CloseReadTransaction(ctx, tx)
   435  
   436  	assert.ElementsMatch(t, types, remote.allowUnready)
   437  	assert.ElementsMatch(t, types, local.allowUnready)
   438  	assert.Equal(t, "my-payload", tx.Payload)
   439  }
   440  
   441  func TestTxWithDeadline(t *testing.T) {
   442  	t.Run("expired", func(t *testing.T) {
   443  		payload := "my-payload"
   444  		trType := TransactionType("my-type")
   445  
   446  		ctx := context.Background()
   447  
   448  		man := newTestTxManager()
   449  
   450  		tx, err := man.BeginTransaction(ctx, trType, payload, 1*time.Nanosecond)
   451  		require.Nil(t, err)
   452  
   453  		ctx, cancel := context.WithDeadline(context.Background(), tx.Deadline)
   454  		defer cancel()
   455  
   456  		assert.NotNil(t, ctx.Err())
   457  	})
   458  
   459  	t.Run("still valid", func(t *testing.T) {
   460  		payload := "my-payload"
   461  		trType := TransactionType("my-type")
   462  
   463  		ctx := context.Background()
   464  
   465  		man := newTestTxManager()
   466  
   467  		tx, err := man.BeginTransaction(ctx, trType, payload, 10*time.Second)
   468  		require.Nil(t, err)
   469  
   470  		ctx, cancel := context.WithDeadline(context.Background(), tx.Deadline)
   471  		defer cancel()
   472  
   473  		assert.Nil(t, ctx.Err())
   474  	})
   475  }
   476  
   477  func newTestTxManager() *TxManager {
   478  	logger, _ := test.NewNullLogger()
   479  	m := NewTxManager(&fakeBroadcaster{}, &fakeTxPersistence{}, logger)
   480  	m.StartAcceptIncoming()
   481  	return m
   482  }
   483  
   484  func newTestTxManagerWithRemote(remote Remote) *TxManager {
   485  	logger, _ := test.NewNullLogger()
   486  	m := NewTxManager(remote, &fakeTxPersistence{}, logger)
   487  	m.StartAcceptIncoming()
   488  	return m
   489  }
   490  
   491  func newTestTxManagerWithRemoteLoggerHook(remote Remote) (*TxManager, *test.Hook) {
   492  	logger, hook := test.NewNullLogger()
   493  	m := NewTxManager(remote, &fakeTxPersistence{}, logger)
   494  	m.StartAcceptIncoming()
   495  	return m, hook
   496  }
   497  
   498  func newTestTxManagerAllowUnready(types []TransactionType) *TxManager {
   499  	logger, _ := test.NewNullLogger()
   500  	m := NewTxManager(&fakeBroadcaster{}, &fakeTxPersistence{}, logger)
   501  	m.SetAllowUnready(types)
   502  	m.StartAcceptIncoming()
   503  	return m
   504  }
   505  
   506  // does nothing as these do not involve crashes
   507  type fakeTxPersistence struct{}
   508  
   509  func (f *fakeTxPersistence) StoreTx(ctx context.Context,
   510  	tx *Transaction,
   511  ) error {
   512  	return nil
   513  }
   514  
   515  func (f *fakeTxPersistence) DeleteTx(ctx context.Context,
   516  	txID string,
   517  ) error {
   518  	return nil
   519  }
   520  
   521  func (f *fakeTxPersistence) IterateAll(ctx context.Context,
   522  	cb func(tx *Transaction),
   523  ) error {
   524  	return nil
   525  }