Skip to content

Commit 5e6e4de

Browse files
committed
WebSocket API: implements idle timeout, defaults to 5m
1 parent 9f51e1f commit 5e6e4de

File tree

12 files changed

+101
-11
lines changed

12 files changed

+101
-11
lines changed

jooby/src/main/java/io/jooby/Router.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby;
77

8+
import com.typesafe.config.Config;
89
import org.slf4j.Logger;
910

1011
import javax.annotation.Nonnull;
@@ -83,6 +84,13 @@ interface Match {
8384
/** Web socket. */
8485
String WS = "WS";
8586

87+
/**
88+
* Application configuration.
89+
*
90+
* @return Application configuration.
91+
*/
92+
@Nonnull Config getConfig();
93+
8694
/**
8795
* Mutable map of application attributes.
8896
*

jooby/src/main/java/io/jooby/internal/RouterImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby.internal;
77

8+
import com.typesafe.config.Config;
89
import io.jooby.BeanConverter;
910
import io.jooby.Context;
1011
import io.jooby.Jooby;
@@ -154,6 +155,10 @@ public RouterImpl(ClassLoader loader) {
154155
beanConverters = new ArrayList<>(3);
155156
}
156157

158+
@Nonnull @Override public Config getConfig() {
159+
throw new UnsupportedOperationException();
160+
}
161+
157162
@Nonnull @Override public Map<String, Object> getAttributes() {
158163
return attributes;
159164
}

jooby/src/main/kotlin/io/jooby/HandlerContext.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ class AfterContext(val ctx: Context, val result: Any?)
1010
class DecoratorContext(val ctx: Context, val next: Route.Handler)
1111

1212
class HandlerContext (val ctx: Context)
13+
14+
class WebSocketInitContext (val ctx: Context, val configurer: WebSocketConfigurer)

jooby/src/main/kotlin/io/jooby/Kooby.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ open class Kooby constructor() : Jooby() {
235235
return super.route(method, pattern) { ctx -> handler(HandlerContext(ctx)) }.setHandle(handler)
236236
}
237237

238+
@RouterDsl
239+
fun ws(pattern: String, handler: WebSocketInitContext.() -> Any): Route {
240+
return super.ws(pattern) { ctx, initializer -> handler(WebSocketInitContext(ctx, initializer)) }
241+
}
242+
238243
@OptionsDsl
239244
fun serverOptions(configurer: ServerOptions.() -> Unit): Kooby {
240245
val options = ServerOptions()

jooby/src/test/kotlin/io/jooby/Idioms.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ class Idioms : Kooby({
130130
ctx.pathString()
131131
}
132132
}
133+
134+
/** WebSocket: */
135+
ws("/ws") {
136+
configurer.onConnect { ws ->
137+
ws.send("SS")
138+
}
139+
}
133140
})
134141

135142
class IdiomsController {}

modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby.internal.jetty;
77

8+
import com.typesafe.config.Config;
89
import io.jooby.Body;
910
import io.jooby.ByteRange;
1011
import io.jooby.Context;
@@ -70,6 +71,7 @@
7071
import java.util.List;
7172
import java.util.Map;
7273
import java.util.concurrent.Executor;
74+
import java.util.concurrent.TimeUnit;
7375
import java.util.concurrent.atomic.AtomicBoolean;
7476

7577
import static org.eclipse.jetty.http.HttpHeader.CONTENT_TYPE;

modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
import io.jooby.WebSocketCloseStatus;
1313
import io.jooby.WebSocketConfigurer;
1414
import io.jooby.WebSocketMessage;
15+
import org.eclipse.jetty.websocket.api.CloseException;
1516
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
1617
import org.eclipse.jetty.websocket.api.Session;
1718
import org.eclipse.jetty.websocket.api.WebSocketListener;
1819
import org.eclipse.jetty.websocket.api.WriteCallback;
19-
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
2020

