banniere.png
Document Actions

07/30/2007

Au coeur de Python Volume 1

J'ai reçu la traduction de l'ouvrage de Wesley Chun, "Core Python Programming". La version française se présente en deux tomes, dont voici le premier:

Au coeur de Python, Tome 1, Notions fondamentales


Si vous l'avez lu, n'hésitez pas à commenter la fiche
Categories: documentation 0 comments - commenter | Trackbacks (441) |

07/22/2007

Un site de critiques de livre

J'ai commencé à regrouper sur un site toutes mes lectures. L'application qui fait fonctionner ce site reprend tous les principes énoncés dans les billets suivants:


Un widget en flash permet aussi d'afficher sur un blog (regardez à gauche) ses 5 dernières critiques (merci Julien).

Et c'est Django Powered. Si vous aimez bien lire et partager, rejoignez moi sur fr.luvdit.com, même s'il n'est pas tout à fait terminé, il est déjà utilisable.

07/06/2007

Un joli trac pour mes projets

J'ai insallé un trac pour mes projets, avec le plugin Mercurial.

http://hg.programmation-python.org/

L'interface est beaucoup pus agréable que le browser web par défaut de Mercurial.

Joie.
Categories: misc 0 comments - commenter | Trackbacks (380) |

07/05/2007

Service de mailing asynchrone pour Django, Plone, etc..

Django ne déroge pas à la règle, et fourni dans comme pour la plupart des frameworks web Python, un module pour l'envoi de mail totalement... inutilisable dans des conditions de production. En effet, send_mail et send_mass_mail, les deux API de django.core.mail, envoient tous les deux les mails de manière synchrone.

L'effet est relativement désastreux sur les performances du site puisque chaque envoi de mail provoque un blocage du thread en cours, le temps de l'échange avec le serveur SMTP via telnet. Sur un site chargé, ou qui utilise de façon massive les envois de mails, l'emploi de django.core.mail est donc fortement déconseillé.

Plus globalement, tout code qui n'entre pas en ligne de compte pour calculer la page à afficher, ne doit pas s'exécuter de manière synchrone (autres exemples de calculs asynchrones: calculs des voisins, indexation, etc).

Zope 3, un peu plus mature et sophistiqué que les autres frameworks sur ce point précis, propose un module d'envoi de mails asynchrone, qui recopie les mails dans un répertoire au format Maildir, et lance un thread en charge de dépiler les mails du répertoire. L'interêt, outre l'aspect asynchrone qui permet d'accélerer les envois et de libérer le thread qui sert la page immédiatement, est la robustesse: si le serveur tombe, le thread peut reprendre son travail d'envoi lorsqu'il est relancé.

Mais cette solution reste liée au serveur d'application car le thread est lié au processus. L'autre défaut est qu'il est nécessaire, si l'on veut ajouter des informations supplémentaires aux mails à traiter, d'ajouter des en-têtes pour respecter le format RFC-2822 des mails qui sont recopiés dans la maildir (et de les retirer avant l'envoi réel des mails). Enfin, le code devient dépendant du système de fichiers, ce qui peut poser des problèmes d'infrastructure si l'on déploie ce service d'envoi de mails sur une machine tierce au serveur d'application.

Une autre solution, beaucoup plus robuste, consiste à déposer ces mails dans une table de base de donnée relationnelle (celle employée par le site dans Django, ou une dédiée pour Zope) qui est lue régulièrement par un service d'envois de mails, totalement indépendant du serveur web.

Deux tables pour le prix d'une


Pour mettre en place ce service, deux tables sont créées dans la base de données, grâce à SQLAlchemy:
mail_data = Table('mailer_mail_data', metadata,
Column('id', Integer, primary_key=True, autoincrement=True),
Column('subject', String(300)),
Column('sender', String(300)),
Column('recipients', String(300)),
Column('date', DateTime()),
Column('data', TEXT()))

mailed_data = Table('mailer_mailed_data', metadata,
Column('id', Integer, primary_key=True, autoincrement=True),
Column('subject', String(300)),
Column('sender', String(300)),
Column('original_id', Integer),
Column('recipients', String(300)),
Column('error', String(300)),
Column('data', TEXT()),
Column('date', DateTime()),
Column('status', String(10)),)
La table mail_data sert à stocker les informations sur les mails à envoyer, et la table mailed_data permet de stocker les mails envoyés, avec pour chaque un statut, si jamais l'envoi a échoué. Cette deuxième table permet aux applications de mettre en place du feedback en cas de problème d'envoi.

Travailleuse, travailleur


Le programme en charge d'envoyer les mails est un thread qui ouvre régulièrement la base pour:
  • Lire la table mail_data et envoyer les mails qu'elle contient
  • Archiver les mails envoyés, avec ou sans erreurs, dans mailed_data
  • Supprimer les mails envoyés de la table mail_data

Il est lancé comme programme indépendant vi un script run.py, qui peut être dameonisé sur le serveur grâce aux dameontools par exemple (il y a surement plus simple de nos jours avis aux experts Linux...)
class MailWorker(Thread):
"""reads the SQLDB to do the jobs"""

def __init__(self):
Thread.__init__(self)
self.is_working = False
self.running = False

def _get_mails(self):
"""returns lines of mail_data """
return mail_data.select().execute().fetchall()

def _get_message(self, mail):
"""returns a Mime"""
msg = MIMEText(b64decode(mail.data))
msg['From'] = mail.sender
msg['To'] = mail.recipients
msg['Subject'] = mail.subject

msg['Date'] = mail.date.isoformat()
return msg

def _send_mail(self, mail):
"""sends the mail"""
server = smtplib.SMTP(settings.SMTP_SERVER)
msg = self._get_message(mail)
try:
server.sendmail(msg['From'], msg['To'], msg.as_string())
finally:
server.quit()
logging.debug('mailer:message sent to %s' % msg['To'])

def _store_mail(self, mail, error=None):
"""stores the mail"""
if error is not None:
error = str(error)
ins = mailed_data.insert()
ins.execute(subject=mail.subject, sender=mail.sender,
original_id=mail.id, recipients=mail.recipients,
error=error, data=mail.data, date=datetime.now(),
status='processed')

# removes from original table
mail_data.delete().execute(id=mail.id)

def run(self):
"""called threaded"""
self.running = True
logging.debug('mailer:launched')

while self.running:
# index
self.is_working = True
try:
# get mails to send
mails = self._get_mails()
for mail in mails:
try:
# send then
self._send_mail(mail)
except Exception, e:
logging.debug('mailer:failed to send mail')
self._store_mail(mail, e)
else:
self._store_mail(mail)
finally:
self.is_working = False
time.sleep(.1)

logging.debug('mailer:stopped')

worker = None

def start_server():
"""starts the worker"""
global worker
worker = MailWorker()
worker.start()

def stop_server():
"""stops the worker"""
global worker
if worker is not None:
worker.running = False
worker.join()
worker = None

def is_working():
"""tells if the worker works"""
return worker.is_working

# will make sure the thread stops when the process quits
from atexit import register
register(stop_server)

API d'envoi de mail


Enfin, les applications peuvent se servir du module sender pour envoyer des mails. Ce dernier injecte dans la table mail_data le mail et rend la main immédiatement
def send_mail(sender, recipients, subject, msg):
"""sends the mail by storing it into the DB"""
inserter = mail_data.insert()
res = inserter.execute(subject=subject, sender=sender,
recipients=','.join(recipients),
data=b64encode(msg), date=datetime.now())

return res.last_inserted_ids()[0]

J'utilise cette API dans mes applications pour tous les envois de mail. Dans Django, elle remplace avantageusement django.core.mail.

Exemple complet


Voici la docstring du paquet que j'ai conçu
mailer
=====

Mailer provides:

- a simple method to send mails (eg: store them)
- a worker that actually sends them

Let's work on a sql data file for the tests:

>>> import settings
>>> settings.DATABASE = test_db

send mails
----------

To send a mail, the package provides the `sender` module::

>>> from sender import send_mail
>>> mail_id = send_mail(sender='tarek@ziade.org',
... recipients=['ziade.tarek@gmail.com'],
... subject='hello',
... msg='héllo')


The mail is then stored in the database, and the id returned is the
mail id in the DB.

Another API will give the status of the mail in process::

>>> from sender import mail_status
>>> mail_status(mail_id)
u'processing'

When the id is not given, the whole mailed table is returned::

>>> mail_status()
[]

We can ask for the queue size as well::

>>> from sender import mail_queue_size
>>> mail_queue_size()
1

sending mails, for real
-----------------------

A worker is in charge of sending mails::

>>> from mailer import start_server, stop_server
>>> start_server()

The mail is then processed::

>>> import time
>>> while mail_status(mail_id) == u'processing':
... time.sleep(0.2)
>>> mail_status(mail_id)
u'processed'

Let's stop the server::

>>> stop_server()

And see the status::

>>> status = mail_status()
>>> status[0]['subject']
u'hello'
>>> status[0]['status']
u'processed'


Et, Ô joie, le code est disponible, comme d'habitude, sur http://hg.programmation-python.org, dans le paquet mailer.

Categories: coding, django, zope 0 comments - commenter | Trackbacks (397) |

Tarek Ziadé. Copyright 2006. Tous droits réservés. Licence contenu site
BuzTrucs
Add to Technorati Favorites