crossbar – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Nouvelle release de crossbar: historique des events et crypto http://sametmax.com/nouvelle-release-de-crossbar-historique-des-events-et-crypto/ http://sametmax.com/nouvelle-release-de-crossbar-historique-des-events-et-crypto/#comments Sun, 31 Jan 2016 18:25:33 +0000 http://sametmax.com/?p=18018 Crossbar, le routeur WAMP, passe en 0.12.]]> Je suis méga à la bourre. Entre les tickets github qui s’accumulent, les comments auxquels j’ai toujours pas répondu et la liste d’articles à écrire qui augmente au lieu de diminuer (mais comment, bordel, comment ?), j’ai vraiment du mal à suivre.

Je dois toujours un article sur Polymer + Crossbar à Tavendo. Et il faut que je fasse un tuto sur l’authentification également (en attendant, y a des exemples plus à jour).

Fichtre.

En attendant, je vais en profiter pour faire un article vite fait sur la dernière release, puisque Crossbar, le routeur WAMP, passe en 0.12.

Comme d’hab, correction de bugs, amélioration du support de Python 3, plus de docs et d’exemples, blablabla…

Mais ce qui est vraiment intéressant, c’est l’historique des évènements.

Normalement un évènement est éphémère, dans le sens où une fois qu’il a été propagé, vous ne pouvez plus le recevoir. Si vous arrivez après la fête, c’est terminé.

C’est un problème, par exemple si vous redémarrez un client qui a besoin de ces évènements. Ou si vous implémentez un client qui veut savoir ce qui vient de se passer avant de se pointer, comme dans le cas d’un chat : on veut avoir les derniers messages postés.

Par défaut, l’historique n’est pas activé, puisqu’il y un coût pour chaque pub/sub. On doit explicitement le demander pour chaque event dans le fichier de config :

{
   "name": "realm1",
   "roles": [
   ],
   "store": {
      "type": "memory", # ou stocker l'historique
      "event-history": [
         {
            "uri": "mon.uri.pour.un.event", # quel type event
            "limit": 10000 # combien d’events stocker
         }
      ]
   }
}

type n’accepte pour le moment que memory, qui est une simple liste en mémoire dans crossbar, et bouffe donc de la RAM. On perd aussi l’historique au redémarrage du routeur, mais ça a l’avantage d’être très rapide.

Pour la prochaine version, Tavendo va implémenter un stockage dans une base lmdb et si ils font une belle API, on peut s’attendre à voir fleurir des backends pour SQLAlchemy, Django, Redis, etc.

event-history prend un liste d’events (les URIs peuvent utiliser les jokers introduits dans la version précédente), on met la limite du nombre total d’events à stocker pour cet URI en particulier.

Pour profiter de l’historique côté client, il faut obligatoirement avoir un abonnement à un event dont les messages sont sauvegardés. On ne peut pas récupérer l’historique de messages auxquels on n’est pas abonnés : forcer l’abonnement oblige en effet le client à passer le check des permissions.

Par exemple, en JS, d’abord on s’inscrit:

var promise = session.subscribe('mon.uri.pour.un.event',
   function (args, kwargs, details) {
      // bon là vous faites bien ce que vous voulez avec les nouveaux events
      //, car ça n’a rien à voir avec l’historique
   }
)

Puis on demande les events:

promise = promise.then(function (sub) {
      // L’abonnement retourne un objet "subcription" qui possède l’id
      // dont on a besoin pour demander l’historique des events.
      // On fait un petit RPC sur la meta API 'wamp.subscription.get_events'
      // qui demande aux routeurs tous les X derniers events qui matchent
      // notre abo. Ici x = 10
      return session.call('wamp.subscription.get_events', [sub.id, 10]);
)

Et enfin, on a droit à l’histo:

promise.then(function (history) {
    console.log(history.length, " events:");
    history.forEach(function(event){
        console.log(event.timestamp, event.publication, event.args[0]);
    })
 });

En Python, le code pour récupérer l’histo est logiquement:

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner


class Component(ApplicationSession):

    async def onJoin(self, details):


        def on_event(i):
            print("Got: {}".format(i))

        # pareil on chope la souscription
        sub = await self.subscribe(on_event, u'mon.uri.pour.un.event')
        # et on demande la liste des 10 derniers events pour cet abo
        events = await self.call('wamp.subscription.get_events', sub.id, 10)
        # et on boucle. Et on kiff await parceque putain c’est pratique.
        for event in events:
            print(event['timestamp'], event['publication'], event['args'][0])

    def onDisconnect(self):
        asyncio.get_event_loop().stop()


if __name__ == '__main__':
    runner = ApplicationRunner("ws://127.0.0.1:8080/ws", 'realm1')
    runner.run(Component)

L’autre point phare de cette release, c’est la dépréciation de Mozilla Persona comme méthode d’authentification (le projet est dead) et la promotion de deux nouvelles méthodes: les certificats TLS et les paires de clés publiques/privées (Curve25519 pour le moment).

C’est une très bonne nouvelle, car ça veut dire plus de mots de passe dans les fichiers de configuration en production pour identifier vos propres clients qui ont forcément des privilèges supplémentaires.

Je reviendrais là dessus en faisant le tuto sur l’authentification.

Une release chouette donc. Mais qui introduit plein de dépendances à des extensions en C qui pourraient être optionnelles, ce qui rend l’installation plus compliquée. Je suis en train de discuter avec la team pour voir si on peut arranger ça, mais Tobias à l’air plutôt pour les garder. Si vous aussi vous voulez garder la simplicité de la base pure Python, rejoignez la discussion.

Enfin, on a pu voir l’annonce d’une feature très intéressante : le chiffrement end-to-end des messages WAMP. Ça, c’est chouette. C’est pas encore implémenté, mais ça veut dire que la prochaine release, vous pourrez probablement envoyer des messages à travers le serveur WAMP sans que celui-ci puisse les lire.

]]>
http://sametmax.com/nouvelle-release-de-crossbar-historique-des-events-et-crypto/feed/ 8 18018
Nouvelle release de crossbar : support de Python 3 ! http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/ http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/#comments Wed, 09 Sep 2015 15:13:22 +0000 http://sametmax.com/?p=16908 WAMP en général, et je me suis fais un plaisir de leur rapporter toutes les merdes donc vous m'avez fait part.]]> L’équipe de Tavendo est à l’écoute de toutes les critiques de Crossbar et WAMP en général, et je me suis fait un plaisir de leur rapporter toutes les merdes dont vous m’avez fait part.

Cette nouvelle release contient beaucoup de choses qui corrigent ou pallient un paquet de trucs relou dans le routeur Crossbar (et par conséquent la lib client Autobahn) :

  • Support officiel de Python 3. Yes. Yes, yes yes !
  • Le debug a été complètement revu : meilleure console, meilleur login, meilleurs messages d’erreur et meilleur comportement en cas d’exceptions.
  • Un service dédié à l’upload de fichier intégré.
  • Un bridge HTTP complet qui permet d’utiliser Crossbar depuis n’importe quelle app qui peut faire des requêtes HTTP.

Pour la suite, ils travaillent sur la doc, et l’amélioration de l’API. En attendant, on peut pip install crossbar et profiter de ces nouveautés sans avoir à passer par github.

