Что нужно не забыть сделать в распределенной системе?

Нужно обязательно синхронизировать время. Нужно на все сервера установить ntp, чтобы часы постоянно синхронизировались с любым доступным сервером.

Это нужно для того, чтобы не повторить моей ошибки. Вот код:

my_model.save
MyWorker.perform_in(5.minutes, my_model.id)

Всё выглядит обычным и правильным. Через 5 минут после сохранения должна запуститься джоба и что-то сделать. Но в логах sidekiq я заметил, что слишком часто воркер не находил MyModel с нужным id. Есть известная проблема, что sidekiq настолько быстрый, что запускается до тех пор, пока не закоммитится транзакция. Но ведь вряд ли она висит 5 минут?

Конечно нет. Просто в моем случае на сервере, исполняющем код, время на 7 минут отставало от сервера с sidekiq, соответственно джоба моментально выполнялась, не ожидая 5 минут.

Как правильно посылать код подтверждения в SMS

Вот так:

1234 — используйте этот код для подтверждения номера телефона

Зачем? Чтобы нужный мне код был первым, что я увижу в push-уведомлении:

В 99% случаев я буду ждать эту смс и из неё мне будет нужен именно код, потому что я знаю, что я делаю и зачем.

Кто делает не так? Например, альфа-банк для подтверждения операции шлет SMS:

Dlya oplaty pokupki Podarok Mame na summu 100000.00 RUB vash odnorazoviy parol 123456

А Тинькофф делает немного лучше, но не идеально:

SMS-kod: 1234 Operatsiya: Bileti v Thailand na summu 500.00 RUB Nikomu ne govorite etot kod! www.tinkoff.ru

Что сегодня изменилось в моём блоге?

Во-первых, теперь я буду выносить суть в первый абзац. Во-вторых, я буду стараться чаще писать. В-третьих, постараюсь меньше писать.

Я уверен, что большая часть людей не будет читать забавную историю о том, как я столкнулся с интересной проблемой, долго старался понять в чем дело и как я пришел к решению. Точнее я буду это описывать, но уже после вывода.

Ну и еще я постараюсь переделать дизайн, а то сейчас читать невозможно. А пока я шрифт немного увеличил.

И еще у меня скопилась стопка тем, которые я хотел объединить в большие посты, но решил делать наоброт, поэтому в ближайшее время понемногу всё напишу.

method_missing в консоли

Я постоянно забываю менять язык в консоли, особенно для каких-то мелких команд, вроде ls, cd, rs (алиас для rails server) и получается ды, св, кы, которые, само собой, не работают.

Я решил, что хватит это терпеть и нашел решение проблемы. Для zsh есть функция command_not_found_handler. Это как method_missing из руби, только для zsh. Задаем эту функцию, например так:

function command_not_found_handler() {
  ~/.dotfiles/bin/shell_method_missing $*
}

и потом в файле ~/.dotfiles/bin/shell_method_missing на любимом языке (в моём случае на ruby) пишем обработчик. Я пока сделал только смену языка, но так можно сделать вообще, что угодно. Кто-то, например, запускает cucumber по имени .feature файла.

PS: эта же функция работает в bash, а для fish-fish шелла есть свой вариант.

Alfred workflow для быстрого перевода текста

Решил я себе добавить в alfred workflow для быстрого перевода текста.Оказалось таких воркфлоу очень много но у всех был недостаток — нужно вводить, на какой язык переводить. И запросы получаются translate ru hello! или translate en привет!. В связи с этим я взял и написал свой workflow, который переводит на английский, если введен русский и наоборот. Выглядит вот так:

Скачать workflow | Код на гитхабе

Создание кроссбраузерной визуализации аудио

Примерно год назад появилась задача сделать красивый html5 аудио-плеер. Дизайнеры нарисовали простейшую визуализацию:

Самый простой вариант — рисовать на фоне «шум», никак не связанный с играющей музыкой. Но мы не ищем лёгких путей.

На хабре нашлась статья визуализация аудио в HTML5. Существует 2 стандарта получения информации об аудио:

Стандарты сильно отличаются друг от друга, используя эти API мы могли добиться поддержки только в этих браузерах. Мы же хотели расширить список и придумали решение, которое заработало даже в старых версиях IE.

Упрощенное демо

Вся музыка импортировалась со стороннего сервиса и проходила препроцессинг написанный на ruby — конвертирование в stereo mp3 128kb. Мы добавили к этому создание json-файла для визуализации.

