Guide de résolution des problèmes spring JMS

Cet article explique comment résoudre les problèmes connus et les erreurs courantes lors de l’utilisation de Spring JMS. L’article répond également à certaines questions fréquemment posées pour spring-cloud-azure-starter-servicebus-jms.

Problèmes de connectivité

MessageProducer a été fermé en raison d’une erreur irrécupérable

Description du problème

Lorsque vous utilisez JmsTemplate pour envoyer des messages, JmsTemplate devient indisponible pendant un intervalle d’inactivité compris entre 10 et 15 minutes. L’envoi de messages dans cet intervalle peut obtenir les exceptions indiquées dans l’exemple de sortie suivant :

2022-11-06 11:12:05.762  INFO 25944 --- [   scheduling-1] c.e.demo.ServiceBusJMSMessageProducer    : Sending message: 2022-11-06T11:12:05.762072 message 1
2022-11-06 11:12:05.772 ERROR 25944 --- [   scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task

org.springframework.jms.IllegalStateException: The MessageProducer was closed due to an unrecoverable error.; nested exception is javax.jms.IllegalStateException: The MessageProducer was closed due to an unrecoverable error.
    at org.springframework.jms.support.JmsUtils.convertJmsAccessException(JmsUtils.java:274) ~[spring-jms-5.3.23.jar:5.3.23]
  ...
Caused by: org.apache.qpid.jms.provider.ProviderException: The link 'G0:36906660:qpid-jms:sender:azure:5caf3ef4-9602-413c-964d-cf1292d6e1f5:1:1:1:t4' is force detached. Code: publisher(link376). Details: AmqpMessagePublisher.IdleTimerExpired: Idle timeout: 00:10:00. [condition = amqp:link:detach-forced]
    at org.apache.qpid.jms.provider.amqp.AmqpSupport.convertToNonFatalException(AmqpSupport.java:181) ~[qpid-jms-client-0.53.0.jar:na]
  ...

Analyse de la cause

Les exceptions se produisent pour Azure Service Bus lorsque la connexion AMQP et le lien sont actifs, mais qu’aucun appel ( par exemple, envoyer ou recevoir des appels) est effectué à l’aide du lien pendant 10 minutes. Dans ce cas, le lien est fermé. Et quand tous les liens de la connexion ont été fermés, car il n’y a pas eu d’activité (inactif) et qu’un nouveau lien n’a pas été créé en 5 minutes, la connexion est fermée.

Pour le démarrage JMS Service Bus, le CachingConnectionFactory est utilisé par défaut, qui met en cache la session, le producteur et le consommateur. Lorsque le JmsProducer est inactif pendant plus de 10 minutes, mais moins de 15, le lien que le producteur mis en cache occupe est fermé. Les messages ne peuvent pas être envoyés pendant cet intervalle. Ensuite, après 5 minutes d’inactivité, toute la connexion est fermée. Ainsi, toute opération d’envoi après l’intervalle d’inactivité de 15 minutes pousse le CachingConnectionFactory à créer une nouvelle connexion pour effectuer l’envoi. L’opération d’envoi devient disponible après 15 minutes.

Contournement

Actuellement, le starter fournit une solution de contournement au problème de détachement des liens en appliquant le JmsPoolConnectionFactory, qui met en pool Connection, Session et MessageProducer, et gère le cycle de vie des instances mises en pool. Cette solution de contournement peut s’assurer qu’un producteur est supprimé après avoir été indisponible et, par conséquent, toutes les opérations d’envoi sont effectuées sur les producteurs actifs.

Pour utiliser cette solution de contournement, ajoutez la configuration suivante :

spring:
  jms:
    servicebus:
      pool:
        enabled: true
        max-connections: ${your-expected-max-connection-value}

Utilisation de spring.jms.servicebus.idle-timeout

Les propriétés de délai d’inactivité configurent le délai d’inactivité d’une connexion AMQP. La spécification AMQP fournit la description suivante :

Les connexions sont soumises à un seuil de délai d’inactivité. Le délai d’expiration est déclenché par un pair local lorsqu’aucune trame n’est reçue après dépassement d’une valeur seuil. Le délai d’inactivité est mesuré en millisecondes et commence à partir du moment où la dernière image est reçue. Si le seuil est dépassé, un pair devrait essayer de fermer proprement la connexion au moyen d’une trame de fermeture accompagnée d’une erreur expliquant la raison. Si l’homologue distant ne répond pas de manière appropriée à ceci dans le délai imparti, alors l’homologue PEUT fermer le socket TCP.

Pour un client JMS, lorsque vous configurez cette propriété, vous contrôlez côté serveur combien de temps vous attendez que le serveur envoie une trame vide pour conserver une connexion active quand aucun message n’est remis. Cette propriété contrôle le comportement de l’homologue distant, et chaque homologue peut avoir sa propre valeur isolée.

Problèmes JmsTemplate

Messages planifiés

Azure Service Bus prend en charge le traitement différé des messages. Pour plus d’informations, consultez la section Messages planifiés de séquencement de messages et horodatages. Pour JMS, pour planifier un message, définissez la propriété ScheduledEnqueueTimeUtc à l’aide de l’en-tête d’annotation de message x-opt-scheduled-enqueue-time.

Problèmes liés à JmsListener

Trop de demandes sont envoyées à Service Bus même s’il n’y a pas de messages sur le serveur

Description du problème

Lorsque vous utilisez l’API @JmsListener, dans certains cas, vous pouvez voir dans le portail Azure que des valeurs persistantes sont affichées pour les requêtes entrantes envoyées à leur file d’attente ou à leurs rubriques, même s’il n’y a aucun message sur le serveur en attente de réception.

Analyse de la cause

@JmsListener est un écouteur de scrutation conçu pour des tentatives répétées de scrutation.

L’écouteur se trouve sur une boucle d’interrogation en cours. Chaque boucle appelle la méthode JMS MessageConsumer.receive() pour interroger le consommateur local pour les messages à consommer. Par défaut, pour chaque opération de sondage, le consommateur local envoie des demandes de tirage au répartiteur de messages pour demander des messages, puis bloque pendant une certaine période de temps. Le processus d’interrogation concret est déterminé par plusieurs propriétés, notamment receiveTimeout, prefetchSize et receiveLocalOnly ou receiveNoWaitLocalOnly. La méthode receiveNoWaitLocalOnly est utilisée uniquement lorsque vous définissez receiveTimeout sur une valeur négative.

Lorsque ce problème se produit à votre application, vérifiez les paramètres de configuration suivants :

  • Vérifiez si votre politique de prélecture est définie sur 0, qui correspond également à la valeur par défaut. La prérécupération 0 signifie qu’un consommateur de tirage envoie des demandes de tirage à Service Bus pour chaque sondage.

  • Si vous avez configuré une prérécupération non nulle, déterminez si la propriété receiveLocalOnly ou receiveNoWaitLocalOnly est définie à false, qui est l’option par défaut. Une valeur false ici entraîne toujours l’envoi de demandes de tirage au serveur, car elle n’interroge pas uniquement le consommateur local.

  • La configuration receiveTimeout détermine combien de temps elle bloque pour chaque requête d’extraction, ce qui peut affecter la fréquence d’envoi des requêtes d’extraction au serveur. La valeur par défaut est 1 seconde.

Pour une analyse complète, consultez la discussion dans le problème GitHub.

Solutions

Les sections suivantes décrivent deux solutions pour traiter ce problème

Solution 1. Passer à « push consumer » et à « local-check » uniquement

En passant le mode à push, le consommateur devient un consommateur à notification asynchrone qui ne récupère pas les messages auprès du broker, mais maintient un niveau cible de crédit de lien. La quantité est déterminée par une propriété de préchargement. À mesure que service Bus (expéditeur) envoie des messages, le crédit de lien de l’expéditeur diminue et lorsque le crédit de lien de l’expéditeur tombe en dessous d’un seuil, le client (destinataire) envoie une demande au serveur pour augmenter le crédit de lien de l’expéditeur au montant cible souhaité.

Pour accomplir cette solution, ajoutez la configuration suivante :

Tout d’abord, définissez le nombre de prefetch sur une valeur non nulle, ce qui configure le consommateur en mode non-pull. Le tableau suivant présente plusieurs propriétés de prélecture, chacune d’elles contrôlant différentes entités de Service Bus. Définissez les propriétés qui s’appliquent à votre cas.

Propriété Description
spring.jms.servicebus.prefetch.all Valeur de repli de l’option de prélecture dans cet espace de noms Service Bus
spring.jms.servicebus.prefetch.queue-prefetch Numéro de prérécupération de la file d’attente.
spring.jms.servicebus.prefetch.queue-browser-prefetch Le nombre de messages prérécupérés pour le navigateur de files d’attente.
spring.jms.servicebus.prefetch.topic-prefetch Le nombre d'éléments prérécupérés pour le sujet.
spring.jms.servicebus.prefetch.durable-topic-prefetch Le nombre de messages prérécupérés pour la rubrique durable.

Ensuite, configurez l'non-local-check en ajoutant une classe de configuration pour le personnalisateur d’usine, comme illustré dans l’exemple suivant :

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    ServiceBusJmsConnectionFactoryCustomizer customizer() {
        return factory -> {
            factory.setReceiveLocalOnly(true);
            // Configure the below ReceiveNoWaitLocalOnly instead if you have specified the property 
            // spring.jms.listener.receive-timeout with negative value. Otherwise, configure the above `ReceiveLocalOnly`.
            //factory.setReceiveNoWaitLocalOnly(true);
        };
    }
}