De mon côté j’ai un article sur l’authentification avec Crossbar dans les cartons. ETA dans les 10 prochains jours.

]]>
http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/feed/ 18 16908
Today is a glorious day http://sametmax.com/today-is-a-glorious-day/ http://sametmax.com/today-is-a-glorious-day/#comments Sun, 12 Jul 2015 21:29:59 +0000 http://sametmax.com/?p=16600 >>> import crossbar >>> crossbar.__version__ '0.10.4' >>> import twisted >>> twisted.__version__ '15.2.1' >>> import sys >>> print('Wait for it...') Wait for it... >>> sys.version '3.4.0 (default, Apr 11 2014, 13:05:11) \n[GCC 4.8.2]' ]]> http://sametmax.com/today-is-a-glorious-day/feed/ 15 16600 Pendant ce temps, à Vera Cruz http://sametmax.com/pendant-ce-temps-a-vera-cruz/ http://sametmax.com/pendant-ce-temps-a-vera-cruz/#comments Sun, 10 May 2015 09:29:24 +0000 http://sametmax.com/?p=16198 Pour une fois, ce n’est pas un article payé par Tavendo, mais bien un truc que je ponds par enthousiasme :)

Pendant qu’on en parle pas, la stack WAMP continue d’évoluer, des mises à jours significatives ayant été apportées à Crossbar.io, ainsi qu’aux libs Python et JS d’autobahn. Parmi les plus intéressantes :

  • Le code passe de la licence Apache 2 à la licence MIT, augmentant la compatibilité avec un tas d’autres licences.
  • On peut faire un SUB avec un joker, et donc lier un seul callback à plusieurs événements.
  • On peut faire un register avec un joker également.
  • On peut choisir la stratégie à appliquer si plusieurs registers sont faits sur le même nom.
  • Une meta API permet d’être prévenu quand un client fait quelque chose ou de demander l’état des nœuds en cours.

Inutile de dire que c’est trop cool.

Pour profiter de tout ça, il suffit de faire :

pip install crossbar autobahn --upgrade

Et de télécharger la nouvelle version de la dernière version de la lib JS.

Licence MIT

Auparavant le travail de Tavendo était essentiellement sous Licence Apache. Une licence libre, certes, mais qui pouvait poser problème quand on mélangeait tout ça avec d’autres licences (par exemple, elle n’est pas compatible avec la GPL2). Avec la version 0.10, le code est maintenant sous licence MIT, beaucoup plus permissive.

Joker pour les subs

Supposez que vous faites un système de jeu d’échec donc chaque coup déclenche un événement “chess.game.[id_de_partie]”. C’est pratique, car seuls les clients intéressés à cette partie vont recevoir les événements. Mais si votre serveur doit enregistrer un log de tous les coups d’une partie, il faut que chaque client envoie AUSSI les coups au serveur explicitement.

C’était en tout cas vrai avant cette mise à jour, puisque maintenant on peut spécifier des jokers dans les noms des topics au moment de l’abonnement.

Essentiellement il y a deux modes.

Le mode “prefix”, qui match tous les events qui commencent par ce nom :

session.subscribe("debut.du.nom.du.topic", callback, { match: "prefix" });
# matchera debut.du.nom.du.topic.genial et debut.du.nom.du.topic.trop.cool

Et le mode “wildcard” qui permet, un peu comme les glob Unix (mais on utilise “..” au lieu de “*””), de faire un texte à trou :

session.subscribe("nom.du.topic..general", callback, { match: "wildcard" });
# matchera "nom.du.topic.moins.general" et "nom.du.topic.oui.mon.general"

Tous les callbacks qui matchent un topic seront appelés.

Plusieurs clients pour la même procédure

On peut utiliser le même principe que pour les sub avec joker, mais pour les procédures.

session.register("debut.du.nom.de.la.procedure", callback, { match: "prefix" });    
session.register("nom.de.la.procedure..generale", callback, { match: "wildcard" });

La différence avec le subscribe, c’est que seule UNE procédure est appelée. Dans les cas simples, un match exact prend le dessus sur un prefix (et le plus long prefix gagne toujours), qui prend le dessus sur un wildcard. Crossbar n’implemente pas encore de résolution pour deux wildcards en conflits, et je ne sais pas ce qu’il fait dans ce cas.

Il est aussi possible de de définir des règles d’appels en faisant :

session.register("nom.de.la.procedure..generale", procedure1, { invoke: "regle"});

La règle peut être :

  • roundrobin: on prend la liste de clients, on regarde le dernier appelé, et on utilise le suivant.
  • random: on prend un client au hasard.
  • last: on prend le dernier client ajouté de la liste.
  • first: on prend premier client ajouté à la liste.

“roundrobin” et “random” sont pratiques pour faire du load balancing.

“last” et “first” sont pratique pour les mises à jour d’un client sans arrêter le serveur. En gros on rajoute un client, on attend un peu, “last” route tout sur le dernier client, donc le nouveau client prend les requêtes, et on peut arrêter le vieux clients sans souci.

Meta RPC

Crossbar met automatiquement à notre disposition des procédures distantes toutes faites qui donnent des informations sur l’état des clients et du routeur. Voici les RPC que vous pouvez maintenant faire :

  • wamp.session.list: lister les sessions des clients connectés au routeur.
  • wamp.session.get: obtenir les infos d’un session pour un ID en particulier.
  • wamp.session.count: obtenir le nombre de client connectés.
  • wamp.registration.lookup: absolument aucune idée.
  • wamp.registration.get: obtenir des infos sur une procédure distante enregistrée.
  • wamp.registration.list_callees: lister les clients ayant enregistré pour une procédure avec ce nom.
  • wamp.registration.count_callees: compter les clients ayant enregistré une procédure avec ce nom.
  • wamp.registration.list: lister toutes les procédures distantes disponibles.
  • wamp.registration.remove_callee: virer un client de la liste de des clients enregistrés pour cet procédure.
  • wamp.subscription.lookup: toujours aucune idée.
  • wamp.subscription.get: récupérer des infos sur l’abonnement avec cet ID.
  • wamp.subscription.list_subscribers: lister les clients qui sont abonnés à ce sujet.
  • wamp.subscription.count_subscribers: compter les clients abonnés à ce sujet.
  • wamp.subscription.match: aucune idée.
  • wamp.subscription.list: lister tous les sujets d’abonnement disponibles.
  • wamp.subscription.remove_subscriber:
  • virer un client de la liste des abonnés à ce sujet.

En gros, si vous voulez faire une admin qui vous permet de killer certains client ou rechercher si des events existent, vous utilisez ça.

Meta SUB

De même, le routeur envoie maintenant des publications sur des sujets concernant le cycle son cycle de vie et celui des clients. On peut donc s’abonner à ces meta topic pour réagir à l’activité de son système :

  • wamp.session.on_join : un client s’est connecté au routeur.
  • wamp.session.on_leave : un client s’est déconnecté du routeur.
  • wamp.subscription.on_create : un nouveau topic existe.
  • wamp.subscription.on_subscribe : un client s’est abonné à un topic.
  • wamp.subscription.on_unsubscribe : un client s’est désabonné à un topic.
  • wamp.subscription.on_delete : un topic est retiré de la liste des topics disponibles.
  • wamp.registration.on_create : une procédure distante porte ce nom pour la première fois.
  • wamp.registration.on_register : un client propose ajoute un callable pour ce nom de procédure distante..
  • wamp.registration.on_unregister : un client retire son callable pour ce nom de procédure distante.
  • wamp.registration.on_delete : le nom de cette procédure n’a plus aucun callable lié.
  • wamp.schema.on_define : aucune idée.
  • wamp.schema.on_undefine : kamolox.

