Técnicas de Concorrência e Gerenciamento de Estado em Elixir com FSM
Tags: architecture, elixir, programming
Published: 2024-06-21 19:09:00

## 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:

```elixir
defmodule PaymentFSM do
  @moduledoc """
  Define a FSM para estados de pagamento.
  """
  
  use Fsmx.Fsm,
    transitions: %{
      "pending" => ["authorized", "denied", "failed"],
      "authorized" => [],
      "denied" => [],
      "failed" => []
    }
end
```

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

```elixir
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
```

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.

```elixir
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
```

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:

```elixir
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
```

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.