страничное кэширование мультидоменного сайта на Rails 3+Nginx

«Наиболее эффективными считаются те запросы, которые никогда не выдаются», ― говорит Сэм Руби. Страничное кэширование с этой точки зрения не максимально эффективно, но близко к максимуму т.к. страница загруженная полностью из кэша не делает ни одного запроса к БД. Однако, в случае если сайт имеет поддомены, например, для определения локали (en.site.com, ru.site.com), кэш страницы в одной локале будет затираться кэшом в другой. В этой статье мой опыт борьбы с этой проблемой.

Если загуглить решение этой проблемы, то одно из первых, что выскакивает это статья Ryan Stout «Page Caching by Subdomain in Rails and Nginx». Собственно, по ней я и пытался кэшировать на поддоменах, но не все так гладко как там описано. Поэтому я кое-что добавил от себя, а кое-что изменил.

ApplicationController

Прежде всего, как и описано в статье надо добавить кое-что в ApplicationController, но тут помимо указанного у Ryan’а метода cache_page надо будет еще перегрузить expire_page иначе обновить кэш из под рельс можно будет только на велосипеде.

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
36
37
38
class ApplicationController < ActionController::Base
...
  def cache_page(content = nil, options = nil)
    path = "/#{request.host}/"
    path << case options
    when Hash
      url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format]))
    when String
      options
    else
      if request.path.empty? || request.path == '/'
        '/index'
      else
        request.path
      end
    end
    super(content, path)
  end
 
  def expire_page(options = {})
    path = "/#{request.host}/"
    params[:format]=:html if options[:format]
    path << case options
    when Hash
      url_for(options.merge(:only_path => true, :format => params[:format]))
    when String
      options
    else
      if request.path.empty? || request.path == '/'
        '/index'
      else
        request.path
      end
    end
    self.class.expire_page(path)
  end
...
end

настройка самого rails-приложения

Согласно статье, кэш будет храниться в директории public/cache. Чтобы так и было нужно, во-первых, создать папку cache в public, а, во-вторых, сообщить о пути кэша самому приложению. Для этого можно, например, в config/environments/production.rb дописать:

MyApp::Application.configure do
...
  config.action_controller.page_cache_directory = Rails.root.join("public/cache")
end

nginx

В настройке сайта в nginx.conf нужно указать, что и в оригинальной статье:

# If the file exists in the public folder, send it
if (-f $request_filename) {
  break;	  
}
 
# Check / files with index.html
if (-f $document_root/cache/$host/$uri/index.html) {
  rewrite (.*) /cache/$host/$1/index.html break;
}
 
# Check the path + .html
if (-f $document_root/cache/$host/$uri.html) {
  rewrite (.*) /cache/$host/$1.html break;
}
 
# Check directly
if (-f $document_root/cache/$host/$uri) {
  rewrite (.*) /cache/$host/$1 break;
}

Добавление этих условников говорит Nginx’у куда записывать кэш если его нет и откуда его читать если для данного адреса есть кэш. Вот тут и возникает проблема, Nginx не будет различать это GET-запрос или POST. И если в случае с GET, все хорошо, то когда поступает PUT-запрос на обновление модели (экшены update и show имеют один и тот же URL по умолчанию) Nginx с радостью обнаружит закэшированную страницу и выдаст ошибку 505.
Язык конфигурации Nginx не поддерживает вложенные условники и не понимает конъюнкцию (&& или AND или еще как угодно), все, что остается это строить велосипед что-то типа:

if ( $request_method = GET ) {
  set $method_and_file "method";
}
 
if (-f $request_filename) {
  set $method_and_file $method_and_file+"&file";
}
 
if ($method_and_file = "method&file") {
  break;
}

Но эти костыли у меня не завелись с первого раза и я забил на эти грязные мысли.

как же тогда обновить кэш?

Для обновления кэша я предлагаю такой, на мой взгляд, не самый кровавый вариант. Надо делать форму модели AJAX-вую и в вызове expire_cache явно указывать формат кэша.

пример на модели Page

app/controllers/pages_controller.rb:

class PagesController < ApplicationController
...
  caches_page :show
  caches_page :index
...
  def update
    respond_to do |format|
      if @page.update_attributes(params[:page])
        expire_page({action: :show, id: @page.id, format: :html})
        expire_page({action: :index, format: :html})
        format.html { redirect_to @page, notice: 'Page was successfully updated.' }
        format.js
      else
        format.html { render action: "edit" }
        format.js
        #format.json { render json: @page.errors, status: :unprocessable_entity }
      end
    end
  end
...
end

/app/views/pages/update.js:

setTimeout( 'window.location.replace("<%=page_path(@page)%>")', 1500);

/app/views/pages/_form.html.erb:

<%= form_for(@page, remote: true, format: :js) do |f|%>
...
<%end%>

No Comments.

Leave a Reply

(обязательно)

(обязательно)