Archive for September, 2009

Rails for Kids 2009

O Rails for Kids foi uma maratona beneficente de palestras on-line realizada no dia 12/09/2009  que contou com grandes nomes da comunidade Ruby on Rails do Brasil e de fora. Nós da Plataforma Tecnologia fomos representados por mim (Hugo Baraúna) e pelo George Guimarães.

A qualidade do evento foi surpreendente, ainda mais quando levamos em consideração que ele foi realizado totalmente on-line. Na minha opinião, a organização do Rails for Kids foi excelente pois juntou um respeitado time de palestrantes, utilizou uma ferramenta para apresentações online/e-learning  muito boa e teve presença em peso da comunidade. Parabéns ao Carlos Eduardo pela organização!  Eventos assim são sempre bem-vindos! =)

Mas para aqueles que não puderam participar do evento, não se preocupem. Ainda há uma chance: as palestras foram gravadas! Basta entrar no site do Rails for Kids e fazer uma simbólica doação.  Você ajuda quem precisa e assiste a palestras de primeira qualidade. É um benefício em dose dupla! =D

Se não puder ajudar com doações, divulgue. Vale lembrar que o objetivo maior é ajudar crianças carentes.

Valeu pessoal!

obs.: para ler um bom resumo das palestras do evento, dê uma olhada nesse post do Carlos Antônio, da para ter uma ótima idéia!

Sometimes users want to slice and dice data as they wish. In such scenarios, it’s usual to export the data in a tabular format so your users can use any spreadsheet editor and do whatever they want.

Usually, we do that using CSV, right? OpenOffice and other editors can open CSV files flawlessly. Just double-click an CSV file and voilà.

However, Excel has some gotchas. If you double-click a CSV file, Excel will open your data with everything in just one column. Of course you can go to Tools > Import, browse to the file, set up comma as separator and then after some clicks you get your data as you wish. This is not user-friendly at all and this is not something we want to explain to our clients. Our clients must be able to simply double-click the my_data.csv file and see the data well structured.

So, here’s what we do at Plataforma to deal with Excel formats.

It’s TSV, not CSV, dude!

First of all, Excel expects your data with tabulations as fields separator. So, what you need is actually a TSV (tab-separated values).

If you are using FasterCSV, you just need to do:

tsv_str = FasterCSV.generate(:col_sep => "\t") do |tsv|
  tsv << headers
  # append your beautiful data here
end

Keep this in mind. Excel demands tabulations, not commas! But there are worse things to come…

No newline in fields

Excel doesnt’ like when you put “\n” inside fields. Although fields are separated by tabulations, it appears Excel can’t cope with extra newlines.

So, if you have some text fields in your model, beware that it may contain newlines and you need to strip those before exporting your data to Excel.

Forget about UTF-8. Use UTF-16!

One of the most hidden specifications of Excel is that it expects our TSV files to be encoded using UTF-16 Little Endian. Did you know that? Well, we didn’t!

Some sources even say that this is the only Unicode format supported in Excel.

What’s the difference between UTF-8 and UTF-16? UTF-8 is a variable byte encoding in which characters may use up to 4 bytes, however for western languages it usually uses one or two bytes. UTF-16 characters always uses at least 2 bytes (remember that characters and bytes are two different things in Unicode “slang”). Generally speaking, UTF-16 takes more space in the filesystem (most of the time).

There’s also the Little Endian part. UTF-16 always uses a pair of bytes to represent a character, however we need to know what’s the correct order of those bytes. We won’t get into details here, but the order is indicated by Byte-order Mark (BOM). In practice, the BOM in UTF-16 will add two bytes in the beginning of a file (you can see that in a hexeditor).

One way to convert your TSV string (generated by FasterCSV) is with Iconv, a tool written for the GNU C Library. Luckly, Iconv is well wrapped in pure Ruby joy. It’s inside Ruby’s standard library and you just need to require it.

However, if you convert an string to UTF-16 Little Endian, Iconv will NOT put the BOM in the beginning. This is compatible with the Unicode FAQ. But since Excel is way out of the standards, you must manually insert the BOM.

