Mostrando postagens com marcador rails. Mostrar todas as postagens
Mostrando postagens com marcador rails. Mostrar todas as postagens

quarta-feira, 3 de novembro de 2010

Gem brazilian-rails em versões de rails antigas

Se você estiver usando uma versão antiga do brazilian-rails é bem provável que ao tentar instalar esta gem ela comece a instalar também versões superiores do rails. Isso porque esta gem possui um conjunto de dependências definidas somente com o operador ">=".

Ou seja, se você tentar instalar a versão 2.1.13 por exemplo, que funciona com o rails 2.3.2, ele tentará baixar a dependência da gem brdinheiro ">= 2.1.13", acabando por baixar a versão 3.0.0, que depende do activerecord 3.0.0, do rails 3. Nem preciso dizer que isso acabará dando merda.

A solução é baixar todos os componentes independentemente sem suas dependências da seguinte forma:

sudo gem install brnumeros --version=2.1.13 --ignore-dependencies
sudo gem install brdinheiro --version=2.1.13 --ignore-dependencies
sudo gem install brcep --version=2.1.13 --ignore-dependencies
sudo gem install brdata --version=2.1.13 --ignore-dependencies
sudo gem install brhelper --version=2.1.13 --ignore-dependencies
sudo gem install brstring --version=2.1.13 --ignore-dependencies
sudo gem install brcpfcnpj --version=2.1.13 --ignore-dependencies
sudo gem install brI18n --version=2.1.13 --ignore-dependencies
sudo gem install brazilian-rails --version=2.1.13 --ignore-dependencies

quinta-feira, 2 de setembro de 2010

Solucionando IO bloqueante do mysql no ruby

Fui a uma palestra muito interessante sobre performance na Oscon. A palestra No Callbacks, No Threads: Async & Cooperative Web Servers with Ruby 1.9, tratava do problema de IO bloqueante do driver do mysql para ruby, e como solução era proposto o uso de recursos como Event Machine e Fibers do ruby 1.9. Contudo, a solução não ficava nada elegante, tornando a manutenção do código muito complicada. Embora hoje haja um esforço para tornar esse trabalho transparente, consegui obter o mesmo resultado em performance basicamente aumentando o número de processos a atenderem as requisições.

Mas antes de explicar a solução é preciso demonstrar o problema. O caso é que embora tenhamos threads no ruby, alguns drivers como o do mysql são bloqueantes, ou seja, quando estão em uma operação de IO eles bloqueiam o processo inteiro, inclusive todas suas threads. Veja por exemplo o seguinte código:

class TestesController < ApplicationController
def index
Thread.new { Teste.connection.execute("insert into testes (id) select sleep(2)") }
render :text => 'ok'
end
end

Teoricamente ao fazermos a requisição a este controller de teste, a requisição não deveria durar os dois segundos de espera pelo retorno do insert ao mysql. Mas não é isso que acontece. As requisições acabam sendo enfileiradas pois o processo inteiro fica bloqueado a cada execução de query no banco. Isso pode ser comprovado utilizando o Apache Benchmark.

> ab -c 10 -n 10 http://localhost:3000/testes
...
Concurrency Level: 10
Time taken for tests: 20.514 seconds
Complete requests: 10
...

É bom deixar claro que o bloqueio ocorre somente ao usar o método execute do driver, que é responsável por fazer atualizações no banco. Fazendo somente consultas, o bloqueio não ocorre.

Na palestra em questão, foi demonstrado que utilizando Event Machine e Fibers é possivel desbloquear o processo utilizando callbacks. No final o tempo total foi reduzido para 2 segundos e alguns milésimos. Esse mesmo resultado eu obtive configurando um nginx com passenger configurado com 10 forks. Uma solução bem mais limpa. São apenas dois parametros, um do nginx, e outro do passenger:

worker_processes  10;
#...
http {
passenger_max_pool_size 10;
}

E o resultado do teste:

> ab -c 10 -n 10 http://localhost/testes

Concurrency Level: 10
Time taken for tests: 2.424 seconds
Complete requests: 10

É claro que ainda sim a solução proposta na palestra é mais performática, até porque nela o consumo de memória é bem menor. Resta saber se essa economia vale a pena quando se pesa na balança o custo de manter um código mais complicado e os problemas que a concorrência podem trazer ao seu projeto. E para demonstrar o quão escalável é dividir as requisições em processos, fiz ainda um ultimo teste, com 1000 requisições, sendo 200 simultâneas, configurando o nginx e o passenger para trabalhar com 200 forks:

> ab -c 200 -n 1000 http://localhost/testes

Concurrency Level: 200
Time taken for tests: 13.043 seconds
Complete requests: 1000

Ou seja, o tempo total de teste manteve-se estável. Levando- em conta que dificilmente alguém fará um insert no banco com sleep, acredito que esssa seja uma boa solução para o problema de IO bloqueante. Ao invés de threads, utilizar processos.

