github.com/hashicorp/vault/sdk@v0.13.0/physical/transactions.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package physical 5 6 import ( 7 "context" 8 "fmt" 9 10 "github.com/hashicorp/go-multierror" 11 ) 12 13 // TxnEntry is an operation that takes atomically as part of 14 // a transactional update. Only supported by Transactional backends. 15 type TxnEntry struct { 16 Operation Operation 17 Entry *Entry 18 } 19 20 func (t *TxnEntry) String() string { 21 return fmt.Sprintf("Operation: %s. Entry: %s", t.Operation, t.Entry) 22 } 23 24 // Transactional is an optional interface for backends that 25 // support doing transactional updates of multiple keys. This is 26 // required for some features such as replication. 27 type Transactional interface { 28 // The function to run a transaction 29 Transaction(context.Context, []*TxnEntry) error 30 } 31 32 type TransactionalBackend interface { 33 Backend 34 Transactional 35 } 36 37 // TransactionalLimits SHOULD be implemented by all TransactionalBackend 38 // implementations. It is separate for backwards compatibility reasons since 39 // this in a public SDK module. If a TransactionalBackend does not implement 40 // this, the historic default limits of 63 entries and 128kb (based on Consul's 41 // limits) are used by replication internals when encoding batches of 42 // transactions. 43 type TransactionalLimits interface { 44 TransactionalBackend 45 46 // TransactionLimits must return the limits of how large each transaction may 47 // be. The limits returned indicate how many individual operation entries are 48 // supported in total and an overall size limit on the contents of each 49 // transaction if applicable. Vault will deduct any meta-operations it needs 50 // to add from the maxEntries given. maxSize will be compared against the sum 51 // of the key and value sizes for all operations in a transaction. The backend 52 // should provide a reasonable margin of safety for any overhead it may have 53 // while encoding, for example Consul's encoded transaction in JSON must fit 54 // in the configured max transaction size so it must leave adequate room for 55 // JSON encoding overhead on top of the raw key and value sizes. 56 // 57 // If zero is returned for either value, the replication internals will use 58 // historic reasonable defaults. This allows middleware implementations such 59 // as cache layers to either pass through to the underlying backend if it 60 // implements this interface, or to return zeros to indicate that the 61 // implementer should apply whatever defaults it would use if the middleware 62 // were not present. 63 TransactionLimits() (maxEntries int, maxSize int) 64 } 65 66 type PseudoTransactional interface { 67 // An internal function should do no locking or permit pool acquisition. 68 // Depending on the backend and if it natively supports transactions, these 69 // may simply chain to the normal backend functions. 70 GetInternal(context.Context, string) (*Entry, error) 71 PutInternal(context.Context, *Entry) error 72 DeleteInternal(context.Context, string) error 73 } 74 75 // Implements the transaction interface 76 func GenericTransactionHandler(ctx context.Context, t PseudoTransactional, txns []*TxnEntry) (retErr error) { 77 rollbackStack := make([]*TxnEntry, 0, len(txns)) 78 var dirty bool 79 80 // Update all of our GET transaction entries, so we can populate existing values back at the wal layer. 81 for _, txn := range txns { 82 if txn.Operation == GetOperation { 83 entry, err := t.GetInternal(ctx, txn.Entry.Key) 84 if err != nil { 85 return err 86 } 87 if entry != nil { 88 txn.Entry.Value = entry.Value 89 } 90 } 91 } 92 93 // We walk the transactions in order; each successful operation goes into a 94 // LIFO for rollback if we hit an error along the way 95 TxnWalk: 96 for _, txn := range txns { 97 switch txn.Operation { 98 case DeleteOperation: 99 entry, err := t.GetInternal(ctx, txn.Entry.Key) 100 if err != nil { 101 retErr = multierror.Append(retErr, err) 102 dirty = true 103 break TxnWalk 104 } 105 if entry == nil { 106 // Nothing to delete or roll back 107 continue 108 } 109 rollbackEntry := &TxnEntry{ 110 Operation: PutOperation, 111 Entry: &Entry{ 112 Key: entry.Key, 113 Value: entry.Value, 114 }, 115 } 116 err = t.DeleteInternal(ctx, txn.Entry.Key) 117 if err != nil { 118 retErr = multierror.Append(retErr, err) 119 dirty = true 120 break TxnWalk 121 } 122 rollbackStack = append([]*TxnEntry{rollbackEntry}, rollbackStack...) 123 124 case PutOperation: 125 entry, err := t.GetInternal(ctx, txn.Entry.Key) 126 if err != nil { 127 retErr = multierror.Append(retErr, err) 128 dirty = true 129 break TxnWalk 130 } 131 132 // Nothing existed so in fact rolling back requires a delete 133 var rollbackEntry *TxnEntry 134 if entry == nil { 135 rollbackEntry = &TxnEntry{ 136 Operation: DeleteOperation, 137 Entry: &Entry{ 138 Key: txn.Entry.Key, 139 }, 140 } 141 } else { 142 rollbackEntry = &TxnEntry{ 143 Operation: PutOperation, 144 Entry: &Entry{ 145 Key: entry.Key, 146 Value: entry.Value, 147 }, 148 } 149 } 150 151 err = t.PutInternal(ctx, txn.Entry) 152 if err != nil { 153 retErr = multierror.Append(retErr, err) 154 dirty = true 155 break TxnWalk 156 } 157 rollbackStack = append([]*TxnEntry{rollbackEntry}, rollbackStack...) 158 } 159 } 160 161 // Need to roll back because we hit an error along the way 162 if dirty { 163 // While traversing this, if we get an error, we continue anyways in 164 // best-effort fashion 165 for _, txn := range rollbackStack { 166 switch txn.Operation { 167 case DeleteOperation: 168 err := t.DeleteInternal(ctx, txn.Entry.Key) 169 if err != nil { 170 retErr = multierror.Append(retErr, err) 171 } 172 case PutOperation: 173 err := t.PutInternal(ctx, txn.Entry) 174 if err != nil { 175 retErr = multierror.Append(retErr, err) 176 } 177 } 178 } 179 } 180 181 return 182 }