viernes, 20 de febrero de 2009

ruby "retry" más elegante utilizando module_eval

Muchas veces necesitamos hacer un retry de un bloque de código n veces después de recibir una excepción. Por ejemplo podríamos recibir un "execution expired" tratando de conectarse a un mail server , un "Connection closed" transfiriendo un archivo por ftp, etc.

Como hacerlo al estilo ruby?

Si queremos hacer un retry de 2 o 3 veces es tan simple como esto:


retries = 2
begin
raise "error"
rescue => e
puts e.message
retry if (retries-=1)>0
end


La sentencia raise simula la detección del error en el ejemplo, ya que llama la excepción (manejada por el Objecto Exception) enviando un mensaje llamado "error".

El resultado es:


error
error


Cae al bloque rescue que maneja el error y imprime el error ("puts e.message").

Como hacerlo más elegante? :)

Me gustaría poder hacerlo así:


ok = retry_two do
raise 'error'
end
puts ok


con el siguiente resultado:


error
error
false


Es decir, ejecutar el bloque haciendo un retry de 2 escrito en texto que reciba un bloque y que devuelta si este fue ejecutado correctamente (asignándolo a la variable ok).

Este código permite hacer eso:


require 'rubygems'
require 'linguistics'
Linguistics::use(:en)
module FortyRetry
40.times do |n|
module_eval %Q{
def retry_#{(n+1).en.numwords.gsub(' ','_').gsub('-','_')}
x = #{n} + 1
ok = true
begin
yield
rescue => e
puts e.message
retry if (x-=1)>0
ok = false
end
ok
end
}
end
end


Esta implementación utiliza "module_eval" que evalúa un string o bloque en el contexto de un modulo (utilicé module_eval en ves de define_method por ciertos problemas con los bloques explicados aquí). En este caso definí un modulo llamado "FortyRetry" porque esta genera 40 retries en el siguiente estilo:

retry_numero_en_letras do
...
...
end

hagamosle un "puts FortyRetry.methods" para verlos:


puts FortyRetry.methods.sort


....
public_method_defined?
public_methods
respond_to?
retry_eight
retry_eighteen
retry_eleven
retry_fifteen
retry_five
retry_forty
retry_four
retry_fourteen
retry_nine
retry_nineteen
retry_one
retry_seven
retry_seventeen
retry_six
retry_sixteen
retry_ten
retry_thirteen
retry_thirty
retry_thirty_eight
retry_thirty_five
retry_thirty_four
retry_thirty_nine
retry_thirty_one
retry_thirty_seven
retry_thirty_six
retry_thirty_three
retry_thirty_two
retry_three
retry_twelve
retry_twenty
retry_twenty_eight
retry_twenty_five
retry_twenty_four
retry_twenty_nine
retry_twenty_one
retry_twenty_seven
retry_twenty_six
retry_twenty_three
retry_twenty_two
retry_two
send
singleton_met......

La librería "linguistics" es la encargada de realizar la conversión de números a números-palabras.

Por ejemplo:

10.en.numwords => ten
28.en.numwords => twenty eight

entonces cuando hacemos:

module_eval %Q{
def retry_#{(n+1).en.numwords.gsub(' ','_').gsub('-','_')}
....

Crea los métodos en texto convirtiendo los números a texto-números.

Ya podemos hacerlo elegantemente en nuestro código:


include FortyRetry
puts retry_seventeen{ raise 'error'}
puts retry_six{ raise 'error'}
puts retry_sixteen{ raise 'error'}
puts retry_ten{ raise 'error'}
puts retry_thirteen{ raise 'error'}


Con los siguientes resultados:


error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
false
error
error
error
error
error
error
false
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
error
false
error
error
error
error
error
error
error
error
error
error
false
error
error
error
error
error
error
error
error
error
error
error
error
error
false


Mucho más elegante sin duda :).

jueves, 15 de enero de 2009

Función de busqueda: usando hash o sqlite3?

Muchas veces necesitamos tener una función que devuelva varios valores usando como parámetro uno solo, la mayoría de las veces estás son pequeñas y se almacena usando hash, y si es más grande usualmente se utiliza una base de datos donde se monta una tabla con llave primaria el valor en el búsqueda (por lo tanto única).
Este pequeño articulo no pretende predicar uno o el otro, sino mostrar su beneficios tanto en flexibilidad como en rendimiento.
Cuando estamos hablando de grande cantidades de datos usualmente optamos por el uso de bases de datos, pero a veces estos motores salen sobrando para lo que realmente necesitamos (más ahora que la memoria RAM viene en gigas). Por esta razón se me vino a la mente sqlite que es una micro-base de datos (en su versión 3-3-6-10 pesa tan solo 206.85KiB) gratuita disponible en todos los sistemas operativos de mayor uso (linux, mac, windows).
En este caso solo necesitamos tener los binarios accesibles y poder construir librería que manejen considerable cantidad de datos disponibles sin instalar grandes motores de base de datos para tareas simples, como por ejemplo funciones de conversiones de datos de alto costo computacional pre-computadas.
El problema especifico se basa es la necesidad de una función que reciba un nombre de una empresa y devuelva este nombre en su versión "estandar".

veamos el siguiente ejemplo:

Mac Donalds
Restaurantes Mac Donalds
Rest Mac Donalds
Mac Donalds S.A.

La función que necesitamos recibiría estos nombres y devolvería su nombre "estandar" que sería "Mac Donalds" para todos los casos anteriores.
Esto es muy común en limpieza de datos. En mi caso estas relaciones de estandarización fueron hechas por personas "a pata" ayudados por software.
La herramienta utiliza estos metadatos como fuente, y se vería maso menos así:

std_name('Mac Donalds') => 'Mac Donalds'
std_name('Restaurantes Mac Donalds') => 'Mac Donalds'
std_name('Rest Mac Donalds') => 'Mac Donalds'
std_name('Mac Donalds S.A.') => 'Mac Donalds'

Los metadatos están en un archivo CSV. Se realizaron dos versiones una usando hash's y array's en memoria y otra usando sqlite3.

Estas realizan los siguientes tipos de búsqueda:
  • búsqueda exacta: la versión por match exacto devuelve el standard name en donde el nombre es exactamente igual al suministrado.
  • búsqueda aproximada al estilo 'like': en este caso devolverá el primer match en donde la palabra suministrada este contenida dentro del campo de búsqueda, al estilo de name "like '%campo_parametro%'”

Versión Hash

archivo: std_names_hash.rb
STD_NAME = {}
File.new("#{File.dirname(__FILE__)}/STD_NAMES_MASTER.csv",'r').each{|line|
record = line.gsub(/[\n\r]/,'').split(",",-1)
STD_NAME[record[0]]=[record[1],record[2]]
}
STD_NAME_FLAT = STD_NAME.to_a
def std_names_fake_like name
res = STD_NAME_FLAT.detect{|v| v[0] =~ Regexp.new("^.*(#{name}).*$")}
res||=[nil,[nil,nil]]
res[1]
end


La preparación del Hash es bastante simple, un simple barrido secuencial archivo e su inserción. Para simular búsquedas estilo like convertí el hash en array para poder utilizar el método detect en conjunto con expresiones regulares. El método detect devuelve el primer valor que haga match en este caso con la expresión regular "^.*(valor).*$" usando el objeto Regexp, esta expresión regular equivale al "like '%valor%'" de sql.

Versión Sqlite3
_______
Nota: se necesita tener creada la base de datos corriendo el siente comando:

~#sqlite3 test.db
SQLite version 3.4.2
Enter ".help" for instructions
sqlite> .quit

Le damos '.quit' para salir.
_______

archivo: std_names_sqlite3.rb
`sqlite3 test.db "drop table std_names"`
`sqlite3 test.db "create table std_names(std_name TEXT,bus_name TEXT,sic TEXT)"`
`sqlite3 -separator , test.db ".import STD_NAMES_MASTER.csv std_names"`
`sqlite3 test.db "create index au_name_idx on std_names (std_name)"`
require 'dbi'
@dbh = DBI.connect('DBI:SQLite3:test.db', '', '')
def std_names name
@dbh.select_one("select bus_name,sic from std_names where std_name = '#{name}';") rescue [nil,nil,nil]
end
def std_names_with_like name
@dbh.select_one("select bus_name,sic from std_names where std_name like '%#{name}%';") rescue [nil,nil,nil]
end


La primera linea borra la tabla en caso de que exista, la segunda crea la tabla, la tercera inserta el archivo CSV y la cuarta crea un índice sobre la columna de búsqueda. Están definidos dos métodos, uno para búsquedas exactas y otro para busquedas utilizando like, estas dos devolviendo la primera ocurrencia que haga match.

Para verificar que esta corriendo correctamente corrí cuatro variaciones de nombres de empresas que son la misma (todas deberian de tener el mismo nombre estándar de empresa) para match, y un par de ejemplo match por aproximación con un segmento del nombre de la empresa.

archivo: test_run_std_names.rb
require 'std_names_hash'
require 'std_names_sqlite3'
puts "exact match:"
['SUPERCUTS 90198','SUPERCUTS 90250','SUPERCUTS 90471','SUPERCUTS 9765'].each{|business_name|
puts "hash: #{STD_NAME[business_name][0]}"
puts "sqlite3: #{std_names(business_name)[0]}"
}
puts "using like (or fake like):"
puts "Using sqlite3:"
puts std_names_with_like('PERCU')[0]
puts std_names_with_like('PERCUTS 9')[0]
puts "Using array, detect and regular expresion:"
puts std_names_fake_like('PERCU')[0]
puts std_names_fake_like('PERCUTS 9')[0]

Después de correrlo dio los siguientes resultados:

exact match:
hash: SUPERCUTS
sqlite3: SUPERCUTS
hash: SUPERCUTS
sqlite3: SUPERCUTS
hash: SUPERCUTS
sqlite3: SUPERCUTS
hash: SUPERCUTS
sqlite3: SUPERCUTS
using like (or fake like):
Using sqlite3:
SUPERCUTS
SUPERCUTS
Using array, detect method and regular expresion:
SUPERCUTS
SUPERCUTS


Hasta el momento las dos implementaciones soluciona el problema. Para medir cual de las dos solucionan mejor problema realicé pruebas de rendimiento, a tres niveles, carga de datos, match exacto y match aproximado. Esto se resume en el siguiente script.

archivo: test_hash_vs_sqlite3.rb
require 'benchmark'
puts 'load time:'
Benchmark.bm do |x|
x.report("hash: ") { require 'std_names_hash'}
x.report("sqlite3:") {require 'std_names_sqlite3'}
end
data = []
File.new('test_data.csv','r').each{|line| data.push line.split('","',-1)[9] if line.split('","',-1)[9]}
puts 'exact match:'
Benchmark.bm do |x|
x.report("hash: "){
data.each{|bus_name| STD_NAME[bus_name]}
}
x.report("sqlite3:"){
data.each{|bus_name| std_names bus_name}
}
end
puts 'match using like:'
Benchmark.bm do |x|
x.report("hash: "){
data.each{|bus_name| std_names_fake_like(bus_name)}
}
x.report("sqlite3:"){
data.each{|bus_name| std_names_with_like(bus_name)}
}
end

El primer benchmark mide el tiempo de carga en los dos casos, el recorrido secuencial para la implementación mediante hash y array, y la carga de datos a sqlite y la creación del indice sobre la tabla (con el comando: sqlite3 -separator , test.db ".import STD_NAMES_MASTER.csv std_names").
El segundo mide el tiempo de las funciones de búsqueda "exacta" en hash y con sqlite. Para esta se utiliza datos de prueba en el archivo test_data.csv que contiene 10000 registros.
El tercer benchmark mide el tiempo de ejecución de las funciones de busqueda con expresiones regulares para el caso del archivo y array, y el uso de la sentencia "like" en sql, para una búsqueda aproximada con un segmento del la llave. En este ulitmo caso se utilizaron 100 datos de prueba.
Esto corrió en una Intel(R) Core(TM)2 Duo CPU T7100 @ 1.80GHz, con 2066088 kB de memoria.

Estos fueron los resultados de correr el script test_hash_vs_sqlite3.rb:

load time:
user system total real
hash: 3.500000 0.130000 3.630000 ( 3.677747)
sqlite3: 0.360000 0.060000 5.340000 ( 21.539347)

exact match (test data 10000 rows):
user system total real
hash: 0.020000 0.000000 0.020000 ( 0.022092)
sqlite3: 7.260000 0.430000 7.690000 ( 7.856472)

match using like (test data 100 rows):
user system total real
hash: 599.450000 10.640000 610.090000 (627.246236)
sqlite3: 24.260000 2.530000 26.790000 ( 27.629173)


Cabe mencionar que mi CPU se veía así (95% cpu)cuando computaba la función version array-expresion regular :D.


Con los resultados podemos concluir:

Funciones de búsqueda exacta:
  • utilice hash definitivamente

Función de búsqueda aproximada:
  • Utilice la solución con motor de base de datos
  • Si tiene un motor de bases de datos instalado utilicelo, sino utilice algún motor portarle, gratuito y funcionará con tiempos respuesta bastante aceptables.

Si necesita las dos:
  • combine las dos opciones para búsqueda exacta utilice hash, y para aproximadas la versión con base de datos, en serio vale la pena combinar la solución ya que los hash son invencibles en match exactos.

  • Otras:
  • Las funciones de match exacto son realmente eficientes, los datos de prueba fueron 10000 y corrieron en 3.7 segundos es decir 2702 matcheos por segundo, para la versión de sqlite3 465.1 registros por segundo. Las funciones de match aproximado son mucho más lentas de los de prueba fueron mucho menos y duró considerablemente más que los otros tipos de match.
  • Para el caso de la versión array-expresion regular tomó 627.2 segundos para computar 100 entradas, lo que corresponde a 0.159 por segundo, mientras que la versión sqlite con like 3.6 registros por segundo.
Los ejemplos estan accesible por github.com mediante el siguiente comando:

git clone git://github.com/pabloq/hash-vs-sqlite-examples.git

jueves, 1 de enero de 2009

Instalando ruby "politics" en linux (Ubuntu 8.04)

Revisando las presentaciones de rubyconf2008, seleccionado las que más me llamaron la atención (aún sigo revisándolas, son demasiadas!) vi la de Mike Perham sobre "patterns in distributed processing" en la que presenta una implementación de procesamiento distribuido montado sobre memcached ("high-performance, distributed memory object caching system").
Esto puedo ser bastante util cuando se tiene un problema computacional que puede ser segmentado para su procesamiento paralelo.
Tiene las siguientes dependencias:
  • memcached - the mechanism to elect a leader amongst a set of peers.
  • DRb - the mechanism to communicate between peers.
  • mDNS - the mechanism to discover peers.
Instalando memcached:

~# sudo apt-get install memcached
Reading package lists... Done
Building dependency tree
Reading state information... Done
Suggested packages:
libcache-memcached-perl
The following NEW packages will be installed:
memcached
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 44.6kB of archives.
After this operation, 176kB of additional disk space will be used.
Get:1 http://us.archive.ubuntu.com hardy/universe memcached 1.2.2-1 [44.6kB]
Fetched 44.6kB in 2min1s (367B/s)
Selecting previously deselected package memcached.
(Reading database ... 169259 files and directories currently installed.)
Unpacking memcached (from .../memcached_1.2.2-1_i386.deb) ...
Setting up memcached (1.2.2-1) ...
Starting memcached: memcached.

instalando mdns:

~# sudo gem install net-mdns
Successfully installed net-mdns-0.4
1 gem installed
Updating class cache with 1251 classes...

instalando el cliente de memcached (para ruby):

~# sudo gem install memcache-client
Successfully installed rubyforge-1.0.1
Successfully installed rake-0.8.3
Successfully installed hoe-1.8.2
Successfully installed ZenTest-3.11.0
Successfully installed memcache-client-1.5.0
5 gems installed
Installing ri documentation for rubyforge-1.0.1...
Installing ri documentation for rake-0.8.3...
Installing ri documentation for hoe-1.8.2...
Installing ri documentation for ZenTest-3.11.0...
Installing ri documentation for memcache-client-1.5.0...
Installing RDoc documentation for rubyforge-1.0.1...
Installing RDoc documentation for rake-0.8.3...
Installing RDoc documentation for hoe-1.8.2...
Installing RDoc documentation for ZenTest-3.11.0...
Installing RDoc documentation for memcache-client-1.5.0...


Ahora lo siguiente es arrancar el server memcached:

~# /etc/init.d/memcached start
Starting memcached: memcached.

Ya tenemos todo lo que necesitamos instalado. Ahora nos disponemos a instalar politics y corremos un pequeño ejemplo:

NOTA: para bajar la librería se necesita instalado el git (sudo apt-get install git & sudo apt-get install git-core)


~# git clone git://github.com/mperham/politics.git
Initialized empty Git repository in /home/castor/projects/politics/.git/
remote: Counting objects: 165, done.
remote: Compressing objects: 100% (158/158), done.
remote: Total 165 (delta 76), reused 0 (delta 0)
Receiving objects: 100% (165/165), 32.50 KiB, done.
Resolving deltas: 100% (76/76), done.

Listo. Ahora tenemos una carpeta llamada politics, nada mas no situamos en ella, y vemos un poco la carpeta examples.

~# cd politics/
~# ls
examples History.rdoc lib LICENSE Manifest politics.gemspec Rakefile README.rdoc test
~# cd examples/
~# ls
queue_worker_example.rb token_worker_example.rb

Ok, ahora solo tratemos de correr uno de los ejemplos, en este caso el queue_worker_example.rb.

Veamos como se ve por dentro:

~#cat queue_worker_example.rb
#gem 'mperham-politics'
require 'politics'
require 'politics/static_queue_worker'

# Test this example by starting memcached locally and then in two irb sessions, run this:
#
=begin
require 'queue_worker_example'
p = Politics::QueueWorkerExample.new
p.start
=end
#
# You can then watch as one of them is elected leader. You can kill the leader and verify
# the backup process is elected after approximately iteration_length seconds.
#
module Politics
class QueueWorkerExample
include Politics::StaticQueueWorker
TOTAL_BUCKETS = 20

def initialize
register_worker 'queue-example', TOTAL_BUCKETS, :iteration_length => 60, :servers => memcached_servers
end

def start
process_bucket do |bucket|
puts "PID #{$$} processing bucket #{bucket}/#{TOTAL_BUCKETS} at #{Time.now}..."
sleep 1.5
end
end

def memcached_servers
['127.0.0.1:11211']
end

end
end

Pura vida, entonces nada mas de crear un archivo test para correr varios QueueWorker y listo (como dice al inicio el archivo). Los server memcached se especifican en el método "memcached_servers" en nuestro caso lo tenemos corriendo localmente, si existen otros memchaded servers nada más de agregarlo ahí.

El archivo de ejemplo quedaría mas o menos así:

#test.rb
require 'rubygems'
require 'queue_worker_example'
p = Politics::QueueWorkerExample.new
p.start

Muy importante incluir 'rubygems' para que pueda acceder a los gems que instalamos antes.

Y corramos el test en varias consolas (en este caso 3) y vemos a ver que hacen.

~#ruby test.rb
/usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load -- politics (LoadError)
from /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require'
from ./queue_worker_example.rb:2
from /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require'
from /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require'
from test.rb:2

Nos da un error, ya que no puede ver modulo 'politics'. Nada más modificamos las líneas dos y tres por la siguientes en el archivo queue_worker_example.rb.

require 'politics'
require 'politics/static_queue_worker'

por

require '../lib/politics'
require '../lib/politics/static_queue_worker'

Listo. Ahora si, el script correrá. Para probarlo haré el siguiente pequeño experimento:

1. Arranco los tres procesos, uno de ellos será elegido como líder y será el encargado de asignar el trabajo.
2. Matamos este líder y vemos que tendrá que asignar otro líder.
3. El nuevo lider debería de asignar todo el trabajo al tercer proceso (único worker que queda), este último worker ejecuta todo el trabajo.
4. Esperamos que el último proceso worker termine y damos por concluido el experimento.

Paso 1
consola 1

~# ruby test.rb
I, [2009-01-02T18:03:20.062033 #1925] INFO -- : Registered # in group queue-example at port 58354
I, [2009-01-02T18:03:20.065879 #1925] INFO -- : druby://pc:58354 has been elected leader

consola 2

~# ruby test.rb
I, [2009-01-02T18:03:22.317582 #1930] INFO -- : Registered # in group queue-example at port 48267
I, [2009-01-02T18:03:24.347506 #1930] INFO -- : druby://pc:48267 is processing 19
PID 1930 processing bucket 19/20 at Fri Jan 02 18:03:24 -0600 2009...
I, [2009-01-02T18:03:25.852976 #1930] INFO -- : druby://pc:48267 is processing 17
PID 1930 processing bucket 17/20 at Fri Jan 02 18:03:25 -0600 2009...
I, [2009-01-02T18:03:27.357718 #1930] INFO -- : druby://pc:48267 is processing 15
PID 1930 processing bucket 15/20 at Fri Jan 02 18:03:27 -0600 2009...
I, [2009-01-02T18:03:28.864040 #1930] INFO -- : druby://pc:48267 is processing 13
PID 1930 processing bucket 13/20 at Fri Jan 02 18:03:28 -0600 2009...
I, [2009-01-02T18:03:30.367844 #1930] INFO -- : druby://pc:48267 is processing 11
PID 1930 processing bucket 11/20 at Fri Jan 02 18:03:30 -0600 2009...
I, [2009-01-02T18:03:31.872520 #1930] INFO -- : druby://pc:48267 is processing 9
PID 1930 processing bucket 9/20 at Fri Jan 02 18:03:31 -0600 2009...

consola 3

# ruby test.rb
I, [2009-01-02T18:03:23.748698 #1934] INFO -- : Registered # in group queue-example at port 47170
I, [2009-01-02T18:03:24.777129 #1934] INFO -- : druby://pc:47170 is processing 18
PID 1934 processing bucket 18/20 at Fri Jan 02 18:03:24 -0600 2009...
I, [2009-01-02T18:03:26.280513 #1934] INFO -- : druby://pc:47170 is processing 16
PID 1934 processing bucket 16/20 at Fri Jan 02 18:03:26 -0600 2009...
I, [2009-01-02T18:03:27.793545 #1934] INFO -- : druby://pc:47170 is processing 14
PID 1934 processing bucket 14/20 at Fri Jan 02 18:03:27 -0600 2009...
I, [2009-01-02T18:03:29.295903 #1934] INFO -- : druby://pc:47170 is processing 12
PID 1934 processing bucket 12/20 at Fri Jan 02 18:03:29 -0600 2009...
I, [2009-01-02T18:03:30.800739 #1934] INFO -- : druby://pc:47170 is processing 10
PID 1934 processing bucket 10/20 at Fri Jan 02 18:03:30 -0600 2009...
I, [2009-01-02T18:03:32.305096 #1934] INFO -- : druby://pc:47170 is processing 8
PID 1934 processing bucket 8/20 at Fri Jan 02 18:03:32 -0600 2009...

Paso 2 y 3

Ahora matamos con 'Control + C' el primero (consola 1) y vemos que pasa en las otras consolas.

consola 1

../lib/politics/static_queue_worker.rb:195:in `sleep': Interrupt
from ../lib/politics/static_queue_worker.rb:195:in `relax'
from ../lib/politics/static_queue_worker.rb:97:in `process_bucket'
from ./queue_worker_example.rb:26:in `start'
from test.rb:4

consola 2

E, [2009-01-02T18:03:33.376720 #1930] ERROR -- : Error talking to leader: druby://pc:58354 - #
I, [2009-01-02T18:04:33.380112 #1930] INFO -- : druby://pc:48267 has been elected leader

consola 3

E, [2009-01-02T18:03:33.826448 #1934] ERROR -- : Error talking to leader: druby://pc:58354 - #
I

Paso 4

Ya el líder está en la consola 2, el tercero nada más ejecutaría el trabajo restante.

consola 3

I, [2009-01-02T18:04:33.829416 #1934] INFO -- : druby://pc:47170 is processing 19
PID 1934 processing bucket 19/20 at Fri Jan 02 18:04:33 -0600 2009...
I, [2009-01-02T18:04:35.334133 #1934] INFO -- : druby://pc:47170 is processing 18
PID 1934 processing bucket 18/20 at Fri Jan 02 18:04:35 -0600 2009...
I, [2009-01-02T18:04:36.836062 #1934] INFO -- : druby://pc:47170 is processing 17
PID 1934 processing bucket 17/20 at Fri Jan 02 18:04:36 -0600 2009...
I, [2009-01-02T18:04:38.340842 #1934] INFO -- : druby://pc:47170 is processing 16
PID 1934 processing bucket 16/20 at Fri Jan 02 18:04:38 -0600 2009...
I, [2009-01-02T18:04:39.844057 #1934] INFO -- : druby://pc:47170 is processing 15
PID 1934 processing bucket 15/20 at Fri Jan 02 18:04:39 -0600 2009...
I, [2009-01-02T18:04:41.349662 #1934] INFO -- : druby://pc:47170 is processing 14
PID 1934 processing bucket 14/20 at Fri Jan 02 18:04:41 -0600 2009...
I, [2009-01-02T18:04:42.853141 #1934] INFO -- : druby://pc:47170 is processing 13
PID 1934 processing bucket 13/20 at Fri Jan 02 18:04:42 -0600 2009...
I, [2009-01-02T18:04:44.356687 #1934] INFO -- : druby://pc:47170 is processing 12
PID 1934 processing bucket 12/20 at Fri Jan 02 18:04:44 -0600 2009...
I, [2009-01-02T18:04:45.860273 #1934] INFO -- : druby://pc:47170 is processing 11
PID 1934 processing bucket 11/20 at Fri Jan 02 18:04:45 -0600 2009...
I, [2009-01-02T18:04:47.363697 #1934] INFO -- : druby://pc:47170 is processing 10
PID 1934 processing bucket 10/20 at Fri Jan 02 18:04:47 -0600 2009...
I, [2009-01-02T18:04:48.869261 #1934] INFO -- : druby://pc:47170 is processing 9
PID 1934 processing bucket 9/20 at Fri Jan 02 18:04:48 -0600 2009...
I, [2009-01-02T18:04:50.372352 #1934] INFO -- : druby://pc:47170 is processing 8
PID 1934 processing bucket 8/20 at Fri Jan 02 18:04:50 -0600 2009...
I, [2009-01-02T18:04:51.873700 #1934] INFO -- : druby://pc:47170 is processing 7
PID 1934 processing bucket 7/20 at Fri Jan 02 18:04:51 -0600 2009...
I, [2009-01-02T18:04:53.376756 #1934] INFO -- : druby://pc:47170 is processing 6
PID 1934 processing bucket 6/20 at Fri Jan 02 18:04:53 -0600 2009...
I, [2009-01-02T18:04:54.880311 #1934] INFO -- : druby://pc:47170 is processing 5
PID 1934 processing bucket 5/20 at Fri Jan 02 18:04:54 -0600 2009...
I, [2009-01-02T18:04:56.384764 #1934] INFO -- : druby://pc:47170 is processing 4
PID 1934 processing bucket 4/20 at Fri Jan 02 18:04:56 -0600 2009...
I, [2009-01-02T18:04:57.888371 #1934] INFO -- : druby://pc:47170 is processing 3
PID 1934 processing bucket 3/20 at Fri Jan 02 18:04:57 -0600 2009...
I, [2009-01-02T18:04:59.392192 #1934] INFO -- : druby://pc:47170 is processing 2
PID 1934 processing bucket 2/20 at Fri Jan 02 18:04:59 -0600 2009...
I, [2009-01-02T18:05:00.897304 #1934] INFO -- : druby://pc:47170 is processing 1
PID 1934 processing bucket 1/20 at Fri Jan 02 18:05:00 -0600 2009...
I, [2009-01-02T18:05:02.399987 #1934] INFO -- : druby://pc:47170 is processing 0
PID 1934 processing bucket 0/20 at Fri Jan 02 18:05:02 -0600 2009...
I, [2009-01-02T18:05:03.905741 #1934] INFO -- : No more buckets in this iteration, sleeping for 29.474557 sec

Listo ya instalamos los pre-requisitos, instalamos 'politics' y probamos con un ejemplo.

En una próxima entrada, utilizaré 'politics' para distribuir trabajo en varios servers memcached.