Repository: eclipse-vertx/vert.x

Problem statement:
Provide read/write idle timeout granularity for TCP options



Hints:

Patch:
diff --git a/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java b/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java
--- a/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java
+++ b/src/main/generated/io/vertx/core/eventbus/EventBusOptionsConverter.java
@@ -150,6 +150,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, EventBu
             obj.setPort(((Number)member.getValue()).intValue());
           }
           break;
+        case "readIdleTimeout":
+          if (member.getValue() instanceof Number) {
+            obj.setReadIdleTimeout(((Number)member.getValue()).intValue());
+          }
+          break;
         case "receiveBufferSize":
           if (member.getValue() instanceof Number) {
             obj.setReceiveBufferSize(((Number)member.getValue()).intValue());
@@ -245,6 +250,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, EventBu
             obj.setUseAlpn((Boolean)member.getValue());
           }
           break;
+        case "writeIdleTimeout":
+          if (member.getValue() instanceof Number) {
+            obj.setWriteIdleTimeout(((Number)member.getValue()).intValue());
+          }
+          break;
       }
     }
   }
@@ -318,6 +328,7 @@ static void toJson(EventBusOptions obj, java.util.Map<String, Object> json) {
       json.put("pfxTrustOptions", obj.getPfxTrustOptions().toJson());
     }
     json.put("port", obj.getPort());
+    json.put("readIdleTimeout", obj.getReadIdleTimeout());
     json.put("receiveBufferSize", obj.getReceiveBufferSize());
     json.put("reconnectAttempts", obj.getReconnectAttempts());
     json.put("reconnectInterval", obj.getReconnectInterval());
@@ -341,5 +352,6 @@ static void toJson(EventBusOptions obj, java.util.Map<String, Object> json) {
       json.put("trustStoreOptions", obj.getTrustStoreOptions().toJson());
     }
     json.put("useAlpn", obj.isUseAlpn());
+    json.put("writeIdleTimeout", obj.getWriteIdleTimeout());
   }
 }
diff --git a/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java b/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java
--- a/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java
+++ b/src/main/generated/io/vertx/core/net/TCPSSLOptionsConverter.java
@@ -95,6 +95,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, TCPSSLO
             obj.setPfxTrustOptions(new io.vertx.core.net.PfxOptions((io.vertx.core.json.JsonObject)member.getValue()));
           }
           break;
+        case "readIdleTimeout":
+          if (member.getValue() instanceof Number) {
+            obj.setReadIdleTimeout(((Number)member.getValue()).intValue());
+          }
+          break;
         case "soLinger":
           if (member.getValue() instanceof Number) {
             obj.setSoLinger(((Number)member.getValue()).intValue());
@@ -150,6 +155,11 @@ static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, TCPSSLO
             obj.setUseAlpn((Boolean)member.getValue());
           }
           break;
+        case "writeIdleTimeout":
+          if (member.getValue() instanceof Number) {
+            obj.setWriteIdleTimeout(((Number)member.getValue()).intValue());
+          }
+          break;
       }
     }
   }
@@ -204,6 +214,7 @@ static void toJson(TCPSSLOptions obj, java.util.Map<String, Object> json) {
     if (obj.getPfxTrustOptions() != null) {
       json.put("pfxTrustOptions", obj.getPfxTrustOptions().toJson());
     }
+    json.put("readIdleTimeout", obj.getReadIdleTimeout());
     json.put("soLinger", obj.getSoLinger());
     json.put("ssl", obj.isSsl());
     json.put("sslHandshakeTimeout", obj.getSslHandshakeTimeout());
@@ -219,5 +230,6 @@ static void toJson(TCPSSLOptions obj, java.util.Map<String, Object> json) {
       json.put("trustStoreOptions", obj.getTrustStoreOptions().toJson());
     }
     json.put("useAlpn", obj.isUseAlpn());
+    json.put("writeIdleTimeout", obj.getWriteIdleTimeout());
   }
 }
diff --git a/src/main/java/io/vertx/core/eventbus/EventBusOptions.java b/src/main/java/io/vertx/core/eventbus/EventBusOptions.java
--- a/src/main/java/io/vertx/core/eventbus/EventBusOptions.java
+++ b/src/main/java/io/vertx/core/eventbus/EventBusOptions.java
@@ -331,6 +331,24 @@ public EventBusOptions setIdleTimeout(int idleTimeout) {
     return this;
   }
 
+  @Override
+  public EventBusOptions setReadIdleTimeout(int idleTimeout) {
+    super.setReadIdleTimeout(idleTimeout);
+    return this;
+  }
+
+  @Override
+  public EventBusOptions setWriteIdleTimeout(int idleTimeout) {
+    super.setWriteIdleTimeout(idleTimeout);
+    return this;
+  }
+
+  @Override
+  public EventBusOptions setIdleTimeoutUnit(TimeUnit idleTimeoutUnit) {
+    super.setIdleTimeoutUnit(idleTimeoutUnit);
+    return this;
+  }
+
   @Override
   @GenIgnore
   public EventBusOptions setKeyCertOptions(KeyCertOptions options) {
diff --git a/src/main/java/io/vertx/core/http/HttpClientOptions.java b/src/main/java/io/vertx/core/http/HttpClientOptions.java
--- a/src/main/java/io/vertx/core/http/HttpClientOptions.java
+++ b/src/main/java/io/vertx/core/http/HttpClientOptions.java
@@ -416,6 +416,18 @@ public HttpClientOptions setIdleTimeout(int idleTimeout) {
     return this;
   }
 
+  @Override
+  public HttpClientOptions setReadIdleTimeout(int idleTimeout) {
+    super.setReadIdleTimeout(idleTimeout);
+    return this;
+  }
+
+  @Override
+  public HttpClientOptions setWriteIdleTimeout(int idleTimeout) {
+    super.setWriteIdleTimeout(idleTimeout);
+    return this;
+  }
+
   @Override
   public HttpClientOptions setIdleTimeoutUnit(TimeUnit idleTimeoutUnit) {
     super.setIdleTimeoutUnit(idleTimeoutUnit);
diff --git a/src/main/java/io/vertx/core/http/HttpServerOptions.java b/src/main/java/io/vertx/core/http/HttpServerOptions.java
--- a/src/main/java/io/vertx/core/http/HttpServerOptions.java
+++ b/src/main/java/io/vertx/core/http/HttpServerOptions.java
@@ -313,6 +313,18 @@ public HttpServerOptions setIdleTimeout(int idleTimeout) {
     return this;
   }
 
+  @Override
+  public HttpServerOptions setReadIdleTimeout(int idleTimeout) {
+    super.setReadIdleTimeout(idleTimeout);
+    return this;
+  }
+
+  @Override
+  public HttpServerOptions setWriteIdleTimeout(int idleTimeout) {
+    super.setWriteIdleTimeout(idleTimeout);
+    return this;
+  }
+
   @Override
   public HttpServerOptions setIdleTimeoutUnit(TimeUnit idleTimeoutUnit) {
     super.setIdleTimeoutUnit(idleTimeoutUnit);
diff --git a/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java b/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java
--- a/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java
+++ b/src/main/java/io/vertx/core/http/impl/Http1xClientConnection.java
@@ -25,6 +25,7 @@
 import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker;
 import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
 import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker;
+import io.netty.handler.timeout.IdleStateEvent;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.FutureListener;
 import io.vertx.core.*;
@@ -1091,13 +1092,13 @@ protected void handleClosed() {
     }
   }
 
-  protected void handleIdle() {
+  protected void handleIdle(IdleStateEvent event) {
     synchronized (this) {
       if (webSocket == null && responses.isEmpty() && requests.isEmpty()) {
         return;
       }
     }
-    super.handleIdle();
+    super.handleIdle(event);
   }
 
   @Override
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java b/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ClientConnection.java
@@ -20,6 +20,7 @@
 import io.netty.handler.codec.http2.Http2Exception;
 import io.netty.handler.codec.http2.Http2Headers;
 import io.netty.handler.codec.http2.Http2Stream;
+import io.netty.handler.timeout.IdleStateEvent;
 import io.vertx.core.*;
 import io.vertx.core.buffer.Buffer;
 import io.vertx.core.http.*;
@@ -584,9 +585,9 @@ public HttpClientConnection connection() {
   }
 
   @Override
-  protected void handleIdle() {
+  protected void handleIdle(IdleStateEvent event) {
     if (handler.connection().local().numActiveStreams() > 0) {
-      super.handleIdle();
+      super.handleIdle(event);
     }
   }
 
diff --git a/src/main/java/io/vertx/core/http/impl/Http2ConnectionBase.java b/src/main/java/io/vertx/core/http/impl/Http2ConnectionBase.java
--- a/src/main/java/io/vertx/core/http/impl/Http2ConnectionBase.java
+++ b/src/main/java/io/vertx/core/http/impl/Http2ConnectionBase.java
@@ -23,6 +23,7 @@
 import io.netty.handler.codec.http2.Http2Headers;
 import io.netty.handler.codec.http2.Http2Settings;
 import io.netty.handler.codec.http2.Http2Stream;
+import io.netty.handler.timeout.IdleStateEvent;
 import io.vertx.codegen.annotations.Nullable;
 import io.vertx.core.AsyncResult;
 import io.vertx.core.Future;
@@ -33,7 +34,6 @@
 import io.vertx.core.http.GoAway;
 import io.vertx.core.http.HttpConnection;
 import io.vertx.core.http.StreamPriority;
-import io.vertx.core.impl.ContextInternal;
 import io.vertx.core.impl.EventLoopContext;
 import io.vertx.core.impl.future.PromiseInternal;
 import io.vertx.core.impl.VertxInternal;
@@ -106,8 +106,8 @@ protected void handleInterestedOpsChanged() {
   }
 
   @Override
-  protected void handleIdle() {
-    super.handleIdle();
+  protected void handleIdle(IdleStateEvent event) {
+    super.handleIdle(event);
   }
 
   synchronized void onConnectionError(Throwable cause) {
diff --git a/src/main/java/io/vertx/core/http/impl/HttpChannelConnector.java b/src/main/java/io/vertx/core/http/impl/HttpChannelConnector.java
--- a/src/main/java/io/vertx/core/http/impl/HttpChannelConnector.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpChannelConnector.java
@@ -144,14 +144,20 @@ public void httpConnect(EventLoopContext context, Handler<AsyncResult<HttpClient
   }
 
   private void applyHttp2ConnectionOptions(ChannelPipeline pipeline) {
-    if (options.getIdleTimeout() > 0) {
-      pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout(), options.getIdleTimeoutUnit()));
+    int idleTimeout = options.getIdleTimeout();
+    int readIdleTimeout = options.getReadIdleTimeout();
+    int writeIdleTimeout = options.getWriteIdleTimeout();
+    if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
+      pipeline.addLast("idle", new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, options.getIdleTimeoutUnit()));
     }
   }
 
   private void applyHttp1xConnectionOptions(ChannelPipeline pipeline) {
-    if (options.getIdleTimeout() > 0) {
-      pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout(), options.getIdleTimeoutUnit()));
+    int idleTimeout = options.getIdleTimeout();
+    int readIdleTimeout = options.getReadIdleTimeout();
+    int writeIdleTimeout = options.getWriteIdleTimeout();
+    if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
+      pipeline.addLast("idle", new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, options.getIdleTimeoutUnit()));
     }
     if (options.getLogActivity()) {
       pipeline.addLast("logging", new LoggingHandler());
diff --git a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java
--- a/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java
+++ b/src/main/java/io/vertx/core/http/impl/HttpServerWorker.java
@@ -144,8 +144,11 @@ private void configurePipeline(Channel ch) {
         handleHttp1(ch);
       } else {
         IdleStateHandler idle;
-        if (options.getIdleTimeout() > 0) {
-          pipeline.addLast("idle", idle = new IdleStateHandler(0, 0, options.getIdleTimeout(), options.getIdleTimeoutUnit()));
+        int idleTimeout = options.getIdleTimeout();
+        int readIdleTimeout = options.getReadIdleTimeout();
+        int writeIdleTimeout = options.getWriteIdleTimeout();
+        if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
+          pipeline.addLast("idle", idle = new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, options.getIdleTimeoutUnit()));
         } else {
           idle = null;
         }
@@ -211,8 +214,11 @@ void configureHttp2(ChannelPipeline pipeline) {
       pipeline.channel().close();
       return;
     }
-    if (options.getIdleTimeout() > 0) {
-      pipeline.addBefore("handler", "idle", new IdleStateHandler(0, 0, options.getIdleTimeout(), options.getIdleTimeoutUnit()));
+    int idleTimeout = options.getIdleTimeout();
+    int readIdleTimeout = options.getReadIdleTimeout();
+    int writeIdleTimeout = options.getWriteIdleTimeout();
+    if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
+      pipeline.addBefore("handler", "idle", new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, options.getIdleTimeoutUnit()));
     }
   }
 