sábado, 18 de julho de 2009

Dica rápida de charset para projetos rails com mysql

Se você está trabalhando em um projeto ruby on rails com banco mysql, e o charset definido tanto no servidor como no database.yml é UTF-8, tome o cuidado para que inserções no banco por conta de terceiros também seja feito neste charset.

Um simples insert feito na mão utilizando o cliente padrão do mysql pode te atrapalhar bastante. E o erro ficará bem difícil de encontrar pois este mesmo client exibirá o texto corretamente e só o rails entregará o dado incorreto, fazendo você acreditar que o problema está no ruby.

Para evitar isso, sempre que fizer inserts na mão e de massa de dados execute como primeira linha o seguinte comando:

charset utf8;

quinta-feira, 9 de julho de 2009

Teste de aceitação automático com múltiplas configurações no rails

Dado que uma aplicação possua duas maneiras ou mais de funcionar de acordo com alguns parâmetros de inicialização, como proceder com os testes de aceitação automáticos sem ter que iniciar e parar o servidor entre cada cenário ou grupo de cenários?

A solução simples, mas bem funcional que adotamos, foi ter uma task rake para inicializar vários servidores no ambiente local, cada um com um environment e uma porta diferente. Assim, ao testar uma determinada funcionalidade com determinada configuração basta acessar o servidor naquela porta.

A task rake que poderia até mesmo ser um shell script, ficou mais ou menos como mostrado abaixo. Créditos ao Tiago PacMan, mestre em shell, que fez a linha que finaliza os servidores.


desc 'Inicia servidores para teste de aceitação'
task "server:test" do
system 'script/server -d -e test_conf_1 -p 3001'
sleep 1
system 'script/server -d -e test_conf_2 -p 3002'
sleep 1
system 'tail -f log/test.log'
system "ps aux | awk '/3001/{print $2}' | xargs kill -9"
sleep 1
system "ps aux | awk '/3002/{print $2}' | xargs kill -9"
sleep 1
end

Repare que para este caso fizemos uma modificação no environments.rb para unificar os logs de todas os ambientes de teste.

Já no ambiente de integração contínua, onde utilizamos apache com passenger, foi mais simples ainda, bastando definir para diferentes VirtualHosts RailsEnvs diferentes.

quinta-feira, 19 de fevereiro de 2009

Plugin do rails para copiar erros de um model para outro

Em um projeto pessoal precisei desenvolver uma maneira de copiar os erros de um model para outro. Como é uma funcionalidade que outrora já havia precisado, aproveitei para criar um plugin e disponibilizá-lo para quem mais tiver esse mesmo problema.

O plugin está disponível no GitHub pelo endereço http://github.com/timotta/copy_errors_from/tree/master. Para instalar no seu projeto basta rodar a seguinte linha:

script/plugin install git://github.com/timotta/copy_errors_from.git

Após instalar todos os seus models terão o método copy_errors_from, que pode ser utilizado como mostrado abaixo:

> filme = Filme.new :titulo => 'Corra que a polícia vem aí'
> ator = Ator.new
> filme.atores.push ator
> filme.save #return false
> filme.errors.entries #return []
> ator.errors.entries #return [['nome','Não pode ser vazio']]
> filme.copy_errors_from ator
> filme.errors.entries #return [['ator_nome','Não pode ser vazio']]
> filme.errors.on(:ator_nome) #return 'Não pode ser vazia'

sexta-feira, 23 de janeiro de 2009

Cuidado ao cachear named_scope no Rails

Deve-se tomar cuidado ao cachear o acesso a um named_scope pois ele tem um carregamento de forma preguiçosa, o famoso lazy load. Assim, o código abaixo gravaria no cache não os atores femininos, mas sim todos os atores.

class Filme
#...
def atores_femininos_cacheado
Rails.cache.fetch("#{self.id}:atores-principais") { self.atores.femininos }
end

class Ator
#...
named_scope :femininos, :conditions => { :sexo => 'F' }

Se você observar o log, verá que o rails executará a query sem as condições definidas na classe Ator para o named_scope :femininos. E que após ser cacheado, será essa a query que deixará de ser executada.

A query para buscar atores femininos do named_scope só será realmente executada quando algum método do objeto retornado pelo método .atores.femininos for chamado. Veja no exemplo abaixo:

@filme = Filme.find_by_id 10
filme.atores_femininos_cacheado.last

Ou seja, o cacheamento não está servindo ao propósito definido. Uma solução para isso é cachear diretamente o resultado de um find, como pode ser visto abaixo:

class Filme
#...
def atores_femininos_cacheado
Rails.cache.fetch("#{self.id}:atores-principais") do
Ator.find_all_by_filme_id self, :conditions => { :sexo => 'F' }
end
end

quarta-feira, 21 de janeiro de 2009

PGError com Rails no Heroku

Essa dica é para quem utiliza o Heroku Garden para desenvolver suas aplicações rails. Ao rodar os specs diretamente da interface web do Heroku pode ser que um dia você se depare com o seguinte erro:

