-
-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Error when sending an email in Java with CompletableFuture.runAsync #104
Comments
Looks like a classpath issue but you already added
Do you have a dependency on |
No, I don't have such dependencies. |
I think this issue is related to the I don't know why but threads in this pool have a "special" classpath and If you use another CompletableFuture.runAsync(() -> mailerClient.send(email), Executors.newSingleThreadExecutor())
.exceptionally(exc -> {
exc.printStackTrace();
return null;
}); The following is also working: private static ForkJoinPool commonExecutor = new ForkJoinPool(ForkJoinPool.getCommonPoolParallelism());
CompletableFuture.runAsync(() -> mailerClient.send(email), commonExecutor)
.exceptionally(exc -> {
exc.printStackTrace();
return null;
}); NOTE: In this cases you don't need to redefine CAUTION: I've never used this API before so maybe there's a better, simpler workaround |
Hi Mogztter, I tried, but the behavior is the same, I still get the exception
Could be all this related about the new play classloader? I use oracle java 8 |
Are you running Play! in debug mode ? in production mode ? I'm running the application with Here's the modified files: build.bt name := """IssueTest"""
version := "1.0-SNAPSHOT"
lazy val root = (project in file(".")).enablePlugins(PlayJava)
scalaVersion := "2.11.7"
routesGenerator := StaticRoutesGenerator
libraryDependencies ++= Seq(
javaJdbc,
cache,
javaWs,
"org.webjars" %% "webjars-play" % "2.3.0",
"org.webjars" % "bootstrap" % "3.1.1-2",
"com.typesafe.akka" % "akka-remote_2.11" % "2.4.8",
"com.typesafe.play" %% "play-mailer" % "5.0.0"
) SmtpConnector.java package goodies;
import play.libs.mailer.Email;
import play.libs.mailer.MailerClient;
import javax.inject.Inject;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
public class SmtpConnector {
private final static String DEFAULT_EMAIL_SENDER = "[email protected]";
private final MailerClient mailerClient;
private static ForkJoinPool commonExecutor = new ForkJoinPool(ForkJoinPool.getCommonPoolParallelism());
@Inject
public SmtpConnector(MailerClient mailer) {
this.mailerClient = mailer;
}
public void send_mail(String[] recipients, String subject, String textBody, Optional<String> htmlBody) {
send_mail(DEFAULT_EMAIL_SENDER, recipients, subject, textBody, htmlBody);
}
public void send_mail(String sender, String[] recipients, String subject, String textBody, Optional<String> htmlBody) {
if (sender == null || recipients == null) {
return;
}
Email email = new Email()
.setFrom(sender)
.setSubject(subject)
.setBodyText(textBody);
Arrays.stream(recipients).forEach(recipient -> email.addTo(recipient));
if (htmlBody.isPresent()) {
email.setBodyHtml(htmlBody.get());
}
CompletableFuture.runAsync(() -> mailerClient.send(email), commonExecutor)
.exceptionally(exc -> {
exc.printStackTrace();
return null;
});
}
}
Probably
Me too
|
I found that specifying executor works, but only if the email sending method is called directly from the Play controller method. .thenApplyAsync(new Function<Message, JsonNode>() {
@Override
public JsonNode apply(Message response) {
try
{
Object json_reply = output_parametrized_constructor.newInstance(response, parameter.get(),http_ctx);
} and in the class construtor
I don't know if even moving out the sending from the constructor and reflectively calling a method for that on the just built object would fix the problem because the classloader context should not be Play's anyway, right? |
This seems rather complex but I don't know your use case. If we send an email synchronously everything is working fine. So I'm pretty sure that the classloader context is different when using |
I tried, with and w/o explicitly setting Here the class I used: public class SmtpConnector {
private final static MailerClient mailerClient = new SMTPMailer(
new SMTPConfiguration(
"127.0.0.1",
25,
false,
false,
scala.Option.apply(null),
scala.Option.apply(null),
false,
scala.Option.apply(null),
scala.Option.apply(null),
false));
private final static ForkJoinPool mail_pool = new ForkJoinPool(ForkJoinPool.getCommonPoolParallelism());
public static void send_mail(String[] recipients, String subject, String textBody, Optional<String> htmlBody) {
send_mail(Preferences.DEFAULT_EMAIL_SENDER, recipients, subject, textBody, htmlBody);
}
public static void send_mail(String sender, String[] recipients, String subject, String textBody, Optional<String> htmlBody) {
if (sender == null || recipients == null)
{
return;
}
Email email = new Email()
.setFrom(sender)
.setSubject(subject)
.setBodyText(textBody);
Arrays.stream(recipients).forEach(recipient -> email.addTo(recipient));
if (htmlBody.isPresent())
email.setBodyHtml(htmlBody.get());
//async call for sending email
CompletableFuture.supplyAsync( () -> mailerClient.send(email),mail_pool).exceptionally(exc -> {exc.printStackTrace(); return null;});
}
} |
This is working for me. Are you testing with your sample projet or with your full project ? Did you tried to use |
Yeah, but nothing changed. EDIT |
I'm not familiar with this Java API but I think you are doing too much asynchronous:
Why not simplify: HomeController.java public static CompletionStage<Result> index() {
try {
return CompletableFuture.supplyAsync(() -> build_me)
.thenApply(new Function<Constructor<?>, Result>() {
@Override
public Result apply(Constructor<?> constructor) {
try {
constructor.newInstance(null);
} catch (Exception exc) {
exc.printStackTrace();
return internalServerError("D'OH");
}
return ok("YAY");
}
});
} catch (Exception exc) {
exc.printStackTrace();
return CompletableFuture.completedFuture(internalServerError());
}
} SmtpConnector.java public void send_mail(String sender, String[] recipients, String subject, String textBody, Optional<String> htmlBody) {
if (sender == null || recipients == null)
{
return;
}
Email email = new Email()
.setFrom(sender)
.setSubject(subject)
.setBodyText(textBody);
Arrays.stream(recipients).forEach(recipient -> email.addTo(recipient));
if (htmlBody.isPresent()) {
email.setBodyHtml(htmlBody.get());
}
mailerClient.send(email);
} Also why are you using reflection ? |
That was just an example on how to reproduce the issue, In my real setting I have a generic webservice handling system that validates requests, queries backend and build responses automatically and asynchrously, so I have no idea about the types that are going to be built/returned by the system. Basically every webservice have to specify only which kind of object it wants as a reply and parsing/validation/querying is all automatic: private final static String SET_TASK_PARAMETERS_ROUTER = play.Play.application().configuration().getString("actor.set_task_parameters");
private final static GenericBrainAsyncer set_task_parameters_ws = new GenericBrainAsyncer(
SetTaskParametersInput.class,
GenericRestOutputGenerator.class,
SET_TASK_PARAMETERS_ROUTER,
WebServicesHotCache.GENERIC_TIMEOUT_REPLY);
@CorsRest
@VerboseRest
@RequireAuthentication
@BodyParser.Of(BodyParser.Json.class)
public static CompletionStage<Result> set_task_parameters() {
return set_task_parameters_ws.WS_behave(request().body().asJson());
} In this example, the ws request will be validated using Since the code of input builders and validators/output analyzers can be automatically generated too with a special compiler, in this way I can autogenerate the code of every new webservice regardless of what it takes, gives or what it does simply using a name convention in the actor conf file In my case I have to be asynchronous with the email sending because I don't want to add the email-sending delay while generating the response object the user, and only inside the reply validator I will have the infos required for the email sending, since they are been validated and extracted from the request/reply by an automatic validator |
Thanks for this detailed explanation. |
Since you are using Akka you could also try to send an email asynchronously with: system.scheduler().scheduleOnce(
Duration.create(10, TimeUnit.MILLISECONDS),
() -> // Send email here,
system.dispatcher()
); |
I confirm that even using commons-email it doesn't work due to the same error. I found out that seems another workaround for the issue |
I don’t know if you already read this page but you could try to use the application class loader to load play-mailer: https://www.playframework.com/documentation/2.5.x/ThreadPools#class-loaders-and-thread-locals @gmethvin any idea about the root cause ? These workarounds are not perfect especially for a Play! plugin. I think plugin integration (classpath context...) should be easier in Java. |
Hi @disbrain, Looking up the exception you got, it appears the same solution has been mentioned on stackoverflow quite long ago, though there are no references to CompletableFuture. You can use the application class loader if you want but it's going to basically do the same thing as your workaround. I'm not that familiar with JavaMail so I don't know if I can help much. Seems like there are certain expectations JavaMail has about how the context class loader is set up. I'd suggest asking on the Play mailing list; maybe someone there has a better solution. |
Yes @Mogztter but even now that I perfectly understand what it means I think that's not useful at all how is written, just confusing. |
Sorry for spamming everyone here, but looks like the task gets completed only with workaround told by @Mogztter. Is anyone able to reason out why it works only in case of new Execution Context. Thanks for the workaround though, it has literally solved my long pending problem. |
I just migrated from Play 2.3 to Play 2.5 but now I can't send mails anymore.
But now I get the following exception:
I touched NOTHING except the mailer/play version in
build.sbt
. Any clues please?The text was updated successfully, but these errors were encountered: