1

I want to run 2 goroutines parallel in App Engine, so that when the first goroutine finish its job, the handler doesn't need to wait the second goroutine - it stops the secend goroutine and returns the result to the client. Is this possible? I tried it with context.WithCancel(), but it didn't work (I use go1.6).

Here is my code:

package mytest

import (
    "net/http"
    "sync"
    "time"

    "golang.org/x/net/context"
    "google.golang.org/appengine"
    "google.golang.org/appengine/log"
    "google.golang.org/appengine/urlfetch"
)

func init() {
    http.HandleFunc("/test", handlerTest)
    http.HandleFunc("/testwait10s", handlerTest10s)
    http.HandleFunc("/testwait5s", handlerTest5s)
}

func handlerTest(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    ctx, _ := context.WithTimeout(c, 30*time.Second)

    ctx1, ctx1Cancel := context.WithCancel(ctx)
    ctx2, ctx2Cancel := context.WithCancel(ctx)

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        log.Infof(ctx1, "Go1 begin ...")
        client1 := urlfetch.Client(ctx1)
        _, err := client1.Get("http://APP_NAME.appspot.com/testwait5s")
        if err != nil {
            log.Errorf(ctx1, "Go1 failed:  %v", err)
        }
        ctx2Cancel()
        log.Infof(ctx1, "Go1 over ...")
    }()

    go func() {
        defer wg.Done()
        log.Infof(ctx2, "Go2 begin ...")
        client2 := urlfetch.Client(ctx2)
        _, err := client2.Get("http://APP_NAME.appspot.com/testwait10s")
        if err != nil {
            log.Errorf(ctx2, "Go2 failed %v", err)
        }
        ctx1Cancel()
        log.Infof(ctx2, "Go2 over ...")
    }()

    wg.Wait()
    log.Infof(ctx1, "Go1 and GO2 over")
}

func handlerTest10s(w http.ResponseWriter, r *http.Request) {
    time.Sleep(10 * time.Second)
    return
}

func handlerTest5s(w http.ResponseWriter, r *http.Request) {
    time.Sleep(5 * time.Second)
    return
}

Any ideas? Thanks!

Ryan A.
  • 411
  • 4
  • 13
Xin
  • 153
  • 2
  • 15
  • 2
    [You can't kill a goroutine from the "outside"](https://stackoverflow.com/questions/28240133/cancel-a-blocking-operation-in-go/28240299#28240299), the goroutine has to support its termination, e.g. by monitoring a channel, which may be communicated via a `context.Context`. – icza Apr 03 '18 at 09:19
  • "You can't kill a goroutine from the "outside". - yes, I agree. So I try to control the context app engine, maybe there is any solution ? – Xin Apr 04 '18 at 11:42
  • "a channel, which may be communicated via a context.Context" -> I tried context.WithCancel(), but it doesn't work – Xin Apr 04 '18 at 11:45

2 Answers2

1

Just create a notification channel and send there a signal that one of computations is over and you can proceed without waiting for the other.

func handlerTest(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    ctx, cancel := context.WithTimeout(c, 30*time.Second)
    done := make(chan error, 2)

    work := func(url, name string) {
        log.Infof(ctx, "%s begin ...", name)
        client := urlfetch.Client(ctx)
        req, err := http.NewRequest(http.MethodGet, url, nil)
        if err != nil {
            log.Errorf(ctx, "%s failed:  %v", name, err)
            done <- err
            return
        }
        req = req.WithContext(ctx)
        _, err = client.Do(req)
        done <- err
        if err != nil {
            log.Errorf(ctx, "%s failed:  %v", name, err)
            return
        }
        cancel()
        log.Infof(ctx, "%s over ...", name)
    }
    go work("go1", "http://APP_NAME.appspot.com/testwait5s")
    go work("go2", "http://APP_NAME.appspot.com/testwait10s")

    for i := 0; i < cap(done); i++ {
        if err := <-done; err == nil {
            log.Infof(ctx, "executed without errors")
            return
        }
    }
    log.Error(ctx, "both computations have failed")
}
Pavlo Strokov
  • 1,967
  • 14
  • 14
  • Thanks your answer. I hope that: when goroutine1 finish its job, the handler can stop goroutine2 immediately. – Xin Apr 04 '18 at 11:49
  • @Xin I have changed implementation, please take a look. Now once one request succeeded another will be stopped waiting for the response. – Pavlo Strokov Apr 04 '18 at 13:34
  • I tried your code, yes, it works! Thanks. But I have to find another solution, because the request.WithContext() is supported from go1.7, but I use still go1.6 in my project, so the func cancel() with timeout doesn't work for me. – Xin Apr 05 '18 at 14:52
  • For 1.6 there is no way to cancel in-fly connection. The only possible thing you can do is to call `https://golang.org/pkg/net/http/#Transport.CancelRequest`, but this can be called only after RoundTrip is finished (request response returned). – Pavlo Strokov Apr 06 '18 at 05:53
  • you are right, I think that I have to change my go version, or just give up the idea of concurrence. Thanks your help! – Xin Apr 06 '18 at 13:16
  • @Xin, if you consider Pavlo's answer to be helpful for you, please consider accepting it. Thank you. – Rodrigo C. Apr 16 '18 at 10:06
0

You can try reducing the value of wg.Add() to wg.Add(1) instead of wg.Add(2).

When one go-routine completes, wg.Done() will reduce the counter value by 1. So, In this case, the WaitGroup (wg) counter value will become ZERO. As a result, wg.Wait() on last line, will not wait for other go-routines to complete.

Note that, if the value of wg counter falls below zero, it will panic the remaining go-routines. So, the go-routines will be exited forcefully.

  • This is exactly my first idea. But the risk is: the seconde goroutine is still running, even the handler has finished, as a result app engine will create too many instances – Xin Apr 04 '18 at 11:37