Ce genre de truc est idéal pour faire un petit outil de monitoring pour son archi et voir ce qui se passe en temps réel.

Le HTTP bridge est complet

Le bridge HTTP propose maintenant PUB/SUB, et tout RPC. On peut donc maintenant utiliser crossbar depuis n’importe quel app qui peut faire du HTTP : flask, pyramid, ruby on rails, du PHP pur, wget en ligne de commande et tout le bordel. C’est plus verbeux, mais ça dépanne bien.

]]>
http://sametmax.com/pendant-ce-temps-a-vera-cruz/feed/ 8 16198
Les managers le détestent : faites tourner WAMP dans Django avec cette astuce insolite http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/ http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/#comments Sun, 04 Jan 2015 19:45:07 +0000 http://sametmax.com/?p=15665 directement dans Django.]]> Il existe une lib appelée crochet qui permet de faire marcher des API de twisted entre deux bouts de code bloquants. Certes, ça ne marche qu’en 2.7 et c’est pas hyper performant, mais on peut faire des trucs mignons du genre cette démo qui mélange flask et WAMP.

C’est du pur Python, pas de process externe à gérer, c’est presque simple.

Bref, si on veut utiliser WAMP avec une app synchrone comme flask, c’est un bon moyen de s’y mettre. On aura jamais des perfs fantastiques, mais on peut pusher vers le browser.

Du coup je me suis demandé si on pouvait faire ça avec Django.

Évidement, ça a été un peu plus compliqué car par défaut runserver lance plusieurs workers et fait un peu de magie avec les threads. Mais après un peu de bidouillage, ça marche !

On peut utiliser WAMP, directement dans Django.

Suivez le guide

D’abord, on installe tout le bouzin (python 2.7, souvenez-vous) :

pip install crossbar crochet django

Il vous faudra un Django 1.7, le tout dernier, car il possède une fonctionnalité qui nous permet de lancer du code quand tout le framework est chargé.

Vous vous faites votre projet comme d’hab, et vous ouvrez le fichier de settings et au lieu de mettre votre app dans INSTALLED_APPS, vous rajoutez ça :

INSTALLED_APPS = (
    '...',
    'votreapp.app.VotreAppConfig'
)

Puis dans le module de votre app, vous créez un fichier app.py, qui va contenir ça:

# -*- coding: utf-8 -*-

import crochet

from django.apps import AppConfig

# On charge l'objet contenant la session WAMP définie dans la vue
from votreapp.views import wapp

class VotreAppConfig(AppConfig):
    name = 'votreapp'
    def ready(self):
        # On dit a crochet de faire tourner notre app wamp dans sa popote qui
        # isole le reactor de Twisted
        @crochet.run_in_reactor
        def start_wamp():
           # On démarre la session WAMP en se connectant au serveur
           # publique de test
           wapp.run("wws://demo.crossbar.io/ws", "realm1", start_reactor=False)
        start_wamp()

On passe à urls.py dans lequel on se rajoute des vues de démo :

    url(r'^ping/', 'votreapp.views.ping'),
    url(r'^$', 'votreapp.views.index')

Puis dans notre fichier views.py, on met :

# -*- coding: utf-8 -*-

import uuid

from django.shortcuts import render

import crochet

# Crochet se démerde pour faire tourner le reactor twisted de
# manière invisible. A lancer avant d'importer autobahn
crochet.setup()

from autobahn.twisted.wamp import Application

# un objet qui contient une session WAMP
wapp = Application()

# On enrobe les primitives de WAMP pour les rendre synchrones
@crochet.wait_for(timeout=1)
def publish(topic, *args, **kwargs):
   return wapp.session.publish(topic, *args, **kwargs)

@crochet.wait_for(timeout=1)
def call(name, *args, **kwargs):
   return wapp.session.call(name, *args, **kwargs)

def register(name, *args, **kwargs):
    @crochet.run_in_reactor
    def decorator(func):
        wapp.register(name, *args, **kwargs)(func)
    return decorator

def subscribe(name, *args, **kwargs):
    @crochet.run_in_reactor
    def decorator(func):
        wapp.subscribe(name, *args, **kwargs)(func)
    return decorator

# Et hop, on peut utiliser nos outils WAMP !

@register('uuid')
def get_uuid():
    return uuid.uuid4().hex

@subscribe('ping')
def onping():
    with open('test', 'w') as f:
        f.write('ping')

# Et à côté, quelques vues django normales

def index(request):
    # pub et RPC en action côté Python
    publish('ping')
    print call('uuid')

    with open('test') as f:
        print(f.read())
    return render(request, 'index.html')

def ping(request):
    return render(request, 'ping.html')

Après, un peu de templating pour que ça marche…

Index.html :

{% load staticfiles %}


  
    
    
       UUID
    

    
    


UUID

ping.html :

{% load staticfiles %}


  
    
    
       Ping
    

    
    


Ping me !

On ouvre la console, on lance son routeur :

    crossbar init
    crossbar start

On lance dans une autre console son serveur Django :

./manage.py runserver

Et si on navigue sur http://127.0.0.1:8000, on récupère un UUID tout frais via RCP.

On peut aussi voir dans le shell que ça marche côté Python :

94cfccf0899d4c42950788fa655b65ed
ping

D’ailleurs un fichier nommé “test” est créé à la racine du projet.

Et si on navigue sur http://127.0.0.1:8000/ping/ et qu’on refresh http://127.0.0.1:8000 plusieurs fois, on voit la page se mettre à jour.

Achievement unlock : use WAMP and Django code in the same file.

A partir de là

Il y a plein de choses à faire.

On pourrait faire une lib qui wrap tout ça pour pas à avoir à le mettre dans son fichier de vue et qui utilise settings.py pour la configuration.

Il faut tester ça avec des setups plus gros pour voir comment ça se comporte avec gunicorn, plusieurs workers, le logging de Django, etc. Je suis à peu près sûr que les callbacks vont être registrés plusieurs fois et ça devrait faire des erreurs dans les logs (rien de grave ceci dit).

On pourrait aussi adapter le RPC pour qu’il utilise les cookies d’authentification Django, et pouvoir les protéger avec @login_required.

Mais un monde d’opportunités s’offrent à vous à partir de là.

Moi, ça fait 6 h que je taffe dessus, je vais me pieuter.


Télécharger le code de l’article

]]>
http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/feed/ 16 15665
Corrections des slides WAMP http://sametmax.com/corrections-des-slides-wamp/ http://sametmax.com/corrections-des-slides-wamp/#comments Thu, 25 Dec 2014 09:58:47 +0000 http://sametmax.com/?p=13011 Suite aux commentaires, j’ai fais une refonte des dispos :

  • Plus d’insistance sur la différence entre RPC et PUB/SUB.
  • Les exemples sont amenés plus tôt, et les schémas sont en premier.
  • Des lourdeurs et des redondances sont supprimées.
  • J’ai ajouté des réponses à quelques questions posées : perf, sécu, etc.

Histoire d’éviter d’éparpiller des versions partout, je l’ai juste réup au même endroit.

Merci, donc, pour toutes les remarques qui ont significativement permises d’améliorer la prez.

]]>
http://sametmax.com/corrections-des-slides-wamp/feed/ 8 13011
Full disclosure http://sametmax.com/full-disclosure/ http://sametmax.com/full-disclosure/#comments Tue, 16 Dec 2014 15:32:26 +0000 http://sametmax.com/?p=12883 Depuis quelques jours je suis en discussion avec Tobias de Tavendo. Comme vous avez pu le remarquer avec mes précédents articles sur WAMP et Crossbar :

  • Ils sont bons techniquement, et nuls pour expliquer ce qu’ils ont techniqué.
  • Cette techno est une techno de rêve pour moi. J’y crois à mort.
  • Je suis le seul à avoir pondu des explications décentes sur WAMP et Crossbar. Et ça n’a pas suffit à faire battre un cil.

Bref, ils ont embauché des mecs de haute voltige pour la technique (du genre un contributeur PyPy). Et ils m’ont contacté pour me demander si je n’étais pas chaud pour faire de l’évangélisme, rémunéré, autour de WAMP, Autobahn et Crossbar.

L’idée : écrire des tutos, des articles, améliorer la doc, répondre sur le chan IRC, etc.

J’adore le concept, vu que j’aime leur projet et que je le faisais gratos avant, surtout qu’ils sont pas trop contraignants sur le temps que je vais passer dessus.

Donc voilà le deal : quand je vais pondre des tutos et des articles sur WAMP et Co, je vais d’abord les faire en français ici. Comme ça j’aurai les retours des lecteurs du blog qui pourront, comme d’habitude, me faire part de leurs douces remarques sur à quel point on ne pige rien.

Une fois la prose aiguisée, je traduis et je publie chez Tavendo.

Je disclose donc ici que vous verrez peut-être des prochaines rédactions qui seront attachées à une activité pro. Pas impartial donc. Mais bon, depuis quand je suis impartial ? Javascript c’est de la merde, et je préfère les rousses.

Par saucisse d’honnêteté, je signalerai chaque choucroute concernée avec un lien vers ce post.

Enfin, le contrat est pas signé encore, mais vu que je vais commencer à taffer dessus aujourd’hui, je pense à une première publication demain sous la forme d’un slide show expliquant avec de jolies diapos ce que sont WAMP, Autobahn et Crossbar. À quoi ça sert et ce qu’on peut faire avec.

]]>
http://sametmax.com/full-disclosure/feed/ 28 12883
Petite démo pragmatique d’un usage de WAMP en Python http://sametmax.com/introduction-a-wamp-en-python/ http://sametmax.com/introduction-a-wamp-en-python/#comments Thu, 26 Jun 2014 07:27:03 +0000 http://sametmax.com/?p=11146 L’API a changé depuis, j’ai donc mis à jour l’article pour refléter ces changements

Vu que dernièrement je vous ai bien gavé avec WAMP, ça mérite un tuto non ?

Il se trouve que l’équipe derrière WAMP a publié plus tôt que prévu une version de leurs libs contenant l’API flaskesque sur laquelle on bosse. L’idée est que même si on n’a pas encore les tests unitaires, on peut déjà jouer avec.

Maintenant il me fallait un projet sexy, histoire de donner envie. Donc j’ai fouillé dans ce qui se faisait côté temps réel (essentiellement du NodeJS et du Tornado, mais pas que) pour trouver l’inspiration.

Et j’ai trouvé un truc très sympa : un player vidéo piloté à distance.

En effet, n’est-il pas chiant de regarder une vidéo en ligne sur son ordi posé sur la commode pendant qu’on est enfoncé dans le canap ? Si on veut faire pause ou changer le son, il faut se lever, arg.

Les problèmes du tiers monde, c’est du pipi de chat à côté. Ils ont de la chance, eux, ils ne connaissent pas le streaming.

Voici donc le projet :

Une page avec un player HTML 5 et un QR code.

Capture d'écran de la démo, côté player

Pour simplifier la démo, on peut cliquer sur le QR code et avoir la télécommande dans un autre tab pour ceux qui n’ont pas de smartphone ou d’app de scan de QRCode.

Si on scanne le QR code avec son téléphone, il vous envoie sur une page avec une télécommande pour contrôler le player sans bouger votre cul :

Capture d'écrand de la démo, côté contrôles

Évidement, c’est basique. Je vais pas m’amuser à faire un produit complet juste pour un truc dont le code source ne sera même pas regardé par la plupart d’entre vous. Je vous connais, bandes de feignasses !

Et vous allez voir, c’est même pas dur à faire.

Démo en ligne:

La démo

Vous pouvez télécharger le code ici.

Pour comprendre ce qui va suivre, il va vous falloir les bases en prog Javascript et Python, ainsi que bien comprendre la notion de callback. Être à l’aise avec promises peut aider.

Et pour bien digérer ce paté, rien ne vaut un peu de son :

Le Chteumeuleu

Il va nous falloir deux pages Web, une pour le player vidéo, et une pour la télécommande.

Le player :





   Video
   
   
   
   

   






Et la télécommande :





  Télécommande
  
  
  
  
  


  

Rien d’incroyable. C’est du HTML, un peu de CSS, on charge les dépendances en JS. Classique.

Vu qu’on utilise des ressources hotlinkées par souci de simplicité, il vous faudra être connecté à Internet.

Setup du routeur

On va travailler avec Python 2.7 puisque Crossbar.io est uniquement en 2.7 et que je n’ai pas envie de vous faire installer deux versions de Python juste pour le tuto.

Il nous faut avant tout un serveur HTTP pour servir les fichiers HTML et un routeur WAMP. On installe donc Crossbar.io :

pip install crossbar

Ca va aussi installer autobahn, twisted et tout le bordel.

On va ensuite dans le dossier qui contient ses fichiers HTML, et on créé le fichier de config de Crossbar.io avec un petit :

crossbar init

Vous noterez la création d’un dossier .crossbar qui contient un fichier config.json. C’est la config de crossbar. Videz moi ce fichier, on va le remplir avec notre config :

{
   "workers": [
      {
         "type": "router",
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],
         "transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080,
                  "interface": "0.0.0.0"
               },
               "paths": {
                  "/": {
                     "type": "static",
                     "directory": ".."
                  },
                  "ws": {
                     "type": "websocket",
                  }
               }
            }
         ]
      }
   ]
}

Crossbar est en effet un gestionnaire de processus : il ne gère vraiment rien lui même. Il démarre d’autres processus, appelés workers, à qui il délègue le travail.

On définit dans ce fichier de config quels processus (les workers) lancer quand Crossbar.io démarre. Les valeurs qu’on utilise disent de créer un seul worker de type “router”, c’est à dire un worker capable de gérer les entrées et les sorties WAMP. Hey oui, le routeur n’est qu’un worker comme les autres :)

Il y a d’autres sortes de workers, mais aujourd’hui on s’en branle.

