Twisted – Arquitectura del framework de red más popular



Una traducción del artículo titulado The Architecture of Open Source Applications (Volume 2): Twisted, por Jessica McKellar.

Twisted es un motor de red dirigido a eventos escrito en Python. Nació a comienzos del año 2000, cuando los desarrolladores de juegos en línea tenían a su disposición un pequeño repertorio de librerías escalables que solo corrían en algunas plataformas. Los autores de Twisted intentaron desarrollar juegos en el escenario de las librerías de red del momento, pero vieron la clara necesidad de un framework escalable, multiplataforma y dirigido a eventos. De esta manera dicidieron crear uno, aprendiendo de los errores e imperfecciones del resto de las utilidades.

Twisted soporta gran cantidad de los protocolos de transmisión más comunes, entre los que se encuentran TCP, UDP, SSL/TLS, HTTP, IMAP, SSH, IRC y FTP. Tal como en el lenguaje que está escrito, “incluye las baterías”; viene con implementaciones de cliente y servidor para todos sus protocolos, como también utilidades para hacerlo de fácil configuración y despliegue desde la línea de comandos.

¿Por qué Twisted?

En el año 2000, Glyph, creador de Twisted, se encontraba trabajando en un juego multijugador basado en texto llamando Twisted Reality. Era un gran “desastre” de threads (hilos), 3 por conexión, en Java. Había un thread para la entrada que se bloquearía durante la lectura, otro para la salida que se bloquearía durante la escritura, y un tercero para la lógica que se mantendría dormido mientras esperaba eventos en la cola. Mientras los jugadores se movían e interactuaban en el mundo virtual, los threads y el caché se volvían corruptos, y la lógica de bloqueo nunca resultaba correcta en su totalidad — el uso de threads había hecho el software complicado, propenso a errores y difícil de escalar.

Buscando alternativas, descubrió Python, y particularmente el módulo select para tareas de entrada y salida (I/O) utilizando sockets y pipes (la Single UNIX Specification, versión 3 (SUSv3) describe la API de select). Al mismo tiempo, Java no proveía la interfaz select del sistema operativo o alguna otra API asincrónica de I/O (el paquete java.nio fue añadido en 2002). Un rápido prototipo del juego en Python usando select permitió inmediatamente una solución menos compleja y más segura que la versión con threads en Java.

Una vez introducido en el tema de Python, select, y la programación dirigida a eventos, Glyph decidió escribir un cliente y servidor para el juego utilizando estos recursos. Pero luego quiso más. ¿Por qué? Ya que quería tener la posibilidad de transformar la actividad de red en llamadas a métodos (funciones) en los objetos del juego. ¿Qué si podrías recibir un correo electrónico en el juego, como NetHack? ¿Qué si cada jugador tenía una página de inicio? Glyph se encontró con la necesidad de clientes y servidores para IMAP y HTTP que utilicen select.

En un principio migró a Medusa, una plataforma desarrollada a mediados de los ’90 para escribir servidores de red en Python basada en el módulo asyncore (un administrador asincrónico de sockets basado en la API de select).

Esto fue un hallazgo motivador para Glyph, pero Medusa presentaba dos inconvenientes:

  1. Estaba en su camino para dejar de ser mantenido en 2001 cuando Glyph comenzó a trabajar en Twisted Reality.
  2. asyncore es un pequeño wrapper en el cual los programadores aun requieren manipular sockets directamente. Esto significa que la portabilidad aun es responsabilidad del programador. Además, al mismo tiempo, el soporte para Windows de asyncore era propenso a errores, y Glyph sabía que quería ejecutar un cliente con una interfaz gráfica de usuario en Windows.

Glyph consideraba la posibilidad de implementar una plataforma de red él mismo y se dió cuenta que Twisted Reality había abierto la puerta a un problema que era tan interesante como el mismo juego.

Al pasar el tiempo, el juego Twisted Reality se convirtió en la plataforma de red Twisted, que haría lo que los paquetes de red existentes (en Python) no hacían:

  • Usar programación dirigida a eventos en lugar de múltiples hilos.
  • Ser multiplataforma: proveer una interfaz uniforme a los sistemas de notificación de eventos de la mayoría de los sistemas operativos.
  • “Incluir las baterías”: proveer implementaciones de protocolos populares, siendo así inmediatamente útil para desarrolladores.
  • Conforme a los RFCs, y demostrar conformidad con una robusta suite de prueba.
  • Uso fácil de múltiples protocolos de red juntos.
  • Extensible.

La arquitectura de Twisted

Twisted es un motor de red dirigido a eventos. La programación dirigida a eventos está tan integrada con la filosofía en el diseño de Twisted que es necesario tomar un momento para recordar qué significa exactamente esto.

La programación dirigida a eventos es un paradigma en el que el flujo del programa es determinado por eventos externos. Está caracterizado por un bucle de eventos y el uso de callbacks para enlazar determinadas acciones cuando los eventos ocurren. Otros dos paradigmas comunes son la programación sincrónica (un solo hilo) y la programación multi-hilo.

Comparemos y diferenciemos los tres paradigmas mencionados anteriormente con un ejemplo. La imagen de abajo muestra el trabajo realizado por un programa a lo largo del tiempo bajo estos tres modelos. El programa tiene tres tareas para completar, y cada una se bloquea mientras espera a que finalicen las tareas de I/O. El tiempo transcurrido durante el bloqueo es aquel coloreado con gris.

En la versión sincrónica del programa, las tareas se ejecutan secuencialmente. Si una tarea se bloquea por un momento para tareas de I/O, todas las otras tareas tienen que esperar a que termine. Este orden preciso y procesamiento secuencial son fáciles de comprender, pero el programa es innecesariamente lento si las tareas no dependen de las demás, como también si tienen que esperar.

En la versión multi-hilo del programa, las tres tareas que se bloquean mientras trabajan son ejecutadas en hilos de control separados. Estos son manejados por el sistema operativo y se ejecutarán concurrentemente en múltiples procesadores o intercalados en uno solo. Esto permite que el programa progrese mientras alguno de los hilos está bloqueado. Es generalmente más eficiente a nivel tiempo que el programa análogo de forma sincrónica, pero el programador debe escribir código para proteger a los recursos que se comparten entre los hilos, para que no sean accedidos al mismo tiempo. Los programas multi-hilo pueden ser más difíciles de entender porque ahora uno debe preocuparse acerca de la seguridad de los threads vía serialización de procesos, reentrada, almacenamiento local, u otros mecanismos que implementados erróneamente pueden generar un código propenso a errores.

La versión dirigida a eventos del programa intercala la ejecución de las tres tareas, pero en un único hilo. Al realizar operaciones extensas o de entrada / salida, se registra una callback en un bucle de eventos, y la ejecución continúa mientras dicha operación se completa. Una callback indica cómo debe tratarse un evento una vez que ha finalizado. El bucle principal obtiene los eventos y los envía a sus callbacks correspondientes a medida que llegan. Esto permite que el programa progrese cuando puede sin el uso de hilos adicionales. Los programas dirigidos a eventos son más fácil de comprender que los multi-hilo ya que el programador no debe ocuparse de la seguridad de los hilos.

El modelo dirigido a eventos es generalmente una elección acertada cuando existen:

  1. varias tareas, que son…
  2. en gran parte, independientes (no deben comunicarse entre sí), y…
  3. alguna de éstas deben bloquearse mientras esperan a ciertos eventos.

Es también una buena elección cuando una aplicación tiene que compartir información mutable entre tareas, ya que no es necesario realizar la sincronización.

Las aplicaciones de red generalmente tienen estas propiedades, que es lo que hace que se ajusten tan bien al modelo de programación dirigida a eventos.

Reutilizando aplicaciones existentes

Cuando Twisted fue creado, ya existían muchos clientes y servidores populares para varios protocolos de red. ¿Por qué Glyph no utilizó Apache, IRCd, BIND, OpenSSH, o cualquiera de las otras aplicaciones ya existentes cuyos clientes y servidores deberían ser re-implementados por completo para Twisted?

El problema es que todas esas implementaciones de servidores tienen código de red escrito desde cero, generalmente en C, en donde también está acoplado directamente el código de la aplicación. Esto los hace de difícil utilización como librerías independientes. Tendrían que ser tratadas como “cajas negras” cuando se usen en conjunto, sin darle la posibilidad al programador de reutilizar código si quiere exponer la misma información en múltiples protocolos. Además, las implementaciones de servidor y cliente son generalmente aplicaciones separadas que no comparten código. Extender estas aplicaciones y mantener compatibilidad entre plataformas es más difícil de lo que debería.

Con Twisted, clientes y servidores están escritos en Python utilizando una interfaz consistente. Esto facilita la escritura de nuevos clientes y servidores, compartir código entre ellos, la lógica entre los distintos protocolos, y probar la aplicación.

El reactor

Twisted implementa el patrón de diseño de reactor, que describe cómo obtener y redireccionar eventos desde múltiples fuentes a sus respectivos manejadores (handlers) en un único hilo.

El núcleo de Twisted es el bucle de eventos del reactor. Este último sabe acerca de red, sistema de archivos y eventos. Espera a estos eventos y luego los procesa, abstrayendo comportamiento específico de una plataforma y presentando interfaces para facilitar la respuesta.

El reactor cumple, principalmente, con las siguientes tareas:

while True:
    timeout = time_until_next_timed_event()
    events = wait_for_events(timeout)
    events += timed_events_until(now())
    for event in events:
        event.process()

El reactor por defecto en todas las plataformas está basado en la API de poll, descrita en la Single UNIX Specification, Version 3 (SUSv3). Adicionalmente, Twisted soporta un número de APIs específicas de cada plataforma. Por ejemplo, kqueue en FreeBSD, epoll en sistemas que soporten dicha interfaz (actualmente Linux 2.6), y un reactor IOCP basado en el sistema de entrada y salida de Windows (Windows Input/Output Completion Ports).

Ejemplos de las caracterísitcas de las APIs en sistemas específicos por las cuales Twisted se preocupa incluyen:

  • Límites de red y el sistema de archivos.
  • Comportamiento de los búfers.
  • Cómo detectar una conexión caída.
  • Los valores retornados en caso de error.

La implementación del reactor de Twisted también se encarga de utilizar correctamente la API y manejar casos extremos con cautela. Python no expone la API de IOCP, por lo que Twisted mantiene su propia implementación.

Manejando las callbacks

Las callbacks son una parte fundamental de la programación dirigida a eventos y la forma en la que el reactor le indica a una aplicación que un evento se ha completado. A medida que un programa crece, el manejo de casos de éxito y error de un evento en vuestra aplicación se va complejizando. Un fallo al registrar una callback apropiada puede dejar a un programa bloqueado en un evento que nunca ocurrirá, y los errores podrían tener que propagarse hasta una serie de callbacks desde el framework a través de las capas de una aplicación.

Examinemos alguno de los riesgos de los programas dirigidos a eventos al comparar una versión sincrónica y otra asincrónica de una aplicación que carga una dirección de URL, en un pseudocódigo Python.

Versión sincrónica

import getPage

def processPage(page):
    print page

def logError(error):
    print error

def finishProcessing(value):
    print "Shutting down..."
    exit(0)

url = "http://google.com"
try:
    page = getPage(url)
    processPage(page)
except Error, e:
    logError(error)
finally:
    finishProcessing()
Versión asincrónica

from twisted.internet import reactor
import getPage

def processPage(page):
    print page
    finishProcessing()

def logError(error):
    print error
    finishProcessing()

def finishProcessing(value):
    print "Shutting down..."
    reactor.stop()

url = "http://google.com"
# getPage takes: url, 
#    success callback, error callback
getPage(url, processPage, logError)

reactor.run()

En la versión asincrónica, reactor.run() comienza el bucle de eventos del reactor. En ambos programas, una hipotética función getPage retorna información sobre una página. processPage es invocada si el proceso anterior es exitoso, mientras que logError es llamada si una excepción es lanzada al intentar retornar la página. En ambos casos, finishProcessing es llamada al final.

La callback para logError en la versión asincrónica refleja el bloque de código except en la sincrónica. La callback para processPage equivale al bloque else, y finishProcessing a finally.

En la versión sincrónica, por virtud de la estructura de un bloque try/except, logError y processPage son llamadas en caso de producirse un error o concluir correctamente, respectivamente, y finishProcessing se llama siempre una vez al finalizar las tareas anteriores. En el programa asincrónico, es responsabilidad del programador la de invocar la serie de callbacks correspondientes para los casos de error y éxito. Si, por un error de programación, la llamada a finishProcessing fuese dejada fuera de la serie de callbacks, el reactor nunca sería interrumpudo y el programa correría infintamente.

Este tipo de ejemplos frustró a los programadores durante los primeros años del desarrollo de Twisted, por lo que respondieron con la creación de un objecto llamado Deferred.

Deferreds

Un objeto Deferred es una abstracción de la idea de un resultado que aún no existe. Además, ayuda a manejar el conjunto de callbacks para este resultado. Cuando es retornado por una función, es una promesa que la función tendrá un resultado en algún momento. Ese único Deferred retornado contiene referencias a las callbacks registradas para un evento, por lo que solo se necesita ese único objeto para ser pasado entre funciones, que es mucho más simple que manejar las callbacks individualmente.

Un Deferred tiene un par de callbacks, una en caso de éxito (que lleva el mismo nombre, callback) y otra en caso de error (llamada errback). Por defecto, este par de callbacks está vacío. Uno añade pares de callbacks y errbacks para manejar éxitos y fallas en cada punto del procesamiento de un evento. Cuando llega un resultado asincrónico, el Deferred es “disparado” y sus correspondientes callbacks o errbacks son llamadas en el orden en que fueron añadidas.

A continuación, una versión asicrónica de la aplicación que obtiene una dirección de URL, pero utilizando Deferreds.

from twisted.internet import reactor
import getPage

def processPage(page):
    print page

def logError(error):
    print error

def finishProcessing(value):
    print "Shutting down..."
    reactor.stop()

url = "http://google.com"
deferred = getPage(url) # getPage returns a Deferred
deferred.addCallbacks(success, logError)
deferred.addBoth(stop)

reactor.run()

En esta versión, son invocadas las mismas funciones (success y logError), pero todas son registradas con un único Deferred en lugar de esparcirse por el código y pasarse como argumentos a getPage.

El Deferred es creado con dos etapas de callbacks. Primero, addCallbacks añade a la función processPage como callback y a logError como errback. Luego, addBoth añade a finishProcessing a la segunda etapa de ambas callbacks. Esquemáticamente, el objeto se comporta de la siguiente manera.

Funcionamiento de un Deferred

Un Deferred puede ser disparado solo una vez. Un intento por lanzarlo nuevamente lanzará una excepción (Exception). Esto acerca la idea de los Deferreds a la de los bloques try/except en la versión sincrónica, lo que facilita el razonamiento del procesamiento de eventos de forma asincrónica, y evita errores sutiles causados por callbacks que son invocadas más de una vez para un mismo evento.

Entender a los Deferreds es una parte importante para entender el flujo de los programas en Twisted. De todas maneras, al utilizar las abstracciones de alto nivel que provee Twisted para los protocolos de red, uno generalmente no utiliza al objeto Deferred directamente.

El concepto de Deferred es potente y ha sido tomado por otras plataformas dirigidas a eventos, incluyendo jQuery, Dojo y Mochikit.

Transports

Los transports (transportes) representan la conexión entre dos puntos que se comunican a través de una red. Son los responsables de describir los detalles de la conexión, como el control del flujo, la seguridad, o el hecho de ser TCP o UDP. Estos últimos dos junto con los sockets de Unix son ejemplos de transports. Están diseñados para ser unidadades mínimamente funcionales que son máximamente reutilizables y están desvinculadas de las implementaciones de protocolo, permitiendo a múltiples protocolos utilizar el mismo tipo de transport. Implementan la interfaz ITransport, que define los siguientes métodos.

write Escribe datos a la conexión física, en secuencia, sin bloqueos.
writeSequence Escribe una lista de cadenas a la conexión física.
loseConnection Escribe todos los datos pendientes y cierra la conexión.
getPeer Obtiene la dirección remota de la conexión.
getHost Obtiene la dirección local de la conexión.

Desvincular a los transports de los protocolos facilita las pruebas en ambas capas. Para inspeccionar, un transport simulado puede simplemente escribir los datos a una cadena.

Protocolos

Los protocolos describen cómo deben procesarse los eventos de red de forma asincrónica. Algunos ejemplos de estos son HTTP, DNS e IAMP. En Twisted los protocolos implementan la interfaz IProtocol, que tiene los siguientes métodos.

makeConnection Realiza una conexión a un transport y a un servidor.
connectionMade Llamado cuando la conexión se ha concretado.
dataReceived Llamado al recibir datos.
connectionLost Llamado cuando la conexión es cerrada.

La relación entre el reactor, los protocolos y los transports es mejor ilustrarla con un ejemplo. A continuación una completa implementación de un servidor y cliente “eco” (reenvía todo lo recibido). Primero el servidor:

from twisted.internet import protocol, reactor

class Echo(protocol.Protocol):
    def dataReceived(self, data):
        # Reenviar todo lo recibido.
        self.transport.write(data)

class EchoFactory(protocol.Factory):
    def buildProtocol(self, addr):
        return Echo()

reactor.listenTCP(8000, EchoFactory())
reactor.run()

Y el cliente:

from twisted.internet import reactor, protocol

class EchoClient(protocol.Protocol):
   def connectionMade(self):
       self.transport.write("hola, mundo!")

   def dataReceived(self, data):
       print "El servidor dice:", data
       self.transport.loseConnection()

   def connectionLost(self, reason):
       print "Se ha perdido la conexión"

class EchoFactory(protocol.ClientFactory):
   def buildProtocol(self, addr):
       return EchoClient()

   def clientConnectionFailed(self, connector, reason):
       print "La conexión ha fallado"
       reactor.stop()

   def clientConnectionLost(self, connector, reason):
       print "La conexión se ha cerrado"
       reactor.stop()

reactor.connectTCP("localhost", 8000, EchoFactory())
reactor.run()

El primer script inicia un servidor TCP escuchando conexiones en el puerto 8000. Utiliza el protocolo Echo, y la información es enviada a través del transport. El cliente realiza una conexión TCP al servidor, reenvía la respuesta del servidor, termina la conexión y para el reactor. Las clases *Factory son utilizadas para producir instancias de los protocolos en ambos lados de la conexión. La comunicación es asincrónica en ambos lados; connectTCP se encarga de registrar las callbacks en el reactor para ser notificado cuando la información está disponible para ser leída desde un socket.

Aplicaciones

Twisted es un motor para producir servidores y clientes escalables y multiplataforma. Facilitando el despliegue de estas aplicaciones de forma estandarizada en entornos de producción es una parte importante de este tipo de plataformas para ser adoptadas a gran escala.

Con ese fin, Twisted desarrolló la infraestructura de aplicaciones, una forma re-utilizable y configurable de desplegar una aplicación de Twisted. Permite a los programadores evitar código repetitivo enlazando una aplicación en herramientas existentes para personalizar el modo en el que corre, incluyendo “demonización” (referente a los Linux daemon processes), registro de errores, utilización de un reactor personalizado, código de perfiles, y más.

La infraestructura de aplicaciones tiene cuatro partes principales: Servicios, Aplicaciones, manejo de configuración (vía archivos TAC y plugins), y la herramienta de línea de comandos twistd. Para ilustrar esta infraestructura, tornaremos el servidor eco de la sección previa a una aplicación.

Servicio

Un servicio es cualquier cosa que puede ser iniciada y parada y se adhiere a la interfaz IService. Twisted incluye implementaciones de servicios para los protocolos TCP, FTP, HTTP, SSH, DNS, y otros. Muchos servicios pueden ser registrados con una única aplicación.

El núcleo de la interfaz IService es:

startService Inicia el servicio. Esto puede incluir la carga de la configuración, establecer una conexión de base de datos, o escuchar en un puerto.
stopService Apaga el servicio. Esto puede incluir guardar el estado en el disco, cerrar una conexión de base de datos, o dejar de escuchar en un puerto.

Nuestro servicio eco utiliza TCP, por lo que podemos usar la implementación por defecto de Twisted TCPServer de esta interfaz IService.

Aplicación

Una aplicación es el servicio de más alto nivel que representa la aplicación completa de Twisted. Los servicios se registran a sí mismos con una aplicación, y la herramienta de despliegue twistd descrita abajo busca y ejecuta aplicaciones.

Crearemos una aplicación eco con la cual el servicio eco pueda registrarse.

Archivos TAC

Al manejar aplicaciones de Twisted en un archivo de Python convencional, el programador es responsable de escribir código para iniciar y detener el reactor y configurar la aplicación. Bajo la infraestructura de aplicaciones, las implementaciones de protocolo están en un módulo, los servicios que utilizan esos protocolos están registrados en un archivo TAC (Twisted Applicacion Configuration), y el reactor y la configuración son manejados por una herramienta externa.

Para transformar nuestro servidor eco en una aplicación eco, podemos seguir un simple algoritmo:

  1. Mover la partes del protocolo del servidor eco en su propio módulo.
    1. Dentro de un archivo TAC:

    2. Crear una aplicación eco.
    3. Crear una instancia del servicio TCPServer que utilizará nuestra EchoFactory, y regitrarla con la aplicación.

Del código para manejar el reactor se encargará twistd, discutido más abajo. El código de la aplicación termina por verse de la siguiente forma.

El archivo echo.py:

from twisted.internet import protocol, reactor

class Echo(protocol.Protocol):
    def dataReceived(self, data):
        self.transport.write(data)

class EchoFactory(protocol.Factory):
    def buildProtocol(self, addr):
    return Echo()

El archivo echo_server.tac:

from twisted.application import internet, service
from echo import EchoFactory

application = service.Application("echo")
echoService = internet.TCPServer(8000, EchoFactory())
echoService.setServiceParent(application)

twistd

twistd (pronunciado “twist-di”) es una herramienta multiplataforma para desplegar aplicaciones de Twisted. Ejecuta archivos TAC y administra el inicio y la detención de una aplicación. Como parte de las baterías incluidas de Twisted, twistd integra viaras opciones de configuración, incluyendo la utilización de procesos demonio, la ubicación de los archivos de registro, manejo de privilegios, correr en un chroot, correr bajo un reactor personalizado, o incluso ejecutar la aplicación bajo un perfilador.

Podemos ejecutar nuestra aplicación eco con:

$ twistd -y echo_server.tac

En el caso más simple, twistd inicia una instancia de la aplicación como un demonio, registrado en twistd.log. Luego de iniciar y detener la aplicación, el registro se ve de esta manera:

2011-11-19 22:23:07-0500 [-] Log opened.
2011-11-19 22:23:07-0500 [-] twistd 11.0.0 (/usr/bin/python 2.7.1) starting up.
2011-11-19 22:23:07-0500 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2011-11-19 22:23:07-0500 [-] echo.EchoFactory starting on 8000
2011-11-19 22:23:07-0500 [-] Starting factory
2011-11-19 22:23:20-0500 [-] Received SIGTERM, shutting down.
2011-11-19 22:23:20-0500 [-] (TCP Port 8000 Closed)
2011-11-19 22:23:20-0500 [-] Stopping factory
2011-11-19 22:23:20-0500 [-] Main loop terminated.
2011-11-19 22:23:20-0500 [-] Server Shut Down.

Ejecutar un servicio utilizando la infraestructura de aplicaciones de Twisted permite a los desarrolladores evitar código repetitivo para funcionalidades comunes como registro o demonización. También establece una interfaz de línea de comandos estándar para desplegar aplicaciones.

Plugins

Una alternativa al sistema basado en archivos TAC para correr aplicaciones de Twisted es el sistema de plugins. Mientras que el sistema TAC facilita el registro de simples jerarquías de servicios predefinidos con un archivo de configuración, los plugins facilitan el registro de servicios personalizados como subcomandos de la herramienta twistd, y la extensión de la interfaz de la línea de comandos a una aplicación.

Utilizando este sistema:

  • Solo la API del plugin es necesaria para mantenerse estable, lo que facilita la extensión del software para terceros desarrolladores.
  • La detección de plugins está codificada. Estos pueden ser cargados y guardados cuando un programa se ejecuta por primera vez, re-detectados cada vez que el programa se inicia, o consultados varias veces durante la ejecución, permitiendo la detección de nuevos plugins instalados luego de que el programa se haya iniciado.

Para extender un programa utilizando el sistema de plugins de Twisted, todo lo que uno tiene que hacer es crear objetos que implementen la interfaz IPlugin y ponerlos en una ubicación en particular donde el sistema sabe que tiene que buscar.

Ya habiendo convertido nuestro servidor eco a una aplicación de Twisted, transformarla en un plugin es sencillo. Junto con el módulo echo anterior, que contiene las definiciones del Echo y EchoFactory, añadimos un directorio llamado twisted, que contiene un subdirectorio llamado plugins, que alberga la definición de nuestro plugin eco. Este plugin nos permitirá iniciar un servidor eco y especificar el puerto a utilizar como argumentos para la herramienta twistd.

from zope.interface import implements

from twisted.python import usage
from twisted.plugin import IPlugin
from twisted.application.service import IServiceMaker
from twisted.application import internet

from echo import EchoFactory

class Options(usage.Options):
    optParameters = [["port", "p", 8000, "The port number to listen on."]]

class EchoServiceMaker(object):
    implements(IServiceMaker, IPlugin)
    tapname = "echo"
    description = "A TCP-based echo server."
    options = Options

    def makeService(self, options):
        """
        Construct a TCPServer from a factory defined in myproject.
        """
        return internet.TCPServer(int(options["port"]), EchoFactory())

serviceMaker = EchoServiceMaker()

Nuestro servidor eco se mostrará como una opción en el mensaje obtenido al ingresar twistd --help, y al ejecutar twistd echo --port=1235 se iniciará un servidor eco en el puerto 1235.

Twisted viene con un sistema de autenticación para servidores llamado twisted.cred, y un uso comun del sistema de plugins es añadir un patrón de autenticación a la aplicación. Uno puede utilizar twisted.cred AuthOptionMixin para añadir soporte de línea de comandos para varios tipos de autenticación fuera de la plataforma, o agregar uno nuevo. Por ejemplo, puede añadirse autenticación a través de una contraseña local Unix o un servidor LDAP utilizando el sistema de plugins.

twistd incluye plugins para varios de los protocolos soportados por Twisted, que convierten la tarea de montar un servidor en un simple comando. A continuación algunos ejemplos de lo servidores que están integrados.

twistd web --port 8080 --path .

Ejecuta un servidor HTTP en el puerto 8080, sirviendo tanto contenido estático como dinámico en el directorio actual.

twistd dns -p 5553 --hosts-file=hosts

Corre un servidor DNS en el puerto 5553, resolviendo dominios en un archivo llamado hosts en el formato de /etc/hosts.

sudo twistd conch -p tcp:2222

Inicia un servidor SSH en el puerto 2222. Las claves SSH deben configurarse independientemente.

twistd mail -E -H localhost -d localhost=emails

Ejecuta un servidor ESMTP POP3, aceptando emails para localhost y guardándolos en la carpeta emails.

twistd facilita el montaje de un servidor para probar clientes, pero es también código adaptable y listo para producción.

En este sentido, el despliegue de aplicaciones Twisted vía archivos TAC, plugins, y la herramienta twistd ha sido un éxito. De todas formas, de forma anecdótica, la mayoría de las grandes programas Twisted termiaron rescribiendo alguna de estas herramientas de administración y monitoreo; la arquitectura no termina de exponer lo que los administradores de sistemas necesitan exactamente. Esto es un reflejo del hecho de que, históricamente, Twisted no ha tenido mucho aporte de arquitectura por parte de los administradores de sistemas, la gente experta en desplegar y mantener aplicaciones.

Twisted sería adecuado para solicitar agresivamente más comentarios de expertos y usuarios finales al hacer futuras decisiones de la arquitectura en este espacio.

Retrospectiva y lecciones aprendidas

Twisted celebró recientemente su décimo aniversario. Desde sus inicios, inspirado por el mundo de los videojuegos en línea en el año 2000, ha alcanzado su objetivo de ser un motor de red extensible, multiplataforma y dirigido a eventos. Twisted es utilizado en entornos de producción en compañías desde Google y Lucasfilm a Justin.TV y el software de colaboración Launchpad. Implementaciones de servidores en Twisted son el núcleo de numerosas aplicaciones de código abierto, incluyendo BuildBot, BitTorrent, y Tahoe-LAFS.

Twisted ha tenido algunos cambios relevantes en su arquitectura desde el inicio de su desarrollo. Una implementación crucial fue el Deferred, como se comentó anteriormente, para manejar resultados pendientes y su serie de callbacks.

Tuvo lugar una eliminación importante, que casi no tiene entidad en la implementación actual: Twisted Application Persistence.

Twisted Application Persistence

La Twisted Application Persistence (TAP) fue una forma de mantener la configuración y el estado de una aplicación serializados. Correr un programa utilizando este esquema era un proceso de dos pasos:

  • Crear un pickle (véase pickle – Serialización de objetos) que represente una aplicación, utilizando la antigua herramienta mktap.
  • Usar twistd para de-serealizar y ejecutar la aplicación.

Este proceso fue inspirado por las imágenes de Smalltalk, una renuncia a los aparentemente lenguajes de configuración “ad hoc” que eran difíciles de escribir o codificar, y un deseo de expresar los detalles de la configuración en Python.

Los archivos TAP introdujeron inmediatamente una complejidad no deseada. Las clases cambiarían en Twisted sin que las instancias de éstas cambien en los archivos serializados. Intentos de utilizar métodos o atributos de una versión nueva de Twisted en el antiguo objeto serializado haría corromper la aplicación. La noción de “actualizadores” que actualizarían los objetos serializados a las nuevas versiones de las APIs fue introducida, pero esto llevaría a tener que mantener un número de actualizadores, distintas versiones de módulos encargados de la serialización y de-serialización de objetos, y unidades de testeo para cubrir todas las posibilidades; y mantener la cuenta de todos los cambios que presentaba una interfaz era todavía difícil y propenso a errores.

La Twisted Application Persistence y sus utilidades asociadas fueron abandonadas y luego eventualmente removidas de Twisted para ser reemplazas por los archivos TAC y los plugins. El nombre Twisted Application Plugin fue utilizado para encajar con la sigla TAP, y algunas porciones del sistema fallido de serialización aún existen en Twisted.

La lección aprendida del fracaso de la TAP fue que para tener una mantenibilidad razonable, el código persistente necesita un esquema explícito. Más generalmente, fue una lección acerca de añadir complejidad a un proyecto: al momento de considerar introducir un nuevo sistema para resolver un problema, hay que asegurarse que la complejidad de esa solución es bien entendida y probada, y que los beneficios hagan valer claramente esta complejidad antes de añadirlo al proyecto.

web2: una lección de las reimplementaciones

A pesar de no ser una decisión primaria de la arquitectura, la resolución de rescribir la implementación de Twisted Web ha tenido ramificaciones a largo plazo para la imagen del proyecto y la habilidad de los desarrolladores de realizar mejoras a la arquitectura de otras partes del código, por lo que merece una pequeña discusión.

A mediados del año 2000, los desarrolladores de Twisted decidieron hacer una completa rescritura de la API de twisted.web como un poryecto por separado llamado web2. Esta nueva API contendría numerosas mejoras por sobre la implementación original, incluyendo soporte total para el protocolo HTTP 1.1 y una API para datos en streaming.

web2 fue catalogada como experimental, pero terminó siendo usada por la mayoría de los proyectos e incluso fue accidentalmente liberada y empaquetada por Debian. El desarrollo en web y web2 continuó simultáneamente por años, y los nuevos usuarios fueron frustados por la existencia de ambos proyectos y una falta de documentación acerca de cuál utilizar. El cambio a web2 nunca sucedió, y en 2011 web2 fue finalmente removido del código y la página web. Algunas de las mejoras de web2 están siendo lentamente portadas a web.

Parcialmente por web2, Twisted desarrolló una reputación por ser difícil de navegar y estructuralmente confuso para nuevos usuarios. Años más tardes, la comunidad todavía trabaja arduamente para combatir esta imagen.

La lección aprendida de web2 fue que rescribir un proyecto completamente es generalmente una mala idea, pero si tiene que pasar, mejor asegurarse que la comunidad de desarrolladores entienda el plan a largo plazo, y que la comunidad de usuarios tengan una elección clara acerca de cuál implementación utilizar.

Si Twisted podría regresar y hacer web2 nuevamente, los desarrolladores habrían hecho una serie de cambios incompatibles con versiones anteriores y descontinuaciones a twisted.web en lugar de rescribirlo.

Manteniéndose al día con Internet

El modo en el que utilizamos Internet evoluciona constantemente. La decisión de mantener varios protocolos como parte del núcleo del software ha cargado a Twisted con la responsabilidad de manter código para todos esos protocolos. Las implementaciones tienen que evolucionar con los estándares variables y la adopción de nuevos protocolos mientras mantienen estrictamente la política de compatibilidad con versiones anteriores.

Twisted es principalmente un proyecto colaborativo, y el factor limitante para el desarrollo no es el entusiasmo de la comunidad, sino más bien el tiempo de los voluntarios. Por ejemplo, la definición del protocolo HTTP 1.1 (RFC 2616) fue liberada en 1999, el desarrollo en Twisted para su soporte comenzó en 2005 y finalizó en 2009. El soporte para IPv6, definido en el RFC 2460 en 1998, está en progreso pero aun no se ha incluido en el proyecto.

Las implementaciones también tienen que evolucionar a medida que las interfaces que exponen los sistemas operativos cambian. Por ejemplo, el sistema de notificación de eventos epoll fue añadido en Linux 2.5.44 en el año 2002, y Twisted desarrolló un reactor basado en esta herramienta para aprovechas las ventajas de la nueva API. En 2007, Apple liberó OS 10.5 Leopard con una implementación de poll que no soportaba dispositivos, lo que llevaba a un comportamiento lo suficientemente erróneo como para no exponer select.poll en su versión de Python. Desde entonces, Twisted tuvo que hacerse cargo de este problema y documentarlo para los usuarios.

Algunas veces, el desarrollo de Twisted no está a la par con los cambios de todas las herramientas que utiliza, y las mejoras son movidas a librerías por fuera del núcleo del software. Por ejemplo, el proyecto Wokkel, un conjunto de mejoras al soporte para Jabber/XMPP de Twisted, ha vivido como un proyecto para ser combinado con el núcleo del framework por años sin un campeón que supervise la combinación. Se intentó añadir WebSockets a Twisted a medida que los navegadores comenzaron a adoptar el soporte para el nuevo protocolo en 2009, pero el desarrollo se movió a proyectos externos luego de una decisión de no incluir el protocolo hasta que haya migrado desde IETF a un estándar.

Dicho todo esto, la proliferación de liberías y add-ons es un testamento para la flexibilidad y extensibilidad de Twisted. Una estricta política que incluye testeo, documentación y estándares ayudan al proyecto a evitar retrocesos y preservar compatibilidad con versiones anteriores mientras mantienen una amplia lista de protocolos y plataformas soportadas. Es un proyecto maduro y estable que continúa siendo adoptado y teniendo un desarrollo activo.

Twisted considera ser el motor de tu Internet por otros diez años.



Deja un comentario