You can use Iconv just before sending the file to the user in your controller. It will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'iconv'
 
class ProjectsController < ActionController::Base
  BOM = "\377\376" #Byte Order Mark
 
  def index
    @projects = Project.all
    respond_to do |format|
      format.html
      format.csv { export_csv(@projects) }
    end
  end
 
protected
 
  def export_csv(projects)
    filename = I18n.l(Time.now, :format => :short) + "- Projects.csv"
    content = Project.to_csv(projects)
    content = BOM + Iconv.conv("utf-16le", "utf-8", content)
    send_data content, :filename => filename
  end
end

You can see that we are still using CSV as an extension to the file. This is because TSV files are not usually associated with Excel. This excerpt uses a timestamp as the filename, which is generally a good practice.

Wrapping up

So, these are the three rules for dealing with Excel-friendly-CSV:

  1. Use tabulations, not commas.
  2. Fields must NOT contain newlines.
  3. Use UTF-16 Little Endian to send the file to the user. And include a Little Endian BOM manually.

Keep this in mind and you’ll never have to explain to your clients how to open the data export in Excel.

There is one last issue: OpenOffice will NOT open files in the Excel “specification” easily. Google Analytics solves this by showing two links to the user: “Export to CSV” and “Export to Excel”. The first is a regular CSV file and the second is the specially crafted Excel-friendly TSV file.

And blame Microsoft for this odd behaviour.

UPDATE: @danielvlopes pointed us an already existing solution which encapsulates the process described in this post, called csv_builder. You just need to set the @output_encoding to use the “utf-16″ (beware of the BOM).

@jncoward also provided a link to the spreadsheet gem, which handles directly Excel formats. TSV is a much simpler and faster format, but the spreadsheet gem might be useful to you in more complex cases.

Finally, Kieran pointed in the comments that we can have XML spreadsheets, while Niko and Chris told us that we can you can also work with HTML tables in Excel, which is simple as well and even allows formatting.

To see this post in English, click here.

Às vezes os usuários querem manipular os dados de diversos modos. Nesses cenários, é comum exportar os dados num formato de tabela para que se possa usar um editor de planilhas e então filtrar, particionar e mudar os dados da forma que o usuário quiser.

Generalmente, fazemos isso usando CSV, certo? O OpenOffice e outros editores podem abrir arquivos CSV sem problemas. É só clicar duas vezes no arquivo CSV e voilà, os dados aparecem.

Entretanto, o Excel não funciona exatamente da mesma forma. Se você clicar duas vezes no arquivo, o Excel vai lhe mostrar todo o conteúdo em apenas uma coluna. Claro que você poderia ir em Ferramentas > Importar dados, navegar até o arquivo, selecionar a vírgula como separador de campos e então, após alguns cliques você terá seus dados como desejado. Isso não é nada amigável para o usuário e é o tipo de coisa que não queremos explicar para nossos clientes. Nossos clientes devem ser capazes de simplemente clicar duas vezes no arquivo meus_dados.csv e ver os dados bem estruturados.

Tendo isso em mente, é assim que fazemos na Plataforma para lidar com os formatos do Excel.

É TSV, e não CSV, rapaz!

O Excel espera que seus dados venham com tabulações como separador de campo. Então, o que você precisa na verdade é um TSV (tab-separated values).

Se você está usando o FasterCSV, só precisa fazer:

tsv_str = FasterCSV.generate(:col_sep => "\t") do |tsv|
  tsv << headers
  # coloque seus bonitos dados aqui...
end

Não esqueça! Excel exige tabulações, e não vírgulas. Mas tem coisa pior vindo…

Sem quebras de linha nos campos

O Excel não gosta quando você põe um “\n” dentro dos campos. Apesar dos campos serem separados por tabulações, ele parece não entender o que a quebra de linha está fazendo ali.

Então, se você tem campos de texto no seu modelo, tome cuidado. Eles podem conter quebras de linhas e você precisa tirá-las antes de exportar seus dados para o Excel.

Esqueça UTF-8. Use UTF-16!

Uma das especificações mais escondidas do Excel é que ele espera que seus arquivos TSV sejam codificados usando UTF-16 Little Endian. Você sabia disso? Bem, nós não!

