Sitemap

[Dica rápida] Implementando resiliência em projeto Golang

5 min readOct 21, 2024

Veja nesse post rápido como configurar o Circuit Breaker no seu projeto Golang

Circuit Breaker com Golang

Dando continuidade ao meu post anterior: Introdução ao Circuit Breaker, onde abordei um dos conceitos mais utilizados quando falamos sobre resiliência e tolerância a falhas em sistemas distribuídos.

Hoje vamos explorar a implementação prática dele em um projeto Golang.

Caso tenha interesse em clonar a versão final do código que será apresentado neste artigo, segue o seu link no meu Github: go-circuitbreaker

Bom, avançando para parte do código, eu criei uma arquitetura simples com os seguintes arquivos:

├── config
│ └── config.go
├── handler
│ └── handler.go
├── main.go
├── request
│ └── investment.go
└── request.http

Vamos conhecer cada um destes arquivos. Iniciando pelo arquivo: config/config.go ele é responsável por configurar o Circuit Breaker, definindo:

package config

import (
"time"

"github.com/sony/gobreaker"
)

func NewCircuitBreaker() *gobreaker.CircuitBreaker {
settings := gobreaker.Settings{
Name: "InvestmentAPI",
Timeout: 5 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 3 && failureRatio >= 0.2
},
}
return gobreaker.NewCircuitBreaker(settings)
}

Analisando este trecho de código nós temos:

  • Name: o nome do circuito
  • Timeout: Define quanto tempo o Circuit Breaker permanecerá aberto antes de permitir novas requisições para testar se o serviço se recuperou
  • ReadyToTrip: Este é um callback (função) que define a condição para que o Circuit Breaker entre no estado aberto (ou seja, para que ele “dispare”). O ReadyToTrip é chamado sempre que uma requisição falha ou é bem-sucedida. O retorno true fará com que o Circuit Breaker se abra, enquanto o retorno de false o mantém fechado.

Aqui, a lógica é a seguinte:

  • counts.TotalFailures: O número total de requisições que falharam;
  • counts.Requests: O número total de requisições feitas (bem-sucedidas ou falhas).

A função calcula a proporção de falhas:

failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)

Isso converte os valores inteiros de falhas e requisições para float64 para obter a razão entre o número de falhas e o número total de requisições.

Se o número de requisições for maior ou igual a 3 e a proporção de falhas for maior ou igual a 20% (0.2), o Circuit Breaker será disparado (abrirá):

return counts.Requests >= 3 && failureRatio >= 0.2

Emrequest/investment.go nós temos a implementação das requisições HTTP que o cliente faz para o servidor. Essa função está diretamente integrada ao Circuit Breaker, garantindo que o sistema não tente acessar o servidor de investimentos repetidamente quando ele está fora do ar ou com problemas.

O Circuit Breaker abre o circuito após um número de falhas consecutivas e fecha após um tempo de recuperação.

No trecho de código a seguir eu adicionei um case para que possamos ter um print na console com os status relacionados ao nosso circuito.

package request

import (
"fmt"
"io"
"net/http"

"github.com/sony/gobreaker"
)

type InvestmentRequest struct {
cb *gobreaker.CircuitBreaker
client http.Client
url string
}

func NewInvestmentRequest(cb *gobreaker.CircuitBreaker) *InvestmentRequest {
return &InvestmentRequest{
cb: cb,
client: http.Client{},
url: "http://localhost:8081/api/v1/investment",
}
}

func (r *InvestmentRequest) GetInvestmentData() ([]byte, error) {
switch r.cb.State() {
case gobreaker.StateClosed:
fmt.Println("Circuit Breaker Status: FECHADO")
case gobreaker.StateOpen:
fmt.Println("Circuit Breaker Status: ABERTO")
case gobreaker.StateHalfOpen:
fmt.Println("Circuit Breaker Status: SEMI-ABERTO")
}

body, err := r.cb.Execute(func() (interface{}, error) {
req, err := http.NewRequest(http.MethodGet, r.url, nil)
if err != nil {
return nil, err
}

resp, err := r.client.Do(req)
if err != nil {
fmt.Println("Erro ao chamar a API de investimentos:", err)
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
fmt.Printf("Erro na resposta da API de investimentos: %v\n", resp.Status)
return nil, fmt.Errorf("erro na resposta: %v", resp.Status)
}

return io.ReadAll(resp.Body)
})

if err != nil {
fmt.Printf("Erro ao buscar dados: %v\n", err)
return nil, err
}

return body.([]byte), nil
}

Avançando para o arquivo:handler/handler.go, nele nós temos a lógica que lida com as requisições HTTP.

Esse arquivo é responsável por lidar com as respostas e erros vindos do servidor de investimentos. Caso a requisição falhe, o erro é tratado e o Circuit Breaker entra em ação para impedir que novas requisições sobrecarreguem o sistema.

package handler

import (
"encoding/json"
"fmt"
"go-circuitbreaker/request"
"net/http"
)

type Handler struct {
investmentRequest *request.InvestmentRequest
}

func NewHandler(investmentRequest *request.InvestmentRequest) *Handler {
return &Handler{
investmentRequest: investmentRequest,
}
}

func (h *Handler) GetInvestmentData(w http.ResponseWriter, r *http.Request) {
data, err := h.investmentRequest.GetInvestmentData()
if err != nil {
http.Error(w, fmt.Sprintf("Erro: %v", err), http.StatusInternalServerError)
return
}

response := map[string]interface{}{
"status": "success",
"data": string(data),
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}

Avançando para o nosso arquivo: main.go, aqui como já sabemos é o ponto de entrada da aplicação Golang.

Neste arquivo nós estamos iniciando o servidor de investimentos e configuramos as rotas HTTP que serão consumidas pelo cliente. A função principal também é responsável por configurar o Circuit Breaker e chamar o serviço de investimentos periodicamente, aplicando o Circuit Breaker para proteger o sistema contra falhas repetitivas.

package main

import (
"fmt"
"go-circuitbreaker/config"
"go-circuitbreaker/handler"
"go-circuitbreaker/request"
"net/http"
)

func main() {

circuitBreaker := config.NewCircuitBreaker()
investmentRequest := request.NewInvestmentRequest(circuitBreaker)

investmentHandler := handler.NewHandler(investmentRequest)

http.HandleFunc("/api/v1/investment", investmentHandler.GetInvestmentData)
fmt.Println("Investment API running on port 8080")
http.ListenAndServe(":8080", nil)
}

Por fim, eu criei um arquivo chamado request.http: que nos permite testar as requisições diretamente usando ferramentas como o REST Client no VS Code.

GET http://localhost:8080/api/v1/investment
Accept: application/json

Agora para simular um microserviço de investimentos que simula algumas falhas entre algumas requisições, eu criei o projeto go-invent-example e publiquei ele no server render.com gratuito.

Caso tenha interesse em saber como publicar o seu projeto no Render, eu recomendo a leitura do seguinte artigo onde demonstro esse passo a passo: [Dica rápida] Publicando projeto Golang em server grátis

Agora para testar o nosso passo a passo, eu atualizarei o arquivo investment.go com a nova url publicado do meu projeto:

func NewInvestmentRequest(cb *gobreaker.CircuitBreaker) *InvestmentRequest {
return &InvestmentRequest{
cb: cb,
client: http.Client{},
url: "https://go-invest-example.onrender.com/api/v1/investment",
}
}

Executando ele para que possamos ver o circuito funcionando, nós temos o seguinte resultado na nossa console:

Golang Circuit Breaker

Bom, com isso finalizo mais este artigo, espero que tenham gostado e até o próximo pessoal.

--

--

No responses yet