Skip to content

Commit 4fdbdf1

Browse files
GH-8577: Revise ImapIdleChannelAdapter logic (#8588)
* GH-8577: Revise `ImapIdleChannelAdapter` logic Fixes #8577 When we process mail messages in async manner, it is possible that we end up in a race condition situation where the next idle cycle closes the folder. It is possible to reopen the folder, but feels better to block the current idle cycle until we are done with the message and therefore keep folder opened. * Deprecate `ImapIdleChannelAdapter.sendingTaskExecutor` in favor of an `ExecutorChannel` as an output for this channel adapter or similar async hand-off downstream. * Make use of `shouldReconnectAutomatically` as it is advertised for this channel adapter * Optimize the proxy creation for message sending task * * Remove `ImapIdleChannelAdapter.sendingTaskExecutor` * Fix language in docs Co-authored-by: Gary Russell <[email protected]> --------- Co-authored-by: Gary Russell <[email protected]>
1 parent 22d47e7 commit 4fdbdf1

File tree

9 files changed

+112
-118
lines changed

9 files changed

+112
-118
lines changed

spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java

Lines changed: 87 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,14 +19,12 @@
1919
import java.io.Serial;
2020
import java.time.Instant;
2121
import java.util.List;
22-
import java.util.concurrent.Executor;
23-
import java.util.concurrent.ExecutorService;
24-
import java.util.concurrent.Executors;
2522
import java.util.concurrent.ScheduledFuture;
23+
import java.util.concurrent.atomic.AtomicBoolean;
24+
import java.util.function.Consumer;
2625

2726
import jakarta.mail.Folder;
2827
import jakarta.mail.Message;
29-
import jakarta.mail.MessagingException;
3028
import org.aopalliance.aop.Advice;
3129

3230
import org.springframework.aop.framework.ProxyFactory;
@@ -38,6 +36,7 @@
3836
import org.springframework.integration.transaction.IntegrationResourceHolder;
3937
import org.springframework.integration.transaction.IntegrationResourceHolderSynchronization;
4038
import org.springframework.integration.transaction.TransactionSynchronizationFactory;
39+
import org.springframework.messaging.MessagingException;
4140
import org.springframework.scheduling.TaskScheduler;
4241
import org.springframework.scheduling.Trigger;
4342
import org.springframework.scheduling.TriggerContext;
@@ -78,12 +77,10 @@ public class ImapIdleChannelAdapter extends MessageProducerSupport implements Be
7877

7978
private boolean shouldReconnectAutomatically = true;
8079

81-
private Executor sendingTaskExecutor = Executors.newFixedThreadPool(1);
82-
83-
private boolean sendingTaskExecutorSet;
84-
8580
private List<Advice> adviceChain;
8681

82+
private Consumer<Object> messageSender;
83+
8784
private long reconnectDelay = DEFAULT_RECONNECT_DELAY; // milliseconds
8885

8986
private volatile ScheduledFuture<?> receivingTask;
@@ -103,21 +100,10 @@ public void setAdviceChain(List<Advice> adviceChain) {
103100
this.adviceChain = adviceChain;
104101
}
105102

106-
/**
107-
* Specify an {@link Executor} used to send messages received by the
108-
* adapter.
109-
* @param sendingTaskExecutor the sendingTaskExecutor to set
110-
*/
111-
public void setSendingTaskExecutor(Executor sendingTaskExecutor) {
112-
Assert.notNull(sendingTaskExecutor, "'sendingTaskExecutor' must not be null");
113-
this.sendingTaskExecutor = sendingTaskExecutor;
114-
this.sendingTaskExecutorSet = true;
115-
}
116-
117103
/**
118104
* Specify whether the IDLE task should reconnect automatically after
119-
* catching a {@link jakarta.mail.FolderClosedException} while waiting for messages. The
120-
* default value is <code>true</code>.
105+
* catching a {@link jakarta.mail.MessagingException} while waiting for messages. The
106+
* default value is true.
121107
* @param shouldReconnectAutomatically true to reconnect.
122108
*/
123109
public void setShouldReconnectAutomatically(boolean shouldReconnectAutomatically) {
@@ -148,6 +134,26 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv
148134
this.applicationEventPublisher = applicationEventPublisher;
149135
}
150136

137+
@Override
138+
@SuppressWarnings("unchecked")
139+
protected void onInit() {
140+
super.onInit();
141+
142+
Consumer<?> messageSenderToUse = new MessageSender();
143+
144+
if (!CollectionUtils.isEmpty(this.adviceChain)) {
145+
ProxyFactory proxyFactory = new ProxyFactory(messageSenderToUse);
146+
this.adviceChain.forEach(proxyFactory::addAdvice);
147+
for (Advice advice : this.adviceChain) {
148+
proxyFactory.addAdvice(advice);
149+
}
150+
messageSenderToUse = (Consumer<?>) proxyFactory.getProxy(this.classLoader);
151+
}
152+
153+
this.messageSender = (Consumer<Object>) messageSenderToUse;
154+
}
155+
156+
151157
/*
152158
* Lifecycle implementation
153159
*/
@@ -162,77 +168,64 @@ protected void doStart() {
162168
@Override
163169
// guarded by super#lifecycleLock
164170
protected void doStop() {
165-
this.receivingTask.cancel(true);
171+
if (this.receivingTask != null) {
172+
this.receivingTask.cancel(true);
173+
this.receivingTask = null;
174+
}
166175
this.mailReceiver.cancelPing();
167176
}
168177

169178
@Override
170179
public void destroy() {
171180
super.destroy();
172181
this.mailReceiver.destroy();
173-
// If we're running with the default executor, shut it down.
174-
if (!this.sendingTaskExecutorSet && this.sendingTaskExecutor != null) {
175-
((ExecutorService) this.sendingTaskExecutor).shutdown();
182+
}
183+
184+
private void publishException(Exception ex) {
185+
if (this.applicationEventPublisher != null) {
186+
this.applicationEventPublisher.publishEvent(new ImapIdleExceptionEvent(ex));
187+
}
188+
else {
189+
logger.debug(() -> "No application event publisher for exception: " + ex.getMessage());
176190
}
177191
}
178192

179-
private Runnable createMessageSendingTask(Object mailMessage) {
180-
Runnable sendingTask = prepareSendingTask(mailMessage);
193+
private class MessageSender implements Consumer<Object> {
181194

182-
// wrap in the TX proxy if necessary
183-
if (!CollectionUtils.isEmpty(this.adviceChain)) {
184-
ProxyFactory proxyFactory = new ProxyFactory(sendingTask);
185-
if (!CollectionUtils.isEmpty(this.adviceChain)) {
186-
for (Advice advice : this.adviceChain) {
187-
proxyFactory.addAdvice(advice);
188-
}
189-
}
190-
sendingTask = (Runnable) proxyFactory.getProxy(this.classLoader);
195+
MessageSender() {
191196
}
192-
return sendingTask;
193-
}
194197

195-
private Runnable prepareSendingTask(Object mailMessage) {
196-
return () -> {
197-
@SuppressWarnings("unchecked")
198-
org.springframework.messaging.Message<?> message =
198+
@Override
199+
public void accept(Object mailMessage) {
200+
org.springframework.messaging.Message<?> messageToSend =
199201
mailMessage instanceof Message
200202
? getMessageBuilderFactory().withPayload(mailMessage).build()
201-
: (org.springframework.messaging.Message<Object>) mailMessage;
203+
: (org.springframework.messaging.Message<?>) mailMessage;
202204

203205
if (TransactionSynchronizationManager.isActualTransactionActive()
204-
&& this.transactionSynchronizationFactory != null) {
206+
&& ImapIdleChannelAdapter.this.transactionSynchronizationFactory != null) {
205207

206-
TransactionSynchronization synchronization = this.transactionSynchronizationFactory.create(this);
208+
TransactionSynchronization synchronization =
209+
ImapIdleChannelAdapter.this.transactionSynchronizationFactory.create(this);
207210
if (synchronization != null) {
208211
TransactionSynchronizationManager.registerSynchronization(synchronization);
209212

210-
if (synchronization instanceof IntegrationResourceHolderSynchronization
213+
if (synchronization instanceof IntegrationResourceHolderSynchronization integrationSync
211214
&& !TransactionSynchronizationManager.hasResource(this)) {
212215

213-
TransactionSynchronizationManager.bindResource(this,
214-
((IntegrationResourceHolderSynchronization) synchronization).getResourceHolder());
216+
TransactionSynchronizationManager.bindResource(this, integrationSync.getResourceHolder());
215217
}
216218

217219
Object resourceHolder = TransactionSynchronizationManager.getResource(this);
218-
if (resourceHolder instanceof IntegrationResourceHolder) {
219-
((IntegrationResourceHolder) resourceHolder).setMessage(message);
220+
if (resourceHolder instanceof IntegrationResourceHolder integrationResourceHolder) {
221+
integrationResourceHolder.setMessage(messageToSend);
220222
}
221223
}
222224
}
223-
sendMessage(message);
224-
};
225-
}
226-
227-
private void publishException(Exception ex) {
228-
if (this.applicationEventPublisher != null) {
229-
this.applicationEventPublisher.publishEvent(new ImapIdleExceptionEvent(ex));
225+
sendMessage(messageToSend);
230226
}
231-
else {
232-
logger.debug(() -> "No application event publisher for exception: " + ex.getMessage());
233-
}
234-
}
235227

228+
}
236229

237230
private class ReceivingTask implements Runnable {
238231

@@ -246,10 +239,23 @@ public void run() {
246239
ImapIdleChannelAdapter.this.idleTask.run();
247240
logger.debug("Task completed successfully. Re-scheduling it again right away.");
248241
}
249-
catch (Exception ex) { //run again after a delay
250-
logger.warn(ex, () -> "Failed to execute IDLE task. Will attempt to resubmit in "
251-
+ ImapIdleChannelAdapter.this.reconnectDelay + " milliseconds.");
252-
ImapIdleChannelAdapter.this.receivingTaskTrigger.delayNextExecution();
242+
catch (Exception ex) {
243+
if (ImapIdleChannelAdapter.this.shouldReconnectAutomatically
244+
&& ex.getCause() instanceof jakarta.mail.MessagingException messagingException) {
245+
246+
//run again after a delay
247+
logger.info(messagingException,
248+
() -> "Failed to execute IDLE task. Will attempt to resubmit in "
249+
+ ImapIdleChannelAdapter.this.reconnectDelay + " milliseconds.");
250+
ImapIdleChannelAdapter.this.receivingTaskTrigger.delayNextExecution();
251+
}
252+
else {
253+
logger.warn(ex,
254+
"Failed to execute IDLE task. " +
255+
"Won't resubmit since not a 'shouldReconnectAutomatically'" +
256+
"or not a 'jakarta.mail.MessagingException'");
257+
ImapIdleChannelAdapter.this.receivingTaskTrigger.stop();
258+
}
253259
publishException(ex);
254260
}
255261
}
@@ -274,21 +280,19 @@ public void run() {
274280
Object[] mailMessages = ImapIdleChannelAdapter.this.mailReceiver.receive();
275281
logger.debug(() -> "received " + mailMessages.length + " mail messages");
276282
for (Object mailMessage : mailMessages) {
277-
Runnable messageSendingTask = createMessageSendingTask(mailMessage);
278283
if (isRunning()) {
279-
ImapIdleChannelAdapter.this.sendingTaskExecutor.execute(messageSendingTask);
284+
ImapIdleChannelAdapter.this.messageSender.accept(mailMessage);
280285
}
281286
}
282287
}
283288
}
284-
catch (MessagingException ex) {
289+
catch (jakarta.mail.MessagingException ex) {
285290
logger.warn(ex, "error occurred in idle task");
286291
if (ImapIdleChannelAdapter.this.shouldReconnectAutomatically) {
287292
throw new IllegalStateException("Failure in 'idle' task. Will resubmit.", ex);
288293
}
289294
else {
290-
throw new org.springframework.messaging.MessagingException(
291-
"Failure in 'idle' task. Will NOT resubmit.", ex);
295+
throw new MessagingException("Failure in 'idle' task. Will NOT resubmit.", ex);
292296
}
293297
}
294298
}
@@ -298,16 +302,20 @@ public void run() {
298302

299303
private class ExceptionAwarePeriodicTrigger implements Trigger {
300304

301-
private volatile boolean delayNextExecution;
305+
private final AtomicBoolean delayNextExecution = new AtomicBoolean();
306+
307+
private final AtomicBoolean stop = new AtomicBoolean();
302308

303309

304310
ExceptionAwarePeriodicTrigger() {
305311
}
306312

307313
@Override
308314
public Instant nextExecution(TriggerContext triggerContext) {
309-
if (this.delayNextExecution) {
310-
this.delayNextExecution = false;
315+
if (this.stop.getAndSet(false)) {
316+
return null;
317+
}
318+
if (this.delayNextExecution.getAndSet(false)) {
311319
return Instant.now().plusMillis(ImapIdleChannelAdapter.this.reconnectDelay);
312320
}
313321
else {
@@ -316,7 +324,11 @@ public Instant nextExecution(TriggerContext triggerContext) {
316324
}
317325

318326
void delayNextExecution() {
319-
this.delayNextExecution = true;
327+
this.delayNextExecution.set(true);
328+
}
329+
330+
void stop() {
331+
this.stop.set(true);
320332
}
321333

322334
}

spring-integration-mail/src/main/java/org/springframework/integration/mail/config/ImapIdleChannelAdapterParser.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,8 +54,6 @@ protected AbstractBeanDefinition doParse(Element element, ParserContext parserCo
5454
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, txElement,
5555
"synchronization-factory", "transactionSynchronizationFactory");
5656
}
57-
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "task-executor",
58-
"sendingTaskExecutor");
5957
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
6058
IntegrationNamespaceUtils.configureAndSetAdviceChainIfPresent(null,
6159
DomUtils.getChildElementByTagName(element, "transactional"), beanDefinition, parserContext);

spring-integration-mail/src/main/java/org/springframework/integration/mail/dsl/ImapIdleChannelAdapterSpec.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2022 the original author or authors.
2+
* Copyright 2014-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,7 +22,6 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Properties;
25-
import java.util.concurrent.Executor;
2625
import java.util.function.Consumer;
2726
import java.util.function.Function;
2827

@@ -348,17 +347,6 @@ public ImapIdleChannelAdapterSpec transactional() {
348347
return transactional(transactionInterceptor);
349348
}
350349

351-
/**
352-
* Specify a task executor to be used to send messages to the downstream flow.
353-
* @param sendingTaskExecutor the sendingTaskExecutor.
354-
* @return the spec.
355-
* @see ImapIdleChannelAdapter#setSendingTaskExecutor(Executor)
356-
*/
357-
public ImapIdleChannelAdapterSpec sendingTaskExecutor(Executor sendingTaskExecutor) {
358-
this.target.setSendingTaskExecutor(sendingTaskExecutor);
359-
return this;
360-
}
361-
362350
/**
363351
* @param shouldReconnectAutomatically the shouldReconnectAutomatically.
364352
* @return the spec.

spring-integration-mail/src/main/resources/org/springframework/integration/mail/config/spring-integration-mail.xsd

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -164,21 +164,6 @@
164164
</xsd:documentation>
165165
</xsd:annotation>
166166
</xsd:attribute>
167-
<xsd:attribute name="task-executor" type="xsd:string">
168-
<xsd:annotation>
169-
<xsd:documentation><![CDATA[
170-
Reference to a bean that implements
171-
org.springframework.core.task.TaskExecutor which is used
172-
to send Messages received by this adapter.
173-
If not provided, the adapter uses a single-threaded executor.
174-
]]></xsd:documentation>
175-
<xsd:appinfo>
176-
<tool:annotation kind="ref">
177-
<tool:expected-type type="org.springframework.core.task.TaskExecutor"/>
178-
</tool:annotation>
179-
</xsd:appinfo>
180-
</xsd:annotation>
181-
</xsd:attribute>
182167
<xsd:attribute name="cancel-idle-interval" type="xsd:string">
183168
<xsd:annotation>
184169
<xsd:documentation>
@@ -197,7 +182,7 @@
197182
<xsd:attribute name="store-uri" type="xsd:string">
198183
<xsd:annotation>
199184
<xsd:documentation><![CDATA[
200-
The URI for the Mail Store. Typically of the form: [pop3|imap]://user:password@host:port/INBOX
185+
The URI for the Mail Store. Typically, of the form: [pop3|imap]://user:password@host:port/INBOX
201186
If this is not provided, then the store will be retrieved via the no-arg Session.getStore()
202187
instead of the Session.getStore(url) method.
203188
]]></xsd:documentation>
@@ -206,7 +191,8 @@
206191
<xsd:attribute name="mail-filter-expression" type="xsd:string">
207192
<xsd:annotation>
208193
<xsd:documentation><![CDATA[
209-
Allows you to provide a SpEL expression which defines a fine grained filtering criteria for the mail messages to be processed by this adapter.
194+
Allows you to provide a SpEL expression which defines a fine-grained
195+
filtering criteria for the mail messages to be processed by this adapter.
210196
]]></xsd:documentation>
211197
</xsd:annotation>
212198
</xsd:attribute>

0 commit comments

Comments
 (0)