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.
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.
A parte mais interessante é o login:
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:
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:
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:
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).
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:
Agora, o que fazemos é expor a classe Server via rede pelo DRb:
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.
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:
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!
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:
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):
Muito simples!
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é.
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;
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 é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é.