Что нужно не забыть сделать в распределенной системе?
Нужно обязательно синхронизировать время. Нужно на все сервера установить 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 стандарта получения информации об аудио:
- Web Audio API для Chrome,
- Audio Data API для Firefox.
Стандарты сильно отличаются друг от друга, используя эти 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
Кроме команды 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 типов:
- Undefined
- Null
- Boolean
- String
- Number
- 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-зависимые страницы
Спустя какое-то время, на проекте вводится валюта, в которой отображаются все цены. Валюта хранится в 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-ответ
Полезные ссылки
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й версии