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 }