Alguns até dizem que este é o único formato Unicode suportado pelo Excel.

E qual a diferença entre UTF-8 e UTF-16? O UTF-8 é uma codificação de tamanho variável, cujos caracteres podem usar até 4 bytes, mas para idiomas ocidentais generalmente é usado um ou dois bytes. Caracteres UTF-16 usam sempre 2 bytes para serem representados (lembre-se que caracteres e bytes são duas coisas bem diferentes no mundo Unicode). Simplificando bastante as coisas, UTF-16 usa mais espaço (na maioria das vezes).

E tem também a parte do Little Endian. O UTF-16 sempre usa um par de bytes para representar um caractere. Contudo, nós precisamos saber qual a ordem certa desses bytes. Não vamos entrar em detalhes aqui, mas essa ordem é indicada pelo Byte-order Mark (BOM). Na prática, o BOM no UTF-16 vai adicionar dois bytes no começo do seu arquivo (você pode ver isso usando um editor hexadecimal).

Um dos jeitos de converter sua string TSV (gerado pelo FasterCSV) é com o Iconv, uma ferramenta escrita para a Biblioteca C do GNU. Felizmente, o Iconv está bem empacotado em pura felicidade Rubyana. Está dentro do biblioteca padrão do Ruby e vocÊ só precisar dar um require nela.

Entretando, se você converter uma string para UTF-16 Little Endiand, Iconv NÃO irá colocar o BOM no começo. Isso é compatível com a especificação do Unicode. Mas já que o Excel está totalmente fora dos padrões, você deve inserir manualmente o BOM para aumentar a compatibilidade.

Você pode usar o Iconv na hora que enviar o arquivo ao usuário dentro do seu controller. Ficaria mais ou menos assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require 'iconv'
 
class ProjectsController < ActionController::Base
  BOM = "\377\376" #Byte Order Mark
 
  def index
    @projects = Project.all
    respond_to do |format|
      format.html
      format.csv { export_csv(@projects) }
    end
  end
 
protected
 
  def export_csv(projects)
    filename = I18n.l(Time.now, :format => :short) + "- Projects.csv"
    content = Project.to_csv(projects)
    content = BOM + Iconv.conv("utf-16le", "utf-8", content)
    send_data content, :filename => filename
  end
end

Perceba que estamos usando CSV como extensão do arquivo. Isso é porque geralmente arquivos TSV não estão associados ao Excel. Este trecho usa um timestamp como nome de arquivo, o que é geralmente uma boa prática.

Resumindo

Essas são as 3 leis para lidar com CSVs para o Excel:

  1. Use tabulações, e não vírgulas.
  2. Campos NÃO podem conter quebras de linha.
  3. Use UTF-16 Little Endian para enviar o arquivo ao usuário. E adicione o BOM do Little Endian manualmente.

Tenha isso em mente e você nunca terá que explicar para seus clientes como abrir os dados exportados no Excel.

Há um último problema. O OpenOffice não irá abrir facilmente arquivos feitos na “especificação” do Excel. O Google Analytics resolve este problema mostrando dois links ao usuário: “Exportar para CSV” e “Exportar para Excel”. O primeiro é o CSV normal e o segundo é o arquivo TSV feito especialmente para o Excel.

E culpe a Microsoft por este comportamento estranho.

UPDATE: @danielvlopes nos avisou de uma solução existente que encapsula o processo deste post, chamado csv_builder. Você só precisa configurar o @output_encoding para usar “utf-16″ (preste atenção com o BOM).

@jncoward também enviou um link para a gem spreadsheet, que escreve formatos nativos do Excel. TSV é um formato mais simples e mais rápido, mas a gem spreadsheet pode lhe ajudar em casos mais complexos.

Everyone already heard about scalability at least once. Everyone already heard about memcached as well. What not everyone might heard is the dog-pile effect and how to avoid it. But before we start, let’s take a look on how to use Rails with memcached.

Rails + Memcached = <3

First, if you never used memcached with rails or never read/heard a lot about scalability, I recommend checking out Scaling Rails episodes done by Gregg Pollack, in special the episode about memcached.

Assuming that you have your memcached installed and want to use it on your application, you just need to add the following to your configuration files (for example production.rb):

1
config.cache_store = :mem_cache_store

By default, Rails will search for a memcached process running on localhost:11211.

But wait, why would I want to use memcached? Well, imagine that your application has a page where a slow query is executed against the database to generate a ranking of blog posts based on the author’s influence and this query takes on average 5 seconds. In this case, everytime an user access this page, the query will be executed and your application will end up having a very high response time.

Since you don’t want the user to wait 5 seconds everytime he wants to see the ranking, what do you do? You store the query results inside memcached. Once your query result is cached, your app users do not have to wait for those damn 5 seconds anymore!

What is the dog-pile effect?

Nice, we start to cache our query results, our application is responsive and we can finally sleep at night, right?

That depends. Let’s suppose we are expiring the cache based on a time interval, for example 5 minutes. Let’s see how it will work in two scenarios:

1 user accessing the page after the cache was expired:

In this first case, when the user access the page after the cache was expired, the query will be executed again. After 5 seconds the user will be able to see the ranking, your server worked a little and your application is still working.

N users accessing the page after the cache was expired:

Imagine that in a certain hour, this page on your application receives 4 requests per second on average. In this case, between the first request and the query results being returned, 5 seconds will pass and something around 20 requests will hit your server. The problem is, all those 20 requests will miss the cache and your application will try to execute the query in all of them, consuming a lot of CPU and memory resources. This is the dog-pile effect.

Depending on how many requests hit your server and the amount of resources needed to process the query, the dog-pile effect can bring your application down. Holy cow!

Luckily, there are a few solutions to handle this effect. Let’s take a look at one of them.

Dog pile effect working on your application!

Dog pile effect working on your application!

How to avoid the dog-pile effect?

The dog-pile effect is triggered because we allowed more than one request to execute the expensive query. So, what if we isolate this operation to just the first request and let the next requests use the old cache until the new one is available? Looks like a good idea, so let’s code it!

Since Rails 2.1, we have an API to access the cache, which is defined by an abstract class called ActiveSupport::Cache::Store. You can read more about it in this post or in this excellent railscast episode.

The code below simply implements a new and smarter memcached store on top of the already existing MemCacheStore:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module ActiveSupport
  module Cache
    class SmartMemCacheStore < MemCacheStore
 
      alias_method :orig_read, :read
      alias_method :orig_write, :write
 
      def read(key, options = nil)
        lock_expires_in = options.delete(:lock_expires_in) if !options.nil?
        lock_expires_in ||= 10
 
        response = orig_read(key, options)
        return nil if response.nil?
 
        data, expires_at = response
        if Time.now > expires_at && !exist?("lock_#{key}")
          orig_write("lock_#{key}", true, :expires_in => lock_expires_in)
          return nil
        else
          data
        end
      end
 
      def write(key, value, options = nil)
        expires_delta = options.delete[:expires_delta] if !options.nil?
        expires_delta ||= 300
 
        expires_at = Time.now + expires_delta
        package = [value, expires_at]
        orig_write(key, package, options)
        delete("lock_#{key}")
      end
    end
  end
end

The code above is mainly doing:

  1. Suppose that your query is already cached;
  2. In the first five minutes, all requests will hit the cache;
  3. In the next minutes, the first request will notice that the cache is stale (line 17) and will create a lock so only it will calculate the new cache;
  4. In the next 5 seconds, the new query is calculated and all requests, instead of missing the cache, will access the old cache and return it to the client (lines 17 and 21)k;
  5. When the query result is returned, it will overwrite the old cache with the new value and remove the lock (lines 31 and 32);
  6. From now on, all new requests in the next five minutes will access the fresh cache and return it (lines 17 and 21).

Fallbacks and a few things to keep in mind

First, is not recommend to set the :expires_in value in your cache:

1
Rails.cache.write('my_key', 'my_value', :expires_in => 300)

With the solution proposed above, you just need to set :expires_delta. This is due to the fact that our application will now be responsible to expire the cache and not memcached.