Dans notre config du worker router, on crée d’abord un realm, qui est juste un namespace avec des permissions. Si un client WAMP se connecte à ce routeur, il doit choisir un realm (qui est juste un nom), et il ne peut parler qu’avec les clients du même realm. C’est une cloture quoi.

Dans un realm, on définit des roles qui déclarent quelles opérations PUB/SUB et RPC on a le droit de faire. Ici on dit que tout le monde (anonymous) a le droit de tout faire sur toutes les urls (“uri”: ‘*”) histoire de pas se faire chier. Si on met en prod, évidement on va se pencher sur la sécurité et faire ça plus proprement.

"realms": [
{
   "name": "realm1",
   "roles": [
      {
         "name": "anonymous",
         "permissions": [
            {
               "uri": "*",
               "publish": true,
               "subscribe": true,
               "call": true,
               "register": true
            }
         ]
      }
   ]
}
],

Puis on définit les transports, c’est à dire sur quoi notre worker va ouvrir ses oreilles pour écouter les messages entrant :

"transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080,
                  "interface": "0.0.0.0"
               },
               "paths": {
                  "/": {
                     "type": "static",
                     "directory": ".."
                  },
                  "ws": {
                     "type": "websocket",
                  }
               }
            }
        ]

Encore une fois on en déclare un seul, de type “web”. Ce transport peut écouter HTTP et Websocket sur le même port. On lui dit d’écouter sur “0.0.0.0:8080” :


“endpoint”: {
“type”: “tcp”,
“port”: 8080,
“interface”: “0.0.0.0”
},

Ensuite on dit que si quelqu’un arrive sur “/”, on sert en HTTP les fichiers statiques histoire que nos pages Web soient servies :

"/": {
 "type": "static",
 "directory": ".."
},

Si on arrive sur “/ws”, on route les requêtes WAMP via Websocket :

"ws": {
 "type": "websocket",
}

Le routeur est prêt, on lance Crossbar.io :

$ crossbar start
2015-01-07 20:02:55+0700 [Controller  26914] Log opened.
2015-01-07 20:02:55+0700 [Controller  26914] ============================== Crossbar.io ==============================
    
2015-01-07 20:02:55+0700 [Controller  26914] Crossbar.io 0.9.12-2 starting
2015-01-07 20:02:55+0700 [Controller  26914] Running on CPython using EPollReactor reactor
2015-01-07 20:02:55+0700 [Controller  26914] Starting from node directory /home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar
2015-01-07 20:02:55+0700 [Controller  26914] Starting from local configuration '/home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar/config.json'
2015-01-07 20:02:55+0700 [Controller  26914] Warning, could not set process title (setproctitle not installed)
2015-01-07 20:02:55+0700 [Controller  26914] Warning: process utilities not available
2015-01-07 20:02:55+0700 [Controller  26914] No WAMPlets detected in enviroment.
2015-01-07 20:02:55+0700 [Controller  26914] Starting Router with ID 'worker1' ..
2015-01-07 20:02:55+0700 [Controller  26914] Entering reactor event loop ...
2015-01-07 20:02:55+0700 [Router      26917] Log opened.
2015-01-07 20:02:55+0700 [Router      26917] Warning: could not set worker process title (setproctitle not installed)
2015-01-07 20:02:55+0700 [Router      26917] Running under CPython using EPollReactor reactor
2015-01-07 20:02:56+0700 [Router      26917] Entering event loop ..
2015-01-07 20:02:56+0700 [Router      26917] Warning: process utilities not available
2015-01-07 20:02:56+0700 [Controller  26914] Router with ID 'worker1' and PID 26917 started
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': realm 'realm1' started
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': role 'role1' started on realm 'realm1'
2015-01-07 20:02:56+0700 [Router      26917] Site starting on 8080
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': transport 'transport1' started

Setup du client

Pour cette démo, le serveur n’a pas grand chose à faire. On pourrait en fait la faire sans aucun code Python, mais ça va nous simplifier la vie et donner un peut de grain à moudre pour le tuto.

En effet, on a deux problématiques que le serveur va résoudre facilement pour nous : créer un ID unique pour le player et récupérer l’IP sur le réseau local.

L’ID, c’est simplement que si plusieurs personnes lancent en même temps un player, on ne veut pas que les télécommandes puissent lancer un ordre à un autre player que le sien. On pourrait utiliser un timestamp, mais ils sont contigus, n’importe quel script kiddies pourrait faire un script pour foutre la merde. On va donc créer un ID unique qui ne soit pas facilement prévisible. Javascript n’a rien pour faire ça en natif, et c’est un peu con de charger une lib de plus pour ça alors que Python peut le faire pour nous.

L’IP, c’est parce qu’il faut donner l’adresse de notre machine contient notre routeur. Et le téléphone qui sert de télécommande doit se connecter à ce routeur. Il faut donc qu’il connaisse l’adresse de celui-ci, donc on va la mettre dans notre QR code.

Cela veut dire aussi que le téléphone doit être sur le même réseau local pour que ça fonctionne. Donc mettez votre téléphone en Wifi, pas en 3G.

Voilà ce que donne notre code WAMP côté serveur :

# -*- coding: utf-8 -*-

from autobahn.twisted.wamp import Application

import socket
import uuid

# Comme pour flask, l'objet app
# est ce qui lie tous les éléments
# de notre code ensemble. On lui donne
# un nom, ici "demo"
app = Application('demo')
# Bien que l'app va démarrer un serveur
# pour nous, l'app est bien un CLIENT
# du serveur WAMP. Le serveur démarré
# automatiquement n'est qu'une facilité
# pour le dev. En prod on utiliserait
# crossbar.

# Juste un conteneur pour y mettre notre IP
app._data = {}

# On déclare que cette fonction sera appelée
# quand l'app se sera connectée au serveur WAMP.
# Ceci permet de lancer du code juste après
# le app.run() que l'on voit en bas du fichier.
# '_' est une convention en Python pour dire
# "ce nom n'a aucune importance, c'est du code
# jetable qu'on utilisera une seule fois".
@app.signal('onjoined')
def _():
   # On récupère notre adresse IP sur le réseau local
   # C'est une astuce qui demande de se connecter et donc
   #  à une IP externe, on a besoin d'une connexion internet.
   s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   s.connect(("8.8.8.8", 80))
   # On stocke l'adresse IP locale dans un conteneur
   # qui sera accessible partout ailleur.
   app._data['LOCAL_IP'] = s.getsockname()[0]
   s.close()

# On déclare que la fonction "ip()" est appelable
# via RCP. Ce qui veut dire que tout autre client
# WAMP peut obtenir le résultat de cette fonction.
# Donc on va pouvoir l'appeler depuis notre navigateur.
# Comme notre app s'appelle "demo" et notre fonction
# s'appelle "ip", un client pourra l'appeler en faisant
# "demo.ip".
@app.register()
def ip():
   # On ne fait que retourner l'IP locale. Rien de fou.
   return app._data['LOCAL_IP']

# Je voulais appeler cette fonction distante "uuid", mais ça
# override le module Python uuid. Ce n'est pas une bonne
# idée. Je l'appelle donc 'get_uuid' mais je déclare le
# namespace complet dans register(). Un client WAMP pourra donc
# bien l'appeler via "demo.uuid".
# Notez que ce namespace doit toujours s'écrire
# truc.machine.bidule. Pas truc/machin ou truc:machin.
# ou truc et bidule.MACHIN.
@app.register('demo.uuid')
def get_uuid():
   # Retourne un UUID, sans les tirets.
   # ex: b27f7e9360c04efabfae5ac21a8f4e3c
   return str(uuid.uuid4()).replace('-', '')

# On lance notre client qui va se connecter au
# routeur.
if __name__ == '__main__':
    app.run(url="ws://127.0.0.1:8080/ws")
# On ne peut rien mettre comme code ici, il faut le
# mettre dans @app.signal('onjoined') si on veut
# entrer du code après que l'app soit lancée.

Et on lance notre app dans un autre terminal:

python app.py

Nous avons maintenant Crossbar.io qui tourne d’une console, et le client Python qui tourne dans une seconde console, connecté au routeur.

Le lecteur vidéo

Il nous faut maintenant définir le comportement de notre lecteur vidéo, un client WAMP Javascript. Il s’agit essentiellement de se connecter au serveur WAMP, et d’échanger des messages via RPC ou PUB/SUB :

  var player = {};
  var url;
  /* On va utiliser du pur JS histoire de pas mélanger
    des notions de jQuery dans le tas. Je ne vais
    PAS utiliser les best practices sinon vous allez
    être noyés dans des détails */

  /* Lancer le code une fois que la page est chargée */
  window.addEventListener("load", function(){

    /* Connexion au serveur WAMP. J'utilise
       les valeurs par défaut du serveur de
       dev. On ouvre explicitement la connection
       à la fin du script. */
    var connection = new autobahn.Connection({
       url: 'ws://' + window.location.hostname + ':8080/ws',
       realm: 'realm1'
    });

    /* Lancer ce code une fois que la connexion
       est réussie. Notez que je ne gère pas
       les erreurs dans dans une APP JS, c'est
       un puits sans fond. */
    connection.onopen = function (session) {

      /* Appel de la fonction ip() sur le serveur */
      session.call('demo.ip')

      /* Une fois qu'on a récupéré l'IP,
         on peut fabriquer l'URL de notre
         projet et on appelle la fonction
         get_uuid() du serveur */
      .then(function(ip){
        url = 'http://' + ip + ':8000';
        return session.call('demo.uuid');
      })

      /* Une fois qu'on a l'UUID, on peut commencer
         à gérer la partie télécommande */
      .then(function(uuid){

        /* Création du QR code avec le lien pointant
           sur la bonne URL. On met l'ID dans le hash. */
        var controlUrl = url + '/control.html#' + uuid;
        var codeDiv = document.getElementById("qrcode");
        new QRCode(codeDiv, controlUrl);
        var ctrllink = document.getElementById("ctrllink");
        ctrllink.href = controlUrl;

        /* Notre travail consiste essentiellement à
           manipuler cet élément */
        var video = document.getElementById("vid");

        /* On attache déclare 4 fonctions comme étant
           appelable à distance. Ces fonctions sont
           appelables en utilisant le nom composé
           de notre ID et de l'action qu'on souhaite
           faire. Ex:
           'b27f7e9360c04efabfae5ac21a8f4e3c.play'
           pour appeler "play" sur notre session. */
        session.register(uuid + '.play', function(){
           video.play();
        });

        session.register(uuid + '.pause', function(){
           video.pause();
        });

        session.register(uuid + '.volume', function(val){
           video.volume = val[0];
        });

        session.register(uuid + '.status', function(val){
          return {
            'playing': !video.paused,
            'volume': video.volume
          };
        });



       /* Quelqu'un peut très bien
           appuyer sur play directement sur cette page.

          Il faut donc réagir si l'utilisateur le fait,
          publier un événement via WAMP pour permettre
          à notre télécommande de se mettre à jour
          */
       video.addEventListener('play', function(){
         /* On publie un message indiquant que
            le player a recommencé à lire la vidéo.
            */
         session.publish(uuid + '.play');
       });

        video.addEventListener('pause', function(){
          session.publish(uuid + '.pause');
        });

        video.addEventListener('volumechange', function(){
          session.publish(uuid + '.volume', [video.volume]);
        });

     });
    };

    /* Ouverture de la connection une fois que tous les
       callbacks sont bien en place.*/
    connection.open();
  });

Code de la télécommande

La télécommande est notre dernier client WAMP (on peut avoir plein de clients WAMP, ne vous inquiétez, ça tient 6000 connections simultanées sur un tout petit Raspberry PI).

Son code a pour but d’envoyer des ordres au player HTML5, mais aussi de mettre à jour son UI si le player change d’état.

/* L'objet qui se charge de la logique de nos
   controles play/pause et changement de
   volume.
   Rien de fou, il change l'affichage
   du bouton et du slider selon qu'on
   est en pause/play et la valeur du
   volume.
   */
var control = {
   playing: false,
   setPlaying: function(val){
      control.playing = val;
      var button = window.document.getElementById('play');
      if (!val){
         button.innerHTML = 'Play'
      } else {
         button.innerHTML = 'Pause';
      }
   },
   setVolume: function(val){
      var slider = window.document.getElementById('volume');
      slider.value = val;
   }
};
window.onload = function(){
  var connection = new autobahn.Connection({
    url: 'ws://' + window.location.hostname + ':8080/ws',
    realm: 'realm1'
  });

  connection.onopen = function (session) {

    /* Récupération de l'ID dans le hash de l'URL */
    var uuid = window.location.hash.replace('#', '');

    /* Mise à jour des controles selon le status actuel
       du player grace à un appel RPC vers notre autre
       page. */
    session.call(uuid + '.status').then(function(status){

      control.setPlaying(status['playing']);
      control.setVolume(status['volume'])

      /* On attache l'appui sur les contrôles à
         un appel de la fonction play() sur le
         player distant. L'uuid nous permet
         de n'envoyer l'événement que sur le
         bon player. */
      control.togglePlay = function() {
        if (control.playing){
          session.call(uuid + '.pause');
          control.setPlaying(false);
        } else {
          session.call(uuid + '.play');
          control.setPlaying(true);
        }
      };

      control.volume = function(val){
        session.call(uuid + '.volume', [val / 100]);
      };

      /* On ajoute un callback sur les événements
         de changement de status du player. Si
         quelqu'un fait play/pause ou change le
         volume, on veut mettre à jour la page. */
      session.subscribe(uuid + '.play', function(){
        control.setPlaying(true);
      });

      session.subscribe(uuid + '.pause', function(){
        control.setPlaying(false);
      });

      session.subscribe(uuid + '.volume', function(val){
        control.setVolume(val[0] * 100);
      });
    });
  };

  connection.open();
};

En résumé

Voici à quoi ressemble le projet final :

.
├── app.py
├── control.html
├── .crossbar
│   └── config.json
└── index.html
Schéma de fonctionnement de la démo

Bien que l’app Python lance le serveur automatiquement et de manière invisible, c’est bien un composant à part.

Pour ce projet, on aura utilisé :

  • WAMP: le protocole qui permet de faire communiquer en temps réel des parties d’application via RPC et PUB/SUB.
  • Autobahn.js: une lib pour créer des clients WAMP en javascript.
  • Autobahn.py: une lib pour créer des clients WAMP en Python.
  • Crossbar.io: un routeur WAMP.

