Race
func Race[T any](ctx context.Context, fns ...AsyncFunc[T]) (T, error)Race starts every function in its own goroutine and returns the first result that comes with a nil error. Use it when several sources can answer the same question and you want the fastest good answer: replicas of a database, mirrors of a file, regions of a service.
user, err := gollback.Race(ctx, func(ctx context.Context) (User, error) { return primary.User(ctx, id) }, func(ctx context.Context) (User, error) { return replica.User(ctx, id) },)Who wins
Section titled “Who wins”The first function to return a nil error wins. A function that fails doesn’t end the race: the others keep going, and the first of them to succeed still wins.
Race returns as soon as there is a winner. It doesn’t wait for the others.
The losers are cancelled
Section titled “The losers are cancelled”Every function gets a context derived from the one you pass. When a function wins, Race cancels that
context, so the others’ requests, queries and timers stop. A function that respects its context returns
early with context.Canceled; Race has already returned, and that result is dropped.
When every function fails
Section titled “When every function fails”If no function succeeds, Race returns the zero value and all of their errors, joined with
errors.Join in the order of the functions, not the order they failed in.
Look for a particular one with errors.Is or errors.As:
_, err := gollback.Race(ctx, fromCache, fromDatabase)if errors.Is(err, sql.ErrNoRows) { // at least one source says there is no such row}With no functions at all, Race returns ErrNoCallbacks.
When your context is done first
Section titled “When your context is done first”If the context you passed is cancelled, or its deadline passes, before any function succeeds,
Race returns at once with that context’s error, joined with the errors of the functions that had failed by then,
and the functions see their context cancelled too. errors.Is(err, context.DeadlineExceeded) tells you the deadline
passed. If the context is done before you call Race, no function is called.
Give a race a deadline the same way you give one to a request:
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)defer cancel()
user, err := gollback.Race(ctx, fromPrimary, fromReplica)Example: the fastest mirror
Section titled “Example: the fastest mirror”// Command race fetches the same file from several mirrors and prints whichever answers first.//// go run ./examples/race 'https://go.dev/VERSION?m=text' 'https://golang.google.cn/VERSION?m=text'package main
import ( "context" "fmt" "io" "log" "net/http" "os" "time"
"github.com/vardius/gollback/v2")
// fetchFirst asks every mirror at once. The first good answer wins and the other requests are cancelled.func fetchFirst(ctx context.Context, client *http.Client, urls ...string) (string, error) { fetches := make([]gollback.AsyncFunc[string], len(urls)) for i, url := range urls { fetches[i] = func(ctx context.Context) (string, error) { return get(ctx, client, url) } }
return gollback.Race(ctx, fetches...)}
func get(ctx context.Context, client *http.Client, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", err } resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("%s: %s", url, resp.Status) } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", fmt.Errorf("%s: %w", url, err) }
return string(body), nil}
func main() { log.SetFlags(0) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()
body, err := fetchFirst(ctx, http.DefaultClient, os.Args[1:]...) if err != nil { log.Fatal(err) } fmt.Print(body)}