1
Rails.cache.write('my_key', 'my_value', :expires_delta => 300)

However, there are a few cases where memcached can eventually expire the cache. When you initialize memcached, it allocates by default 64MB in memory. If eventually those 64MB are filled, what will memcached do when you try to save a new object? It uses the LRU algorithm and deletes the less accessed object in memory.

In such cases, where memcached removes a cache on its own, the dog pile effect can appear again. Suppose that the ranking is not accessed for quite some time and the cached ranking is discarded due to LRU. If suddenly a lot of people access the page in the five initial seconds where the query is being calculated, requests will accumulate and once again the dog-pile effect can bring your application down.

It’s important to have this scenario in mind when you are sizing your memcached, mainly on how many memory will be allocated.

Now I can handle the dog-pile effect and sleep again!

Summarizing, when your are using a cache strategy, you will probably need to expire your cache. In this process, the dog-pile effect can appear and haunt you down. Now you have one (more) tool to solve it.

You just need to add the SmartMemCacheStore code above to your application (for example in lib/), set your production.rb (or any appropriated environment) to use the :smart_mem_cache_store. If you use Rails default API to access the cache (Rails.cache.read, Rails.cache.write) and designed well your memcached structure, you will be protected from the dog-pile effect.

A real dog-pile! =p

A real dog-pile! =p

Rails for Kids 2009

A comunidade de Rails do Brasil está sempre ativa, sinal disso é que dia 12 de setembro vai acontecer o Rails for Kids 2009.
O Rails for Kids 2009 é um evento composto por uma maratona de palestras on-line ao vivo relacionados a Ruby on Rails. Teremos alguns dos melhores Railers do Brasil contando um pouco de suas experiências próprias sobre assuntos muito legais, como: deployment de aplicativos Rails, Agile, Git, Integração contínua, Cucumber, CouchDB e muito mais.
Eu (Hugo Baraúna) e o George Guimarães estamos entre os palestrantes =D. Falarei sobre metodologias ágeis e a motivação por trás delas, enquanto o George falará sobre deployment automatizado de aplicações em Rails com Capistrano e Passenger.
Agora, mais importante do que as palestras em si, é o fato que toda renda gerada pelo evento será doada à instituição Cotolengo, no Mato Grosso do Sul. Ou seja, você fará um grande gesto humano e ainda por cima poderá assistir à ótimas palestras!
Parabéns ao Carlos Eduardo da e-Genial, que é o responsável por este importante evento.
O Rails for Kids já está chegando, agora basta você ir ao site e se inscrever.
Nos vemos por lá!

A comunidade de Rails do Brasil está sempre ativa, sinal disso é que dia 12 de setembro vai acontecer o Rails for Kids 2009.

O Rails for Kids 2009 é um evento composto por uma maratona de palestras on-line ao vivo relacionados a Ruby on Rails. Teremos alguns dos melhores Railers do Brasil contando um pouco de suas experiências próprias sobre assuntos muito legais, como: deployment de aplicativos Rails, Agile, Git, Integração contínua, Cucumber, CouchDB e muito mais.

Eu (Hugo Baraúna) e o George Guimarães estamos entre os palestrantes =D. Falarei sobre metodologias ágeis e a motivação por trás delas, enquanto o George falará sobre deployment automatizado de aplicações em Rails com Capistrano e Passenger.

Agora, mais importante do que as palestras em si, é o fato que toda renda gerada pelo evento será doada à instituição Cotolengo, no Mato Grosso do Sul. Ou seja, você fará um grande gesto humano e ainda por cima poderá assistir à ótimas palestras!

Parabéns ao Carlos Eduardo da e-Genial, que é o responsável por este importante evento.

O Rails for Kids já está chegando, agora basta você ir ao site e se inscrever.

Nos vemos por lá!

Todo mundo já ouviu falar de escalabilidade. Todo mundo também já ouviu falar de Memcached. O que nem todos ouviram falar ainda é do dog-pile effect e de como evitar esse problema. Mas antes de discutirmos sobre o que é o dog-pile effect, vamos primeiro dar uma olhada em como é simples usar o Memcached com o Rails.

Rails com Memcached