Server-side

На рисунке 30 столбцов показывают «насыщенность» диапазона частот: низкие частоты — слева, высокие — справа.

Не вдаваясь в подробности алгоритма, на сервере, используя 2 гема — wavefile для получения данных из .wav файлов и fftw3 для подсчета быстрого преобразования Фурье, генерировался json следующего формата:

 {
  0.0: [/*размеры для 30 столбцов*/], 
  0.04: [...], 
  0.08: [...]
  ...
 }

В среднем, размер файла визуализации — 500kb.

Экспериментально мы выбрали «разрешение» в 0.04 секунды, при котором визуализация меняется плавно (вы же помните про 24 кадра в секунду?) и не увеличивается размер файла. Забавный факт: при первой реализации высота столбцов представлялась числом с точкой и размер файла доходил до 5 мегабайт. Добавление приведения к целому числу уменьшило размер файла в десятки раз.

Client-side

На клиенте алгоритм очевидный: для каждого аудио-файла с сервера берётся json-визуализация. Во время воспроизведения для текущего времени берётся ближайшее время из файла и рисуются 30 стобцов, высота которых подсчитана на сервере.

Cons and pros

Очевидный минус подхода: чтобы изменить внешний вид визуализации нужно перегенерировать все файлы. Но за год внешний вид не менялся и такой проблемы не возникало. Другой минус: для каждого аудио нужно хранить ещё и полумегабайтный файл визуализации. Но при этом визуализация работает во всех браузерах.

Sublime text: go to definition

Многим сильно не хватает навигации по методам/переменным в саблайме, специально для них есть такая вещь, как ctags. На маке утилита устанавливается через homebrew:

brew install ctags

Для работы в саблайме ставим плагин.

Теперь для индексации исходного кода нужно запустить команду ctags. Для rails-проектов нет смысла индексировать директории .git и logs. Запускаем индексацию и сохраняем результат в файл .tags:

ctags -R --exclude=.git --exclude=log -f .tags

Помимо директории проекта, удобно иметь доступ к гемам. Для индексации используемых гемов запускаем ctags еще раз:

ctags -R -f .gemtags `bundle show --paths`

Не забудьте добавить файлы ctags в file_exclude_patterns, чтобы они не мешали вам во время работы с проектом:

"file_exclude_patterns": [...".tags", ".gemtags"]

Для переидексации тегов внутри саблайма можно использовать хоткей: ctrl+t, ctrl+r. Для правильной переидексации изменяем запускаемую команду в настройках плагина:

"command"   :  "ctags -R --exclude=.git --exclude=log -f .tags && ctags -R -f .gemtags `bundle show --paths`"

Теперь переиндексация тегов будет работать правильно. Для “проваливания” в метод можно использовать пункт в контекстном меню или хоткей: ctrl+t, ctrl+t.

Отмечу, что индексация работает очень быстро: на большом проекте, с 60 гемами индексация занимает примерно 30 секунд.

Лично я долгое время обходился без ctags и сейчас пользуюсь ими не часто. Не забывайте, что в саблайме вы можете использовать go to symbol для перехода к методу внутри открытого файла (нажать ⌘+P и начать ввод, начиная с @). Так же, вы можете использовать @ не в начале строки, для перехода к методу в другом файле, например для перехода к методу index в home_controller.rb: homecont@in.

Кстати, ctags интегрируется и в другие редакторы, такие как vim.

SCM Breeze: набор улучшений для работы с гитом из коммандной строки

SCM Breeze — расширение для вашей коммандной строки, основное предназначение которого — создание нумерованных шоткатов для путей к файлам.

Команда git status выводит список файлов, затем командой git add вы добавляете файлы в stage, для каждого файла вводя “git add ", причем имя файла может быть достаточно длинным (с учетом всех директорий). SCM Breeze за вас создаёт шоткаты для команд git status и git add — `gs` и `ga`, в результате которого вы получаете нумерованный список файлов, а затем вы можете добавить файлы в stage с помощью вызова команды ga 1,2. Выглядит это так:

Кроме команды gs, нумерованный список можно получить командой ll, выводящей список всех файлов в директории.

Для каждого нумерованного файла создаётся переменная $e1 - …, которую можно передать любой другой команде, например subl $e1 откроет первый файл из списка в саблайме. Ну и для еще большего удобства, можно опускать имя переменной, добавив для любой команды префикс ge: ge subl 1 так же откроет первый файл из списка.

Советую всем, кто хоть иногда работает с гитом в консоли!

P.S. Я добавил scm breeze в свои дотфайлы, основанные на дотфайлах Зака Холмана. Если вы используете их же, можете установить scm breeze так же, как это сделал я:

Devtools: связь консоли и elements tree

Используйте $0 в консоли для работы с текущим выделенным в elements tree элементом. $1 для предыдущего выделенного элемента и т.д.

Для выбора элемента в elements tree, используйте inspect(element):

Rails: Переопределение collection и member путей

Сразу пример: у вас есть контроллер “channels” и вы хотите использовать стандартные методы channels_path и channel_path, которым соответствуют действия index и show. По умолчанию, адреса для них будут /channels и /channels/:id, но вы хотите изменить стандартные адреса на /channels/:flag, (где :flag, например active или archive). Для show вы не хотите использовать :id, а а что-то вроде /channels/:category/:slug. Для переопределения пути к index достаточно сделать обычно описание через get с параметром :as, равному пустой строке и :on => :colleciton:

resources :channels do
  get '(/:flag)', :defaults => {:flag => 'all'}, :as => '', :on => :collection
end

Немного сложнее переопределить путь к show, т.к. если просто написать

resources :channels do
  get ':category/:slug', :as => '', :on => :member
end 

вы получите путь :id/:category/:slug. Для определения нового формата используйте resource вместо resources с параметром :path:

resource :channel, :path => ':category/:slug'

Теперь у вас будут правильные методы channels_path and channel_path`.

Эту же проблему можно решить с помощью метода mathch в рутах, но в таком случае нужно будет использовать match для всех вложенных действий и контроллеров.

Zsh: история

По-умолчанию, в zsh клавиши вверх/вниз — это навигация по истории, независимо от введённого текста. Для поиска по истории введенного текста используется ctrl+r, что не совсем удобно. Для поиска по стрелкам, добавляем в .zshrc следующее:

bindkey    "^[[A" history-beginning-search-backward
bindkey    "^[[B" history-beginning-search-forward

Поиск по истории можно сделать чуть лучше, добавив флаги:

setopt INC_APPEND_HISTORY SHARE_HISTORY  #добавлять каждую команду в историю сразу после нажатия enter
setopt HIST_IGNORE_ALL_DUPS  #не отображать дубликаты
setopt HIST_REDUCE_BLANKS #не отображать пустые команды

Подводные камни JavaScript

Мне очень нравится JavaScript и я считаю его мощным и удобным. Но для большинства начинающих JS-программистов, много проблем создаёт недопонимание аспектов языка. Часто конструкции языка ведут себя «нелогично». В данной статье я хочу привести примеры «граблей», на которые я наступил; объяснить поведение языка и дать пару советов.

Типы

Как написано в спецификации ECMAScript, всего существует 6 типов:

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Object

Все значения должны принадлежать к ним. В JS есть оператор typeof, который, как казалось бы, должен возвращать тип объекта. Казалось бы, один из перечисленных. Что получается на самом деле:

  typeof 5;             //"number",        ок, похоже на правду
  typeof "hello";       //"string" 
  typeof true;          //"boolean" 
  typeof undefined;     //"undefined"
  typeof {};            //"object".        Пока 5 из 5
  typeof null;          //"object".        WTF?
  typeof function(){};  //"function".      Разве у нас есть тип function?

Проблема: несмотря на то, что тип у null — Null, оператор возвращает ‘object’; а тип у функции — Object, оператор возвращает ‘function’, а такого типа нет. Объяснение: typeof возвращает не тип, а строку, которая зависит от аргумента и не является именем типа. Совет: забудьте про типы. Серьезно, я считаю что знание 6 типов JS не даст вам пользы, а оператор typeof используется довольно часто, поэтому лучше запомнить результаты его работы:

Тип аргумента Результат
Undefined undefined
Null object
Boolean boolean
Number number
String string
Object (результаты оператора new, inline-объекты ({key: value})) object
Object (функции) function

Магические значения: undefined, null, NaN

В спецификации описаны так:

  • undefined value — primitive value used when a variable has not been assigned a value
  • Undefined type — type whose sole value is the undefined value
  • null value — primitive value that represents the intentional absence of any object value
  • Null type — type whose sole value is the null value
  • NaN — number value that is a IEEE 754 “Not-a-Number” value

У себя в голове я держу следующее:

  • undefined — значение переменной, которая не была инициализирована. Единственное значение типа Undefined. & null — умышленно созданный «пустой» объект. Единственное значение типа Null.
  • NaN — специальное значение типа Number, для выражения «не чисел», «неопределенности». Может быть получено, например, как результат деления 0 на 0 (из курса матанализа помним, что это неопределенность, а деление других чисел на 0 — это бесконечность, для которой в JS есть значения Infinity).

С этими значениями я обнаружил много «магии». Для начала, булевы операции с ними:

  !!undefined; //false
  !!NaN; //false
  !!null; //false
  //как видим, все 3 значения при приведении к boolean дают false

  null == undefined; //true

  undefined === undefined; //true
  null === null; //true

  NaN == undefined; //false
  NaN == null; //false

  NaN === NaN; //false!
  NaN == NaN; //false!

Проблема: с чем бы мы ни сравнивали NaN, результатом сравнения всегда будет false. Объяснение: NaN может возникать в результате множества операций: 0/0, parseInt(‘неприводимая к числу строка’), Math.sqrt(-1) и было бы странно, если корень из -1 равнялся 0/0. Именно поэтому NaN !== NaN. Совет: не использовать булевы операторы с NaN. Для проверки нужно использовать функцию isNaN.

  typeof a; //'undefined'
  a; //ReferenceError: a is not defined

Проблема: оператор typeof говорит нам, что тип необъявленной переменной — undefined, но при обращении к ней происходит ошибка. Объяснение: на самом деле, есть 2 понятия — Undefined и Undeclared. Так вот, необъявленная переменная является Undeclared-переменной и обращение к ней вызывает ошибку. Объявленная, но не инициализированная переменная принимает значение undefined и при обращении к ней ошибок не возникает. Совет: перед обращением к переменной, вы должны быть уверенны, что она объявлена. Если вы обратитесь к Undeclared-переменной, то код, следующий за обращением, не будет выполнен.

  var a; //вновь объявленная переменная, для которой не указано значение, принимает значение undefined
  console.log(undefined); //undefined
  console.log(a); // undefined
  a === undefined; //true
  undefined = 1;
  console.log(undefined); //1
  a === undefined; //false

Проблема: в любой момент мы можем прочитать и записать значение undefined, следовательно, кто-то может перезаписать его за нас и сравнение с undefined будет некорректным. Объяснение: undefined — это не только значение undefined типа Undefined, но и глобальная переменная, а значит, любой может её переопределить. Совет: просто сравнивать переменные с undefined — плохой тон. Есть 3 варианта решения данной проблемы, для создания «пуленепробиваемого» кода.

  • Вы можете сравнивать не значение переменной, а её тип: «typeof a === ‘undefined’»ю
  • Использовать паттерн immediately-invoked function:
  (function(window, undefined){
    //т.к. второй аргумент не был передан, значение переменной undefined будет «правильным».
  }(this));
  • Для получения реального «undefined»-значения можно использовать оператор void (кстати, я не знаю другого применения этому оператору):
  typeof void(0) === 'undefined' // true

Теперь попробуем совершить аналогичные действия с null:

  console.log(null); //null
  null = 1; //ReferenceError: Invalid left-hand side in assignment

Проблема: несмотря на некоторые сходства между null и undefined, null мы перезаписать не можем. На самом деле проблема не в этом, а в том, что язык ведёт себя нелогично: даёт перезаписать undefined, но не даёт перезаписать null.</p> Объяснение: null — это не глобальная переменная и вы не можете её создать, т. к. null — зарезервированное слово. **Совет: ** в JavaScript не так много зарезервированных слов, проще их запомнить и не использовать как имена переменных, чем вникать, в чём проблема, когда она возникнет.


И теперь сделаем тоже самое с NaN:

  console.log(NaN); //NaN
  NaN = 1;
  console.log(NaN); //NaN
  isNaN(NaN); //true

Проблема: при переопределении undefined всё прошло успешно, при переопределении null возникла ошибка, а при переопределении NaN операция не вызвала ошибки, но свойство не было переопределено. Объяснение: нужно понимать, что NaN — переменная глобального контекста (объекта window). Помимо этого, к NaN можно «достучаться» через Number.NaN. Но это неважно, ниодно из этих свойств вы не сможете переопределить, т. к. NaN — not writable property:

  Object.getOwnPropertyDescriptor(window, NaN).writable; //false
  Object.getOwnPropertyDescriptor(Number, NaN).writable; //false

Совет: как JS-программисту, вам нужно знать об атрибутах свойств:

Атрибут Тип Смысл
enumerable Boolean Если true, то данное свойство будет участвовать в циклах for-in
writable Boolean Если false, то значение этого свойства нельзя будет изменить
configurable Boolean Если false, то значение этого свойства нельзя изменить, удалить и изменить атрибуты свойства тоже нельзя
value Любой Значение свойства при его чтении
get Object (или Undefined) функция-геттер
set Object (или Undefined) функция-сеттер

Вы можете объявлять неудаляемые или read-only свойства и для созданных вами объектов, используя метод Object.defineProperty:

  var obj = {};
  Object.defineProperty(obj, 'a', {writable: true,  configurable: true,  value: 'a'});
  Object.defineProperty(obj, 'b', {writable: false, configurable: true,  value: 'b'});
  Object.defineProperty(obj, 'c', {writable: false, configurable: false, value: 'c'});

  console.log(obj.a); //a
  obj.a = 'b';
  console.log(obj.a); //b
  delete obj.a; //true

  console.log(obj.b); //b
  obj.b = 'a';
  console.log(obj.b); //b
  delete obj.b; //true

  console.log(obj.c); //c
  obj.b = 'a';
  console.log(obj.c); //c
  delete obj.b; //false

Работа с дробными числами

Давайте вспомним 3-й класс и сложим несколько десятичных дробей. Результаты сложения в уме проверим в консоли JS:

  0.5 + 0.5; //1
  0.5 + 0.7; //1.2
  0.1 + 0.2; //0.30000000000000004;
  0.1 + 0.7; //0.7999999999999999;
  0.1 + 0.2 - 0.2; //0.10000000000000003

Проблема: при сложении некоторых дробных чисел, выдаётся арифметически неверный результат. Объяснение: такие результаты получаются из-за особенностей работы c числами с плавающей точкой. Это не является особенностью JavaScript, другие языки работают также (я проверил в PHP, Python и Ruby). Совет: во-первых, вы, как программист, обязаны знать об особенностях работы компьютера с числами с плавающей точкой. Во-вторых, в большинстве случаев достаточно просто округлять результаты. Но, если вдруг необходимо выдавать пользователю точный результат, например, при работе с данными о деньгах, вы можете просто умножать все аргументы на 10 и результат делить обратно на 10, например так:

  function sum() {
    var result = 0;
    for (var i = 0, max = arguments.length; i< max; i++ ) {
      result += arguments[i]*10;
    }
    return result / 10;
  }
  sum(0.5, 0.5); //1
  sum(0.5, 0.7); //1.2
  sum(0.1, 0.2); //0.3
  sum(0.1, 0.7); //0.8
  sum(0.1, 0.2, -0.2); //0.1

Вывод

Это только несколько необычных примеров с непредсказуемым результатом. Если помнить о них, то получится не наступить на те же грабли и быстро понять, в чём проблема. Если найдёте новый «нелогичный» кусок кода, то попробуйте осознать, что происходит с точки зрения языка, почитав спецификацию или MDN.

Quick rake routes

В больших проектах с кучей контроллеров, выполнение команды rake routes может занимать много времени. В большистве проектов routes.rb банальный, без хитрой логики, поэтому если файл routes.rb не изменялся с последнего запуска, можно закешировать результат выполнения команды rake routes. Для использования, создайте файл, например /usr/local/bin/routes (не забудьте дать права на исполнение) и добавьте в него код:

if [ -f config/routes.rb ]; then
  recent=`ls -t config/routes.rb .routeslist\~ 2>/dev/null | head -n 1`
  if [[ $recent != '.routeslist~' ]]; then
    rake routes > .routeslist~
  fi
  cat .routeslist~ | grep "$1"
else
  echo 'Routes file not found';
fi

Теперь при повторном запуске команды «routes», вы получите моментальный результат. Не забудьте добавить файл «.routeslist~», в который кешируется результат, в .gitignore.

Передав аргумент, например «routes POST», к результатам будет применён grep и они будут отфильтрованы.

Sublime Text 2: gem source

Часто бывает нужно заглянуть в исходники используемого гема. Большинство IDE (вроде RubyMine) позволяют это делать внутри себя, но они для меня медленны и монструозны и я использую свой любимый Sublime Text 2.

Для быстрого доступа к исходникам гема я написал bash-скрипт (немного дополнив этот):

#!/bin/sh
# view gem source in sublime text 2
if test "$1" == ""
then
  echo 'Specify gem name';
else
  p=$(bundle show $1);
  if [[ $p == /* ]]
  then
    subl $p;
  else
    echo $p;
  fi
fi

После некоторого времени использования я понял, что сильно не хватает автокомплита, который я и добавил:

#compdef gemsource
if [ -f Gemfile ]; then
  recent=`last_modified .gemlist~ Gemfile`
  if [[ $recent != '.gemlist~' ]]; then
    bundle show | cut -d " " -f 4 | sed 1d > .gemlist~
  fi
  compadd `cat .gemlist~`
fi

Для кеширования списка гемов создаётся файл .gemlist~, так что не забудьте добавить его в gitignore, если будете использовать.

Ruby on Rails + Nginx: кеширование

Хочу поделиться опытом настройки кеширования на высоконагруженном проекте в связке ruby on rails + nginx.

Описание проекта

Проект — страница выдачи сущностей с фильтром и endless-скроллом (или пагинацией, разницы не имеет), страница просмотра сущности и несколько статичных страниц. Важно, что отображение страницы однозначно зависит только от адреса, то есть нет случайных данных на странице и нет авторизации пользователей.

Conditional GET

Conditional GET — это полезная возможность HTTP-протокола. Клиент в запросе уточняет условия, при которых он хочет получить новый ответ от сервера, иначе он берет закешированную версию. Если сервер поддерживает conditional get, вместе с ответом он выдает заголовкок Last Modified, в котором хранится время модификации страницы и (или) ETag, в котором хранится хеш ответа, в виде короткой строки (например, md5-хеш от всего содержимого страницы). Клиент, получив страницу, кеширует её у себя и при следующем запросе шлёт серверу заголовки If-Modified-Since и (или) If-None-Match, в которые, соответственно, подставляются Last Modified и ETag. Сервер, в свою очередь, сравнивает хеши и время модификации. Если хеши совпадают, а Last Modified ≤ If-Modified-Since, то сервер возвращает ответ 304 Not Modified и пустое тело ответа:

Request Headers: 
If-Modified-Since:Fri, 12 Oct 2012 08:13:56 GMT
If-None-Match:"b86b53268ada9613191d3c8a59ce42b8"

Response Headers: 
HTTP/1.1 304 Not Modified
ETag:"b86b53268ada9613191d3c8a59ce42b8"
Last-Modified:Fri, 12 Oct 2012 08:13:56 GMT

В противном случае 200 OK и результат:

Request Headers: 
If-Modified-Since:Fri, 12 Oct 2012 07:33:50 GMT
If-None-Match:"89cd0d10d56469d67171b41c00ddf100"

Response Headers: 
HTTP/1.1 200 OK
ETag:"b86b53268ada9613191d3c8a59ce42b8"
Last-Modified:Fri, 12 Oct 2012 08:13:56 GMT

Conditional GET — отличный вариант для кеширования динамических страниц. При изменении страницы изменяется её хеш и клиент сразу получает новую версию. В нашем случае при добавлении новой сущности или при изменении существующей, должен измениться хеш страницы выдачи.

В Ruby on Rails есть встроенная поддержка conditional GET. Для работы с ним есть 2 метода: «fresh_when» и «stale?». Пример использования:

  def action_fresh
    #если есть кеш — возвращаем 304, если нет — рендерим view
    fresh_when :last_modified => 1.year.ago, :etag => 'action_fresh'
  end

  def action_stale
    if stale?(:last_modified => 1.year_ago, :etag => 'action_stale') do
      #код, который будет выполняться в случае, если кеш устарел.
      #если кеш не устарел, код не выполняется и сервер возвращает 304 Not Modified
    end
  end

В нашем случае используем это следующим образом:

  #будем обновлять данные после рестарта сервера
  def or_deploy_date date 
    restart_date = File.new(Rails.root.join('tmp', 'restart.txt')).mtime rescue 1.year.ago
    [restart_date, date].max
  end

  def list
    updated_at = collection.maximum(:updated_at)
    if stale?(last_modified: or_deploy_date(updated_at), etag: or_deploy_date(updated_at)) do
      #query, filter, order, paginate, etc.
    end
  end 

  def show
    updated_at = resource.updated_at
    if stale?(last_modified: or_deploy_date(updated_at), etag: or_deploy_date(updated_at)) do
      #query, etc.
    end
  end

Кеширование статических страниц

Для статических страниц время обновление данных не критично и если пользователь получит новую страницу спустя 10 минут после её публикации, ничего страшного не произойдет. Добавим в контроллер следующий код:

  before_filter :only => [:custom] do
    expires_in 10.minutes, :public => true
  end

Теперь со статическими страницами пользователь получает заголовок:

Cache-Control: max-age=600, public

Получив страницу один раз, на протяжении 10 минут он может загружать её из локального кеша браузера (почти моментально) и не отсылать запросы на сервер, который мы еще немного разгрузили от лишней нагрузки.

Nginx proxy-cache

Если nginx один раз получает статическую страницу от rails-сервера и отдаёт клиенту, он может запомнить содержимое и не дергать сервер постоянно. Для этого в конфигурацию nginx необходимо добавить:

  proxy_cache_path /var/www/cache levels=1:1 keys_zone=zone:10m;
  proxy_cache zone;
  proxy_cache_bypass $http_pragma;
  proxy_cache_use_stale updating;
  • /var/www/cache — директория для хранения кеша,
  • 1:1 — уровни иерархии. То, как в каком виде будут создавать папки и файлы с кешем;
  • keys_zone — имя зоны. Можно сделать несколько настроек кеширования, например, для различных поддоменов;
  • 10m — время кеширования;
  • proxy_cache_bypass — условие, при котором кеш не используется. Если строка пустая, используется кеш;
  • $http_pragma — http-заголовок Pragma. Обычно браузеры подставляют Pragma: no-cache, если нужны обновленные данные (cmd+r в хроме);
  • proxy_cache_use_stale — позволяет использовать устаревший закешированный ответ, если в данный момент он обновляется;

Спустя какое-то время, на проекте вводится валюта, в которой отображаются все цены. Валюта хранится в cookie в поле currency_code. Всё хорошо, но вот только теперь при изменении валюты клиент получает страницы из кеша с ценами в старой валюте. Для conditional GET кеша решение очевидно: нужно добавить в хеш страницы cookie:

  def or_deploy_date date
    restart_date = File.new(Rails.root.join('tmp', 'restart.txt')).mtime rescue 1.year.ago
    [restart_date, date].max
  end

  def etag_by_date date
    res = or_deploy_date(date).to_s
    res += cookies[:currency_code].to_s if cookies[:currency_code]
    res
  end
  
  def list
    updated_at = collection.maximum(:updated_at)
    if stale?(last_modified: or_deploy_date(updated_at), etag: etag_by_date(updated_at)) do
      #query, filter, order, paginate, etc.
    end
  end 

  def show
    updated_at = resource.updated_at
    if stale?(last_modified: or_deploy_date(updated_at), etag: etag_by_date(updated_at)) do
      #query, etc.
    end
  end

Теперь получается, что браузер клиента может закешировать страницу и после смены валюты без запроса на сервер выдать старую версию. Для предотвращения этого, удалим строку с expires_in — теперь браузеру запрещено кеширование. Осталось изменить кеширование в nginx. После некоторого знакомства с proxy cache, становится очевидным, что нужно добавить наш cookie в proxy_cache_key:

  proxy_cache_key $scheme$proxy_host$uri$is_args$args$cookie_currency_code;
  #$scheme — протокол
  #$proxy_host, $uri — хост и урл
  #$is_args — "?" если есть query string, иначе пустая строка
  #$args — query string
  #$cookie_ — значение паметров из куки.

Пробуем и… nginx ничего не кеширует. Всё потому, что rails отдаёт заголовок «Cache-Control: max-age=0, private, must-revalidate», увидев его nginx понимает, что страницы кешировать не стоит. Так же nginx не кеширует страницы, отдающие заголовок SetCookie. Для игнорирования заголовка cache-control добавим:

  proxy_ignore_headers "Cache-Control";

Последний этап оказался для меня самым сложным, т. к. работа nginx, как мне кажется, не очень логична. Клиент делает запрос, nginx ищет в своём кеше страницу по $proxy_cache_key — если не находит её, запрашивает у rails сервера. Но потом он не просто отдаёт её, а сравнивает Last-Modified с If-None-Match, и если Last-Modified = If-None-Match, отдаёт клиенту 304 Not Modified, несмотря на то, что ETag ≠ If-None-Match. Немного потестировав, я пришел к выводу, что ответ 304 приходил только в случае точного совпадения Last Modified и If-None-Match. Почитав еще немного, выяснил, что во всём виноват параметр if_modified_since. Он может принимать 3 значения: off, exact и before, соответстветственно выключает сравнение, вклюет сравнение на точное совпадение и сравнение на Last-Modified ≤ If-Modified-Since. Поставив off, всё заработало, как и планировалось. Так вот, nginx ведёт себя не логично, т. к. сравнивает только If-Modified-Since, без ETag. Причем похожего на if_modified_since параметра для ETag я не нашел в документации. Итоговая конфигурация nginx:

  proxy_cache_path /var/www/cache levels=1:1 keys_zone=zone:10m;
  proxy_cache zone;
  proxy_cache_bypass $http_pragma;
  proxy_cache_use_stale updating;
  proxy_cache_key $scheme$proxy_host$uri$is_args$args$cookie_currency_code;
  proxy_ignore_headers "Cache-Control";
  if_modified_since off;

Debug кеширования

При отладке и настройке дебага, необходимо знать, что делает nginx: берет ли он страницы из кеша, какие заголовки получает от бэкэнда. Первый вариант для этого — создание лога, в который будут записываться данные о кешировании:

    log_format cache '***$time_local '
                     '$upstream_cache_status '
                     'Cache-Control: $upstream_http_cache_control '
                     'Expires: $upstream_http_expires '
                     '"$request" ($status) '
                     '"$http_user_agent" ';
    access_log  /var/log/nginx/cache.log cache;

Мне показалось, что этот вариант неудобен и проще добавить нужные данные в заголовки ответа, например:

  add_header Debug-Status $upstream_cache_status;
  add_header Debug-Expires $upstream_http_expires;
  add_header Debug-Cache-Control $upstream_http_cache_control;

Теперь в web-испекторе хрома можно посмотреть полученные от сервера заголовки. Возможные значения для Debug-Status:

  • HIT — загружен из кеша,
  • MISS — кеша нет,
  • BYPASS — обход кеша, например при наличии заголовка Pragma,
  • EXPIRED — кеш «просрочен»,
  • UPDATING — кеш «просрочен», но nginx отдал старый кеш, т. к. включена опция proxy_cache_use_stale updating;

Для создания http-запросов через командную строку можно использовать curl, но удобнее использовать HTTPie, который позволяет в человеческом формате задать все заголовки и параметры, делает вывод с подсветкой и еще много всего полезного.

PUT запрос с параметром hello = world и json-ответ

Полезные ссылки

  1. Спецификация кеширования в протоколе http;
  2. Еще про кеширование в http;
  3. Про кеширование в Ruby on Rails ;
  4. Про proxy cache в nginx

Iphone Remote Debugging

В ios6 появилось много новых фич: поддержка <input type="file">, requestAnimationFrame, remote debugging и еще много чего.

Safari Remote Debugging

Недавно я попробовал remote debugging — в пару тапов и кликов можно получить доступ в обычному вебкитовскому веб-инспектору и всей его мощи: к консоли, изменению html и css на лету, отладке JS. Для активации нужно включить web inspector на айфоне: Settings -> Safari -> Advanced -> Web Inspector.

После этого, открыв страницу в сафари (на айфоне), можно получить к ней доступ через сафари на компе (не забудьте включить Develop Menu в Preferences → Advanced):

Работает всё очень быстро, есть доступ ко всем функциям веб-инспектора, но телефон нужно подключать к компьютеру кабелем, через WiFi всё сделать у меня так и не вышло. Плюс, есть возможность аналогичным способом отлаживать страницы, открытые в iOS эмуляторе из xCode (не забудьте его предварительно обновить).

Adobe Edge Inpect

Помимо стандартного решения, есть софт от Адоби, до недавнего времени называвшийся Shadow, а теперь Edge Inspect. Для использования нужно скачать приложение для айфона или айпада в апп сторе, расширение для гугл хрома и десктопное приложение. Для подключения нужно

  • Запустить десктопное приложение, которое будет работать в фоне;
  • Открыть расширение и в нём посмотреть ip:

  • Запустить приложение на айфоне и добавить подключение, ввести показанный ip:

  • В расширении ввести passcode:

  • Открыть страницу в Хроме (открытая вкладка продублируется на айфоне) и запустить Remote Inspection:

После этого вам будет доступен веб-инспектор.

Из минусов: нужно ставить 3 программы, десктопное приложение периодически просто зависает, а сам процесс дебага очень томозной, вне зависимости от скорости вашего вай-фая, не все функции веб-инспектора доступны, например, я не нашел js-профайлера.

Из плюсов: такой вариант можно использовать с iOS начиная с 4й версии