Distributed Ruby (DRb) + XMPP4R

November 15th, 2008
This article is in Portuguese because I believe that it will be more useful for portuguese-speaking people, since there is a lot of material in English already. If you can speak both of them and would like to contribute, please do. Unfortunately I don’t have enough free time to translate it… yet.

Eu tenho a idéia de escrever esse blog em inglês para praticá-lo. Porém, acredito que escrever um artigo sobre DRb em português seja muito mais proveitoso para meus colegas brasileiros e eu gostaria MUITO de feedback, qualquer dúvida, sugestão ou comentário, fique à vontade.

Nota: Eu não gosto de jogar código completo, mostro as partes mais interessantes/complexas e as explico. As partes mais gerais e fáceis deixo para você, leitor. Se alguma coisa não ficar clara, por favor comente. Este é o meu primeiro tutorial sobre Ruby e posso realmente deixar escapar alguma coisa. Críticas construtivas são sempre bem-vindas.


O Distributed Ruby (DRb) é uma biblioteca padrão do ruby para execução de métodos via rede (similar ao Java RMI). É incrivelmente poderosa e mais incrível, faz parte da biblioteca padrão, ou seja, não precisa instalar absolutamente nada. Sempre me surpreendo com o ruby conforme eu descubro seus poderes.

Assim, esse artigo é um tutorial do uso básico do DRb para criar um instant messenger em Ruby on Rails usando o XMPP4R, para conectar-se via GoogleTalk.

Nota: Eu sei da existência do XMPP4R::Simple, mas tive diversos problemas com seu funcionamento embaixo de um firewall bem seleto: o XMPP4R::Simple usa o @servidor.com do seu JID para conectar no servidor, só que o servidor real do GoogleTalk é talk.google.com, e é esse o único servidor que o firewall mencionado deixava passar. O XMPP4R::Simple, pelo que pude ver, não permite eu fazer essa distinção.

Nota 2: Sei que é possível também realizar o marshalling de uma maneira absurdamente simples (o que torna isso genial), só que o uso de sockets, que por sua vez estendem de IO, impede o marshalling e portanto nada de passar as conexões XMPP4R pela rede.

Abaixo eu escrevi sobre o meu problema e como eu usei o DRb para resolvê-lo. Se você quer ir direto para o tutorial, clique aqui.

Motivação

Eu e uns colegas estávamos fazendo um trabalho para faculdade, um Instant Messenger, usando AJAX e Ruby On Rails. Tudo funcionava perfeitamente usando o Mongrel localmente. Os problemas começaram a aparecer quando eu fui fazer o deploy da aplicação no meu servidor pessoal no DreamHost, usando o ModRails, ou Passenger (que aliás, é fantástico, recomendo). Nunca tinha trabalhado com o mod_rails e, após ler sobre sua arquitetura, descobri o problema.

Após o início do mod_rails, ele carregava algumas instâncias da minha aplicação, de modo a agilizar a resposta de requests. Só que minhas conexões XMPP eram gerenciadas através de um Singleton com um hash (o e-mail da pessoa logada), e isso daria problema em diversas instâncias da minha aplicação, ou até mesmo em uma solução clusterizada. A figuras abaixo ilustram isso, o processo de login, com o usuário “vinibaggio”:



Nessa situação, ao fazer login, o request dispatcher chama o método login do controller MessengerController. Por sua vez, ele faz a chamada para a camada XMPP. O login marcado com o asterisco foi inserido no momento. Em seguida, o usuário é redirecionado para o path /chat.



A figura acima mostra o processo de login usando o mod_rails. A diferença é que o dispatcher escolhe uma instância para executar o login, colocando em sua camada XMPP associada (Singleton).

Ao enviar mensagens, no Mongrel simples, a requisição irá sempre na mesma instância da aplicação, mas como no exemplo dado, é possível que a requisição de login vá a instância 2 do Rails, porém, ao enviar a mensagem, o dispatcher escolhe uma instância para processar a requisição e por isso, pode redirecionar a requisição para a instância 5, que não sabe que o usuário vinibaggio logou-se, retornando erro:



Com o uso do DRb, eu consegui criar a seguinte arquitetura:



Dessa forma, independentemente da instância Rails a ser executada, sempre a mesma camada de XMPP será acessada, podendo então compartilhar as sessões XMPP entre várias instâncias Rails ou até mesmo diferentes computadores em um cluster. Em seguida eu mostro como fiz isso com DRb.

