Implementation of future request handling is not thread-safe and may lead to unexpected behaviours
See original GitHub issueA few days ago I was reading async part of request handling. I’ve found up an interesting fact - result future is always accessed through resultFuture()
method call on mutable context. Well…, finally found some time to confirm these observations - the async context implementation is not thread-safe and may lead to some improper behaviors, especially in high-load environments.
What’s the problem?
By checking this condition Javalin determines if request should be handled sync/async:
And now we can be pretty much doomed, because further request handling still refers to the future stored in mutable filed in Context. For instance:
- User sets CompletableFuture as a result and delegates this task to some other thread pool to complete this task
- Javalin receives future result and starts async handling with code linked above
- User’s thread pool got priority and completed queued task where user responded with a new result that cleaned up current future https://github.com/tipsy/javalin/blob/2eb9211d96de713a3b9e24ba45189145caba11e7/javalin/src/main/java/io/javalin/http/Context.kt#L345
- Thread context restored, Javalin got some time to complete async context initialization
- Oopps, ctx is now corrupted, because future has disappeared. It may affect various things, e.g.:
- https://github.com/tipsy/javalin/blob/2eb9211d96de713a3b9e24ba45189145caba11e7/javalin/src/main/java/io/javalin/http/JavalinServlet.kt#L93
- https://github.com/tipsy/javalin/blob/2eb9211d96de713a3b9e24ba45189145caba11e7/javalin/src/main/java/io/javalin/http/JavalinServlet.kt#L98
- And probably other internals that somehow based on a promise that future in context is preserved during request processing
It’s quite breaking, because it completely breaks request and leads to difficult to define behaviors. For now I see at least 2 solutions:
- Store future result as a variable to at least guarantee built-in initialization phase. (fast)
- Provide some kind of a field that preserves future result of the current context (better, might be considered in the future). Let’s say that future result might be set only once in the Context and it shouldn’t be cleaned up.
Example exception
16:42:49.103 ERROR | Exception occurred while servicing http-request: java.lang.NullPointerException
at io.javalin.http.JavalinServlet.service(JavalinServlet.kt:98)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at io.javalin.jetty.JavalinJettyServlet.service(JavalinJettyServlet.kt:58)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:799)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:550)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)
at io.javalin.jetty.JettyServer$start$wsAndHttpHandler$1.doHandle(JettyServer.kt:52)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:501)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1349)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
at org.eclipse.jetty.server.Server.handle(Server.java:516)
at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:388)
at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:633)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:380)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:338)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:315)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:173)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:386)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034)
at java.lang.Thread.run(Thread.java:748)
Issue Analytics
- State:
- Created 2 years ago
- Comments:8 (8 by maintainers)
Top GitHub Comments
I’m happy you found this (and surprised it hasn’t been found before).
Thank you ❤️