Infrastructures programmatiques (2/4) : Kotlin et Kable

50 views
Skip to first unread message

Laurent Caillette

unread,
Feb 10, 2016, 1:23:25 AM2/10/16
to tec...@googlegroups.com
"Kotlin"
https://kotlinlang.org
est un langage développé par JetBrains, l'éditeur d'IntelliJ IDEA.
Kotlin est un langage statiquement typé, compilé soit vers du bytecode
`Java 6` ou Android, soit vers du JavaScript. L'intéropérabilité avec
Java et le support par IDEA sont très poussés. La dernière version est
la `1.0 beta 3`, tout à fait utilisable. Les sources du compilateurs
sont ouvertes et disponibles sous license "Apache 2".

Kotlin intègre de nombreuses facilités syntaxiques ayant fait le
succès de Ruby, Python ou Groovy, mais sans jamais sacrifier le typage
statique. C'est à dire qu'on peut atteindre le même niveau de
concision sans se faire mal à l'exécution. Il va également piquer des
idées chez Scala et `C#`.

Ce qui est impressionnant, c'est qu'au lieu d'un entassement de
fonctionnalités trop spécialisées, Kotlin offre des mécanismes
d'extension simples et élégants, qui se combinent de façon
surprenante.

Les temps de compilation et d'exécution ne diffèrent pas beaucoup de
ceux de Java. Kotlin manque encore de bibliothèques spécialisées, mais
l'intéropérabilité avec Java comble le vide, et permet également de
tester Kotlin sur un morceau non-critique d'un projet déjà développé
en Java. Pour tous ceux qui ont un patrimoine cognitif ou logiciel lié
à Java (y compris pour Android), je recommande vivement de surveiller
l'évolution de Kotlin.


=== Ansible à la loupe

Voici un vrai morceau de Playbook Ansible pour installer le JRE d'Oracle :

<<<
- stat: path='{{ jre_parent_dir }}/{{ jre_archive }}'
register: jre_archive_stat

- file: path='{{ jre_parent_dir }}' state=directory
when: not jre_archive_stat.stat.exists

- command: "wget --no-cookies --no-check-certificate --header
\"Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F;
oraclelicense=accept-securebackup-cookie\"
\"http://download.oracle.com/otn-pub/java/jdk/{{ jre_version_precise
}}/{{ jre_archive }}\""
args:
chdir: '{{ jre_parent_dir }}'
when: not jre_archive_stat.stat.exists

- command: tar -zxvf {{ jre_archive }} chdir='{{ jre_parent_dir }}'

- command: update-alternatives --install /usr/bin/java java {{
jre_home }}/bin/java 100

- command: update-alternatives --config java

- file: path=~/.oracle_jre_usage state=absent

>>>

Ça dit en gros : si le répertoire-cible n'existe pas, on le crée, puis
on télécharge le JRE. Puis on éclate l'archive, et on en fait la
version de Java par défaut.

