Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't interrupt tasks of ExecutorService

Edit:

To test this problem outside of Android environment I've created a Java application that creates an ExecutorService, provides a task of AttackScript (identical class) and then terminates.

This works 100% as expected, the thread is interrupted and the task is stopped.

You don't even have to cancel a task by its Future.cancel(true). ExecutorService.shutdownNow() does the job. Is there something in the Android's Service that somehow messes with the thread pool?

Code that works as expcepted:

public static void main(String[] args) {
        AttackScript script = new AttackScript("http://ninjaflex.com/");

        ExecutorService executor = Executors.newFixedThreadPool(5);
        executor.submit(script);
        executor.submit(script);
        executor.submit(script);
        executor.submit(script);

        sleep(1300);

        // Automatically interrupts threads in the pool.
        executor.shutdownNow();
    }

    private static void sleep(long timeMilli){
        try {
            Thread.sleep(timeMilli);
        } catch(Exception e) {
            System.out.println("Error sleep()");
        }
    }

Original post:

I have an Android Service where it includes an ExecutorService field, responsible to run some tasks.

The tasks are objects of the AttackScript class. I cache the Future references in a Map<String,Future>, called tasks, so that I will be able to cancel them later.

Future future = executor.submit(new AttackScript(attack.getWebsite()));
tasks.put(attack.getPushId(), future);

In Service'sonDestroy() (called when user presses a notification button) I am cancelling all tasks

private void cancelAllTasks() {
    for (Map.Entry<String, Future> futureEntry : tasks.entrySet()) {
        futureEntry.getValue().cancel(true);
    }
}

and then shutdown the executor:

private void shutdownThreadPool() {
     // https://www.baeldung.com/java-executor-service-tutorial
     executor.shutdown();
     try {
         if (executor.awaitTermination(800, TimeUnit.MILLISECONDS))
                executor.shutdownNow();
     } catch (InterruptedException e) {
            executor.shutdownNow();
     }
}

Finally here is the AttackScript class:

public class AttackScript implements Runnable {
    private static final String TAG = "AttackScript";
    private URL url;

    public AttackScript(String website) {
        initializeUrl(website);
    }

    private void initializeUrl(String website) {
        try {
            url = new URL(website);
        } catch (MalformedURLException e) {
            Log.e(TAG, "Wrong url?", e);
        }
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            readUrl();
        }
        Log.d(TAG, "Stopped requesting from " + url + " server.");
    }

    private void readUrl() {
        InputStream in = null;
        try {
            in = url.openStream();
        } catch (IOException e) {
            Log.e(TAG, "openStream() error.", e);
        } finally {
            closeInputStream(in);
        }
    }

    private void closeInputStream(InputStream in) {
        try {
            in.close();
            Log.d(TAG, "InputStream closed for " + url);
        } catch (IOException e) {
            Log.e(TAG, "Error while closing the input stream.", e);
        }
    }
}

The weird part is that rarely, like 1 out of 10, tasks are interrupted and AttackScript's execution stops. But the other 9 the tasks were not interrupted, continuing to openStreams() on URLs.

like image 308
Themelis Avatar asked Jan 28 '19 22:01

Themelis


2 Answers

Forced to find an alternative solution I completely removed the use of a thread pool and now implementing single Threads, stored in a Map.

The interruption was, again, never happening so an AtomicBoolean is now controlling the Thread's execution.

private AtomicBoolean stopped = new AtomicBoolean(false);

 @Override
    public void run() {
        while (!stopped.get()) {
            readUrl();
        }
}

public void stopExecution() {
        stopped.set(true);
}

It's a desperate move, but the only one that works so far.

like image 82
Themelis Avatar answered Sep 19 '22 04:09

Themelis


You answered already with a valid workaround to avoid this issue but I will explain the cause. The fault is not with the ExecutorService but with the thread's interrupt status being cleared silently by the network library.

As you and another commenter have found this could very well depend on the particular device you are using and its Android version.

As of Android 4.4 OkHttp is used as the HttpUrlConnection. There is a race condition between when each thread is interrupted and whether or not the InputStream has been closed in older versions.

As a part of the close() call this code is eventually executed:

public void throwIfReached() throws IOException {
    if (Thread.interrupted()) {
        throw new InterruptedIOException("thread interrupted");
    }

    if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
        throw new InterruptedIOException("deadline reached");
    }
}

You can see based on the Thread.interrupted() call it clears the interrupt status of the thread and never sets it again.

To make matters worse it looks like you can instead rely on the InterruptedIOException but that is silently handled internally when closing the stream so you have no chance to handle it.

Your code sample worked for me when using a more recent version of OkHttp. In later versions it looks like more care was taken to keep the interrupt status and it actually works as expected.

However, based on some searching it looks like historically interrupts do not play nicely with OkHttp and to stop a request they recommend Call.cancel() where possible instead.

like image 21
George Mulligan Avatar answered Sep 21 '22 04:09

George Mulligan