Repository: netty/netty
Issue: HashedWheelTimer doesn't handle very large values
Labels: defect

### Expected behavior
If I set a timeout with Long.MAX_VALUE (effectively trying to disable it), it should never run.

### Actual behavior
It runs. It runs a lot.

### Steps to reproduce
```
    @Test
    public void testMassiveTimeout() throws InterruptedException {
        final Timer timer = new HashedWheelTimer();
        final CountDownLatch barrier = new CountDownLatch(1);
        final Timeout timeout = timer.newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                fail("This should not have run");
                barrier.countDown();
            }
        }, Long.MAX_VALUE, TimeUnit.SECONDS);
        assertFalse(barrier.await(3, TimeUnit.SECONDS));
        assertFalse("timer should not expire", timeout.isExpired());
        timer.stop();
    }
```

### Netty version

Behaviour observed in 4.1.21.Final
Tested on 4.1 branch from January 2 (062d9269120bac0064408025b78788613d41d36d)

### JVM version (e.g. `java -version`)

openjdk version "1.8.0_151"
OpenJDK Runtime Environment (build 1.8.0_151-8u151-b12-1~deb9u1-b12)
OpenJDK 64-Bit Server VM (build 25.151-b12, mixed mode)


### OS version (e.g. `uname -a`)

Linux eshmun 4.9.0-5-amd64 #1 SMP Debian 4.9.65-3+deb9u2 (2018-01-04) x86_64 GNU/Linux


Issue comments:
@ivankelly this sounds clearly like a bug... Let me check as soon as I have a few cycles or if you like you could also try to provide a PR .
@normanmaurer I had a quick look. The logic looks sound. If I get more time, I'll look again. It's going to be something to do with something being added to turn it negative.
@ivankelly yep sounds like it... looking now
@ivankelly ok I know whats wrong... writing a fix now
:+1: 
@ivankelly https://github.com/netty/netty/pull/7765

Pull request review comments:
@normanmaurer what about the case when someone intentionally passes a negative value into delay?
@johnou hmm good question. I am not sure this is expected but let me add a check as it was "allowed" before.

Patch that resolved the issue:
diff --git a/common/src/main/java/io/netty/util/HashedWheelTimer.java b/common/src/main/java/io/netty/util/HashedWheelTimer.java
index 77d956b12bb..b62ace4695e 100644
--- a/common/src/main/java/io/netty/util/HashedWheelTimer.java
+++ b/common/src/main/java/io/netty/util/HashedWheelTimer.java
@@ -419,6 +419,11 @@ public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
         // Add the timeout to the timeout queue which will be processed on the next tick.
         // During processing all the queued HashedWheelTimeouts will be added to the correct HashedWheelBucket.
         long deadline = System.nanoTime() + unit.toNanos(delay) - startTime;
+
+        // Guard against overflow.
+        if (delay > 0 && deadline < 0) {
+            deadline = Long.MAX_VALUE;
+        }
         HashedWheelTimeout timeout = new HashedWheelTimeout(this, task, deadline);
         timeouts.add(timeout);
         return timeout;


Test patch:
diff --git a/common/src/test/java/io/netty/util/HashedWheelTimerTest.java b/common/src/test/java/io/netty/util/HashedWheelTimerTest.java
index e1a7bccc6ef..2bf1c34819e 100644
--- a/common/src/test/java/io/netty/util/HashedWheelTimerTest.java
+++ b/common/src/test/java/io/netty/util/HashedWheelTimerTest.java
@@ -230,6 +230,21 @@ public void reportPendingTimeouts() throws InterruptedException {
         timer.stop();
     }
 
+    @Test
+    public void testOverflow() throws InterruptedException  {
+        final HashedWheelTimer timer = new HashedWheelTimer();
+        final CountDownLatch latch = new CountDownLatch(1);
+        Timeout timeout = timer.newTimeout(new TimerTask() {
+            @Override
+            public void run(Timeout timeout) {
+                latch.countDown();
+            }
+        }, Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        assertFalse(latch.await(1, TimeUnit.SECONDS));
+        timeout.cancel();
+        timer.stop();
+    }
+
     private static TimerTask createNoOpTimerTask() {
         return new TimerTask() {
             @Override