Getting started
Dispatch makes it easy to work with rate limited or unreliable servers, like the one we’ve prepared for this guide. We’re hosting a server that serves random cat facts, but it’s very unreliable and fails with 500 quite often.
cat-facts.dispatch.run
Imagine our application is critically dependant on that crucial cat knowledge. Normally, we’d have to implement retries on request errors or even push this work into a queue. But with Dispatch, all we have to do is wrap the function with a dispatch.Func
call and we’ll fetch our cat facts reliably no matter what.
Let’s see how simple it is to build reliable systems with Dispatch and maybe learn a thing or two about cats.
Setup the development environment
CLI is the fastest way to create a Dispatch account and set up local development environment.
It can be installed via Homebrew:
brew tap dispatchrun/dispatch && \brew install dispatch
Or install from sources with Go
go install github.com/dispatchrun/dispatch@latest
Next, log in to create an account:
dispatch login
login
command will open a browser to create a Dispatch account, create an API key and save it on your machine.
If you’re starting from scratch, initialize a Go project:
go mod init
Then install our Go SDK:
go get github.com/dispatchrun/dispatch-go@latest
Create a basic application
Put Dispatch aside for now and create a simple HTTP server that fetches a cat fact from https://cat-facts.dispatch.run and prints it. Create an main.go file and paste the following code:
package main
import ( "context" "errors" "io" "log" "net/http")
func main() { fetchCatFact := func(ctx context.Context) (any, error) { response, err := http.Get("https://cat-facts.dispatch.run") if err != nil { return nil, err }
if response.StatusCode != 200 { return nil, errors.New("cat facts server is down") }
defer response.Body.Close()
body, err := io.ReadAll(response.Body) if err != nil { return nil, err }
log.Printf("Cat fact: %s", body)
return nil, nil }
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := fetchCatFact(r.Context()) if err != nil { log.Println(err) }
w.WriteHeader(http.StatusOK) })
if err := http.ListenAndServe("localhost:8000", nil); err != nil { log.Fatal(err) }}
Start the application:
go run main.go
Send a request to /
to confirm it’s working:
curl http://localhost:8000
If it hasn’t failed right away, try sending a few more requests to stumble upon a failure in the cat facts API.
cat facts server is down
This means that fetchCatFact
function raised an exception and now the application is left without a cat fact.
Make the application reliable
Fortunately, Dispatch can be added to make any Go function retry on failure and limit the execution rate.
First, import a Dispatch SDK.
package main
import ( "context" "errors" "io" "log" "net/http"
"github.com/dispatchrun/dispatch-go")
func main() { fetchCatFact := func(ctx context.Context) (any, error) { response, err := http.Get("https://cat-facts.dispatch.run") if err != nil { return nil, err }
if response.StatusCode != 200 { return nil, errors.New("failed to fetch a cat fact") }
defer response.Body.Close()
body, err := io.ReadAll(response.Body) if err != nil { return nil, err }
log.Printf("Cat fact: %s", body)
return nil, nil }
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := fetchCatFact(r.Context()) if err != nil { log.Println(err) }
w.WriteHeader(http.StatusOK) })
if err := http.ListenAndServe("localhost:8000", nil); err != nil { log.Fatal(err) }}
Next, set up a handler that Dispatch will call remotely on your server.
package main
import ( "context" "errors" "io" "log" "net/http"
"github.com/dispatchrun/dispatch-go")
func main() { fetchCatFact := func(ctx context.Context) (any, error) { response, err := http.Get("https://cat-facts.dispatch.run") if err != nil { return nil, err }
if response.StatusCode != 200 { return nil, errors.New("failed to fetch a cat fact") }
defer response.Body.Close()
body, err := io.ReadAll(response.Body) if err != nil { return nil, err }
log.Printf("Cat fact: %s", body)
return nil, nil }
endpoint, err := dispatch.New() if err != nil { log.Fatal(err) }
http.Handle(endpoint.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := fetchCatFact(r.Context()) if err != nil { log.Println(err) }
w.WriteHeader(http.StatusOK) })
if err := http.ListenAndServe("localhost:8000", nil); err != nil { log.Fatal(err) }}
Next, wrap fetchCatFact
in a dispatch.Func
call, add it to the Dispatch endpoint and update how fetchCatFact
is called.
Use the Dispatch
method on the fetchCatFact
function to call it asynchronously via Dispatch.
package main
import ( "context" "errors" "io" "log" "net/http"
"github.com/dispatchrun/dispatch-go")
func main() { fetchCatFact := func(ctx context.Context) (any, error) { fetchCatFact := dispatch.Func(func(ctx context.Context, arg any) (any, error) { response, err := http.Get("https://cat-facts.dispatch.run") if err != nil { return nil, err }
if response.StatusCode != 200 { return nil, errors.New("failed to fetch a cat fact") }
defer response.Body.Close()
body, err := io.ReadAll(response.Body) if err != nil { return nil, err }
log.Printf("Cat fact: %s", body)
return nil, nil } })
endpoint, err := dispatch.New() endpoint, err := dispatch.New(fetchCatFact) if err != nil { log.Fatal(err) }
http.Handle(endpoint.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := fetchCatFact(r.Context()) if err != nil { log.Println(err) } fetchCatFact.Dispatch(r.Context())
w.WriteHeader(http.StatusOK) })
if err := http.ListenAndServe("localhost:8000", nil); err != nil { log.Fatal(err) }}
Lastly, we need to tell Dispatch that it’s safe to retry fetchCatFact
execution when we failed to fetch a cat fact due to API issues.
package main
import ( "context" "errors" "io" "log" "net/http"
"github.com/dispatchrun/dispatch-go")
type CatFactUnavailable struct{}
func (error *CatFactUnavailable) Error() string { return "failed to fetch a cat fact" }
func (error *CatFactUnavailable) Temporary() bool { return true }
func main() { fetchCatFact := func(ctx context.Context) (any, error) { fetchCatFact := dispatch.Func(func(ctx context.Context, arg any) (any, error) { response, err := http.Get("https://cat-facts.dispatch.run") if err != nil { return nil, err return nil, &CatFactUnavailable{} }
if response.StatusCode != 200 { return nil, errors.New("failed to fetch a cat fact") return nil, &CatFactUnavailable{} }
defer response.Body.Close()
body, err := io.ReadAll(response.Body) if err != nil { return nil, err return nil, &CatFactUnavailable{} }
log.Printf("Cat fact: %s", body)
return nil, nil } })
endpoint, err := dispatch.New(fetchCatFact) if err != nil { log.Fatal(err) }
http.Handle(endpoint.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, err := fetchCatFact(r.Context()) if err != nil { log.Println(err) }
fetchCatFact.Dispatch(r.Context())
w.WriteHeader(http.StatusOK) })
if err := http.ListenAndServe("localhost:8000", nil); err != nil { log.Fatal(err) }}
That’s all it took to make fetchCatFact
retry on request errors and asynchronously execute in background.
Restart the application, but this time use Dispatch CLI:
dispatch run -- go run main.go
Now, try sending a few more GET /
requests to this server and see how failures are automatically retried.
This is the magic of Dispatch. It ensures that fetchCatFact
is executed successfully no matter what, whether HTTP requests fail or your server has suddenly rebooted.
Under the hood, when fetchCatFact.Dispatch
is called, fetchCatFact
is not executed immediately. Instead, Dispatch schedules the execution of fetchCatFact
and pings back our server when it’s the time to do so. That way, fetchCatFact
is executed asynchronously in background. Whenever a failure happens, Dispatch is notified and retries the execution again.