miércoles, 5 de diciembre de 2012

Cómo implementar un URL Shortener funcional con Sinatra y Redis

En esta entrada estaremos implementando un servicio web muy común, URL Shortening. Para lograr nuestros objetivos estaremos utilizando Ruby, Sinatra y Redis.

¿Por qué Sinatra?
 Es cierto que hay entornos más completos y funcionales que Sinatra, pero la pega está en que los entornos populares como Rails requieren una pila de dependencias enorme y están orientados para aplicaciones web un poco más complejas y extensas. Sinatra es la opción ideal debido a su simplicidad y su cantidad mínima de dependencias. más información

¿Por qué Redis?
Si bien se podría usar un Hash interno, que de manera más eficiente lleve el registro de todo. El uso de Redis agrega unas cuantas ventajas muy atractivas, como la persistencia de datos, tiempo de vida para los datos y la capacidad de acceder externamente y realizar métricas sobre los datos. más información

El Algoritmo
De toda la aplicación, esta es la parte mas fácil de implementar, pero sin embargo, es la que requiere un poco más de cuidado de nuestra parte. Una mala elección en el algoritmo interno podría resultar en procesamiento innecesario, perdidas de tiempo y por que no, problemas de mantenimiento. A continuación listo algunos acercamientos junto con sus pros y contras.

  1. Strings Aleatorios
    - PROS
             - Fácil de implementar.
    - CONS
             - Se debe comprobar mucho.
             - Se podría necesitar generar varias cadenas aleatorias para obtener una disponible.
  2. Strings Secuenciales
    - PROS
             - Fácil de implementar.
             - No requiere comprobaciones.
    - CONS
             - Cualquier usuario podría predecir direcciones válidas.
  3. Permutación De Carácteres
    - PROS
             - Fácil de implementar.
             - No requiere comprobaciones.
             - Las direcciones no se pueden predecir.
    - CONS
             -
    Para implementar el tercer acercamiento, solo basta con usar el método Array#permutation para generar las secuencias únicas, y para mitigar el problema de la predictibilidad, se baraja el array de carácteres con el método Array#shuffle y listo, solo queda iterar por todas las permutaciones.
    class Url
         def initialize(n=1)
              @n = n
              @seq = mkseq(@n)
         end
    
         def next
              begin
                   @seq.next.join
              rescue StopIteration   
                   @n += 1
                   fail 'No more resources available!' if @n > CHARS.length
    
                   @seq = mkseq(@n)
                   self.next()
              end
         end
    
         private
    
         def mkseq(n, random=true)
              if random
                   CHARS.shuffle.permutation(n)
              else
                   CHARS.permutation(n)
              end
         end
    
         CHARS = [*(0..9), *('A'..'Z'), *('a'..'z')]
    end 
    Es muy importante señalar que es una mala idea convertir el resultado del método Array#permutation en un array, pues podría resultar en una perdida de tiempo del orden de minutos o días. Bueno, implementado ya el corazón de la aplicación, solo nos queda manejar las peticiones con Sinatra y unir todo.

    La mecánica es la siguiente: Cuando un usuario hace una petición a nuestro servidor a través de una URL corta, se toma la segunda parte de la dirección — el recurso o path y se hace una petición a Redis, si dicho recurso existe, el usuario es redirigido al enlace devuelto por Redis utilizando el método redirect, si no existe, entonces en vez de redirigir al usuario, se presenta un aviso con información al respecto.
    #!/usr/bin/env ruby
    #encoding: UTF-8
    require './url.rb'
    require 'redis'
    
    rd_child, wr_parent = IO.pipe()
    rd_parent, wr_child = IO.pipe()
    
    if fork()
         rd_child.close
         wr_child.close
    
         resources = Url.new
         loop do
              IO.select([rd_parent])
              rd_parent.read(1)
              wr_parent.write(resources.next)
         end
    else
         rd_parent.close
         wr_parent.close
    
         require 'sinatra'
    
         redis = Redis.new
    
         set :static, true
         set :public_folder, File.join(File.dirname(__FILE__), 'static')
    
         get '/' do
              erb :index #template
         end
    
         get '/:resource' do
              @url = redis.get(params[:resource])
    
              if @url
                   redirect(@url)
              else
                   halt 400, '<html><head><title>Error</title></head><body><h1>Not Found</h1></body></html>'
              end
         end
    
         post '/create' do
              begin
                   URI.parse params[:url]
              rescue
                   halt 400, '<html><head><title>Error</title></head><body><h1>Bad Url</h1></body></html>'
              end
    
              wr_child.write('g')
              IO.select([rd_child])
              @resource = rd_child.sysread(512)
              redis.set(@resource, params[:url])
    
              erb :index   # tamplate
         end
    end
    Hay un pequeño inconveniente del que no se habla mucho en la documentación oficial de Ruby. Los Fibers no se pueden utilizar a través de múltiples hilos, por ejemplo si intentas ejecutar el siguiente código, producirá un error:
    fiber = nil
    Thread.new do
         fiber = Fiber.new do { Fiber.yield 1 }
    end
    fiber.resume # FiberError: fiber called across threads
    
    Debido a que el método Array#permutation utiliza Fibers y algunos servidores web como Thin usan Threads, en efecto,  para evitar errores en tiempo de ejecución está la decisión de crear dos procesos en el código de arriba, uno para ejecutar Sintra y otro exclusivamente para generar combinaciones. Bien, ahora solo queda la parte web; el front end.

    gina Web
    Bueno, como realmente el diseño es para los diseñadores, me haré de una página que genere una interfaz más o menos aceptable para luego descargar el código y modificarlo a gusto.

    Para lograr una interfaz parecida a la de esta entrada, visita http://www.phpform.org/ y selecciona a gusto los elementos de la página principal, descarga el código y personifícalo, recuerda, la página principal debe quedar lo más simple posible.

    Una cosa más, para mostrar la nueva url al usuario, se utiliza el valor almacenado por la aplicación en la variable de instancia @resource. Cada vez que se acorta un enlace, dicha variable es accesible desde el view, en mi caso el fichero index.erb — el cual elaboré a partir del código generado por la página phpform —. El siguiente es un fragmento del código necesario para mostrar la URL recién creada en el pie (footer) de la página desde el view index.erb.

    <div id="footer">
         <% if @resource %>
              <% url = "http://localhost:4567/#@resource" %>
              <p><a href="<%=url%>"><%=url%></a></p>
         <% else %>
              <p><a href="">Submit new url</a> </p>
         <% end %>
    </div>

    Descargar Aplicación De La Entrada
    Actualización 9/12/12
     Uso del método Array#permutation en lugar del método Array#combination.

    No hay comentarios:

    Publicar un comentario