Tutorial

Bom, esse tutorial vem da solução do meu problema que descrevi anteriormente, e então eu vou construir a aplicação RemoteMessenger incrementalmente:

  • Criação do gerenciador de conexão XMPP com o XMPP4R;
  • Criação do servidor DRb;
  • Criação do cliente DRb;
  • Exemplos de uso;
Note que alguns passos eu pretendo pular já que não são objetivos do tutorial e por isso recomendo algumas leituras prévias sobre XMPP4R (a documentação oficial, este tutorial, e sua segunda parte) e de Rails (recomendo os tutoriais e screencasts do Fábio Akita, no seu blog, o AkitaOnRails). Por exemplo, eu não explicarei o XMPP4R nem como funciona o XMPP, e por isso existem as leituras prévias.

Criação do gerenciador de conexão XMPP com o XMPP4R

Primeiramente, eu crio uma classe que faz toda a interface entre Ruby e o XMPP. Ela é bem simples: mantém um Hash que usa o JID do usuário conectado como chave e o valor é o Jabber::Client. Há outras estruturas também, como a pilha de mensagens (que também é um hash com o JID como chave) e a pilha de presence updates.

A parte mais interessante é o login:

    def login(login, password)
      login = "#{login}@gmail.com" unless login.include?("@gmail.com") 
      jid_string = login
      begin
        jid = Jabber::JID.new "#{jid_string}/MyMessenger"
        # Connect
        jabber_client = Jabber::Client.new jid
        jabber_client.connect 'talk.google.com'
        jabber_client.auth password
        jabber_client.send(Presence.new.set_type(:available))
 
        @connections[jid_string] = jabber_client
        @msg_stack[jid_string] = []
        @presence_stack[jid_string] = []
 
        # Setup callbacks
        jabber_client.add_message_callback do |msg|
        unless msg.type == :error or msg.body.nil?
            push_msg msg
          end
        end  
 
        jabber_client.add_presence_callback do |presence|
            push_presence presence
        end
 
      rescue StandardError => error
        jid_string = MESSENGER_ERROR_MSG
      end
 
      jid_string
    end

A única coisa que fizemos aqui é conectarmos no servidor, criar as pilhas e criar os callbacks. Entre as linhas 7 e 10 eu crio uma instância de Jabber::Client com meu JID, coloco o servidor e conecto. A senha é enviada depois que conectado. Depois, ficamos on-line.

Agora, os métodos push_msg e push_presence, responsáveis pelo tratamento de novas mensagens recebidas e de novas atualizações de presença:

    def push_msg(msg)
      from = msg.from.bare.to_s
      to = msg.to.bare.to_s
      body = msg.body
 
      new_msg = {:body => body, :from => from}
      @msg_stack[to].push new_msg
    end

Esse código não tem segredo. Uso algumas coisas do XMPP4R para extrair do XML as informações que quero e colocá-las na minha pilha. O push_presence é um pouco mais complicado:

    def push_presence(presence)
      from = presence.from.bare.to_s
      to = presence.to.bare.to_s
      if presence.type == :unavailable
        status = :unavailable
      else
        if presence.show.nil?
          status = :available
        else
          status = presence.show
        end
      end
 
      new_presence = {:status => status, :from => from}
      @presence_stack[to].push new_presence
    end

Depois de alguns experimentos, percebi que o status varia um pouco de lugar. Se o usuário desconectou, o TIPO de Presence é alterado para :unavailable. Caso contrário, o status ainda depende se a propriedade show tem valor. Se não tiver, significa que o usuário ficou online, senão, é o status que o Presence diz. Dado o tratamento que dou no código, posso estar errado. Infelizmente a documentação do XMPP4R ainda é um pouco escassa.

O resto dos métodos são triviais: receber as mensagens e as presenças é apenas retornar o conteúdo da pilha e limpá-la. O envio de mensagens é apenas isso:

    def send_msg(jid, to, msg)
      jabber_client = @connections[jid]
      msg = Jabber::Message::new(to, msg)
      msg.type = :chat
      jabber_client.send(msg)
    end

Lógico que eu não faço tratamento de erros e portanto não é um código maduro, mas serve para aprender como enviar a mensagem.

