Ktor

16 views
Skip to first unread message

Laurent Caillette

unread,
Jan 8, 2019, 12:13:28 AM1/8/19
to tec...@googlegroups.com
[Ktor]
est un socle technique en Kotlin pour réaliser des applications connectées, notamment des clients et serveurs Web. 

Ktor est officiellement supporté par JetBrains, et disponible sous license `Apache 2`. La version `1.0.0` est sortie fin novembre mais des gens disent utiliser Ktor en production depuis plus d'un an. Ktor nécessite `Kotlin 1.3` et une JVM (de préférence `Java 8`) sur le serveur.

Ktor (prononcer "kay-tor") est tellement génial que je lui pardonne son nom débile. Ktor est le premier socle technique transformant l'écriture d'applications Web en une activité non-ridicule. 

Pourquoi est-ce que Ktor est important ? Dans un monde où la plupart des applications sont connectées, avec des déploiement multiplateformes (Linux-Android-iOS-Windows-HTML5), le besoin d'une solution de bout en bout est de plus en plus pressant. Pour l'instant il n'y a pas de solution aux sources ouvertes qui se soient imposées comme standard, avec des performances convenables et un langage sexy (Rust est moyennement sexy, `C++` pas du tout). Ktor s'engouffre dans la brèche, avec tous les arguments pour balayer la concurrence. L'interface programmatique de Ktor est un modèle de concision et de clarté, ainsi qu'un merveilleux recueil d'idiomes astucieux spécifiques à Kotlin.

Très schématiquement, nous pouvons dire que Kotlin encourage notamment les mini-langages embarqués avec typage statique, et trivialise la programmation concurrente avec les Coroutines. 

Cet article se concentre sur certaines fonctionnalités techniques de Ktor. L'idée sous-jacente est que Ktor est à la fois un produit très attirant, et une excellente vitrine pour Kotlin dont il devrait précipiter l'adoption. J'espère que je ne gave pas tout le monde avec Kotlin, mais sérieux ça fait un paquet d'années que je n'ai rien vu d'aussi enthousiasmant, ça serait dommage de passer à côté.


=== HTML, CSS et patronnage

Kotlin fournit des facilités syntaxiques pour réaliser ce qui s'appelle des [Monteurs statiquement typés ("type-safe builders")]
. En gros on définit ses propres mots-clés avec des blocs dans des accolades, pour faire des choses avec la même lisibilité que du code déclaratif. Mais comme c'est du pur Kotlin typé le compilateur (ou l'environnement de développement) vérifie que la structure est valide. Que ce soit la configuration d'un serveur, du HTML ou une CSS, le bruit syntaxique est proche de zéro. 

La conséquence directe est qu'on peut jeter à la poubelle tous les moteurs de patronnage ("templating") pour produire du HTML ou du CSS. Après avoir essayé pas mal de trucs (StringTemplate, Thymeleaf...) j'ai fini par écrire le mien, qui est horrible mais moins que les autres. Puis, récemment, avec la sortie de versions stables de `kotlinx.html` et `kotlinx.css` j'ai introduit Kotlin dans un projet jusqu'alors en pur `Java 8`. Il a juste fallu bien lire la doc pour la configuration des plugins Maven, mais ça s'est mis à marcher très vite, avec l'intéropérabilité Java-Kotlin qui fonctionne comme promis. Maintenant l'écriture de pages HTML est un vrai plaisir, si je veux embarquer du code impératif ou l'appel à des bibliothèques, ça marche sans effort puisqu'on est toujours dans du Kotlin "normal", pas du texte qui sera réinjecté dans du code généré (genre JSP) avec d'éventuels dommages collatéraux.

Plus récemment je me suis lancé dans la réécriture d'une console d'administration en HTML. Celle-ci se basait sur Chiron, qui comporte quelques fonctionnalités pour [l'aiguillage de conversations HTTP]
. Mais c'est un ajout pas-si-génial-que-ça au propos initial de Chiron qui est de câbler des connexions WebSockets à du code applicatif respectant le Contrat Réactif. Donc autant alléger Chiron d'une fonctionnalité qui ne sera jamais que l'ombre de ce que propose Ktor.


=== Contrat Réactif

À propos du Contrat Réactif il y a un gros loupé dans Chiron (mentionné dans la précédente série d'articles sur le sujet) : le connecteur WebSocket pousse des objets Commande qui "sortent du tuyau" sans savoir s'il y a la disponibilité pour les traiter. C'est donc une violation du Contrat Réactif. 

Le [Contrat Réactif] 
est une variante du modèle de conception "publier-s'abonner" où l'abonné indique s'il est prêt à recevoir des événements. Le Contrat Réactif aide à maximiser l'emploi des ressources système en minimisant le nombre de fils d'exécution ("threads"), notamment pour éviter l'invalidation des caches des processeurs qui a lieu lors des changements de contexte. Une conséquence du Contrat Réactif est qu'il faut propager l'information d'un abonné prêt à recevoir des événements. Pour une application connectée cela implique de ne pas envoyer l'acquittement TCP pour un message entrant avant qu'il y ait de la disponibilité pour traiter le suivant. Ce non-envoi porte le nom de retour de pression ("backpressure") et Netty sait faire ça. 

Mais l'interfaçage de Netty avec du code réactif supportant un contrat du type ``java.util.concurrent.Flow`` n'existe pas dans Chiron. Oui je connais [Reactor-Netty]
mais il traite chaque connexion indépendemment, alors que Chiron est justement là pour sérialiser les traitements applicatifs. Chiron est un entennoir avec tous les objets Commande issus des sessions WebSocket qui se retrouvent traitées à la queue-leu-leu, et re-ventilés par session. À y regarder de près l'interfaçage avec Reactor-Netty n'est pas trivial (ou peut être qu'il faut juste [bien lire la doc ?]
).

Ajoutons que respecter le Contrat Réactif, même avec l'interface programmatique très aboutie de Project Reactor, ça devient compliqué dès qu'on sort d'un monde purement fonctionnel où les transformations sont facilement composables.

Bon mais pourquoi est-il question du Contrat Réactif ? Apparemment Ktor ne fait rien pour le supporter. On a l'impression que les requêtes sont traitées comme elles arrivent et si ça bouchonne tant pis. D'ailleurs [quelqu'un s'en est inquiété]
. La réponse m'a mis sur le cul : avec les Coroutines pas besoin de Contrat Réactif, point.

Mais ça c'est juste que je n'avais rien compris aux Coroutines.


=== Coroutines

Les Coroutines vont faire à la programmation concurrente ce que Java a fait à la gestion de la mémoire et au développement multiplateforme. Avant Java c'étaient des sujets qu'il fallait aborder avec onction, componction et précaution ; maintenant le programmeur moyen n'a plus vraiment à s'en soucier. Les grincheux grincheront qu'il y a toujours moyen de faire des bêtises, mais quand l'outil se met à faire `95 %` du boulot ça reconfigure les habitudes de travail et les modèles économiques attenants. 

La programmation concurrente n'a pas encore eu droit à ce grand nivelage. Il y a eu d'héroïques tentatives, avec la mémoire transactionnelle de Clojure et la propriété ("ownership") de Rust, habilement raffinée par Pony, mais pour différentes raisons ces langages restent confinés à des niches. 

Qu'est-ce que la programmation concurrente, d'ailleurs ? Ça se résume toujours à ça : des processeurs de tâches vont consommer des tâches qui attendent leur tour dans des files. Et si on veut maximiser les performances l'exécution de ces tâches doit être du code non-bloquant. 

Java fournit à cet effet des briques de bases tout à fait décentes : on a l'interface ``Executor`` qui mange des ``Runnable`` et dont les implantations alimentent une file d'attente où vont se servir plusieurs fils d'exécution. Après le code applicatif doit être redécoupé pour que chaque objet représentant une tâche arrive avec son propre contexte d'exécution, de façon à ne pas devoir verrouiller l'accès à une ressource partagée. Si on veut accéder à une ressource partagée ça se fait à travers une file de tâches consommées séquentiellement, avec chaque tâche qui décrit l'obtention de la ressource, et les traitements qui suivent son obtention, sous forme de fonction de rappel. 

Il y a des variations autour de ça comme le Modèle Acteur mais la terrible vérité c'est que le cerveau a plus de mal avec du code non-séquentiel, même s'il est correctement découpé. S'il est mal découpé c'est la mort. S'il est bien découpé on peut faire le malin : "Moi je tire des perfs de malade grâce à Netty et Project Reactor nananère-heu !" Et se sentir un peu seul parce qu'il n'y pas tant de gens qui comprennent ce que ça veut vraiment dire.

Et les Coroutines là-dedans ? Au niveau du langage, les Coroutines de Kotlin consistent à découper la séquence d'exécution sous forme de tâches qui véhiculent leur propre contexte d'exécution. Chaque tâche est -- pour schématiser -- une variante super-musclée de la lambda. Une séquence d'exécution peut être initiée dans un fil d'exécution et se terminer dans un autre, avec si besoin des parties traitées en parallèle. Quand la séquence d'exécution dit qu'elle bloque en attendant le résultat d'une autre séquence, ce n'est pas le fil d'exécution qui est bloqué. La suite de la séquence est mise en attente et une autre tâche qui attendait son tour est exécutée. 

Si les messages ou commandes sortant de Netty sont exploitées par des Coroutines, la cinématique générale n'ira pas demander à Netty de lire un tampon mémoire avant d'avoir fini les tâches en attente. C'est le beurre et l'argent du beurre : le retour de pression //et// du code séquentiel.

Est-ce que tout cela n'est pas un peu trop magique ? C'est là que les concepteurs de Kotlin sont très forts : c'est le code applicatif qui définit le contexte d'exécution, et notamment qui est le consommateur de tâches. Le contexte d'exécution est propagé entre les blocs de code, et par défaut il y a un contexte partagé. Mais on peut imposer son propre ``Executor`` et décorer l'exécution de chaque tâche. 

La magie des Coroutines c'est d'enchaîner des centaines de milliers d'appels qu'on peut visualiser comme bloquants, mais s'ils étaient vraiment bloquants le système d'exploitation ne pourrait jamais fournir suffisamment de fils d'exécution. 

Bon les Coroutines de Kotlin ont plein d'autres avantages. Elles rendent possible l'inversion de contrôle de la cinématique du code, comme le font les Generators de Python (``yield``) mais d'une façon plus générale, et sans que ce soit une fonctionnalité du langage. Je le redis, le compilateur de Kotlin délègue tout ce qu'il peut aux bibliothèques, à travers des interfaces programmatiques conceptuellement simples et bien documentées.

Peut-être que les Coroutines enchaînent moins vite les tâches que Project Reactor. Mais ça devrait aller en s'améliorant. 

Ce qu'il faut retenir c'est que Ktor fournit un socle pour bâtir des applications connectées avec les bénéfices du Contrat Réactif, et la lisibilité du code bloquant. C'est complètement dingue.


=== Locations

Parmi les chouettes idées de Ktor qui mettent en valeur les fonctionnalités du langage Kotlin, je dois aussi mentionner les "Locations"
. On associe à une URL une structure définie en Kotlin sous forme de data class. La data class c'est la résurrection de la bonne vielle structure de données sans comportement. En Java l'idiome correspondant implique plein de petites vexations comme le ``toString/hashCode/equals`` à générer avec l'IDE ou à se palucher. Là tu écris juste :

<<<
data class Person( val name : String, val age : Int? = null )
>>> 

Et tu as gagné ta journée d'expert Java. Pour en revenir à Ktor tu ajoutes une annotation pour transformer la data class en paramètre d'URL et tu branches l'URL dans le routeur de requêtes :

<<<
@Location("/list/{name}") 
data class Person( val name : String )

routing {
  get< Person > { 
    person -> call.respondText( "Person ${person.name}" ) 
  }
}
>>>

Il faut noter la syntaxe très compacte, très expressive, et avec du typage statique y compris dans l'expansion de la chaîne de caractères. Et ça c'est l'utilisation de base des Locations mais on peut imbriquer les data class pour "rallonger" le chemin d'une URL etc.


=== Conclusion

Même s'il y a quelques années Rust aurait pu prétendre au rôle de plateforme généraliste qui allait trivialiser toutes les autres, ce dernier a pris trop de longueurs de retard du fait de l'outillage notamment. Maintenant c'est Kotlin qui devient le choix logique pour une multitude de projets, grâce à la montée en puissance de bibliothèques de grande qualité, qui se complémentent astucieusement. 

Par exemple on a [Kotlinx-serialisation]
, une bibliothèque de sérialisation multiplateforme et multiformat (binaire et JSON en standard), qui n'utilise pas la réflexion. Ça et la connectivité WebSocket de Ktor qui fonctionne sur toutes les plateformes supportées par Kotlin, ça trivialise tout un tas de développements.

2019 sera l'année charnière où sur les nouveaux projets la question ne sera pas "Et si on faisait du Ktor" mais "Qu'est-ce qu'on pourrait bien prendre d'autre ? Heu..."

Dernière minute : avec [Korlibs]
JetBrains semble sur le chemin de mettre sa race à Unity, Haxe, LibGDX et autres solutions multiplateformes pour créer des jeux vidéo.




Dominique Jocal

unread,
Jan 8, 2019, 7:48:45 AM1/8/19
to techos
As tu regardé Micronaut en kotlin?

--
Vous recevez ce message, car vous êtes abonné au groupe Google Groupes "techos".
Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+un...@googlegroups.com.
Pour envoyer un message à ce groupe, envoyez un e-mail à l'adresse tec...@googlegroups.com.
Visitez ce groupe à l'adresse https://groups.google.com/group/techos.
Pour obtenir davantage d'options, consultez la page https://groups.google.com/d/optout.

Laurent Caillette

unread,
Jan 8, 2019, 8:45:29 AM1/8/19
to tec...@googlegroups.com
Salut Doj,

Micronaut je découvre à l'instant. 

Me basant uniquement sur la page d'accueil et "Creating your first Micronaut Kotlin app" je vois que les mecs favorisent une approche de haut niveau qui ne te laisse pas la main sur le HTTP. Déjà ça c'est mal. En plus il y a une sévère indigestion d'annotations. (Ktor en utilise aussi pour les Locations mais de façon complètement périphérique.) Les annotations impliquent de la génération de bytecode à chaud, le truc qui rend pas toujours les choses claires. C'est terrible, dans l'exemple fourni tu vois que le point d'entrée c'est ``@Controller("/hello")`` et la classe qui fournit ce contrôleur n'est pas indiquée ailleurs, donc il suffit de laisser traîner une telle déclaration dans le classpath pour que la ressource soit servie ? C'est vrai que Micronaut c'est concis, au bout de 10 lignes d'exemple on sait si on aura envie de l'utiliser.

Bon j'ai peut-être trop fait de Netty mais avec Netty tu as toujours le contrôle sur tout donc tu peux te tirer de toutes les situations. Ktor n'essaye pas de cacher ça par une sémantique magique. (Les Coroutines sont un mécanisme de Kotlin, pas de Ktor, et elles évitent l'excès de magie.) 

Les auteurs de Micronaut ont déjà Grails à leur actif, donc pour dire les choses gentiment, je ne pense pas me sentir à l'aise avec leurs processus mentaux.

Dominique Jocal

unread,
Jan 8, 2019, 10:04:17 AM1/8/19
to techos
Moi je suis très à l'aise pour la raison contraire, héhé.
C'est bien netty sous le capot, et tout est réactif dedans.
Reply all
Reply to author
Forward
0 new messages