Técnicas de Concorrência e Gerenciamento de Estado em Elixir com FSM

zoedsoupe

Zoey de Souza Pessanha

Posted on June 21, 2024

Técnicas de Concorrência e Gerenciamento de Estado em Elixir com FSM

Introdução

Elixir é uma linguagem funcional que roda na máquina virtual BEAM, a mesma do Erlang, famosa por suas capacidades de concorrência e tolerância a falhas. Um dos padrões poderosos que podem ser implementados em Elixir é a Máquina de Estado Finito (FSM - Finite State Machine), combinada com processamento assíncrono. Este artigo explora como esses padrões podem beneficiar programas em Elixir, proporcionando controle sobre o fluxo de eventos e melhorando a eficiência e a robustez dos sistemas.

Máquinas de Estado Finito (FSM)

Uma Máquina de Estado Finito é um modelo computacional que consiste em um número finito de estados, transições entre esses estados, e ações, que podem ser acionadas por eventos. Em um sistema FSM, um objeto pode estar em apenas um estado de cada vez, e ele pode mudar de estado em resposta a um evento. Cada transição pode ser associada a uma ação específica.

Benefícios da FSM

  1. Organização e Clareza: FSM ajuda a organizar e clarear o comportamento de sistemas complexos, tornando o fluxo de eventos mais previsível e fácil de entender.

  2. Controle de Fluxo: Com FSM, o controle do fluxo de estados é explícito e definido claramente, permitindo transições apenas quando condições específicas são atendidas.

  3. Facilidade de Manutenção: FSM permite encapsular a lógica de estado em um único lugar, facilitando a manutenção e atualização do sistema sem introduzir bugs.

Exemplo de FSM

Considere um sistema simples de autorização de pagamento com os seguintes estados: pending, authorized, denied e failed.

  1. Estado Inicial: O pagamento começa no estado pending.

  2. Transição para authorized: Quando uma solicitação de autorização é aprovada, o estado muda para authorized.

  3. Transição para denied: Se a solicitação for negada, o estado muda para denied.

  4. Transição para failed: Se houver um erro no processamento, o estado muda para failed.

Um fluxo simplificado de FSM poderia ser representado assim:

defmodule PaymentFSM do
  @moduledoc """
  Define a FSM para estados de pagamento.
  """

  use Fsmx.Fsm,
    transitions: %{
      "pending" => ["authorized", "denied", "failed"],
      "authorized" => [],
      "denied" => [],
      "failed" => []
    }
end
Enter fullscreen mode Exit fullscreen mode

Aqui usamos a biblioteca Fsmx, mas é possível usar funções simples ou mesmo o ecto para definir uma FSM.

Neste exemplo, um pagamento pode mudar de pending para authorized, denied, ou failed. Uma vez que está em authorized, denied, ou failed, ele não pode transitar para outros estados.

Processamento Assíncrono

O processamento assíncrono permite que operações sejam executadas em paralelo ou de maneira não bloqueante, aumentando a eficiência e capacidade de resposta do sistema. Em Elixir, isso é conseguido utilizando processos leves gerenciados pela máquina virtual BEAM.

Benefícios do Processamento Assíncrono

  1. Escalabilidade: O processamento assíncrono permite que múltiplas tarefas sejam executadas simultaneamente, aumentando a escalabilidade do sistema.

  2. Responsividade: Sistemas assíncronos podem continuar a responder a novas requisições enquanto outras operações estão em andamento.

  3. Tolerância a Falhas: A arquitetura de supervisão de Elixir permite que processos falhem e sejam reiniciados sem afetar o restante do sistema.

Exemplo de Processamento Assíncrono

Imagine um sistema que processa eventos de pagamento. Cada evento pode desencadear várias operações, como verificação de saldo, registro de transações, etc. Para não bloquear o processamento de novos eventos, podemos usar tarefas assíncronas.

defmodule PaymentProcessor do
  @moduledoc """
  Processa eventos de pagamento de forma assíncrona.
  """

  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def process_event(event) do
    GenServer.cast(__MODULE__, {:process, event})
  end

  @impl true
  def init(:ok) do
    {:ok, []}
  end

  @impl true
  def handle_cast({:process, event}, deadletter) do
    case handle_event(event) do
      :ok -> {:noreply, deadletter}
      {:error, reason} -> {:noreply, [event | deadletter]}
    end
  end

  # dependendo da complexidade, podem existir
  # diferentes módulos de handler para eventos
  defp handle_event(event) do
    # Lógica para processar o evento de pagamento
    # Efeitos colaterais
    # Observabilidade
    IO.puts("Processando evento: #{inspect(event)}")
  end
end
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, cada evento é processado em uma nova tarefa, permitindo que o sistema continue a receber e enfileirar novos eventos sem esperar que os eventos anteriores sejam processados.

Lock Otimista

O Lock Otimista é uma técnica utilizada para lidar com concorrência em sistemas distribuídos. Ao contrário do Lock Pessimista, que bloqueia os recursos até que uma transação seja concluída, o Lock Otimista assume que as colisões são raras e permite que múltiplas transações ocorram simultaneamente, verificando conflitos apenas no momento da confirmação.

Benefícios do Lock Otimista

  1. Melhor Desempenho: Reduz a quantidade de bloqueios, permitindo maior paralelismo.
  2. Menos Deadlocks: Minimiza a chance de deadlocks, uma vez que os recursos não são bloqueados por longos períodos.
  3. Maior Escalabilidade: Facilita a escalabilidade horizontal, essencial para sistemas distribuídos de grande porte.

Exemplo de Lock Otimista

Vamos adicionar a lógica de lock otimista em nosso sistema de pagamentos. A ideia é verificar e atualizar o estado do pagamento apenas se ele não foi modificado por outro processo.

defmodule Payments do
  alias PaymentSystem.Repo
  alias PaymentSystem.Payments.Payment

  def update_payment_with_lock(id, attrs) do
    Repo.transaction(fn ->
      payment = Repo.get!(Payment, id)

      updated_payment = Payment.changeset(payment, attrs)
      case Repo.update(updated_payment) do
        {:ok, _payment} -> :ok
        {:error, changeset} -> Repo.rollback(changeset)
      end
    end)
  end
end
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, usamos uma transação para garantir que o pagamento seja atualizado apenas se ele não tiver sido modificado por outro processo desde a última leitura.

Integração de FSM, Lock Otimista e Processamento Assíncrono

Combinar FSM, Lock Otimista e Processamento Assíncrono em Elixir pode levar a sistemas altamente eficientes e robustos. A FSM controla o fluxo de estados, garantindo que as transições sejam ordenadas e previsíveis. O Lock Otimista permite que transações concorrentes sejam gerenciadas eficientemente, enquanto o Processamento Assíncrono garante que operações de longa duração não bloqueiem o sistema.

Exemplo Completo

Vamos combinar os conceitos discutidos anteriormente em um exemplo completo:

defmodule PaymentFSM do
  use Fsmx.Fsm,
    transitions: %{
      "pending" => ["authorized", "denied", "failed"],
      "authorized" => [],
      "denied" => [],
      "failed" => []
    }

  def start_payment(payment) do
    process_event(payment, "start")
  end

  defp process_event(payment, event) do
    PaymentProcessor.process_event(event)
  end

  defp handle_event(payment, event) do
    case transition(payment, event) do
      {:ok, new_payment} ->
        IO.puts("Evento processado com sucesso: #{inspect(new_payment)}")
      {:error, reason} ->
        IO.puts("Erro ao processar evento: #{inspect(reason)}")
    end
  end

  defp transition(payment, "start") do
    Fsmx.transition(payment, "authorized")
  end
end

defmodule PaymentProcessor do
  alias PaymentSystem.{Payments, PaymentFSM}

  def process_payment_async(payment) do
    Task.start(fn -> process_payment(payment) end)
  end

  defp process_payment(payment) do
    with {:ok, payment} <- PaymentFSM.authorize_payment(payment),
         {:ok, payment} <- Payments.update_payment_with_lock(payment.id, %{status: "authorized"}) do
      IO.puts("Pagamento processado com sucesso: #{inspect(payment)}")
    else
      {:error, reason} ->
        PaymentFSM.fail_payment(payment)
        IO.puts("Falha ao processar pagamento: #{inspect(payment)}. Motivo: #{reason}")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, a FSM gerencia o estado do pagamento, o Lock Otimista garante que as atualizações de estado ocorram de maneira concorrente segura, e o processamento de eventos é feito de forma assíncrona, garantindo que o sistema permaneça responsivo e eficiente.

Conclusão

O uso de Máquinas de Estado Finito (FSM) e processamento assíncrono interno pode trazer inúmeros benefícios para programas em Elixir, desde melhor organização e controle de fluxo até maior resiliência e escalabilidade. Ao combinar esses padrões, é possível criar sistemas robustos, capazes de lidar com complexidade e alta carga de forma eficaz.

💖 💪 🙅 🚩
zoedsoupe
Zoey de Souza Pessanha

Posted on June 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related