Se você nunca usou o Memcached com Raills ou ainda não conhece muito sobre escalabilidade com Rails, os episódios do Scaling Rails feitos pelo Gregg Pollack são extremamente recomendados, especialmente o episódio sobre Memcached.

Dado que você tem o Memcached instalado e quer usá-lo dentro do seu aplicativo Rails, basta você ir em um dos arquivos de configuração, por exemplo o production.rb e colocar:

1
config.cache_store = :mem_cache_store

Por padrão, o Rails vai procurar por um processo do Memcached rodando no localhost na porta 11211.

Mas pra quê eu vou querer usar o Memcached? Bem, digamos que o seu aplicativo possui alguma página que precise rodar uma query lenta no banco de dados, tal como uma query para gerar um ranking de blog posts baseados na influencia dos autores, que demora em média 5 segundos. Nesse caso, toda vez que um usuário entrar nessa página do seu aplicativo, essa query será executada e o aplicativo terá um tempo de resposta muito alto.

Como você não vai querer que todo usuário sofra esperando 5 segundos para ver o ranking, o que você faz? Armazena o resultado dessa query no Memcached. Assim, uma vez que o resultado da query estiver cacheado, seus usuários não terão que esperar pelos malditos 5 segundos!

O que é o dog-pile effect?

Legal, você está cacheando o resultado da query do ranking e você finalmente vai poder dormir a noite. Será mesmo?

Vamos supor que você está expirando o cache baseado no tempo, ou seja, digamos que a query cacheada deva expirar após 5 minutos. Vejamos como esse processo funciona diante de dois cenários:

Um único usuário acessando depois do cache ficar obsoleto (stale):

Nesse primeiro cenário, quando um usuário acessar a página do ranking, a query deve ser executada novamente, fazendo com que a CPU do seu sistema tenha um pico de consumo para conseguir processá-la. Depois dos 5 segundos, seu usuário, poderá visualizar o ranking. Até aqui, ok, demorou, mas o usuário conseguiu ver o ranking e seu sistema (ainda) está de pé.

N usuários acessando depois do cache ficar obsoleto (stale):

Digamos que, em um dado horário, essa página do seu aplicativo tenha em média 4 requests/segundo. Nesse cenário, entre a chegada do primeiro request e o banco de dados retornar o resultado da query, passarão 5 segundos e ocorrerá algo em torno de 20 requests. O problema é, todos esses 20 requests irão dar cache miss e seu aplicativo executará a query em todos os casos, consumindo muitos recursos de CPU e memória. Esse é o dog-pile effect.

Dependendo de quantas requests resultarem em cache miss, do quão pesado for o processamento de sua query e dos recursos de infra disponíveis, o dog-pile effect pode acabar derrubando sua aplicação!

Caramba!!! Quer dizer que o dog-pile effect é tão sério assim e pode chegar a derrubar minha aplicação!? Sim, isso pode acontecer. Mas existem algumas soluções para evitarmos esse tipo de problema. Vamos dar uma olhada em uma delas.

O dog-pile effect pode fazer isso com seu aplicativo!

O dog-pile effect pode fazer isso com seu aplicativo!

Como resolver o problema do dog-pile?

Bem, o problema do dog-pile effect é que deixamos mais de um request executar o custoso cálculo da query. Hum… e se isolarmos esse cálculo para apenas o primeiro request e servir o antigo cache para os próximos requests até que o resultado cacheado da nova query esteja disponível? Parece uma boa idéia… Então vamos implementá-la!

Desde o Rails 2.1, temos uma API padrão para acessar o cache dentro do Rails definida por uma classe abstrata chamada ActiveSupport::Cache::Store. Você pode ver mais dessa API nesse post ou nesse ótimo episódio do railscast.

