Contexto
Softwares geralmente iniciam processos de longa duração e de uso intensivo de recursos (muitas vezes em goroutines). Se a ação que causou isso é cancelada ou falha por algum motivo, você precisa parar esses processos de forma consistente dentro da sua aplicação.
Se você não gerenciar isso, sua aplicação Go tão ágil da qual você tem tanto orgulho pode começar a ter problemas de desempenho difíceis de depurar.
Neste capítulo vamos usar o pacote
context
para nos ajudar a gerenciar processos de longa duração.Vamos começar com um exemplo clássico de um servidor web que, quando iniciado, abre um processo de longa execução que vai buscar alguns dados para devolver em uma resposta.
Colocaremos em prática um cenário em que um usuário cancela a requisição antes que os dados possam ser recuperados e faremos com que o processo seja instruído a desistir.
Criei um código no caminho feliz para começarmos. Aqui está o código do nosso servidor.
func Server(store Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, store.Fetch())
}
}
A função
Server
(servidor) recebe uma Store
(armazenamento) e nos retorna um http.HandlerFunc
. Store está definida como:type Store interface {
Fetch() string
}
A função retornada chama o método
Fetch
(busca) da Store
para obter os dados e escrevê-los na resposta.Nós temos um stub correspondente para
Store
que usamos em um teste.type StubStore struct {
response string
}
func (s *StubStore) Fetch() string {
return s.response
}
func TestHandler(t *testing.T) {
data := "olá, mundo"
svr := Server(&StubStore{data})
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
svr.ServeHTTP(response, request)
if response.Body.String() != data {
t.Errorf(`resultado "%s", esperado "%s"`, response.Body.String(), data)
}
}
Agora que temos um caminho feliz, queremos fazer um cenário mais realista onde a
Store
não consiga finalizar o Fetch
antes que o usuário cancele a requisição.Nosso handler precisará de uma maneira de dizer à
Store
para cancelar o trabalho, então atualize a interface.type Store interface {
Fetch() string
Cancel()
}
Precisaremos ajustar nosso spy para que leve algum tempo para retornar
data
e uma maneira de saber que foi dito para cancelar. Nós também o renomearemos para SpyStore
, pois agora vamos observar a forma como ele é chamado. Ele terá que adicionar Cancel
como um método para implementar a interface Store
.type SpyStore struct {
response string
cancelled bool
}
func (s *SpyStore) Fetch() string {
time.Sleep(100 * time.Millisecond)
return s.response
}
func (s *SpyStore) Cancel() {
s.cancelled = true
}
Vamos adicionar um novo teste onde cancelamos a requisição antes de 100 milissegundos e verificamos a store para ver se ela é cancelada.
t.Run("avisa a store para cancelar o trabalho se a requisição for cancelada", func(t *testing.T) {
store := &SpyStore{response: data}
svr := Server(store)
request := httptest.NewRequest(http.MethodGet, "/", nil)
cancellingCtx, cancel := context.WithCancel(request.Context())
time.AfterFunc(5 * time.Millisecond, cancel)
request = request.WithContext(cancellingCtx)
response := httptest.NewRecorder()
svr.ServeHTTP(response, request)
if !store.cancelled {
t.Errorf("store não foi avisada para cancelar")
}
})
Do blog da Google novamente:
O pacote context fornece funções para derivar novos valores de contexto dos já existentes. Estes valores formam uma árvore: quando um contexto é cancelado, todos os contextos derivados dele também são cancelados.
É importante que você derive seus contextos para que os cancelamentos sejam propagados através da pilha de chamadas (call stack) para uma determinada requisição.
O que fazemos é derivar um novo
cancellingCtx
da nossa requisição que nos retorna uma função cancel
. Nós então programamos que a função seja chamada em 5 milissegundos usando time.AfterFunc
. Por fim, usamos este novo contexto em nossa requisição chamando request.WithContext
.O teste falha como seria de esperar.
--- FAIL: TestServer (0.00s)
--- FAIL: TestServer/avisa_a_store_para_cancelar_o_trabalho_se_a_requisicao_for_cancelada (0.00s)
context_test.go:62: store no foi avisada para cancelar
Lembre-se de ser disciplinado com o TDD. Escreva a quantidade mínima de código para fazer nosso teste passar.
func Server(store Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
store.Cancel()
fmt.Fprint(w, store.Fetch())
}
}
Isto faz com que este teste passe, mas não parece tão bom! Certamente não deveríamos estar cancelando a
Store
antes do fetch em cada requisição.Ao ser disciplinado ele destacou uma falha em nossos testes, isso é uma coisa boa!
Vamos precisar atualizar nosso teste de caminho feliz para verificar que ele não será cancelado.
t.Run("retorna dados da store", func(t *testing.T) {
store := SpyStore{response: data}
svr := Server(&store)
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
svr.ServeHTTP(response, request)
if response.Body.String() != data {
t.Errorf(`resultado "%s", esperado "%s"`, response.Body.String(), data)
}
if store.cancelled {
t.Error("não deveria ter cancelado a store")
}
})
Execute ambos os testes. O teste do caminho feliz deve agora estar falhando e somos forçados a fazer uma implementação mais sensata.
func Server(store Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
data := make(chan string, 1)
go func() {
data <- store.Fetch()
}()