Real case: I need to retrieve data from a web service (main process), but in case that web service is taking too long, I retrieve the data from the cache (that data might be older) in a parallel process (alt process). If the main process completes first, I return that data. If the main process doesn't complete before the timeout, then the alt process returns either the data (if alt process has completed) or a default value, if alt process hasn't completed by the timeout.
I have built the function that provides the generic capability, but I am not sure if it's the best way to implement the logic.
package common
import (
"time"
)
// Function launches the race between mainFunc and mainFunc, with a cutoff timeout
// If mainFunc finishes before the timeout, its result is sent to chResult
// If timeout is triggered before mainFunc is complete, then:
// If altFunc is complete by then, its result is sent to chResult
// Otherwise, defaultResult is sent to chResult
func LaunchRace[T any](chResult chan T, delayTimeout time.Duration, mainFunc func(chan T), altFunc func(chan T), defaultResult T) {
go func(ch chan T) {
mainFunc(ch)
}(chResult)
go func(ch chan T) {
chTimeout := time.After(delayTimeout)
ch1 := make(chan T)
go func() {
altFunc(ch1)
}()
result := defaultResult
for {
select {
case result = <-ch1:
continue
case <-chTimeout:
ch <- result
return
}
}
}(chResult)
}
And some unit tests for it:
package common
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type Delay struct {
DelayMain time.Duration
DelayAlt time.Duration
Timeout time.Duration
}
func Scenario(t *testing.T, delay Delay, chResult chan string) {
LaunchRace(chResult, delay.Timeout,
func(ch chan string) {
time.Sleep(delay.DelayMain)
ch <- "main"
}, func(ch chan string) {
time.Sleep(delay.DelayAlt)
ch <- "alt"
}, "default")
}
func TestMain(t *testing.T) {
delay := Delay{
DelayMain: 1 * time.Second,
DelayAlt: 2 * time.Second,
Timeout: 3 * time.Second,
}
chResult := make(chan string)
Scenario(t, delay, chResult)
result := <-chResult
assert.Equal(t, "main", result)
}
func TestAlt(t *testing.T) {
delay := Delay{
DelayMain: 10 * time.Second,
DelayAlt: 1 * time.Second,
Timeout: 2 * time.Second,
}
chResult := make(chan string)
Scenario(t, delay, chResult)
result := <-chResult
assert.Equal(t, "alt", result)
}
func TestDefault(t *testing.T) {
delay := Delay{
DelayMain: 10 * time.Second,
DelayAlt: 2 * time.Second,
Timeout: 1 * time.Second,
}
chResult := make(chan string)
Scenario(t, delay, chResult)
result := <-chResult
assert.Equal(t, "default", result)
}