github.com/mailru/activerecord@v1.12.2/pkg/iproto/util/pool/pool.go (about) 1 package pool 2 3 import ( 4 "container/list" 5 "errors" 6 "runtime" 7 "sync" 8 "time" 9 10 ptime "github.com/mailru/activerecord/pkg/iproto/util/time" 11 "golang.org/x/net/context" 12 ) 13 14 const ( 15 DefaultExtraWorkerTTL = time.Second 16 ) 17 18 var ( 19 ErrConfigMalformed = errors.New("malformed pool config: max number of workers is lower than number of unstoppable workers") 20 ErrConfigQueueUnstoppable = errors.New("malformed pool config: queue is buffered, but unstoppable workers set to 0") 21 ErrUnavailable = errors.New("pool: temporarily unavailable") 22 ErrPoolClosed = errors.New("pool: closed") 23 ) 24 25 // Task represents task that need to be executed by some worker. 26 type Task interface { 27 Run() 28 } 29 30 type TaskFunc func() 31 32 func (fn TaskFunc) Run() { fn() } 33 34 type Config struct { 35 // Number of workers that are always running. 36 UnstoppableWorkers int 37 38 // Maximum number of workers that could be spawned. 39 // It includes UnstoppableWorkers count. 40 // If MaxWorkers is <= 0 then it interpreted as extra workers are unlimited. 41 // If MaxWorkers is in range [1, UnstoppableWorkers) then config is malformed. 42 MaxWorkers int 43 44 // When work flow becomes too much for unstoppable workers, 45 // Pool could spawn extra worker if it fits max workers limit. 46 // Spawned worker will be alive for this amount of time. 47 ExtraWorkerTTL time.Duration 48 49 // This parameter manages the size of work queue. 50 // The smaller this value, the greater the probability of 51 // spawning the extra worker. And vice versa. 52 WorkQueueSize int 53 54 // QueueTiming is used to calculate duration from task receiving 55 // to begining of task execution. 56 // QueueTiming stat.SimpleTiming 57 58 // ExecTiming is used to calculate duration of task execution. 59 // ExecTiming stat.SimpleTiming 60 61 // IdleTiming is used to calculate idle time of each worker. 62 // IdleTiming stat.SimpleTiming 63 64 // OnTaskIn, OnTaskOut are used to signal whan task is scheduled/pulled from queue. 65 OnTaskIn, OnTaskOut func() 66 67 // OnWorkerStart, OnWorkerStop are called on extra worker spawned/stopped. 68 OnWorkerStart, OnWorkerStop func() 69 } 70 71 func dummyStatPush() {} 72 73 func (c *Config) withDefaults() (config Config) { 74 if c != nil { 75 config = *c 76 } 77 78 if config.ExtraWorkerTTL == 0 { 79 config.ExtraWorkerTTL = DefaultExtraWorkerTTL 80 } 81 82 // if config.QueueTiming == nil { 83 // config.QueueTiming = stat.DummyTiming 84 // } 85 // if config.ExecTiming == nil { 86 // config.ExecTiming = stat.DummyTiming 87 // } 88 // if config.IdleTiming == nil { 89 // config.IdleTiming = stat.DummyTiming 90 // } 91 92 if config.OnTaskIn == nil { 93 config.OnTaskIn = dummyStatPush 94 } 95 96 if config.OnTaskOut == nil { 97 config.OnTaskOut = dummyStatPush 98 } 99 100 if config.OnWorkerStart == nil { 101 config.OnWorkerStart = dummyStatPush 102 } 103 104 if config.OnWorkerStop == nil { 105 config.OnWorkerStop = dummyStatPush 106 } 107 108 return 109 } 110 111 func (c Config) check() error { 112 if c.MaxWorkers > 0 && c.MaxWorkers < c.UnstoppableWorkers { 113 return ErrConfigMalformed 114 } 115 116 // If work queue set to be buffered, but unstoppable workers are set to 0, then 117 // pool will not process tasks until work queue will be full. 118 if c.UnstoppableWorkers == 0 && c.WorkQueueSize > 0 { 119 return ErrConfigQueueUnstoppable 120 } 121 122 return nil 123 } 124 125 type Pool struct { 126 config Config 127 128 work chan Task 129 sem chan struct{} 130 kill chan struct{} 131 done chan struct{} 132 wg sync.WaitGroup 133 noCork bool 134 135 mu sync.RWMutex 136 list *list.List 137 } 138 139 func New(c *Config) (*Pool, error) { 140 config := c.withDefaults() 141 142 if err := config.check(); err != nil { 143 return nil, err 144 } 145 146 p := &Pool{ 147 config: config, 148 work: make(chan Task, config.WorkQueueSize), 149 kill: make(chan struct{}), 150 done: make(chan struct{}), 151 list: list.New(), 152 } 153 154 if config.MaxWorkers > 0 { 155 p.sem = make(chan struct{}, config.MaxWorkers) 156 } 157 158 for i := 0; i < config.UnstoppableWorkers; i++ { 159 if p.sem != nil { 160 p.sem <- struct{}{} 161 } 162 163 _ = p.spawn(nil, false) 164 } 165 166 return p, nil 167 } 168 169 // Must is a helper that wraps a call to a function returning (*Pool, error) 170 // and panics if the error is non-nil. 171 // It is intended for use in variable initializations with New() function. 172 func Must(p *Pool, err error) *Pool { 173 if err != nil { 174 panic(err) 175 } 176 177 return p 178 } 179 180 func (p *Pool) SetNoCork(v bool) { 181 p.noCork = v 182 } 183 184 // Close terminates all spawned goroutines. 185 func (p *Pool) Close() error { 186 p.mu.Lock() 187 select { 188 case <-p.kill: 189 // Close() was already called. 190 // Wait for done to be closed. 191 p.mu.Unlock() 192 <-p.done 193 194 default: 195 // Close the kill channel under the mutex to accomplish two things. 196 // First is to get rid from concurrent wg.Add() calls while waiting for 197 // wg.Done() below. 198 // Second is to avoid panics on concurrent Close() calls. 199 close(p.kill) 200 p.mu.Unlock() 201 202 // Wait for all workers exit. 203 p.wg.Wait() 204 205 // No more alive workers at this point. So we can safely fulfill work 206 // queue to avoid concurrent Schedule*() tasks get stucked in the work 207 // queue without progress. 208 if !p.noCork { 209 cork(p.work) 210 } 211 212 // Signal that pool is completely closed. 213 close(p.done) 214 } 215 216 return nil 217 } 218 219 // Done returns channel which closure means that pool is done with all its work. 220 func (p *Pool) Done() <-chan struct{} { 221 return p.done 222 } 223 224 // Schedule makes task to be scheduled over pool's workers. 225 // It returns non-nil only if pool become closed and task can not be executed 226 // over its workers. 227 func (p *Pool) Schedule(t Task) error { 228 return p.schedule(t, nil, nil) 229 } 230 231 // immediate indicates that task scheduling must complete or fail right now. 232 var immediate = make(chan time.Time) 233 234 // ScheduleImmediate makes task to be scheduled without waiting for free 235 // workers. That is, if all workers are busy and pool is not rubber, then 236 // ErrUnavailable is returned immediately. 237 func (p *Pool) ScheduleImmediate(t Task) error { 238 return p.schedule(t, immediate, nil) 239 } 240 241 // ScheduleTimeout makes task to be scheduled over pool's workers. 242 // 243 // Non-nil error only available when tm is not 0. That is, if no workers are 244 // available during timeout, it returns error. 245 // 246 // Zero timeout means that there are no timeout for awaiting for available 247 // worker. 248 func (p *Pool) ScheduleTimeout(tm time.Duration, t Task) error { 249 var timeout <-chan time.Time 250 251 if tm != 0 { 252 timer := ptime.AcquireTimer(tm) 253 defer ptime.ReleaseTimer(timer) 254 timeout = timer.C 255 } 256 257 return p.schedule(t, timeout, nil) 258 } 259 260 // ScheduleContext makes task to be scheduled over pool's workers. 261 // 262 // Non-nil error only available when ctx is done. That is, if no workers are 263 // available during lifetime of context, it returns ctx.Err() call result. 264 func (p *Pool) ScheduleContext(ctx context.Context, t Task) error { 265 if err := p.schedule(t, nil, ctx.Done()); err != nil { 266 return ctx.Err() 267 } 268 269 return nil 270 } 271 272 // ScheduleCustom makes task to be scheduled over pool's workers. 273 // 274 // Non-nil error only available when cancel closed. That is, if no workers are 275 // available until closure of cancel chan, it returns error. 276 // 277 // This method useful for implementing custom cancellation logic by a caller. 278 func (p *Pool) ScheduleCustom(cancel <-chan struct{}, t Task) error { 279 return p.schedule(t, nil, cancel) 280 } 281 282 // Barrier blocks until workers complete their current task. 283 func (p *Pool) Barrier() { 284 _ = p.barrier(nil, nil) 285 } 286 287 // BarrierTimeout blocks until workers complete their current task or until 288 // timeout is expired. 289 func (p *Pool) BarrierTimeout(tm time.Duration) error { 290 var timeout <-chan time.Time 291 292 if tm != 0 { 293 timer := ptime.AcquireTimer(tm) 294 defer ptime.ReleaseTimer(timer) 295 timeout = timer.C 296 } 297 298 return p.barrier(timeout, nil) 299 } 300 301 // BarrierContext blocks until workers complete their current task or until 302 // context is done. In case when err is non-nil err is always a result of 303 // ctx.Err() call. 304 func (p *Pool) BarrierContext(ctx context.Context) error { 305 if err := p.barrier(nil, ctx.Done()); err != nil { 306 return ctx.Err() 307 } 308 309 return nil 310 } 311 312 // BarrierCustom blocks until workers complete their current task or until 313 // given cancelation channel is non-empty. 314 func (p *Pool) BarrierCustom(cancel <-chan struct{}) error { 315 return p.barrier(nil, cancel) 316 } 317 318 // Wait blocks until all previously scheduled tasks are executed. 319 func (p *Pool) Wait() { 320 _ = p.wait(nil, nil) 321 } 322 323 // WaitTimeout blocks until all previously scheduled tasks are executed or until 324 // timeout is expired. 325 func (p *Pool) WaitTimeout(tm time.Duration) error { 326 var timeout <-chan time.Time 327 328 if tm != 0 { 329 timer := ptime.AcquireTimer(tm) 330 defer ptime.ReleaseTimer(timer) 331 timeout = timer.C 332 } 333 334 return p.wait(timeout, nil) 335 } 336 337 // WaitContext blocks until all previously scheduled tasks are executed or 338 // until context is done. In case when err is non-nil err is always a result of 339 // ctx.Err() call. 340 func (p *Pool) WaitContext(ctx context.Context) error { 341 if err := p.wait(nil, ctx.Done()); err != nil { 342 return ctx.Err() 343 } 344 345 return nil 346 } 347 348 // WaitCustom blocks until all previously scheduled tasks are executed or until 349 // given cancelation channel is non-empty. 350 func (p *Pool) WaitCustom(cancel <-chan struct{}) error { 351 return p.wait(nil, cancel) 352 } 353 354 // workers returns current alive workers list. 355 func (p *Pool) workers() []*worker { 356 p.mu.RLock() 357 ws := make([]*worker, 0, p.list.Len()) 358 359 for el := p.list.Front(); el != nil; el = el.Next() { 360 ws = append(ws, el.Value.(*worker)) 361 } 362 363 p.mu.RUnlock() 364 365 return ws 366 } 367 368 // barrier spreads a technical task over all running workers and blocks until 369 // every worker execute it. It returns earlier with non-nil error if timeout or 370 // done channels are non-empty. 371 func (p *Pool) barrier(timeout <-chan time.Time, done <-chan struct{}) error { 372 snap := make(chan struct{}, 1) 373 n, err := p.multicast(TaskFunc(func() { 374 select { 375 case snap <- struct{}{}: 376 case <-timeout: 377 case <-done: 378 } 379 }), timeout, done) 380 381 if err != nil { 382 return err 383 } 384 385 for i := 0; i < n; i++ { 386 select { 387 case <-snap: 388 case <-timeout: 389 return ErrUnavailable 390 case <-done: 391 return ErrUnavailable 392 } 393 } 394 395 return nil 396 } 397 398 // multicast makes task to be executed on every running worker. 399 // It returns number of workers received the task and an error that could arise 400 // only when timeout or done channels are non-nil and non-empty. 401 // 402 // Note that if pool is closed it does not returns ErrPoolClosed error. 403 // 404 // If some worker can not receive incoming task until timeout or done chanel it 405 // stops trying to send the task to remaining workers. 406 func (p *Pool) multicast(task Task, timeout <-chan time.Time, done <-chan struct{}) (n int, err error) { 407 // NOTE: if for some reasons it is neccessary to check that pool is closed 408 // and return ErrPoolClosed, then you MUST edit the barrier() method such 409 // that it will check the ErrPoolClosed case and make p.Done() waiting or 410 // its timeout or done channels closure. 411 // You will also need to edit the worker run() method, which is also rely 412 // on this behaviour iniside kill case. 413 for _, w := range p.workers() { 414 callMaybe(p.config.OnTaskIn) 415 t := p.wrapTask(task) 416 417 switch { 418 case timeout == immediate: 419 select { 420 case w.direct <- t: 421 n++ 422 case <-w.done: 423 // Worker killed. 424 default: 425 return n, ErrUnavailable 426 } 427 default: 428 select { 429 case w.direct <- t: 430 n++ 431 case <-w.done: 432 // Worker killed. 433 case <-timeout: 434 return n, ErrUnavailable 435 case <-done: 436 return n, ErrUnavailable 437 } 438 } 439 } 440 441 return n, nil 442 } 443 444 func (p *Pool) schedule(task Task, timeout <-chan time.Time, done <-chan struct{}) (err error) { 445 callMaybe(p.config.OnTaskIn) 446 w := p.wrapTask(task) 447 448 // First, try to put task into the work queue in optimistic manner. 449 select { 450 case <-p.kill: 451 // Drop it after Close() call 452 callMaybe(p.config.OnTaskOut) 453 return ErrPoolClosed 454 case p.work <- w: 455 return nil 456 default: 457 } 458 459 switch { 460 case p.sem == nil: 461 // If pool is configured to be "rubber" we just spawn worker 462 // without limits. 463 case timeout == immediate: 464 // Immediate timeout means that caller dont want to wait for 465 // scheduling. In that case our only option here is to try to spawn 466 // goroutine in non-blocking mode. 467 select { 468 case p.sem <- struct{}{}: 469 default: 470 err = ErrUnavailable 471 } 472 default: 473 // If pool is not "rubber", try to enqueue task into the work queue or 474 // spawn new worker until the timeout or done channel are filled. 475 select { 476 case p.work <- w: 477 return nil 478 case p.sem <- struct{}{}: 479 case <-timeout: 480 err = ErrUnavailable 481 case <-done: 482 err = ErrUnavailable 483 case <-p.kill: 484 err = ErrPoolClosed 485 } 486 } 487 488 if err == nil { 489 // We get here only if p.sem is nil or write to p.sem succeed. 490 err = p.spawn(w, true) 491 } 492 493 if err != nil { 494 callMaybe(p.config.OnTaskOut) 495 } 496 497 return err 498 } 499 500 func (p *Pool) wait(timeout <-chan time.Time, done <-chan struct{}) error { 501 crossed := make(chan struct{}) 502 // Execution of this task means that all previous tasks from the work queue 503 // were received by worker(s). After that, we only need to wait them to 504 // finish their current labor. 505 bound := TaskFunc(func() { 506 close(crossed) 507 }) 508 // Put the task manually to the queue without using schedule() method. This 509 // is done to avoid side-effects of schedule() like spawning extra worker 510 // which will execute this task out of queue order. 511 select { 512 case p.work <- bound: 513 // OK. 514 case <-p.done: 515 // No need to do additional work – pool closed and workers are done. 516 return nil 517 518 case <-timeout: 519 return ErrUnavailable 520 case <-done: 521 return ErrUnavailable 522 } 523 select { 524 case <-crossed: 525 return p.barrier(timeout, done) 526 527 case <-timeout: 528 return ErrUnavailable 529 case <-done: 530 return ErrUnavailable 531 } 532 } 533 534 func (p *Pool) spawn(t Task, extra bool) (closed error) { 535 // Prepare worker before taking the mutex. 536 // That is we act here in optimistic manner that pool is not closed. 537 w := &worker{ 538 direct: make(chan Task, 1), 539 done: make(chan struct{}), 540 work: p.work, 541 kill: p.kill, 542 noCork: p.noCork, 543 // idleTiming: p.config.IdleTiming, 544 } 545 if extra { 546 w.ttl = p.config.ExtraWorkerTTL 547 } 548 549 // We must synchronize workers wait group to avoid panic "WaitGroup is 550 // reused before previous Wait has returned". That is, there could be race 551 // when we spawning worker while pool is closing; in that case inside 552 // Close(): wg.Done() is not yet returned but its counter could reach the 553 // zero due to all previous workers are done; here inside spawn(): we can 554 // call wg.Add(1), which will produce the panic inside wg.Wait(). 555 p.mu.Lock() 556 { 557 select { 558 case <-p.kill: 559 closed = ErrPoolClosed 560 default: 561 p.wg.Add(1) 562 w.elem = p.list.PushBack(w) 563 } 564 } 565 p.mu.Unlock() 566 567 if closed != nil { 568 if p.sem != nil { 569 // We can release token from the workers semaphore not under the 570 // mutex. 571 <-p.sem 572 } 573 574 return closed 575 } 576 577 w.onStop = func() { 578 if p.sem != nil { 579 <-p.sem 580 } 581 582 p.wg.Done() 583 callMaybe(p.config.OnWorkerStop) 584 585 p.mu.Lock() 586 p.list.Remove(w.elem) 587 p.mu.Unlock() 588 } 589 w.onStart = func() { 590 callMaybe(p.config.OnWorkerStart) 591 } 592 593 go w.run(t) 594 595 return nil 596 } 597 598 func (p *Pool) wrapTask(task Task) *taskWrapper { 599 w := getTaskWrapper() 600 601 w.task = task 602 603 // w.queueTimer = p.config.QueueTiming.Timer() 604 // w.execTimer = p.config.ExecTiming.Timer() 605 606 // w.queueTimer.Start() 607 w.onDequeue = func() { 608 // w.queueTimer.Stop() 609 p.config.OnTaskOut() 610 } 611 612 return w 613 } 614 615 // Schedule is a helper that returns function which purpose is to schedule 616 // execution of next given function over p. 617 func Schedule(p *Pool) func(func()) { 618 return func(fn func()) { 619 _ = p.Schedule(TaskFunc(fn)) 620 } 621 } 622 623 // ScheduleTimeout is a helper that returns function which purpose is to 624 // schedule execution of next given function over p with timeout. 625 func ScheduleTimeout(p *Pool) func(time.Duration, func()) error { 626 return func(t time.Duration, fn func()) error { 627 return p.ScheduleTimeout(t, TaskFunc(fn)) 628 } 629 } 630 631 // ScheduleTimeout is a helper that returns function which purpose is to 632 // schedule execution of next given function over p with context. 633 func ScheduleContext(p *Pool) func(context.Context, func()) error { 634 return func(ctx context.Context, fn func()) error { 635 return p.ScheduleContext(ctx, TaskFunc(fn)) 636 } 637 } 638 639 // ScheduleTimeout is a helper that returns function which purpose is to 640 // schedule execution of next given function over p with cancellation chan. 641 func ScheduleCustom(p *Pool) func(chan struct{}, func()) error { 642 return func(ch chan struct{}, fn func()) error { 643 return p.ScheduleCustom(ch, TaskFunc(fn)) 644 } 645 } 646 647 type worker struct { 648 elem *list.Element 649 650 work <-chan Task 651 kill <-chan struct{} 652 653 direct chan Task 654 done chan struct{} 655 656 ttl time.Duration 657 // idleTiming stat.SimpleTiming 658 noCork bool 659 660 onStop func() 661 onStart func() 662 } 663 664 func (w *worker) run(t Task) { 665 defer func() { 666 close(w.done) 667 w.onStop() 668 }() 669 w.onStart() 670 671 if t != nil { 672 t.Run() 673 } 674 675 var timeout <-chan time.Time 676 677 if w.ttl != 0 { 678 tm := ptime.AcquireTimer(w.ttl) 679 defer ptime.ReleaseTimer(tm) 680 timeout = tm.C 681 } 682 683 // idle := w.idleTiming.Timer() 684 // idle.Start() 685 /// XXX: wrap Stop with func, because of receiver is defined on moment of evaluation 686 // defer func() { idle.Stop() }() 687 688 run := func(t Task) { 689 // idle.Stop() 690 // XXX: spawn new timer, because realtime timer should be used only once 691 // idle = w.idleTiming.Timer() 692 // idle.Start() 693 t.Run() 694 } 695 696 for { 697 select { 698 case t := <-w.direct: 699 run(t) 700 case t := <-w.work: 701 run(t) 702 case <-timeout: 703 // Cork direct work queue because it could contain buffered tasks, 704 // which execution can block the user. 705 cork(w.direct) 706 707 return 708 case <-w.kill: 709 // Receving from w.kill means that pool is closing and no more 710 // tasks will be queued soon. 711 if w.noCork { 712 // Drop everything 713 return 714 } 715 716 // We can give a last chance to read some tasks from work queue and 717 // reduce the pressure on Close() calling goroutine by reducing 718 // amount of possible tasks executed during cork(p.work) there. 719 runtime.Gosched() 720 721 for { 722 // We do not handle here w.direct due no pool closure checking 723 // iniside multicast(). That is, multicast() tries to send 724 // direct message for every worker listed in p.list. Reading 725 // messages from w.direct inside this loop could produce 726 // infinite living worker if client calls multicast() in a 727 // loop. Thats why we just cork(w.direct) below. 728 select { 729 case t := <-w.work: 730 // No idle timer usage here. 731 t.Run() 732 default: 733 cork(w.direct) 734 return 735 } 736 } 737 } 738 } 739 } 740 741 type taskWrapper struct { 742 task Task 743 // queueTimer stat.Timer 744 // execTimer stat.Timer 745 onDequeue func() 746 } 747 748 var taskWrapperPool sync.Pool 749 750 func getTaskWrapper() *taskWrapper { 751 if w, _ := taskWrapperPool.Get().(*taskWrapper); w != nil { 752 return w 753 } 754 755 return &taskWrapper{} 756 } 757 758 func putTaskWrapper(w *taskWrapper) { 759 *w = taskWrapper{} 760 taskWrapperPool.Put(w) 761 } 762 763 func (w *taskWrapper) Run() { 764 w.onDequeue() 765 766 // w.execTimer.Start() 767 w.task.Run() 768 // w.execTimer.Stop() 769 770 putTaskWrapper(w) 771 } 772 773 func callMaybe(fn func()) { 774 if fn != nil { 775 fn() 776 } 777 } 778 779 // cork fulfills given work channel with stub tasks. It executes every task 780 // found in queue during corking. It returns when all tasks in queue are stubs. 781 // 782 // Note that no one goroutine must own the work channel while calling cork. 783 // In other case it could lead to spinlock. 784 func cork(work chan Task) { 785 n := cap(work) 786 for i := 0; i != n; { 787 corking: 788 for i = 0; i < n; i++ { 789 select { 790 case work <- stubTask: 791 default: 792 // Blocked to write due to some lost task in the queue. 793 break corking 794 } 795 } 796 // Drain the work queue and execute non stub tasks that stucked in the 797 // queue after Close() call. 798 // 799 // NOTE: it runs only when we does not fulfilled work queue with stubs 800 // in loop from above (i != n). 801 for j := 0; i != n && j < n; j++ { 802 w := <-work 803 if w != stubTask { 804 w.Run() 805 } 806 } 807 } 808 } 809 810 type panicTask struct{} 811 812 func (s panicTask) Run() { 813 panic("pool: this task must never be executed") 814 } 815 816 var stubTask = panicTask{}