La valeur de prérécupération peut affecter la vitesse de distribution des messages vers la mémoire tampon locale du consommateur. Vous devez ajuster la valeur en fonction de vos performances de consommation et du volume de messages. Une valeur appropriée peut accélérer le processus de consommation, tandis qu’une valeur trop importante peut entraîner la mise en mémoire tampon locale des messages obsolètes et distribués à nouveau. Pour les volumes de messages faibles, où chaque message prend beaucoup de temps à traiter, définissez la prérécupération sur 1. Cette valeur garantit qu’un consommateur ne traite qu’un seul message à la fois.

Solution 2. Augmenter le délai d’expiration de réception pour diminuer la fréquence d’extraction

La propriété de délai d’attente de réception détermine la stratégie pour la durée pendant laquelle le consommateur bloque l’attente d’un résultat de tirage. Par conséquent, en étendant le délai d’expiration, vous pouvez réduire la fréquence d’extraction, puis réduire le nombre de demandes de tirage lorsque vous choisissez le mode pull. Dans les cas extrêmes, vous pouvez définir une stratégie consistant à attendre indéfiniment jusqu’à l’arrivée d’un message, ce qui signifie que le consommateur ne récupère le message suivant qu’après en avoir consommé un. Dans ce cas, lorsqu’aucun message n’est présent sur le serveur, il bloque l’attente.

