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 }