lab.nexedi.com/kirr/go123@v0.0.0-20240207185015-8299741fa871/xcontext/xcontext.go (about) 1 // Copyright (C) 2017-2020 Nexedi SA and Contributors. 2 // Kirill Smelkov <kirr@nexedi.com> 3 // 4 // This program is free software: you can Use, Study, Modify and Redistribute 5 // it under the terms of the GNU General Public License version 3, or (at your 6 // option) any later version, as published by the Free Software Foundation. 7 // 8 // You can also Link and Combine this program with other software covered by 9 // the terms of any of the Free Software licenses or any of the Open Source 10 // Initiative approved licenses and Convey the resulting work. Corresponding 11 // source of such a combination shall include the source code for all other 12 // software used. 13 // 14 // This program is distributed WITHOUT ANY WARRANTY; without even the implied 15 // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 // 17 // See COPYING file for full licensing terms. 18 // See https://www.nexedi.com/licensing for rationale and options. 19 20 // Package xcontext provides addons to std package context. 21 // 22 // # Merging contexts 23 // 24 // Merge could be handy in situations where spawned job needs to be canceled 25 // whenever any of 2 contexts becomes done. This frequently arises with service 26 // methods that accept context as argument, and the service itself, on another 27 // control line, could be instructed to become non-operational. For example: 28 // 29 // func (srv *Service) DoSomething(ctx context.Context) (err error) { 30 // defer xerr.Contextf(&err, "%s: do something", srv) 31 // 32 // // srv.serveCtx is context that becomes canceled when srv is 33 // // instructed to stop providing service. 34 // origCtx := ctx 35 // ctx, cancel := xcontext.Merge(ctx, srv.serveCtx) 36 // defer cancel() 37 // 38 // err = srv.doJob(ctx) 39 // if err != nil { 40 // if ctx.Err() != nil && origCtx.Err() == nil { 41 // // error due to service shutdown 42 // err = ErrServiceDown 43 // } 44 // return err 45 // } 46 // 47 // ... 48 // } 49 package xcontext 50 51 import ( 52 "context" 53 "sync" 54 "sync/atomic" 55 "time" 56 ) 57 58 // XXX if we could change std context, then Merge could work by simply creating 59 // cancelCtx and registering it to parent1 and parent2. 60 // 61 // https://github.com/golang/go/issues/36503 62 // https://github.com/golang/go/issues/36448 63 // 64 // For the reference: here is how it is done in pygolang: 65 // 66 // https://lab.nexedi.com/kirr/pygolang/blob/d3bfb1bf/golang/context.py#L115-130 67 // https://lab.nexedi.com/kirr/pygolang/blob/d3bfb1bf/golang/context.py#L228-264 68 69 70 // mergeCtx represents 2 context merged into 1. 71 type mergeCtx struct { 72 parent1, parent2 context.Context 73 74 done chan struct{} 75 doneMark uint32 76 doneOnce sync.Once 77 doneErr error 78 79 cancelCh chan struct{} 80 cancelOnce sync.Once 81 } 82 83 // Merge merges 2 contexts into 1. 84 // 85 // The result context: 86 // 87 // - is done when parent1 or parent2 is done, or cancel called, whichever happens first, 88 // - has deadline = min(parent1.Deadline, parent2.Deadline), 89 // - has associated values merged from parent1 and parent2, with parent1 taking precedence. 90 // 91 // Canceling this context releases resources associated with it, so code should 92 // call cancel as soon as the operations running in this Context complete. 93 func Merge(parent1, parent2 context.Context) (context.Context, context.CancelFunc) { 94 mc := &mergeCtx{ 95 parent1: parent1, 96 parent2: parent2, 97 done: make(chan struct{}), 98 cancelCh: make(chan struct{}), 99 } 100 101 // if src ctx is already cancelled - make mc cancelled right after creation 102 // 103 // this saves goroutine spawn and makes 104 // 105 // ctx = Merge(ctx1, ctx2); ctx.Err != nil 106 // 107 // check possible. 108 select { 109 case <-parent1.Done(): 110 mc.finish(parent1.Err()) 111 112 case <-parent2.Done(): 113 mc.finish(parent2.Err()) 114 115 default: 116 // src ctx not canceled - spawn parent{1,2}.done merger. 117 go mc.wait() 118 } 119 120 return mc, mc.cancel 121 } 122 123 // finish marks merge ctx as done with specified error. 124 // 125 // it is safe to call finish multiple times and from multiple goroutines 126 // simultaneously - only the first call has the effect. 127 // 128 // finish returns the first error - with which ctx was actually marked as done. 129 func (mc *mergeCtx) finish(err error) error { 130 mc.doneOnce.Do(func() { 131 mc.doneErr = err 132 atomic.StoreUint32(&mc.doneMark, 1) 133 close(mc.done) 134 }) 135 return mc.doneErr 136 } 137 138 // wait waits for (.parent1 | .parent2 | .cancelCh) and then marks mergeCtx as done. 139 func (mc *mergeCtx) wait() { 140 var err error 141 select { 142 case <-mc.parent1.Done(): 143 err = mc.parent1.Err() 144 145 case <-mc.parent2.Done(): 146 err = mc.parent2.Err() 147 148 case <-mc.cancelCh: 149 err = context.Canceled 150 } 151 152 mc.finish(err) 153 } 154 155 // cancel sends signal to wait to shutdown. 156 // 157 // cancel is the context.CancelFunc returned for mergeCtx by Merge. 158 func (mc *mergeCtx) cancel() { 159 mc.cancelOnce.Do(func() { 160 close(mc.cancelCh) 161 }) 162 } 163 164 // Done implements context.Context . 165 func (mc *mergeCtx) Done() <-chan struct{} { 166 return mc.done 167 } 168 169 // Err implements context.Context . 170 func (mc *mergeCtx) Err() error { 171 // fast path: if already done 172 if atomic.LoadUint32(&mc.doneMark) != 0 { 173 return mc.doneErr 174 } 175 176 // slow path: poll all sources so that there is no delay for e.g. 177 // parent1.Err -> mergeCtx.Err, if user checks mergeCtx.Err directly. 178 var err error 179 select { 180 case <-mc.parent1.Done(): 181 err = mc.parent1.Err() 182 183 case <-mc.parent2.Done(): 184 err = mc.parent2.Err() 185 186 case <-mc.cancelCh: 187 err = context.Canceled 188 189 default: 190 return nil 191 } 192 193 return mc.finish(err) 194 } 195 196 // Deadline implements context.Context . 197 func (mc *mergeCtx) Deadline() (time.Time, bool) { 198 d1, ok1 := mc.parent1.Deadline() 199 d2, ok2 := mc.parent2.Deadline() 200 switch { 201 case !ok1: 202 return d2, ok2 203 case !ok2: 204 return d1, ok1 205 case d1.Before(d2): 206 return d1, true 207 default: 208 return d2, true 209 } 210 } 211 212 // Value implements context.Context . 213 func (mc *mergeCtx) Value(key interface{}) interface{} { 214 v := mc.parent1.Value(key) 215 if v != nil { 216 return v 217 } 218 return mc.parent2.Value(key) 219 } 220 221 // ---------------------------------------- 222 223 // chanCtx wraps channel into context.Context interface. 224 type chanCtx struct { 225 done <-chan struct{} 226 } 227 228 // MergeChan merges context and channel into 1 context. 229 // 230 // MergeChan, similarly to Merge, provides resulting context which: 231 // 232 // - is done when parent1 is done or done2 is closed, or cancel called, whichever happens first, 233 // - has the same deadline as parent1, 234 // - has the same associated values as parent1. 235 // 236 // Canceling this context releases resources associated with it, so code should 237 // call cancel as soon as the operations running in this Context complete. 238 func MergeChan(parent1 context.Context, done2 <-chan struct{}) (context.Context, context.CancelFunc) { 239 return Merge(parent1, chanCtx{done2}) 240 } 241 242 // Done implements context.Context . 243 func (c chanCtx) Done() <-chan struct{} { 244 return c.done 245 } 246 247 // Err implements context.Context . 248 func (c chanCtx) Err() error { 249 select { 250 case <-c.done: 251 return context.Canceled 252 default: 253 return nil 254 } 255 } 256 257 // Deadline implements context.Context . 258 func (c chanCtx) Deadline() (time.Time, bool) { 259 return time.Time{}, false 260 } 261 262 // Value implements context.Context . 263 func (c chanCtx) Value(key interface{}) interface{} { 264 return nil 265 }