Pour accomplir cette solution, configurez la propriété spring.jms.listener.receive-timeout. Cette propriété est de type java.time.Duration et a une valeur par défaut de 1 seconde. La liste suivante explique l’effet de différentes valeurs :

  • La définition du délai d’expiration de réception sur 0 signifie que les blocs de tirage sont indéfinis jusqu’à ce qu’un message soit distribué.
  • Définir le délai d’expiration de réception à une valeur positive signifie que l’opération de récupération se bloque pendant une durée pouvant aller jusqu’à celle du délai d’expiration.
  • Régler le délai d’expiration de réception sur une valeur négative signifie que l’opération de pull est une réception sans attente, c’est-à-dire qu’elle renvoie immédiatement un message, ou null si aucun message n’est disponible.

Note

Une valeur de délai d’expiration élevée peut apporter des effets secondaires. Par exemple, une valeur de délai d’attente élevée étend également la durée pendant laquelle le thread principal est dans un état de bloc. Cet état signifie que le conteneur sera moins réactif aux appels stop() et ne peut s’arrêter qu’entre les appels receive().

En outre, le conteneur ne peut envoyer des requêtes qu’une fois l’intervalle de receive-timeout passé. Si l’intervalle est supérieur à 10 minutes, Service Bus ferme le lien et empêche l’écouteur d’envoyer ou de recevoir. Pour plus d’informations, consultez la section Lien fermé de Erreurs AMQP dans Azure Service Bus. Par défaut, l’écouteur utilise une CachingConnectionFactory.

