08 Jul 2014

Agile vs. Логирование Работы

Резюме: есть много статей в интернетах о том как это полезно и прекрасно логировать время и проделанную работу. Однако это для себя. Но зачем этого требует от вас руководство? Наверно из благих целей.

Есть одна менеджерская мечта - команда должна хорошо логировать свою работу и время, потраченное на нее. Ведь тогда будет понятно кто сачкует, а кто перегружен; кому медаль золотую вручить, а кому - позолоченную; какие задачи сложно даются, а какие - просто; сколько денег нужно потратить, а сколько - отобрать. Но на практике хренушки, к сожалению это не работает для большинства команд. Как правило по двум причинам:

  • Кто-то слишком ленивый, ведь логирование отвлекает от прямых обяазанностей ради “какой-то бюрократии”.

  • У кого-то просто нет для этого навыков. Есть люди, которые не умеют этого делать - когда забудут, а когда вовсе не придумают что написать… А чукча, оказывается, не писатель. Всему конечно можно обучиться при желании, но желания нет.

Чаще всего дело в обоих причинах, но если копнуть чуть глубже - обычные смертные не видят полезности в логировании работы, а понимать менеджеров никто не хочет - их принято нелюбить.

Полезность в логировании на самом деле огромная - человек упорядочивает свою работу; понимает насколько он завершил задачу, а сколько еще осталось; дает возможность другим понять и продолжить его труд если вдруг кирпич на голову. В общем-то это важная практика для time management’a.

Хоть и трудно переоценить необходимость логирования, стоит все же смириться, что делать это готов далеко не каждый, а всех жизни учить тоже плохо - раздражает. И нужно ли это на самом деле для руководства - спорный вопрос. Вот почему…

Как Agile начистил рожу логированию

По залогированному времени видно кто сачкует, а кто работу работает. На самом же деле в плане отчетности люди постоянно мухлюют. Если прошло 2 часа, а на реальную работу человек потратил лишь час, залогирует он все равно 2 часа. Даже если говорить про инструменты записывающие состояние рабочего стола и активность нажатий, народ заводит доп. компьютеры и/или пытается отдохнуть кликая без цели по окошкам.

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

Если же говорить про выявление совсем ленивых, так их продуктивность будет и так видна по количеству и качеству сделанной работы, для этого не нужно ничего логировать. Вы, как ни крути, заметите разницу между продуктивными людьми и лежебоками (жопосидами).

По залогированному времени видно насколько правильно мы оцениваем задачи. Если вы до сих пор оцениваете задачи во временных единицах, пора задумываться, пора прислушиваться.. Оценка времени давно показала себя с плохой стороны - не умеет человек оценивать временные рамки. Поэтому задачи оценивают в более грубых единицах - по сложности (story points), либо, и это приобретает популярность, просто на глаз определяют сколько задач брать в итерацию. Ведь в итоге важно не то сколько времени потратил конкретный человек, а сколько задач он сделал.

Залогированное время помогает находить проблемы. Т.е. если человек потратил слишком много времени на какую-то задачу, то либо он плохо разбирается в этой области (и ему нужно помочь), либо задача поставлена неверно, либо та часть приложения с которой он работает - проблематична. Да, это правда может помочь, но только если эти все проблемы не обсуждаются на ежедневных статус-митингах. Т.е. логирование времени тут заменяет общение с людьми. На самом деле общение с людьми не заменить, это всегда более эффективно, поэтому в данном случае (логирование времени как инструмент для выявления проблем) - это один из симптомов того что команда мало общается. На daily standups проблемы и так бы выявились при правильном подходе.

Если логировать, то как?

Во-первых, главное - это логировать не время, а сделанную работу. Лучшее место для этого - commit message если вы работаете с кодом и/или issue tracker. Так мы прогресс держим близко к тому где все крутится, не нужно далеко ходить чтоб узнать что конкретно сделал тот или иной член команды.

У красноглазых есть отличные гайдлайны о том как должны выглядеть сообщения в коммитах и что там должно помещаться, вот еще статейка по-короче. Как правило если ваш коллега подошел к вам с вопросом, ему что-то не понятно о вашем изменении, значит коммит был описан плохо. Это важно во-первых для повседневной работы: не все можно обсудить на статус-митингах, а дергать по каждой мелочи коллег - значит мешать им делать свои задачи. Во-вторых, это история изменений - то что было сделано определить хоть как-то можно по содержимому, то зачем вы сделали это и почему не по-другому - об этом коллегам прийдется догадываться, а ведь не каждая фирма готова оплатить курсы молодого телепата.

По логированию времени (или в общем по time management’у) есть много всего, повторять чужое не хочу, но посоветую все-таки очередной раз попробовать Pomodoro ;) У меня получилось где-то с третьего, и все равно периодически забрасываю.

Подытожим

Логирование как работы, так и потраченного времени - безусловно полезные вещи. Особенно для себя любимого. Научить кого-то это делать - тоже полезно. Но строить по этому какие-то метрики - бесполезная затея.

Read more
19 Jun 2014

InnoDB: транзакции и блокировки

В MySQL, начиная с версии 5.5 по-умолчанию используется движок таблиц InnoDB. В данной статье речь пойдет том каким образом этот движок поддерживает транзакции, какие используются типы и режимы работы блокировок.

InnoDB следует принципам ACID (Atomicity, Consistency, Isolation, Durability). В рамках обсуждаемой темы нас интересуют два принципа: атомарность и изоляция, которые обеспечиваются в основном за счет транзакций и блокировок.

Транзакции

Для обеспечения атомарности любая последовательность операций выполняется в рамках своей транзакции, т.е. все изменения выполняемые транзакцией либо применяются - COMMIT, либо откатываются - ROLLBACK. По-умолчанию каждая новая сессия работает в режиме автоматического коммита. В этом режиме любое SQL-выражение либо будет автоматически закоммичено (при отсутсвии ошибок), либо будет откатано назад (при возникновении ошибки поведение коммита и роллбека зависит от типа ошибки, см. Error Handling). Чтобы выполнять последовательность операций без автокоммита, можно либо воспользоваться выражениями START TRANSACTION / BEGIN, либо выключить автокоммит с помощью выражения SET autocommit = 0. При выключенном автокоммите транзакция будет открыта до тех пор пока явно не выполнится COMMIT или ROLLBACK, которые завершат текущую тразакцию и начнут новую. При выполнении COMMIT или ROLLBACK все блокировки установленные внутри транзакции снимаются.

Изоляция транзакций в InnoDB реализована в соответсвии со стандартом SQL-92 в виде четырех уровней: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE. По-умолчанию используется REPEATABLE READ, т.к. он обычно является более подходящим под требования, чем остальные. Реже используется READ COMMITED, например в ситуациях когда нужно повысить concurrency и можно немного забить на изоляцию. READ UNCOMMITED и SERIALIZABLE используются редко, т.к. это крайности: либо никакой изоляции, либо полная изоляция. Пользователь может изменить уровень изоляции для текущей сессии или для всех новых сессий с помощью выражения SET TRANSACTION. Перед тем как рассмотреть что из себя представляют эти уровни изоляции, стоит кратко пояснить что чтение бывает двух типов: консистентное неблокирующее (Consistent Nonlocking Reads - обычные SELECT) и блокирущее (Locking Reads - SELECT ... FOR UPDATE и SELECT ... LOCK IN SHARE MODE). О типах чтения и блокировках поговорим подробнее чуть позже. Итак, рассмотрим что же из себя представляют уровни изоляции транзакций:

  1. READ UNCOMMITTED. Самый низкий уровень изоляции. При этом уровне изоляции для неблокирующих чтений возможны ситуации когда будут использованы ранние версии данных. Такие чтения будут неконсистентны, их еще называют “грязным чтением”. В остальном этот уровень работает так же как READ COMMITED.
  2. READ COMMITED. При этом уровне изоляции каждое неблокирующее чтение работает со своим собственным свежим снимком данных, в том числе и в рамках одной транзакции. При выполнении блокирующих чтений, UPDATE и DELETE будут блокироваться только индексные записи (record lock) и не будут блокираться интервалы (gaps). Поэтому данный уровень изоляции позволяет вставлять новые строки в интервалы перед индексными записями. Такие строки называют фантомными. Уровень REPEATABLE READ решает данную проблему.
  3. REPEATABLE READ. При этом уровне изоляции каждое неблокирующие чтение (так же как и при READ COMMITED) работает со своим снимком данных, но снимок этот не обновляется в рамках одной транзакции, а используется тот что был получен при первом чтении. Т.е. каждое неблокирующее чтение в рамках одной транзакции является консистентным по отношению к остальным. Поведение блокирующего чтения, UPDATE и DELETE зависит от уникальности индекса и уникальности условия поиска. Когда и индекс и условие поиска уникально - блокируется только найденная индексная запись и не блокируется интервал перед ней. В остальных случаях InnoDB будет блокировать некоторый индексный интервал, используя так называемую интервальную блокировку gap lock или блокировку следующего ключа next-key lock. Блокировки интервалов и следующего ключа позволяют избежать фантомных чтений (Phantom Problem).
  4. SERIALIZABLE. Этот уровень аналогичен REPEATABLE READ, но InnoDB будет неявно преобразовывать все неблокирующие чтения (SELECT), в блокирующие (SELECT ... LOCK IN SHARE MODE) если автокоммит выключен. В случае если автокоммит включен, неблокирующее чтение будет единственным оператором в транзакции, т.е. понятно что транзакция будет read only, соответственно не имеет смысла блокировать записи такой транзакцией.

Блокировки