A partir de agora, eu chamo essa classe criada de ConnectionPooler: ela gerencia as conexões XMPP. Ainda há muito o que fazer, como dar suporte a mudanças de status, fotos do perfil, etc., mas isso eu ainda, pessoalmente, não sei fazer. Se você tiver interesse, o pessoal da Peepcode tem um screencast sobre o assunto e são meros US$ 9 (e sim, eu descobri a possibilidade de usar o DRb com o XMPP lá no preview do screencast).

Criação do servidor DRb

Agora chegou a parte legal. Você vai ver como é extremamente difícil servir métodos pela rede usando DRb.

Eu criei, antes da conexão com o DRb, uma classe que não faz basicamente nada além de instanciar o ConnectionPool (na verdade eu havia criado como Singleton, e portanto só fiz ConnectionPooler.instance()) e depois métodos para expor os métodos do ConnectionPool. Fica assim:

require 'remote_messenger/connection_pooler'
 
module RemoteMessenger
 
  class Server
 
    def initialize()
      @pooler = ConnectionPooler.instance()
    end
 
    def login(login, password)
      @pooler.login(login, password)
    end
 
    def logout(jid)
      @pooler.logout(jid)
    end
 
    def get_msgs(jid)
      @pooler.get_msgs(jid)
    end
 
    # outros metodos...
 
  end
end

Agora, o que fazemos é expor a classe Server via rede pelo DRb:

  messenger_server = RemoteMessenger::Server.new
  DRb.start_service('druby://localhost:9000', messenger_server)
  DRb.thread.join

Eu dificilmente acho que possa ficar mais simples do que isso. Simplesmente instanciamos nossa classe que faz interface com o Pooler na linha 1. Na linha 2, expomos os métodos de messenger_server no URI definido, protocolo druby, servidor local, porta 9000. A partir desse momento, o servidor DRb já está executando e escutando na porta 9000.

A linha 3 faz com que o servidor fique esperando por conexões (não vou explicar sobre threads em Ruby, mas a documentação oficial é farta e suficiente) e processando-as. A thread do DRb termina quando recebe um sinal de interrupção, como o ^C (Ctrl-C), fazendo o programa terminar.

Sim, é só isso. Eu ainda coloco algum código extra para daemonificar o processo, redirecionando STDIN, STDOUT e STDERR, criando um fork do processo e várias outras coisas. Se você quer ver como é, o código-fonte do projeto Starling, escrito pelo pessoal do Twitter.com, tem um código excelente que faz isso.

Criação do cliente DRb

Criar o servidor é simples, mas criar o cliente é ainda mais simples:

     messenger = DRbObject.new(nil, "druby://localhost:9000")

DRbObject faz o wrapping entre um objeto local e o objeto sendo servido na URI descrita. O primeiro parâmetro é um objeto local que você queira criar um stub. Eu não sei exatamente para que isso possa servir, mas a própria documentação do DRb fala que, normalmente, esse parâmetro é nil. O segundo parâmetro é a URI do nosso serviço.

NOTA Importante: Muitos, e eu digo muitos, tutoriais falam que você precisa fazer DRb.start_service antes de instanciar um DRbObject. Isso NÃO é verdade. Se você o fizer, você irá criar um servidor local, e não é este o objetivo (e isso me causou uma grande dor de cabeça com a DreamHost…), como diz na própria documentação do DRb:
—————————————————— DRb#start_service
start_service(uri=nil, front=nil, config=nil)
————————————————————————
Start a dRuby server locally.

The new dRuby server will become the primary server, even if
another server is currently the primary server.

C’est finit. Não tem mais nada o que fazer. Agora você pode fazer coisas como messenger.login(’meuemail@gmail.com’, ‘minhasenha’).
Agora, aplicando isso em uma aplicação Rails, isso fica muito simples!

Exemplos de uso

Após criar nossa aplicação Rails com um Controller (o meu eu chamei de MessengerController). Estou fazendo muita coisa via AJAX, então ficou mais ou menos assim:

  def login
    login = params[:login]
    password = params[:password]
    username = login
    messenger_proxy = DRbObject.new(nil, "druby://localhost:9000")
    unless password.eql? "" and login.eql? ""
      jid = messenger_proxy.login(login, password)
      session[:jid] = jid
      redirect_to :action => :go_chat
    else
      flash[:error] = "Login or password empty. Try again."
      render :action => :index
    end
  end