O código abaixo simplesmente cria um novo cache store baseado no já existente MemCacheStore:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module ActiveSupport
  module Cache
    class SmartMemCacheStore < MemCacheStore
 
      alias_method :orig_read, :read
      alias_method :orig_write, :write
 
      def read(key, options = nil)
        lock_expires_in = options.delete(:lock_expires_in) if !options.nil?
        lock_expires_in ||= 10
 
        response = orig_read(key, options)
        return nil if response.nil?
 
        data, expires_at = response
        if Time.now > expires_at && !exist?("lock_#{key}")
          orig_write("lock_#{key}", true, :expires_in => lock_expires_in)
          return nil
        else
          data
        end
      end
 
      def write(key, value, options = nil)
        expires_delta = options.delete(:expires_delta) if !options.nil?
        expires_delta ||= 300
 
        expires_at = Time.now + expires_delta
        package = [value, expires_at]
        orig_write(key, package, options)
        delete("lock_#{key}")
      end
    end
  end
end

A idéia do código acima é a seguinte:

  1. Digamos que o resultado da sua query já está cacheada;
  2. Durante os 5 minutos seguintes, todas as requests resultarão em cache hit;
  3. Depois dos 5 minutos, o primeiro request que chegar vai ver que o resultado cacheado já está stale, ou seja, obsoleto (linha 17). Esse request vai criar um lock para apenas ele ter direto de calcular o novo valor a ser cacheado;
  4. Durante os próximos 5 segundos, estará sendo feito o cálculo da query, e todos os requests que chegarem, ao invés de resultarem em cache miss, vão resultar em cache hit (devido ao lock), devolvendo o último valor cacheado (linhas 17 e 21);
  5. Após o primeiro request acabar de fazer o cálculo da query, ele vai cachear o novo valor e apagar o lock que tinha criado (linhas 31 e 32);
  6. Todas os novos requests que chegarem nos próximos 5 minutos verão que o valor cacheado não está “stale”, resultarão em cache hit e retornarão o novo valor cacheado (linhas 17 e 21).

Armadilhas e algumas coisas para se lembrar

Primeiramente, não é recomendado setar o valor :expires_in do seu objeto cacheado:

1
Rails.cache.write('my_key', 'my_value', :expires_in => 300)

Com a nossa solução, basta que você sete o valor do :expires_delta. Isso porque a aplicação será responsável por expirar o cache, e não mais o Memcached.

1
Rails.cache.write('my_key', 'my_value', :expires_delta => 300)

Entretanto, existem alguns casos em que o Memcached ainda pode expirar o cache. Quando você inicializa o Memcached, ele aloca por default 64 MB de memória. Digamos que os 64 MB estão cheios, o que ele faz quando você tenta salvar o próximo objeto em memória? Ele usa o algorítmo de LRU e deleta o objeto cacheado menos acessado.

Nos casos em que o Memcached expira o cache por conta própria, há uma possibilidade de termos o dog pile effect novamente. Digamos que o ranking não é acessado por muito tempo e o objeto cacheado é descartado devido ao LRU. Se, de repente, muitas pessoas voltam a acessar a página e ocorrem muitos acessos durante os cincos segundos iniciais em que o ranking é gerado, o efeito pode ocorrer novamente.

Vale a pena ter essa situação em mente na hora que você for dimensionar por exemplo o quanto de memória vai ficar alocado para o seu Memcached.

Já consigo lidar com o dog-pile, posso voltar a dormir!

Resumindo, quando você estiver usando uma estratégia de caching, você provavelmente irá precisar expirar o seu cache. Com o processo de expiração, pode surgir o problema de dog-pile effect. Agora você já tem (mais) uma ferramenta na sua toolbox para resolver esse problema.

Basta colocar o código acima do SmartMemCacheStore na sua aplicação Rails (no diretório lib/ por exemplo), setar no seu production.rb (ou em outro environment.rb que você queira) e configurar a cache store para :smart_mem_cache_store (config.cache_store = :smart_mem_cache_store). Se você usa a interface padrão do Rails para acessar o cache (Rail.cache.read, Rails.cache.write) e arquitetou bem sua estrutura com o memcached, você estará protegido do dog-pile effect.

Curiosidade: de onde vem o termo dog-pile effect? Dog-pile é um termo comum em inglês para se referir a um monte de coisas empilhadas. A idéia da metáfora então seria comparar uma pilha de requests uma em cima da outra, esperando pelo processamento da query custosa.

Um dog-pile de verdade  =p

Um dog-pile de verdade =p