Si vous avez besoin d’un délai d’expiration de réception élevé, veillez à utiliser JmsPoolConnectionFactory.

Pour plus d’informations sur le problème de fermeture de lien et sur l’utilisation de JmsPoolConnectionFactory, consultez le MessageProducer a été fermé en raison d’une erreur irrécupérable section.

Problème de préchargement

Description du problème

Une stratégie de prérécupération inappropriée peut entraîner les problèmes suivants :

  • Les mêmes messages sont consommés à plusieurs reprises.
  • Les messages sont placés dans la file d’attente de lettres mortes après MaxDeliveryCountExceeded, même lorsque les messages sont traités sans erreur ou exception.

Analyse de la cause

Ce problème se produit généralement lorsque la prefetch valeur est supérieure à la capacité de consommation réelle, avec l’effet que trop de messages sont prérécupérés dans la mémoire tampon locale en attente d’être consommés. Toutefois, du côté de Service Bus, les messages préchargés sont considérés comme envoyés en mode peek-lock. Chaque message distribué possède les attributs max-delivery-count et lock-duration. En mode de réception peek-lock, les messages récupérés dans la mémoire tampon de prérécupération sont acquis dans la mémoire tampon à l’état verrouillé et déclenchent l’horloge de délai d’expiration du verrouillage. Si la mémoire tampon de prérécupération est volumineuse et que le traitement prend tellement de temps que les verrous de message expirent tout en restant dans la mémoire tampon de prérécupération, le message est traité comme abandonné et est à nouveau mis à disposition pour la récupération à partir de la file d’attente.

Ce problème peut entraîner la récupération du message dans la mémoire tampon de prérécupération, puis son placement à la fin. Si la mémoire tampon de prérécupération n’est pas traitée avant l’expiration du message, les messages sont prérécupérés à plusieurs reprises, mais jamais remis efficacement dans un état utilisable (verrouillé validement). Ensuite, lorsque ces copies obsolètes sont mises en file d’attente, l’application consomme ensuite le même message à plusieurs reprises et ne peut pas les terminer. Dans un autre cas, les messages répétés ont expiré dans la mémoire tampon avant d’être consommés. Dans ce cas, le message dans le Service Bus finira par être déplacé vers la file de messages morts après dépassement du nombre maximal de tentatives de remise.

Pour plus d’informations, consultez la Pourquoi la prérécupération n’est-elle pas l’option par défaut ? section de prérécupération des messages Azure Service Bus.

Solution

Veillez à bien configurer le prefetch afin qu’il soit adapté à la capacité de consommation. Vous devez équilibrer le nombre maximal de prérécupérations et la durée de verrouillage configurée sur la file d’attente ou l’abonnement afin que le délai d’expiration du verrouillage dépasse au moins le temps de traitement des messages attendu cumulé pour la taille maximale de la mémoire tampon de prérécupération, plus un message. Dans le même temps, la durée d’expiration du verrou ne doit pas être si longue que les messages dépassent leur durée de vie maximale lorsqu’ils sont accidentellement abandonnés, ce qui impose d’attendre l’expiration de leur verrou avant qu’ils ne soient remis en distribution.

Pour configurer l’attribut de prérécupération, qui a la valeur par défaut zéro, utilisez l’une des propriétés suivantes :

Propriété Description
spring.jms.servicebus.prefetch.all Valeur de repli de l’option de prérécupération de cet espace de noms Service Bus.
spring.jms.servicebus.prefetch.queue-prefetch Numéro de prérécupération de la file d’attente.
spring.jms.servicebus.prefetch.queue-browser-prefetch Le nombre de messages prérécupérés pour le navigateur de files d’attente.
spring.jms.servicebus.prefetch.topic-prefetch Le nombre d'éléments prérécupérés pour le sujet.
spring.jms.servicebus.prefetch.durable-topic-prefetch Le nombre de messages prérécupérés pour la rubrique durable.

Comment définir une disposition AMQP pour Service Bus ?

JMS prend en charge cinq types de disposition AMQP lors de l’acquittement des messages auprès du broker de messagerie. Les valeurs prises en charge sont ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDet MODIFIED_FAILED_UNDELIVERABLE. Pour plus d’informations, consultez la section Modes de disposition AMQP et mappage des opérations de Service Bus de Utilisation de Java Message Service 1.1 avec Azure Service Bus Standard et AMQP 1.0.

Par conséquent, pour terminer manuellement, abandonner, reporter ou libérer un message à l’aide de JmsListener, procédez comme suit :

  1. Désactivez les transactions de session et utilisez le mode client ack.

    Pour effectuer cette tâche, déclarez soit votre propre bean JmsListenerContainerFactory puis définissez les propriétés, soit post-traitez le défini dans le starter. L’exemple suivant utilise l’approche de déclaration d’un autre haricot :

    @Configuration(proxyBeanMethods = false)
    public class CustomJmsConfiguration {
    
        @Bean
        public JmsListenerContainerFactory<?> customQueueJmsListenerContainerFactory(
                DefaultJmsListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
            DefaultJmsListenerContainerFactory jmsListenerContainerFactory = new DefaultJmsListenerContainerFactory();
            configurer.configure(jmsListenerContainerFactory, connectionFactory);
            jmsListenerContainerFactory.setPubSubDomain(Boolean.FALSE);
            jmsListenerContainerFactory.setSessionTransacted(Boolean.FALSE);
            jmsListenerContainerFactory.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
            return jmsListenerContainerFactory;
        }
    }
    
  2. Dans votre gestionnaire de messages, complétez ou abandonnez explicitement les messages.

    @JmsListener(destination = "QUEUE_NAME", containerFactory = "customQueueJmsListenerContainerFactory")
    public void receiveMessage(JmsTextMessage message) throws Exception {
        String event = message.getBody(String.class);
        try {
            logger.info("Received event: {}", event);
            logger.info("Received message: {}", message);
            // by default complete the message
            message.acknowledge();
        } catch (Exception e) {
            logger.error("Exception while processing re-source event: " + event, e);
            JmsAcknowledgeCallback acknowledgeCallback = message.getAcknowledgeCallback();
            // explicitly abandon the message
            acknowledgeCallback.setAckType(MODIFIED_FAILED);
            message.setAcknowledgeCallback(acknowledgeCallback);
            message.acknowledge();
            throw e;
        }
    }
    

Problèmes de configuration

Désactiver la configuration automatique de Service Bus JMS

Description du problème

Certains utilisateurs importent certains Spring Cloud Azure Starter pour la configuration automatique d’un service Azure autre que Service Bus JMS. Ils utilisent également l’infrastructure Spring JMS sans avoir besoin de JMS Service Bus. Ensuite, lorsque l’application tente de démarrer, les exceptions suivantes sont levées :

Caused by: java.lang.IllegalArgumentException: 'spring.jms.servicebus.connection-string' should be provided
    at com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties.afterPropertiesSet(AzureServiceBusJmsProperties.java:210)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
    ... 98 more

Analyse de la cause

Ce problème se produit parce que toutes les classes de configuration automatique Spring Cloud Azure sont placées dans le même module, de sorte que n’importe quel Spring Cloud Azure Starter importe réellement toutes ces configurations automatiques, qui incluent également Service Bus JMS. Ensuite, lorsque l’application utilise l’API Spring JMS, elle répond à la condition de configuration automatique JMS Service Bus et la déclenche. Ensuite, pour les utilisateurs qui n’ont pas l’intention d’utiliser spring-cloud-azure-starter-servicebus-jms, les conditions de propriété ne sont pas remplies, car il n’y a aucune raison pour eux de configurer Service Bus pour JMS. Cette situation entraîne la levée des exceptions.

Solution

Spring Cloud Azure pour Service Bus JMS fournit une propriété pour activer ou désactiver sa configuration automatique. Vous pouvez choisir de désactiver cette fonctionnalité si nécessaire à l’aide du paramètre de propriété suivant :

spring.jms.servicebus.enabled=false

Configurer les attributs de message

Comment définir le type de contenu des messages sortants ?

Pour configurer le type de contenu, personnalisez le convertisseur de messages pour modifier l’attribut de type de contenu lors de la conversion de messages. Le code suivant prend des messages d’octets comme exemple.

Tout d’abord, personnalisez le convertisseur de messages à utiliser dans le JmsTemplate, comme illustré dans l’exemple suivant :

public class CustomMappingJackson2MessageConverter extends MappingJackson2MessageConverter {

    public static final String CONTENT_TYPE = "application/json";

    public CustomMappingJackson2MessageConverter() {
        this.setTargetType(MessageType.BYTES);
    }

    @Override
    protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectWriter objectWriter)
            throws JMSException, IOException {
        final BytesMessage message = super.mapToBytesMessage(object, session, objectWriter);
        JmsBytesMessage msg = (JmsBytesMessage) message;
        AmqpJmsMessageFacade facade = (AmqpJmsMessageFacade) msg.getFacade();
        facade.setContentType(Symbol.valueOf(CONTENT_TYPE));
        return msg;
    }
}

Ensuite, déclarez votre bean de convertisseur de message personnalisé, comme illustré dans cet exemple :

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    public MessageConverter messageConverter() {
        return new CustomMappingJackson2MessageConverter();
    }
}

Comment définir le nom de propriété id de type pour MappingJackson2MessageConverter ?

L’attribut type-id-property-name permet au MappingJackson2MessageConverter de déterminer la classe à utiliser pour désérialiser la charge utile du message. Lors de la sérialisation de chaque objet Java vers une charge utile Spring Message, le convertisseur stocke le type de charge utile dans une propriété de message avec le nom de propriété enregistré par type-id-property-name. Ensuite, lors de la désérialisation du message, le convertisseur lit l’ID de type du message et effectue la désérialisation.

Pour définir le type-id-property-name, déclarez votre propre bean MappingJackson2MessageConverter et configurez cette propriété, comme illustré dans l’exemple suivant :

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    public MessageConverter jacksonJmsMessageConverter()
    {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTypeIdPropertyName("your-custom-type-id-property-name");
        return converter;
    }
}

Détection dupliquée

Azure Service Bus prend en charge détection en double, qui applique la propriété MessageId pour identifier de manière unique les messages et ignorer les doublons envoyés à Service Bus.

Toutefois, pour l’API JMS, vous ne devez pas définir l’ID de message JMS, qui est considéré comme illégal dans les spécifications JMS. Par conséquent, cette fonctionnalité n’est actuellement pas prise en charge pour Spring Cloud Azure Service Bus JMS Starter.

Pour obtenir d’autres mises à jour de cette fonctionnalité, consultez le problème GitHub.

Activer la journalisation des transports AMQP

Pour plus d’informations, consultez la section Activation de la journalisation du transport AMQP de Résolution des problèmes de Service Bus.

Obtenir de l’aide supplémentaire

Pour plus d’informations sur les moyens de contacter le support, consultez support à la racine du dépôt.

Ressources pour le starter Spring Cloud Azure Service Bus JMS

Signaler des problèmes GitHub

Lorsque vous déposez des problèmes GitHub, les détails suivants sont demandés :

  • Configuration de Service Bus / Environnement d’espace de noms
    • Quel niveau est l’espace de noms (standard ou Premium) ?
    • Quel type d’entité de messagerie est utilisé (file d’attente ou rubrique) ? et sa configuration.
    • Quelle est la taille moyenne de chaque message ?
  • Quel est le schéma de trafic ? (autrement dit, le nombre de messages par minute et si le client est toujours occupé ou a des périodes de trafic lentes.)
  • Code et étapes de reproduction
    • Cela est important, car nous ne pouvons souvent pas reproduire le problème dans notre environnement.
  • Journaux