Статьи

Обработка ядовитых сообщений с помощью Glassfish

Ядовитые сообщения — это в основном тупики доставки, вызванные непрерывной повторной доставкой сообщения в очередь или тему JMS. Обычно это происходит из-за ошибки кода или проблем с конфигурацией в проекте.

Как воспроизвести вредоносные сообщения

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

@MessageDriven(activationConfig = {@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")}, mappedName = "MyQueue")
public class RegistrationMessageBean implements MessageListener {
@Override
public void onMessage(Message registration) {
throw new RuntimeException("poison message");
}
}

Устранение тупика

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

Обойти эту проблему можно следующими способами:

  1. Исправьте все ошибки в своем коде: это лучшее решение для вредоносных сообщений, но, как вы знаете, ошибки свойственны любому программному обеспечению, и неудивительно, что вы проиграете борьбу с вредоносными сообщениями 🙂
  2.  Попробуйте поймать и переварить исключения: для параноика хорошим выбором будет окружить весь код onMessage попыткой {…} catch (Exception e) {…}. Даже если вы твердо уверены в правильности своего кода, рекомендуется делать это. Итак, переписывая наш пример кода безопасным способом, это выглядит так:

 

      @Override
public void onMessage(Message registration) {
try {
throw new RuntimeException("poison message");
} catch(Exception error) {
logger.severe("I am ignoring the JMS exception: " + e.getMessage());
}
}

Второе решение — это надежный способ гарантировать получение сообщения, несмотря на любые проблемы. Не очень элегантно, но без исключений в методе onMessage, Сообщение будет подтверждено и транзакция JMS будет зафиксирована.

Более сложный сценарий с

субтранзакциями Предыдущее решение работает для общего случая — простого Java-кода внутри метода onMessage. Проблема в том, что JMS использует Java Transaction API (JTA) для управления транзакциями сообщений, а JTA API поддерживает субтранзакции. Так что представьте, если onMessage вызывает транзакционный метод JPA:

@PersistenceUnit(name = "arenapuj")
protected EntityManagerFactory emf;

@Override
public void onMessage(Message registration) {
try {
create(new MyJpaEntity());
} catch(Exception error) {
logger.severe("I am ignoring the JMS exception: " + e.getMessage());
}
}

public MyJpaEntity create(final MyJpaEntity entity) throws Exception {
EntityManager manager = emf.createEntityManager();
try {
manager.persist(entity);
manager.flush();
return entity;
} finally {
if (manager != null && manager.isOpen()) {
manager.close();
}
}
}

Надежный блок try-catch все еще там, но угадайте, что: если откат транзакции метода JPA метода create откатывается, транзакция JMS также откатывается, вызывая проблему вредоносного сообщения, несмотря на блок try-catch в коде onMessage. Таким образом, удобная надежность, обеспечиваемая блоком try-catch, на самом деле является ловушкой, тихим убийцей в сценарии суб-транзакций JMS.

 

Как избежать ядовитых сообщений, вызванных суб-транзакциями?

Вы должны аннотировать метод первой субтранзакцией с помощью @TransactionAttribute (TransactionAttributeType.REQUIRES_NEW), чтобы избежать зависимостей между транзакцией JMS и ее субтранзакциями . Обратите внимание, что от JMS необходимо отделить только первую суб-транзакцию, чтобы избежать вредоносных сообщений.

@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public MyJpaEntity create(final MyJpaEntity entity) throws Exception {
EntityManager manager = emf.createEntityManager();
try {
manager.persist(entity);
manager.flush();
return entity;
} finally {
if (manager != null && manager.isOpen()) {
manager.close();
}
}
}

Готово, теперь ваш метод JMS отделен от новой транзакции JPA, и если код JPA откатывается, сообщение JMS все равно будет подтверждено.

Отказ от ответственности: я использую термины «транзакция JMS» и «транзакции JPA» для пояснения здесь. На самом деле таких вещей нет, у нас есть только один тип транзакций, определенный в Java Transaction API (JTA). Мне кажется, проще представить проблему, думая о методах и объеме транзакций отдельно, но в конце концов все дело в транзакциях JTA 🙂

Обработка проблем вне кода

Другой распространенный сценарий JMS — проблемы с ресурсами, такие как сбои соединения, сбой базы данных и т. Д. Если у нас отключен приемник сообщений и работает производитель сообщений, он попытается отправить сообщение, выполнить сбой и попытаться отправить сообщение еще раз. Это может привести к другому типу взаимоблокировки, но контейнеры Java EE предоставляют набор параметров конфигурации для предотвращения таких проблем. ActivationSpec для компонентов, управляемых сообщениями, определяет две аннотации для обхода проблем активации:

  1. endpointExceptionRedeliveryAttempts: количество повторных доставок сообщения, когда MDB генерирует исключение во время доставки сообщения.
  2. sendUndeliverableMsgsToDMQ: Поместить сообщение в очередь недействительных сообщений, когда MDB создает исключение времени выполнения, а число попыток повторной доставки превышает значение endpointExceptionRedeliveryAttempts? Если значение равно false, брокер очереди сообщений попытается повторно доставить сообщение любому действительному потребителю, включая тот же MDB.

Вы можете проверить эти конфигурации в своем обзоре кода, но это не так критично, так как значения по умолчанию приемлемы для общих сценариев. И вам нужно знать конкретные атрибуты контейнера. Свойства активации Glassfish V3 немного изменились, и я предполагаю, что для других контейнеров мы найдем другие имена. К счастью, мы можем ожидать, что все настройки по умолчанию работают нормально, и мы не обязаны постоянно копаться в деталях, относящихся к конкретному продукту.

 

Резюме

JMS является одним из самых мощных ресурсов Java EE, доступных для разработчиков и архитекторов, но очень важно, чтобы любой, кто разрабатывает такие приложения, знал в деталях спецификацию, а также некоторые приемы реализации. Ядовитые сообщения могут заставить ваш сервер и, в конечном счете, весь хост-компьютер зависать в течение нескольких часов, что приводит к перезагрузке машины. Это общая проблема, на мой взгляд, слабо поддерживаемая контейнерами, и проблема, о которой мы должны знать. Цель этой записи в блоге — дать вам возможность выявить проблему с ядовитыми сообщениями в вашем приложении. Чтобы узнать больше о JMS и ее деталях, вам нужно прочитать больше, и ссылки, представленные ниже, являются хорошей отправной точкой:

  •     Создание надежных приложений JMS — руководство по Java EE 5
  •     Обмен сообщениями JMS с использованием GlassFish от Deepa Sobhana
  •     Горькие сообщения: анти-паттерны обмена сообщениями Java * от Брюса А. Тейта
  •     Тема: как я могу настроить Glassfish для борьбы с «ядовитым сообщением»

Благодарность: отдельная благодарность Марине Ваткиной и Найджелу Дикину за их дружескую поддержку в списке рассылки Glassfish. И благодарность «Профессору» и его мудрым намекам на эту тему.