Usar o DRb para execução de métodos remotamente é extremamente simples. Eu já havia feito isso usando XML-RPC e fica mais difícil e mais lento. DRb torna tudo bem mais natural no seu código ruby. O login basicamente é isso (sem muito tratamento de erros). Note que eu coloco o JID do usuário na sessão, de forma que o envio de mensagens fica assim:

  def send_msg
    jid = session[:jid]
    to = params[:to]
    msg = params[:msg]
    output = DRbObject.new(nil, "druby://localhost:9000").send_msg(jid, to, msg)
    render :text =>  ""
  end

Usando JSON, podemos fazer a checagem por atualizações da seguinte forma (e eu coloquei Presence updates junto com mensagens de forma a diminuir o número de requests):

  def check_updates
    if request.xhr?
      jid = session[:jid]
      messenger_proxy = DRbObject.new(nil, "druby://localhost:9000")
      msgs = messenger_proxy.get_msgs(jid)
      presences = messenger_proxy.get_presences(jid)
      output = {}
      output[:messages] = msgs unless msgs.eql? RemoteMessenger::MESSENGER_NO_NEW_MSGS
      output[:presences] = presences unless presences.eql? RemoteMessenger::MESSENGER_NO_UPDATES
 
      render :json => output.to_json
    else
      redirect_to :action => :index
    end
  end

Muito simples!

The end

Bom, o básico é isso. Veja que isso é muito poderoso e muito simples. O que fizemos, apesar de simples, não é uma arquitetura trivial e é extremamente poderoso. É incrível o que se pode fazer com ruby de maneira eficiente e eficaz. Eu realmente fiquei muito feliz com a descoberta do DRb e como fica bem interessante unir o DRb com o XMPP4R, dá pra fazer muita coisa bacana.

E como não podia deixar de ser, eu gostaria de agradecer aos meus colegas Paulo R. A. Margarido e Ricardo F. Verhaeg por me ajudarem a criar o messenger e ao Fábio Akita pelo seu excelente blog e tutoriais que me ajudaram demais.

Assim que eu tiver mais material interessante, pretendo escrever sobre ele, mas com certeza os próximos posts serão bem curtos, hehehe.

Até.

Web development , , ,

Writing report, some random/interesting stuff

October 19th, 2008
There’s a lot of things I want to write about, but, unfortunately I have to write a lot for something I don’t want to. Writing for a blog post is so much more interesting than writing an internship report for college, but I have no choice but to do it. So, I’m terribly late, the due date is coming and I’m far from something good. So, no (big) blog posts for a while.

So, here I will post some interesting stuff for you to read (sorry people, I believe everything’s in Brazilian Portuguese):



Como acompanhar suas séries favoritas:

http://baixonivel.blogspot.com/2008/09/como-acompanhar-sries-de-tv-pela.html



EXCELENTE post sobre produtividade pelo Akita (eu sou fã dele, escreve muito bem sobre coisas muito interessantes):

http://www.akitaonrails.com/2008/10/7/off-topic-o-manifesto-gil-ou-como-se-tornar-o-google



Comentário interessante sobre a comunidade Ruby e Ruby on Rails, no Rails Summit (que eu tive a infelicidade de não poder ir). Eu penso algo bem parecido com isso:

http://blog.seatecnologia.com.br/articles/2008/10/19/a-nova-escola-da-ti



Done for the Portuguese part, I’ll soon post a interesting problem I’m having with my Rails application. As I’m a huge fan of OmniGraffle (one of the best pieces of software I’ve ever bought), I’ll try to publish my problem full of cool and stylish graphics.

See you soon, I hope.

Uncategorized

Productivity, Scripting languages, Rails

October 11th, 2008
I’m one of those people who have trouble finishing my own personal projects. I have a lot of unfinished projects (which I might put them online to what I’m calling “Coconut Labs”) or a lot of ideas that doesn’t have even a single line of code/text written down.

On the past I talked about productivity using scripted languages, such as Ruby or Python. They have a lot of libraries that do cool stuff and scripting languages reduce the time you need to have those cool stuff implemented. For example, this Python code, using some libs to play an ogg media file:

import mad, ogg.vorbis, ao
f = ogg.vorbis.VorbisFile("comfortably.ogg")
 