@@ -261,8 +267,11 @@ private void configureHttp1OrH2C(ChannelPipeline pipeline) {
       // only add ChunkedWriteHandler when SSL is enabled otherwise it is not needed as FileRegion is used.
       pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());       // For large file / sendfile support
     }
-    if (options.getIdleTimeout() > 0) {
-      pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout(), options.getIdleTimeoutUnit()));
+    int idleTimeout = options.getIdleTimeout();
+    int readIdleTimeout = options.getReadIdleTimeout();
+    int writeIdleTimeout = options.getWriteIdleTimeout();
+    if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
+      pipeline.addLast("idle", new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, options.getIdleTimeoutUnit()));
     }
     if (disableH2C) {
       configureHttp1(pipeline);
diff --git a/src/main/java/io/vertx/core/http/impl/VertxHttp2ConnectionHandler.java b/src/main/java/io/vertx/core/http/impl/VertxHttp2ConnectionHandler.java
--- a/src/main/java/io/vertx/core/http/impl/VertxHttp2ConnectionHandler.java
+++ b/src/main/java/io/vertx/core/http/impl/VertxHttp2ConnectionHandler.java
@@ -150,8 +150,8 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
     try {
       super.userEventTriggered(ctx, evt);
     } finally {
-      if (evt instanceof IdleStateEvent && ((IdleStateEvent) evt).state() == IdleState.ALL_IDLE) {
-        connection.handleIdle();
+      if (evt instanceof IdleStateEvent) {
+        connection.handleIdle((IdleStateEvent) evt);
       }
     }
   }
diff --git a/src/main/java/io/vertx/core/net/ClientOptionsBase.java b/src/main/java/io/vertx/core/net/ClientOptionsBase.java
--- a/src/main/java/io/vertx/core/net/ClientOptionsBase.java
+++ b/src/main/java/io/vertx/core/net/ClientOptionsBase.java
@@ -261,6 +261,16 @@ public ClientOptionsBase setIdleTimeout(int idleTimeout) {
     return (ClientOptionsBase) super.setIdleTimeout(idleTimeout);
   }
 
+  @Override
+  public ClientOptionsBase setReadIdleTimeout(int idleTimeout) {
+    return (ClientOptionsBase) super.setReadIdleTimeout(idleTimeout);
+  }
+
+  @Override
+  public ClientOptionsBase setWriteIdleTimeout(int idleTimeout) {
+    return (ClientOptionsBase) super.setWriteIdleTimeout(idleTimeout);
+  }
+
   @Override
   public ClientOptionsBase setIdleTimeoutUnit(TimeUnit idleTimeoutUnit) {
     return (ClientOptionsBase) super.setIdleTimeoutUnit(idleTimeoutUnit);
diff --git a/src/main/java/io/vertx/core/net/NetClientOptions.java b/src/main/java/io/vertx/core/net/NetClientOptions.java
--- a/src/main/java/io/vertx/core/net/NetClientOptions.java
+++ b/src/main/java/io/vertx/core/net/NetClientOptions.java
@@ -151,6 +151,18 @@ public NetClientOptions setIdleTimeout(int idleTimeout) {
     return this;
   }
 
+  @Override
+  public NetClientOptions setReadIdleTimeout(int idleTimeout) {
+    super.setReadIdleTimeout(idleTimeout);
+    return this;
+  }
+
+  @Override
+  public NetClientOptions setWriteIdleTimeout(int idleTimeout) {
+    super.setWriteIdleTimeout(idleTimeout);
+    return this;
+  }
+
   @Override
   public NetClientOptions setIdleTimeoutUnit(TimeUnit idleTimeoutUnit) {
     super.setIdleTimeoutUnit(idleTimeoutUnit);
diff --git a/src/main/java/io/vertx/core/net/NetServerOptions.java b/src/main/java/io/vertx/core/net/NetServerOptions.java
--- a/src/main/java/io/vertx/core/net/NetServerOptions.java
+++ b/src/main/java/io/vertx/core/net/NetServerOptions.java
@@ -181,6 +181,18 @@ public NetServerOptions setIdleTimeout(int idleTimeout) {
     return this;
   }
 
+  @Override
+  public NetServerOptions setReadIdleTimeout(int idleTimeout) {
+    super.setReadIdleTimeout(idleTimeout);
+    return this;
+  }
+
+  @Override
+  public NetServerOptions setWriteIdleTimeout(int idleTimeout) {
+    super.setWriteIdleTimeout(idleTimeout);
+    return this;
+  }
+
   @Override
   public NetServerOptions setIdleTimeoutUnit(TimeUnit idleTimeoutUnit) {
     super.setIdleTimeoutUnit(idleTimeoutUnit);
diff --git a/src/main/java/io/vertx/core/net/TCPSSLOptions.java b/src/main/java/io/vertx/core/net/TCPSSLOptions.java
--- a/src/main/java/io/vertx/core/net/TCPSSLOptions.java
+++ b/src/main/java/io/vertx/core/net/TCPSSLOptions.java
@@ -57,6 +57,16 @@ public abstract class TCPSSLOptions extends NetworkOptions {
    */
   public static final TimeUnit DEFAULT_IDLE_TIMEOUT_TIME_UNIT = TimeUnit.SECONDS;
 
+  /**
+   * Default read idle timeout = 0
+   */
+  public static final int DEFAULT_READ_IDLE_TIMEOUT = 0;
+
+  /**
+   * Default write idle timeout = 0
+   */
+  public static final int DEFAULT_WRITE_IDLE_TIMEOUT = 0;
+
   /**
    * Default use alpn = false
    */
@@ -105,6 +115,8 @@ public abstract class TCPSSLOptions extends NetworkOptions {
   private boolean tcpKeepAlive;
   private int soLinger;
   private int idleTimeout;
+  private int readIdleTimeout;
+  private int writeIdleTimeout;
   private TimeUnit idleTimeoutUnit;
   private boolean ssl;
   private long sslHandshakeTimeout;
@@ -141,6 +153,8 @@ public TCPSSLOptions(TCPSSLOptions other) {
     this.soLinger = other.getSoLinger();
     this.idleTimeout = other.getIdleTimeout();
     this.idleTimeoutUnit = other.getIdleTimeoutUnit() != null ? other.getIdleTimeoutUnit() : DEFAULT_IDLE_TIMEOUT_TIME_UNIT;
+    this.readIdleTimeout = other.getReadIdleTimeout();
+    this.writeIdleTimeout = other.getWriteIdleTimeout();
     this.ssl = other.isSsl();
     this.sslHandshakeTimeout = other.sslHandshakeTimeout;
     this.sslHandshakeTimeoutUnit = other.getSslHandshakeTimeoutUnit() != null ? other.getSslHandshakeTimeoutUnit() : DEFAULT_SSL_HANDSHAKE_TIMEOUT_TIME_UNIT;
@@ -184,6 +198,8 @@ private void init() {
     tcpKeepAlive = DEFAULT_TCP_KEEP_ALIVE;
     soLinger = DEFAULT_SO_LINGER;
     idleTimeout = DEFAULT_IDLE_TIMEOUT;
+    readIdleTimeout = DEFAULT_READ_IDLE_TIMEOUT;
+    writeIdleTimeout = DEFAULT_WRITE_IDLE_TIMEOUT;
     idleTimeoutUnit = DEFAULT_IDLE_TIMEOUT_TIME_UNIT;
     ssl = DEFAULT_SSL;
     sslHandshakeTimeout = DEFAULT_SSL_HANDSHAKE_TIMEOUT;
@@ -259,11 +275,11 @@ public TCPSSLOptions setSoLinger(int soLinger) {
 
   /**
    * Set the idle timeout, default time unit is seconds. Zero means don't timeout.
-   * This determines if a connection will timeout and be closed if no data is received within the timeout.
+   * This determines if a connection will timeout and be closed if no data is received nor sent within the timeout.
    *
    * If you want change default time unit, use {@link #setIdleTimeoutUnit(TimeUnit)}
    *
-   * @param idleTimeout  the timeout, in seconds
+   * @param idleTimeout  the timeout
    * @return a reference to this, so the API can be used fluently
    */
   public TCPSSLOptions setIdleTimeout(int idleTimeout) {
@@ -281,6 +297,54 @@ public int getIdleTimeout() {
     return idleTimeout;
   }
 
+  /**
+   * Set the read idle timeout, default time unit is seconds. Zero means don't timeout.
+   * This determines if a connection will timeout and be closed if no data is received within the timeout.
+   *
+   * If you want change default time unit, use {@link #setIdleTimeoutUnit(TimeUnit)}
+   *
+   * @param idleTimeout  the read timeout
+   * @return a reference to this, so the API can be used fluently
+   */
+  public TCPSSLOptions setReadIdleTimeout(int idleTimeout) {
+    if (idleTimeout < 0) {
+      throw new IllegalArgumentException("readIdleTimeout must be >= 0");
+    }
+    this.readIdleTimeout = idleTimeout;
+    return this;
+  }
+
+  /**
+   * @return the read idle timeout, in time unit specified by {@link #getIdleTimeoutUnit()}.
+   */
+  public int getReadIdleTimeout() {
+    return readIdleTimeout;
+  }
+
+  /**
+   * Set the write idle timeout, default time unit is seconds. Zero means don't timeout.
+   * This determines if a connection will timeout and be closed if no data is sent within the timeout.
+   *
+   * If you want change default time unit, use {@link #setIdleTimeoutUnit(TimeUnit)}
+   *
+   * @param idleTimeout  the write timeout
+   * @return a reference to this, so the API can be used fluently
+   */
+  public TCPSSLOptions setWriteIdleTimeout(int idleTimeout) {
+    if (idleTimeout < 0) {
+      throw new IllegalArgumentException("writeIdleTimeout must be >= 0");
+    }
+    this.writeIdleTimeout = idleTimeout;
+    return this;
+  }
+
+  /**
+   * @return the write idle timeout, in time unit specified by {@link #getIdleTimeoutUnit()}.
+   */
+  public int getWriteIdleTimeout() {
+    return writeIdleTimeout;
+  }
+
   /**
    * Set the idle timeout unit. If not specified, default is seconds.
    *
diff --git a/src/main/java/io/vertx/core/net/impl/ConnectionBase.java b/src/main/java/io/vertx/core/net/impl/ConnectionBase.java
--- a/src/main/java/io/vertx/core/net/impl/ConnectionBase.java
+++ b/src/main/java/io/vertx/core/net/impl/ConnectionBase.java
@@ -15,6 +15,7 @@
 import io.netty.channel.*;
 import io.netty.handler.ssl.SslHandler;
 import io.netty.handler.stream.ChunkedFile;
+import io.netty.handler.timeout.IdleStateEvent;
 import io.netty.util.AttributeKey;
 import io.netty.util.ReferenceCountUtil;
 import io.netty.util.concurrent.EventExecutor;
@@ -396,8 +397,9 @@ private void checkCloseHandler(AsyncResult<Void> ar) {
    * <p/>
    * Subclasses can override it to prevent the idle event to happen (e.g when the connection is pooled) or
    * perform extra work when the idle event happens.
+   * @param event
    */
-  protected void handleIdle() {
+  protected void handleIdle(IdleStateEvent event) {
     chctx.close();
   }
 
diff --git a/src/main/java/io/vertx/core/net/impl/NetClientImpl.java b/src/main/java/io/vertx/core/net/impl/NetClientImpl.java
--- a/src/main/java/io/vertx/core/net/impl/NetClientImpl.java
+++ b/src/main/java/io/vertx/core/net/impl/NetClientImpl.java
@@ -58,6 +58,8 @@ public class NetClientImpl implements MetricsProvider, NetClient, Closeable {
 
   private static final Logger log = LoggerFactory.getLogger(NetClientImpl.class);
   protected final int idleTimeout;
+  protected final int readIdleTimeout;
+  protected final int writeIdleTimeout;
   private final TimeUnit idleTimeoutUnit;
   protected final boolean logEnabled;
 
@@ -77,6 +79,8 @@ public NetClientImpl(VertxInternal vertx, NetClientOptions options, CloseFuture
     this.metrics = vertx.metricsSPI() != null ? vertx.metricsSPI().createNetClientMetrics(options) : null;
     this.logEnabled = options.getLogActivity();
     this.idleTimeout = options.getIdleTimeout();
+    this.readIdleTimeout = options.getReadIdleTimeout();
+    this.writeIdleTimeout = options.getWriteIdleTimeout();
     this.idleTimeoutUnit = options.getIdleTimeoutUnit();
     this.closeFuture = closeFuture;
     this.proxyFilter = options.getNonProxyHosts() != null ? ProxyFilter.nonProxyHosts(options.getNonProxyHosts()) : ProxyFilter.DEFAULT_PROXY_FILTER;
@@ -92,8 +96,8 @@ protected void initChannel(ChannelPipeline pipeline) {
       // only add ChunkedWriteHandler when SSL is enabled otherwise it is not needed as FileRegion is used.
       pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());       // For large file / sendfile support
     }
-    if (idleTimeout > 0) {
-      pipeline.addLast("idle", new IdleStateHandler(0, 0, idleTimeout, idleTimeoutUnit));
+    if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
+      pipeline.addLast("idle", new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, idleTimeoutUnit));
     }
   }
 
diff --git a/src/main/java/io/vertx/core/net/impl/NetServerImpl.java b/src/main/java/io/vertx/core/net/impl/NetServerImpl.java
--- a/src/main/java/io/vertx/core/net/impl/NetServerImpl.java
+++ b/src/main/java/io/vertx/core/net/impl/NetServerImpl.java
@@ -112,8 +112,11 @@ protected void initChannel(ChannelPipeline pipeline) {
       // only add ChunkedWriteHandler when SSL is enabled otherwise it is not needed as FileRegion is used.
       pipeline.addLast("chunkedWriter", new ChunkedWriteHandler());       // For large file / sendfile support
     }
-    if (options.getIdleTimeout() > 0) {
-      pipeline.addLast("idle", new IdleStateHandler(0, 0, options.getIdleTimeout(), options.getIdleTimeoutUnit()));
+    int idleTimeout = options.getIdleTimeout();
+    int readIdleTimeout = options.getReadIdleTimeout();
+    int writeIdleTimeout = options.getWriteIdleTimeout();
+    if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
+      pipeline.addLast("idle", new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, options.getIdleTimeoutUnit()));
     }
   }
 
diff --git a/src/main/java/io/vertx/core/net/impl/VertxHandler.java b/src/main/java/io/vertx/core/net/impl/VertxHandler.java
--- a/src/main/java/io/vertx/core/net/impl/VertxHandler.java
+++ b/src/main/java/io/vertx/core/net/impl/VertxHandler.java
@@ -161,8 +161,8 @@ public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exce
 
   @Override
   public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
-    if (evt instanceof IdleStateEvent && ((IdleStateEvent) evt).state() == IdleState.ALL_IDLE) {
-      conn.handleIdle();
+    if (evt instanceof IdleStateEvent) {
+      conn.handleIdle((IdleStateEvent) evt);
     } else {
       ctx.fireUserEventTriggered(evt);
     }


Test patch:
diff --git a/src/test/java/io/vertx/core/net/NetTest.java b/src/test/java/io/vertx/core/net/NetTest.java
--- a/src/test/java/io/vertx/core/net/NetTest.java
+++ b/src/test/java/io/vertx/core/net/NetTest.java
@@ -1169,40 +1169,96 @@ public void testReconnectAttemptsNotEnough() {
   }
 
   @Test
-  public void testServerIdleTimeout() {
-    server.close();
-    NetServerOptions netServerOptions = new NetServerOptions();
-    netServerOptions.setIdleTimeout(1000);
-    netServerOptions.setIdleTimeoutUnit(TimeUnit.MILLISECONDS);
-    server = vertx.createNetServer(netServerOptions);
-    server.connectHandler(s -> {}).listen(testAddress, ar -> {
-      assertTrue(ar.succeeded());
-      client.connect(testAddress, res -> {
-        assertTrue(res.succeeded());
-        NetSocket socket = res.result();
-        socket.closeHandler(v -> testComplete());
-      });
-    });
-    await();
+  public void testServerIdleTimeout1() {
+    testTimeout(new NetClientOptions(), new NetServerOptions().setIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), received -> assertEquals("0123456789", received.toString()), true);
   }
 
   @Test
-  public void testClientIdleTimeout() {
-    client.close();
-    NetClientOptions netClientOptions = new NetClientOptions();
-    netClientOptions.setIdleTimeout(1000);
-    netClientOptions.setIdleTimeoutUnit(TimeUnit.MILLISECONDS);
-    client = vertx.createNetClient(netClientOptions);
+  public void testServerIdleTimeout2() {
+    testTimeout(new NetClientOptions(), new NetServerOptions().setReadIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), received -> assertEquals("0123456789", received.toString()), true);
+  }
 
-    server.connectHandler(s -> {
-    }).listen(testAddress, ar -> {
-      assertTrue(ar.succeeded());
-      client.connect(testAddress, res -> {
-        assertTrue(res.succeeded());
-        NetSocket socket = res.result();
-        socket.closeHandler(v -> testComplete());
+  @Test
+  public void testServerIdleTimeout3() {
+    // Usually 012 but might be 01 or 0123
+    testTimeout(new NetClientOptions(), new NetServerOptions().setWriteIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), received -> assertFalse("0123456789".equals(received.toString())), true);
+  }
+
+  @Test
+  public void testServerIdleTimeout4() {
+    testTimeout(new NetClientOptions(), new NetServerOptions().setIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), received -> assertEquals("0123456789", received.toString()), false);
+  }
+
+  @Test
+  public void testServerIdleTimeout5() {
+    // Usually 012 but might be 01 or 0123
+    testTimeout(new NetClientOptions(), new NetServerOptions().setReadIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), received -> assertFalse("0123456789".equals(received.toString())), false);
+  }
+
+  @Test
+  public void testServerIdleTimeout6() {
+    testTimeout(new NetClientOptions(), new NetServerOptions().setWriteIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), received -> assertEquals("0123456789", received.toString()), false);
+  }
+
+  @Test
+  public void testClientIdleTimeout1() {
+    testTimeout(new NetClientOptions().setIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), new NetServerOptions(), received -> assertEquals("0123456789", received.toString()), true);
+  }
+
+  @Test
+  public void testClientIdleTimeout2() {
+    testTimeout(new NetClientOptions().setWriteIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), new NetServerOptions(), received -> assertEquals("0123456789", received.toString()), true);
+  }
+
+  @Test
+  public void testClientIdleTimeout3() {
+    // Usually 012 but might be 01 or 0123
+    testTimeout(new NetClientOptions().setReadIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), new NetServerOptions(), received -> assertFalse("0123456789".equals(received.toString())), true);
+  }
+
+  @Test
+  public void testClientIdleTimeout4() {
+    testTimeout(new NetClientOptions().setIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), new NetServerOptions(), received -> assertEquals("0123456789", received.toString()), false);
+  }
+
+  @Test
+  public void testClientIdleTimeout5() {
+    // Usually 012 but might be 01 or 0123
+    testTimeout(new NetClientOptions().setWriteIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), new NetServerOptions(), received -> assertFalse("0123456789".equals(received.toString())), false);
+  }
+
+  @Test
+  public void testClientIdleTimeout6() {
+    testTimeout(new NetClientOptions().setReadIdleTimeout(500).setIdleTimeoutUnit(TimeUnit.MILLISECONDS), new NetServerOptions(), received -> assertEquals("0123456789", received.toString()), false);
+  }
+
+  private void testTimeout(NetClientOptions clientOptions, NetServerOptions serverOptions, Consumer<Buffer> check, boolean clientSends) {
+    server.close();
+    server = vertx.createNetServer(serverOptions);
+    client.close();
+    client = vertx.createNetClient(clientOptions);
+    Buffer received = Buffer.buffer();
+    Handler<NetSocket> receiver = s -> s.handler(received::appendBuffer);
+    Handler<NetSocket> sender = socket -> {
+      AtomicInteger times = new AtomicInteger();
+      vertx.setPeriodic(100, id -> {
+        int val = times.getAndIncrement();
+        if (val < 10) {
+          socket.write("" + val);
+        } else {
+          vertx.cancelTimer(id);
+        }
       });
-    });
+      socket.closeHandler(v -> {
+        check.accept(received);
+        testComplete();
+      });
+    };
+    Handler<NetSocket> clientHandler = clientSends ? sender : receiver;
+    Handler<NetSocket> serverHandler = clientSends ? receiver : sender;
+    server.connectHandler(serverHandler).listen(testAddress, onSuccess(s -> {
+      client.connect(testAddress, onSuccess(clientHandler::handle));
+    }));
     await();
   }
 
@@ -4117,4 +4173,5 @@ public void testConnectTimeout() {
         testComplete();
       }));
     await();
-  }}
+  }
+}