github.com/git-amp/amp-sdk-go@v0.7.5/stdlib/task/task.go (about) 1 package task 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "sync" 9 "sync/atomic" 10 "time" 11 12 "github.com/git-amp/amp-sdk-go/stdlib/log" 13 ) 14 15 // ctx implements Context 16 type ctx struct { 17 log.Logger 18 19 task Task 20 21 id int64 22 state int32 23 idle bool 24 idleCloseRetry atomic.Int64 // time.Duration 25 idleCloseMin time.Time 26 27 chClosing chan struct{} // signals Close() has been called and close execution has begun. 28 chClosed chan struct{} // signals Close() has been called and all close execution is done. 29 err error // See context.Err() for spec 30 busy sync.WaitGroup // blocks until all execution is complete 31 subsMu sync.Mutex // Locked when .subs is being accessed 32 subs []Context 33 } 34 35 // Errors 36 var ( 37 ErrAlreadyStarted = errors.New("already started") 38 ErrUnstarted = errors.New("unstarted") 39 ErrClosed = errors.New("closed") 40 ) 41 42 var gSpawnCounter = int64(0) 43 44 func (p *ctx) Close() error { 45 first := atomic.CompareAndSwapInt32(&p.state, Running, Closing) 46 if first { 47 close(p.chClosing) 48 } 49 return nil 50 } 51 52 func (p *ctx) PreventIdleClose(delay time.Duration) bool { 53 p.subsMu.Lock() 54 p.idleCloseMin = time.Now().Add(delay) 55 p.idle = false 56 p.subsMu.Unlock() 57 58 select { 59 case <-p.Closing(): 60 return false 61 default: 62 return true 63 } 64 } 65 66 func (p *ctx) CloseWhenIdle(delay time.Duration) { 67 if delay <= 0 { 68 delay = 0 69 } 70 71 // Can this be folded into the main go routine in StartChild() to save a goroutine? 72 prevDelay := p.idleCloseRetry.Swap(int64(delay)) 73 74 // Only spawn a new timer when the delay is changed from 0 75 if prevDelay > 0 { 76 return 77 } 78 79 go func() { 80 var timer *time.Timer 81 82 for idleClose := true; idleClose; { 83 p.idle = true 84 p.busy.Wait() // wait until there is a chance of catching ctx idle 85 86 retry := false 87 88 p.subsMu.Lock() 89 delay := time.Duration(p.idleCloseRetry.Load()) 90 if !p.idle { 91 retry = true 92 } else if delay <= 0 { 93 idleClose = false 94 } else { 95 if !p.idleCloseMin.IsZero() { 96 minDelay := time.Until(p.idleCloseMin) 97 if minDelay <= 0 { 98 p.idleCloseMin = time.Time{} 99 } 100 // Wait for the more restrictive time constraint 101 if delay < minDelay { 102 delay = minDelay 103 } 104 } 105 } 106 p.subsMu.Unlock() 107 108 if retry || !idleClose { 109 continue 110 } 111 112 if delay > 0 { 113 if timer == nil { 114 timer = time.NewTimer(delay) 115 } else { 116 timer.Reset(delay) 117 } 118 select { 119 case <-timer.C: 120 case <-p.Closing(): 121 idleClose = false 122 } 123 } 124 125 // If no new children were added while we were waiting, then we have been idle and can close. 126 // Note in the case that we're closing, the below has no effect 127 if idleClose { 128 p.subsMu.Lock() 129 if p.idle { 130 p.Close() 131 idleClose = false 132 } 133 p.subsMu.Unlock() 134 } 135 } 136 }() 137 } 138 139 func (p *ctx) Deadline() (deadline time.Time, ok bool) { 140 return time.Time{}, false 141 } 142 143 func (p *ctx) Err() error { 144 select { 145 case <-p.Done(): 146 if p.err == nil { 147 return context.Canceled 148 } 149 return p.err 150 default: 151 return nil 152 } 153 } 154 155 func (p *ctx) Value(key interface{}) interface{} { 156 return nil 157 } 158 159 func (p *ctx) TaskRef() interface{} { 160 return p.task.TaskRef 161 } 162 163 func (p *ctx) ContextID() int64 { 164 return p.id 165 } 166 167 func (p *ctx) Label() string { 168 return p.task.Label 169 } 170 171 func printContextTree(ctx Context, out *strings.Builder, depth int, prefix []rune, lastChild bool) { 172 icon := ' ' 173 if depth > 0 { 174 icon = '┣' 175 if lastChild { 176 icon = '┗' 177 } 178 } 179 prefix = append(prefix, icon, ' ') 180 181 out.WriteString(fmt.Sprintf("%04d%s%s\n", ctx.ContextID(), string(prefix), ctx.Label())) 182 183 icon = '┃' 184 if lastChild { 185 icon = ' ' 186 } 187 prefix = append(prefix[:len(prefix)-2], icon, ' ', ' ', ' ', ' ') 188 189 var subBuf [20]Context 190 children := ctx.GetChildren(subBuf[:0]) 191 for i, ci := range children { 192 printContextTree(ci, out, depth+1, prefix, i == len(children)-1) 193 } 194 } 195 196 func (p *ctx) GetChildren(in []Context) []Context { 197 p.subsMu.Lock() 198 defer p.subsMu.Unlock() 199 return append(in, p.subs...) 200 } 201 202 // StartChild starts the given child Context as a "sub" task. 203 func (p *ctx) StartChild(task *Task) (Context, error) { 204 child := &ctx{ 205 state: Running, 206 id: atomic.AddInt64(&gSpawnCounter, 1), 207 chClosing: make(chan struct{}), 208 chClosed: make(chan struct{}), 209 } 210 if task != nil { 211 child.task = *task 212 } 213 if child.task.Label == "" { 214 child.task.Label = fmt.Sprintf("ctx_%d", child.id) 215 } 216 child.Logger = log.NewLogger(child.task.Label) 217 218 // If a parent is given, add the child to the parent's list of children. 219 if p != nil { 220 221 var err error 222 p.subsMu.Lock() 223 if p.state == Running { 224 p.busy.Add(1) 225 p.idle = false 226 p.subs = append(p.subs, child) 227 } else { 228 err = ErrUnstarted 229 } 230 p.subsMu.Unlock() 231 232 if err != nil { 233 return nil, err 234 } 235 } 236 237 go func() { 238 239 // If there is a parent, wait until child.Close() *or* p.Close() 240 // TODO: merge CloseWhenIdle() into this block? 241 if p != nil { 242 select { 243 case <-p.Closing(): 244 child.Close() 245 case <-child.Closing(): 246 } 247 } 248 249 // Wait for child to begin closing phase 250 <-child.Closing() 251 252 // Fire callback if given 253 if child.task.OnClosing != nil { 254 child.task.OnClosing() 255 } 256 257 if p != nil && p.task.OnChildClosing != nil { 258 p.task.OnChildClosing(child) 259 } 260 261 // Once all child's children are closed, proceed with completion. 262 child.busy.Wait() 263 264 var idleClose time.Duration 265 266 if p != nil { 267 268 p.subsMu.Lock() 269 { 270 // remove the child from its parent 271 N := len(p.subs) 272 for i := 0; i < N; i++ { 273 if p.subs[i] == child { 274 copy(p.subs[i:], p.subs[i+1:N]) 275 N-- 276 p.subs[N] = nil // show GC some love 277 p.subs = p.subs[:N] 278 break 279 } 280 } 281 282 // If removing the last child and in IdleClose mode, queue the parent to be closed 283 if N == 0 { 284 idleClose = p.task.IdleClose 285 } 286 } 287 p.subsMu.Unlock() 288 } 289 290 // Move to Closed state now that all all that remains is the OnClosed callback and release of the chClosed chan. 291 child.state = Closed 292 if child.task.OnClosed != nil { 293 child.task.OnClosed() 294 } 295 close(child.chClosed) 296 297 // With the child now fully closed, the parent is no longer waiting on this child 298 if p != nil { 299 p.busy.Done() 300 } 301 302 if idleClose > 0 { 303 p.CloseWhenIdle(idleClose) 304 } 305 }() 306 307 if child.task.OnStart != nil { 308 err := child.task.OnStart(child) 309 child.task.OnStart = nil 310 if err != nil { 311 child.Close() 312 return nil, err 313 } 314 } 315 316 if child.task.OnRun != nil { 317 child.busy.Add(1) 318 go func() { 319 child.task.OnRun(child) 320 child.task.OnRun = nil 321 child.busy.Done() 322 323 // If idleclose is set, try to do so 324 if child.task.IdleClose > 0 { 325 child.CloseWhenIdle(child.task.IdleClose) 326 } 327 }() 328 } 329 330 return child, nil 331 } 332 333 func (p *ctx) Go(label string, fn func(ctx Context)) (Context, error) { 334 return p.StartChild(&Task{ 335 Label: label, 336 IdleClose: time.Nanosecond, 337 OnRun: fn, 338 }) 339 } 340 341 func (p *ctx) Closing() <-chan struct{} { 342 return p.chClosing 343 } 344 345 func (p *ctx) Done() <-chan struct{} { 346 return p.chClosed 347 } 348 349 const ( 350 Unstarted int32 = iota 351 Running 352 Closing 353 Closed 354 )