Loafer é uma biblioteca open-source feita em python, para facilitar a criação de serviços, em especial aqueles que consomem ou geram eventos (mensagens).
O projeto nasceu como prova de conceito para Olist e hoje é o alicerce de dezenas de micro-serviços em produção, facilitando a manutenção e criação de novos serviços.
Para compreender um pouco melhor sobre nossa arquitetura, o cenário e onde os micro-serviços se encaixam, recomendo a leitura do artigo escrito pelo osantana sobre o funcionamento da plataforma da Olist.
O loafer também serviu para um grande aprendizado, principalmente sobre asyncio e programação assíncrona de modo geral — embora serviços também possam ser escritos com código síncrono, não recomendamos essa prática com o loafer (nesse caso, utiliza-se threads para execução do serviço).
Não é preciso ter conhecimentos profundos sobre asyncio, python ou da AWS para utilizar o loafer, mas também não atrapalha se tiver 😊
Todos os trechos de código estão no github.
Cenário
Uma visão simplificada da arquitetura que vamos precisar:
- Tópico SNS : notificador de eventos, com zero ou mais filas SQS inscritas
- Fila SQS : recebe as mensagens do tópico SNS, possui uma dead-letter-queue que retêm as mensagens que não puderam ser processadas na fila principal (maiores detalhes posteriormente)
- Um serviço que consome mensagens da fila SQS e realiza uma determinada ação (ou gera um novo evento/mensagem). O serviço é dono da fila, ela nunca é compartilhada.
Ambiente de desenvolvimento
Embora seja opcional, é sempre interessante criar um virtualenv
e considere o uso de qualquer versão acima de Python 3.5.2 daqui em diante.
Pra refletir com mais fidelidade o funcionamento de uma aplicação, vamos utilizar um clone local da AWS, com os elementos arquiteturais descritos anteriormente usando goaws.
$ docker pull pafortin/goaws
$ docker run -d --name goaws -p 4100:4100 pafortin/goaws
# caso queira apenas parar ou iniciar o processo: # docker start goaws # docker stop goaws # -> resets causam perda de dados/configurações
Para interagir e configurar o “nosso” AWS, vamos utilizar o aws-cli.
pip install awscli
Se for a primeira vez que tenha instalado o awscli, você pode precisar configurar suas credenciais, elas não precisam necessariamente ser válidas para conseguir manipular o goaws.
Vamos criar um tópico SNS “friend-created”:
$ aws --endpoint-url <http://localhost:4100> sns create-topic \ --name friend-created { "TopicArn": "arn:aws:sns:local:000000000000:friend-created" }
E uma fila SQS chamada “foobar-friend-created”:
$ aws --endpoint-url <http://localhost:4100> sqs create-queue \ --queue-name foobar-friend-created { "QueueUrl": "<http://localhost:4100/queue/foobar-friend-created>" }
Para simplificar um pouco, não vamos configurar uma dead-letter-queue. O mecanismo faz bastante sentido em um ambiente de produção, mas para testes locais, sempre podemos reenviar uma mensagem manualmente quando necessário.
Finalmente, vamos inscrever a fila “foobar-friend-created” no tópico “friend- created”:
$ aws --endpoint-url <http://localhost:4100> sns subscribe \ --topic-arn arn:aws:sns:local:000000000000:friend-created \ --protocol sqs --notification-endpoint <http://localhost:4100/queue/foobar-friend-created> { "SubscriptionArn": "arn:aws:sns:local:000000000000:friend-created:70b25381-80e4-4cce-bf44-65c505ec8d4b" }
Montar esse ambiente não é exatamente um requisito obrigatório, mas creio que dê mais credibilidade e segurança durante o desenvolvimento de um serviço. Caso tenha disponibilidade, nada impede o uso de uma conta da AWS diretamente ou injetar manualmente mensagens no serviço sem utilizar qualquer infra- estrutura.
Para concluir nosso ambiente, vamos organizar nosso serviço dentro de um diretório “foobar_friend_service” (módulo) e instalar o loafer:
$ mkdir -p foobar_friend_service $ touch foobar_friend_service/__init__.py $ pip install loafer
🚀
Loafer
Os componentes principais do loafer são:
- handler : processa a mensagem
- message-translator : faz adequação da mensagem para que um handler faça o processamento
- provider : busca/recebe mensagens de uma determinada fonte (no caso, uma fila SQS)
- route : combinação de um handler , message-translator e provider
- manager : gerencia uma ou mais routes
Pode parecer que há muitos elementos para gerenciar, mas na prática é bem trivial: o “ handler ” é o código que será implementado, o restante é praticamente configuração e boilerplate.
Vamos exemplificar a implementação de um serviço simples, iremos processar uma mensagem (json) que possui as informações abaixo:
- id : identificador do recurso
- username : nome de um usuário
- github_url : url do usuário no github
{ "id": "test-1", "username": "georgeyk", "github_url": "" }
Caso um usuário não tenha configurado o “github_url”, nosso serviço deve buscar essa informação e adicionar no registro. A implementação deve ficar parecida com (“foobar_friend_service/handlers.py”):
Tente ignorar todas as possíveis melhorias no código, o método importante da classe é async def handle(self, message, *args)
. Todo handler precisa ter uma assinatura semelhante; o *args
contêm alguns metadados e eles podem variar bastante, inclusive vir vazio, então se houver alternativa, não confie nas informações que vierem ali (mas podem ser úteis para auditoria).
A message
é um dicionário python comum e reflete o json
enviado ao evento. Um handler obrigatoriamente precisa retornar um boolean ou algo que possa ser avaliado com bool()
. O retorno é utilizado para confirmar o processamento da mensagem:
True
: mensagem processada com sucesso, ela será confirmada e removida da fila SQS
False
: mensagem não processada com sucesso, ela ficará na fila SQS ou será movida para a dead-letter-queue
Qualquer desvio do fluxo será tratado do mesmo modo como se houvesse um return False
, nunca removemos nada da fila SQS de forma implícita. Cuidado com retornos dúbios para não remover uma mensagem acidentalmente.
Agora precisamos definir nossas rotas (“foobar_friend_service/routes.py”):
Perceba que nossa rota define uma instância de FoobarFriendCreatedHandler
como handler , alternativamente poderíamos passar uma função/ coroutine também; usando classes convencionamos que o entrypoint é sempre o método handle()
.
Outro detalhe sobre a configuração das rotas é que o SNSQueueRoute
automaticamente define um message-translator apropriado para mensagens que fazem o caminho SNS ▶️ SQS. A mensagem vem “envelopada” de diferentes modos dependendo de quem a colocou na fila.
O provider_options
só é necessário porque precisamos apontar para nosso ambiente local, removendo essa customização, a biblioteca boto automaticamente apontará para os endpoints corretos da amazon.
Por fim, criamos o ponto de partida para o nosso serviço (“foobar_friend_service/run.py”):
Se os deuses dos tutorais permitirem 🙏, podemos executar nosso serviço:
$ python -m foobar_friend_service.run iniciando serviço ...
Ué ~ nada acontece feijoada ~ ?
Para o nosso serviço trabalhar de verdade, precisamos criar um evento no SNS:
$ aws --endpoint-url <http://localhost:4100> sns publish \ --topic-arn arn:aws:sns:local:000000000000:friend-created \ --message file://test_user.json { "MessageId": "e2d086a8-7cbc-473b-b50e-2923976db4d4" }
Nosso serviço deve ter impresso o response de uma requisição bem sucedida:
{'args': {}, 'data': '{"id": "test-1", "github_url": "<https://github.com/georgeyk>"}', 'files': {}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'close', 'Content-Length': '61', 'Content-Type': 'application/json', 'Host': 'httpbin.org', 'User-Agent': 'Python/3.5 aiohttp/2.2.5'}, 'json': {'github_url': 'https://github.com/georgeyk', 'id': 'test-1'}, 'origin': '...', 'url': 'http://httpbin.org/patch'}
Para finalizar, use Control-C
ou mate o processo, o padrão é que o serviço fique “ouvindo” a fila indefinidamente.
Uma dica final sobre a implementação, lembre sempre que é apenas código python. Parece óbvio, mas é para reforçar que tentamos não impor ou restringir o modo como um serviço é desenvolvido, o que temos aqui é apenas um exemplo, nada além disso.
Se você não fez o setup do goaws por exemplo, uma validação local seria parecida com:
Pronto! Terminamos nosso serviço 👊
Lembrando novamente que os códigos de exemplo estão no github , caso queira aprofundar um pouco mais, também temos uma documentação inicial.
Erros e monitoramento
Nosso serviço é bem simples e praticamente não possui regras de negócio sofisticadas, mas é bem próximo de um serviço real e em produção.
A ausência de tratamento de erros por exemplo, é intencional. No contexto do nosso serviço e arquitetura, é importante notar que a infra-estrutura e serviços de monitoramento são partes integrantes da aplicação.
Sempre que um erro ( exception) não tratado interromper o fluxo de execução do serviço, temos o seguinte comportamento:
- o handler não irá confirmar o processamento da mensagem (isto é, o
return True
não será alcançado fora do fluxo esperado) - toda fila SQS possui um “visibility timeout” (por padrão, 30 segundos) e após o tempo expirar, a mensagem voltará a ficar disponível ao serviço (ou seja, um mecanismo de retry )
- as retentivas irão ocorrer até a mensagem ser confirmada pelo handler ou então, após atingir um “maximum receives” a mensagem será encaminhada para uma dead-letter-queue
Vamos levantar algumas situações fora do fluxo esperado de execução e seu reflexo na confirmação da mensagem:
- indisponibilidade de serviços : caso alguma API pare de responder corretamente, a mensagem não deve ser confirmada (HTTP 5xx, timeout, etc)
- informações inválidas na mensagem : caso alguma “coisa” alimente a fila SQS com mensagens contendo valores inválidos, o serviço dispara uma requisição inválida e a mensagem não será confirmada (HTTP 4xx)
- chaves inválidas na mensagem: mensagem não confirmada (via
KeyError
por exemplo) - mais de uma instância do serviço processando a mesma mensagem : se uma das instâncias confirmar a mensagem, a mensagem sairá da fila SQS independente da confirmação das outras instâncias; do contrário, a mensagem ficará no ciclo de retentiva como descrito anteriormente
O último cenário levantado é o único que não é um erro, mas uma característica do SQS. Recomenda-se que todo serviço seja idempotente, mas existem situações em que essa característica pode ser desconsiderada (ou não implementada), pois não existe um efeito colateral grave.
A responsabilidade de implementar algum mecanismo de idempotência geralmente é delegada para o componente que possuir maior conhecimento das regras de negócio. Depende bastante do domínio e da arquitetura, usualmente o serviço não faz isso, apenas “trabalha junto” com as regras definidas externamente.
Nos demais cenários, caso um serviço de monitoramento esteja configurado (como o sentry), ele também deve capturar esses “erros” de execução. Existem alguns casos em que não precisamos desse report , uma vez que o comportamento de retentiva é esperado (erros HTTP 5xx por exemplo). Para tal, basta tratar essa situação e com um return False
no handler, indicamos que não queremos confirmar a mensagem (deixo como exercício).
E as outras situações que o serviço de monitoramento capturar?
É bug (ou feature?)! 😄 🐛
Cenas do próximo capítulo…
A introdução ficou um pouco maior que o planejado, mas acredito que dê subsídio suficiente para quem quiser experimentar a criar os primeiros serviços utilizando o loafer e compreender os motivos que levaram a criação da biblioteca.
Como dito anteriormente, o loafer é um projeto de código aberto (contribuições são bem vindas 😃) e que ainda tem bastante para amadurecer. Acredito que seja um passo adiante para facilitar a criação de serviços em uma arquitetura orientada à eventos, embora ainda tenha restrições e vários pontos de melhoria.
Futuramente, vamos abordar casos de uso mais complexos, comparar com outras soluções e discutir vantagens e desvantagens da biblioteca. Maiores informações também podem ser obtidas na documentação do projeto.
Caso tenha se interessado ou tenha dúvidas e sugestões, não deixe de enviar um feedback.
✌️
Originalmente publicado no medium.