PGError: ERROR: relation "filmes" does not exist
: SELECT a.attname, format_type(a.atttypid, a.atttypmod),
d.adsrc, a.attnotnull
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = 'filmes'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum

O erro acontece porque o link para rodar as migrations, que aparece na interface do heroku, não as executa no ambiente de testes. Portanto, é preciso abrir a tela do rake e executar as seguintes linhas antes de rodar seus specs.

db:migrate
db:test:prepare

UPDATE

Descobri outro problema do Heroku. O schema.rb é criado e alterado no diretório tmp. Dessa forma para que o PGError não dê novamente em outra atualização do banco, você precisa copiá-lo para o diretório db e então no console do rake rodar:

db:test:clone

quinta-feira, 9 de outubro de 2008

Imagem de fundo no captcha gerado pelo simple_captcha

O simple_captcha, plugin de geração de captcha para rails, não possui opção para colocar um imagem de fundo na imagem gerada. Mas fazer isso não é nenhum bicho de sete cabeças, basta alterar algumas partes chaves do plugin utilizando um pouco de rmagick. Vou mostrar aqui mais ou menos como fiz isso em um recente projeto.

No arquivo simple_captcha_image.rb basta alterar o método generate_simple_captcha_image para que na criação da imagem seja colocado o fundo desejado. Veja o exemplo de código abaixo:

def generate_simple_captcha_image(options={})  #:nodoc

fundo = Magick::Image.read(url_da_imagem_de_fundo).first
preenchimento = Magick::TextureFill.new(fundo)

@image = Magick::Image.new(197, 45, preenchimento) do
self.format = 'JPG'
end

# ...

Repare que existe ali uma chamada ao método url_da_imagem_de_fundo que no caso é um método que pega o path de uma imagem aleatória. Dessa forma o captcha poderá ter vários fundos diferentes. Veja um exemplo de como pode ser a implementação desse método:

  def url_da_imagem_de_fundo
"#{RAILS_ROOT}/vendor/plugins/simple_captcha/assets/imgs/picture_" + (rand(9)+1).to_s + ".jpg"
end

Foi preciso também comentar a chamada ao método que aplica estilo e distorção à imagem, mas não sei bem se isso é necessário. Como fiz isso, precisei alterar o método append_simple_captcha_code que escreve o texto na imagem, de forma a utilizar diversos tamanhos, posições e fontes diferentes.

quarta-feira, 8 de outubro de 2008

Validação no lado cliente com form_remote_tag

Para fazer validações de formulário no lado cliente basta informar ao método form_tag ou ao form_for, utilizando o parâmetro :onsubmit, o código javascript que se deseja executar para validá-lo. Veja no exemplo:

  <% form_tag "/entrar", :onsubmit => 'return valida(this)' do %>
<h1>ok</h1>
<% end %>

Contudo para a tag form_remote_tag e form_remote_for não há a possibilidade de utilizar o parâmetro :onsubmit. Isso porque o formulário gerado pelo rails já possui a ação onsubmit definida com o código que fará o acesso remoto. Para colocar então sua validação antes deste código deve-se usar o parâmetro :before como é mostrado no exemplo:

  <% form_remote_tag :url => "/entrar", 
:update => 'meu_div',
:before => 'if( !valida(this) ) return false' do %>
<h1>ok</h1>
<% end %>

Repare que é importante que haja o "return false" no caso de não ter sido validado, caso contrário ele executará o restante do código colocado pelo rails no onsubmit.

terça-feira, 6 de maio de 2008

Undefined method *_path no uso do form_for

Essa é uma dica boba para aqueles que estão começando em ruby on rails como eu. Fazendo aqui um exemplos simples de cadastro de filmes recebi o seguinte erro:
NoMethodError in Filme#incluir

Showing filme/incluir.html.erb where line #5 raised:

undefined method `filmes_path' for #<ActionView::Base:0xb76f144c>

Extracted source (around line #5):

2:
3: <%= error_messages_for :filme %>
4:
5: <% form_for @filme do |f| %>
6: <p>
7: <b>Título</b><br />
8: <%= f.text_field :titulo %>
Pelo que eu entendi até agora, isso acontece porque eu não estou utilizando os métodos padrões do ruby on rails. Se eu estiver falando besteira por favor me corrijam. O fato é que resolvi isso definindo a ação que o form deverá executar, o código da view então ficaria da seguinte forma:
<h1>Incluir novo filme</h1>

<%= error_messages_for :filme %>

<% form_for @filme, :url => { :action => 'salvar' } do |f| %>
<p>
<b>Título</b><br />
<%= f.text_field :titulo %>
</p>

<p>
<b>Resumo</b><br />
<%= f.text_area :resumo %>
</p>

<p>
<%= f.submit "Incluir" %>
</p>
<% end %>
No caso o controlador possui o método salvar.