DEV Community

Renan for Trybe

Posted on

Como automatizamos a avaliação de projetos com Github Actions e o Broadway do Elixir

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.

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

O processamento do commit no backend foi implementado em cima do conceito de processamento de dados em multi-etapas com a biblioteca 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.

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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

Fluxo de processamento da nota no backend

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, 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 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 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.

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
Enter fullscreen mode Exit fullscreen mode

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

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.

Top comments (0)