PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Le potentiel de WAMP, autobahn et crossbar.io

dimanche 1 juin 2014 à 12:09

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 :

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 :

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.

flattr this!

Error happened! 0 - Undefined variable: xml In: /home/www/carrade/autoblogs/autoblogs/autoblog.php:77 http://autoblogs.carrade.eu/autoblogs/sametmaxcom_9a0374ae99e9509a2ae767584a96873592123527/?Le-potentiel-de-WAMP-autobahn-et-crossbar-io #0 /home/www/carrade/autoblogs/autoblogs/autoblog.php(77): exception_error_handler(8, 'Undefined varia...', '/home/www/carra...', 77, Array) #1 /home/www/carrade/autoblogs/autoblogs/autoblog.php(368): VroumVroum_Feed_Exception::getXMLErrorsAsString(Array) #2 /home/www/carrade/autoblogs/autoblogs/autoblog.php(935): VroumVroum_Blog->update() #3 /home/www/carrade/autoblogs/autoblogs/sametmaxcom_9a0374ae99e9509a2ae767584a96873592123527/index.php(1): require_once('/home/www/carra...') #4 {main}