lunes, 30 de noviembre de 2009

PSGI y Plack: el futuro de las aplicaciones web

[English translation]
Hace unas semanas le mostraba a mi amigo Joel un one-liner de Perl que implementaba un servidor web, tal vez tenía mucho trabajo que hacer porque no pareció sorprendido por esta fantástica línea de perl que usa el módulo IO::All:

perl -MIO::All -e 'io(":8080")->fork->accept->(sub { $_[0] < io(-x $1 ? "./$1 |" : $1) if /^GET \/(.*) / })'

Pero además sorprendentemente (sobre todo para un fanático de Perl) su respuesta fue: "Sabes que la gente de Python tiene software para implementar servidores web con muchísima facilidad, aunque no se cuál es", ahí me dí cuenta de que no entendió el punto, tal vez lo agarre en un mal momento así que lo deje ir.

Pero ya me había intrigado, aunque estaba seguro que se refería a WSGI (también conocido como el PEP-333): una especificación para un API de aplicaciones web, permitiendo la separación e responsabilidades entre la interfaz (política) y su implementación (mecanismos), de manera que los desarrolladores se pueden preocupar por desarrollar y optimizar los mecanismos independientemente de las aplicaciones que los utilizan.

En Perl ese era el trabajo de HTTP::Engine utilizado entre otros por Catalyst.

Sin embargo, me quedó la curiosidad y me puse a buscar en CPAN, ¿habría algo nuevo por allí?. Encontré módulos como Mojo, que utilizan internamente una interfaz similar a WSGI, sin embargo lo más interesante que conseguí fue PSGI y Plack.

Al parecer HTTP::Engine está lejos de ser una solución ideal. Según he leído es monolítico, difícil de adaptar y no muy eficiente, para ambientes integrados (embedded) supongo. Lo cierto es que Miyagawa decidió separar HTTP::Engine en tres partes:
  1. La Especificación: PSGI
  2. Una implementación de referencia: Plack::Server
  3. Herramientas: Plack::*

Lo más interesante de PSGI y Plack es la rapidez con la que se implementó, hace solo semanas era una idea y ya están disponibles desde hace algún tiempo implementaciones de referencia que permiten ejecutar aplicaciones Plack por si mismas (standalone) en un solo hilo o con perfork, también hay interfaces para FastCGI, CGI y por supuesto mod-perl, y como si esto fuera poco, PSGI tiene la capacidad trabajar con sin bloqueo de entrada/salida (non blocking I/O), así que se hicieron servidores basados en POE, AnyEvent y Coro, incluso ya está disponible un módulo de PSGI para Apache (mod-psgi).

Por otra parte, de la nada aparecieron adaptadores de PSGI para frameworks como Catalyst (Catalyst::Engine::PSGI), Squatting (Squatting::On::PSGI), CGI::Application (CGI::Application::PSGI), Dancer y hasta para WebGUI (PlebGUI), también hay herramientas para facilitar la migración de otras tecnologías a PSGI, por ejemplo si tienes alguna aplicación escrita para HTTP::Engine, puedes utilizarla prácticamente sin modificación en PSGI con HTTP::Engine::Interface::PSGI , si tienes alguna aplicación CGI tienes la oportunidad de migrarla con muy pocas modificaciones con CGI::PSGI, y si aún esto es demasiado trabajo puedes usar CGI::Emulate::PSGI que permite ejecutar los CGI como un servidor desde la línea de comandos!.

En el articulo anterior hice un servidorcito de documentos POD que implementé como CGI, seguramente más de uno tuvo problemas para hacerlo funcionar, porque hay que montar el web server y configurar el CGI entre otros. Usando CGI::Emulate::PSGI solamente escribimos un programa que inicie el servidor (perldocweb_starter):

1 use CGI::Emulate::PSGI;
2 my $app = CGI::Emulate::PSGI->handler(sub { do "perldocweb" })

y luego ejecutamos el comando plackup:

$ plackup perldocweb_starter
Plack::Server::Standalone: Accepting connections at http://0:5000/

y ahora tenemos nuestro servidor de documentación ejecutándose en el puerto 5000, así que al visitar:

http://localhost:5000/perldocweb?PSGI

Debería aparecer la especificación de PSGI en el navegador, fácil ¿no?.

Ahora si estamos dispuestos a tocar el código del programa, no necesitaremos el emulador y podremos ejecutar la aplicación directamente con plackup, lo cual es mucho más eficiente.

La primera modificación es cambiar la línea 4 para usar CGI::PSGI, además ya no se usa CGI::Carp, porque Plack tiene una manera mucho más elegante de mostrar los errores utilizando Devel::StackTrace::AsHTML.

Cuando usamos CGI::PSGI el programa debe crear (y retornar) una clausura que será nuestra aplicación así que el código principal entre las líneas 20 y 50 debe encerrarse en una clausura, además la línea 20 ahora debe inicializar un objeto CGI::PSGI, así que la reemplazamos por las líneas 20 a 22 de en la nueva aplicación:

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use CGI::PSGI;
 5 use IO::File;
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my %content_types = (
10     RTF   => "application/rtf",
11     LaTeX => "application/x-latex",
12     PDF   => "application/pdf",
13 );
14 my @wikis   = qw(Usemod Twiki Template Kwiki Confluence Moinmoin Tiddlywiki Mediawiki Textile);
15 my %formats = (
16     ( map { $_ => "Pod::Simple::$_" } keys %content_types ),
17     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
18 );
19 
20 my $app = sub {
21     my $env      = shift;
22     my $q        = CGI::PSGI->new($env);
23     my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
24     my $format   = $q->param("format") || "HTML";
25     given ($format) {
26         when ("source") {
27             return [ $q->psgi_header("text/plain"), IO::File->new($filename) ];
28         }
29         when ('HTML') {
30             my $parser = Pod::Simple::HTML->new;
31             $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
32             my $footer = "<hr>"
33                 . join( " ", map { make_link( $_, $q ) } "source", keys %content_types )
34                 . " | Wiki formats: "
35                 . join( " ", map { make_link( $_, $q ) } @wikis );
36             $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
37             $parser->output_string( my $output );
38             $parser->parse_file($filename);
39             return [ $q->psgi_header("text/html"), [$output] ];
40         }
41         when (%formats) {
42             my $class = $formats{$format};
43             eval "require $class";
44             my $parser = $class->new;
45             $parser->output_string( my $output );
46             $parser->parse_file($filename);
47             return [ $q->psgi_header( $content_types{$format} || "text/plain" ), [$output] ];
48         }
49         default {
50             die("Formato desconocido '$format'");
51         }
52     }
53 };
54 
55 sub make_link {
56     my $fmt = shift;
57     my $q   = shift;
58     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
59 }

La clausura recibe como parámetro el ambiente de PSGI (21) y lo utiliza para crear el objeto $q que usaremos como si fuera un objeto CGI. Esta clausura debe retornar un arreglo de dos elementos:
  1. Los encabezados: un arreglo de nombres y valores alternados
  2. El cuerpo: que debe ser un arreglo de líneas o un objeto IO::Handle

Una diferencia fundamental entre CGI::PSGI y CGI es que en el segundo se envía al navegador la salida estándar (STDOUT), mientras que en el primero, se retorna el cuerpo.

Así que la generación del contenido en la aplicación debe ser modificada. En el caso del código fuente (línea 26) ha quedado más simple, solo se retornan los encabezados junto con un objeto IO::Handle (creado con IO::File). y CGI::PSGI se encarga de leer los datos del objeto y enviarlos al navegador, de hecho en el caso de que sea un archivo real (como en este caso) y que el sistema operativo implemente sendfile(2) (como en mi caso que uso linux), el envío de los datos se realiza completamente en el kernel así que no habrá diferencia de eficiencia entre este programa y uno optimizado hecho en C (como apache).

En el caso de HTML (línea 29) he cambiado el uso de output_fh por output_string para que el contenido generado por Pod::Simple quede en $output, que se retorna en línea 39.

Como ya no se puede usar el STDOUT para enviar el contenido al navegador tampoco podemos utilizar el atajo $class->filter de Pod::Simple, así que lo he reemplazado por su equivalencia en las líneas 44 a 46 de la nueva aplicación.

Aunque tal vez no es obvio, el código retorna la clausura (línea 20) porque es el último valor que se calcula, ya que lo que sigue es una declaración.

Si llamamos a nuestro nuevo programa "server_pod" podremos arrancarlo con plackup de la siguiente manera:

$ plackup server_pod
Plack::Server::Standalone: Accepting connections at http://0:5000/

y podemos visualizar el contenido de los POD utilizando el navegador como ya se indicó, el servidor que utiliza plackup por defecto (Plack::Server::Standalone) es de un proceso de un solo hilo así que es ideal para el desarrollo o para una aplicación personal, pero si necesitas un servidor con calidad de producción debes ver otras opciones, la más recomendable para código que viene de CGI es probablemente Plack::Server::Standalone::Prefork, que puedes arrancar así:

$ plackup -s Standalone::Prefork server_pod
Plack::Server::Standalone: Accepting connections at http://0:5000/

Eso fue fácil, se utilizan valores por defecto para todo, pero si necesitas entonar el rendimiento del servidor puedes hacerlo dado opciones en la línea de comandos, las opciones generales se documentan en plackup y las de cada servidor en su clase respectiva.

Finalmente este código es mucho más eficiente que el del emulador del primer ejemplo porque no se necesita utilizar archivos temporales para capturar la salida estándar, aún así puede ejecutarse bajo Apache en modo CGI, FastCGI o incluso en mod-perl.

En una próxima ocasión mejoraré la aplicación utilizando directamente Plack y su middleware.

2 comentarios: