The interrupt status of a worker thread in the standard ExecutorService implementations will be cleared when a task is completed, and it's ignored for the purposes of testing whether a task was canceled before completion (as detected by Future.isCancelled()).
So, you can safely throw an unchecked ("runtime") exception rather than InterruptedException, and it won't interfere with the operation of the ExecutorService.
However, if you are still concerned, you can preserve the interrupt status by re-asserting it:
if (Thread.interrupted()) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Time limit exceeded.");
}
Or using a different method that doesn't clear it:
if (Thread.currentThread().isInterrupted())
throw new IllegalStateException("Time limit exceeded.");
Normally custom runtime exceptions are a bad idea, because they can result in leaky abstractions, but here, a custom type in place of IllegalStateException may make it cleaner for you to distinguish your timeout exceptions from runtime exceptions raised by the student's code.
Update: Canceling a task running with ExecutorService is only reliable if the task is written correctly to support interruption, and you can't trust students to do that.
So your options are:
- Manually review the source code, inserting interruption detection where necessary.
- Get rid of
ExecutorService and create worker threads yourself. Use the stop() or stop(Throwable) method to terminate the threads after timeout. To be truly robust, use a byte code engineering library like ASM to automate analysis of the compiled plugin code to see if it can catch the Throwable (or ThreadDeath or the custom Throwable) you are using to stop() the worker thread.
- Fork Java processes to run the agent code, and kill the whole process after timeout.