Quelles technologies se cachent derrière le format de paquets snaps « by Canonical » ?

Principe de fonctionnement des snaps

Contrairement aux paquets habituels sous Linux, les snaps ne partagent pas leurs dépendances. C’est ce qui explique que les logiciels empaquetés dans des snaps soient indépendants du système. Les paquets sont bien plus lourds et chaque logiciel lancé chargera en RAM l’intégralité de ses dépendances sans les partager.

Un paquet snap est une archive SquashFS montée en lecture seule, exactement comme un Live CD sous Linux. Les paquets sont signés comme les dépôts et mis à jours de manière transactionnelle. Les mises à jours n’ajoutent que les différences entre la nouvelle version et l’actuelle, sans retélécharger intégralement le paquet.

Chaque paquet dispose d’autorisations précises pour accéder à d’autres applications, services ou fonctionnalités du système d’exploitation (via AppArmor). Leurs dossiers « tmp » sont séparés.

Un snap peut difficilement endommager ou déstabiliser le système :

Contrairement aux conteneurs à la mode, type docker et compagnie, les paquets sont montés dans des dossiers isolés par confinement, ils utilisent donc le noyau du système et n’ont pas d’interface réseau individuelle requérant une gestion NAT compliquée.

Le système Ubuntu complet a été découpé et mis dans des snaps dans l’édition Ubuntu Core. Voici à quoi ressemble l’OS actuel :

Et voici Ubunutu Core :

Sur Ubuntu Core, les .deb ne disparaissent pas du système pour autant mais ne sont plus la voie principale pour installer des paquets, puisqu’apt semble placé dans un conteneur (assez peu d’info trouvée à ce sujet). Dans un premier temps en revanche, l’option la plus souple semble d’installer snapd (l’apt des snaps) sur un système non-snappy tel que l’Ubuntu Desktop classique, ce qui vous prive toutefois du déploiement facilité de mises à jour d’Ubuntu Core.

Vous pouvez trouver plus d’informations en écoutant cette conférence de l’Ubuntu Party de Paris.

Que sont les paquets Snaps développés par Canonical pour la portabilité ?

Introduction

Avec son désir d’interface graphique « convergeante », c’est à dire adaptative entre des tailles d’écran très différentes, Canonical doit faire face à des problèmes de portabilité de ses applications.

Le monde mobile a ceci de particulier que les périphériques sont vendus préinstallés, à des utilisateurs néophytes, le plus souvent sur l’architecture ARM réputée plus complexe que le monde standardisé des processeurs x86 de bureau. Mettre le téléphone d’un utilisateur à jour et à distance relève dès lors du défis. L’univers Android le connait bien, puisque les téléphones reçoivent au mieux quelques mises à jour avant d’être abandonnés.

L’entreprise Canonical veut éviter de tomber dans ce piège et pouvoir gérer elle même les mises à jours de tous les périphériques utilisant son OS, mobiles comme ordinateurs de bureau. Cela lui permettra d’une part d’éviter une fragmentation importante des versions de son OS sur le marché mobile, d’autre part d’étendre la prise en charge de ses applications sur ordinateur de bureau vers d’autres distributions Linux.

Des paquets universels

La solution envisagée repose sur la création d’un format de paquet universel, différent de deb ou rpm, capable de s’installer sur tout système d’exploitation Linux ayant installé le logiciel snapd, équivalent d’apt. Snappy est déjà disponible dans Gentoo, Fedora, Arch Linux, Debian etc.

Puisque ces paquets sont portables, il existe un site expliquant comment packager des applications ainsi qu’un store d’applications empaquetées.

Microsoft Azure supporte Ubuntu Snappy depuis fin 2014, une version serveur d’Ubuntu utilisant les snaps, et Ubuntu Desktop les supporte depuis la version 16.04.

L’intérêt pour les développeurs

Tout le casse tête pour un développeur d’applications souhaitant prendre en charge l’univers Linux est de devoir s’intéresser à chaque distribution individuellement, empaqueter son logiciel en .deb, .rpm et continuer avec les nombreux autres formats, tout en s’impliquant dans un processus long et complexe de maintient de ses paquets dans les dépôts officiels des distributions qu’il veut toucher, et recommencer lors de chaque mise à jour desdites distributions, là où un .exe sous Windows peut fonctionner de XP à Windows 10 sans avoir eu à s’en soucier.

Les paquets snaps viennent de résoudre cette problématique.

L’intérêt pour les utilisateurs

En revanche, côté utilisateur, les .deb ont beaucoup d’avantages :

  • On peut les installer via un store applicatif (apt, la logithèque Ubuntu, le centre de logiciels Gnome, celui de KDE etc)
  • L’installation de paquets sur le système requiers les droits « root »
  • Tous les paquets sont vérifiés et signés, donc sûrs, leur présence dans un dépôt officiel garanti qu’ils soient libres et que ce soit bien la version officielle du développeur initial (et non trafiquée par Sourceforge)
  • Les mises à jours de tous les logiciels sont centralisées dans un seul outil, ce dont les utilisateurs de Windows rêvent depuis sa création
  • Les dépendances d’un logiciel sont partagées, rendant son téléchargement initial ultra léger (plus on a de logiciels installés plus on a de change d’avoir déjà toutes les bibliothèques les plus populaires). Autre avantage, la consommation en RAM est réduite à son minimum car une bibliothèque n’est chargée qu’une fois pour plusieurs logiciels (GTK est un bon exemple). Le système est donc rapide pour installer, lancer et mettre à jour ses logiciels.

Et quelques inconvénients :

  • Les versions des logiciels sont figées dans les dépôts, on a rarement la dernière version du développeur, seulement les mises à jour pour la version en cours
  • Ajouter des dépôts externe créé irrémédiablement de l’instabilité, en proposant parfois des mises à jour de dépendances
  • Installer un logiciel hors des dépôts n’est pas une facilité et le mettre à jour est plus compliqué

L’avantage des paquets snaps pour l’utilisateur est la possibilité d’avoir les dernières versions disponibles de chaque logiciel sans avoir à attendre la prochaine montée de version du système. Les paquets snaps pourraient convaincre des développeurs dont le logiciel fonctionne sous Linux de faire l’effort de l’empaqueter pour le distribuer et le maintenir. Certains logiciels comme l’interface Unity représentent actuellement un trop grand défi pour être empaquetés sur d’autres distributions. Enfin, les vieux logiciels abandonnés finissent souvent par ne plus avoir de paquets deb/rpm récents et ne peuvent plus être exécutés sur les distributions récentes à cause de leurs dépendances.

Les paquets snaps arrivent avec un inconvénient, en terme de poids et consommation en RAM. Pour savoir de quoi ils sont faits, lisez la suite !

Bonnes pratiques concernant l’envoi de newsletters

Ma séance de veille du jour concerne une conférence Alsacréations de Sébastien Lejeune en 2016.

  • Pas plus de 50 caractères dans le sujet. Un sujet court et précis est plus percutant, surtout sur des emailing qui ne sont pas sollicités par les internautes.
  • Souvent dans un client mail le bouton SPAM en haut et près du corps du mail, Sébastien recommande de placer le lien de désinscription tout en haut du mail car il vaut mieux qu’un internaute se désinscrive plutôt que de signaler notre serveur et adresse comme SPAM
  • Avoir un lien miroir de l’email est pratique si l’usager n’arrive pas à visualiser correctement l’email dans son client, car les clients lourds ont souvent un mauvais moteur de rendu
  • On peut mettre ces deux liens côte côte
  • On peut mettre juste après un résumé de l’email en 100 caractères si le mail est long, car les gens passent vite à autre chose
  • On peut mettre un footer avec encore le lien de désinscription + un lien pour gérer ses préférences s’il y a un abonnement à plusieurs newsletters + des liens vers les réseaux sociaux
  • Utiliser le moins d’images possible et le plus de CSS possible car les images chargent lentement ou sont bloquées
  • Le choix d’un lien vs un bouton n’a pas toujours l’impact qu’on imagine, les liens fonctionnent très bien dans les mailing
  • Utiliser des call to action type « Lire plus sur le blog », « Acheter maintenant », « Télécharger l’application », « Réserver maintenant », « Comparer encore d’autre » etc
  • Mettre le prénom de la personne personnalise l’email, c’est très efficace
  • Sur desktop il propose en 500 et 640px de large pour éviter les scroll latéraux
  • Sur mobile, entre 280 et 320px de large
  • HTML5/CSS3 on oublie, il propose XHTML 1.0, les breakpoints sont faits en prenant les propriétés CSS par browser (par exemple pour les iPhone et compagnie), les doctypes ne sont pas pris en compte par GMAIL Yahoo Hotmail.
  • mettre un meta viewport avec un initial-scale à 1.0 pour éviter que le mail soit zoomé par défaut par les clients mails
  • Outlook jusqu’à la version 2007 utilise le moteur de rendu de Microsoft Word
  • Tester, tester, tester. Il utilise deux services en ligne, litmus et Email on acid

Nokogiri : installer avec « Building native extensions » sous Mac OSX 10.12 Sierra

Si l’installation de la gem Nokogiri provoque une erreur telle que :

$ gem install nokogiri -v 1.6.7.1
Fetching: nokogiri-1.6.7.1.gem (100%)
Building native extensions.  This could take a while...
ERROR:  Error installing nokogiri:
  ERROR: Failed to build gem native extension.

    current directory: /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/ext/nokogiri
/Users/username/.rvm/rubies/ruby-2.3.1/bin/ruby -r ./siteconf20170214-68028-185u3z7.rb extconf.rb
checking if the C compiler accepts ... yes
checking if the C compiler accepts -Wno-error=unused-command-line-argument-hard-error-in-future... no
Building nokogiri using packaged libraries.
Using mini_portile version 2.0.0
checking for iconv.h... yes
checking for gzdopen() in -lz... yes
checking for iconv using --with-opt-* flags... yes
************************************************************************
IMPORTANT NOTICE:

Building Nokogiri with a packaged version of libxml2-2.9.2
with the following patches applied:
  - 0001-Revert-Missing-initialization-for-the-catalog-module.patch
  - 0002-Fix-missing-entities-after-CVE-2014-3660-fix.patch
  - 0003-Stop-parsing-on-entities-boundaries-errors.patch
  - 0004-Cleanup-conditional-section-error-handling.patch
  - 0005-CVE-2015-1819-Enforce-the-reader-to-run-in-constant-.patch
  - 0006-Another-variation-of-overflow-in-Conditional-section.patch
  - 0007-Fix-an-error-in-previous-Conditional-section-patch.patch
  - 0008-CVE-2015-8035-Fix-XZ-compression-support-loop.patch
  - 0009-Updated-config.guess.patch
  - 0010-Fix-parsering-short-unclosed-comment-uninitialized-access.patch
  - 0011-Avoid-extra-processing-of-MarkupDecl-when-EOF.patch
  - 0012-Avoid-processing-entities-after-encoding-conversion-.patch
  - 0013-CVE-2015-7497-Avoid-an-heap-buffer-overflow-in-xmlDi.patch
  - 0014-CVE-2015-5312-Another-entity-expansion-issue.patch
  - 0015-Add-xmlHaltParser-to-stop-the-parser.patch
  - 0016-Detect-incoherency-on-GROW.patch
  - 0017-CVE-2015-7500-Fix-memory-access-error-due-to-incorre.patch
  - 0018-CVE-2015-8242-Buffer-overead-with-HTML-parser-in-pus.patch

Team Nokogiri will keep on doing their best to provide security
updates in a timely manner, but if this is a concern for you and want
to use the system library instead; abort this installation process and
reinstall nokogiri as follows:

    gem install nokogiri -- --use-system-libraries
        [--with-xml2-config=/path/to/xml2-config]
        [--with-xslt-config=/path/to/xslt-config]

If you are using Bundler, tell it to use the option:

    bundle config build.nokogiri --use-system-libraries
    bundle install

Note, however, that nokogiri is not fully compatible with arbitrary
versions of libxml2 provided by OS/package vendors.
************************************************************************
Extracting libxml2-2.9.2.tar.gz into tmp/x86_64-apple-darwin15.0.0/ports/libxml2/2.9.2... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0001-Revert-Missing-initialization-for-the-catalog-module.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0002-Fix-missing-entities-after-CVE-2014-3660-fix.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0003-Stop-parsing-on-entities-boundaries-errors.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0004-Cleanup-conditional-section-error-handling.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0005-CVE-2015-1819-Enforce-the-reader-to-run-in-constant-.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0006-Another-variation-of-overflow-in-Conditional-section.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0007-Fix-an-error-in-previous-Conditional-section-patch.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0008-CVE-2015-8035-Fix-XZ-compression-support-loop.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0009-Updated-config.guess.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0010-Fix-parsering-short-unclosed-comment-uninitialized-access.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0011-Avoid-extra-processing-of-MarkupDecl-when-EOF.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0012-Avoid-processing-entities-after-encoding-conversion-.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0013-CVE-2015-7497-Avoid-an-heap-buffer-overflow-in-xmlDi.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0014-CVE-2015-5312-Another-entity-expansion-issue.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0015-Add-xmlHaltParser-to-stop-the-parser.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0016-Detect-incoherency-on-GROW.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0017-CVE-2015-7500-Fix-memory-access-error-due-to-incorre.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxml2/0018-CVE-2015-8242-Buffer-overead-with-HTML-parser-in-pus.patch... OK
Running 'configure' for libxml2 2.9.2... OK
Running 'compile' for libxml2 2.9.2... OK
Running 'install' for libxml2 2.9.2... OK
Activating libxml2 2.9.2 (from /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/ports/x86_64-apple-darwin15.0.0/libxml2/2.9.2)...
************************************************************************
IMPORTANT NOTICE:

Building Nokogiri with a packaged version of libxslt-1.1.28
with the following patches applied:
  - 0001-Adding-doc-update-related-to-1.1.28.patch
  - 0002-Fix-a-couple-of-places-where-f-printf-parameters-wer.patch
  - 0003-Initialize-pseudo-random-number-generator-with-curre.patch
  - 0004-EXSLT-function-str-replace-is-broken-as-is.patch
  - 0006-Fix-str-padding-to-work-with-UTF-8-strings.patch
  - 0007-Separate-function-for-predicate-matching-in-patterns.patch
  - 0008-Fix-direct-pattern-matching.patch
  - 0009-Fix-certain-patterns-with-predicates.patch
  - 0010-Fix-handling-of-UTF-8-strings-in-EXSLT-crypto-module.patch
  - 0013-Memory-leak-in-xsltCompileIdKeyPattern-error-path.patch
  - 0014-Fix-for-bug-436589.patch
  - 0015-Fix-mkdir-for-mingw.patch
  - 0016-Fix-for-type-confusion-in-preprocessing-attributes.patch
  - 0017-Updated-config.guess.patch

Team Nokogiri will keep on doing their best to provide security
updates in a timely manner, but if this is a concern for you and want
to use the system library instead; abort this installation process and
reinstall nokogiri as follows:

    gem install nokogiri -- --use-system-libraries
        [--with-xml2-config=/path/to/xml2-config]
        [--with-xslt-config=/path/to/xslt-config]

If you are using Bundler, tell it to use the option:

    bundle config build.nokogiri --use-system-libraries
    bundle install
************************************************************************
Extracting libxslt-1.1.28.tar.gz into tmp/x86_64-apple-darwin15.0.0/ports/libxslt/1.1.28... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0001-Adding-doc-update-related-to-1.1.28.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0002-Fix-a-couple-of-places-where-f-printf-parameters-wer.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0003-Initialize-pseudo-random-number-generator-with-curre.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0004-EXSLT-function-str-replace-is-broken-as-is.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0006-Fix-str-padding-to-work-with-UTF-8-strings.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0007-Separate-function-for-predicate-matching-in-patterns.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0008-Fix-direct-pattern-matching.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0009-Fix-certain-patterns-with-predicates.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0010-Fix-handling-of-UTF-8-strings-in-EXSLT-crypto-module.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0013-Memory-leak-in-xsltCompileIdKeyPattern-error-path.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0014-Fix-for-bug-436589.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0015-Fix-mkdir-for-mingw.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0016-Fix-for-type-confusion-in-preprocessing-attributes.patch... OK
Running git apply with /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/patches/libxslt/0017-Updated-config.guess.patch... OK
Running 'configure' for libxslt 1.1.28... OK
Running 'compile' for libxslt 1.1.28... ERROR, review '/Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1/ext/nokogiri/tmp/x86_64-apple-darwin15.0.0/ports/libxslt/1.1.28/compile.log' to see what happened. Last lines are:
========================================================================
                      ^
xsltutils.c:1876:24: note: expanded from macro 'XSLT_CLOCK'
#    define XSLT_CLOCK CLOCK_REALTIME
                       ^
xsltutils.c:1886:23: error: use of undeclared identifier 'CLOCK_REALTIME'
        clock_gettime(XSLT_CLOCK, &startup);
                      ^
xsltutils.c:1876:24: note: expanded from macro 'XSLT_CLOCK'
#    define XSLT_CLOCK CLOCK_REALTIME
                       ^
xsltutils.c:1890:19: error: use of undeclared identifier 'CLOCK_REALTIME'
    clock_gettime(XSLT_CLOCK, &cur);
                  ^
xsltutils.c:1876:24: note: expanded from macro 'XSLT_CLOCK'
#    define XSLT_CLOCK CLOCK_REALTIME
                       ^
1 warning and 3 errors generated.
make[2]: *** [xsltutils.lo] Error 1
make[1]: *** [all-recursive] Error 1
make: *** [all] Error 2
========================================================================
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
  --with-opt-dir
  --with-opt-include
  --without-opt-include=${opt-dir}/include
  --with-opt-lib
  --without-opt-lib=${opt-dir}/lib
  --with-make-prog
  --without-make-prog
  --srcdir=.
  --curdir
  --ruby=/Users/username/.rvm/rubies/ruby-2.3.1/bin/$(RUBY_BASE_NAME)
  --help
  --clean
  --use-system-libraries
  --enable-static
  --disable-static
  --with-zlib-dir
  --without-zlib-dir
  --with-zlib-include
  --without-zlib-include=${zlib-dir}/include
  --with-zlib-lib
  --without-zlib-lib=${zlib-dir}/lib
  --enable-cross-build
  --disable-cross-build
/Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/mini_portile2-2.0.0/lib/mini_portile2/mini_portile.rb:366:in `block in execute': Failed to complete compile task (RuntimeError)
  from /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/mini_portile2-2.0.0/lib/mini_portile2/mini_portile.rb:337:in `chdir'
  from /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/mini_portile2-2.0.0/lib/mini_portile2/mini_portile.rb:337:in `execute'
  from /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/mini_portile2-2.0.0/lib/mini_portile2/mini_portile.rb:111:in `compile'
  from /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/mini_portile2-2.0.0/lib/mini_portile2/mini_portile.rb:150:in `cook'
  from extconf.rb:289:in `block (2 levels) in process_recipe'
  from extconf.rb:182:in `block in chdir_for_build'
  from extconf.rb:181:in `chdir'
  from extconf.rb:181:in `chdir_for_build'
  from extconf.rb:288:in `block in process_recipe'
  from extconf.rb:187:in `tap'
  from extconf.rb:187:in `process_recipe'
  from extconf.rb:490:in `
' To see why this extension failed to compile, please check the mkmf.log which can be found here: /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/extensions/x86_64-darwin-15/2.3.0/nokogiri-1.6.7.1/mkmf.log extconf failed, exit code 1 Gem files will remain installed in /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/gems/nokogiri-1.6.7.1 for inspection. Results logged to /Users/username/.rvm/gems/ruby-2.3.1@test-gemset3/extensions/x86_64-darwin-15/2.3.0/nokogiri-1.6.7.1/gem_make.out

Vous pouvez essayer les solutions suivantes, en adaptant le numéro de version à celui voulu :
gem install nokogiri -v 1.6.8.1 -- --use-system-libraries
gem install nokogiri -v 1.6.8.1 -- --use-system-libraries=true --with-xml2-include=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/libxml2
brew uninstall libxml2 (fourni par Xcode) puis relancer gem install nokogiri -v 1.6.8.1 -- --use-system-libraries
brew reinstall xz --universal puis relancer gem install nokogiri -v 1.6.8.1 -- --use-system-libraries

Les Rails Engines, poupées russes d’applications Rails

Il n’y a pas si longtemps, dans une galaxie pas si lointaine, nous souhaitions factoriser du code au sein d’une Gem Ruby. Son but, traiter à notre façon les erreurs 402, 404 et 500 que Rails affiche par défaut aux visiteurs. Ces erreurs disposent de routes automatiques native (/402, /404 et /500) qui ne sont que des routes par défaut pointant vers des fichiers statiques présent dans le dossier public.

Notre Gem devait :
– intercepter les redirections qu’opère Rails en repassant dans le routeur d’URL lors d’une erreur ou page introuvable avec match « /404 », to: « erreurs#page_introuvable », via: :all
– redéfinir un template de vue un peu plus présentable pour le visiteur (charté selon le style de l’application)
– intercepter les exceptions de l’application principale
– nous envoyer un email avec le contenu de l’exception
– éventuellement appeler notre webservice d’enregistrement de logs (selon une variable d’environnement dans l’appli)

En bref, le code déporté dans la Gem était une quasi application Rails à part entière, faisant intervenir des routes, des actions dans un contrôleur, des fichiers de vue, des actions dans un mailer et ses vues associées.

Pour cela, nous avons utilisé un Rails Engine :

rails plugin new ma_sous_app --full

Le code généré peut être facilement déporté et intégré dans n’importe quelle application Rails habituelle. Nous l’avons packagée au sein d’une gem et installée sur notre serveur de gem interne sans rencontrer de difficulté.

Pour en savoir plus, je vous invite à lire ce superbe article de Brian Leonard.

Programmation asynchrone en Ruby 3 : les Guilds (Partie 5)

Nous avons fait un tour plutôt large des diverses solutions pour réaliser un programme asynchrone en Ruby. Je vous propose de terminer cette série d’articles par une ouverture sur ce que pourrait apporter la prochaine version majeure de Ruby.

C’est en tombant sur cet article d’Olivier Lacan que j’ai entendu parler de la révision du modèle concurrentiel de Ruby.

Comme évoqué dans la partie 2, l’interpréteur Ruby souffre d’une limitation dans sa gestion des threads : ces derniers ne peuvent pas être exécutés simultanément en parallèle mais le sont chacun à leur tour. Ce mécanisme a été introduit pour offrir l’API des mutex aux développeurs et ainsi permettre de synchroniser les threads. Cette limitation a un impact important sur la performance de nos applications multi-processus.

Koichi Sasada est un développeur core de CRuby, l’interpréteur Ruby « officiel ». Il a proposé un nouveau modèle mêlant Threads et Fibers.

Voici une traduction de l’article d’Olivier cité plus haut, avec mes ajouts personnels entre crochets :
« Les Guilds sont composées d’au moins un Thread, qui à son tour a au moins une Fiber. Les Threads de différentes Guilds peuvent s’exécuter en parallèle, tandis que les Threads dans la même guilde ne peuvent pas [ils le sont à tour de rôle]. Les objets d’une Guild ne peuvent ni lire ni écrire sur les objets d’une autre Guild. »

ruby_3_guilds_threads_and_fibers

« Les Threads appartenant à la même Guild ne peuvent pas s’exécuter en parallèle car il existe un GGL (Giant Guild Lock). Cependant, les Threads de différentes Guilds peuvent s’exécuter en parallèle.

Vous pouvez penser un programme Ruby 2.x comme ayant une Guild unique. »

ruby_3_guilds_concurrency

« Un objet d’une Guild ne sera pas capable de lire ou d’écrire sur un objet mutable d’une Guild différente. Empêcher la modification [d’objets] permet aux Guilds de fonctionner en parallèle sans courir le risque d’accéder et de modifier les mêmes objets. »

ruby_3_guilds_object_access_restrictions

En revanche il sera possible de réaliser des « deep copy » d’objets d’une Guild à l’autre.

ruby_3_guilds_channels_object_copy

Il existera une méthode .freeze pour rendre immuable (constant) tout objet mutable :

# While Numeric types like Integers are immutable by
# default, Hash instances aren't.
mutable = [1, { "key" => "value" }, 3].freeze
 
# But if you freeze String or Hash instances and the
# Array instances that reference them, then you have
# a "deeply frozen" immutable object.
immutable = [
  "bar".freeze,
  { "key" => "value".freeze }.freeze
].freeze

Je vous invite à lire cette synthèse de Maciej Mensfeld qui récapitule ce que sont les Guilds.

Programmation asynchrone en Ruby : présentation de la gem Celluloid (Partie 4)

Après concurrent-ruby, continuons notre tour des gems avec Celluloid.

La syntaxe de la gem est identique au module Async de concurrent-ruby :

require "celluloid/current"
 
class Demonstration
  include Celluloid
 
  def dire_3s_plus_tard(phrase)
    sleep 3
    puts phrase
  end
end
 
demo = Demonstration.new
 
# Exemple synchrone, non thread-safe
demo.dire_3s_plus_tard("Premier")
puts "Second"
 
# Exemple asynchrone, non bloquant, thread-safe
demo.async.dire_3s_plus_tard("Second")
puts "Premier"

Ceci dit contrairement à concurrent-ruby, la méthode await n’existe pas pour écrire du code synchrone bloquant thread-safe. La gem implémente également les Futures, mais pas les Promises ni toutes les autres fonctionnalités présentées dans la partie 3. Elle est donc bien moins intéressante et ne semble pas avoir, au vu de sa faible documentation, de fonctionnalité particulière que n’offrirait pas son alternative.

Il existe probablement d’autres gems à étudier, telle que EventMachine (plus complexe et lourde à mettre en place), ceci dit je pense que concurrent-ruby peut déjà répondre à un éventail très large de cas de figure. Passons sans attendre à notre 5ème et dernière partie sur l’avenir en Ruby 3.

Ruby on Rails, Passenger et MySQL : interrompre un thread sans détruire le ConnectionPool

Imaginons. Une application Rails dispose d’une action qui lance la génération d’un gigantesque CSV. Afin de ne pas attendre le retour de l’action, on place la génération du CSV dans un thread et on fait retourner un message à l’utilisateur type « Ne vous inquiétez pas on s’occupe de tout ». Et on part tranquillement pour une heure de génération, relax.

Un petit bout de JavaScript va appeler périodiquement une autre action tant que le fichier ne sera pas prêt. Il changera un bouton de couleur en y ajoutant un lien de téléchargement dès que ce sera fini. Mais subitement, l’utilisateur change d’avis, modifie quelques options sur l’interface et demande la génération d’un nouveau fichier.

Comment on tue ce thread sans tout péter ?

La génération du CSV devant recommencer, on souhaite éviter de gâcher des ressources serveur et l’écrasement de notre fichier de sortie.

Je suis un développeur bourrin : Thread.kill

Je vais ressortir mon schéma favoris qui illustre comment le module Apache Passenger se charge de load balancer les requêtes qu’il reçoit :
passenger-request_load_balancing
L’application Rails est démarrée plusieurs fois dans des processus séparés. De ce fait, en production, Thread.list peut ne pas lister le thread dans lequel notre premier CSV est en cours de génération : tout dépend dans quelle application tombe notre requête.

Mettons que notre développeur bourrin ait de la chance, tombe aléatoirement dans le bon processus et qu’il tranche net notre thread. La connexion à la base de données ouverte par l’application Rails dans le thread pendouille sans avoir été déconnectée proprement, elle sera définitivement inutilisable jusqu’à l’arrêt de ce processus Passenger.

L’ORM ActiveRecord est configuré par défaut pour disposer d’un pool de 5 connexions à la base de données. Lorsque les 5 connexions seront bloquées, tout accès à la base de données lèvera l’erreur suivante :

#<Mysql2::Error: This connection is in use by: #<Thread:0x007f9d913dd6b0 dead>>

Je suis un développeur ignoble : je pousse le thread à se suicider

La parade consiste alors à faire lire régulièrement un booléen « dois_arreter » au thread. Dès que le booléen passe à true, la boucle dans le thread s’interrompt et le thread se laisse mourir. Il suffit de mettre à jour ce booléen pour interrompre proprement le thread, et MySQL est un bon endroit pour stocker cette variable commune à tous les threads.

Même problème en revanche concernant la connexion à MySQL. Elle n’est plus en cours d’utilisation par un thread mort, comme disait l’erreur précédente, mais elle n’est pour autant pas refermée, donc pas libérée pour retourner dans le ConnectionPool. Après 5 décès de threads, l’erreur suivante apparait :

#<ActiveRecord::ConnectionTimeoutError: could not obtain a database connection within 5.000 seconds (waited 5.000 seconds)>

On ajoute le bout de code suivant juste avant la fin du thread et le tour est joué :

if (ActiveRecord::Base.connection && ActiveRecord::Base.connection.active?)
  ActiveRecord::Base.connection.close
end

Suite et fin

Bon, après, libre à vous d’identifier plus précisément les threads de l’utilisateur en joignant un identifiant au booléen. Libre aussi à vous de définir si le lancement d’un nouveau thread doit attendre la mort de tous ses prédécesseurs, par exemple, en ajoutant la suppression de la ligne de BDD avant la mort du thread et en attendant que toutes les lignes aient disparues pour lancer le nouveau.

Que faire si Apache redémarre et interrompt un thread qui n’aura pas le temps d’effacer son entrée en BDD ? Comment supprimer toutes les lignes d’états de thread au démarrage de l’appli, alors que Passenger démarre et arrête des processus à tout moment ? On peut éventuellement stocker le timestamp de démarrage du serveur.

Sous Linux :

ls -dl --full-time /proc/$(ps axo pid,cmd,user | grep apache | grep root | cut -d" " -f2) | cut -d" " -f6,7

Sous Mac :

ps axo pid,user,lstart,command | grep "rails s" | grep -v "grep

Et voilà, jusque là nous on s’en est sorti, si vous rencontrez d’autres problèmes n’hésitez pas à les partager en commentaire 🙂

Passenger, gestion des ressources avec plus de 6 applications

Erreur type rencontrée : This website is under heavy load, too many requests

Too many requests ?

Passenger contient dans sa configuration un paramètre PassengerMaxRequestQueueSize définissant le nombre de requêtes en attente à 100 par défaut.
Contrairement à ce que l’on pourrait penser, rallonger cette file d’attente n’est pas une bonne idée. Le serveur a une capacité à traiter un volume de requêtes simultanées donné. Lorsqu’il ne peut plus en traiter, les clients doivent alors patienter dans la file d’attente : lorsque le serveur a placé 100 requêtes en attente, toutes les prochaines recevront l’erreur « 503 Service Unavailable ».
Cette file d’attente est une sorte de tampon lors de montée en charge momentanée et non une solution durable. On peut en effet s’attendre à voir le temps d’attente devenir de plus en plus long à mesure que la file s’allonge, et des clients seront alors tentés de quitter ou de rafraichir leur page, augmentant le nombre de requêtes à traiter sans même en attendre la réponse.

Si le serveur ne peut plus faire face, il faut alors en changer ou augmenter ses ressources. Mais ce n’est pas la seule raison de l’apparition de notre erreur initiale.

Beaucoup d’appli, pool trop petit

Lorsque l’erreur « This website is under heavy load » apparait, vous pouvez soupçonner une autre cause qu’un nombre trop élevé de requêtes. C’est le cas si vous venez de lancer des actions gourmandes sur plusieurs applications, comme dans notre cas.

Ce qu’explique cet article c’est la manière dont Passenger gère des applications. Par nature, une application Rails ne peut gérer qu’une requête à la fois. Pour paralléliser le traitement des requêtes, Passenger créé des processus pour chaque nouvelle requête, dans lesquels l’application est lancée à nouveau.
passenger-request_load_balancing
Un processus ne peut donc traiter qu’une requête à la fois, mais Passenger n’attribue pas de limite de création de processus et il s’assure qu’au moins un processus reste toujours actif par application. Les processus inactifs depuis plus de 5 minutes sont éteints.

En revanche, Passenger définit une limite globale du nombre de processus actifs, par défaut de 6 processus, qui fait autorité sur tout autre paramètre de configuration. Cela signifie qu’avec 7 applications, Passenger passera son temps à jongler entre les processus actifs pour n’en garder que 6, éteignant un processus pour en démarrer un autre.

Par défaut, si une action telle qu’un import de données est lancée simultanément sur 6 applications, les 6 processus actifs ne peuvent plus être « tués » car ils sont en cours de traitement long et la queue va se remplir très rapidement. Le paramètre « passengermaxpoolsize » est donc à configurer en fonction du nombre d’applications que Passenger gère, selon vos besoins et la capacité du serveur.

Pour ne pas manquer de mémoire vive, la documentation conseille de garder 25% de la RAM totale pour l’OS. On divise les 75% de RAM restants par la consommation en RAM totale de toutes les applications (attention aux unités), ce qui indique combien de processus le serveur est en théorie capable d’exécuter.

Il est possible de surveiller la consommation en ressources de chaque application via la commande passenger-status. On y trouve la longueur courante de la queue, bien qu’il ne soit pas possible de la visualiser. Il est cependant possible d’afficher les requêtes en cours de traitement avec passenger-status --show=requests.

Programmation asynchrone en Ruby : présentation de la gem concurrent-ruby (Partie 3)

Après avoir découvert les outils natifs en Ruby, nous allons passer en revue la gem concurrent-ruby. Cette gem affirme être la seule avec un modèle de gestion de la mémoire réalisant des opérations atomiques, garantissant un comportement thread-safe pour toutes ses fonctionnalités.

concurrent-ruby est principalement un framework permettant de manipuler des threads de différentes saveurs.

Le module Async

« Lorsque ce module est inclus dans une classe, les objets de la classe deviennent intrinsèquement asynchrones. Chaque objet obtient son propre thread d’arrière-plan dans lequel réaliser ses appels de méthodes asynchrones. Les appels de méthodes asynchrones sont exécutés un par un dans l’ordre où ils sont reçus. »

require "concurrent"
 
class Demonstration
  include Concurrent::Async
 
  def dire_3s_plus_tard(phrase)
    sleep 3
    puts phrase
  end
end
 
demo = Demonstration.new
 
# Exemple synchrone, non thread-safe
demo.dire_3s_plus_tard("Premier")
puts "Second"
 
# Exemple asynchrone, non bloquant, thread-safe
demo.async.dire_3s_plus_tard("Second")
puts "Premier"
 
# Exemple synchrone, bloquant, thread-safe
demo.await.dire_3s_plus_tard("Premier")
puts "Second"

« Note importante : la garantie thread-safe tient seulement quand les appels de méthodes asynchrones ne sont pas mélangés à des appels directs aux méthodes. Utilisez uniquement des appels directs aux méthodes quand l’objet est utilisé exclusivement par un seul processus. Utilisez uniquement async et await quand l’objet est partagé entre plusieurs threads. »

La classe Future

« Un objet Future représente la promesse d’achever une action à un moment donné dans l’avenir. L’idée derrière une Future est d’envoyer une opération dans une exécution asynchrone, continuer à faire d’autres choses, puis retourner et récupérer le résultat de l’opération asynchrone à un moment ultérieur. »

require 'concurrent'
require 'open-uri'
 
class Crawler
  # Ma méthode qui prend du temps...
  def lire_mon_ip_publique
    uri = "http://canihazip.com/s"
    html = open(uri) {|f| f.collect{|line| line.strip } }
    html[0] # parce que open-uri renvoie un tableau
  end
end
 
ma_future = Concurrent::Future.execute{ Crawler.new.lire_mon_ip_publique }
ma_future.state #=> :pending
ma_future.pending? #=> true
ma_future.value(0) #=> nil (ne bloque pas)
 
sleep(3) # faire d'autres trucs
 
ma_future.state #=> :fulfilled
ma_future.fulfilled? #=> true
ma_future.value #=> "8.8.8.8"

Notez qu’on peut aussi appeler Future.new à la place de Future.execute et .execute plus tard sur l’objet pour en différer le départ. Les Futures ont différents états tout au long de leur vie (:unscheduled, :pending, :processing, :rejected et :fulfilled) et sont considérées complétées lorsque l’état est soit :rejected soit :fulfilled.

Il existe même un outil dataflow permettant d’exécuter du code uniquement lorsque plusieurs Futures sont considérées complétées.

La classe Promise

C’est une implémentation complètement inspirée de JavaScript. « Les Promises sont semblables aux Futures et partagent beaucoup des mêmes comportements. Les Promises sont cependant beaucoup plus robustes. Elles peuvent être chaînées dans une structure en arbre où chaque Promise peut avoir zéro ou plus d’enfants. Les Promises sont chaînées en utilisant la méthode then. Le résultat d’un appel à then est toujours une autre Promise. Les Promises sont résolues de manière asynchrone (par rapport au fil principal) mais dans un ordre strict : les parents sont garantis d’être résolus devant leurs enfants, les enfants avant leurs frères et sœurs plus jeunes. La méthode then prend deux paramètres: un bloc facultatif à exécuter lors de la résolution du parent et un appel facultatif à exécuter en cas d’échec du parent. Le résultat de chaque promesse est transmis à chacun de ses enfants jusqu’à la résolution de l’arbre. Quand une promesse est rejetée, tous ses enfants seront rejetés et recevront la raison. »

Il faut reconnaitre que c’est touffu. En même temps ça fait plus de choses que les Futures :

require 'concurrent'
 
ma_promesse = Concurrent::Promise.new{10}.then{ |x| x * 2 }.then{ |resultat| sleep 5; resultat + 10 }
ma_promesse.execute # comme pour les Futures, c'est quand on veut
ma_promesse.state #=> :pending
sleep 3
ma_promesse.state #=> :pending
sleep 3
ma_promesse.state #=> :fulfilled
ma_promesse.value #=> 30
ma_promesse.complete? #=> true

Sachez que le retour de la dernière opération d’un then sera passé à la Promise suivante, attention inline.

ma_promesse = Concurrent::Promise.execute{ raise StandardError.new("J'ai tout cassé !") }
sleep(0.1)
 
ma_promesse.state     #=> :rejected
ma_promesse.rejected? #=> true
ma_promesse.reason    #=> "#<StandardError: J'ai tout cassé !>"

Les états et les méthodes des Promises sont semblables à ceux des Futures (:unscheduled, :pending, :processing, :rejected et :fulfilled, .complete? etc).

Et au delà

Il existe tout un tas d’outils que je vais me garder de présenter en détail, ce serait trop long. Il existe des classes pour planifier des tâches et appeler du code périodiquement, des structures de données thread-safe (array, hash, map, tuple et une tonne d’autres) et plus encore.

La gem concurrent-ruby-edge va encore plus loin avec des fonctionnalités instables qui sont en train d’être regroupées dans un framework. Mais celle là ne sera pas prête d’être prod-ready avant un moment.