Il y a pas mal de notions à prendre en compte.

D’abord, le RPC.

Cela permet à un client de dire “les autres clients peuvent appeler cette fonction à distance”. On l’utilise pour exposer ip() et get_uuid() sur notre serveur et notre Javascript peut donc les appeler. Mais on l’utilise AUSSI pour qu’une des pages (le player) expose play(), pause() et volume() et que l’autre page (notre télécommande) puisse les utiliser.

La grosse différence, c’est que ip() peut être appelé par tous les clients en utilisant “demo.ip” alors que play() ne peut être appelé que par les clients qui connaissent l’ID du player, puisqu’il faut utiliser “<id>.play”.

Ensuite, il y a le PUB/SUB.

Cela permet à un client de dire “j’écoute tous les messages adressés à ce nom”. Et un autre client peut envoyer un message (on appelle ça aussi un événement, c’est pareil) sur ce nom, de telle sorte que tous les clients abonnés le reçoivent.

On l’utilise pour que notre télécommande dise “j’écoute tous les messages qui concernent les changements de status du player.” De l’autre côté, quand on clique sur un contrôle du player, on envoie un message précisant si le volume a changé, ou si on a appuyé sur play/pause. La télécommande peut ainsi mettre son UI à jour et refléter par exemple, la nouvelle valeur du volume.

Cela résume bien les usages principaux de ces deux outils :

  • RPC permet de donner un ordre ou récupérer une information.
  • PUB/SUB permet de (se) tenir au courant d’un événement.

Voici le workflow de notre projet :

  • On lance un serveur WAMP.
  • On connecte des clients dessus (du code Python ou Js dans notre exemple).
  • Les clients déclarent les fonctions qu’ils exposent en RPC et les événements qu’ils écoutent en PUB/SUB.
  • Ensuite on réagit aux actions utilisateurs et on fait les appels RPC et les publications PUB/SUB en conséquence.

Si vous virez tous les commentaires, vous verrez que le code est en fait vraiment court pour une application aussi complexe.

Encore une fois, il est possible de le faire sans WAMP, ce sera juste plus compliqué. Je vous invite à essayer de le faire pour vous rendre compte. Avec PHP, Ruby ou une app WSGI, c’est pas marrant du tout. Avec NodeJs, c’est plus simple, mais il faut quand même se taper la logique de gestion RPC et PUB/SUB à la main ou installer pas mal de libs en plus.

WAMP rend ce genre d’app triviale à écrire. Enfin triviale parce que là j’ignore tous les edge cases, évidemment. Pour un produit solide, il faut toujours suer un peu.

Les limites du truc

C’est du Python 2.7. Bientôt on pourra le faire avec asyncio et donc Python 3.4, mais malheureusement sans le serveur de dev.

Heureusement, Twisted est en cours de portage vers Python 3, et donc tout finira par marcher en 3.2+.

C’est du HTML5, mais bien entendu, rien ne vous empêche de faire ça avec du Flash si ça vous amuse.

C’est du WebSocket, mais on peut utiliser un peu de Flash pour simuler WebSocket pour les vieux navigateurs qui ne le supportent pas.

Non, la vraie limite c’est encore la jeunesse du projet : pas d’autoreload pour le serveur (super chiant de devoir le faire à la main à chaque fois qu’on modifie le code) et les erreurs côté serveur se lisent dans la console JS, et pas dans le terminal depuis lequel on a lancé le serveur. Plein de petits détails comme ça.

]]>
http://sametmax.com/introduction-a-wamp-en-python/feed/ 47 11146
Le potentiel de WAMP, autobahn et crossbar.io http://sametmax.com/le-potentiel-de-wamp-autobahn-et-crossbar-io/ http://sametmax.com/le-potentiel-de-wamp-autobahn-et-crossbar-io/#comments Sun, 01 Jun 2014 10:09:32 +0000 http://sametmax.com/?p=10380 crossbar et autobahn. Mais ça me tue de ne pas voir plus de monde exploiter cette techno.]]> Je sais, je sais, je vous fais chier avec crossbar et autobahn.

Mais ça me tue de ne pas voir plus de monde exploiter cette techno.

Pendant que Max fait la sieste, j’ai pris mon stylo et j’ai fait la liste des besoins d’une app Web actuelle. Quels sont les composants qu’on utilise presque systématiquement, mais en agrégeant divers bouts de trucs à droite et à gauche ?

Ensuite j’ai regardé les possibilités des outils WAMP :

  • PUB/SUB et RPC.
  • Asynchrone.
  • Gestionnaire de process intégré.
  • Serveur stand alone qui n’a pas besoin d’un proxy pour être en prod.

M’inspirant de cela, et du travail que je suis en train de faire avec l’équipe de Tavendo pour faire une API flaskesque pour autobahn, j’ai prototypé une API d’un framework Web qu’on pourrait coder au dessus de cette techno.

Voilà ce que ça donne…

Une API qui mélange flask et nodejs pour le Web

app = Application('YourProjectName')

# Envoyer et recevoir des requêtes HTTP
@app.http.post(r'/form')
def _(req, res):
    res.json({'data': 'pouet'})

@app.http.get(r'/user/:id/')
def _(req, res):
    res.render('index.html', {'data': 'pouet'})

# Servir des fichiers statiques
@app.http.serve('uri', '/path/to/dir', [allow_index])

app.run()

Comme c’est asynchrone, on a de très bonnes perfs. Comme c’est basé sur Twisted, on a pas besoin d’un serveur wsgi (gunicorn, uwsgi, etc) ni d’un proxy (nginx) devant. On peut le mettre en prod tel quel.

Parti de ce principe, on peut ajouter la gestion du PUB/SUB et du RPC pour WAMP :

# Callback attendant l'événement
@app.wamp.event('auth.signedin')
def _(ctx, a, b, c):
    pass

# déclenchement de l'événément
app.wamp.pub('auth.signedin')

# Déclaration du fonnction appelable à distance
@app.wamp.remote('auth.signin')
def _(ctx, a, b, c):
    pass

# appel de la fonnction
app.wamp.call('auth.signin')

On est souvent perdu quand on fait de l’asynchrone pour la première fois avec Python car on ne sait pas comment lancer du code après .run(). On peut régler la question proposant des hooks pour les instants clés de l’app.

# Callback à lancer quand l'app est prête
@app.on('app.ready')
def _(ctx, args):
    pass

# Signalement que l'app est prête (fait automatiquement en interne
# pour les moments les plus importants)
app.emit('app.ready')

Et tant qu’on y est, puisqu’on a une event loop, profitons en pour proposer du CRON intégré à l’app. C’est moins chiant à déployer qu’un script CRON, c’est cross plateforme, et on a accès facilement à toute sa stack.

# Lancer du code tous les x temps ou a une date précise
@app.cron(every=seconds)
@app.cron(every=timedelta, overlap=False)
@app.cron(hour=7, minute=30, day_of_week=1)
@app.cron(when=datetime)
def _(ctx, args):
    pass

Pourquoi s’arrêter là ? Event loop + message passing + safe queues + workers = tasks queues !

# Créer une file d'attente
queue = @app.queue('name', [workers], [result_backend])

