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  }