Unexpected exception java.nio.channels.ClosedByInterruptException when deleting from Artifactory

Hi all,
In my Jenkins shared lib, I’ve created a class called ArtifactManager which performs docker cleanup from Artifactory when branch is deleted.

When there is a really massive directory of docker images to delete (~50 GB), I’m getting an unexpected interrupt:

java.nio.channels.ClosedByInterruptException
	at java.base/java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:199)
	at java.base/sun.nio.ch.FileChannelImpl.endBlocking(FileChannelImpl.java:162)
	at java.base/sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:285)
	at org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverWriter.<init>(RiverWriter.java:109)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.saveProgram(CpsThreadGroup.java:560)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.saveProgram(CpsThreadGroup.java:537)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.saveProgramIfPossible(CpsThreadGroup.java:520)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.run(CpsThreadGroup.java:444)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.access$400(CpsThreadGroup.java:97)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:315)
	at org.jenkinsci.plugins.workflow.cps.CpsThreadGroup$2.call(CpsThreadGroup.java:279)
	at org.jenkinsci.plugins.workflow.cps.CpsVmExecutorService$2.call(CpsVmExecutorService.java:67)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at hudson.remoting.SingleLaneExecutorService$1.run(SingleLaneExecutorService.java:139)
	at jenkins.util.ContextResettingExecutorService$1.run(ContextResettingExecutorService.java:28)
	at jenkins.security.ImpersonatingExecutorService$1.run(ImpersonatingExecutorService.java:68)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)
Finished: FAILURE

My code is not included in the stack trace…
The code which performs the HTTP requests is:

    def deleteArtifact(String pathOnServer){
        def responseCode, data
        Boolean deleted = true
        Logger.printInfo(steps, "Deleting $server/$pathOnServer artifact from server")
        steps.withCredentials([steps.usernamePassword(credentialsId: 'artifactory_user', passwordVariable: 'PASSWORD', usernameVariable: 'USERNAME')]){
            (responseCode, data) = sendApiRequest("$server/$pathOnServer", "DELETE")
        }
        //if !2xxSuccessful() -> Http response codes family - 1xx: Informational, 2xx: Success, 3xx: Redirection, 4xx: Client Error, 5xx: Server Error
        if((responseCode / 100 as int) != 2){
            deleted = false
            Logger.printError(steps, "Failed to delete: `$pathOnServer` from `$server`. Status Code: $responseCode")
            Logger.printError(steps, "Data: $data")
        }
        return deleted
    }

    def sendApiRequest(String query, String httpMethod, String contentType ="", String data =""){
        def responseCode
        def responseData
        def conn = new URL("${this.protocol}://$query").openConnection()

        //Trying to increase timeout
        conn.setConnectTimeout(15 * 60 *60 * 1000);
        conn.setReadTimeout(15 * 60 *60 * 1000);

        def auth = "${steps.env.USERNAME}:${steps.env.PASSWORD}".getBytes().encodeBase64().toString()
        conn.setRequestProperty("Authorization", "Basic ${auth}")

        conn.setRequestMethod(httpMethod)
        if(contentType) conn.setRequestProperty( "Content-Type", contentType); 
        if(data){
            conn.setDoOutput(true)
            conn.getOutputStream().write(data.getBytes("UTF-8"));
            //Note: the POST will start when you try to read a value from the HttpURLConnection, such as responseCode, inputStream.text, or getHeaderField('...'). (https://stackoverflow.com/a/47489805/10025322)
        }
        responseCode = conn.getResponseCode()
        try{
            responseData = conn.getInputStream().getText()
        }catch(IOException e){
            responseData = e.getMessage()
        }
        conn = null
        return [responseCode, responseData]
    }

I also tried to use a different library for performing the request but still getting the exception when Artifactory responses slowly (Delete huge directory):

   def sendApiRequest(String query, String httpMethod, String contentType ="", String data =""){
        def responseCode
        def responseData
        def http = new HTTPBuilder("${this.protocol}://$query")
        http.request(Method.valueOf(httpMethod)) {
            headers.'Authorization' = "Basic ${steps.env.USERNAME}:${steps.env.PASSWORD}".getBytes().encodeBase64().toString()
            if (contentType) {
                headers.'Content-Type' = contentType
                requestContentType = contentType
            }
            if (data) {
                body = data
            }
            response.success = { resp, reader ->
                responseCode = resp.statusLine.statusCode
                responseData = reader.text
            }
            response.failure = { resp, reader ->
                responseCode = resp.statusLine.statusCode
                responseData = reader.text
            }
        }
        echo("RESPONSE_CODE: " + responseCode.toString() + " RESPONSE_DATA: " + responseData.toString())
        return [responseCode, responseData]
    }

I found that the ClosedByInterruptException may occur due to the following reasons:

  1. The thread running the code was interrupted.
  2. The connection to the remote server was closed due to a network error.
  3. The server explicitly closed the connection.
  4. The read/write operation timed out.
  5. There was an interruption or failure in the underlying I/O operations.
  6. The JVM was shut down while the operation was in progress.

Any idea?

All that code runs on the controller. You may want to switch to a shell statement so jenkins doesn’t become unresponsive/think the controller has timed out or something.

1 Like

I thought about moving to curl but I have no idea how to get the response’s status code, and data separately.
Any idea?

The provided code doesn’t work due to a bug in Jenkins’ core (this issue was marked as fixed in jenkins >= 2.332.1LTS or 2.335)

As a workaround I can use:

def sendApiRequest(String query, String httpMethod, String contentType ="", String data =""){
    /*
        Caller must wrap function call with 'withCredentials' statement 
        with 'USERNAME' and 'PASSWORD' environment varaiables
    */
    String apiCmd = """curl -s -w '###%{http_code}' -X '${httpMethod}' \
                '${this.protocol}://$query' \
                ${contentType ? "-H 'Content-Type: $contentType'" : ''} \
                -u "\$USERNAME:\$PASSWORD" \
                ${data ? "-d '${data}'" : ''}"""


    def response = steps.sh(returnStdout: true, script: apiCmd, label: "Send API request to Artifactory").trim()
    
    def responseCode = response.split("###")[1]
    def responseData = response.split("###")[0]

    try{
        responseCode = responseCode.toInteger()
    } catch (NumberFormatException e) {
        throw new RuntimeException("Invalid status code: `$responseCode` cannot cast to integer")
    }

    return [responseCode, responseData]
}