Guida alla risoluzione dei problemi di Spring JMS

Questo articolo descrive come risolvere i problemi noti e gli errori comuni quando si usa Spring JMS. L'articolo risponde anche ad alcune domande frequenti per spring-cloud-azure-starter-servicebus-jms.

Problemi di connettività

MessageProducer è stato chiuso a causa di un errore irreversibile

Descrizione del problema

Quando si usa JmsTemplate per inviare messaggi, JmsTemplate diventa non disponibile durante un intervallo di inattività compreso tra 10 e 15 minuti. L'invio di messaggi in tale intervallo può ottenere le eccezioni illustrate nell'output di esempio seguente:

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]
  ...

Analisi delle cause

Le eccezioni si verificano per bus di servizio di Azure quando la connessione e il collegamento AMQP sono attivi, ma non vengono effettuate chiamate, ad esempio l'invio o la ricezione, usando il collegamento per 10 minuti. In questo caso, il collegamento viene chiuso. Quando tutti i collegamenti nella connessione sono stati chiusi perché non è stata eseguita alcuna attività (inattiva) e non è stato creato un nuovo collegamento in 5 minuti, la connessione viene chiusa.

Per lo starter JMS di bus di servizio, per impostazione predefinita viene utilizzato CachingConnectionFactory, il quale memorizza nella cache la sessione, il produttore e il consumatore. Quando JmsProducer rimane inattivo per più di 10 minuti ma meno di 15 minuti, la connessione occupata dal producer nella cache è stata chiusa. I messaggi non possono essere inviati durante questo intervallo. Quindi, dopo altri 5 minuti di inattività, l'intera connessione viene chiusa. Pertanto, qualsiasi operazione di invio dopo l'intervallo di inattività di 15 minuti fa sì che il CachingConnectionFactory crei una nuova connessione da inviare. L'operazione di invio diventa disponibile dopo 15 minuti.

Soluzione alternativa

Attualmente, lo starter offre una soluzione alternativa per il problema di scollegamento del link utilizzando il JmsPoolConnectionFactory, che mette in pool Connection, Session e MessageProducer e gestisce il ciclo di vita delle istanze raggruppate nel pool. Questa soluzione alternativa può garantire che un producer venga rimosso dopo essere stato non disponibile e quindi tutte le operazioni di invio vengano eseguite sui producer attivi.

Per usare questa soluzione alternativa, aggiungere la configurazione seguente:

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

Utilizzo di spring.jms.servicebus.idle-timeout

Le proprietà relative al timeout di inattività configurano il timeout di inattività di una connessione AMQP. La specifica AMQP fornisce la descrizione seguente:

Le connessioni sono soggette a un limite di timeout per inattività. Il timeout viene attivato da un peer locale quando non viene ricevuto alcun frame dopo il superamento di un valore soglia. Il timeout di inattività viene misurato in millisecondi e inizia dal momento in cui viene ricevuto l'ultimo fotogramma. Se la soglia viene superata, un nodo peer DOVREBBE tentare di chiudere in modo controllato la connessione utilizzando un frame di chiusura con un messaggio di errore che ne spieghi il motivo. Se il peer remoto non risponde in modo appropriato a questo entro una determinata soglia di tempo, il peer può chiudere il socket TCP.

Per un client JMS, quando si configura questa proprietà, si controlla sul lato server per quanto tempo si prevede che il server invii un frame vuoto per mantenere attiva una connessione quando non vengono recapitati messaggi. Questa proprietà controlla il comportamento del peer remoto e ogni peer può avere un proprio valore isolato.

Problemi relativi a JmsTemplate

Messaggi pianificati

Il bus di servizio di Azure supporta l'elaborazione ritardata dei messaggi. Per ulteriori informazioni, vedere la sezione Messaggi pianificati di Sequenziamento dei messaggi e timestamp. Per JMS, per programmare un messaggio, impostare la proprietà ScheduledEnqueueTimeUtc usando l'intestazione di annotazione del messaggio x-opt-scheduled-enqueue-time.

