I was surprised to find Java concurrency timeouts do not stop blocked socket read operation in the thread.
I was using Selenium RemoteWebDriver to simulate a load test scenario. I have wrapped the execution of Selenium commands in a Callable
and used get()
method with the timeout parameter. It works perfectly when there are less than 3 concurrent threads executing but the situation deteriorates when there are 4 or more concurrent threads. Some of the threads get stuck at socket read and are stuck for longer than the timeout setting on the Callable
.
I did some research online, the root cause was a hard-coded socket timeout of 3 hours in Selenium code. There used to be a hack to overwrite the setting using reflection but with the latest version I don't think it's hackable any more.
I wonder whether there is a way to stop the thread that is IO blocked externally since I don't want to change Selenium code and end up having to maintain my own version of Selenium.
Here's how I handle the thread timeout in my code:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Long> future = null;
try {
future = executorService.submit(new Callable<Long>() {
@Override
public Long call() throws Exception {
return commandProcessor.process(row);
}
});
timeTaken = future.get(currentTimeout, TimeUnit.MILLISECONDS);
executorService.shutdown();
} catch (Exception e) {
log.error(e.getMessage(), e);
// cancel the task
if (future != null && !future.isDone()) {
future.cancel(true);
}
executorService.shutdownNow();
}
And since it fails to timeout randomly, I even created another daemon thread to monitor this thread and shut it down from the outside, but it still fails to terminate the thread when socket read blocks.
In the try block:
TimeoutDaemon timeoutDaemon = new TimeoutDaemon(future, executorService, timeStarted);
// put a daemon on the main execution thread so it behaves
ExecutorService daemonExecutorService = Executors.newSingleThreadExecutor();
daemonExecutorService.submit(timeoutDaemon);
The TimeoutDaemon
class:
private class TimeoutDaemon implements Runnable {
private Future<?> future;
private long timeStarted;
private ExecutorService taskExecutorService;
private TimeoutDaemon(Future<?> future, ExecutorService taskExecutorService, long timeStarted) {
this.future = future;
this.timeStarted = timeStarted;
this.taskExecutorService = taskExecutorService;
}
@Override
public void run() {
boolean running = true;
while (running) {
long currentTime = System.currentTimeMillis();
if (currentTime - timeStarted > currentTimeout + 1000) {
running = false;
if (!future.isDone()) {
String message = "Command execution is taking longer (%d ms) than the current timeout setting %d. Canceling the execution.";
message = String.format(message, currentTime - timeStarted, currentTimeout);
taskExecutorService.shutdownNow();
}
} else {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
log.error("Timeout Daemon interrupted. Test may be stuck. Close stuck browser windows if any.", e);
}
}
}
}
}