Est-il possible de forcer Java à générer une exception après l'exécution d'un bloc de code plus long qu'acceptable?
Oui, mais c’est généralement une très mauvaise idée de forcer un autre thread à s’interrompre sur une ligne de code aléatoire. Vous ne feriez cela que si vous avez l'intention d'arrêter le processus.
Ce que vous pouvez faire est d'utiliser Thread.interrupt()
pour une tâche après un certain temps. Cependant, à moins que le code ne vérifie cela, cela ne fonctionnera pas. Un ExecutorService peut rendre cela plus facile avec Future.cancel(true)
C'est beaucoup mieux pour que le code se chronomètre et s'arrête quand il le faut.
Voici le moyen le plus simple que je connaisse pour le faire:
final Runnable stuffToDo = new Thread() {
@Override
public void run() {
/* Do stuff here. */
}
};
final ExecutorService executor = Executors.newSingleThreadExecutor();
final Future future = executor.submit(stuffToDo);
executor.shutdown(); // This does not cancel the already-scheduled task.
try {
future.get(5, TimeUnit.MINUTES);
}
catch (InterruptedException ie) {
/* Handle the interruption. Or ignore it. */
}
catch (ExecutionException ee) {
/* Handle the error. Or ignore it. */
}
catch (TimeoutException te) {
/* Handle the timeout. Or ignore it. */
}
if (!executor.isTerminated())
executor.shutdownNow(); // If you want to stop the code that hasn't finished.
Sinon, vous pouvez créer une classe TimeLimitedCodeBlock pour envelopper cette fonctionnalité, puis l'utiliser où vous le souhaitez, comme suit:
new TimeLimitedCodeBlock(5, TimeUnit.MINUTES) { @Override public void codeBlock() {
// Do stuff here.
}}.run();
J'ai compilé certaines des autres réponses en une seule méthode utilitaire:
public class TimeLimitedCodeBlock {
public static void runWithTimeout(final Runnable runnable, long timeout, TimeUnit timeUnit) throws Exception {
runWithTimeout(new Callable<Object>() {
@Override
public Object call() throws Exception {
runnable.run();
return null;
}
}, timeout, timeUnit);
}
public static <T> T runWithTimeout(Callable<T> callable, long timeout, TimeUnit timeUnit) throws Exception {
final ExecutorService executor = Executors.newSingleThreadExecutor();
final Future<T> future = executor.submit(callable);
executor.shutdown(); // This does not cancel the already-scheduled task.
try {
return future.get(timeout, timeUnit);
}
catch (TimeoutException e) {
//remove this if you do not want to cancel the job in progress
//or set the argument to 'false' if you do not want to interrupt the thread
future.cancel(true);
throw e;
}
catch (ExecutionException e) {
//unwrap the root cause
Throwable t = e.getCause();
if (t instanceof Error) {
throw (Error) t;
} else if (t instanceof Exception) {
throw (Exception) t;
} else {
throw new IllegalStateException(t);
}
}
}
}
Exemple de code utilisant cette méthode d’utilitaire:
public static void main(String[] args) throws Exception {
final long startTime = System.currentTimeMillis();
log(startTime, "calling runWithTimeout!");
try {
TimeLimitedCodeBlock.runWithTimeout(new Runnable() {
@Override
public void run() {
try {
log(startTime, "starting sleep!");
Thread.sleep(10000);
log(startTime, "woke up!");
}
catch (InterruptedException e) {
log(startTime, "was interrupted!");
}
}
}, 5, TimeUnit.SECONDS);
}
catch (TimeoutException e) {
log(startTime, "got timeout!");
}
log(startTime, "end of main method!");
}
private static void log(long startTime, String msg) {
long elapsedSeconds = (System.currentTimeMillis() - startTime);
System.out.format("%1$5sms [%2$16s] %3$s\n", elapsedSeconds, Thread.currentThread().getName(), msg);
}
Résultat de l’exécution du code exemple sur ma machine:
0ms [ main] calling runWithTimeout!
13ms [ pool-1-thread-1] starting sleep!
5015ms [ main] got timeout!
5016ms [ main] end of main method!
5015ms [ pool-1-thread-1] was interrupted!
Si vous voulez chronométrer le code de test, vous pouvez utiliser l'attribut time
:
@Test(timeout = 1000)
public void shouldTakeASecondOrLess()
{
}
S'il s'agit d'un code de production, il n'existe pas de mécanisme simple et la solution à utiliser dépend de la possibilité de modifier ou non le code.
Si vous pouvez modifier le code chronométré, une approche simple consiste à ce que votre code chronométré se souvienne de son heure de début et périodiquement de l'heure actuelle. Par exemple.
long startTime = System.currentTimeMillis();
// .. do stuff ..
long elapsed = System.currentTimeMillis()-startTime;
if (elapsed>timeout)
throw new RuntimeException("tiomeout");
Si le code lui-même ne peut pas vérifier le délai d'expiration, vous pouvez l'exécuter sur un autre thread et attendre son achèvement ou le délai d'expiration.
Callable<ResultType> run = new Callable<ResultType>()
{
@Override
public ResultType call() throws Exception
{
// your code to be timed
}
};
RunnableFuture future = new FutureTask(run);
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(future);
ResultType result = null;
try
{
result = future.get(1, TimeUnit.SECONDS); // wait 1 second
}
catch (TimeoutException ex)
{
// timed out. Try to stop the code if possible.
future.cancel(true);
}
service.shutdown();
}
EDIT: Peter Lawrey a tout à fait raison: ce n'est pas aussi simple que d'interrompre un fil (ma suggestion initiale), et Executors & Callables sont très utiles ...
Plutôt que d'interrompre les threads, vous pouvez définir une variable sur l'appelable une fois le délai d'attente écoulé. L'appelable doit vérifier cette variable à des moments appropriés de l'exécution de la tâche pour savoir quand s'arrêter.
Les Callables renvoient des contrats à terme, avec lesquels vous pouvez spécifier un délai d'attente lorsque vous essayez d'obtenir le résultat du futur. Quelque chose comme ça:
try {
future.get(timeoutSeconds, TimeUnit.SECONDS)
} catch(InterruptedException e) {
myCallable.setStopMeAtAppropriatePlace(true);
}
Voir Future.get, Executors et Callable ...
https://docs.Oracle.com/javase/8/docs/api/Java/util/concurrent/Callable.html
Je peux suggérer deux options.
Dans la méthode, en supposant qu'il boucle et n'attend pas un événement externe, ajoutez un champ local et testez l'heure à chaque fois autour de la boucle.
void method() {
long endTimeMillis = System.currentTimeMillis() + 10000;
while (true) {
// method logic
if (System.currentTimeMillis() > endTimeMillis) {
// do some clean-up
return;
}
}
}
Exécutez la méthode dans un thread et demandez à l'appelant de compter jusqu'à 10 secondes.
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
method();
}
});
thread.start();
long endTimeMillis = System.currentTimeMillis() + 10000;
while (thread.isAlive()) {
if (System.currentTimeMillis() > endTimeMillis) {
// set an error flag
break;
}
try {
Thread.sleep(500);
}
catch (InterruptedException t) {}
}
L'inconvénient de cette approche est que method () ne peut pas renvoyer une valeur directement, elle doit mettre à jour un champ d'instance pour renvoyer sa valeur.
J'ai créé une solution très simple sans utiliser de frameworks ou d'API. Cela semble plus élégant et compréhensible. La classe s'appelle TimeoutBlock.
public class TimeoutBlock {
private final long timeoutMilliSeconds;
private long timeoutInteval=100;
public TimeoutBlock(long timeoutMilliSeconds){
this.timeoutMilliSeconds=timeoutMilliSeconds;
}
public void addBlock(Runnable runnable) throws Throwable{
long collectIntervals=0;
Thread timeoutWorker=new Thread(runnable);
timeoutWorker.start();
do{
if(collectIntervals>=this.timeoutMilliSeconds){
timeoutWorker.stop();
throw new Exception("<<<<<<<<<<****>>>>>>>>>>> Timeout Block Execution Time Exceeded In "+timeoutMilliSeconds+" Milli Seconds. Thread Block Terminated.");
}
collectIntervals+=timeoutInteval;
Thread.sleep(timeoutInteval);
}while(timeoutWorker.isAlive());
System.out.println("<<<<<<<<<<####>>>>>>>>>>> Timeout Block Executed Within "+collectIntervals+" Milli Seconds.");
}
/**
* @return the timeoutInteval
*/
public long getTimeoutInteval() {
return timeoutInteval;
}
/**
* @param timeoutInteval the timeoutInteval to set
*/
public void setTimeoutInteval(long timeoutInteval) {
this.timeoutInteval = timeoutInteval;
}
}
exemple :
try {
TimeoutBlock timeoutBlock = new TimeoutBlock(10 * 60 * 1000);//set timeout in milliseconds
Runnable block=new Runnable() {
@Override
public void run() {
//TO DO write block of code to execute
}
};
timeoutBlock.addBlock(block);// execute the runnable block
} catch (Throwable e) {
//catch the exception here . Which is block didn't execute within the time limit
}
C'était tellement utile pour moi quand je devais me connecter à un compte FTP. Ensuite, téléchargez et téléchargez des choses. parfois, la connexion FTP est suspendue ou totalement interrompue. Cela a provoqué la panne de tout le système. et j'avais besoin d'un moyen de le détecter et de l'empêcher de se produire. Alors j'ai créé ça et je l'ai utilisé. Fonctionne assez bien.
Au lieu d'avoir la tâche dans le nouveau thread et le minuteur dans le thread principal, placez le minuteur dans le nouveau thread et la tâche dans le thread principal:
public static class TimeOut implements Runnable{
public void run() {
Thread.sleep(10000);
if(taskComplete ==false) {
System.out.println("Timed Out");
return;
}
else {
return;
}
}
}
public static boolean taskComplete = false;
public static void main(String[] args) {
TimeOut timeOut = new TimeOut();
Thread timeOutThread = new Thread(timeOutThread);
timeOutThread.start();
//task starts here
//task completed
taskComplete =true;
while(true) {//do all other stuff }
}
Il y a une façon hacky de le faire.
Définissez un champ booléen pour indiquer si le travail est terminé. Ensuite, avant le bloc de code, configurez une minuterie pour exécuter un morceau de code après votre délai d'attente. Le minuteur vérifiera si le bloc de code a fini de s’exécuter et sinon, lève une exception. Sinon, cela ne fera rien.
La fin du bloc de code doit bien sûr définir le champ sur true pour indiquer que le travail a été effectué.
J'ai rencontré un problème similaire dans lequel ma tâche était d'envoyer un message à SQS dans un délai donné. J'ai utilisé la logique triviale de l'exécuter via un autre thread et d'attendre son futur objet en spécifiant le délai d'expiration. Cela me donnerait une exception TIMEOUT en cas de dépassement de délai.
final Future<ISendMessageResult> future =
timeoutHelperThreadPool.getExecutor().submit(() -> {
return getQueueStore().sendMessage(request).get();
});
try {
sendMessageResult = future.get(200, TimeUnit.MILLISECONDS);
logger.info("SQS_Push_SUCCESSFUL");
return true;
} catch (final TimeoutException e) {
logger.error("SQS_Push_TIMEOUT_EXCEPTION");
}
Mais il existe des cas où vous ne pouvez pas empêcher le code d'être exécuté par un autre thread et vous obtenez de vrais négatifs dans ce cas.
Par exemple - Dans mon cas, ma demande a atteint SQS et, lors de la transmission du message, la logique de mon code a rencontré le délai d'expiration spécifié. Maintenant, en réalité, mon message a été placé dans la file d'attente mais mon thread principal a supposé que son échec était dû à l'exception TIMEOUT. C'est un type de problème qui peut être évité plutôt que d'être résolu. Comme dans mon cas, je l'ai évité en prévoyant un délai qui suffirait dans presque tous les cas.
Si le code que vous souhaitez interrompre se trouve dans votre application et ne ressemble pas à un appel d'API, vous pouvez simplement utiliser
future.cancel(true)
Cependant, rappelez-vous que Java Docs dit que cela garantit que l'exécution sera bloquée.
"Tente d'annuler l'exécution de cette tâche. Cette tentative échouera si la tâche est déjà terminée, a déjà été annulée ou ne peut pas être annulée pour une autre raison. En cas de succès, et cette tâche n'a pas démarré cancel est appelé, cette tâche ne doit jamais être exécutée. Si la tâche a déjà démarré, le paramètre mayInterruptIfRunning détermine si le thread qui l'exécute doit être interrompu pour tenter de l'arrêter. "
Si vous voulez une méthode CompletableFuture, vous pourriez avoir une méthode comme
public MyResponseObject retrieveDataFromEndpoint() {
CompletableFuture<MyResponseObject> endpointCall
= CompletableFuture.supplyAsync(() ->
yourRestService.callEnpoint(withArg1, withArg2));
try {
return endpointCall.get(10, TimeUnit.MINUTES);
} catch (TimeoutException
| InterruptedException
| ExecutionException e) {
throw new RuntimeException("Unable to fetch data", e);
}
}
Si vous utilisez spring, vous pouvez annoter la méthode avec un @Retryable
afin qu'il la répète trois fois si une exception est levée.