DEV Community

Matheus Mina
Matheus Mina

Posted on

Circuit Breaker em aplicações Go

Nos dias de hoje, é bem comum que nossa aplicação dependa de outras, principalmente se estamos trabalhando em um ambiente de microsserviços. É bem comum que nossa aplicação comece a reportar erros, que ao se investigar, notamos que alguma API de uma equipe parceira ou fornecedor está fora do ar.

Uma boa prática para aumentar a resiliência da nossa aplicação, é cortar a comunicação com essas aplicações que estão em estado depreciados. Observando outras áreas, absorvermos da Engenharia Elétrica o conceito de Circuit Breaker. Nele é colocado um equipamento, ou disjuntor, que se desliga automaticamente caso alguma falha aconteça. Isso é muito comum em nossas casas, que possuem disjuntores que se desligam sozinhos caso a rede elétrica comece a ficar instável.

Já na computação, o nosso Circuit Breaker é um pouco mais complexo, uma vez que definimos também um estado intermediário. O desenho abaixo explica melhor o funcionamento de um Circuit Breaker:

Circuit Breaker

Por fim, os estados são:

  • open: não há comunicação entre as aplicações. Ao atingir este estado, um temporizador se inicia para dar tempo do serviço de reestabeler. Ao fim do temporizador, transitamos para half-open.
  • closed: há comunicação entre as aplicações. A cada requisição feita com falhas, um contador é atualizado. Se for atingido o limite de falhas, transitamos para o circuito para open.
  • half-open: estado de recuperação até a comunicação poder fluir completamente. Nele um contador de sucessos é atualizado a cada requisição. Se for atingido o número ideal de sucessos, transitamos o circuito para closed. Se as requisições falharem, transitamos de volta para open.

Bem legal, né? Mas para exemplificar melhor o conceito, que tal fazermos na prática?

Primeiro, vamos construir nosso serviço A. Ele vai ser responsável por receber as requisições, ou seja, ele vai ser o serviço que nossa aplicação depende, o serviço do fornecedor, ou etc. Para facilitar, vamos expor dois endpoints, um /success que vai retornar sempre 200 e um /failure que vai retornar sempre 500.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) { 
    w.WriteHeader(http.StatusOK) })
    http.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) { 
    w.WriteHeader(http.StatusInternalServerError) })

    fmt.Println("Server is running at http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Já o serviço B vai ser responsável por chamar o serviço A. É ele quem vai construir o nosso circuit breaker. Para nossa sorte, a comunidade de Go já tem a biblioteca gobreaker que implementa o padrão! Primeiro, definimos as propriedades do nosso breaker:

var st gobreaker.Settings
st.Name = "Circuit Breaker PoC"
st.Timeout = time.Second * 5
st.MaxRequests = 2
st.ReadyToTrip = func(counts gobreaker.Counts) bool {
    return counts.ConsecutiveFailures >= 1
}
Enter fullscreen mode Exit fullscreen mode

Apesar da biblioteca nos permitir customizar mais coisas, vamos focar em três:

  • Timeout: o tempo que o circuito vai ficar no estado open. No nosso caso, foi definido o tempo de 5 segundos.
  • MaxRequests: quantidade de requisições bem sucedidas antes de ir para closed. No nosso exemplo, definimos em 2 requisições.
  • ReadyToTrip: define a condição para transitar de closed para open. Para facilitar, vamos dizer que uma falha é suficiente.

Depois já podemos inicializar o breaker e realizar requisições:

cb := gobreaker.NewCircuitBreaker[int](st)

url := "http://localhost:8080/success"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Circuit Breaker state:", cb.State()) // closed!

url = "http://localhost:8080/failure"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Circuit Breaker state:", cb.State()) // open!

time.Sleep(time.Second * 6)
url = "http://localhost:8080/success"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Circuit Breaker state:", cb.State()) // half-open!

url = "http://localhost:8080/success"
cb.Execute(func() (int, error) { return Get(url) })
fmt.Println("Circuit Breaker state:", cb.State()) // closed!
Enter fullscreen mode Exit fullscreen mode

Podemos notar que o gobreaker funciona como um wrapper em uma função. Se a função retornar um erro, ele aumenta a quantidade de erros, se não, aumenta a quantidade de sucessos. Vamos então definir essa função:

func Get(url string) (int, error) {
    r, _ := http.Get(url)

    if r.StatusCode != http.StatusOK {
        return r.StatusCode, fmt.Errorf("failed to get %s", url)
    }

    return r.StatusCode, nil
}
Enter fullscreen mode Exit fullscreen mode

E temos nosso serviço Go usando um circuit breaker! Ao utilizar esse padrão, você consegue aumentar a resiliência e a tolerância a falhas dos seus serviços. Podemos notar que ao utilizar a biblioteca, a complexidade foi toda abstraida, tornando muito simples o processo de integramos isso em nosso dia a dia. Se quiser ver o código todo da prova de conceito é só acessar aqui.

Se tiver curiosidade para conhecer outros padrões de resiliência, o Elton Minetto publicou um ótimo post sobre o tema!

Me diga o que você achou dessa postagem nos comentários e fica uma pergunta: você já utilizou circuit breakers antes? Ah, você também pode me encontrar no meu blog pessoal!

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (0)