Problemi di JmsListener

Troppe richieste vengono inviate al bus di servizio anche se non sono presenti messaggi nel server

Descrizione del problema

Quando si usa l'API @JmsListener, in alcuni casi è possibile vedere nel portale di Azure che esistono valori in corso per le richieste in ingresso inviate alla coda o agli argomenti, anche se non sono presenti messaggi nel server da ricevere.

Analisi delle cause

@JmsListener è un listener per il polling, progettato per eseguire tentativi di polling ripetuti.

Il listener si trova in un ciclo di polling in corso. Ogni ciclo chiama il metodo di MessageConsumer.receive() JMS per eseguire il polling del consumer locale affinché i messaggi vengano utilizzati. Per impostazione predefinita, per ogni operazione di polling, il consumer locale invia richieste pull al broker di messaggi per richiedere messaggi e quindi blocca per un determinato periodo di tempo. Il processo concreto di polling è determinato da diverse proprietà, tra cui receiveTimeout, prefetchSize e receiveLocalOnly o receiveNoWaitLocalOnly. Il metodo receiveNoWaitLocalOnly viene utilizzato solo quando si imposta receiveTimeout su un valore negativo.

Quando questo problema si verifica nell'applicazione, controllare le impostazioni di configurazione seguenti:

  • Determinare se il criterio di prelettura è 0, che è anche l'opzione predefinita. Prefetch pari a 0 indica un consumer pull che invia richieste pull al bus di servizio a ogni polling.

  • Se hai configurato un prefetch diverso da zero, verifica se la proprietà receiveLocalOnly o receiveNoWaitLocalOnly è impostata su false, che è l'opzione predefinita. Un valore false in questo caso comporta comunque l'invio di richieste pull al server perché non esegue solo il polling del consumer locale.

  • La configurazione receiveTimeout determina per quanto tempo si blocca per ogni richiesta pull, in modo che possa influire sulla frequenza delle richieste pull inviate al server. Il valore predefinito è 1 secondo.

Per un'analisi completa, vedere la discussione nel problema di GitHub.

Soluzioni

Le sezioni seguenti descrivono due soluzioni per la gestione di questo problema

Soluzione 1. Passare al push del consumer e del solo controllo locale

Modificando la modalità in push, il consumer diventa un consumer di notifiche asincrone che non preleva i messaggi dal broker, ma mantiene un determinato livello di credito del collegamento. L'importo viene determinato da una proprietà di prelettura. Quando il bus di servizio (mittente) esegue il push dei messaggi, il credito di collegamento del mittente diminuisce e quando il credito di collegamento del mittente scende al di sotto di una soglia, il client (destinatario) invia una richiesta al server per aumentare il credito di collegamento del mittente all'importo di destinazione desiderato.

Per eseguire questa soluzione, aggiungere la configurazione seguente:

Per prima cosa, impostare il numero prefetch su un valore diverso da zero, in modo che il consumer non sia di tipo pull. La tabella seguente mostra diverse proprietà di prefetch, ognuna delle quali controlla diverse entità di bus di servizio. Impostare le proprietà applicabili al caso.

Proprietà Descrizione
spring.jms.servicebus.prefetch.all Il valore di fallback per l'opzione di prelettura in questo namespace di bus di servizio
spring.jms.servicebus.prefetch.queue-prefetch Numero di prelettura per la coda.
spring.jms.servicebus.prefetch.queue-browser-prefetch Il numero di prelettura per il browser della coda.
spring.jms.servicebus.prefetch.topic-prefetch Il numero di prefetch per il topic.
spring.jms.servicebus.prefetch.durable-topic-prefetch Numero di prelettura per l'argomento durevole.

In secondo luogo, configurare il non-local-check aggiungendo una classe di configurazione per il personalizzatore di factory, come illustrato nell'esempio seguente:

@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);
        };
    }
}

Il valore di prefetch può influire sulla rapidità con cui i messaggi vengono inviati al buffer locale del consumer. Si consiglia di regolare il valore in base alle prestazioni di consumo e ai volumi dei messaggi. Un valore appropriato può velocizzare il processo di consumo, mentre un valore troppo elevato può far sì che i messaggi memorizzati nel buffer locale diventino obsoleti e vengano inviati nuovamente. Per volumi ridotti di messaggi, in cui ogni messaggio richiede molto tempo per essere elaborato, impostare il prefetch su 1. Questo valore garantisce che un consumer stia elaborando un solo messaggio alla volta.

Soluzione 2. Aumentare il timeout di ricezione per ridurre la frequenza delle richieste pull

La proprietà di timeout di ricezione determina la strategia per definire quanto a lungo il consumatore rimane bloccato in attesa di un risultato dell'operazione di pull. Estendendo quindi il timeout, è possibile ridurre la frequenza di pull, quindi ridurre il numero di richieste pull quando si sceglie la modalità pull. In casi estremi, è possibile impostare una strategia di attesa indefinita finché non arriva un messaggio, il che significa che il consumer effettua il pull solo dopo aver consumato un messaggio. In questo caso, quando non sono presenti messaggi nel server, verrà bloccato per l'attesa.

Per eseguire questa soluzione, configurare la proprietà spring.jms.listener.receive-timeout. Questa proprietà è di tipo java.time.Duration e ha un valore predefinito di 1 secondo. L'elenco seguente illustra l'effetto di vari valori:

  • L'impostazione del timeout di ricezione su 0 indica che l'operazione di pull rimane bloccata indefinitamente finché non viene recapitato un messaggio.
  • L'impostazione del timeout di ricezione su un valore positivo significa che l'operazione di pull resta bloccata fino al timeout impostato.
  • L'impostazione del timeout di ricezione su un valore negativo indica che il pull è una ricezione senza attesa, ovvero restituisce immediatamente un messaggio o null se non sono disponibili messaggi.

Nota

Un valore di timeout elevato può portare alcuni effetti collaterali. Ad esempio, un valore di timeout elevato estenderà anche il tempo in cui il thread principale si trova in uno stato di blocco. Questo stato indica che il contenitore sarà meno reattivo alle chiamate stop() e può interrompersi solo tra receive() chiamate.

Inoltre, il contenitore può inviare richieste solo dopo il superamento dell'intervallo di receive-timeout. Se l'intervallo è superiore a 10 minuti, il bus di servizio chiuderà il collegamento e impedirà al listener di inviare o ricevere. Per altre informazioni, vedere la sezione Link is closed di errori AMQP nel bus di servizio di Azure. Per impostazione predefinita, il listener usa un CachingConnectionFactory.

Se è necessario un timeout di ricezione lungo, assicurarsi di utilizzare JmsPoolConnectionFactory.

Per altre informazioni sul problema di chiusura dei collegamenti e su come usare JmsPoolConnectionFactory, vedere la sezione MessageProducer è stata chiusa a causa di un errore irreversibile.

Problema di prefetch

Descrizione del problema

Un criterio di precaricamento non appropriato può causare i seguenti problemi:

  • Gli stessi messaggi vengono utilizzati ripetutamente.
  • I messaggi vengono inseriti nella coda dei messaggi non recapitati dopo MaxDeliveryCountExceeded, anche quando vengono elaborati senza errori né eccezioni.

Analisi delle cause

Questo problema si verifica in genere quando il prefetch valore è superiore alla capacità di consumo effettiva, con l'effetto che troppi messaggi vengono preletati nel buffer locale in attesa di essere utilizzati. Tuttavia, i messaggi recuperati in anticipo sono considerati inviati in modalità peek-lock dal lato di bus di servizio. Ogni messaggio inviato ha attributi max-delivery-count e lock-duration . Nella modalità di ricezione peek-lock, i messaggi recuperati nel buffer di prelettura vengono acquisiti nel buffer in stato bloccato, mentre il timeout del blocco continua a decorrere. Se il buffer di prelettura è di grandi dimensioni e l'elaborazione richiede così tanto tempo che i blocchi dei messaggi scadono mentre rimangono nel buffer di prelettura, il messaggio viene considerato abbandonato e viene nuovamente reso disponibile per il recupero dalla coda.

