Repository: square/okhttp
Issue: okhttp 2.0.0 OkHttpClient.cancel(tag) doesn't work

The Recipes [Canceling a Call](https://github.com/square/okhttp/wiki/Recipes#canceling-a-call) worked, but after I used `OkHttpClient.cancel(tag)` instead, it didn't work.

My code:

``` java
import java.io.IOException;
import java.util.concurrent.*;
import com.squareup.okhttp.*;

public class Test {
    private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    private final OkHttpClient client = new OkHttpClient();

    public void run() throws Exception {
        Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .tag("asdf")
        .build();

        final long startNanos = System.nanoTime();
        final Call call = client.newCall(request);

        // Schedule a job to cancel the call in 1 second.
        executor.schedule(new Runnable() {
            @Override public void run() {
                System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
                client.cancel("asdf");
                System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
            }
        }, 1, TimeUnit.SECONDS);

        try {
            System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
            Response response = call.execute();
            System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
                    (System.nanoTime() - startNanos) / 1e9f, response);
        } catch (IOException e) {
            System.out.printf("%.2f Call failed as expected: %s%n",
                    (System.nanoTime() - startNanos) / 1e9f, e);
        }
    }

    public static void main(String[] args) throws Exception {
        new Test().run();
    }
}
```

Result:

```
0.25 Executing call.
1.25 Canceling call.
1.25 Canceled call.
3.46 Call was expected to fail, but completed: Response{protocol=http/1.1, code=200, message=OK, url=http://httpbin.org/delay/2}
```


Changed files:
okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java, okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java, okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java, okhttp/src/main/java/com/squareup/okhttp/Call.java

diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
index 40519c61d..cefb681be 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
@@ -1172,6 +1172,33 @@ public final class CallTest {
     assertEquals(1, server.getRequestCount());
   }
 
+  @Test public void cancelInFlightBeforeResponseReadThrowsIOE() throws Exception {
+    server.setDispatcher(new Dispatcher() {
+      @Override public MockResponse dispatch(RecordedRequest request) {
+        client.cancel("request");
+        return new MockResponse().setBody("A");
+      }
+    });
+    server.play();
+
+    Request request = new Request.Builder().url(server.getUrl("/a")).tag("request").build();
+    try {
+      client.newCall(request).execute();
+      fail();
+    } catch (IOException e) {
+    }
+  }
+
+  @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTP_2() throws Exception {
+    enableProtocol(Protocol.HTTP_2);
+    cancelInFlightBeforeResponseReadThrowsIOE();
+  }
+
+  @Test public void cancelInFlightBeforeResponseReadThrowsIOE_SPDY_3() throws Exception {
+    enableProtocol(Protocol.SPDY_3);
+    cancelInFlightBeforeResponseReadThrowsIOE();
+  }
+
   /**
    * This test puts a request in front of one that is to be canceled, so that it is canceled before
    * I/O takes place.
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Call.java b/okhttp/src/main/java/com/squareup/okhttp/Call.java
index db2cf6107..785a9db38 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Call.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Call.java
@@ -77,10 +77,20 @@ public class Call {
       if (executed) throw new IllegalStateException("Already Executed");
       executed = true;
     }
-    Response result = getResponse();
-    engine.releaseConnection(); // Transfer ownership of the body to the caller.
-    if (result == null) throw new IOException("Canceled");
-    return result;
+    try {
+      client.getDispatcher().executed(this);
+      Response result = getResponse();
+      System.out.println("releasing");
+      engine.releaseConnection(); // Transfer ownership of the body to the caller.
+      if (result == null) throw new IOException("Canceled");
+      return result;
+    } finally {
+      client.getDispatcher().finished(this);
+    }
+  }
+
+  Object tag() {
+    return request.tag();
   }
 
   /**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
index 0e4644d5b..21f60254a 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
@@ -46,6 +46,9 @@ public final class Dispatcher {
   /** Running calls. Includes canceled calls that haven't finished yet. */
   private final Deque<AsyncCall> runningCalls = new ArrayDeque<>();
 
+  /** In-flight synchronous calls. Includes canceled calls that haven't finished yet. */
+  private final Deque<Call> executedCalls = new ArrayDeque<>();
+
   public Dispatcher(ExecutorService executorService) {
     this.executorService = executorService;
   }
@@ -123,6 +126,12 @@ public final class Dispatcher {
         if (engine != null) engine.disconnect();
       }
     }
+
+    for (Call call : executedCalls) {
+      if (Util.equal(tag, call.tag())) {
+        call.cancel();
+      }
+    }
   }
 
   /** Used by {@code AsyncCall#run} to signal completion. */
@@ -156,4 +165,14 @@ public final class Dispatcher {
     }
     return result;
   }
+
+  /** Used by {@code Call#execute} to signal it is in-flight. */
+  synchronized void executed(Call call) {
+    executedCalls.add(call);
+  }
+
+  /** Used by {@code Call#execute} to signal completion. */
+  synchronized void finished(Call call) {
+    if (!executedCalls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
+  }
 }
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 916062617..69029dfa4 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -467,8 +467,8 @@ public class OkHttpClient implements Cloneable {
   }
 
   /**
-   * Cancels all scheduled tasks tagged with {@code tag}. Requests that are already
-   * complete cannot be canceled.
+   * Cancels all scheduled or in-flight calls tagged with {@code tag}. Requests
+   * that are already complete cannot be canceled.
    */
   public OkHttpClient cancel(Object tag) {
     getDispatcher().cancel(tag);