Pour la suite on laissera tomber la création de l'URL. Ici le YAML est
tout à fait lisible, avec quelques petites irrégularités qui d'abord
ne sautent pas aux yeux. Il faut échapper la valeur YAML si elle
commence par le ``{`` utilisé par les expressions Jinja. Pour accéder
au résultat de la commande ``stat`` qui nous intéresse (l'existence du
fichier), il faut naviguer dans la structure avec un ``.stat.exists``
parce qu'il n'y a pas moyen d'enregistrer un sous-élément du résultat.
Cet enregistrement se fait dans un espace de noms global. Bien sûr, si
on écrit une variable de travers, on est bon pour un problème à
l'exécution.

Que donnerait la même chose, recodée en utilisant les facilités
offertes par Kotlin ?


=== Kable

Kable est une maquette pour voir s'il est possible de développer en
Kotlin l'équivalent des Playbooks Ansible. Il y a moyen de sauter la
visite guidée en allant voir tout de suite le "projet github"
https://github.com/caillette/Kable
.

Déjà, pour se faire une idée, voici déjà une réécriture de l'exemple
précédent avec Kable :

<<<
class JreInstallationPlaybook(
jreArchivesDir : String,
jreHome : String,
jreVersionPrecise : String
) : Playbook( {

val jreArchive = "jre" + jreVersionPrecise

val wgetCommandLine = StringBuilder()
.append( "wget --no-cookies --no-check-certificate " )
.append( "--header \"Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; " )
.append( "oraclelicense=accept-securebackup-cookie\" " )
.append( "\"http://download.oracle.com/otn-pub/java/jdk/" )
.append( jreVersionPrecise )
.append( "/" )
.append( jreArchive )
.append( "\"" )
.toString()

val jrePresent =
- Stat( file = jreArchivesDir + "/" + jreArchive )
resultTransformedBy { it.exists }

- DoFile( path = jreArchivesDir, state = DoFile.State.DIRECTORY )
unless( jrePresent )

- RunProcess( wgetCommandLine, changeToDirectory = jreArchivesDir)
unless( jrePresent )

- RunProcess( "tar -zxvg $jreArchive", changeToDirectory = jreArchivesDir )

- RunProcess( "update-alternatives --install /usr/bin/java java
$jreHome/bin/java 100" )

- RunProcess( "update-alternatives --config java" )

- DoFile( path = "~/.oracle_jre_usage", state = DoFile.State.ABSENT )

} )

>>>

Kable n'est pas plus verbeux qu'Ansible, même si on utilise des
paramètres nommés partout. Ce qui est complètement fou, c'est que tout
est typé. Même le résultat de ``Stat``, qui s'évalue finalement comme
un ``Boolean``. L'environnement de développement remonte donc toutes
les erreurs. Par exemple, pas possible de passer une chaîne de
caractères à la fonction ``unless``. Par rapport à Ansible, le gain de
productivité est effarant.


=== Plan d'exécution

Avant de découvrir comment Kotlin permet toutes ces merveilles, voyons
comment fonctionne un ``Playbook``. On avait déjà vu dans PalletOps la
notion de plan d'exécution. Dans Kable, le principe est le suivant :
un ``Playbook`` contient une liste de ``Task``. Chaque ``Task``
contient des paramètres d'exécution. L'exécution du programme Kotlin
consiste d'abord à peupler ces listes pour fabriquer
l'``ExecutionPlan``. Pour l'instant Kable n'en fait pas plus, mais un
vrai outil procéderait alors à diverses phases de validation.

Un autre intérêt de l'``ExecutionPlan`` c'est d'indiquer le niveau
d'achèvement d'une exécution. Chaque machine-cible donne lieu à une
exécution en parallèle.

Si on veut que l'exécution des commandes ait lieu après la
construction du plan, il faut que tous les paramètres soit utilisables
en différé. Si ce sont des données immuables, pas de problème. Si on
veut effectuer un branchement conditionnel, alors on ne va pas
mémoriser la valeur, mais une référence à la valeur future, et une
fonction d'évaluation. Kable définit la classe ``Deferred`` pour
capturer cette valeur. Conceptuellement le ``Deferred`` fait la même
chose qu'un ``Future`` en Java, à ceci près qu'il doit centraliser les
valeurs pour plusieurs exécutions simultanées d'un ``Playbook``.

Ça signifie que Kable affecte au moins un fil d'exécution par
connexion à une machine distante, et n'hésite pas à le bloquer. C'est
une pratique déconseillée si on veut maximiser l'utilisation des
ressources système et qu'on a beaucoup de fils d'exécution.

Mais beaucoup, c'est combien ? D'après l'article de Peter Lawrey
"Java: What is the Limit to the Number of Threads You Can Create?"
https://dzone.com/articles/java-what-limit-number-threads
on atteint la dizaine de milliers sans problème. Après, il faut voir
combien de ressources mange le SSH. Aux dernières nouvelles, Ansible
(qui doit fonctionner de la même manière) arrive à orchestrer
plusieurs milliers de serveurs.

Bon, le but n'étaut pas de rentrer dans les détails d'architecture,
mais juste d'avoir en tête certains impératifs techniques avant de
regarder la syntaxe du ``Playbook``.

archi...@archiloque.net

unread,
Feb 10, 2016, 3:13:37 AM2/10/16
to tec...@googlegroups.com, Laurent Caillette
Hello,

je comprends ton attachement au tapage statique, mais dire que

val wgetCommandLine = StringBuilder()
.append( "wget --no-cookies --no-check-certificate " )
.append( "--header \"Cookie:
gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; " )
.append( "oraclelicense=accept-securebackup-cookie\" " )
.append( "\"http://download.oracle.com/otn-pub/java/jdk/" )
.append( jreVersionPrecise )
.append( "/" )
.append( jreArchive )
.append( "\"" )
.toString()

n'est pas plus verbeux que

- command: "wget --no-cookies --no-check-certificate --header
\"Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F;
oraclelicense=accept-securebackup-cookie\"
\"http://download.oracle.com/otn-pub/java/jdk/{{ jre_version_precise
}}/{{ jre_archive }}\""

Je trouve ça un peu exagéré, tu as 50% de texte en plus. De plus avoir à
déclarer d'abord la signature de la méthode ça ajoute encore du bruit,
on arrive à quoi, 100% d'overhead ?

"Par rapport à Ansible, le gain de productivité est effarant.": non,
concrètement quand je code du Ansible j'ai très peu de problèmes qui
sont causés par du typage, la différence de productivité je l'ai sur la
capacité à lire et comprendre mon code rapidement et pouvoir voir d'un
œil le maximum de choses, et là le surcoût d'une syntaxe plus légère et
plus compacte est significatif.

Julien

Laurent Caillette

unread,
Feb 10, 2016, 3:44:35 AM2/10/16
to Julien Kirch, tec...@googlegroups.com
J'ai bien fait de préciser "Pour la suite on laissera tomber la
création de l'URL." En Kotlin j'avais envie de découper la chaîne sur
plusieurs lignes en gardant les indentations, mais on peut écrire
aussi :

<<<
val wgetCommandLine = "wget --no-cookies --no-check-certificate
--header \"Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F;
oraclelicense=accept-securebackup-cookie\"
\"http://download.oracle.com/otn-pub/java/jdk/$jreVersionPrecise/$jreArchive\""
>>>

Notons que l'expansion de Kotlin est plus compacte (1 caractère ``$``
au lieu de 6 ``{{ }}``), et elle est vérifiée statiquement.

Si on veut encore gratter des caractères, on peut virer le nommage des
paramètres. Tu remarques que dans l'exemple en Ansible, je n'ai rien
fait pour rallonger la sauce. Donc sur des Playbooks un peu long le
surcoût doit tomber sous les 10 %.

Après considérer la signature du constructeur comme juste du bruit, ça
m'ennuie un peu. Ça veut dire "On prend ça en entrée au lieu d'aller
chercher des variables globales déclarées quelque part." Le "quelque
part" dépend de trop de choses. La signature du constructeur permet
justement de dire "On a juste besoin de ça en entrée." C'est le typage
statique qui autorise cette contractualisation, avec des idiomes
fonctionnels quand peut.

Peut-être que je fais des trucs plus violents que toi avec Jinja 2. Genre :

<<<
command: 'vboxmanage sharedfolder add "{{ inventory_hostname }}"
--name "{{ debox_shared_folders[ item ].name }}" {% if
debox_shared_folders[ item ].permissions is defined %} {% if
debox_shared_folders[ item ].permissions == "rwx" %} {% else %}
--readonly {% endif %} {% else %} {% if substrate_dir_permissions[
item ] == "rwx" %} {% else %} --readonly {% endif %} {% endif %}
--hostpath "{{ debox_shared_folders[ item ].hostpath }}"'
>>>

Bon la structure de données derrière comporte quelques astuces pour
composer avec les limitations d'Ansible, mais tu comprends qu'arrivé
là j'ai le sentiment que la quincaillerie Ansible/Jinja 2 n'est plus
mon amie en termes de lisibilité. On peut aussi déléguer tout ce
fatras à une fonction Python, mais l'environnement standard d'Ansible
ne prévoit rien pour connecter un débogueur.

Une autre conséquence du typage statique, c'est la navigabilité. Avec
IDEA je peux faire un "Find references" sur n'importe quel symbole
Kotlin, et naviguer dans les chaînes d'appels de méthodes. Avec
Ansible je peux juste chercher toutes les occurences d'une chaîne de
caractères. Les prochains épisodes vont développer cette idée-là.

Henri Tremblay

unread,
Feb 10, 2016, 9:47:02 PM2/10/16
to tec...@googlegroups.com, Julien Kirch
Personnellement, mon passe-temps du moment c'est plutôt:

FROM java:8 dans un Dockerfile

et hop... j'ai java 8 d'installé (bon, en OpenJDK... J'en ai un autre pour hotspot).


--
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, adressez un e-mail à tec...@googlegroups.com.
Visitez ce groupe à l'adresse https://groups.google.com/group/techos .
Pour plus d'options, visitez le site https://groups.google.com/d/optout .

Reply all
Reply to author
Forward
0 new messages