В InnoDB блокировки происходят на уровне строк, что позволяет различным транзакциям работать с одними и теми же таблицами одновременно. В случае когда транзакция пытается изменить строку, в которую уже вносит изменение другая транзакция, происходит блокировка, т.е. ожидание пока первая транзакция не закончит работу с изменяемой строкой. Любой вид блокировки строк - это по сути блокировка индексов. В случае если в таблице не объявленны уникальные индексы, InnoDB создаст скрытый ключ в виде номера строки (подробнее см. Clustered and Secondary Indexes) и будет вешать блокировки на них. Посмотреть какие индексы есть в таблице можно с помощью команды SHOW INDEX FROM <table name>.

Существует три типа блокировок:

  1. блокировка индексной записи (record lock) - такая блокировка происходит, если условие запроса ограничивает только одну запись уникального индекса (unique index); например, если в таблице t поле c1 является уникальным индексом и существует запись для которой с1 = 10, то при выполнении блокирующего чтения SELECT * FROM t WHERE c1 = 10 FOR UPDATE InnoDB установит блокировку на этот индекс и не допустит чтобы другая транзакция вставила, обновила или удалила строку с полем с1 = 10; если выполнить тот же запрос, но записи с полем с1 = 10 (а соответственно и записи индекса) не будет существовать, то это уже будет блокировка интервала
  2. блокировка интервала (gap lock) - происходит когда блокируется интервал между индексными записями, интервал до первой индексной записи или интервал после последней индексной записи; допустим что в таблице есть две строки для которых с1 = 10 и c1 = 20, т.е. индекс содержит значения 10 и 20; интервалами будут являтся следующие отрезки: (минус бесконечность, 10), (10, 20), (20, плюс беконечность); если мы выполним запрос на блокирующее чтение несуществующей пока записи SELECT * FROM t WHERE c1 = 15 FOR UPDATE, то будет блокирован интервал от 10 до 20, но не включительно, т.е. обновить граничные записи можно, можно даже их удалить, а вот вставка новой строки в этот интервал будет блокирована; еще один интересный пример: если выполнить предыдущий запрос на блокирующее чтение строки, но таблица t будет пуста, то заблокируется интервал, размером во все индексное пространство, т.е. вся таблица
  3. блокировка следующего ключа (next-ket lock) - комбинация блокировок индексных записей и блокировок интервалов; возьмем предыдущий пример, но выполним другой запрос: SELECT * FROM t WHERE c1 > 15; в данном случае помимо индекса со значением 20 заблокируются также интервалы (10, 20) и (20, плюс бесконечность); при этом строку с индексом 10 можно изменять, т.к. она не блокируется; в общем случае блокируемых индексных интервалов и индексных записей может быть гораздо больше, все зависит от условий блокирующего запроса

Таким образом видно, что InnoDB использует блокироки интервалов и блокировки следующего ключа так, чтобы полностью покрыть условие поиска блокирующего запроса и при этом заблокировать наименьшее возможное количество индексного пространства. Рассмотрим как блокировки интервалов и блокировки следующих ключей решают проблему фантомных чтений (Phantom Problem). Фантомное чтение - это когда в рамках одной транзакции возвращается разные результаты. Возьмем последний пример и допустим что для текущей транзакции установлен уровень изоляции READ UNCOMMITTED или READ COMMITED. Выполним блокирующее чтение SELECT * FROM t WHERE c1 BETWEEN 12 AND 18 и получим пустое множество. Теперь в другой транзакции (с любым уровнем изоляции) выполним запрос на вставку строки: INSERT INTO t (c1) VALUES (15). В первой транзакции повторим то же самое блокирующее чтение и в результате получим фантомную строку с полем c1 = 15, вставленную второй транзакцией. Если попробовать провернуть все то же самое но установив для первой транзакции уровень изоляции REPEATABLE READ или SERIALIZABLE, то при попытке вставки строки второй транзакцией она будет блокирована, т.к. первая транзакция заблокирует индексный интервал (10, 20). Таким образом блокирующее чтение будет возвращать один и тот же результат (пустое множество), независимо от действий других транзакций. Блокировка следующего ключа избавляет от фантомных чтений аналогично, с той лишь разницей что в этих блокировках могут участвовать несколько индексных записей и интервалов.

Режимы работы блокировок

InnoDB реализует блокировки строк в двух режимах: чтения (shared (S) lock) и записи (exclusive (X) lock).

Блокировка в режиме чтения позволяет транзакции, получившей эту блокировку читать строку. Блокировка в режиме записи позволяет транзакции не только читать, но и обновлять и удалять строку.

Если транзакция T1 обладает блокировкой чтения на строку r, то запрос от второй транзакции T2 на блокировку той же строки r обрабатывается следующим образом:

  1. запрос на блокировку чтения может быть выдан сразу же, т.е. в результате обе транзакции T1 и T2 будут обладать блокировками чтения на строку r
  2. запрос на блокировку записи не может быть выдан до тех пор, пока транзакция T1 не освободит блокировку чтения на строку r

Если транзакция T1 обладает блокировкой записи на строку r, то запрос от второй транзакции T2 на блокировку любого режима будет приостановлен до тех пор пока транзакция T1 не освободит блокировку записи на строку r.

Для поддежки различной гранулярности блокировки данных, в InnoDB существуют так называемые целевые блокировки (intention locks), которые работают не на уровне строк, а на уровне таблиц. Целевые блокировки предназначены для того чтобы обозначить намерение транзакции получить в будующем блокировки чтения или записи на некоторые строки в данной таблице. Если транзакция собирается получить в таблице только блокировки чтения, то это целевая блокировка чтения (intention shared (IS) lock). Если транзакция намерена получить блокировки записи, то это целевая блокировка записи (intention exclusive (IX) lock). Например, выражение SELECT ... LOCK IN SHARE MODE установит на таблицу целевую блокировку чтения (IS lock), а выражение SELECT ... FOR UPDATE установит целевую блокировку записи (IX lock).

Перед тем как установить блокировку чтения (S lock) на строку в таблице, транзакция должна сначала установить целевую блокировку чтения (IS lock) или блокировку уровнем строже на эту таблицу. Перед тем как получить блокировку записи (X lock) на строку в таблице, транзакция должна сначала установить целевую блокировку записи (IX lock) на эту таблицу. Совместимость блокировок представлена в таблице ниже:

X IX S IS
X Конфликт Конфликт Конфликт Конфликт
IX Конфликт Совместимо Конфликт Совместимо
S Конфликт Конфликт Совместимо Совместимо
IS Конфликт Совместимо Совместимо Совместимо

Если транзакция пытается установить блокировку которая конфликтует с существующей, то она будет остановлена до момента пока существующая блокировка не будет снята. В случаях когда транзакция пытается установить конфликтующую блокировку, которая приведет к так называемой взаимной блокировке (deadlock), InnoDB выбросит ошибку (error).

Целевые блокировки блокируют только запросы на всю таблицу, например LOCK TABLES ... WRITE. Основное предназначение целевых блокировок - это обозначить что транзакция уже блокирует или собирается заблокировать строки в таблице.

Read more
18 Jun 2014

Spock Lifecycle и неинтуитивный блок where

Резюме: как оказывается у Spock’а порою не совсем интуитивный жизненный цикл, особенно это касается блока where который хоть и находится внутри тестового метода, а выполняется не то что до самого метода (что было бы логично), а даже до setup() и до инициализации полей класса.

Spock позволяет достаточно удобно строить data-driven тесты с помощью своего where:

def "square function"(int a, int square) {
        expect:
            square == a * a
        where:
            a | square
           -1 | 1
            0 | 0
            1 | 1
}

Нигде только не упоминается о том как же в итоге это все реализуется. Синтаксис используемый Spock’ом на самом деле не является чистым Groovy, этот код потом трансформируется с помощью Groovy AST. Так вот от того во что он трансформируется зависит возможность использования некоторых фич Spock’a.

Так, например, where будет вынесено отдельным методом в тот же класс и, что интересно, будет вызван до setup(). Цепочку можно проследить тут:

Это значит так же что переменные которые инициализируются в setup() не будут видны в where:

class SomeTest extends Specification {
    int a = 2
    int b = 1

    def setup() {
        b = 1
    }
    def someTest(a, b){
      expect:
        b != a
      where:
        a << [a]
        b << [b]
    }
}

И в итоге мы все равно получим в тесте a == b == 0.

Из этого так же следует, что все слушатели, которые навешаны на beforeTestMethod тоже отработают после того как данные будут инициализированы в where. К таким слушателям относятся и DependencyInjectionTestExecutionListener из Spring TestContext. Т.е. если вы захотите использовать @Autowired поля которые инжектятся из контекста, в where вы их использовать не сможете, там у вас все равно будут null.

Read more
09 Jun 2014

Кэш Hibernate

Одной из основных возможностей Hibernate является использование кэша. При правильной настройке это может давать достаточно большой прирост производительности засчёт сокращения количества запросов к базе данных. Кэш содержит в себе локальную копию данных, которая может сохраняться в памяти либо на диске (имеет смысл, когда приложение и база данных распологаются на разных физических машинах).

Hibernate обращается к кэшу в следующих случаях:

  • Приложение выполняет поиск сущности по идентификатору

  • Приложение выполняет ленивую загрузку коллекции

Кэши разделяются в зависимости от области видимости (scope) на следующие виды:

  • Session scope cache (Кэш привязанный к транзакции, действительный только пока транзакция не завершиться. Каждая транзакция имеет свой кэш, следовательно, доступ к данному кэшу не может быть осуществлён в несколько потоков)

  • Process scope cache (Кэш привязанный к определённому процессу конкретной JVM и общий для многих транзакций с возможностью параллельного доступа)

  • Cluster scope cache (Кэш, общий для нескольких процессов на одной машине или нескольких машин в составе кластера).