# Callback appelé par un worker quand il depop ce 
# message dans la file
@queue.task('encode.video')
def _(ctx, data):
    pass

# Envoie d'une tache dans la queu
queue.append('encode.video', data)

Comme on utilise Twisted, on a accès à une chiée de protocoles, et on peut aussi créer les siens. On peut donc imaginer un système de plugins qui rajoute des protocoles supportés :

app = Application('YourProjectName')
app.plug('lib.ajoutant.sms', [namespace])

Si on en a beaucoup et que le namespace nous convient :

app = Application('YourProjectName', plugins=('lib1', 'lib2', 'etc'))

Exemples de plugins possibles :

# Recevoir et envoyer des SMS (via un service type twilio, une gateway kannel ou
# un modem physique)
@app.sms.receive(r'LOVE \w+ \w+')
def _(ctx, args):
    pass
app.sms.send('test', [contact])


# Envoyer et recevoir des emails (via un server SMTP ou IMAP)
@app.email.receive(src=r'.*@sametmax.com', dest=r'spam.*@*.')
def _(ctx, args):
    pass
app.email.send('test', [contact, title, attachments])


# techniquement n'importe quel service de message pour lequel on peut écrire
# un backend
@app.tweet.receive(r'Chat')
@app.fb.receive(r'Like')
@app.instagram.receive(r'Bouffe')
@app.irc.message(r'dtc')
def _(ctx, args):
    pass

Le problème des apps centrées sur un objet, c’est qu’elles ont souvent un design monolithique. Ce n’est pas un problème du concept d’app, c’est juste que les auteurs ont pensé “point d’entrée”, et pas “élément composable”.

Si besoin, on doit pouvoir composer une app via plusieurs sous-app :

app = Application()
app.embed('autre.app')

ou

app = Application(embed=['app1', 'app2', 'app3'])

Il faut des hooks pour overrider la configuration, mais vous avez compris le principe.

Un autre problème avec les plateformes comme NodeJS, c’est qu’il est difficile d’utiliser plusieurs coeurs. C’est une des raisons du succès de Go.

Or, Crossbar encourage la division en plusieurs process qui communiquent entre eux (un peu comme les channels). Créons aussi une API pour ça :

p1 = app.process()
p2 = app.process()

# Déclarer et appeler une procédure dans process 1
@p1.wamp.remote('auth.signin')
def _(ctx, args):
    pass

# Déclarer et appeler une procédure dans process 2
@p2.wamp.event('auth.signedin')
def _(ctx, args):
    pass

Ainsi on profite enfin de plusieurs CPU. La même chose en plus facile à changer:

# Déclarer et appeler une procédure
@app.wamp.remote('auth.signin')
def _(ctx, args):
    pass

# Déclarer et appeler une procédure
@app.wamp.event('auth.signedin')
def _(ctx, args):
    pass

app.processes({
    1: ['wamp.remote:auth.signin']
    2: ['wamp.event:auth.signedin']
})

En bonus, on fait la nique au GIL.

Mieux, on peut bouger ses process sur plusieurs machines :

Machine 1 (routeur):

router = Application(endpoint="0.0.0.0:8080")
router.run()

Machine 2 (authentification):

# IP du router
auth = Application('auth', connect_to="182.64.1.15:8080")

# Nommage automatique en fonction du nom de la fonction
# et de l'app, avec possibilité d'annuler ou overrider le prefix.
# Ici du coup la fonction s'appellera en RPC via 'auth.signin'
@auth.wamp.remote()
def signin(ctx, args):
    pass

auth.run()

Machine 3 (API REST):

web = Application('site', connect_to="182.64.1.15:8080")

@web.http.post(r'api/auth/')
def _(req, res):
    user = yield res.wamp.call('auth.signin',
                               req.POST['username'],
                               req.POST['password'])*
    if user
        user = yield res.wamp.pub('auth.signedin', user.userid)
        res.json({'token': user.token})
    else:
        res.json({'error': 'nope'})


@web.http.get(r'api/stuff/')
def _(req, res):
    res.json(get_stuff())

@web.http.serve('uri', '/path/to/dir', [allow_index])

web.run()

Et vous savez le plus beau dans tout ça ? En Python on a plein de libs qui sont encore bloquantes. En théorie on ne peut pas les utiliser dans les apps asynchrones. Quand on a toute sa logique métiers dans des classes d’ORM, c’est balot. Mais pas ici ! On met un process avec tous ces appels bloquants, et on les appelle depuis des process non bloquant en RPC de manière asynchrone. Pif, paf, pouf, problème isolé.

Après, libre à son imagination de rajouter des fonctionnalités de confort…

Callback qui sera appelé seulement x fois :

# Déclarer et appeler une procédure
@p1.wamp.event('auth.signedin', options={'limit_calls': x} )
def _(ctx, args):
    pass

Raccourcis pour les opérations courantes :

# Recevoir et envoyer un événement
@app.sub('auth.signin')
def _(ctx, *args):
    # ctx.pub
@app.pub('auth.signedin')

# Déclarer et appeler une procédure
@app.proc('auth.signedin')
def _(ctx, args):
    # ctx.call
app.rpc()

Comme je vous l’avais expliqué, crossbar peut gérer le cycle de vie de services externes à votre application au démarrage. Autant exposer cette API programativement :

@app.service(['/urs/bin/nodejs', 'script.js'], [user], [group])

.run(), c’est cool, mais si on veut changer des options via la ligne de commande, faut se taper tout le boulot alors que ça pourrait très bien se générer automatiquement :

@app.cmd_run()

Et si vous faites : python sites.py --debug=true --endpoint=0.0.0.0:5252, ça le prend automatiquement en compte. Y a pas de raison de se faire chier.

En parlant de générer automatiquement des trucs, le fichiers de configs pour les services externes sur lesquels on peut avoir envie de brancher notre app, c’est toujours galère. Autant fournir un exemple de base qui est sûr de toujours marcher, généré avec les paramètres de notre app :

python site.py template centos:nginx
python site.py template ubuntu:upstart
python site.py template bsd:systemd # :D

On peut partir très loin dans le délire “battery included”. Typiquement, on peut fournir des services externes nous même puisque crossbar nous le propose, et coder des versions moins bien, mais compatibles (et suffisantes pour les petits sites), de projets toujours utilses :

  • cache (compatible redis)
  • live settings (compatible etcd) mais avec en prime un event wamp propagé à chaque
  • changement de valeur

  • build (compatible, heu, j’en sais rien) qui s’occupe en tâche de fond de surveiller le >système de fichier et lancer les compilations, les minifications, les copies, les tests unittaires, etc.
  • logging centralisé (compatible sentry).
  • Un bridge WAMP/REST qui permet d’envoyer et recevoir des events WAMP sur votre app Django ou flask en utilisant HTTP.

On plug tout ça a une admin Web.

J’espère que je vous ai donné maintenant l’envie de vous plonger un peu plus dans cette techno, et peut être coder quelque chose avec.

Il n’y a plus d’excuses pour ne pas avoir de framework web next gen, ultime de la mort qui tue en Python. A part le fait qu’on soit des feignasses.

Ah, merde, on est foutus.

]]>
http://sametmax.com/le-potentiel-de-wamp-autobahn-et-crossbar-io/feed/ 29 10380