Async Servlet Support?

I'm trying to user Jetty's EventSourceServlet (from org.eclipse.jetty:jetty-servlets:10.0.21), which calls:

AsyncContext async = request.startAsync();

and then subsequently throws:

java.lang.IllegalStateException: !asyncSupported: NotAsync:com.inductiveautomation.ignition.gateway.bootstrap.MapServlet@3365c362

at org.eclipse.jetty.server.Request.startAsync(Request.java:2237)

at org.eclipse.jetty.servlets.EventSourceServlet.doGet(EventSourceServlet.java:92)

at javax.servlet.http.HttpServlet.service(HttpServlet.java:503)

at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)

at com.inductiveautomation.ignition.gateway.bootstrap.MapServlet.service(MapServlet.java:86)

at org.eclipse.jetty.servlet.ServletHolder$NotAsync.service(ServletHolder.java:1410)

at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)

at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1665)

at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:527)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:131)

at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:598)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:223)

at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1580)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)

at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1384)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)

at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:484)

at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1553)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)

at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1306)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)

at com.inductiveautomation.catapult.handlers.RemoteHostNameLookupHandler.handle(RemoteHostNameLookupHandler.java:121)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)

at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:301)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:51)

at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:141)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)

at org.eclipse.jetty.server.Server.handle(Server.java:563)

at org.eclipse.jetty.server.HttpChannel$RequestDispatchable.dispatch(HttpChannel.java:1598)

at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:753)

at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:501)

at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:287)

at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:314)

at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)

at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)

at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:421)

at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:390)

at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:277)

at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:199)

at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:411)

at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:969)

at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1194)

at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1149)

at java.base/java.lang.Thread.run(Unknown Source)

Is this surprising or expected?

Seems to me the routers that feed requests to servlets would also have to be async, and they are not. :man_shrugging:

2 Likes

I would agree with Phil. You probably can't directly register your own servlet (that doesn't go through our MapServlet) implementation without doing something a bit unsupported and/or not very clean via a module. You could probably do an "unsafe" cast to IgnitionGateway on your context and find your way to the underlying Jetty instance somehow and register your servlet manually.

Hmmm. Sounds like a challenge..... (No! No nerd-sniping today!)

I'll file this idea away for now... :laughing:

1 Like

:+1:t2: I’ll do some poking. If that doesn’t work out, I’ll just come up with my own implement of the EventSourceServlet.

I'm not going to use this, but here's the proof it can be done.

fun <T : Any> T.getPrivateProperty(variableName: String): Any? {
    return javaClass.getDeclaredField(variableName).let { field ->
        field.isAccessible = true
        return@let field.get(this)
    }
}

val ignitionServer = context.webResourceManager.getPrivateProperty("server")
logger.info(ignitionServer?.javaClass?.name)
// com.inductiveautomation.catapult.IgnitionServer

val jettyServer = ignitionServer?.getPrivateProperty("server")
logger.info(jettyServer?.javaClass?.name)
// org.eclipse.jetty.server.Server
3 Likes

val unmappedServlets = hashMapOf<String, ServletContextHandler>()

fun <T : WebResourceManager> T.getJettyServer(): Server {
    val ignitionServer = this.getPrivateProperty("server")
    return ignitionServer?.getPrivateProperty("server") as Server
}
fun <T : WebResourceManager> T.addUnmappedServlet(servlet: Class<out Servlet>, path: String) {
    val servletHandler = ServletContextHandler().apply {
        contextPath = "/$SHORT_MODULE_ID"
        addServlet(servlet, path)
    }
    unmappedServlets[path] = servletHandler
    (this.getJettyServer().handler as HandlerCollection).prependHandler(servletHandler)
    servletHandler.start()
}

fun <T : WebResourceManager> T.removeUnmappedServlet(path: String) {
    unmappedServlets[path]?.let{
        it.stop()
        (this.getJettyServer().handler as HandlerCollection).removeHandler(it)
    }
}
// Gateway Startup
context.webResourceManager.addUnmappedServlet(TagStreamServlet::class.java, "/sse")

// Gateway Shutdown
context.webResourceManager.removeUnmappedServlet("/sse")
5 Likes