dev = ao.AudioDevice("alsa09")
 
while 1:
 (buffer, bytes, bit) = f.read(4096)
 dev.play(buffer, bytes)

I find this ridiculously easy. It’s very simple (and that may have change, I’ve written this code in 2006, but still prove my point), easy to do and well, you can play a compressed media file in your applications without hassle.

Recently I’ve been playing with Ruby and Ruby on Rails. I loved it. It’s so easy to do web development and I can get my projects to a stage of completeness that I stay motivated to move on with them and then get to a usable point.

And then I got an opportunity to try this. Me and two friends had to pick a project for our Hypermedia class, and we’ve thought of using AJAX and XMPP protocol to create an online Instant Messenger. We then created Chatty, a web application that is able to connect through GoogleTalk servers and then chat with your buddies. It on a very usable point (although is not even close to the maturity of meebo.com) now and as soon as possible I will publish this project, I just need to set things up with my friends. This will be a good opening for my Coconut Labs.

We’re using XMPP4R for the XMPP connection. Although the documentation is somewhat poor (we only have the RDOC, which helped a lot, but I don’t think that’s enough) we hacked something very cool. Here is a snippet of our XMPP layer code:

 def login(login, password)
    jid_string = "#{login}@gmail.com"
    begin
      jid = Jabber::JID.new "#{jid_string}/Chatty"
      # Connect
      jabber_client = Jabber::Client.new jid
      jabber_client.connect 'talk.google.com'
      jabber_client.auth password
      jabber_client.send(Presence.new.set_type(:available))
 
     ...
 
    rescue StandardError => error
      jid_string = CHATTY_ERROR_MSG
    end
  jid_string
end

One of the things that confused us was how to use callbacks and the scope of it’s variables (we’re beginners on Ruby), but I am amazed with our current result, because we:

  • had no prior advanced knowledge on Ruby and Javascript (me)
  • had no prior knowledge of Ruby nor Rails by my other friends (I’m the annoying one that always choose different things to try)
  • are only 3: me, Paulo Rodrigues Alves Margarido and Ricardo Fotao Verhaeg
  • were working a few hours per week (I think I can say 5 hours in average)
  • started the project on mid-August
It loved the result, learned a lot. I love what I do!

That’s all for today!

Productivity, Software development, Web development , ,

Lots of changes and thoughts about software development

September 27th, 2008
I always want to maintain my blog updated but I still didn’t create the habit to do it. I hope I manage to do it someday.

At the beginning of this semester I was checking the classes I would have and I thought: “my god, I’ll have three days with absolutely nothing to do, that can’t go this way”. And so I started to look for an internship, which is obligatory for me to graduate (although I’m not doing it in the right time - the proper time for this would be the next semester).

Very fortunate of me, I’ve found a great place that allows me to work and then travel to have classes on Thurdays and Fridays. The problem is that it is at São Paulo, a 3 hours-by-bus place from São Carlos, where I’m currently doing my undergrad course. Besides all the fuss of moving or having to run to catch the bus on Wednesdays, it’s being a GREAT experience for me.

Until recently, I wasn’t thinking of having a Masters degree, but I’ve changed my mind. At Paggo we use XP (Extreme Programming) for our software development method. It works great! I always feared all the business stuff, with lots of bureaucracy and documentation, which isn’t fun (for me at least), because I love programming and I wouldn’t like to see people to take the fun away from it.

Then I thought, damn, XP is good. It makes our software development process organized, which is something I like (and necessary), but isn’t heavy to maintain. The Productivity Paradigm is solved. And then, I thought once more (yes I have a lot of time to think when commuting): I think it could be a cool thing to do a Master’s on: Agile Methods (not only XP, Scrum seems cool too). Currently I’m reading/listening a lot about it (for the pt_BR-able people: http://www.agilcoop.org.br).

And yes, pair programming is difficult, but works wonders.

I have a lot to say, and some Rails development I did/am doing, so I think I’ll be back on blogging soon!

Career thoughts, Software development , ,

Hello (my inner) world!

June 24th, 2008
This is another blog, but I’ll write it to myself, mostly. I don’t believe there will be many readers, so it’s almost like I’m an schizophrenic, talking to myself in an inner world, heheh. Oh damn, I must hurry, I’m late to class! See ya!

Rants and Thoughts