O Dequeuer

Ou: o Message Broker da DroneMapp

Cléber Zavadniak
clebertech
10 min readSep 21, 2017

--

O nome “dequeuer” vem do fato de que a principal atribuição desse componente é pegar mensagens que estão no SQS (ou seja, a nossa “queue”) e fazer algo com elas.

Mensagens

Conforme explicado em artigo anterior, temos uma arquitetura baseada em eventos. Não é difícil entender por que é geralmente muito bom fazer as coisas assim, mas pode ser uma quebra de paradigma muito grande para desenvolvedores novatos que ainda tem aquele gosto por escrever bastante código (e energia para isso) e resolver as coisas com muita “verbosidade” e o mais imediatamente possível.

Antes de mais nada, recomendo dar uma olhada nesses slides bacanas que eu fiz quando ainda trabalhava em outra empresa:

http://slides.com/cleberz/eda-1#/

(Só ignore a parte do Crossbar. Não gosto mais dele.)

Nesses slides eu explico rapidamente algumas benesses de EDAs e o que são e como são implementados eventos.

Nossa é EDA “impura”, sim

Muita gente tem estudado e escrito sobre Arquiteturas Orientadas a Eventos e muitas tecnologias tem sido criadas, mas há um vão muito grande, ainda, eu acredito, entre o que os “acadêmicos” escrevem sobre o assunto e as necessidades reais do dia-a-dia da indústria. Você pode até ler um artigo lindo do Martin Fowler falando sobre quão maravilhosa é a possibilidade de fazer “replay de eventos” numa EDA (Event Driven Architecture) mas acabará descobrindo que, na prática, isso não somente é complicadérrimo como pode destruir o bom estado dos seus dados.

O mesmo se aplica para o armazenamento local de toda a informação que uma aplicação precisa (veja os slides). Em teoria, uma EDA “pura” não precisaria fazer requisições a outras aplicações para trabalhar com dados. E isso é outro conceito bem legal, mas impraticável na maioria das aplicações. Não somente isso adiciona complexidade e linhas de código em cada aplicação, como acaba multiplicando um problema (gerenciamento dos dados) para ter um ganho mínimo (sentir a “sensação de pureza” e ganhar um “completo desacoplamento” teórico que dificilmente será real e, mesmo sendo, dificilmente será, per si, um fato determinante para a reutilização da aplicação em outros sistemas).

Curiosamente, quem trabalha comigo já me ouviu falar sobre os problemas do excesso de flexibilidade. Os “teóricos” louvam fervorosamente ideias como “altíssimo desacoplamento”, por exemplo, e isso acaba gerando aquelas ferramentas que podem, sim, “fazer tudo”, mas acabam sendo tão complexas de configurar que, no fim das contas, ninguém usa, por mais excelentes que essas ferramentas possam ser. Guardadas as devidas proporções, vide a popularidade de projetos das mais diversas esferas, como Ubuntu versus Gentoo, Django versus Zope ou mesmo faca versus canivete.

Somos uma empresa de tecnologia, não “pesquisadores”, então não podemos perder muito tempo tentando criar super-ferramentas-genéricas que resolvam todos os problemas que ainda não temos tampouco escrever nosso código e planejar nossa arquitetura de maneira engessada e com visão somente de curto-prazo. Há de haver um equilíbrio saudável entre esses dois mundos e o peso ideal de cada metodologia destas na balança é coisa que, em geral, consegue-se somente por meio de muito estudo e, especialmente, de experiência.

E a minha experiência, em particular, me mostrou que seria melhor seguir certos caminhos mais práticos na arquitetura de software da DroneMapp e não contar com certos “milagres” falaciosos que os “trend-makers” apregoam por aí…

Ishikawa

O DroneMapp Engine começou com o Ishikawa, que é o projeto Django que implementa o que comumente chamamos de “backend” ou, pelo menos, o componente que armazena dados e os provê por meio de uma API REST.

