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

Handling slow requests in your Go web server

Handling slow requests in your Go web server

(delivered at dotGo Paris, 2017 https://youtu.be/0qKqVSvB1G0)

A common pattern when serving web requests that produce long-running processes is to send a work message to a queue, such as beanstalkd, and then handle the work in async fashion.
In Go we can
1) implement the queue within the same Go process.
or 2) forgo the queue idea completely.

Jaime Silvela

November 06, 2017
Tweet

More Decks by Jaime Silvela

Other Decks in Programming

Transcript

  1. Slow requests are trouble package foo type SlowJob struct {

    // fields } func doSlowJob(j Job) { // takes minutes }
  2. Slow requests are trouble type handler struct { // stuff

    } func (h handler) ServeHTTP(…) { job := parseJob(r) doSlowJob(job) // render, encode… } package foo type SlowJob struct { // fields } func doSlowJob(j Job) { // takes minutes }
  3. Problems with the solution • Infrastructure deployment and maintenance •

    Your slow jobs now execute
 in a separate process • Serialization and deserialization
  4. Problems with the solution package foo type SlowJob struct {

    file io.Reader // > 100MB other Stuff } func storeDB(j SlowJob) { … } • Infrastructure deployment and maintenance • Your slow jobs now execute
 in a separate process • Serialization and deserialization
  5. Solution 1: queue with channels • Add a channel to

    our handler • Identify jobs with a ticket number, track progress in DB • Send jobs to channel, return
 quickly • A goroutine (or more) checks 
 channel for jobs
  6. Solution 1: queue with channels type SlowReq struct { job

    SlowJob ticket int } type slowHandlerQ struct { queue chan SlowReq db *memcache.Client } • Add a channel to our handler • Identify jobs with a ticket number, track progress in DB • Send jobs to channel, return
 quickly • A goroutine (or more) checks 
 channel for jobs
  7. Solution 1: queue with channels func newSlowHandlerQ(db *memcache.Client) slowHandlerQ {

    queue := make(chan SlowReq) go func(q <-chan SlowReq) { for { req := <-q result := doSlowJob(req.job) publish(req.ticket, result, db) } }(queue) return slowHandlerQ{…} }
  8. Solution 1: queue with channels func newSlowHandlerQ(db *memcache.Client) slowHandlerQ {

    queue := make(chan SlowReq) go func(q <-chan SlowReq) { for { req := <-q result := doSlowJob(req.job) publish(req.ticket, result, db) } }(queue) return slowHandlerQ{…} }
  9. Solution 1: queue with channels func (h slowHandlerQ) ServeHTTP(…) {

    job := // parse request params into SlowJob ticket, _ := getTicket(h.db) h.queue <- SlowReq{ job: job, ticket: int(ticket), } w.Write([]byte(fmt.Sprintf("ticket: %d", ticket))) }
  10. Solution 1: queue with channels func (h slowHandlerQ) ServeHTTP(…) {

    job := // parse request params into SlowJob ticket, _ := getTicket(h.db) h.queue <- SlowReq{ job: job, ticket: int(ticket), } w.Write([]byte(fmt.Sprintf("ticket: %d", ticket))) }
  11. Problems • The channel send is synchronous, and may block/wait

    • We can add a time limit to the send, drop if limit exceeded • We can make the channel buffered • We can add more worker goroutines • … we have a new dimensioning problem in our server
  12. Solution 2: delegate • Handler delegates the work
 to a

    goroutine, returns quickly • Identify jobs with a ticket
 number, track progress in DB
  13. Solution 2: delegate type slowHandlerD struct { db *memcache.Client }

    • Handler delegates the work
 to a goroutine, returns quickly • Identify jobs with a ticket
 number, track progress in DB
  14. Solution 2: delegate func (h slowHandlerD) ServeHTTP(…) { job :=

    // parse request params into SlowJob ticket, _ := getTicket(h.db) go func(j SlowJob, ticket int) { result := doSlowJob(j) publish(ticket, result, h.db) }(job, int(ticket)) w.Write([]byte(fmt.Sprintf("ticket: %d", ticket))) }
  15. Solution 2: delegate func (h slowHandlerD) ServeHTTP(…) { job :=

    // parse request params into SlowJob ticket, _ := getTicket(h.db) go func(j SlowJob, ticket int) { result := doSlowJob(j) publish(ticket, result, h.db) }(job, int(ticket)) w.Write([]byte(fmt.Sprintf("ticket: %d", ticket))) }
  16. Graceful shutdown type slowHandlerD struct { db *memcache.Client wg *sync.Waitgroup

    } func (h slowHandlerD) ServeHTTP(…) { … h.wg.Add(1) go func(j SlowJob, ticket int) { defer h.wg.Done()
  17. Other improvements • Cancellation • Monitoring in-transit jobs • Saving

    state quickly to recover after restart 
 (c.f. 12 factor app)