github.com/weaviate/weaviate@v1.24.6/usecases/cluster/transactions_broadcast.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  	"time"
    18  
    19  	"github.com/sirupsen/logrus"
    20  	enterrors "github.com/weaviate/weaviate/entities/errors"
    21  
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  type TxBroadcaster struct {
    26  	state       MemberLister
    27  	client      Client
    28  	consensusFn ConsensusFn
    29  	ideal       *IdealClusterState
    30  	logger      logrus.FieldLogger
    31  }
    32  
    33  // The Broadcaster is the link between the the current node and all other nodes
    34  // during a tx operation. This makes it a natural place to inject a consensus
    35  // function for read transactions. How consensus is reached is completely opaque
    36  // to the broadcaster and can be controlled through custom business logic.
    37  type ConsensusFn func(ctx context.Context,
    38  	in []*Transaction) (*Transaction, error)
    39  
    40  type Client interface {
    41  	OpenTransaction(ctx context.Context, host string, tx *Transaction) error
    42  	AbortTransaction(ctx context.Context, host string, tx *Transaction) error
    43  	CommitTransaction(ctx context.Context, host string, tx *Transaction) error
    44  }
    45  
    46  type MemberLister interface {
    47  	AllNames() []string
    48  	Hostnames() []string
    49  }
    50  
    51  func NewTxBroadcaster(state MemberLister, client Client, logger logrus.FieldLogger) *TxBroadcaster {
    52  	ideal := NewIdealClusterState(state, logger)
    53  	return &TxBroadcaster{
    54  		state:  state,
    55  		client: client,
    56  		ideal:  ideal,
    57  		logger: logger,
    58  	}
    59  }
    60  
    61  func (t *TxBroadcaster) SetConsensusFunction(fn ConsensusFn) {
    62  	t.consensusFn = fn
    63  }
    64  
    65  func (t *TxBroadcaster) BroadcastTransaction(rootCtx context.Context, tx *Transaction) error {
    66  	if !tx.TolerateNodeFailures {
    67  		if err := t.ideal.Validate(); err != nil {
    68  			return fmt.Errorf("tx does not tolerate node failures: %w", err)
    69  		}
    70  	}
    71  
    72  	hosts := t.state.Hostnames()
    73  	resTx := make([]*Transaction, len(hosts))
    74  	eg := enterrors.NewErrorGroupWrapper(t.logger)
    75  	for i, host := range hosts {
    76  		i := i       // https://golang.org/doc/faq#closures_and_goroutines
    77  		host := host // https://golang.org/doc/faq#closures_and_goroutines
    78  
    79  		eg.Go(func() error {
    80  			// make sure we don't block forever if the caller passes in an unlimited
    81  			// context. If another node does not respond within the timeout, consider
    82  			// the tx open attempt failed.
    83  			ctx, cancel := context.WithTimeout(rootCtx, 30*time.Second)
    84  			defer cancel()
    85  
    86  			// the client call can mutate the tx, so we need to work with copies to
    87  			// prevent a race and to be able to keep all individual results, so they
    88  			// can be passed to the consensus fn
    89  			resTx[i] = copyTx(tx)
    90  			if err := t.client.OpenTransaction(ctx, host, resTx[i]); err != nil {
    91  				return errors.Wrapf(err, "host %q", host)
    92  			}
    93  
    94  			return nil
    95  		}, host)
    96  	}
    97  
    98  	err := eg.Wait()
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	if t.consensusFn != nil {
   104  		merged, err := t.consensusFn(rootCtx, resTx)
   105  		if err != nil {
   106  			return fmt.Errorf("try to reach consenus: %w", err)
   107  		}
   108  
   109  		if merged != nil {
   110  			tx.Payload = merged.Payload
   111  		}
   112  	}
   113  
   114  	return nil
   115  }
   116  
   117  func (t *TxBroadcaster) BroadcastAbortTransaction(rootCtx context.Context, tx *Transaction) error {
   118  	eg := enterrors.NewErrorGroupWrapper(t.logger)
   119  	for _, host := range t.state.Hostnames() {
   120  		host := host // https://golang.org/doc/faq#closures_and_goroutines
   121  		eg.Go(func() error {
   122  			// make sure we don't block forever if the caller passes in an unlimited
   123  			// context. If another node does not respond within the timeout, consider
   124  			// the tx abort attempt failed.
   125  			ctx, cancel := context.WithTimeout(rootCtx, 30*time.Second)
   126  			defer cancel()
   127  
   128  			if err := t.client.AbortTransaction(ctx, host, tx); err != nil {
   129  				return errors.Wrapf(err, "host %q", host)
   130  			}
   131  
   132  			return nil
   133  		}, host)
   134  	}
   135  
   136  	return eg.Wait()
   137  }
   138  
   139  func (t *TxBroadcaster) BroadcastCommitTransaction(rootCtx context.Context, tx *Transaction) error {
   140  	if !tx.TolerateNodeFailures {
   141  		if err := t.ideal.Validate(); err != nil {
   142  			return fmt.Errorf("tx does not tolerate node failures: %w", err)
   143  		}
   144  	}
   145  	eg := enterrors.NewErrorGroupWrapper(t.logger)
   146  	for _, host := range t.state.Hostnames() {
   147  		// make sure we don't block forever if the caller passes in an unlimited
   148  		// context. If another node does not respond within the timeout, consider
   149  		// the tx commit attempt failed.
   150  		ctx, cancel := context.WithTimeout(rootCtx, 30*time.Second)
   151  		defer cancel()
   152  		host := host // https://golang.org/doc/faq#closures_and_goroutines
   153  		eg.Go(func() error {
   154  			if err := t.client.CommitTransaction(ctx, host, tx); err != nil {
   155  				return errors.Wrapf(err, "host %q", host)
   156  			}
   157  
   158  			return nil
   159  		}, host)
   160  	}
   161  
   162  	return eg.Wait()
   163  }
   164  
   165  func copyTx(in *Transaction) *Transaction {
   166  	out := *in
   167  	return &out
   168  }