go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/filter/txndefer/filter.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package txndefer implements a filter that calls best-effort callbacks on
    16  // successful transaction commits.
    17  //
    18  // Useful when an activity inside a transaction has some best-effort follow up
    19  // that should be done once the transaction has successfully landed.
    20  package txndefer
    21  
    22  import (
    23  	"context"
    24  	"sync"
    25  
    26  	ds "go.chromium.org/luci/gae/service/datastore"
    27  )
    28  
    29  // FilterRDS installs the datastore filter into the context.
    30  func FilterRDS(ctx context.Context) context.Context {
    31  	return ds.AddRawFilters(ctx, func(_ context.Context, inner ds.RawInterface) ds.RawInterface {
    32  		return filteredDS{inner}
    33  	})
    34  }
    35  
    36  // Defer schedules `cb` for execution when the current transaction successfully
    37  // lands.
    38  //
    39  // Intended for a best-effort non-transactional follow up to a successful
    40  // transaction. Note that in presence of failures there's no guarantee the
    41  // callback will be called. For example, the callback won't ever be called if
    42  // the process crashes right after landing the transaction. Or if the
    43  // transaction really landed, but RunInTransaction finished with "deadline
    44  // exceeded" (or some similar) error.
    45  //
    46  // Callbacks are executed sequentially in the reverse order they were deferred.
    47  // They receive the non-transactional version of the context initially passed to
    48  // RunInTransaction so that they inherit the deadline of the entire transaction.
    49  //
    50  // Panics if the given context is not transactional or there's no txndefer
    51  // filter installed.
    52  func Defer(ctx context.Context, cb func(context.Context)) {
    53  	state, _ := ctx.Value(&ctxKey).(*txnState)
    54  	if state == nil {
    55  		panic("not a transactional context or no txndefer filter installed")
    56  	}
    57  	state.deferCB(cb)
    58  }
    59  
    60  ////////////////////////////////////////////////////////////////////////////////
    61  
    62  var ctxKey = "txndefer.txnState"
    63  
    64  type txnState struct {
    65  	ctx context.Context // the original transaction context
    66  	m   sync.Mutex
    67  	cbs []func(context.Context)
    68  }
    69  
    70  func (s *txnState) deferCB(cb func(context.Context)) {
    71  	s.m.Lock()
    72  	s.cbs = append(s.cbs, cb)
    73  	s.m.Unlock()
    74  }
    75  
    76  func (s *txnState) execCBs() {
    77  	// Note: execCBs happens after RunInTransaction has finished. If it spawned
    78  	// any goroutines, they must have been finished already too (calling Defer
    79  	// from a goroutine that outlives a transaction is rightfully a race). Thus
    80  	// all writes to `s.cbs` are finished already and we also passed some
    81  	// synchronization barrier that waited for the goroutines to join. It's fine
    82  	// to avoid locking s.m in this case saving 200ns on hot code path.
    83  	if len(s.cbs) != 0 {
    84  		ctx := ds.WithoutTransaction(s.ctx)
    85  		for i := len(s.cbs) - 1; i >= 0; i-- {
    86  			s.cbs[i](ctx)
    87  		}
    88  	}
    89  }
    90  
    91  type filteredDS struct {
    92  	ds.RawInterface
    93  }
    94  
    95  func (fds filteredDS) RunInTransaction(f func(ctx context.Context) error, opts *ds.TransactionOptions) error {
    96  	var state *txnState
    97  	err := fds.RawInterface.RunInTransaction(func(ctx context.Context) error {
    98  		state = &txnState{ctx: ctx}
    99  		return f(context.WithValue(ctx, &ctxKey, state))
   100  	}, opts)
   101  	if err == nil {
   102  		state.execCBs()
   103  	}
   104  	return err
   105  }