Oracle AQ - костыли и альтернативы
От: Neco  
Дата: 23.10.15 12:51
Оценка:
Появилась задача реализовать наколенную ESB по-быстрому, а частью ESB является реализация очередей (queues). В качестве хранилища имеется либо файловая система, либо Oracle. Файловую систему я сразу отбросил, начал с реализации на оракле в виде таблиц.
Создал таблицу, загоняю в неё входящие сообщения (в виде поля NCLOB), отдельные потоки читают свои очереди и рассылают сообщения получателям.
Разумеется возник вопрос — как читать нескольким рассылателям из одной очереди, при этом чтобы не было возможности прочитать одно и то же сообщение дважды?
Начал делать через дополнительную таблицу, т.е. если сама таблица с очередями у меня такая:
create table msg_queue (
    id number not null,
    queue_name nvarchar2(100) not null,
    priority number not null,
    date_created date not null,
    msg_content nclob
);

к ней создаю вторую таблицу для блокирования:
create table msg_queue_locked (
    id number not null
);

и на ней PK.
Соот-но предполагал реализовать такой алгоритм:
1. выбираем следующее сообщение для рассылки.
2. вставляем его ID в msg_queue_locked. коммитим транзакцию.
3. раз коммит удался, значит никто другой уже это сообщение не возьмёт, значит высылаем сообщение получателю и удаляем записи из обеих таблиц.

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

В ходе гугления обнаружил выражение select for update skip locked, а через него вышел на то, что у оракла оказывается уже есть очереди (Advanced Queueing) и можно попросту ими воспользоваться. Начал копать.
Первое, что смутило, что при дёргании Dequeue на пустой очереди вываливается Exception, который приходится отлавливать и глушить:
    } catch (OracleException ex) {
        if (ex.Number == 25228) {
            no_messages_in_the_queue = true;
        } else {
            throw;
        }
    }

другого способа не нашёл — знает ли кто-нибудь правильный способ?

Далее начал реализовывать такой алгоритм для рассылки:
1. начинаем транзакцию.
2. берём сообщение из очереди в режиме удаления (т.е. взять и удалить).
3. высылаем сообщение в систему-назначения, анализируем ответ.
4. если всё ок, то коммитим транзакцию.

в ходе экспериментов обнаружил неприятную особенность. если транзакцию не закоммитить, то сообщение через пять таких попыток перестанет существовать в очереди. Оно мигрирует в очередь исключений со статусом EXPIRED. Причём у параметра max_retries нет значения INFINITY, можно поставить только условно бесконечное число 2**31 -1. И тут начинается головоломка — стоит ли теперь в приложении предусматривать сценарии на этот счёт или нет.

Вопрос №2 — есть ли какой-нибудь адекватный workaround для проблемы с max_retries?
Вопрос №3 — кто-нибудь реализовывал queue в своих приложениях средствами БД? Каким образом это делали?

P.S. Вероятно есть ещё специализированные хранилища, но они не особо интересны, поскольку надо заморачиваться с лицензиями — не то.
всю ночь не ем, весь день не сплю — устаю
Re: Oracle AQ - костыли и альтернативы
От: Vladimir Alekseev  
Дата: 23.10.15 13:58
Оценка:
Здравствуйте, Neco, Вы писали:

N>Появилась задача реализовать наколенную ESB по-быстрому, а частью ESB является реализация очередей (queues). В качестве хранилища имеется либо файловая система, либо Oracle. Файловую систему я сразу отбросил, начал с реализации на оракле в виде таблиц.


Есть ли реальная необходимость "забирать" задачи из очереди в БД напрямую из worker'ов? Почему бы не сделать отдельный поток/процесс, который выбирает в себя весь список задач (список id), которые нужно выполнить, а всем worker'ам выдавет id следующей задачи по запросу "Дай мне следующую задачу"? В случае многопроцессовой реализации это, возможно, выльется в написание некого сервиса, а в случае многопоточной реализации — в простой "Queue<Task>". Для отметки того, что задача выполнена и удаления ее из таблицы-очереди — делается обратная процедура — запрос "Пометить задачу как выполненную" к сервису или обратный Queue<TaskId> для многопроцессовой/многопоточной реализации соответственно.

Побочный эффект — некая задача может быть передана worker'у дважды, если программа упала после того, как она была выполнена в первый раз, но до того, как была удалена из очереди. Но с этим, ИМХО, бороться проще, чем с зависанием задачи если worker ее заблокировал и упал.

Имел опыт реализации подобных вещей и описанный мной вариант оказался проще, чем Oracle AQ.
Re[2]: Oracle AQ - костыли и альтернативы
От: Neco  
Дата: 23.10.15 15:00
Оценка:
Здравствуйте, Vladimir Alekseev, Вы писали:

VA>Есть ли реальная необходимость "забирать" задачи из очереди в БД напрямую из worker'ов? Почему бы не сделать отдельный поток/процесс, который выбирает в себя весь список задач (список id), которые нужно выполнить, а всем worker'ам выдавет id следующей задачи по запросу "Дай мне следующую задачу"? В случае многопроцессовой реализации это, возможно, выльется в написание некого сервиса, а в случае многопоточной реализации — в простой "Queue<Task>".

Дело в том, что уже есть инфраструктура NLB-ESB-DB. Каждый узел выполнен в виде нескольких машин — и сервисы ESB например, работают параллельно (т.е. worker'ы работают на разных машинах и друг о друге ничего не знают). Точка соприкосновения здесь БД — она тоже крутится на двух машинах, но представляет собой одну базу данных и является удобной точкой хранения состояния. Делать новый сервис/кластер не хочется, да и не вижу что принципиально изменится: что меняется от того, что некий процесс вберёт в себя весь список задач? В любом случае каждый чих надо фиксировать в БД транзакцией, иначе при отключении питания состояние будет потеряно. Другое дело, что дополнительный сервис как единая точка может ставить lock на любые действия и работать с БД в одно лицо. Но тогда этот сервис является узким местом в плане производительности и надёжности.
Так в принципе можно вообще в лоб — одним потоком читать из БД, рассылать и удалять.

VA>Побочный эффект — некая задача может быть передана worker'у дважды, если программа упала после того, как она была выполнена в первый раз, но до того, как была удалена из очереди. Но с этим, ИМХО, бороться проще, чем с зависанием задачи если worker ее заблокировал и упал.

От передачи одного сообщения дважды ни один из моих сценариев также не застрахован (по-моему это задача о двух генералах).
всю ночь не ем, весь день не сплю — устаю
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.