Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Job Queue in Golang

Bo-Yi Wu
October 19, 2019

Job Queue in Golang

1. what is the different unbuffered and buffered channel?
2. how to implement a job queue in golang?
3. how to stop the worker in a container?
4. Shutdown with Sigterm Handling
5. Canceling Workers without Context
6. Graceful shutdown with worker
7. How to auto-scaling build agent?
8. How to cancel the current Job?

Bo-Yi Wu

October 19, 2019
Tweet

More Decks by Bo-Yi Wu

Other Decks in Technology

Transcript

  1. Job Queue in Golang
    Bo-Yi Wu
    2019.10.19

    View full-size slide

  2. About me
    • Software Engineer in Mediatek
    • Member of Drone CI/CD Platform
    • Member of Gitea Platform
    • Member of Gin Golang Framework
    • Maintain Some GitHub Actions Plugins.
    • Teacher of Udemy Platform: Golang + Drone

    View full-size slide

  3. Queue (Open Source)
    3BCCJU.2
    /42

    View full-size slide

  4. Why not
    use Open Source?

    View full-size slide

  5. Single Service
    • Build in SQLite (LDAP, Job List)
    • Build in Cache (LRU)
    • Build for single binary
    • No third-party dependency

    View full-size slide

  6. Before talking about
    Job Queue

    View full-size slide

  7. Buffered
    vs
    Unbuffered
    http://bit.ly/understand-channel

    View full-size slide

  8. Goroutine
    func main() {
    go func() {
    fmt.Println("GO GO GO")
    }()
    time.Sleep(1 * time.Second)
    }

    View full-size slide

  9. Unbuffered
    make(chan bool)

    View full-size slide

  10. Unbuffered Channel
    func main() {
    c := make(chan bool)
    go func() {
    fmt.Println("GO GO GO")
    c <- true
    }()
    <-c
    }

    View full-size slide

  11. Unbuffered Channel
    func main() {
    c := make(chan bool)
    go func() {
    fmt.Println("GO GO GO")
    c <- true
    }()
    <-c
    }

    View full-size slide

  12. Unbuffered Channel
    func main() {
    c := make(chan bool)
    go func() {
    fmt.Println("GO GO GO")
    <-c
    }()
    c <- true
    }

    View full-size slide

  13. func main() {
    c := make(chan bool)
    go func() {
    fmt.Println("GO GO GO")
    c <- true
    c <- true
    }()
    <-c
    time.Sleep(1 * time.Second)
    }
    Unbuffered Channel

    View full-size slide

  14. buffered
    make(chan bool, 1)

    View full-size slide

  15. Buffered channel
    func main() {
    c := make(chan bool, 1)
    go func() {
    fmt.Println("GO GO GO")
    <-c
    }()
    c <- true
    }

    View full-size slide

  16. Buffered channel
    func main() {
    c := make(chan bool, 1)
    go func() {
    fmt.Println("GO GO GO")
    <-c
    }()
    c <- true
    }

    View full-size slide

  17. Buffered channel
    func main() {
    c := make(chan bool, 1)
    go func() {
    fmt.Println("GO GO GO")
    c <- true
    }()
    <-c
    }

    View full-size slide

  18. How to implement
    Job Queue in Go

    View full-size slide

  19. Sometimes you don’t need
    A job queue
    go process("job01")

    View full-size slide

  20. func worker(jobChan <-chan Job) {
    for job := range jobChan {
    process(job)
    }
    }
    // make a channel with a capacity of 1024.
    jobChan := make(chan Job, 1024)
    // start the worker
    go worker(jobChan)
    // enqueue a job
    jobChan <- job

    View full-size slide

  21. func worker(jobChan <-chan Job) {
    for job := range jobChan {
    process(job)
    }
    }
    // make a channel with a capacity of 1024.
    jobChan := make(chan Job, 1024)
    // start the worker
    go worker(jobChan)
    // enqueue a job
    jobChan <- job

    View full-size slide

  22. Block if there already are 1024 jobs
    jobChan := make(chan Job, 1024)

    View full-size slide

  23. Enqueue without blocking

    View full-size slide

  24. func Enqueue(job Job, jobChan chan<- Job) bool {
    select {
    case jobChan <- job:
    return true
    default:
    return false
    }
    }
    if !Enqueue(job, job100) {
    Error(
    http.StatusServiceUnavailable,
    "max capacity reached",
    )
    return
    }

    View full-size slide

  25. Stopping the worker?

    View full-size slide

  26. func main() {
    ch := make(chan int, 2)
    go func() {
    ch <- 1
    ch <- 2
    }()
    for n := range ch {
    fmt.Println(n)
    }
    }

    View full-size slide

  27. func main() {
    ch := make(chan int, 2)
    go func() {
    ch <- 1
    ch <- 2
    close(ch)
    }()
    for n := range ch {
    fmt.Println(n)
    }
    }

    View full-size slide

  28. func main() {
    ch := make(chan int, 2)
    go func() {
    ch <- 1
    ch <- 2
    }()
    go func() {
    for n := range ch {
    fmt.Println(n)
    }
    }()
    time.Sleep(1 * time.Second)
    }

    View full-size slide

  29. Setup Consumer

    View full-size slide

  30. type Consumer struct {
    inputChan chan int
    jobsChan chan int
    }
    const PoolSize = 2
    func main() {
    // create the consumer
    consumer := Consumer{
    inputChan: make(chan int, 1),
    jobsChan: make(chan int, PoolSize),
    }
    }

    View full-size slide

  31. func (c *Consumer) queue(input int) {
    fmt.Println("send input value:", input)
    c.jobsChan <- input
    }
    func (c *Consumer) worker(num int) {
    for job := range c.jobsChan {
    fmt.Println("worker:", num, " job value:", job)
    }
    }
    for i := 0; i < PoolSize; i++ {
    go consumer.worker(i)
    }

    View full-size slide

  32. consumer.queue(1)
    consumer.queue(2)
    consumer.queue(3)
    consumer.queue(4)
    blocking by poll size = 2

    View full-size slide

  33. Output (Poll Size = 2)
    send input value: 1
    send input value: 2
    send input value: 3
    worker: 0 job value: 1
    send input value: 4
    worker: 0 job value: 2
    worker: 0 job value: 3
    worker: 0 job value: 4

    View full-size slide

  34. rewrite queue func
    func (c *Consumer) queue(input int) bool {
    fmt.Println("send input value:", input)
    select {
    case c.jobsChan <- input:
    return true
    default:
    return false
    }
    }

    View full-size slide

  35. Output (Poll Size = 2)
    send input value: 1
    send input value: 2
    send input value: 3
    send input value: 4
    worker: 0 job value: 1
    worker: 0 job value: 2

    View full-size slide

  36. Shutdown with
    Sigterm Handling

    View full-size slide

  37. func WithContextFunc(ctx context.Context, f func()) context.Context {
    ctx, cancel := context.WithCancel(ctx)
    go func() {
    c := make(chan os.Signal)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    defer signal.Stop(c)
    select {
    case <-ctx.Done():
    case <-c:
    f()
    cancel()
    }
    }()
    return ctx
    }

    View full-size slide

  38. func WithContextFunc(ctx context.Context, f func()) context.Context {
    ctx, cancel := context.WithCancel(ctx)
    go func() {
    c := make(chan os.Signal)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    defer signal.Stop(c)
    select {
    case <-ctx.Done():
    case <-c:
    f()
    cancel()
    }
    }()
    return ctx
    }

    View full-size slide

  39. func (c Consumer) startConsumer(ctx context.Context) {
    for {
    select {
    case job := <-c.inputChan:
    if ctx.Err() != nil {
    close(c.jobsChan)
    return
    }
    c.jobsChan <- job
    case <-ctx.Done():
    close(c.jobsChan)
    return
    }
    }
    }

    View full-size slide

  40. Cancel by ctx.Done() event
    func (c *Consumer) worker(num int) {
    for job := range c.jobsChan {
    fmt.Println("worker:", num, " job value:", job)
    }
    }

    View full-size slide

  41. Canceling Workers
    without Context

    View full-size slide

  42. cancelChan := make(chan struct{})
    go worker(jobChan, cancelChan)
    func worker(jobChan <-chan Job, cancelChan <-chan struct{}) {
    for {
    select {
    case <-cancelChan:
    return
    case job := <-jobChan:
    process(job)
    }
    }
    }
    // to cancel the worker, close the cancel channel
    close(cancelChan)
    Create a cancel channel

    View full-size slide

  43. cancelChan := make(chan struct{})
    go worker(jobChan, cancelChan)
    func worker(jobChan <-chan Job, cancelChan <-chan struct{}) {
    for {
    select {
    case <-cancelChan:
    return
    case job := <-jobChan:
    process(job)
    }
    }
    }
    // to cancel the worker, close the cancel channel
    close(cancelChan)
    Create a cancel channel
    close(cancelChan)

    View full-size slide

  44. Graceful shutdown
    with worker
    sync.WaitGroup

    View full-size slide

  45. wg := &sync.WaitGroup{}
    wg.Add(numberOfWorkers)
    // Start [PoolSize] workers
    for i := 0; i < PoolSize; i++ {
    go consumer.worker(i)
    }

    View full-size slide

  46. WaitGroup
    WaitGroup
    WaitGroup
    WaitGroup

    View full-size slide

  47. func (c Consumer) worker(wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range c.jobsChan {
    // handle the job event
    }
    }

    View full-size slide

  48. Add WaitGroup
    after Cancel Function

    View full-size slide

  49. func WithContextFunc(ctx context.Context, f func()) context.Context {
    ctx, cancel := context.WithCancel(ctx)
    go func() {
    c := make(chan os.Signal)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    defer signal.Stop(c)
    select {
    case <-ctx.Done():
    case <-c:
    cancel()
    f()
    }
    }()
    return ctx
    }
    Add WaitGroup after Cancel Function

    View full-size slide

  50. wg := &sync.WaitGroup{}
    wg.Add(numberOfWorkers)
    ctx := signal.WithContextFunc(
    context.Background(),
    func() {
    wg.Wait()
    close(finishChan)
    },
    )
    go consumer.startConsumer(ctx)

    View full-size slide

  51. End of Program
    select {
    case <-finished:
    case err := <-errChannel:
    if err != nil {
    return err
    }
    }

    View full-size slide

  52. How to auto-scaling
    build agent?

    View full-size slide

  53. Communicate between
    server and agent

    View full-size slide

  54. r := e.Group("/rpc")
    r.Use(rpc.Check())
    {
    r.POST("/v1/healthz", web.RPCHeartbeat)
    r.POST("/v1/request", web.RPCRquest)
    r.POST("/v1/accept", web.RPCAccept)
    r.POST("/v1/details", web.RPCDetails)
    r.POST("/v1/updateStatus", web.RPCUpdateStatus)
    r.POST("/v1/upload", web.RPCUploadBytes)
    r.POST("/v1/reset", web.RPCResetStatus)
    }
    Check RPC Secret

    View full-size slide

  55. /rpc/v1/accept
    Update jobs set version = (oldVersion + 1)
    where machine = "fooBar" and version = oldVersion

    View full-size slide

  56. Create multiple worker

    View full-size slide

  57. if r.Capacity != 0 {
    var g errgroup.Group
    for i := 0; i < r.Capacity; i++ {
    g.Go(func() error {
    return r.start(ctx, 0)
    })
    time.Sleep(1 * time.Second)
    }
    return g.Wait()
    }

    View full-size slide

  58. Break for and select loop
    func (r *Runner) start(ctx context.Context, id int64) error {
    LOOP:
    for {
    select {
    case <-ctx.Done():
    return ctx.Err()
    default:
    r.poll(ctx, id)
    if r.Capacity == 0 {
    break LOOP
    }
    }
    time.Sleep(1 * time.Second)
    }
    return nil
    }

    View full-size slide

  59. How to cancel the current Job?

    View full-size slide

  60. Context with Cancel or Timeout
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    timeout, cancel := context.WithTimeout(ctx, 60*time.Minute)
    defer cancel()
    Job03 context

    View full-size slide

  61. Context with Cancel or Timeout
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    timeout, cancel := context.WithTimeout(ctx, 60*time.Minute)
    defer cancel()
    Job03 context
    Job05 context

    View full-size slide

  62. Watch the Cancel event
    go func() {
    done, _ := r.Manager.Watch(ctx, id)
    if done {
    cancel()
    }
    }()

    View full-size slide

  63. Handle cancel event on Server
    subscribers: make(map[chan struct{}]int64),
    cancelled: make(map[int64]time.Time),

    View full-size slide

  64. User cancel running job
    c.Lock()
    c.cancelled[id] = time.Now().Add(time.Minute * 5)
    for subscriber, build := range c.subscribers {
    if id == build {
    close(subscriber)
    }
    }
    c.Unlock()

    View full-size slide

  65. Agent subscribe the cancel event
    for {
    select {
    case <-ctx.Done():
    return false, ctx.Err()
    case <-time.After(time.Minute):
    c.Lock()
    _, ok := c.cancelled[id]
    c.Unlock()
    if ok {
    return true, nil
    }
    case <-subscriber:
    return true, nil
    }
    }

    View full-size slide

  66. case <-time.After(time.Minute):
    c.Lock()
    _, ok := c.cancelled[id]
    c.Unlock()
    if ok {
    return true, nil
    }

    View full-size slide

  67. case <-time.After(time.Minute):
    c.Lock()
    _, ok := c.cancelled[id]
    c.Unlock()
    if ok {
    return true, nil
    }
    1 Cancel

    View full-size slide

  68. case <-time.After(time.Minute):
    c.Lock()
    _, ok := c.cancelled[id]
    c.Unlock()
    if ok {
    return true, nil
    }
    1
    2 Reconnect Server
    Cancel

    View full-size slide

  69. https://www.udemy.com/course/golang-fight/?couponCode=GOLANG201911

    View full-size slide

  70. https://www.udemy.com/course/devops-oneday/?couponCode=DEVOPS201911

    View full-size slide

  71. Any Question?

    View full-size slide