2121
import javax.annotation.Nonnull;
2222
import java.nio.charset.StandardCharsets;
@@ -26,6 +26,7 @@
2626
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.concurrent.ConcurrentMap;
2828
import java.util.concurrent.CopyOnWriteArrayList;
29+
import java.util.concurrent.TimeoutException;
2930

3031
public class JettyWebSocket implements WebSocketListener, WebSocketConfigurer, WebSocket,
3132
WriteCallback {
@@ -36,6 +37,7 @@ public class JettyWebSocket implements WebSocketListener, WebSocketConfigurer, W
3637

3738
private final JettyContext ctx;
3839
private final String key;
40+
private final String path;
3941
private Session session;
4042
private WebSocket.OnConnect onConnectCallback;
4143
private WebSocket.OnMessage onMessageCallback;
@@ -44,6 +46,7 @@ public class JettyWebSocket implements WebSocketListener, WebSocketConfigurer, W
4446

4547
public JettyWebSocket(JettyContext ctx) {
4648
this.ctx = ctx;
49+
this.path = ctx.pathString();
4750
this.key = ctx.getRoute().getPattern();
4851
}
4952

@@ -82,19 +85,29 @@ public JettyWebSocket(JettyContext ctx) {
8285

8386
@Override public void onWebSocketError(Throwable x) {
8487
// should close?
85-
if (Server.connectionLost(x) || SneakyThrows.isFatal(x)) {
86-
handleClose(WebSocketCloseStatus.SERVER_ERROR);
87-
}
88+
if (!isTimeout(x)) {
89+
if (Server.connectionLost(x) || SneakyThrows.isFatal(x)) {
90+
handleClose(WebSocketCloseStatus.SERVER_ERROR);
91+
}
8892

89-
if (onErrorCallback == null) {
90-
ctx.getRouter().getLog().error("Websocket resulted in exception: {}", ctx.pathString(), x);
91-
} else {
92-
onErrorCallback.onError(this, x);
93+
if (onErrorCallback == null) {
94+
ctx.getRouter().getLog().error("Websocket resulted in exception: {}", path, x);
95+
} else {
96+
onErrorCallback.onError(this, x);
97+
}
98+
99+
if (SneakyThrows.isFatal(x)) {
100+
throw SneakyThrows.propagate(x);
101+
}
93102
}
103+
}
94104

95-
if (SneakyThrows.isFatal(x)) {
96-
throw SneakyThrows.propagate(x);
105+
private boolean isTimeout(Throwable x) {
106+
if (x instanceof CloseException) {
107+
Throwable cause = x.getCause();
108+
return cause instanceof TimeoutException;
97109
}
110+
return false;
98111
}
99112

100113
@Nonnull @Override public WebSocketConfigurer onConnect(
@@ -181,7 +194,7 @@ public JettyWebSocket(JettyContext ctx) {
181194
}
182195

183196
@Override public void writeFailed(Throwable x) {
184-
ctx.getRouter().getLog().error("Websocket resulted in exception: {}", ctx.pathString(), x);
197+
ctx.getRouter().getLog().error("Websocket resulted in exception: {}", path, x);
185198
}
186199

187200
@Override public void writeSuccess() {

modules/jooby-jetty/src/main/java/io/jooby/jetty/Jetty.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby.jetty;
77

8+
import com.typesafe.config.Config;
89
import io.jooby.Jooby;
910
import io.jooby.ServerOptions;
1011
import io.jooby.SneakyThrows;
@@ -29,6 +30,7 @@
2930
import java.net.BindException;
3031
import java.util.ArrayList;
3132
import java.util.List;
33+
import java.util.concurrent.TimeUnit;
3234

3335
/**
3436
* Web server implementation using <a href="https://www.eclipse.org/jetty/">Jetty</a>.
@@ -112,6 +114,11 @@ public class Jetty extends io.jooby.Server.Base {
112114
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
113115
policy.setMaxTextMessageBufferSize(WebSocket.MAX_BUFFER_SIZE);
114116
policy.setMaxTextMessageSize(WebSocket.MAX_BUFFER_SIZE);
117+
Config conf = application.getConfig();
118+
long timeout = conf.hasPath("websocket.idleTimeout")
119+
? conf.getDuration("websocket.idleTimeout", TimeUnit.MINUTES)
120+
: 5;
121+
policy.setIdleTimeout(TimeUnit.MINUTES.toMillis(timeout));
115122
WebSocketServerFactory wssf = new WebSocketServerFactory(context.getServletContext(), policy);
116123
context.setAttribute(JettyWebSocket.WEBSOCKET_SERVER_FACTORY, wssf);
117124
context.addManaged(wssf);

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby.internal.netty;
77

8+
import com.typesafe.config.Config;
89
import io.jooby.Body;
910
import io.jooby.ByteRange;
1011
import io.jooby.Context;
@@ -55,6 +56,8 @@
5556
import io.netty.handler.stream.ChunkedNioStream;
5657
import io.netty.handler.stream.ChunkedStream;
5758
import io.netty.handler.stream.ChunkedWriteHandler;
59+
import io.netty.handler.timeout.IdleState;
60+
import io.netty.handler.timeout.IdleStateHandler;
5861
import io.netty.util.ReferenceCounted;
5962

6063
import javax.annotation.Nonnull;
@@ -68,6 +71,7 @@
6871
import java.nio.channels.FileChannel;
6972
import java.nio.channels.ReadableByteChannel;
7073
import java.nio.charset.Charset;
74+
import java.time.Duration;
7175
import java.util.ArrayList;
7276
import java.util.Collection;
7377
import java.util.Collections;
@@ -77,6 +81,7 @@
7781
import java.util.Map;
7882
import java.util.Set;
7983
import java.util.concurrent.Executor;
84+
import java.util.concurrent.TimeUnit;
8085

8186
import static io.netty.buffer.Unpooled.copiedBuffer;
8287
import static io.netty.buffer.Unpooled.wrappedBuffer;
@@ -290,6 +295,14 @@ public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, S
290295
webSocket.fireConnect();
291296
}
292297
});
298+
Config conf = getRouter().getConfig();
299+
long timeout = conf.hasPath("websocket.idleTimeout")
300+
? conf.getDuration("websocket.idleTimeout", TimeUnit.MINUTES)
301+
: 5;
302+
if (timeout > 0) {
303+
IdleStateHandler idle = new IdleStateHandler(timeout, 0, 0, TimeUnit.MINUTES);
304+
ctx.pipeline().addBefore("handler", "idle", idle);
305+
}
293306
} catch (Throwable x) {
294307
sendError(x);
295308
}

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.jooby.Router;
1111
import io.jooby.Server;
1212
import io.jooby.StatusCode;
13+
import io.jooby.WebSocketCloseStatus;
1314
import io.netty.channel.ChannelHandlerContext;
1415
import io.netty.channel.ChannelInboundHandlerAdapter;
1516
import io.netty.handler.codec.http.HttpContent;
@@ -25,6 +26,7 @@
2526
import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;
2627
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
2728
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
29+
import io.netty.handler.timeout.IdleStateEvent;
2830
import io.netty.util.AsciiString;
2931
import io.netty.util.concurrent.FastThreadLocal;
3032
import org.slf4j.Logger;
@@ -134,6 +136,15 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
134136
}
135137
}
136138

139+
@Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
140+
if (evt instanceof IdleStateEvent) {
141+
NettyWebSocket ws = ctx.channel().attr(NettyWebSocket.WS).getAndSet(null);
142+
if (ws != null) {
143+
ws.close(WebSocketCloseStatus.GOING_AWAY);
144+
}
145+
}
146+
}
147+
137148
@Override
138149
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
139150
try {

0 commit comments

Comments
 (0)