По сути, Transaction scope cache представляет собой кэш первого уровня hibernate, кэш же второго уровня может быть реализован либо в области видимости процесса илибо как распределённый кэш.

Общий алгоритм работы кэша hibernate:

  1. Когда сессия пытается загрузить объект в первую очередь он ищется в кэше первого уровня
  2. Если в кэше первого уровня присутствует нужный объект, он возвращается как результат выполнения метода
  3. Если в кэше первого уровня объект не был найден, он ищется в кэше второго уровня
  4. Если объект сохранён в кэше второго уровня, он сохраняется так же в кэш первого уровня и возвращается в качестве результата метода
  5. Если в кэше второго уровня объекта так же не находится, то делается запрос к базе данных, и результат записывается сразу в оба кэша.

Ниже подробенее рассмотрим работу кэша первого и второго уровня.

Кэш первого уровня

Кэш первого уровня в hibernate связан с объектом сессии, он включён по умолчанию и нет возможности отключить его. Когда вы передаёте объект в метод save(), update() или saveOrUpdate(), а так же когда пытаетесь обратиться к нему с помощью методов load(), get(), scroll(), list(), iterate() выполняется добавление элемента в кэш сессии и следующий раз, когда нужно будет произвести повторную выборку данного объекта из БД в текущей сессии обращения к БД уже не произойдёт. Объект будет взят из кэша.

Обнуление кэша происходит после закрытия сессии. Так же, содержимым кэша можно управлять используя методы класса Session:

  • contains() - проверяет сохранён ли объект в кэше

  • flush() - синхронизирует содержимое кэша с базой данных

  • evict() - удаляет объект из кэша

  • clear() - обнуляет кэш

Кэш второго уровня

Кэш второго уровня в hibernate может быть настроен как кэш процесса, так и как распеределённый кэш (в рамках JVM или кластера). В отличие от кэша первого уровня, использование кэша второго уровня является опциональным. Он может быть как включён так и отключён.

В кэше второго уровня сущности хранятся в разобранном состоянии (что-то наподобие сериализованного состояния, однако, используемый алгоритм намного быстрее сериализации). Соответственно, доступ к объектам, сохранённым в кэше второго уроня осуществляется не по сслыке, а по значению. Обусловлено это ещё и тем, что доступ к сущности может осуществляться из параллельных транзакций. Так, каждая транзакция будет иметь свою копию данных.

Учитывая вышеперечисленное, были разработаны следующие стратегии паралельного доступа к кэшу второго уровня:

  • read only - используется для данных, которые часто читаются но никогда не изменяются. Изменение данных при использовании этой стратегии приведёт к исключению. Испльзование данного подхода в распределённом кэше позволяет не волноваться об синхронизации данных, однако может привести к сильной загрузке базы на начальном этапе работы кластера. Связано это с тем, что неизменяемые данные необходимы обычно большенству узлов кластера, и каждый из этих узлов будет обращаться напрямую к базе данных, пока не закэширует все необходимые объекты.

  • nonstrict read write - используется для данных, которые изменяются очень редко. При параллельном доступе к данным из разных транзакций данная стратегия не даёт никакой гарантии, что в кэше будут сохранены актуальные данные, так как Hibernate никак не изолирует от других транзакций данные во время изменения. Следовательно, чтение, которое выполняется параллельно изменению может получить доступ к данным, которые ещё не были закоммичены. Не слудует использовать данную стратегию, если небольшая вероятность считывания устаревших данных критична для приложения. При использовании распределённого кэша вероятность работы с устаревшими данными сильно увеличивается, так как устаревшие данные могут быть отражены в кэше многих узлов кластера и сохраняться там достаточно долго (до следующего обновления).

  • read write - используется для данных которые гораздо чаще читаются, чем обновляются, однако, устаревание которых критично для приложения. При этом подходе, данные блокируются для чтения во время их изменении с использованием “soft-lock” механизма. При попытке обратиться к заблокированным данным в данном случае будет произведён запрос к БД. Блокировака будет снята только после коммита. Данная стратегия обеспечивает уровень изоляции транзакций read commited. Использоване этого попхода для распределённого кэша имеет существенные недостатки. Так, если в кластере имеется возможность изменения одних и тех же данных разными узлами, довольно часто могут случаться блокировки устаревших данных в кэше, что сводит на нет приемущества использования данной стратегии. Для того чтобы избавиться от этого нужно использовать кэш-провайдер, поддерживающий блокировки.

  • transactional - используется, когда необходима изоляция транзакций вполоть до уровня repeatable read. Так же как и предыдущие используется для данных, которые гораздо чаще читаются нежели обновляются.

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

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

  • EhCache - изначально разрабатывался как кэш уровня процесса, однако, в последник версиях появилась возможность реализации так же распределённого кэша. Имеется поддрержка кэша запросов и выгрузки данных на диск.

  • OpenSymphony OSCache - реализует кэш только на уровне процесса. Поддерживает кэш запросов и выгрузку данных на диск.

  • SwarmCache - распределённый кэш, который базирется на JGroups. Нет поддержки кэша запросов.

  • JBoss Cache — может быть как локальным так и распределённым. Это полностью транзакционный кэш с возможностью репликации и инвалидаци, а так же с возможностью обмена как синхронными так и асинхронными валидационными сообщениями.

Однако, не каждый провайдер совместим с каждым типом стратегии параллельного доступа (смотри таблицу ниже)

Read-only Nonstrict-read-write Read-write Transactional
EhCache Yes Yes Yes No
OSCache Yes Yes Yes No
SwarmCache Yes Yes No No
Jboss Cache Yes No No Yes

Стоит заметить, что для удобства поиска, сущности разных классов, а так же сущности, принадлежащие разным коллекциям, сохраняются в разных регионах кэша. Под регионом, в данном случае, понимается некая именованная область кэша. Имя региона совпадает с именем класса, в случае кэширования классов, или с именем каласса объеденённым с именем свойства, в случае кэширования коллекций. Типичная настройка региона кэша на примере EhCache приведена наже:

<cache name="org.example.model.Entity"
	maxElementsInMemory="500" 
	timeToIdleSeconds="0" 
	timeToLiveSeconds="0" 
	eternal="true" 
	overflowToDisk="false" 
/>

Рассмотрим подробнее каждый из параметров:

  1. name — имя региона кэша
  2. maxElemetsInMemory — максимально возможное число объектов, хранимых в памяти
  3. timeToIdleSeconds — задаёт время жизни объекта в кэше с последнего обращения
  4. timeToLiveSeconds — задаёт время жизни объекта в кэше, начиная с его помещения туда
  5. eternal — атрибут определяющий будут ли элементы удаляться из кэша по истечении таймаута. Если этот атрибут установлен в true, то таймаут будет игнорироваться
  6. overflowToDisk — атрибут, определяющий, будут ли объекты выгружаться на диск при достижении maxElemetsInMemory. Имеет смысл включать, если БД и приложение развёрнуты на разных машинах.

Механизмы синхронизации данных в распределённом кэше

Приложения, которыми пользуется параллельно большое количество людей могут потребовать гораздо больей вычислительной мощности, чем может предоставить единичный сервер. В таком случае, несколько физических машин объеденяются в кластер. Для увеличения общей производительности кластера иногда полезно использовать распределённый кэш hibernate. В таком случае, каждый процесс хранит в своём кэше локальную копию недавно используемых данных. Поддержание актуальности этих данных осуществляется засчёт сетевого взаимодействия процессов с использованием протокола TCP либо UDP.

Механизмы поддержания актуальности данных:

  • копирование (replication) - если состояние сущности было изменнено, то её новое состояние будет разослано каждому члену кластера, что ведёт к повышенному потреблению трафика. А так же принуждает разные узлы кластера сохранять в свём кэше одинаковые данные, что не всегда полезно, так как, обычно, разные члены кластера работают с разными данными.

  • анулирование (invalidation) - если состояние сущности было изменено, то всем узлам кластера рассылается сообщение, указывающее на то, что сущность с определённым идентификатором была изменена. После получения сообщения другие члены кластера проверяют свой кэш на предмет наличия данной сущности, и, если таковая там находися, она удаляется из кэша. Данный подход не создаёт такой большой нагрузки на сеть как предыдущий, а основным его недостатком является то, что если изменённая сущность снова понадобится, она будет загружена запросом к базе данных, а это может привести к дополнительной нагрузке на БД. Однако, данные, которые необходимы многим узлам кластера обычно изменяются очень и очень редко.

Механизмы передачи сообщений о состоянии объектов:

  • синхронный - после отправи сообщения об изменении объекта в кэше доступ к кэшу блокируется, пока не будет получено подтверждение от всех узлов кластера.

  • асинхронный - отправка сообщения об изменении сущности не блокирует доступ к кэшу и не требует подтверждения от всех машин кластера. Вообще, при таком подходе, отпрака сообщений об изменении может происходить не обязательно сразу после изменения, а может быть реализована некая очередь собщений и расслыка в фоновом потоке.

Выбор той или иной стратегии, в основном, зависит от конкретной реализации, а именно насколько часто разные узлы кластера обращаются к одним и тем же модифицируемым данным и насколько критично считывание устаревших данных.

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

Read more
15 May 2014

CookieTheftException в Spring Security "remember me"

Многие проекты, использующие для аутентикации пользователей Spring Security, сталкиваются со следующей проблемой: при входе в приложение с использованием «remember me», пользователей то и дело разлогинивает с выбросом CookieTheftException. Так, подобная проблема была найдена на багтрекере Atlassian и Grails. Не обошла она стороной и наш родной JTalks.

Для того, чтобы понять почему выбрасывается данное исключение, нужно разобраться в методах реализации функционала «remember me».
В Spring security для этого используется два основных подхода:

  • Подход, основанный на хешировании токенов (Simple hash-based token approach)

  • Подход, основанный на сохраняемых токенах (Persistent token approach)

Simple hash-based token approach использует md5-хеширование для реализации «remember me» стратегии. Реализуется всё это в классе TokenBasedRememberMeServices. При первом успешном входе пользователя с включённой опцией “запомнить меня”, срабатывает метод onLoginSuccess, который и устанавливает значение remember me куки. Сам файл cookie формируется следующим образом:

base64(username + ":" + expirationTime + ":" +md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

    username:          Имя пользователя
    password:          Пароль пользователя
    expirationTime:    Дата и время окончания срока действия cookie (в милисекундах)
    key:               Уникальный ключ приложения

Cрок действия файла cookie устанавливается равным двум неделям. И, пока пока этот файл не устарел и не удалён, пользователь не будет вынужден вводить свои учётные данные при входе на страницу сайта. При последующем входе сработает метод processAutoLoginCookie, который будет искать пользователя по имени, полученном из cookie, а так же вычислять md5 хэш конкатенации имени пользователя, даты устаревания, пароля и ключа, и сравнивать его с полученным из cookie. Если всё пройдёт успешно, пользователь будет авторизован.

Недостатки:

  • Недостаточная безопасность (файлы cookies на протяжении всего времени жизни не изменяются, а значит легко могут быть украдены и использованы злоумышленником для входа в приложение)

  • Необходимость повторного входа после смены пароля, так как файл cookie зависит от пароля

Persistent token approach реализуется в классе PersistentTokenBasedRememberMeServices и основан на сохранении токенов в хранилище. При данном подходе сам токен представляет собой объект класса PersistentRememberMeToken, в котором хранятся следующие данные :

  • Значение токена

  • Серия токена

  • Имя пользователя

  • Дата и время окончания срока действия токена

При этом механизм сохранения должен реализовывать интерфейс PersistentTokenRepository.
На данный момент существуют две общеизвестных реализации данного интерфейса: InMemoryTokenRepositoryImpl и JdbcTokenRepositoryImpl.

InMemoryTokenRepositoryImpl - хранилище на основе обычного HashMap. В реальных проектах его рекомендуется использовать исключительно для отладки.

JdbcTokenRepositoryImpl - хранилище основанное на реляционной базе данных. Как можно заметить по названию класса, механизмом доступа к хранилищу является spring jdbc. При использовании данного хранилища в базе данных приложения будет создана таблица persistent_logins со столбцами, соответствующими полям класса PersistentRememberMeToken. Первичным ключом будет являться серия токена.
При первом успешном входе пользователя в приложение с включённой опцией “запомнить меня” сработает метод onLoginSuccess. В данном методе создаётся новый объект класса PersistentRememberMeToken, в котором значение и серия токена это случайным образом сгенерированные строки закодированные по алгоритму base64. Файл cookie формируется следующим образом:

base64(tokenSeries + ":" + tokenValue)

tokenSeries:         Серия токена
tokenValue:          Значение токена

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

  • При каждом входе пользователя с использованием механизма «remember me» значение токена генерируется случайно и файл cookie переписывается, таким образом, каждый вход выполняется с новым значением токена. При этом, если cookie были похищены и злоумышленник не успел ими воспользоваться, после следующего входа пользователя они станут недействительными.

  • Значение файла cookie не зависит от пароля пользователя, а значит при смене пароля повторный вход не требуется

При последующем входе пользователя будет срабатывать уже метод processAutoLoginCookie. Данный метод отыскивает токен в хранилище по его серии и, если таковой был найден и не устарел, проверяет совпадают ли значение токена в файле cookie с сохранённым в хранилище. Затем, если всё прошло успешно, случайно генерируется новое значение токена, которое записывается в хранилище и файл cookie.
Если же токен по серии не был найден, пользователь входит в приложение как аноним, потому что могут существовать механизмы удаления устаревших токенов из хранилища.
Если значение токена, найденного в хранилище, не совпадает со значением токена в файле cookie, считается, что куки могли быть похищены злоумышленником. Пользователь уведомляется об этом по средствам выбрасывания CookieTheftException.

Обычно текст ошибки выглядит следующим образом:

RememberMe exception:
org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.

Однако, несовпадение значения токена в хранилище и файлах cookie может вызывать не только похищение последних, но и последовательные запросы от одного клиента, проходящие в достаточно короткий промежуток времени через метод processAutoLoginCookie.
Происходит это по следующей схеме:

  • клиент отправляет серверу запрос 1

  • не дождавшись ответа на запрос 1, клиент отправляет запрос 2

  • запрос 1 попадает в метод processAutoLoginCookie

  • из базы извлекается токен, значение которого совпадает со значением токена в файле cookie запроса 1

  • генерируется новое значение токена, которое сохраняется в базу данных и cookie ответа на запрос 1

  • в это время запрос 2 со старыми куками попадает в метод processAutoLoginCookie

  • из базы извлекается токен, значение которого изменено запросом 1

  • после сравнения значений выбрасывается CookieTheftException, так как значения не совпадают.

При этом возникает вполне закономерный вопрос: «как такое возможно?».

Самой очевидной причиной является то, что разного рода ресурсы, такие как изображения, файлы скриптов, таблицы стилей, проходят через remember me фильтры Spring Security. Исправляется это достаточно просто. Нужно лишь добавить исключения для фильтров в файл secutity-context.xml. Для Spring Security версии ниже 3.1 это будет выглядеть следующим образом:

<security:intercept-url pattern="/resources/**" filters="none"/>
    <security:intercept-url pattern="/admin/logo" filters="none"/>
    <security:intercept-url pattern="/admin/icon/**" filters="none"/>
    <security:intercept-url pattern="/errors/**" filters="none"/>
    <security:intercept-url pattern="/users/*/avatar" filters="none"/>

Начиная со Spring Security 3.1 использование атрибута filters="none" считается устаревшим, и вместо этого рекомендуют использовать множественные тэги <http>:

<http pattern="/resources/**" security="none"/>
    <http pattern="/admin/logo" security="none"/>
    <http pattern="/admin/icon/**" security="none"/>
    <http pattern="/errors/**" security="none"/>
    <http pattern="/users/*/avatar" security="none"/>

Другим источником множественных запросов является предзагрузка веб-страниц браузером, в частности Google Chrome. Работает она следующим образом. Когда пользователь начинает вводить что-нибудь в адресную строку, браузер автоматически загружает содержимое страниц, которые считает наиболее релевантными введённому запросу. К тому времени, как пользователь отдаёт команду перейти на сайт, часть данных, скорее всего, окажется уже загруженной. Однако, на практике случается так, что ответ на запрос предзагрузки не успевает дойти до клиента, когда пользователь отдаёт команду перехода на сайт. И запрос со старыми значениями cookie попадает в метод processAutoLoginCookie из-за чего и вызывается CookieTheftException.

Всё это особо осложняет тот факт, что Google Chrome не посылает никаких отличительных хедеров в запросе на предзагрузку и вообще не позволяет никак отличтить его от обычного запроса. Выходит, единственной отличительной особенностью предзагрузочного запроса является то, что сразу за ним следует запрос с абсолютно такими же данными в файлах cookie.

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

public class CachedRememberMeTokenInfo {

    private String value;
    long cachingTime;

    public CachedRememberMeTokenInfo(String tokenValue, long cachingTime) {
        this.value = tokenValue;
        this.cachingTime = cachingTime;
    }

    /**
     * Gets token date and time of token caching as milliseconds
     * @return Date and time of token caching
     */
    public long getCachingTime() {
        return cachingTime;
    }

    public String getValue() {
        return value;
    }
}

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

public class ThrottlingRememberMeService extends PersistentTokenBasedRememberMeServices {
    private final static String REMOVE_TOKEN_QUERY = "DELETE FROM persistent_logins WHERE series = ? AND token = ?";
    // We should store a lot of tokens to prevent cache overflow
    private static final int TOKEN_CACHE_MAX_SIZE = 100;
    private final RememberMeCookieDecoder rememberMeCookieDecoder;
    private final JdbcTemplate jdbcTemplate;
    private final Map<String, CachedRememberMeTokenInfo> tokenCache = new ConcurrentHashMap<>();
    private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
    // 5 seconds should be enough for processing request and sending response to client
    private int cachedTokenValidityTime = 5 * 1000;

    /**
     * @param rememberMeCookieDecoder needed for extracting rememberme cookies
     * @param jdbcTemplate            needed to execute the sql queries
     * @throws Exception - see why {@link PersistentTokenBasedRememberMeServices} throws it
     */
    public ThrottlingRememberMeService(RememberMeCookieDecoder rememberMeCookieDecoder, JdbcTemplate jdbcTemplate)
            throws Exception {
        super();
        this.rememberMeCookieDecoder = rememberMeCookieDecoder;
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * Causes a logout to be completed. The method must complete successfully.
     * Removes client's token which is extracted from the HTTP request.
     * {@inheritDoc}
     */
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String cookie = rememberMeCookieDecoder.exctractRememberMeCookieValue(request);
        if (cookie != null) {
            String[] seriesAndToken = rememberMeCookieDecoder.extractSeriesAndToken(cookie);
            if (logger.isDebugEnabled()) {
                logger.debug("Logout of user " + (authentication == null ? "Unknown" : authentication.getName()));
            }
            cancelCookie(request, response);
            jdbcTemplate.update(REMOVE_TOKEN_QUERY, seriesAndToken);
            tokenCache.remove(seriesAndToken[0]);
            validateTokenCache();
        }
    }

    /**
     * Solution for preventing "remember-me" bug. Some browsers sends preloading requests to server to speed-up
     * page loading. It may cause error when response of preload request not returned to client and second request
     * from client was send. This method implementation stores token in cache for <link>CACHED_TOKEN_VALIDITY_TIME</link>
     * milliseconds and check token presence in cache before process authentication. If there is no equivalent token in
     * cache authentication performs normally. If equivalent present in cache we should not update token in database.
     * This approach can provide acceptable security level and prevent errors.
     * {@inheritDoc}
     * @see <a href="http://jira.jtalks.org/browse/JC-1743">JC-1743</a>
     * @see <a href="https://developers.google.com/chrome/whitepapers/prerender?csw=1">Page preloading in Google Chrome</a>
     */
    @Override
    protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain " + 2 +
                    " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        }

        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];

        PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);

        if (token == null) {
            // No series match, so we can't authenticate using this cookie
            throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
        }

        UserDetails details = null;

        if (isTokenCached(presentedSeries, presentedToken)) {
            tokenCache.remove(presentedSeries);
            details = getUserDetailsService().loadUserByUsername(token.getUsername());
            rewriteCookie(token, request, response);
        } else {
            /* IMPORTANT: We should store token in cache before calling <code>loginWithSpringSecurity</code> method.
               Because execution of this method can take a long time.
             */
            cacheToken(token);
            try {
                details = loginWithSpringSecurity(cookieTokens, request, response);
            //We should remove token from cache if cookie really was stolen or other authentication error occurred
            } catch (RememberMeAuthenticationException ex) {
                tokenCache.remove(token.getSeries());
                throw ex;
            }
        }
        validateTokenCache();

        return details;
    }

    /**
     * Calls PersistentTokenBasedRememberMeServices#processAutoLoginCookie method.
     * Needed for possibility to test.
     */
    @VisibleForTesting
    UserDetails loginWithSpringSecurity(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        return super.processAutoLoginCookie(cookieTokens, request, response);
    }

    /**
     * Sets valid cookie to response
     * Needed for possibility to test.
     */
    @VisibleForTesting
    void rewriteCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
        setCookie(new String[] {token.getSeries(), token.getTokenValue()}, getTokenValiditySeconds(), request, response);
    }

    @Override
    public void setTokenRepository(PersistentTokenRepository tokenRepository) {
        this.tokenRepository = tokenRepository;
        super.setTokenRepository(tokenRepository);
    }

    /**
     * Stores token in cache.
     * @param token Token to be stored
     * @see CachedRememberMeTokenInfo
     */
    private void cacheToken(PersistentRememberMeToken token) {
        if (tokenCache.size() >= TOKEN_CACHE_MAX_SIZE) {
            validateTokenCache();
        }
        CachedRememberMeTokenInfo tokenInfo = new CachedRememberMeTokenInfo(token.getTokenValue(), System.currentTimeMillis());
        tokenCache.put(token.getSeries(), tokenInfo);
    }

    /**
     * Removes from cache tokens which were stored more than <link>CACHED_TOKEN_VALIDITY_TIME</link> milliseconds ago.
     */
    private void validateTokenCache() {
        for (Map.Entry<String, CachedRememberMeTokenInfo> entry: tokenCache.entrySet()) {
            if (!isTokenInfoValid(entry.getValue())) {
                tokenCache.remove(entry);
            }
        }
    }

    /**
     * Checks if given tokenInfo valid.
     * @param tokenInfo Token info to be checked
     * @return <code>true</code> tokenInfo was stored in cache less than <link>CACHED_TOKEN_VALIDITY_TIME</link> milliseconds ago.
     * <code>false</code> otherwise.
     * @see CachedRememberMeTokenInfo
     */
    private boolean isTokenInfoValid(CachedRememberMeTokenInfo tokenInfo) {
        if ((System.currentTimeMillis() - tokenInfo.getCachingTime()) >= cachedTokenValidityTime) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Checks if token with given series and value stored in cache
     * @param series series to be checked
     * @param value value to be checked
     * @return <code>true</code> if token stored in cache< <code>false</code> otherwise.
     */
    private boolean isTokenCached(String series, String value) {
        if (tokenCache.containsKey(series) && isTokenInfoValid(tokenCache.get(series))
                && value.equals(tokenCache.get(series).getValue())) {
            return true;
        }
        return false;
    }

    /**
     * Needed for possibility to test.
     */
    public void setCachedTokenValidityTime(int cachedTokenValidityTime) {
        this.cachedTokenValidityTime = cachedTokenValidityTime;
    }
}
Read more
14 May 2014