Conforme citado em ainda outro artigo, começar a escrever o backend como uma plataforma monolítica foi uma decisão baseada na relação praticidade versus tempo. Seria muito complicado e trabalhoso já começar a trabalhar com micro-serviços e, além de perder as benesses de um banco de dados relacional e um framework “para perfeccionistas com prazo” já bastante “battle tested”, ganharíamos uma série de preocupações não relacionadas diretamente às nossas necessidades de negócio, como orquestração de várias aplicações, gerenciamento de N contratos (entre as apps), atualização de bibliotecas compartilhadas, etc.

Mas esse componente monolítico não impede que tenhamos uma EDA (Event Driven Architecture). Conforme citei no artigo sobre a divisão de responsabilidades de cada componente, nossa API implementa um conjunto limitado de responsabilidades e pouca ou nenhuma regra de negócio é implementada nessa camada. Regras de negócio são implementadas no próprio Dequeuer, não diretamente aqui.

Acesso a APIs

As aplicações tem, sim, algum conhecimento umas sobre as outras, especialmente sobre o Ishikawa. Não apregoamos o desacoplamento completo de cada uma nem nada semelhante. As aplicações não precisam armazenar seus próprios dados e isso é ok.

Assinaturas em tópicos

Esse é um assunto que eu vejo sendo tratado praticamente sempre de uma maneira padronizada (e geralmente errada) demais.

Na maioria das implementações de roteadores de eventos, existem dois conceitos que, sob um olhar mais cético, acabam mostrando-se mutuamente excludentes, que são (1) inscrição em tópicos e (2) permissões para inscrição em tópicos.

O problema começa quando percebe-se que uma mesma responsabilidade acaba sendo implementada em dois locais distintos por aplicações conceitualmente muito diferentes. Enquanto a “aplicação A ” inscreve-se no tópico “X”, é o roteador de mensagens/eventos quem permite ou não que A inscreva-se mesmo em X.

Ou seja: “A assina X” é um conhecimento duplicado, pois está presente tanto em A quanto no roteador de eventos. Ora, se o roteador já sabe que A vai querer inscrever-se em X, por que A ainda precisa pedir para inscrever-se? É óbvio que tão logo A conecte-se ao roteador (e autentique-se) ela já pode começar a receber mensagens do tópico X!

E isso mostra-se mais evidente quando percebe-se que a maioria das aplicações acaba assinando somente um tópico (quando trabalha-se com microsserviços, inclusive, é um “mau-cheiro” uma aplicação assinar muitos tópicos).

Por isso nós não trabalhamos com assinaturas por parte das aplicações. Já que sempre haverá uma entidade central que conhece previamente as necessidades de assinaturas de tópicos de cada aplicação, já configuramos o Dequeuer para que cada uma delas simplesmente receba as mensagens que precisa, sem necessitar de qualquer tipo de “pedido” por parte delas.

Então, reforçando: na arquitetura da DroneMapp, a configuração da distribuição dos eventos é implementada somente no Dequeuer.

Tópicos

No começo das eras usávamos o SNS como serviço de entrega de mensagens, o que nos impedia de usar ponto ( . ) nos nomes de tópicos de nossas mensagens. Como elas são compostas, geralmente, no formato <objeto><separador><ação-no-passado> , nosso separador acabou sendo o “dunder” ( __ , ou “dois underscores”).

Os “verbos” que usamos são:

  • created : objeto foi persistido.
  • updated : objeto foi alterado e persistido.
  • deleted : objeto foi excluído.
  • new : objeto passou a existir no sistema, mas ainda não foi persistido.

Os nomes de objetos, em geral, seguem a nomenclatura muito intuitiva usadas nas URLs do Ishikawa, só que no singular, como photo , mission , flight , etc.

Hierarquia da implementação

O Dequeuer herda uma boa parte de suas funcionalidades de algumas bibliotecas que publicamos como código livre no reino das powerlibs .

Rapidamente:

  • O dequeuer é filho da powerlibs.aws.sqs.dequeue_to_api .
  • A dequeue_to_api é filha da powerlibs.aws.sqs.dequeuer .

