Como automatizamos a avaliação de projetos com Github Actions e o Broadway do Elixir
Tags: elixir, backend, github, phoenix
Published: 2022-09-08 19:17:54

## Introdução
Aqui na Trybe, ao final de cada seção de conteúdos, as pessoas estudantes precisam desenvolver um projeto para avaliarmos seu aprendizado. O repositório do projeto é disponibilizado no Github e elas fazem clone para iniciar o desenvolvimento em suas máquinas. A cada commit da pessoa no repositório nós disparamos a execução de uma avaliação através das [Github Actions](https://github.com/features/actions).

## Processamento de uma delivery (commit)
Após abrir um pull request com o código desenvolvido, cada commit dispara uma github action que irá avaliar o código da pessoa estudante e guardará no nosso banco de dados informações sobre a avaliação do código dessa pessoa.

Os commits são enfileirados e assim conseguimos controlar o status do seu processamento, para garantir que todos os commits tenham suas avaliações devidamente calculadas. Em caso de falhas, reprocessamos automaticamente a cada 5 minutos.


![Fluxo de processamento da delivery](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3c11n6vhe633al02ub3v.png)

O processamento do commit no backend foi implementado em cima do conceito de processamento de dados em multi-etapas com a biblioteca [Broadway](https://github.com/dashbitco/broadway). Consistindo em um módulo produtor que é responsável por buscar no banco todas as deliveries que estão aguardando processamento para repassar ao consumidor. Já o consumidor é o módulo Broadway que é responsável por demandar as avaliações ao produtor, processar a nota e criar o comentário de feedback no Github.

Como temos uma grande quantidade de commits sendo feitos nos projetos, a primeira coisa que tentamos fazer é utilizar os recursos da programação concorrente disponível na linguagem Elixir  para processar várias deliveries ao mesmo tempo, com o objetivo de evitar um atraso no feedback para as pessoas estudantes. 

No código abaixo, observe nas configurações do Broadway que na linha 10 foi definido qual é o módulo produtor e nas linhas 16 e 17 foi definido a quantidade de processos concorrentes e a quantidade de demanda que cada processo irá solicitar ao produtor.

```elixir
defmodule Trybe.Consumer do
  use Broadway

  alias Broadway.Message

  def start_link(_opts) do
    Broadway.start_link(__MODULE__,
      name: __MODULE__,
      producer: [
        module: Trybe.Producer,
        transformer: {__MODULE__, :transform, []},
        concurrency: 1
      ],
      processors: [
        default: [
          concurrency: 10,
          max_demand: 10
        ]
      ]
    )
  end

  def handle_message(_processor, message, _context),
    do: Message.update_data(message, &process_data/1)

  def transform(event, _opts),
    do: %Message{data: event, acknowledger: {__MODULE__, :ack_id, :ack_data}}

  def ack(:ack_id, _successful, _failed), do: :ok

  defp process_data(delivery) do
    # => 1- Calcula a nota e salva no banco
    # => 2- Faz request no Github criando o comentário de feedback.
    # => 3- Atualiza o status da avaliação para `finished`.
  end
end
```

Já no produtor, com base na demanda definida nas configurações do Broadway, seu papel é buscar no banco de dados as deliveries que estão aguardando o processamento e repassar para o consumidor. Caso o produtor não encontre mais deliveries, é feito um agendamento de um novo processo para buscar novas deliveries nos próximos 10 segundos.

```elixir
defmodule Trybe.Producer do
  use GenStage

  alias Trybe.Deliveries

  def start_link(initial \\ []) do
    GenStage.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def init(deliveries) do
    {:producer, deliveries}
  end
	
  # Função que irá enviar as deliveries para o consumidor
  def handle_demand(demand, state) when demand > 0 do
    send_deliveries(demand, state)
  end

  # Função que é chamada quando um processo é agendado 
  # solicitando mais deliveries.
  def handle_info({:get_deliveries, demand}, state) do 
    send_deliveries(demand, state)
  end

  defp send_deliveries(demand, state) do
    deliveries = list_deliveries(demand)
    maybe_schedule_next_deliveries(deliveries, demand)

    {:noreply, deliveries, state}
  end
	
  # Função responsável por buscar novas deliveries 
  # nos próximos 10 segundos caso a quantidade de deliveries 
  # no estado seja menor do que o solicitado.
  defp maybe_schedule_next_deliveries(deliveries, demand) when length(deliveries) == demand,
    do: nil
	
  defp maybe_schedule_next_deliveries(_deliveries, demand) do
    Process.send_after(self(), {:get_deliveries, demand}, :timer.seconds(10))
  end

  # Função responsável por buscar as deliveries no banco 
  # e mudar o status delas para `processing`.
  defp list_deliveries(demand) do
    with deliveries when deliveries != [] <- Deliveries.list_deliveries_waiting_process(demand),
         {_, deliveries} <- Deliveries.set_processing_status_to_deliveries(deliveries) do
      deliveries
    end
  end
end
```

Logo abaixo um exemplo de como o Broadway se comporta com as configurações definidas.


![Fluxo de processamento da nota no backend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k0gbgrkcq3njhi3d3pmc.png)

## Problemas ao fazer requests simultâneas na API do Github
No primeiro momento tudo parecia perfeito, mas infelizmente não funcionou da maneira esperada por causa do [rate limit do Github](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting), que acabou gerando um grande atraso na entrega dos feedbacks para as pessoas estudantes.

Na API do Github existem dois tipos de rate limit para garantir a disponibilidade da API para todos. Aqui no post iremos focar apenas no [secondary rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits) que é nosso principal vilão, mas saiba que também existe o primary rate limit.

Na documentação do Github existem algumas [práticas recomendadas](https://docs.github.com/en/rest/guides/best-practices-for-integrators) para evitar o rate limit secundário, e os nossos maiores vilões são:

- Não faça solicitações simultâneas para um mesmo token.
- Solicitações que criam conteúdo que acionam notificações, tais como issues, comentários e pull requests, podem ser ainda mais limitadas.Crie este conteúdo em um ritmo razoável para evitar maiores limitações.

## Requisições moderadas ao Github

Sorte a nossa que o Broadway já estava preparado para esse tipo de problema e implementou uma feature que nos possibilita botar o pé no freio do nosso produtor. A configuração é a `rate_limiting`, com ela é possível definir a quantidade de deliveries que podemos processar em um intervalo de tempo. 

```elixir
defmodule Trybe.Consumer do
  use Broadway

  ......
  ......

  def start_link(_opts) do
    Broadway.start_link(__MODULE__,
      name: __MODULE__,
      producer: [
        module: Trybe.Producer,
        transformer: {__MODULE__, :transform, []},
        concurrency: 1,
        rate_limiting: [
          allowed_messages: 10,
          interval: 30_000
        ]
      ],
      processors: [
        default: [
          concurrency: 1,
          max_demand: 10
        ]
      ]
    )
  end
end
```

Mas só essa configuração ainda não foi o suficiente, ela apenas desacelera a quantidade de requisições que fazemos no serviço em um intervalo de tempo, mas o problema de requests simultâneas ainda permanece. Dito isso, tivemos que também mudar as configurações dos processors, definindo o campo `concurrency: 1` para que exista apenas um único processo recebendo deliveries do produtor evitando requisições concorrentes.


![Fluxo de processamento da nota evitando rate limit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s5v6j5gen7yh66zz9j83.png)

Dessa forma, conseguimos fazer requisições moderadas e sequenciais no serviço do Github sem comprometer o tempo de retorno do feedback para as pessoas estudantes.

## Conclusão
A solução implementada nos possibilitou controlar o secondary rate limit e assim conseguimos processar os feedbacks para as pessoas estudantes de forma eficiente, ou seja, com poucas linhas de código podemos reajustar as configurações do Broadway novamente para que o processamento de deliveries  possa ser feita de maneira concorrente em grande escala.

Se você tiver curiosidade de saber como é a infraestrutura que permite a Trybe corrigir mais de 20mil avaliações por semana,  recomendo também a leitura do [post publicado pelo Biasi](https://dev.to/trybe/como-a-trybe-corrige-mais-de-20-mil-avaliacoes-por-semana-1i6f).