Зачем нужны сервера приложений, если есть TomCat

Молодые программисты часто задают вопрос: а зачем нужны зачастую довольно тяжелые и дорогие промышленные сервера приложений (такие как JBoss AS, Oracle WebLogic, IBM WebSphere AS), если у нас есть замечательный легковесный фреймворк Spring и контейнер сервлетов Apache TomCat. Попробуем на него ответить. Сразу замечу, речь сейчас не идет об архитектуре приложения! Не важно, используете вы EJB или нет. Предположим, у вас приложение на Spring Framework и стоит вопрос, на чем его запускать. Итак, какие дополнительные сервисы предлагает нам сервер приложений.

  • Пулы соединений с БД. Да, у TomCat тоже есть пул соединений, но каковы его возможности? Может ли он периодически тестировать доступность СУБД и обновлять соединения в случае восстановления после сбоев? Умеет ли он делать замену прав доступа? Грубо говоря, подключаемся к БД под пользователем в зависимости от того, кто аутентифицировался в нашем приложении, если часть логики вынесена на уровень СУБД, то это бывает полезно. Может ли пул соединений Tomcat балансировать нагрузку между несколькими базами данных (например, в случае Oracle RAC), а так же определять, что вот эти узлы RAC умерли и теперь к ним не нужно пытаться подключаться, а затем понять, что они снова доступны и теперь их тоже можно использовать? В конце концов, может ли ваш пул соединений защитить от некорректного кода в приложении, которое по недосмотру не возвращает соединения, просто отбирая его после какого-то таймаута?
  • JMS. Если вы хотите использовать очереди в вашем приложении, развернутом на TomCat, то придется отдельно еще поднимать сервера очередей сообщений. В случае сервера приложений, очереди как правило доступны их коробки. Вместе с очередями доступны так же следующие вещи: кластеризация - вы можете строить распределенные очереди, расположенные сразу на нескольких серверах, что существенно увеличивает масштабируемость и доступность вашего приложения, миграция - в случае падения одного из серверов, его очереди автоматически перемещаются на другой, сохраняя необработанные сообщения. В некоторых серверах приложений поддерживается Unit-of-Order - гарантированный порядок обработки сообщений, удовлетворяющих некоторым критериям, очень часто при интеграции бывает полезен.
  • JTA. Те самые распределенные транзакции. Кто-то их понимает и использует, кто-то считает слишком тяжелыми. Как правило это так, они слишком тяжелые, но если вам нужно обеспечить согласованность данных в СУБД, разнесенных по разным углам нашей необъятной, или в СУБД и очереди, то без таких транзакций будет трудно. Суть распределенных транзакций в том, что мы не коммитим ни в одну из БД, пока не убедимся, что все БД, участвующие в транзакции, смогут принять наши данные. Тем самым мы избегаем проблемы “с одного счета в одном банке деньги списали, а на другой - в другом банке - не зачислили - сработало ограничение целостности”.
  • Безопасность. Современные сервера приложений предоставляют множество различных провайдеров безопасности. Доступны различные провайдеры аутентификации, вы можете хранить ваших пользователей во множестве мест: во встроенном LDAP-сервере, в базе данных, во внешнем LDAP-сервере, в различных Internet-directory - специализированных приложениях для управления правами доступа. Возможны следующие сценарии: на работу наняли человека, добавили его в Internet-directory/Access-manager, там запустился процесс раздачи прав, который выдал человеку права на все ресурсы вашего предприятия и теперь каждый сервер приложений в вашей компании (а их может быть очень много) видит эти права, так как подключен к этой Internet-directory/Access-manager. Доступно разделение пользовательской сессии между приложениями: мы аутентифицировались в одном приложении - нам уже не нужно аутентифицироваться в другом. Так же доступна реализация Single-Sign-On: вы делаете один из серверов базой для хранения пользователей, все другие сервера при аутентификации пользователя обращаются к этой базе. Реализуется SSO посредством Security Assertion Markup Language (SAML) 1/2 или посредством Simple and Protected Negotiate (SPNEGO) и Kerberos для Windows-клиентов. Возможна авторизация посредством протокола eXtensible Access Control Markup Language (XACML), позволяющего описывать довольно сложные политики (например, приложение доступно пользователю только в рабочее время). Опять же все данные возможности работают в кластерном окружении. Впрочем, стоит отметить, что с помощью Spring Security и Apache Shiro можно реализовать большинство из них, но вам придется “тянуть” эти реализации за каждой вашей программой, в то время как в сервере приложений они доступны из коробки.
  • Масштабируемость и высокая доступность. Да, для TomCat мы можем настроить кластеризацию, но она будет довольно примитивной. Мы не сможем сделать передачу пользовательской сессии из одного центра обработки данных (ЦоД) в другой через Интернет, мы не сможем эффективно настроить репликацию сессий на большом кластере, состоящем из 40-50 экземпляров сервера приложений. В случае сбоев, мы не сможем обеспечить миграцию экземпляров сервера на другую машину и т.д. Так же в TomCat нет механизмов автоматического мониторинга и реакции на ошибки: мы не можем автоматически перезапустить экземпляр сервера, если на нем зависло 10 потоков, мы не можем автоматически отправить письмо администратору при переполнении пула соединений и т.д.
  • Управляемость. В случае большого кластера TomCat у нас нет единого центра управления, т.н. AdminServer и аналога NodeManager’а. Мы не сможем одновременно запустить на старт 50 экземпляров сервера. Мы не можем посмотреть состояние экземпляров, посмотреть сколько у нас обработчиков на той или иной очереди, на том или ином сервере, сколько создано соединений с той или иной БД, какие из них можно убить, какие в данный момент выполняются транзакции, какие ресурсы в них задействованы и т.д. Конечно, можно все сделать “за три минуты на скриптах, ну как в Линуксе принято”, но результат будет плачевный.
  • Скриптовый язык. Кстати о скриптах, большинство промышленных серверов приложений содержат утилиты для выполнения скриптов как правило на языке Python, Пользоваться данными утилитами одно удовольствие. Администратор может описать в виде скрипта все шаги для подготовке к развертыванию сколь угодно большого приложения, таким образом запуск в продуктив или обновление займет сравнительно немного времени. С помощью таких скриптов можно создавать источники данных (представьте себе сервисную шину, подключенную к 120 экземплярам БД), JMS-очереди, менеджеры потоков, создавать новые экземпляры серверов и добавлять их в кластер, выполнять остановку и запуск серверов, их миграцию.
  • Административный канал и развертывание в промышленном режиме. Некоторые сервера приложений позволяют включить так называемый административный канал - отдельный порт, защищенный SSL, запросы по которому имеют приоритет. Таким образом, даже если ваш сервер завис, вы сможете на него зайти и посмотреть, какие транзакции выполняются и какие потоки висят. Но у данного канала есть и другое применение. При обновлении приложения вам не нужно выключать старую версию! Вы можете добавить на сервер новую версию приложения в административном режиме - пользователи продолжают работать со старой, а по административному каналу доступна новая, соответственно мы можем выполнить последнее тестирование перед запуском, проверить все ли у нас правильно развернулось. Затем мы окончательно публикуем приложение, при этом пользователи, уже имеющие сессию, продолжают работать со старой версией, чтобы не потерять данные. Новые пользователи аутентифицируются на новой версии. Тем самым мы обновляем приложение без его простоя, что очень важно для критических систем.

Как мы видим, сервисов, предоставляемых промышленными серверами приложений, довольно много. Возникает вполне закономерный вопрос, а почему сервлет-контейнер TomCat такой популярный? Здесь есть несколько соображений:

  • В первую очередь, цена. За все хорошее нужно платить, за отличное платить еще больше, особенно, если мы хотим доступ к технической поддержке и патчам. К примеру, сервер приложений Oracle WebLogic в базовой комплектации стоит $10 000/processor (под processor здесь понимается одно ядро, умноженное на т.н. core factor). Не каждый заказчик может себе позволить такое решение.
  • Не всем приложениям нужны вышеперечисленные сервисы, а иногда разработчики не умеют ими пользоваться. Например, если у нас простая учетная система, работающая с одной БД, то нам не нужны распределенные транзакции. С другой стороны, масштабирование. Приложение может следовать всем Java EE спецификациям, но при этом не быть масштабируемым. Простой пример: приложение читает из БД измененные записи (которые пишутся с помощью триггеров в отдельную табличку) и передает их в другую БД. При этом авторы как-то забыли про блокировки. Если мы запустим данную программу на кластере, то у нас каждая запись будет обрабатываться N-раз, по числу экземпляров TomCat в кластере. Такая масштабируемость нам не нужна. Аналогичные соображения можно привести и для других сервисов.
  • Простота и легкость освоения. Вообще администратор сервера приложений это отдельная профессия, такая же как и администратор баз данных. Это не просто линукс-админ. Посмотрим еще раз на список сервисов и задумаемся как долго нужно изучать возможности выбранного сервера приложений по их реализации и настройке. Курсы по администрированию IBM WebSphere или Oracle WebLogic могут стоить десятки тысяч рублей.
  • Сварим кашу из топора сами. Бывают ситуации, когда это необходимо. Не всегда есть смысл ждать патча, исправляющего какие-то критические для нашего приложения ошибки. Гораздо быстрее просто подменить версию библиотеки. Правда зачастую это можно сделать и на сервере приложений, добавив библиотеку к нашему приложению и настроив загрузчик классов. Причем современные сервера содержат в себе утилиты для поиска ошибок в иерархии загрузчиков.

Отдельно отметим причины популярности Spring Framework как на TomCat’е, так и на промышленных серверах приложений и немного их прокомментируем:

  • Исторические причины. Почему Spring Framework, а не EJB? Ну потому что я в 88-м году программировал на С++, фигня этот ваш С++. Да, действительно EJB 1.1 и EJB 2.x были очень тяжелы для освоения и для использования, но времена меняются. Опять же, начиная с Java EE 6, появился легковесный IoC-контейнер - CDI. Зачем тянуть в свое приложение сотни мегабайт библиотек, которые будут существенно замедлять процесс развертывания, если можно использовать готовые и довольно качественные реализации, предоставленные производителем сервера приложений? На самом деле иногда есть зачем.
  • Баланс между завязкой на конкретном производителе и переносимостью. Да, EJB это часть спецификации Java EE, причем наиболее сложная, сложнее только J2CA и по хорошему приложения, написанные для одного сервера, должны работать на другом. На практике это не всегда так. Зачастую для эффективного использования всех возможностей сервера приложений приходится в коде вызывать его API, а это уже делает приложение непереносимым. Правда, справедливости ради, с каждой новой версией Java EE таких завязок становится все меньше. С другой стороны, даже без явных завязок на API части стандарта могут быть реализованы разными серверами по своему, например, один сервер будет закрывать EntityManagerFactory при остановке приложения, другой - нет. Реализации иерархии загрузчиков классов так же могут отличаться.
  • При этом, явная завязка на Spring Framework тоже имеет свои минусы. Это такая же завязка на производителе, как и решение использовать только WebLogic. Но если с WebLogic хоть и со скрипом мы сможем уйти, то со Spring Framework скорее всего нет. Что будет, если завтра ведущие разработчики решат оставить свое детище и все дружно перейти в Oracle? Впрочем, думаю, что вероятность такого сценария не высока.
  • Отдельно стоит отметить поддержку Spring Framework со стороны разработчиков серверов приложений. Например, в Oracle WebLogic можно включить отдельную страницу в консоли администрирования для каждого построенного на данном фреймворке приложения. На странице будет отображено дерево бинов и показаны их свойства. Так же доступны бины самого сервера и упрощена разработка MBean’ов. Помимо этого, Spring Framework прозрачно интегрируется в кластерное окружение, а Spring Security может использовать подсистему безопасности сервера приложений.

В заключение хочется отметить, что выбор платформы для приложения это довольно нетривиальная инженерная задача, в которой должна учитываться масса факторов. Это и соотношение стоимости разработки к стоимости поддержки (при этом нужно учитывать, что разработка может идти год, а использоваться ПО может десяток лет), стоимость самих серверов приложений, ваши отношения с вендором, т.к. несмотря на высокую номинальную стоимость зачастую предоставляются скидки под 80%. Учитывайте вашу и вашей команды квалификацию в конце концов. Ну и не нужно быть ретроградом, если вы в 2001-м писали на EJB и с тех пор смотреть на них не можете, то это еще не повод отказываться от этой замечательной технологии и реализующих ее серверов приложений, но даже если вы гуру Spring Framework, подумайте, может быть для него на промышленном сервере тоже найдется место?

Read more
30 Apr 2014

A collection was no longer referenced by the owner

Полная ошибка выглядит так:

org.hibernate.HibernateException: A collection with cascade="all-delete-orphan" 
was no longer referenced by the owning entity instance: 
org.javatalks.training.hibernate.entity.bag.Book.comments`.

Собственно ошибка говорит о том, что была у Сущности коллекция, стоял значит у нее all-delete-orphan, а тут вместо этой коллекции Сущности подсунули другую. Код, ответственный за такой случай находится в классе org.hibernate.engine.internal.Collections:
if ( e != null && e.getStatus() != Status.DELETED && e.getStatus() != Status.GONE ) {
	throw new HibernateException(
			"A collection with cascade=\"all-delete-orphan\" was no longer referenced by the owning entity instance: " +
			loadedPersister.getRole()
	);
}

Разработчики вполне намеренно проверяют, если на коллекцию прежний владелец не ссылается (и владелец не был удален), то это проблема. Почему это проблема к сожалению никто не указывает - видимо это перестраховка на случай когда пользователь случайно перезаписал коллекцию. Правильно все-таки делать remove()/removeAll() на самой коллекции. Два случая когда такое воспроизводится (код на Groovy):

Коллекция перезаписывается и владелец update’ится

@Test
    void 'when updating, if orphan collection is dereferenced, exception thrown'() {
        List<Comment> comments = [new Comment(body: "comment1"), new Comment(body: "comment2")]
        Book original = new Book(title: "with comments", comments: comments)
        bookDao.save(original).flushSession()

        try {
            //it's not allowed to replace collection with cascade=orphan
            original.comments = [new Comment(body: 'new')]
            bookDao.saveOrUpdate(original).flushSession()
            assert false
        } catch (HibernateException e) {
            assert e.message.startsWith('A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance')
        }
    }

Владелец merge’ится, однако новая коллекция - null

А вот этот случай хитрый. Обычно если мы merge’им детачнутую сущность, то коллекция нормально обновляется - отсутствующие в новой коллекции элементы будут удаляться. Но не тут-то было когда коллекция является null’ом:

@Test
    void 'when merging, if new collection is null, exception thrown'() {
        List<Comment> comments = [new Comment(body: "a"), new Comment(body: "b")]
        Book original = new Book(title: "with comments", comments: comments)
        bookDao.save(original).flushAndClearSession()

        try {
            bookDao.merge(new Book(id: original.id, comments: null))//exception only due to null
            bookDao.flushSession()
            assert false
        } catch (HibernateException e) {
            assert e.message.startsWith('A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance')
        }
    }

Read more
13 Jan 2014

Nexus - почему не нужно использовать LATEST

Везде, где мне приходилось работать с Sonatype Nexus в какой-то момент возникало недопонимание о том что такое LATEST версия артефакта и почему ее нельзя использовать.

Любой артефакт обладает неким информационным файлом maven-metadata.xml, содержимое которого приблизительно такое:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-javadoc-plugin</artifactId>
  <versioning>
    <latest>2.9.1</latest>
    <release>2.9.1</release>
    <versions>
      <version>2.8.1</version>
      <version>2.9</version>
      <version>2.9.1</version>
    </versions>
    <lastUpdated>20130628064237</lastUpdated>
  </versioning>
</metadata>

Итак, вы захотели получить последнюю версию артефакта. Вы быстро нагуглите как это можно сделать: http://somerepo.org/service/local/artifact/maven/redirect?r=central-proxy&g=groupId&a=artifactId&v=LATEST И первое время это возможно даже будет работать. Однако в какой-то момент Nexus начнет вам возвращать старые артефакты.

Это происходит из-за алгоритма, который использует Nexus для нахождения LATEST: сначала смотрит если в maven-metadata.xml есть <latest> и возвращает его. Если тега нет, то вернется последняя версия в списке <versions>.

Изначально механизм этот работает лишь потому, что в maven-metadata.xml просто нет <latest>. Версии отсортированы в порядке добавления, возвращается последняя. Но в какой-то момент происходит что-то из двух:

  • Кто-то таки проставляет <latest> в мета-файл.

  • Работа идет в ветках и две команды независимо друг от друга заливают артефакты с разными версиями. Кто залил последний - того и последняя версия. Просто-напросто в конец добавляется та версия, которая сейчас деплоится.

Как оказывается в Nexus LATEST работает правильно только для плагинов и использовать этот механизм для обычных артефактов нельзя.

Несколько ожидаемых вопросов:

Кто обновляет maven-metadata.xml?

Этот файл обновляется не Nexus’ом, а Maven Deploy Plugin’ом: сначала он забирает из репозитория существующий maven-medatadata.xml, затем обновляет информацию и заливает обратно в репозиторий. Если существующего файла нет, создается новый и заливается в Nexus.

Откуда берется <latest> если Maven его никак не использует?

Maven этот тег и не проставляет. У Nexus’a есть некие утилитарные механизмы, среди таких - Rebuild Metadata. Этот пункт можно найти в контекстном меню репозиториев и каталогов. Этот механизм удаляет существующую maven-metadata.xml, просматривает артефакты и создает новый файл. И в этот момент он создает <latest>. Для чего - хрен знает, но создает. Причем я замечал <latest> даже в том случае, когда Rebuild Metadata вроде как и не делался, поэтому есть подозрения что это происходит еще по каким-то событиям. Имейте в виду, что сортировка версий происходит исходя из механизмов зашитых в Maven, а эти механизмы уже используются Nexus’ом:

The ordering is derived by parsing the version string and supports sematic versioning with additional semantics for specific classifiers. Further details can be found in the documentation for the implementing class GenericVersionScheme.

Почему <latest> устаревает?

Даже если Rebuild Metadata и происходит, то это разовое действие. Дальше метадата-файл обновлять все равно будет Maven. А тот просто ничего не делает с <latest> - она копируется из того maven-metadata.xml, который был загружен из Nexus’a.

Подведем итоги

Если вам нужен механизм получения последней версии артефакта, этот механизм вы сами и должны будете создать (написать скрипт). Механизм, используемый Nexus’ом по умолчанию в какой-то момент просто перестанет работать.

К тому же если вы считаете вам нужна где-то последняя версия артефактов, скорей всего вы двигаетесь в неправильном направлении, мне не приходилось видеть хорошо поставленные процессы где была нужда в LATEST.

PS: ссылки на Sonatype Wiki сломались, они либо перенесли куда-то страницы, либо убрали их из открытого доступа. Я обновлю адреса как только найду новое расположение.

Read more
30 Nov 2013

"Правильные" прототипы. Путь Ninja.

Весь код можно посмотреть и скачать на GitHub

Основная идея Ninja framework это сразу приступать к работе, а не сидеть и настраивать проект. Часто прототипы это сделанные на скорую руку поделки для демонстрации и чаще всего сами прототипы не развиваются дальше, а пишется все по новой. Ну или если прототип изначально делался “правильно” и в нем все настроено (cache, i18n, миграция баз, конфиги приложения) он скорее всего будет дорабатываться в дальнейшем, но на него потребуется больше времени для разработки. На такую разработку обычно нет времени и все лепится на скорую руку, жестко забивают настройки приложения в код. Ninja как раз помогает пройти этап настройки и сразу приступить к разработке. Хотя я сам не люблю всякие комбайны, которые ограничивают тебя в выборе, но в данном случае, лично мне понравилось.

Документация
Документации там немного и как говорится - все по делу. Со всей докой можно за день ознакомится.

Лучше всего посмотреть на практике это.
Создаем Maven проект из архетипа mvn archetype:generate -DarchetypeGroupId=org.ninjaframework -DarchetypeArtifactId=ninja-servlet-jpa-blog-archetype
Не обязательно с него, просто полезно посмотреть как и что работает. я по удалял все сущности не нужные, их там пару штук.
Ну вот собственно и все, можно начинать разрабатывать) Мы получили кучу всяких зависимостей для разработки, например: jetty, freemarker, sl4j, ehcache, guice, flyway, h2 base, hibernate и др.

Для запуска приложения можно воспользоватся классом с main методом (**ninja.standalone.NinjaJetty**) ну или варку собирать.

Из приятного. Есть несколько оберток, которыми удобно пользоваться, заинжектим их в контроллер. Logger не является оберткой, но он тоже есть, настроен и им можно пользоваться.

@Inject
private Logger logger;

@Inject
private NinjaProperties ninjaProperties;

@Inject
private NinjaCache ninjaCache;

@Inject
private Messages messages;

Results (Result)

Удобная штука для веб приложений. Это билдер ответов. Собственно, что в нем есть.
У этого объекта есть несколько классов для создания Result. Они там разные, например:
Results.json() или Results.html().
Разобраться в нем просто, все понятно по названиям методов. В объект Result можно добавлять свои объекты, которые будут использоватmся в шаблонах (для генерации ответа).

Results.html().render("nameObj", object);

Шаблон можно указать самому
Results.html()
           .render("nameObj", object)
           .template("path/template.ftl.html")

или положить и назвать его в соответствии с соглашениями. Например в views/ApplicationController/index.ftl.html. Этот шаблон будет использоваться для метода index из контроллера ApplicationController.

Cache

Докумнетация
Для работы с кешом используется обертка NinjaCache. По-умолчанию используется реализация Ehcache, но так же можно использовать memcached. Для этого в конфиг (*application.conf*) надо прописать пару параметров:

cache.implementation=ninja.cache.CacheMemcachedImpl
memcached.host=127.0.0.1:11211
// user and password are optional
memcached.user=USER          
memcached.password=PASSWORD
java
Единственно memcached (в отличие от Ehcache) требует, чтобы объекты хранимые в кеше имплементили интерфейс Serializable. Посмотрим, как работать с оберткой. Тут все просто. Добавим в контроллер метод для теста.
...

private static String CACHE_KEY = "test_cache";

...

public Result testCache() {
        String test = (String) ninjaCache.get(CACHE_KEY);
        if(test == null){
           test = "new_object";
            ninjaCache.set(CACHE_KEY, test, "1d");
        }else{
            test += "_from_cache";
        }
        return Results.html();

Так же нужно прописать в Routes связь на тот метод:
@Override
    public void init(Router router) {  
     ...
     router.GET().route("/cache").with(ApplicationController.class, "testCache"); 
}

И шаблон для этой страницы (*testCache.ftl.html*).
<#import "../layout/defaultLayout.ftl.html" as layout> 
<@layout.myLayout "Test cache">    

    ${cacheObj}

</@layout.myLayout>

при изменения в шаблонах изменения сами подхватятся, просто перегрузить страницу достаточно

Собственно при обращении к http://localhost:8080/cache сначала создастся новый объект и он сохранится в кэше, при последующих запросах, он будет браться из кэша. Так же в методе добавления объекта в кэш, можно указать время хранения (по-умолчанию 1 месяц). Если объект уже там есть с таким ключом, то он заменится.

Configuration

Документация
Все свойства приложения лежат в файле application.conf. Есть стандартные свойства ninja и можно добавлять свои.
Добавим в application.conf строку

test.prop=Hello, world!

В контроллере возьмем это свойство и добавим в ответ
...
.render("prop", ninjaProperties.get("test.prop"))
java
В шаблоне index.ftl.html, просто выведем его
...
${prop}
...

открываем http://localhost:8080 и видим содеhжимое свойства.
Так же в конфиг файле можно указать для какого окружения это значение свойства. Если в конфиг файл добавить свойство test.prop с префиксом
...
%prod.test.prop=Hello, prod!
...

и запустить приложение с ключом -Dninja.mode=prod, то на странице отобразится именно это значение. Но если кто-то любит держать настройки прода отдельно или нужно предоставить возможность эксплуатации менять их, то можно запускать приложение с ключом -Dninja.external.configuration=conf/production.conf. Все свойства с теми же ключами в приложении будут заменены на значения из этого файла.

i18n

Документация
Еще одна полезная штука, о которой следует позаботится в начале пути.
Добавим тестовое сообщение в messages.properties.

...
test=Тест
...

В шаблоне к нему можно обратится так:
...
${i18n("test")}
...

В приложении:
...
messages.get("test", Optional.of("en")).get()
...

Если файла для такой локали не найдено, то берется та, что по умолчанию.

Read more
08 Nov 2013

Хранение и шаринг сессий между приложениями

Идея статьи появилась вместе с задачей поставленной. В принципе была задача попробовать реализовать это и посмотреть, чем это будет сделать удобнее. В качестве языка был выбран Groovy, а точнее фреймворк Grails. Но конечная реализация будет такая же как и для Java, просто на Grails сделать это быстрее.

Задача звучала так: сделать прототип архитектуры на связке Grails + Jetty, web сессии должны шарится между всеми запущенными нодами. Хранение и шаринг только сессий.

Terracotta

Первым смотрел именно ее. После пары дней серфинга по их сайту остался негативный осадок :) Ну правда, очень все не структурировано и не понятно, создается ощущение, что они пытаются спрятать правду. Подробно описывать, что получилось не буду, так как этот вариант не подошел. Необходимо было, чтобы на разных серверах крутился сервер Terracota, и это сделать в принципе можно, проблема в том, что лишь одна нода является активной (**coordinate active Terracotta server**), все остальные лишь репликами, который в случае загибания выбирают нового вождя и слушаются его. При этом, на этих же серверах стояли бы экземпляры приложения и задумывалось, что каждый экземпляр будет использовать именно тот сервер Terracota, который установлен на той же машине. Но так не получилось, потому что можно подключится лишь к активному серверу. С помощью Terracota, это можно сделать, для этого необходимо в конфиге группы зеркал, можно посмотреть тут раздел Scaling the Terracotta Server Arraу. Реализовать это в старых версиях нельзя, но в продуктах BigMemory это есть. Лицензию они на почту присылают после скачивания.

Hazelcast

После неудачи с терракотой начал пробовать этот продукт, версия 3.1. Собственно оказалось все очень просто. Далее опишу несколько шагов для создания проекта. В принципе все просто и IDE не понадобится, все делалось из консоли.

Создание Grails приложения

Grails используется версии 2.3.1.Тут все просто.

grails create-app grails-hazelcast

После инициализации приложение нужно поправить скрипт для сборки приложения (я пользуюсь vim, вы редактируйте чем угодно):
cd grails-hazelcast
vim grails-app/conf/BuildConfig.groovy

Для начала удаляем плагин для томката и добавляем для jetty. Так же добавляем зависимость для Hazelcast:
dependencies {
        ...
        compile "com.hazelcast:hazelcast-all:3.1"
        ...
    }

plugins {
       ...
        build ":jetty:2.0.3"
       ...
}

Добавим наш пакет в конфиг для логгера:
vim grails-app/conf/Config.groovy

log4j = {
   ...
    info   'grails.hazelcast'
   ...
}

Также необходимо установить шаблоны в него, так как нам понадобится изменить web.xml
grails install-templates
vim src/templates/war/web.xml

что нужно прописывать описано в документации на сайте, раздел Http Session Clustering with HazelcastWM (смотрите нужную версию документации).
таким образом добавляем описанную там структуру в наш web.xml, единственно что изменил, это параметр указывающий конфигурационный файл config-location. И параметр map-name указывает имя коллекции, куда будут сохранятся сессии.

<filter>
    <filter-name>hazelcast-filter</filter-name>
        <filter-class>com.hazelcast.web.WebFilter</filter-class>
     
        <init-param>
            <param-name>map-name</param-name>
            <param-value>my-sessions</param-value>
        </init-param>
     
        <init-param>
            <param-name>sticky-session</param-name>
            <param-value>true</param-value>
        </init-param>

        <init-param>
            <param-name>cookie-name</param-name>
            <param-value>hazelcast.sessionId</param-value>
	</init-param>


    
        <init-param>
          <param-name>cookie-secure</param-name>
          <param-value>false</param-value>
	</init-param>
 
        <init-param>
          <param-name>cookie-http-only</param-name>
          <param-value>false</param-value>
        </init-param>
       
       <init-param>
            <param-name>debug</param-name>
            <param-value>true</param-value>
        </init-param>
 
        <init-param>
          <param-name>config-location</param-name>
          <param-value>/WEB-INF/hazelcast.xml</param-value>
        </init-param>
 
        <init-param>
          <param-name>instance-name</param-name>
          <param-value>default</param-value>
       </init-param>

        <init-param>
        <param-name>use-client</param-name>
        <param-value>false</param-value>
       </init-param>

       <init-param>
         <param-name>shutdown-on-destroy</param-name>
         <param-value>true</param-value>
       </init-param>
</filter>
<filter-mapping>
    <filter-name>hazelcast-filter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

<listener>
    <listener-class>com.hazelcast.web.SessionListener</listener-class>
</listener>

Теперь собственно нужно создать этот конфиг Hazelcast (откуда брал не помню, или из доки или из примеров):

vim web-app/WEB-INF/hazelcast.xml

<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config
    http://www.hazelcast.com/schema/config/hazelcast-config-3.1.xsd"
    xmlns="http://www.hazelcast.com/schema/config"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <group>
        <name>dev</name>
        <password>dev-pass</password>
    </group>
    <network>
        <port auto-increment="true">5701</port>
        <join>
            <multicast enabled="true">
                <multicast-group>224.2.2.3</multicast-group>
                <multicast-port>54327</multicast-port>
            </multicast>
            <tcp-ip enabled="false">
                <interface>127.0.0.1</interface>
            </tcp-ip>
            <aws enabled="false">
                <access-key>my-access-key</access-key>
                <secret-key>my-secret-key</secret-key>
                <region>us-west-1</region>
                <security-group-name>hazelcast-sg</security-group-name>
                <tag-key>type</tag-key>
                <tag-value>hz-nodes</tag-value>
            </aws>
        </join>
        <interfaces enabled="false">
            <interface>10.56.10.*</interface>
        </interfaces>
        <ssl enabled="false" />
        <socket-interceptor enabled="false" />
        <symmetric-encryption enabled="false">
            <algorithm>PBEWithMD5AndDES</algorithm>
            <salt>thesalt</salt>
            <password>thepass</password>
            <iteration-count>19</iteration-count>
        </symmetric-encryption>
        <asymmetric-encryption enabled="false">
            <algorithm>RSA/NONE/PKCS1PADDING</algorithm>
            <keyPassword>thekeypass</keyPassword>
            <keyAlias>local</keyAlias>
            <storeType>JKS</storeType>
            <storePassword>thestorepass</storePassword>
            <storePath>keystore</storePath>
        </asymmetric-encryption>
    </network>
    <partition-group enabled="false"/>
    <management-center enabled="false" update-interval="3" >http://127.0.0.1:8080/mancenter</management-center>
    <executor-service>
        <core-pool-size>16</core-pool-size>
        <max-pool-size>64</max-pool-size>
        <keep-alive-seconds>60</keep-alive-seconds>
    </executor-service>
    <queue name="default">
        <max-size-per-jvm>0</max-size-per-jvm>
        <backing-map-ref>default</backing-map-ref>
    </queue>
    <map name="default">
        <backup-count>1</backup-count>
        <time-to-live-seconds>0</time-to-live-seconds>
        <max-idle-seconds>0</max-idle-seconds>
        <eviction-policy>NONE</eviction-policy>
        <eviction-percentage>25</eviction-percentage>
        <merge-policy>hz.ADD_NEW_ENTRY</merge-policy>
    </map>
    <properties>
        <property name="hazelcast.logging.type">log4j</property>
    </properties>
</hazelcast>

Менялись параметры <interface>10.56.10.*</interface> тут указать свою сеть. И еще возможно следует отметить параметр management-center, там указывается ссылка на консоль для мониторинга. Проблема правда, что в бесплатной версии она работает с максимум двумя экземплярами. Скачать можно тут, в папке bin есть скрипт для запуска.

Теперь создадим контроллер для тестирования:

grails create-controller TestSessions
vim grails-app/controllers/grails/hazelcast/TestSessionsController.groovy
package grails.hazelcast

import com.hazelcast.client.*
import com.hazelcast.config.*
import com.hazelcast.core.*
import groovy.util.logging.*

@Log
class TestSessionsController {

    def index() {
        session.setAttribute("testAttr","testVal");
        Config cfg = new Config();
        HazelcastInstance hz = Hazelcast.newHazelcastInstance(cfg);
        IMap map = hz.getMap("my-sessions");
        log.info "SIZE========"+map.size();
    }
}

Собственно все, при каждом обращении он будет создавать новый экземпляр Hazecast (который будет взаимодействовать с уже созданными, если таковые имеются в указанной в конфиге сети). В сессию кладем аттрибут, чтобы там что либо лежало, так как сохраняются в мапу, только сериализуемые объекты.
Собираем варку и кладем ее в jetty (или в несколько экземпляров jetty, так же возможно на разные машины):

grails war

Кому лень все делать, ссылка на GitHub
Запускаем и радуемся.

Read more