3 pontos por GN⁺ 2024-07-05 | 1 comentários | Compartilhar no WhatsApp
  • Na Firezone, o Rust é usado para criar acesso remoto seguro e escalável em celulares Android, computadores macOS e servidores Linux
  • Uma biblioteca de conexão chamada connlib é usada para gerenciar conexões de rede e túneis WireGuard
  • Após várias iterações, chegou-se a um design chamado sans-IO, que oferece testes rápidos e minuciosos, personalização profunda e alta confiabilidade

connlib foi escrita em Rust e segue o design sans-IO

  • Graças à velocidade e à segurança de memória do Rust, ele é adequado para construir serviços de rede
  • Usa o runtime tokio, WebSockets com tungstenite, a implementação WireGuard boringtun e criptografia do tráfego de API com rustls
  • O design sans-IO implementa protocolos como máquinas de estado puras, em vez de enviar e receber bytes por sockets em vários pontos

O modelo assíncrono do Rust e a discussão sobre "colorir funções"

  • Funções assíncronas só podem ser chamadas por outras funções assíncronas
  • Se uma função no fundo da cadeia for assíncrona, todas as funções que a chamam também precisam se tornar assíncronas
  • Isso pode ser um problema para quem quer escrever código indiferente ao fato de as dependências serem assíncronas ou não

Introdução ao sans-IO

  • A ideia central do sans-IO é semelhante ao princípio da inversão de dependência no mundo OOP
  • A política (o que fazer) não deve depender dos detalhes de implementação (como fazer)
  • Em vez de enviar dados usando a struct Transmit, emite-se um Transmit

Aplicando a inversão de dependência

  • Em vez de enviar dados usando a struct Transmit, emite-se um Transmit
  • O loop de eventos implementa os efeitos colaterais e de fato chama UdpSocket::send

Máquina de estado

  • O diagrama da máquina de estado de uma solicitação de binding STUN tem dois estados: Sent e Received
  • A máquina de estado é implementada definindo a struct StunBinding e funções relacionadas

Loop de eventos

  • O loop de eventos conduz a máquina de estado e processa os dados usando poll_transmit e handle_input

Abstração de tempo

  • Requisitos baseados em tempo são tratados usando as APIs poll_timeout e handle_timeout

Premissas do sans-IO

  • O design sans-IO deixa para a aplicação a decisão sobre as dependências serem assíncronas ou não
  • O design sans-IO é fácil de compor, oferece APIs flexíveis, facilita testes e combina bem com os recursos do Rust

Composição fácil

  • A API de StunBinding pode ser aplicada à maioria dos protocolos de rede
  • A biblioteca snownet da Firezone combina ICE e WireGuard para oferecer um túnel IP "mágico" que funciona independentemente da configuração da rede

API flexível

  • Escrever o loop de eventos manualmente permite ajustar o código e facilita a manutenção

Testes rápidos

  • Como o código sans-IO não tem efeitos colaterais, ele é muito fácil de testar
  • Na Firezone, testes são feitos implementando uma máquina de estado de referência e comparando-a com o estado real da connlib

Casos de borda e falhas de IO

  • O design sans-IO separa a implementação do protocolo dos efeitos colaterais reais de IO, facilitando o tratamento de casos de borda e erros

Rust + sans-IO: combinação perfeita?

  • O Rust modela explicitamente propriedade e mutabilidade, o que combina bem com o design sans-IO
  • O design sans-IO usa livremente &mut para expressar mudanças de estado e, ao contrário do Rust async, usa apenas APIs síncronas

Desvantagens

  • Escrever o loop de eventos manualmente pode introduzir bugs sutis
  • Fluxos de trabalho sequenciais podem exigir mais código
  • O design sans-IO ainda não é amplamente usado na comunidade Rust

Conclusão

  • Código sans-IO pode parecer estranho no início, mas se torna muito agradável quando você se acostuma
  • Rust oferece ótimas ferramentas para modelar máquinas de estado
  • O design sans-IO força o tratamento de erros como parte do processamento de entrada, o que faz parecer a forma correta de escrever código de rede

Opinião do GN⁺

  • O design sans-IO combina bem com o modelo de propriedade do Rust e é muito adequado para implementar protocolos de rede
  • Escrever o loop de eventos manualmente aumenta a flexibilidade do código e facilita a manutenção
  • A facilidade de teste ajuda bastante a escrever código confiável
  • Porém, como ainda não é amplamente usado na comunidade Rust, pode haver falta de bibliotecas relacionadas
  • Ao adotar uma nova tecnologia, é preciso considerar a curva de aprendizado e o suporte da comunidade

1 comentários

 
GN⁺ 2024-07-05
Comentários do Hacker News
  • Antes da introdução da sintaxe async/await no Rust, era preciso implementar máquinas de estado manualmente

    • Graças à sintaxe async/await do Rust, a produtividade melhorou bastante
    • O async do Rust é convertido em máquinas de estado automáticas e armazena valores nos pontos de I/O
  • Ao escrever uma biblioteca VT100, percebeu-se um problema no padrão de encapsulamento do Rust

    • A obsessão com encapsulamento causa problemas
    • Isso relembra que computadores são máquinas que recebem entrada, transformam dados e produzem saída
  • Comparação com um design que transmite dados usando canais

    • O código fica mais complexo
    • É preciso implementar manualmente os tipos de mensagem
    • É necessário fornecer explicitamente o transmissor
    • Se a transmissão pela rede falhar, não se obtém o resultado
    • Mas também há pontos convenientes
  • No ecossistema Haskell, existe a ideia de separar lógica e execução

    • Não é mencionado como a chamada tokio::select! foi encapsulada
    • Havia interesse em implementar funções encapsuladas no estilo sans-IO
  • Funções async do Rust são compiladas em máquinas de estado

    • Fica a dúvida se já houve tentativas de combinar sans-IO com async
    • Os principais problemas são usabilidade e tratamento de Pin
  • Ao expor o estado, funções async podem se tornar “puras”

    • Houve uma tentativa de fazer bindings do OpenSSL para async Rust
  • Firezone é uma ferramenta impressionante

    • Foi encontrado um padrão semelhante no Rust-libp2p
  • Seria bom se o compilador pudesse converter código async em sans-IO automaticamente

    • A conversão manual é propensa a erros
  • Depois de ler o artigo e os comentários, parece que isso reinventou o estilo de arquitetura hexagonal ou ports/adapters

  • Fica a dúvida se o tráfego real passa pelo gateway ou se ele é usado apenas para estabelecer a conexão