É importante entender isso, porque vários aspectos importantes dele são implementados nessas outras bibliotecas. A ideia, na verdade, é que muito pouco algoritmo seja implementado no próprio Dequeuer e ele seja uma espécie de “projeto-configuração”, sendo a ponte que liga algoritmos genéricos (implementados em outras libs) às nossas necessidades de negócio (implementadas via config.yaml ou via plugins).

Configuração

YAML?

Para mim é um alívio poder usar YAML como formato de configuração. Eu realmente acho essa combinação muito boa, já que:

  • YAML é pouco verboso, ao contrário de JSON que te obriga a encher os arquivos de { , } e " que não contribuem em nada para a legibilidade dos mesmos.
  • YAML é poderoso, ao contrário de arquivos .ini , que te obrigam a inventar certas soluções mirabolantes quando você descobre que precisa de algo um pouquinho mais complexo na hora de configurar certas coisas.
  • YAML dá suporte a comentários, ao contrário de JSON.
  • É mais fácil validar YAML do que ou escrever testes para um settings.py ou deixar sem testes e sempre correr o risco de inserir algum bug ao alterar alguma configuração.
  • Contanto que o arquivo seja sempre escrito por humanos, dificilmente ele usará aqueles formatos mirabolantes que o YAML suporta, deixando de ser legível.

Tarefas implementas de maneira puramente descritiva?

Sim! E isso é muito bom!

Lembre-se sempre: o desenvolvedor (você!) tem “a mão de Mirdas”: tudo que toca vira cocô. Sério. Quanto menos os desenvolvedores tocarem em código, menos problemas a plataforma terá. E esse é um daqueles motivos que ainda serão mencionados no artigo que ainda vou escrever sobre o porque de Python ser uma escolha melhor do que N linguagens para ser usada nas empresas em geral: Python é conciso e permite que escrevamos menos linhas de código, o que diminui a chance de implementarmos bugs.

O uso principal dessas tarefas implementadas de maneira puramente descritiva é o “to API” da biblioteca dequeue_to_api : dado que o Dequeuer receba uma mensagem, uma determinada requisição a uma API REST deverá ser efetuada.

Darei um exemplo, abaixo.

Actions

Actions são as configurações de tratamento de mensagens. Cada action descreve o que deve ser feito (a ação) quando uma mensagem de determinado tópico chegar ao Dequeuer.

Uma action que acessa a API do Ishikawa, por exemplo, segue o seguinte formato:

save_photo_attributes:
topic: "photo_attributes__new"
endpoint: "photos/{payload[photo_id]}"
method: "PATCH"
payload:
width: "{payload[width]}"
height: "{payload[height]}"
altitude: "{payload[altitude]}"
coordinates: "{payload[coordinates]}"

topic é o nome do tópico e pode receber expressões regulares caso você precise usá-las, incluindo grupos nomeados.

endpoint é o endpoint do Ishikawa para o qual você fará a requisição HTTP.

method é o método HTTP a ser usado.

payload só é usado caso o payload da mensagem não seja exatamente igual o que a API requer. Nesse caso, você poderá montar um payload adequado.

As variáveis às quais você tem acesso na hora de montar os valores da configuração, além de payload , que representa o payload da mensagem recebida, são

  • _topic , que representa o tópico da mensagem recebida;
  • _topic_groups , que representa os grupos da expressão regular, caso presente, que casou com o tópico;
  • _action , que é a configuração a action.

Plugins

O carregamento dinâmico de plugins é implementado na dequeue_to_api . O que nós provemos no Dequeuer são os plugins em si.

O conceito de plugins foi criado para que consigamos ter uma separação mínima entre uma solução que busca ser mais genérica, que é o cerne do Dequeuer, e algoritmos mais específicos, que precisam ter bastante conhecimento de regras de negócio. Dessa forma, requisições simples e genéricas, que funcionam bem se forem implementadas de maneira puramente descritiva são colocadas em funcionamento meramente editando-se o config.yaml . Já soluções que requerem mais complexidade podem ser implementadas em plugins que não precisam ter vergonha alguma de carregar consigo muitos detalhes e conhecimento de determinadas regras de negócio.

