3 points par GN⁺ 2024-07-05 | 1 commentaires | Partager sur WhatsApp
  • Chez Firezone, Rust est utilisé pour construire un accès distant sûr et scalable sur des téléphones Android, des ordinateurs MacOS ou des serveurs Linux
  • Une bibliothèque de connexion appelée connlib est utilisée pour gérer les connexions réseau et les tunnels WireGuard
  • Après plusieurs itérations, l’équipe est arrivée à une conception appelée sans-IO, qui offre des tests rapides et approfondis, une personnalisation poussée et une grande fiabilité

connlib est écrit en Rust et suit une conception sans-IO

  • Grâce à la vitesse de Rust et à sa sûreté mémoire, il convient bien à la construction de services réseau
  • Utilise notamment le runtime tokio, les WebSockets tungstenite, l’implémentation WireGuard boringtun et le chiffrement du trafic API avec rustls
  • La conception sans-IO implémente les protocoles comme des machines à états pures, au lieu d’envoyer et de recevoir des octets via des sockets à plusieurs endroits

Le modèle asynchrone de Rust et le débat sur la « coloration des fonctions »

  • Une fonction asynchrone ne peut être appelée que depuis une autre fonction asynchrone
  • Si une fonction située profondément dans la pile est asynchrone, toutes les fonctions qui l’appellent doivent elles aussi devenir asynchrones
  • Cela peut poser problème à ceux qui veulent écrire du code indépendant du caractère asynchrone ou non de ses dépendances

Présentation de sans-IO

  • L’idée centrale de sans-IO ressemble au principe d’inversion des dépendances dans le monde de l’OOP
  • La politique (ce qu’il faut faire) ne doit pas dépendre des détails d’implémentation (comment le faire)
  • Au lieu de transmettre des données avec la structure Transmit, on émet un Transmit

Application de l’inversion des dépendances

  • Au lieu de transmettre des données avec la structure Transmit, on émet un Transmit
  • La boucle d’événements implémente les effets de bord et appelle réellement UdpSocket::send

Machine à états

  • Le diagramme de machine à états d’une requête de binding STUN comporte deux états : Sent et Received
  • La machine à états est implémentée en définissant la structure StunBinding et les fonctions associées

Boucle d’événements

  • La boucle d’événements pilote la machine à états et traite les données avec poll_transmit et handle_input

Abstraction du temps

  • Les exigences liées au temps sont traitées via les API poll_timeout et handle_timeout

Les prémisses de sans-IO

  • La conception sans-IO laisse à l’application la décision de savoir si les dépendances doivent être asynchrones ou non
  • La conception sans-IO se compose facilement, fournit une API flexible, se teste aisément et s’accorde bien avec les capacités de Rust

Composition facile

  • L’API de StunBinding peut s’appliquer à la plupart des protocoles réseau
  • La bibliothèque snownet de Firezone combine ICE et WireGuard pour fournir un tunnel IP « magique » qui fonctionne quelle que soit la configuration réseau

API flexible

  • Écrire soi-même la boucle d’événements permet d’affiner le code et d’en faciliter la maintenance

Tests rapides

  • Le code sans-IO n’a pas d’effets de bord, ce qui le rend très facile à tester
  • Chez Firezone, une machine à états de référence est implémentée pour comparer son état à l’état réel de connlib dans les tests

Cas limites et échecs d’IO

  • La conception sans-IO sépare l’implémentation du protocole des effets de bord d’IO réels, ce qui facilite le traitement des cas limites et des erreurs

Rust + sans-IO : un duo parfait ?

  • Rust modélise explicitement la propriété et la mutabilité, ce qui s’accorde bien avec la conception sans-IO
  • La conception sans-IO utilise librement &mut pour exprimer les changements d’état et, contrairement à Rust async, n’emploie que des API synchrones

Inconvénients

  • Écrire soi-même la boucle d’événements peut introduire des bugs subtils
  • Les workflows séquentiels peuvent demander davantage de code
  • Dans la communauté Rust, la conception sans-IO n’est pas encore largement adoptée

Conclusion

  • Le code sans-IO peut sembler déroutant au début, mais devient très agréable une fois qu’on s’y habitue
  • Rust fournit d’excellents outils pour modéliser des machines à états
  • La conception sans-IO impose d’intégrer la gestion des erreurs au traitement des entrées, ce qui donne l’impression d’une bonne manière d’écrire du code réseau

L’avis de GN⁺

  • La conception sans-IO s’accorde bien avec le modèle de propriété de Rust et convient donc très bien à l’implémentation de protocoles réseau
  • Écrire soi-même la boucle d’événements améliore la flexibilité du code et sa maintenabilité
  • La facilité de test aide beaucoup à écrire du code fiable
  • En revanche, comme cette approche n’est pas largement utilisée dans la communauté Rust, les bibliothèques associées peuvent manquer
  • Lors de l’adoption d’une nouvelle technique, il faut tenir compte de la courbe d’apprentissage et du support de la communauté

1 commentaires

 
GN⁺ 2024-07-05
Avis Hacker News
  • Avant l’introduction de la syntaxe async/await de Rust, il fallait implémenter manuellement des machines à états

    • Grâce à la syntaxe async/await de Rust, la productivité s’est nettement améliorée
    • L’async de Rust est transformé en machine à états automatique et conserve les valeurs aux points d’I/O
  • En écrivant une bibliothèque VT100, il a réalisé le problème posé par les patterns d’encapsulation en Rust

    • L’obsession de l’encapsulation provoque des problèmes
    • Cela rappelle qu’un ordinateur est une machine qui reçoit des entrées, transforme des données et produit des sorties
  • Comparaison avec une conception qui transmet les données via des channels

    • Le code devient plus complexe
    • Il faut implémenter manuellement les types de messages
    • Il faut fournir explicitement l’émetteur
    • En cas d’échec de transmission réseau, on n’obtient pas le résultat
    • Mais cela présente aussi des aspects pratiques
  • Il existe dans l’écosystème Haskell une idée consistant à séparer la logique de l’exécution

    • Il n’est pas précisé comment l’appel à tokio::select! a été encapsulé
    • Il s’intéressait à l’implémentation de fonctions encapsulées dans le style sans-I/O
  • Les fonctions async de Rust sont compilées en machines à états

    • Il se demande s’il y a eu des tentatives pour combiner sans-I/O et async
    • Les principaux problèmes sont l’ergonomie et la gestion de Pin
  • En exposant l’état, une fonction async peut devenir « pure »

    • Il a essayé de créer un binding d’OpenSSL pour l’async Rust
  • Firezone est un outil remarquable

    • Il a trouvé un pattern similaire dans Rust-libp2p
  • Ce serait bien que le compilateur puisse convertir automatiquement le code async en sans I/O

    • La conversion manuelle est sujette aux erreurs
  • Après avoir lu l’article et les commentaires, cela donne l’impression de réinventer le style architectural hexagonal ou ports/adapters

  • Il se demande si le trafic réel passe par la gateway, ou si elle est utilisée uniquement pour l’établissement de la connexion