Questo problema potrebbe fare sì che il messaggio venga recuperato nel buffer di prelettura e venga posizionato alla fine. Se il buffer di prelettura non viene elaborato prima della scadenza del messaggio, i messaggi vengono recuperati in prelettura ripetutamente, ma senza mai essere effettivamente recapitati in uno stato utilizzabile (con un blocco valido). A quel punto, quando tali copie obsolete vengono rimosse dalla coda, l'applicazione elabora ripetutamente lo stesso messaggio e non è in grado di completarne l'elaborazione. In un altro caso, i messaggi ripetuti sono tutti scaduti nel buffer prima di essere utilizzati. In questo caso, il messaggio nel bus di servizio verrà infine spostato nella coda dei messaggi non recapitabili dopo il superamento del numero massimo di recapito.

Per altre informazioni, vedere la sezione Perché il prefetch non è l'opzione predefinita? di Prefetch dei messaggi di Bus di servizio di Azure.

Soluzione

Prestare attenzione alla configurazione del prefetch per assicurarsi che sia adatta alla capacità di consumo. È necessario bilanciare il conteggio massimo di prelettura e la durata del blocco configurata nella coda o nella sottoscrizione, in modo che il timeout del blocco sia almeno superiore al tempo cumulativo previsto per l'elaborazione dei messaggi corrispondente alla dimensione massima del buffer di prelettura, più un messaggio. Allo stesso tempo, il timeout del blocco non deve essere così lungo da far sì che i messaggi superino la loro durata massima quando vengono accidentalmente scartati, rendendo necessario attendere la scadenza del blocco prima che vengano recapitati nuovamente.

Per configurare l'attributo di prelettura, che ha un valore predefinito pari a zero, utilizzare una delle proprietà seguenti:

Proprietà Descrizione
spring.jms.servicebus.prefetch.all Il valore di fallback per l'opzione prefetch in questo spazio dei nomi di bus di servizio.
spring.jms.servicebus.prefetch.queue-prefetch Numero di prelettura per la coda.
spring.jms.servicebus.prefetch.queue-browser-prefetch Il numero di prelettura per il browser della coda.
spring.jms.servicebus.prefetch.topic-prefetch Il numero di prefetch per il topic.
spring.jms.servicebus.prefetch.durable-topic-prefetch Numero di prelettura per l'argomento durevole.

Come eseguire l'eliminazione AMQP nel bus di servizio?

JMS supporta cinque tipi di disposition AMQP quando si conferma la ricezione dei messaggi al broker di messaggistica. I valori supportati sono ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDe MODIFIED_FAILED_UNDELIVERABLE. Per altre informazioni, vedere la sezione disposizione AMQP e mapping delle operazioni del bus di servizio di Usare Java Message Service 1.1 con il bus di servizio di Azure standard e AMQP 1.0.

Quindi, per completare manualmente, abbandonare, inviare o rilasciare un messaggio usando JmsListener, seguire questa procedura:

  1. Disabilitare le transazioni di sessione e usare la modalità Ack CLIENT.

    Per eseguire questa attività, o dichiarare un bean JmsListenerContainerFactory personalizzato e quindi impostarne le proprietà, oppure post-elaborare il definito nello starter. L'esempio seguente usa l'approccio che consiste nel dichiarare un altro bean:

    @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. Nel gestore dei messaggi, completare o abbandonare esplicitamente i messaggi.

    @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;
        }
    }
    

Problemi di configurazione

Disabilitare la configurazione automatica JMS del bus di servizio

Descrizione del problema

Alcuni utenti importano Spring Cloud Azure Starter per la configurazione automatica di un servizio Azure diverso da bus di servizio JMS. Usano anche il framework Spring JMS senza la necessità di JMS del bus di servizio. Quindi, quando l'applicazione tenta di avviare, vengono generate le eccezioni seguenti:

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

Analisi delle cause

Questo problema si verifica perché tutte le classi di configurazione automatica di Spring Cloud di Azure vengono inserite nello stesso modulo, quindi qualsiasi avvio di Azure Spring Cloud importa effettivamente tutta la configurazione automatica, che include anche JMS del bus di servizio. Quindi, quando l'applicazione usa l'API Spring JMS, soddisfa la condizione di configurazione automatica JMS del bus di servizio e la attiva. Quindi, per gli utenti che non intendono utilizzare spring-cloud-azure-starter-servicebus-jms, le condizioni relative alle proprietà non sono soddisfatte perché non hanno motivo di configurare bus di servizio per JMS. Questa situazione provoca il verificarsi delle eccezioni.

Soluzione

Spring Cloud Azure per JMS del bus di servizio fornisce una proprietà per attivare o disattivare la configurazione automatica. È possibile scegliere di disabilitare questa funzionalità in base alle esigenze usando l'impostazione della proprietà seguente:

spring.jms.servicebus.enabled=false

Configurare gli attributi dei messaggi

Come impostare il tipo di contenuto dei messaggi in uscita?

Per configurare il tipo di contenuto, personalizzare Message Converter per modificare l'attributo content-type durante la conversione dei messaggi. Il codice seguente accetta i messaggi di byte come esempio.

Prima di tutto, personalizzare il convertitore di messaggi da usare nella JmsTemplate, come illustrato nell'esempio seguente:

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;
    }
}

Dichiarare quindi il bean del convertitore di messaggi personalizzato, come illustrato in questo esempio:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Come impostare il nome della proprietà ID di tipo per MappingJackson2MessageConverter?

L'attributo type-id-property-name consente al MappingJackson2MessageConverter di determinare quale classe utilizzare per deserializzare il payload del messaggio. Quando si serializza ogni oggetto Java in un payload Spring Message, il convertitore archivia il tipo di payload in una proprietà del messaggio con il nome della proprietà registrato da type-id-property-name. Quindi, durante la deserializzazione del messaggio, il convertitore legge l'ID del tipo dal messaggio ed esegue la deserializzazione.

Per impostare il type-id-property-name, dichiarare il proprio MappingJackson2MessageConverter bean e configurare tale proprietà, come illustrato nell'esempio seguente:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

Rilevamento duplicati

Il bus di servizio di Azure supporta rilevamento duplicati, che applica la proprietà MessageId per identificare in modo univoco i messaggi ed eliminare i duplicati inviati al bus di servizio.

Tuttavia, per l'API JMS, non è consigliabile impostare l'ID messaggio JMS, considerato illegale nelle specifiche JMS. Questa funzionalità non è quindi attualmente supportata per Spring Cloud Service Bus JMS Starter.

Per altri aggiornamenti per questa funzionalità, vedere il problema di GitHub .

Abilitare la registrazione del trasporto AMQP

Per altre informazioni, vedi la sezione abilitare la registrazione del trasporto AMQP in Risoluzione dei problemi di bus di servizio.

Ottenere assistenza aggiuntiva

Per altre informazioni sui modi per contattare il supporto, vedere supporto nella radice del repository.

Risorse per Spring Cloud bus di servizio di Azure JMS starter

Invio di segnalazioni su GitHub

Quando si archiviano problemi di GitHub, vengono richiesti i dettagli seguenti:

  • Configurazione di bus di servizio / Ambiente dello spazio dei nomi
    • A quale livello appartiene lo spazio dei nomi (Standard o Premium)?
    • Quale tipo di entità di messaggistica viene usato (coda o argomento)? e la relativa configurazione.
    • Qual è la dimensione media di ogni messaggio?
  • Qual è il modello di traffico? Ovvero, il numero di messaggi al minuto e se il client è sempre occupato o ha periodi di traffico lenti.
  • Codice e passaggi per la riproduzione
    • Questo aspetto è importante perché spesso non è possibile riprodurre il problema nell'ambiente.
  • Registri