Os plugins do Dequeuer são armazenados no diretório plugins/ e devem seguir o layout plugins/<nome_do_plugin>/plugin.py , sendo plugin.py o arquivo principal que será carregado dinamicamente e que deve conter uma classe chamada Plugin , que herda de dequeuer.plugins.BasePlugin .

Seu plugin não é obrigado, na verdade, a herdar de BasePlugin . Esta é uma classe de conveniência, apenas. Mas lembre-se que os plugins recebem a instância do Dequeuer como (único) parâmetro de inicialização.

Onde e quando os plugins são chamados?

Lá no config.yaml você verá entradas assim:

update_mission_dates:
topic: "flight__(created|updated)"
custom_handlers:
- "missions.flight_updated"

O que está sendo dito é o seguinte:

1- Quando chegar no Dequeuer uma mensagem cujo tópico seja flight__created ou flight__updated ;

2- Da instância da classe plugins.missions.plugin.Plugin que foi devidamente carregada no Dequeuer (e representa o plugin missions );

3- Chame o método flight_updated .

Os parâmetros de chamada do método são action , topic e payload .

action são dados da Ação ( update_mission_dates ), que nada mais são que uma cópia da configuração que você viu no config.yaml ( dict ).

topic é o tópico da mensagem que acabou de chegar, como flight__updated ( str ).

payload é o conteúdo da mensagem em si ( dict ).

Com isso em mãos, o método flight_updated(self, action, topic, payload) é livre para tratar da mensagem como bem entender, seja acessando dados do Ishikawa, escrevendo em um arquivo de log, enviando um e-mail ou o que mais for.

Métodos de conveniência

Ao herdar da classe BasePlugin você ganha o método get_url , que “prependa” a URL base da configuração do Dequeuer na sua URL. E também ganha os métodos get , post , put , patch e delete , que já estão preparados para acessar a API do Ishikawa usando os headers corretos.

Enviando mensagens para o Dequeuer

Existe uma porção de situações em que uma mensagem recebida acabará gerando N ≥ 1 novas mensagens. Lembre-se que o envio de mensagens é o que provê resiliência à plataforma, então todo tratamento de 1 mensagem deve ser atômico: não faça N tarefas baseadas em 1 mensagem caso o não-cumprimento de uma delas prejudique a consistência do sistema. Se você precisa de uma porção de ações, terá que “encadeá-las” por meio de novas mensagens.

Isso acontece quando enviamos uma foto para o sistema. Há um componente que ouve o tópico photo__created , gera um thumbnail da imagem, sobe para o S3 e envia uma nova mensagem, photo_thumbnail__new , contendo o caminho para o thumbnail e o ID da “foto mãe”. Posteriormente, outro componente trata das mensagens nesse tópico ( photo_thumbnail__new ) e faz o devido PATCH no Ishikawa para salvar o endereço do thumbnail na foto relacionada.

O Dequeuer tem um método notify(topic, payload) para que você possa “realimentá-lo”. De “dentro” de seu plugin você simplesmente chama self.dequeuer.notify(topic, payload) .

WebSocket

Esse assunto fica para o próximo artigo…

E como enviar mensagens para o Dequeuer?

O conceito em si deve ser, na verdade, “como enviar mensagens para a plataforma em si”. O fato de que é o Dequeuer quem as trata é um detalhe de implementação.

Há várias maneiras de se enviar mensagens para a plataforma. O Ishikawa o faz usando a powerlibs-django-contrib-notifiers e a powerlibs-django-contrib-eventful , e faz o envio para o SQS por meio de implementação interna. Já o Saito (que cuida do processamento de imagens) usa a powerlibs-aws-process_status_notifier . Dê uma olhada no código desta última. Não é muito difícil, não.

Resumo

Para entender o Dequeuer da DroneMapp você precisa:

  • Entender como funciona e quais os benefícios de uma Arquitetura Orientada a Eventos.
  • Conhecer algumas bibliotecas das powerlibs.
  • Ler e entender o config.yaml .
  • Ler e entender alguns plugins que já implementamos.

--

--