Home > Software engineering >  Simultaneous calls should fetch data only once
Simultaneous calls should fetch data only once

Time:08-06

I'm running into an unusual scenario where:

  • an external tool calls multiple endpoints of my API at the same time,
  • all endpoints rely on the same config file hosted somewhere on S3.

That works but it's fetching the same config file many times concurrently when it could be fetched only once. To experiment with this I have a minimal version here https://go.dev/play/p/Nx-kidmprQx that gives back random ints rather than doing HTTP calls.

Currently it prints:

#2 start
#1 start
#1 result 5577006791947779410
#2 result 8674665223082153551
#3 start
#3 result 6129484611666145821

But I would like the first 2 calls to return the same value because they are done concurrently:

#2 start
#1 start
#1 result 5577006791947779410
#2 result 5577006791947779410
#3 start
#3 result 6129484611666145821

I'm struggling to imagine a solution for this. The fact that multiple goroutines should wait on a single result is confusing. How could it be done?

CodePudding user response:

You can use the Group.Do implementation from golang.org/x/sync/singleflight

// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.

Modifying your example to separate the synchronous part of the function from the channel operations:

func httpCall(run int) int {
    fmt.Printf("#%d start\n", run)
    time.Sleep(time.Second)
    return rand.Int()
}

You can call that with a simple wrapper function to handle the singleflight.Group call:

go func() {
    res, _, _ := g.Do("httpCall", func() (any, error) {
        return httpCall(1), nil
    })
    done <- res.(int)
}()

See https://go.dev/play/p/ET0jNKEyg1_B

CodePudding user response:

You may use a context with a cancellation.

In your example:

func httpCall(ctx context.Context, run int, done chan<- int) {
    fmt.Printf("#%d start\n", run)
    select {
    case <-time.After(1 * time.Second):
        done <- rand.Int()
    case <-ctx.Done():
        return
    }
}

Modified playground: https://go.dev/play/p/ooPMqbSHuiG

  • Related