Backend Development
Golang
Resilience in communication between microservices using the failsafe-go lib
Resilience in communication between microservices using the failsafe-go lib

Let's start at the beginning. What is resilience? I like the definition in this post:
The intrinsic ability of a system to adjust its functioning prior to, during, or following changes and disturbances, so that it can sustain required operations under both expected and unexpected conditions
As it is a broad term, I will focus on communication between microservices in this post. To do this, I created two services using Go: serviceA and serviceB (my creativity was not high when writing this post).
The initial code for both was as follows:
package main
// serviceA
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"os"
"github.com/go-chi/chi/v5"
slogchi "github.com/samber/slog-chi"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := chi.NewRouter()
r.Use(slogchi.New(logger))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
type response struct {
Message string `json:"message"`
}
resp, err := http.Get("http://localhost:3001")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
var data response
err = json.Unmarshal(body, &data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"messageA": "hello from service A","messageB": "` + data.Message + `"}`))
})
http.ListenAndServe(":3000", r)
}
package main
//serviceB
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "hello from service B"}`))
})
http.ListenAndServe(":3001", r)
}
As you can see in the code, if serviceB has a problem, it will affect the functioning of serviceA, as it does not handle any communication failure. We will improve this by using lib failsafe-go.
According to the documentation on the official website:
Failsafe-go is a library for building resilient, fault tolerant Go applications. It works by wrapping functions with one or more resilience policies, which can be combined and composed as needed.
Let's start by applying some available policies and testing their composition.
Timeout
The first policy we will test is the simplest, including a timeout to ensure that the connection is interrupted if serviceB takes too long to respond and the client knows why.
The first step was to change the serviceB so that it includes a delay to make it easier to demonstrate the scenario:
package main
//serviceB
import (
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second) //add a delay to simulate a slow service
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "hello from service B"}`))
})
http.ListenAndServe(":3001", r)
}
After installing failsafe-go, using the commands:
❯ cd serviceA ❯ go get github.com/failsafe-go/failsafe-go
The code of serviceA/main.go is:
package main
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"os"
"time"
"github.com/failsafe-go/failsafe-go"
"github.com/failsafe-go/failsafe-go/failsafehttp"
"github.com/failsafe-go/failsafe-go/timeout"
"github.com/go-chi/chi/v5"
slogchi "github.com/samber/slog-chi"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := chi.NewRouter()
r.Use(slogchi.New(logger))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
type response struct {
Message string `json:"message"`
}
// Create a Timeout for 1 second
timeout := newTimeout(logger)
// Use the Timeout with a failsafe RoundTripper
roundTripper := failsafehttp.NewRoundTripper(nil, timeout)
client := &http.Client{Transport: roundTripper}
resp, err := client.Get("http://localhost:3001")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
var data response
err = json.Unmarshal(body, &data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"messageA": "hello from service A","messageB": "` + data.Message + `"}`))
})
http.ListenAndServe(":3000", r)
}
func newTimeout(logger *slog.Logger) timeout.Timeout[*http.Response] {
return timeout.Builder[*http.Response](1 * time.Second).
OnTimeoutExceeded(func(e failsafe.ExecutionDoneEvent[*http.Response]) {
logger.Info("Connection timed out")
}).Build()
}
To test how it works, I used curl to access the serviceA:
❯ curl -v http://localhost:3000 * Host localhost:3000 was resolved. * IPv6: ::1 * IPv4: 127.0.0.1 * Trying [::1]:3000... * Connected to localhost (::1) port 3000 > GET / HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/8.7.1 > Accept: */* > * Request completely sent off < HTTP/1.1 500 Internal Server Error < Date: Fri, 23 Aug 2024 19:43:23 GMT < Content-Length: 45 < Content-Type: text/plain; charset=utf-8 < * Connection #0 to host localhost left intact Get "http://localhost:3001": timeout exceeded⏎
And the following output is generated by serviceA:
go run main.go
{"time":"2024-08-20T08:37:36.852886-03:00","level":"INFO","msg":"Connection timed out"}
{"time":"2024-08-20T08:37:36.856079-03:00","level":"ERROR","msg":"500: Internal Server Error","request":{"time":"2024-08-20T08:37:35.851262-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63409","referer":"","length":0},"response":{"time":"2024-08-20T08:37:36.856046-03:00","latency":1004819000,"status":500,"length":45},"id":""}
This way, it is possible to see that the client (curl in this case) had an effective response and that serviceA was no significant impact.
Let's improve the resilience of our application by investigating another beneficial policy: retry.
Retry
Again, it was necessary to make a change to serviceB to add random errors:
package main
import (
"math/rand"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
retryAfterDelay := 1 * time.Second
if fail() {
w.Header().Add("Retry-After", strconv.Itoa(int(retryAfterDelay.Seconds())))
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "hello from service B"}`))
})
http.ListenAndServe(":3001", r)
}
func fail() bool {
if flipint := rand.Intn(2); flipint == 0 {
return true
}
return false
}
To make it easier to understand, I am showing one policy at a time, which is why serviceA was changed to the original version and not to the version with a timeout. Later, we will examine how to compose several policies to make the application more resilient.
The code serviceA/main.go looked like this:
package main
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"time"
"github.com/failsafe-go/failsafe-go"
"github.com/failsafe-go/failsafe-go/failsafehttp"
"github.com/failsafe-go/failsafe-go/retrypolicy"
"github.com/go-chi/chi/v5"
slogchi "github.com/samber/slog-chi"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := chi.NewRouter()
r.Use(slogchi.New(logger))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
type response struct {
Message string `json:"message"`
}
// Create a RetryPolicy that only handles 500 responses, with backoff delays between retries
retryPolicy := newRetryPolicy(logger)
// Use the RetryPolicy with a failsafe RoundTripper
roundTripper := failsafehttp.NewRoundTripper(nil, retryPolicy)
client := &http.Client{Transport: roundTripper}
resp, err := client.Get("http://localhost:3001")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
var data response
err = json.Unmarshal(body, &data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"messageA": "hello from service A","messageB": "` + data.Message + `"}`))
})
http.ListenAndServe(":3000", r)
}
func newRetryPolicy(logger *slog.Logger) retrypolicy.RetryPolicy[*http.Response] {
return retrypolicy.Builder[*http.Response]().
HandleIf(func(response *http.Response, _ error) bool {
return response != nil && response.StatusCode == http.StatusServiceUnavailable
}).
WithBackoff(time.Second, 10*time.Second).
OnRetryScheduled(func(e failsafe.ExecutionScheduledEvent[*http.Response]) {
logger.Info(fmt.Sprintf("Retry %d after delay of %d", e.Attempts(), e.Delay))
}).Build()
}
This way, if serviceB returns the status StatusServiceUnavailable (code 503), the connection will be attempted again at progressive intervals, thanks to the function configuration WithBackoff. The output of serviceA, when accessed via curl, should be something similar to:
go run main.go
{"time":"2024-08-20T08:43:38.297621-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:43:38.283715-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63542","referer":"","length":0},"response":{"time":"2024-08-20T08:43:38.297556-03:00","latency":13840708,"status":200,"length":71},"id":""}
{"time":"2024-08-20T08:43:39.946562-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:43:39.943394-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63544","referer":"","length":0},"response":{"time":"2024-08-20T08:43:39.946545-03:00","latency":3151000,"status":200,"length":71},"id":""}
{"time":"2024-08-20T08:43:40.845862-03:00","level":"INFO","msg":"Retry 1 after delay of 1000000000"}
{"time":"2024-08-20T08:43:41.85287-03:00","level":"INFO","msg":"Retry 2 after delay of 2000000000"}
{"time":"2024-08-20T08:43:43.860694-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:43:40.841468-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63545","referer":"","length":0},"response":{"time":"2024-08-20T08:43:43.860651-03:00","latency":3019287458,"status":200,"length":71},"id":""}
In this example, it is possible to see that errors occurred when accessing the serviceB, and the lib executed the connection again until it was successful. If the connection continues to give an error, the client will receive an error message ”http://localhost:3001": retries exceeded.
Let's go deeper into resilience by adding a circuit breaker to our project.
Circuit breaker
The circuit breaker concept is a more advanced policy that provides greater control over access to services. The pattern circuit breaker works in three states: closed (no errors), open (with errors, interrupts transmission), and semi-open (sends a limited number of requests to the service in difficulty to test its recovery).
To use this policy, I made a new version of serviceB so that it could generate more error scenarios and delays:
package main
import (
"math/rand"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
retryAfterDelay := 1 * time.Second
if fail() {
w.Header().Add("Retry-After", strconv.Itoa(int(retryAfterDelay.Seconds())))
w.WriteHeader(http.StatusServiceUnavailable)
return
}
if sleep() {
time.Sleep(1 * time.Second)
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "hello from service B"}`))
})
http.ListenAndServe(":3001", r)
}
func fail() bool {
if flipint := rand.Intn(2); flipint == 0 {
return true
}
return false
}
func sleep() bool {
if flipint := rand.Intn(2); flipint == 0 {
return true
}
return false
}
And the code of serviceA:
package main
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"time"
"github.com/failsafe-go/failsafe-go/circuitbreaker"
"github.com/failsafe-go/failsafe-go/failsafehttp"
"github.com/go-chi/chi/v5"
slogchi "github.com/samber/slog-chi"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := chi.NewRouter()
r.Use(slogchi.New(logger))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
type response struct {
Message string `json:"message"`
}
// Create a CircuitBreaker that handles 503 responses and uses a half-open delay based on the Retry-After header
circuitBreaker := newCircuitBreaker(logger)
// Use the RetryPolicy with a failsafe RoundTripper
roundTripper := failsafehttp.NewRoundTripper(nil, circuitBreaker)
client := &http.Client{Transport: roundTripper}
sendGet := func() (*http.Response, error) {
resp, err := client.Get("http://localhost:3001")
return resp, err
}
maxRetries := 3
resp, err := sendGet()
for i := 0; i < maxRetries; i++ {
if err == nil && resp != nil && resp.StatusCode != http.StatusServiceUnavailable && resp.StatusCode != http.StatusTooManyRequests {
break
}
time.Sleep(circuitBreaker.RemainingDelay()) // Wait for circuit breaker's delay, provided by the Retry-After header
resp, err = sendGet()
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
var data response
err = json.Unmarshal(body, &data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"messageA": "hello from service A","messageB": "` + data.Message + `"}`))
})
http.ListenAndServe(":3000", r)
}
func newCircuitBreaker(logger *slog.Logger) circuitbreaker.CircuitBreaker[*http.Response] {
return circuitbreaker.Builder[*http.Response]().
HandleIf(func(response *http.Response, err error) bool {
return response != nil && response.StatusCode == http.StatusServiceUnavailable
}).
WithDelayFunc(failsafehttp.DelayFunc).
OnStateChanged(func(event circuitbreaker.StateChangedEvent) {
logger.Info(fmt.Sprintf("circuit breaker state changed from %s to %s", event.OldState.String(), event.NewState.String()))
}).
Build()
}
As we can see in the output of serviceA, the circuit breaker is working:
❯ go run main.go
{"time":"2024-08-20T08:51:37.770611-03:00","level":"INFO","msg":"circuit breaker state changed from closed to open"}
{"time":"2024-08-20T08:51:38.771682-03:00","level":"INFO","msg":"circuit breaker state changed from open to half-open"}
{"time":"2024-08-20T08:51:38.776743-03:00","level":"INFO","msg":"circuit breaker state changed from half-open to open"}
{"time":"2024-08-20T08:51:39.777821-03:00","level":"INFO","msg":"circuit breaker state changed from open to half-open"}
{"time":"2024-08-20T08:51:39.784897-03:00","level":"INFO","msg":"circuit breaker state changed from half-open to open"}
{"time":"2024-08-20T08:51:40.786209-03:00","level":"INFO","msg":"circuit breaker state changed from open to half-open"}
{"time":"2024-08-20T08:51:40.792457-03:00","level":"INFO","msg":"circuit breaker state changed from half-open to closed"}
{"time":"2024-08-20T08:51:40.792733-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:51:37.756947-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63699","referer":"","length":0},"response":{"time":"2024-08-20T08:51:40.792709-03:00","latency":3036065875,"status":200,"length":71},"id":""}
This policy allows greater control over errors, allowing serviceB to recover if it is experiencing a problem.
But what do you do when serviceB can no longer return, for whatever reason? In these cases, we can use a fallback.
Fallback
The idea of this policy is to have an alternative if the desired service has a more severe problem and takes a long time to return. To do this, we will change the code serviceA:
package main
import (
"bytes"
"encoding/json"
"io"
"log/slog"
"net/http"
"os"
"github.com/failsafe-go/failsafe-go"
"github.com/failsafe-go/failsafe-go/failsafehttp"
"github.com/failsafe-go/failsafe-go/fallback"
"github.com/go-chi/chi/v5"
slogchi "github.com/samber/slog-chi"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := chi.NewRouter()
r.Use(slogchi.New(logger))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
fallback := newFallback(logger)
roundTripper := failsafehttp.NewRoundTripper(nil, fallback)
client := &http.Client{Transport: roundTripper}
resp, err := client.Get("http://localhost:3001")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
type response struct {
Message string `json:"message"`
}
var data response
err = json.Unmarshal(body, &data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"messageA": "hello from service A","messageB": "` + data.Message + `"}`))
})
http.ListenAndServe(":3000", r)
}
func newFallback(logger *slog.Logger) fallback.Fallback[*http.Response] {
resp := &http.Response{
StatusCode: http.StatusOK,
Header: map[string][]string{"Content-Type": {"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`{"message": "error accessing service B"}`)),
}
return fallback.BuilderWithResult[*http.Response](resp).
HandleIf(func(response *http.Response, err error) bool {
return response != nil && response.StatusCode == http.StatusServiceUnavailable
}).
OnFallbackExecuted(func(e failsafe.ExecutionDoneEvent[*http.Response]) {
logger.Info("Fallback executed result")
}).
Build()
}
In the function newFallback, we can see the creation of one http.Response that the lib will use if the user serviceB does not respond.
This feature allows us to respond to the client while the team responsible for serviceB have time to get the service up and running again.
The output of serviceA is something similar to this:
❯ go run main.go
{"time":"2024-08-20T08:55:27.326475-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:55:27.31306-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63772","referer":"","length":0},"response":{"time":"2024-08-20T08:55:27.326402-03:00","latency":13343208,"status":200,"length":71},"id":""}
{"time":"2024-08-20T08:55:31.756765-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:55:31.754348-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63774","referer":"","length":0},"response":{"time":"2024-08-20T08:55:31.756753-03:00","latency":2404750,"status":200,"length":71},"id":""}
{"time":"2024-08-20T08:55:34.091845-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:55:33.086273-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63775","referer":"","length":0},"response":{"time":"2024-08-20T08:55:34.091812-03:00","latency":1005580625,"status":200,"length":71},"id":""}
{"time":"2024-08-20T08:55:37.386512-03:00","level":"INFO","msg":"Fallback executed result"}
{"time":"2024-08-20T08:55:37.386553-03:00","level":"INFO","msg":"200: OK","request":{"time":"2024-08-20T08:55:37.38415-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:63777","referer":"","length":0},"response":{"time":"2024-08-20T08:55:37.386544-03:00","latency":2393916,"status":200,"length":76},"id":""}
In the next step, we will combine the concepts we've seen to create a more resilient application.
Policy composition
To do this, we need to change the code of serviceA so that it makes use of the policies we have seen so far:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"time"
"github.com/failsafe-go/failsafe-go"
"github.com/failsafe-go/failsafe-go/circuitbreaker"
"github.com/failsafe-go/failsafe-go/failsafehttp"
"github.com/failsafe-go/failsafe-go/fallback"
"github.com/failsafe-go/failsafe-go/retrypolicy"
"github.com/failsafe-go/failsafe-go/timeout"
"github.com/go-chi/chi/v5"
slogchi "github.com/samber/slog-chi"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
r := chi.NewRouter()
r.Use(slogchi.New(logger))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
type response struct {
Message string `json:"message"`
}
retryPolicy := newRetryPolicy(logger)
fallback := newFallback(logger)
circuitBreaker := newCircuitBreaker(logger)
timeout := newTimeout(logger)
roundTripper := failsafehttp.NewRoundTripper(nil, fallback, retryPolicy, circuitBreaker, timeout)
client := &http.Client{Transport: roundTripper}
sendGet := func() (*http.Response, error) {
resp, err := client.Get("http://localhost:3001")
return resp, err
}
maxRetries := 3
resp, err := sendGet()
for i := 0; i < maxRetries; i++ {
if err == nil && resp != nil && resp.StatusCode != http.StatusServiceUnavailable && resp.StatusCode != http.StatusTooManyRequests {
break
}
time.Sleep(circuitBreaker.RemainingDelay()) // Wait for circuit breaker's delay, provided by the Retry-After header
resp, err = sendGet()
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
defer resp.Body.Close()
var data response
err = json.Unmarshal(body, &data)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"messageA": "hello from service A","messageB": "` + data.Message + `"}`))
})
http.ListenAndServe(":3000", r)
}
func newTimeout(logger *slog.Logger) timeout.Timeout[*http.Response] {
return timeout.Builder[*http.Response](10 * time.Second).
OnTimeoutExceeded(func(e failsafe.ExecutionDoneEvent[*http.Response]) {
logger.Info("Connection timed out")
}).Build()
}
func newFallback(logger *slog.Logger) fallback.Fallback[*http.Response] {
resp := &http.Response{
StatusCode: http.StatusOK,
Header: map[string][]string{"Content-Type": {"application/json"}},
Body: io.NopCloser(bytes.NewBufferString(`{"message": "error accessing service B"}`)),
}
return fallback.BuilderWithResult[*http.Response](resp).
HandleIf(func(response *http.Response, err error) bool {
return response != nil && response.StatusCode == http.StatusServiceUnavailable
}).
OnFallbackExecuted(func(e failsafe.ExecutionDoneEvent[*http.Response]) {
logger.Info("Fallback executed result")
}).
Build()
}
func newRetryPolicy(logger *slog.Logger) retrypolicy.RetryPolicy[*http.Response] {
return retrypolicy.Builder[*http.Response]().
HandleIf(func(response *http.Response, _ error) bool {
return response != nil && response.StatusCode == http.StatusServiceUnavailable
}).
WithBackoff(time.Second, 10*time.Second).
OnRetryScheduled(func(e failsafe.ExecutionScheduledEvent[*http.Response]) {
logger.Info(fmt.Sprintf("Retry %d after delay of %d", e.Attempts(), e.Delay))
}).Build()
}
func newCircuitBreaker(logger *slog.Logger) circuitbreaker.CircuitBreaker[*http.Response] {
return circuitbreaker.Builder[*http.Response]().
HandleIf(func(response *http.Response, err error) bool {
return response != nil && response.StatusCode == http.StatusServiceUnavailable
}).
WithDelayFunc(failsafehttp.DelayFunc).
OnStateChanged(func(event circuitbreaker.StateChangedEvent) {
logger.Info(fmt.Sprintf("circuit breaker state changed from %s to %s", event.OldState.String(), event.NewState.String()))
}).
Build()
}
In the code:
roundTripper := failsafehttp.NewRoundTripper(nil, fallback, retryPolicy, circuitBreaker, timeout)
It is possible to view the use of all defined policies. The lib will execute it in the "rightmost" order, that is:
timeout -> circuitBreaker -> retryPolicy -> fallback
We can see the execution of the policies by observing the serviceA output:
go run main.go
{"time":"2024-08-19T10:15:29.226553-03:00","level":"INFO","msg":"circuit breaker state changed from closed to open"}
{"time":"2024-08-19T10:15:29.226841-03:00","level":"INFO","msg":"Retry 1 after delay of 1000000000"}
{"time":"2024-08-19T10:15:30.227941-03:00","level":"INFO","msg":"circuit breaker state changed from open to half-open"}
{"time":"2024-08-19T10:15:30.234182-03:00","level":"INFO","msg":"circuit breaker state changed from half-open to open"}
{"time":"2024-08-19T10:15:30.234258-03:00","level":"INFO","msg":"Retry 2 after delay of 2000000000"}
{"time":"2024-08-19T10:15:32.235282-03:00","level":"INFO","msg":"circuit breaker state changed from open to half-open"}
{"time":"2024-08-19T10:15:42.23622-03:00","level":"INFO","msg":"Connection timed out"}
{"time":"2024-08-19T10:15:42.237942-03:00","level":"INFO","msg":"circuit breaker state changed from half-open to closed"}
{"time":"2024-08-19T10:15:42.238043-03:00","level":"ERROR","msg":"500: Internal Server Error","request":{"time":"2024-08-19T10:15:29.215709-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:52527","referer":"","length":0},"response":{"time":"2024-08-19T10:15:42.238008-03:00","latency":13022704750,"status":500,"length":45},"id":""}
{"time":"2024-08-19T10:15:56.53476-03:00","level":"INFO","msg":"circuit breaker state changed from closed to open"}
{"time":"2024-08-19T10:15:56.534803-03:00","level":"INFO","msg":"Retry 1 after delay of 1000000000"}
{"time":"2024-08-19T10:15:57.535108-03:00","level":"INFO","msg":"circuit breaker state changed from open to half-open"}
{"time":"2024-08-19T10:15:57.53889-03:00","level":"INFO","msg":"circuit breaker state changed from half-open to open"}
{"time":"2024-08-19T10:15:57.538911-03:00","level":"INFO","msg":"Retry 2 after delay of 2000000000"}
{"time":"2024-08-19T10:15:59.539948-03:00","level":"INFO","msg":"circuit breaker state changed from open to half-open"}
{"time":"2024-08-19T10:15:59.544425-03:00","level":"INFO","msg":"circuit breaker state changed from half-open to open"}
{"time":"2024-08-19T10:15:59.544575-03:00","level":"ERROR","msg":"500: Internal Server Error","request":{"time":"2024-08-19T10:15:56.5263-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:52542","referer":"","length":0},"response":{"time":"2024-08-19T10:15:59.544557-03:00","latency":3018352000,"status":500,"length":245},"id":""}
{"time":"2024-08-19T10:16:11.044207-03:00","level":"INFO","msg":"Connection timed out"}
{"time":"2024-08-19T10:16:11.046026-03:00","level":"ERROR","msg":"500: Internal Server Error","request":{"time":"2024-08-19T10:16:01.043317-03:00","method":"GET","host":"localhost:3000","path":"/","query":"","params":{},"route":"/","ip":"[::1]:52544","referer":"","length":0},"response":{"time":"2024-08-19T10:16:11.045601-03:00","latency":10002596334,"status":500,"length":45},"id":""}
Conclusion
One of the advantages of microservices architecture is that we can break a complex domain into smaller, specialized services that communicate with each other to complete the necessary logic. Ensuring that this communication is resilient and will continue to work even in the face of failures and unforeseen events is fundamental. Using libraries such as failsafe-go makes this process easier.
You can find the codes presented in this post on my Github.
Originally published at https://eltonminetto.dev on August 24, 2024
The above is the detailed content of Resilience in communication between microservices using the failsafe-go lib. For more information, please follow other related articles on the PHP Chinese website!
Hot AI Tools
Undress AI Tool
Undress images for free
Undresser.AI Undress
AI-powered app for creating realistic nude photos
AI Clothes Remover
Online AI tool for removing clothes from photos.
Clothoff.io
AI clothes remover
Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!
Hot Article
Hot Tools
Notepad++7.3.1
Easy-to-use and free code editor
SublimeText3 Chinese version
Chinese version, very easy to use
Zend Studio 13.0.1
Powerful PHP integrated development environment
Dreamweaver CS6
Visual web development tools
SublimeText3 Mac version
God-level code editing software (SublimeText3)
Hot Topics
How do I call a method on a struct instance in Go?
Jun 24, 2025 pm 03:17 PM
In Go language, calling a structure method requires first defining the structure and the method that binds the receiver, and accessing it using a point number. After defining the structure Rectangle, the method can be declared through the value receiver or the pointer receiver; 1. Use the value receiver such as func(rRectangle)Area()int and directly call it through rect.Area(); 2. If you need to modify the structure, use the pointer receiver such as func(r*Rectangle)SetWidth(...), and Go will automatically handle the conversion of pointers and values; 3. When embedding the structure, the method of embedded structure will be improved, and it can be called directly through the outer structure; 4. Go does not need to force use getter/setter,
Strategies for Integrating Golang Services with Existing Python Infrastructure
Jul 02, 2025 pm 04:39 PM
TointegrateGolangserviceswithexistingPythoninfrastructure,useRESTAPIsorgRPCforinter-servicecommunication,allowingGoandPythonappstointeractseamlesslythroughstandardizedprotocols.1.UseRESTAPIs(viaframeworkslikeGininGoandFlaskinPython)orgRPC(withProtoco
Understanding the Performance Differences Between Golang and Python for Web APIs
Jul 03, 2025 am 02:40 AM
Golangofferssuperiorperformance,nativeconcurrencyviagoroutines,andefficientresourceusage,makingitidealforhigh-traffic,low-latencyAPIs;2.Python,whileslowerduetointerpretationandtheGIL,provideseasierdevelopment,arichecosystem,andisbettersuitedforI/O-bo
Is golang frontend or backend
Jul 08, 2025 am 01:44 AM
Golang is mainly used for back-end development, but it can also play an indirect role in the front-end field. Its design goals focus on high-performance, concurrent processing and system-level programming, and are suitable for building back-end applications such as API servers, microservices, distributed systems, database operations and CLI tools. Although Golang is not the mainstream language for web front-end, it can be compiled into JavaScript through GopherJS, run on WebAssembly through TinyGo, or generate HTML pages with a template engine to participate in front-end development. However, modern front-end development still needs to rely on JavaScript/TypeScript and its ecosystem. Therefore, Golang is more suitable for the technology stack selection with high-performance backend as the core.
How to completely and cleanly uninstall Golang from my system?
Jun 30, 2025 am 01:58 AM
TocompletelyuninstallGolang,firstdeterminehowitwasinstalled(packagemanager,binary,source,etc.),thenremoveGobinariesanddirectories,cleanupenvironmentvariables,anddeleterelatedtoolsandcaches.Beginbycheckinginstallationmethod:commonmethodsincludepackage
How to use channels for communication between goroutines in golang?
Jun 26, 2025 pm 12:08 PM
In Go language, channel is used for communication and synchronization between goroutines. Declare the use of make function, such as ch:=make(chanstring), send the ch
How to use the select statement in golang for non-blocking channel operations and timeouts?
Jun 26, 2025 pm 01:08 PM
In Go, using select statements can effectively handle non-blocking channel operations and implement timeout mechanisms. Non-blocking reception or sending operations are realized through the default branch, such as 1. Non-blocking reception: if there is a value, it will be received and printed, otherwise the default branch will be executed immediately; 2. Non-blocking transmission: If there is no receiver in the channel, the sending will be skipped. In addition, timeout control can be implemented in conjunction with time.After, such as waiting for the result or returning after 2 seconds. You can also combine non-blocking and timeout behaviors, try to get the value immediately, and wait for a short time after failure, so as to improve the program's concurrent response capabilities.
How to marshal a golang struct to JSON with custom field names?
Jun 30, 2025 am 01:59 AM
In Go, if you want the structure field to use a custom field name when converting to JSON, you can implement it through the json tag of the structure field. 1. Use the json: "custom_name" tag to specify the key name of the field in JSON. For example, Namestringjson: "username"" will make the Name field output as "username"; 2. Add, omitempty can control that the output is omitted when the field is empty, such as Emailstringjson: "email,omitempty""


