Skip to content

Cancellation of async java.net.http.HttpRequest no longer works when agent is present #14696

@steffan-westcott

Description

@steffan-westcott

Describe the bug

Asynchronous HTTP requests using the JDK-supplied java.net.http.HttpRequest support cancellation. However, if the application is run with the instrumentation agent, the request can no longer be cancelled correctly and instead always runs to completion.

Steps to reproduce

In a new Gradle application, prepare the following files along with file opentelemetry-javaagent.jar:

settings.gradle

rootProject.name = 'req-close-issue'

build.gradle

plugins {
    id 'application'
}

application {
    mainClass = 'org.example.ReqCloseIssue'
    applicationDefaultJvmArgs = ['-javaagent:opentelemetry-javaagent.jar',
                                 '-Dotel.resource.attributes=service.name=req-close-issue-app',
                                 '-Dotel.traces.exporter=none',
                                 '-Dotel.metrics.exporter=none',
                                 '-Dotel.logs.exporter=none']
}

repositories {
    mavenCentral()
}

src/main/java/org/example/ReqCloseIssue.java

package org.example;

import com.sun.net.httpserver.HttpServer;

import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;

public class ReqCloseIssue {

    static synchronized void log(String format, Object... args) {
        System.out.printf(format + "%n", args);
    }

    public static void main(String[] args) throws Exception {

        HttpServer slowServer = HttpServer.create(new InetSocketAddress(8080), 0);
        slowServer.createContext("/", exchange -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ignored) {
            }
            String response = "** message body **";
            exchange.sendResponseHeaders(200, response.length());
            exchange.getResponseBody().write(response.getBytes());
            exchange.close();
        });
        slowServer.setExecutor(Executors.newCachedThreadPool(r -> {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }));

        slowServer.start();

        try (HttpClient client = HttpClient.newHttpClient()) {
            HttpRequest request = HttpRequest.newBuilder(new URI("http://localhost:8080/")).build();
            CompletableFuture<String> future =
                    client.sendAsync(request, BodyHandlers.ofString())
                            .thenApply(HttpResponse::body)
                            .whenComplete((res, ex) -> {
                                if (ex == null) {
                                    log("Request was not cancelled; got response %s", res);
                                } else {
                                    log("Request was cancelled with %s", ex.getCause());
                                }
                            })
                            .exceptionally(ex -> "** cancelled **");
            Thread.sleep(200); // request will still be in progress at end of sleep
            future.cancel(true);
            log("Got response: %s", future.get()); // throws exception when agent present
        } finally {
            slowServer.stop(0);
        }
    }
}

Build and run the application with the following command:

./gradlew run

Expected behavior

The following should be printed to the terminal:

Request was cancelled with java.util.concurrent.CancellationException: Request cancelled
Got response: ** cancelled **

Actual behavior

The following is printed to the terminal:

Request was not cancelled; got response ** message body **
Exception in thread "main" java.util.concurrent.CancellationException
        at java.base/java.util.concurrent.CompletableFuture.cancel(CompletableFuture.java:2510)
        at org.example.ReqCloseIssue.main(ReqCloseIssue.java:55)

Javaagent or library instrumentation version

opentelemetry-javaagent version 2.20.0

Environment

JDK: OpenJDK Runtime Environment Temurin-21.0.1+12
OS: Ubuntu 22.04.5 LTS

Additional context

The above shows that adding the agent changes the application behaviour

Tip

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingneeds triageNew issue that requires triage

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions