Skip to content

Commit 3bbb3d4

Browse files
authored
Merge pull request #51231 from staillebois/feat/blocking-mailer-timeout-support
Add configurable timeout for blocking mailer
2 parents e951ca3 + a467cd7 commit 3bbb3d4

File tree

5 files changed

+160
-3
lines changed

5 files changed

+160
-3
lines changed

extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/BlockingMailerImpl.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.quarkus.mailer.runtime;
22

3+
import java.time.Duration;
4+
35
import io.quarkus.mailer.Mail;
46
import io.quarkus.mailer.Mailer;
57
import io.quarkus.mailer.reactive.ReactiveMailer;
@@ -10,13 +12,21 @@
1012
public class BlockingMailerImpl implements Mailer {
1113

1214
private final ReactiveMailer mailer;
15+
private final Duration timeout;
1316

14-
BlockingMailerImpl(ReactiveMailer mailer) {
17+
BlockingMailerImpl(ReactiveMailer mailer, Duration timeout) {
1518
this.mailer = mailer;
19+
this.timeout = timeout;
1620
}
1721

1822
@Override
1923
public void send(Mail... mails) {
20-
mailer.send(mails).await().indefinitely();
24+
if (timeout == null || timeout.isZero()) {
25+
// Backward compatibility: if timeout is 0 or null, wait indefinitely
26+
mailer.send(mails).await().indefinitely();
27+
} else {
28+
// Use the configured timeout
29+
mailer.send(mails).await().atMost(timeout);
30+
}
2131
}
2232
}

extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailerRuntimeConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,17 @@ public interface MailerRuntimeConfig {
261261
*/
262262
@WithDefault("false")
263263
boolean logInvalidRecipients();
264+
265+
/**
266+
* Sets the timeout duration for blocking mailer operations.
267+
* When using the blocking mailer ({@link io.quarkus.mailer.Mailer} interface), this timeout determines how long to wait
268+
* for the mail to be sent before throwing a {@link java.util.concurrent.TimeoutException}.
269+
* <p>
270+
* This prevents indefinite thread blocking when SMTP servers are misconfigured or unreachable.
271+
* A value of 0 means no timeout (wait indefinitely) - which is not recommended.
272+
* <p>
273+
* Default is 60 seconds.
274+
*/
275+
@WithDefault("PT60S")
276+
Duration timeout();
264277
}

extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/Mailers.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class Mailers {
5454
private final Map<String, io.vertx.mutiny.ext.mail.MailClient> mutinyClients;
5555
private final Map<String, MockMailboxImpl> mockMailboxes;
5656
private final Map<String, MutinyMailerImpl> mutinyMailers;
57+
private final MailersRuntimeConfig mailersRuntimeConfig;
5758

5859
public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRuntimeConfig mailersRuntimeConfig,
5960
LaunchMode launchMode, MailerSupport mailerSupport, TlsConfigurationRegistry tlsRegistry,
@@ -111,6 +112,7 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti
111112
this.mutinyClients = Collections.unmodifiableMap(localMutinyClients);
112113
this.mockMailboxes = Collections.unmodifiableMap(localMockMailboxes);
113114
this.mutinyMailers = Collections.unmodifiableMap(localMutinyMailers);
115+
this.mailersRuntimeConfig = mailersRuntimeConfig;
114116
}
115117

116118
public MailClient mailClientFromName(String name) {
@@ -122,7 +124,8 @@ public io.vertx.mutiny.ext.mail.MailClient reactiveMailClientFromName(String nam
122124
}
123125

124126
public Mailer mailerFromName(String name) {
125-
return new BlockingMailerImpl(reactiveMailerFromName(name));
127+
MailerRuntimeConfig config = mailersRuntimeConfig.mailers().get(name);
128+
return new BlockingMailerImpl(reactiveMailerFromName(name), config.timeout());
126129
}
127130

128131
public ReactiveMailer reactiveMailerFromName(String name) {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package io.quarkus.mailer.runtime;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatCode;
5+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6+
7+
import java.time.Duration;
8+
9+
import org.junit.jupiter.api.AfterEach;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
13+
import io.quarkus.mailer.Mail;
14+
import io.quarkus.mailer.Mailer;
15+
import io.quarkus.mailer.reactive.ReactiveMailer;
16+
import io.smallrye.mutiny.Uni;
17+
import io.vertx.mutiny.core.Vertx;
18+
19+
class BlockingMailerImplTest {
20+
21+
private Vertx vertx;
22+
23+
@BeforeEach
24+
void setup() {
25+
vertx = Vertx.vertx();
26+
}
27+
28+
@AfterEach
29+
void tearDown() {
30+
if (vertx != null) {
31+
vertx.close().await().indefinitely();
32+
}
33+
}
34+
35+
@Test
36+
void testTimeoutThrowsException() {
37+
ReactiveMailer slowMailer = new ReactiveMailer() {
38+
@Override
39+
public Uni<Void> send(Mail... mails) {
40+
return Uni.createFrom().emitter(emitter -> {
41+
// Never complete - simulates hanging connection
42+
});
43+
}
44+
};
45+
Mailer blockingMailer = new BlockingMailerImpl(slowMailer, Duration.ofSeconds(1));
46+
Mail mail = Mail.withText("[email protected]", "Subject", "Body");
47+
48+
assertThatThrownBy(() -> blockingMailer.send(mail))
49+
.isInstanceOf(io.smallrye.mutiny.TimeoutException.class);
50+
}
51+
52+
@Test
53+
void testTimeoutZeroWaitsIndefinitely() throws InterruptedException {
54+
ReactiveMailer delayedMailer = new ReactiveMailer() {
55+
@Override
56+
public Uni<Void> send(Mail... mails) {
57+
return Uni.createFrom().voidItem()
58+
.onItem().delayIt().by(Duration.ofMillis(500));
59+
}
60+
};
61+
Mailer blockingMailer = new BlockingMailerImpl(delayedMailer, Duration.ZERO);
62+
Mail mail = Mail.withText("[email protected]", "Subject", "Body");
63+
64+
// Should complete successfully even though it takes 500ms
65+
// because zero timeout means wait indefinitely
66+
long startTime = System.currentTimeMillis();
67+
blockingMailer.send(mail);
68+
long duration = System.currentTimeMillis() - startTime;
69+
70+
assertThat(duration).isGreaterThanOrEqualTo(500);
71+
}
72+
73+
@Test
74+
void testTimeoutNullWaitsIndefinitely() {
75+
ReactiveMailer delayedMailer = new ReactiveMailer() {
76+
@Override
77+
public Uni<Void> send(Mail... mails) {
78+
return Uni.createFrom().voidItem()
79+
.onItem().delayIt().by(Duration.ofMillis(500));
80+
}
81+
};
82+
Mailer blockingMailer = new BlockingMailerImpl(delayedMailer, null);
83+
Mail mail = Mail.withText("[email protected]", "Subject", "Body");
84+
85+
// Should complete successfully even though it takes 500ms
86+
// because null timeout means wait indefinitely
87+
long startTime = System.currentTimeMillis();
88+
blockingMailer.send(mail);
89+
long duration = System.currentTimeMillis() - startTime;
90+
91+
assertThat(duration).isGreaterThanOrEqualTo(500);
92+
}
93+
94+
@Test
95+
void testSuccessfulSendWithinTimeout() {
96+
ReactiveMailer fastMailer = new ReactiveMailer() {
97+
@Override
98+
public Uni<Void> send(Mail... mails) {
99+
return Uni.createFrom().voidItem();
100+
}
101+
};
102+
Mailer blockingMailer = new BlockingMailerImpl(fastMailer, Duration.ofSeconds(10));
103+
Mail mail = Mail.withText("[email protected]", "Subject", "Body");
104+
105+
long startTime = System.currentTimeMillis();
106+
assertThatCode(() -> blockingMailer.send(mail)).doesNotThrowAnyException();
107+
long duration = System.currentTimeMillis() - startTime;
108+
assertThat(duration).isLessThan(1000);
109+
}
110+
111+
@Test
112+
void testExceptionPropagation() {
113+
ReactiveMailer failingMailer = new ReactiveMailer() {
114+
@Override
115+
public Uni<Void> send(Mail... mails) {
116+
return Uni.createFrom().failure(new RuntimeException("SMTP error"));
117+
}
118+
};
119+
Mailer blockingMailer = new BlockingMailerImpl(failingMailer, Duration.ofSeconds(10));
120+
Mail mail = Mail.withText("[email protected]", "Subject", "Body");
121+
122+
assertThatThrownBy(() -> blockingMailer.send(mail))
123+
.isInstanceOf(RuntimeException.class)
124+
.hasMessage("SMTP error");
125+
}
126+
}

extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/FakeSmtpTestBase.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ public boolean logInvalidRecipients() {
313313
return false;
314314
}
315315

316+
@Override
317+
public Duration timeout() {
318+
return Duration.ofSeconds(60);
319+
}
320+
316321
}
317322

318323
}

0 commit comments

Comments
 (0)