73 votes

Planification de l'exception de gestion du service d'exécution en attente

J'utilise ScheduledExecutorService pour exécuter une méthode périodiquement.

Code:

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture handle =
        scheduler.scheduleWithFixedDelay(new Runnable() {
             public void run() { 
                 //Effectuer la logique métier, une exception peut se produire
             }
        }, 1, 10, TimeUnit.SECONDS);

Ma question:

Comment continuer le planificateur, si run() lance une exception? Dois-je capturer toutes les exceptions dans la méthode run()? Ou existe-t-il une méthode de rappel intégrée pour gérer l'exception? Merci!

144voto

Basil Bourque Points 8938

Tl;dr

Toute exception échappant à votre run méthode arrête tout travail ultérieur sans préavis.

Utilisez toujours un try-catch au sein de votre run méthode. Essayez de récupérer si vous voulez que l'activité programmée continue.

@Override
public void run ()
{
    try {
        doChore();
    } catch ( Exception e ) { 
        logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + t.getStackTrace() );
    }
}

Le problème

La question fait référence à l'astuce critique avec un ScheduledExecutorService : Toute exception ou erreur lancée atteignant l'exécuteur provoque l'arrêt de ce dernier. Plus d'invocations sur le Runnable, plus de travail effectué. Cet arrêt de travail se produit silencieusement, vous n'en serez pas informé. Ce vilain langage affichage du blog raconte de façon divertissante la manière difficile d'apprendre ce comportement.

La solution

El réponse de yegor256 y el réponse de arun_suresh Les deux semblent être fondamentalement correctes. Deux problèmes avec ces réponses :

  • Attraper les erreurs ainsi que les exceptions
  • Un peu compliqué

Erreurs y Exceptions ?

En Java, nous n'attrapons normalement que exceptions pas erreurs . Mais dans ce cas particulier de ScheduledExecutorService, ne pas attraper l'un ou l'autre signifiera un arrêt de travail. Vous pouvez donc vouloir attraper les deux. Je ne suis pas sûr à 100% de cela, ne connaissant pas toutes les implications de la capture de toutes les erreurs. Veuillez me corriger si nécessaire.

Une des raisons pour lesquelles il faut attraper les erreurs et les exceptions peut impliquer l'utilisation de bibliothèques dans votre tâche. Voir le commentaire de jannis .

Une façon d'attraper à la fois les exceptions et les erreurs est d'attraper leur superclasse, Jetable pour un exemple.

} catch ( Throwable t ) {

plutôt que

} catch ( Exception e ) {

L'approche la plus simple : Il suffit d'ajouter un Try-Catch

Mais les deux réponses sont un peu compliquées. Pour mémoire, je vais montrer la solution la plus simple :

Enveloppez toujours le code de votre Runnable dans un Try-Catch pour attraper toutes les exceptions. y erreurs.

Syntaxe Lambda

Avec un lambda (en Java 8 et plus).

final Runnable someChoreRunnable = () -> {
    try {
        doChore();
    } catch ( Throwable t ) {  // Catch Throwable rather than Exception (a subclass).
        logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + t.getStackTrace() );
    }
};

Syntaxe à l'ancienne

A l'ancienne, avant les lambdas.

final Runnable someChoreRunnable = new Runnable()
{
    @Override
    public void run ()
    {
        try {
            doChore();
        } catch ( Throwable t ) {  // Catch Throwable rather than Exception (a subclass).
            logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + t.getStackTrace() );
        }
    }
};

Dans chaque Runnable/Callable

Indépendamment d'un ScheduledExecutorService il me semble judicieux de toujours utiliser une formule générale de try-catch( Exception† e ) sur tout run méthode d'un Runnable . Idem pour tout call méthode d'un Callable .


Exemple complet de code

Dans un travail réel, je définirais probablement la Runnable séparément plutôt qu'imbriqués. Mais cela permet de faire un bel exemple tout-en-un.

package com.basilbourque.example;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 *  Demo `ScheduledExecutorService`
 */
public class App {
    public static void main ( String[] args ) {
        App app = new App();
        app.doIt();
    }

    private void doIt () {

        // Demonstrate a working scheduled executor service.
        // Run, and watch the console for 20 seconds.
        System.out.println( "BASIL - Start." );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        ScheduledFuture < ? > handle =
                scheduler.scheduleWithFixedDelay( new Runnable() {
                    public void run () {
                        try {
                            // doChore ;   // Do business logic.
                            System.out.println( "Now: " + ZonedDateTime.now( ZoneId.systemDefault() ) );  // Report current moment.
                        } catch ( Exception e ) {
                            // … handle exception/error. Trap any unexpected exception here rather to stop it reaching and shutting-down the scheduled executor service.
                            // logger.error( "Caught exception in ScheduledExecutorService. StackTrace:\n" + e.getStackTrace() );
                        }   // End of try-catch.
                    }   // End of `run` method.
                } , 0 , 2 , TimeUnit.SECONDS );

        // Wait a long moment, for background thread to do some work.
        try {
            Thread.sleep( TimeUnit.SECONDS.toMillis( 20 ) );
        } catch ( InterruptedException e ) {
            e.printStackTrace();
        }

        // Time is up. Kill the executor service and its thread pool.
        scheduler.shutdown();

        System.out.println( "BASIL - Done." );

    }
}

Quand il est exécuté.

BASIL - Démarrage.

Now: 2018-04-10T16:46:01.423286-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:03.449178-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:05.450107-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:07.450586-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:09.456076-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:11.456872-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:13.461944-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:15.463837-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:17.469218-07:00[America/Los_Angeles]

Now: 2018-04-10T16:46:19.473935-07:00[America/Los_Angeles]

BASIL - Fait.


† Ou peut-être Throwable au lieu de Exception pour attraper Error également des objets.

3 votes

@ Personne ayant voté négativement... Veuillez laisser un commentaire critique avec votre vote.

4 votes

Ce billet de blog a fait ma journée.

3 votes

Pour fournir un cas réel de capture d'une Throwable plutôt que d'une Exception : je viens de rencontrer un problème avec une bibliothèque tierce qui effectue une vérification périodique de l'état d'un service. Cette bibliothèque utilise un ScheduledExecutorService en interne. Je continuais à recevoir des délais d'attente de la vérification de l'état. J'ai lu l'article de blog lié et j'ai compris que le planificateur pouvait absorber les erreurs. J'ai donc ajouté un bloc try-catch pour capturer l'exception, mais cela n'a pas aidé. Il s'est avéré (après une semaine environ d'investigation) qu'il s'agissait d'un problème de dépendance qui lançait un NoSuchMethodError qui est une Error.

33voto

arun_suresh Points 1964

Vous devriez utiliser l'objet ScheduledFuture retourné par votre scheduler.scheduleWithFixedDelay(...) de la manière suivante :

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture handle =
        scheduler.scheduleWithFixedDelay(new Runnable() {
             public void run() { 
                 throw new RuntimeException("foo");
             }
        }, 1, 10, TimeUnit.SECONDS);

// Créez et lancez un fil d'exception
// passez l'objet "handle" au fil
// À l'intérieur du fil de gestion, faites :
....
try {
  handle.get();
} catch (ExecutionException e) {
  Exception rootException = e.getCause();
}

11 votes

@arun_suresh Je ne comprends pas. Il me semble que votre try-catch s'exécutera une fois immédiatement. Mais votre tâche planifiée s'exécutera de manière répétée, toutes les 10 secondes. Comment votre code attrape-t-il les exceptions données par les exécutions ultérieures ultérieures?

1 votes

@BasilBourque Il n'y a pas d'exécutions ultérieures. Une fois qu'une exception est levée, le travail sera arrêté. Ce code ne fait que s'assurer que cela ne se produit pas silencieusement, afin que vous sachiez quand et pourquoi cela s'est produit, si cela se produit. La seule façon de continuer à exécuter les tâches est d'entourer tout cela avec un bloc try/catch.

3 votes

Je suppose que cela fonctionnerait, mais cela nécessite que vous ayez un thread supplémentaire qui reste bloqué sur handle.get(). Ce serait mieux si cela pouvait être géré à l'intérieur du thread du service d'exécution lui-même.

8voto

davidh Points 107

Ancienne question mais la réponse acceptée ne donne pas d'explications et fournit un mauvais exemple et la réponse la plus votée est en partie correcte mais encourage finalement à ajouter des exceptions catch dans chaque méthode Runnable.run().
Je suis en désaccord car :

  • ce n'est pas propre : il n'est pas standard pour une tâche de capturer ses propres exceptions.
  • ce n'est pas robuste : une nouvelle sous-classe Runnable pourrait oublier d'effectuer la capture d'exception et la reprise associée.
  • cela contrevient au faible couplage promu par les tâches, car cela couple les tâches à exécuter avec la façon de gérer le résultat de la tâche.
  • cela mélange les responsabilités : ce n'est pas la responsabilité de la tâche de gérer l'exception ou de communiquer l'exception à l'appelant. Une tâche est quelque chose à exécuter.

Je pense que la propagation des exceptions devrait être effectuée par le framework ExecutorService et en fait il offre cette fonctionnalité.
De plus, essayer d'être trop intelligent en essayant de court-circuiter la façon de fonctionner de ExecutorService n'est pas une bonne idée non plus : le framework peut évoluer et vous voulez l'utiliser de manière standard.
Enfin, laisser le framework ExecutorService faire son travail ne signifie pas forcément interrompre les invocations de tâches ultérieures.
Si une tâche planifiée rencontre un problème, il incombe à l'appelant de re-planifier ou non la tâche en fonction de la cause du problème.
Chaque couche a ses responsabilités. Maintenir celles-ci rend le code à la fois clair et maintenable.


ScheduledFuture.get() : le bon API pour capturer les exceptions et erreurs survenues dans la tâche

ScheduledExecutorService.scheduleWithFixedDelay()/scheduleAtFixRate() précisent dans leur spécification :

Si une quelconque exécution de la tâche rencontre une exception, les exécutions ultérieures sont supprimées. Sinon, la tâche ne se terminera que par une annulation ou une terminaison de l'exécuteur.

Cela signifie que ScheduledFuture.get() ne retourne pas à chaque invocation planifiée mais qu'il retourne pour la dernière invocation de la tâche, c'est-à-dire une annulation de tâche : provoquée par ScheduledFuture.cancel() ou une exception lancée dans la tâche.
Ainsi, manipuler le retour de ScheduledFuture pour capturer l'exception avec ScheduledFuture.get() semble correct :

  try {
    future.get();

  } catch (InterruptedException e) {
    // ... à gérer
  } catch (ExecutionException e) {
    // ... et déballer l'exception ou l'erreur à l'origine du problème
    Throwable cause = e.getCause();       
  }

Exemple avec le comportement par défaut : arrêter la planification si l'une des exécutions de la tâche rencontre un problème

Il exécute une tâche qui, pour la troisième exécution, lance une exception et met fin à la planification. Dans certains scénarios, nous voulons cela.

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ScheduledExecutorServiceWithException {

  public static void main(String[] args) {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    // variable utilisée pour lancer une erreur à la 3ème invocation de tâche
    AtomicInteger countBeforeError = new AtomicInteger(3);

    // booléen permettant de laisser le client arrêter ou non la tâche de planification après un échec
    Future futureA = executor
        .scheduleWithFixedDelay(new MyRunnable(countBeforeError), 1, 2, TimeUnit.SECONDS);
    try {
      System.out.println("avant get()");
      futureA.get(); // ne retournera que si annulé
      System.out.println("après get()");
    } catch (InterruptedException e) {
      // gérer cela : arrêter ou ne pas arrêter
    } catch (ExecutionException e) {
      System.out.println("exception attrapée :" + e.getCause());
    }

    // arrêter le service executorservice
    executor.shutdown();
  }

  private static class MyRunnable implements Runnable {

    private final AtomicInteger invocationDone;

    public MyRunnable(AtomicInteger invocationDone) {
      this.invocationDone = invocationDone;
    }

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + ", exécution");
      if (invocationDone.decrementAndGet() == 0) {
        throw new IllegalArgumentException("ohhh une Exception dans MyRunnable");
      }
    }
  }
}

Sortie :

avant get()
pool-1-thread-1, execution
pool-1-thread-1, execution
pool-1-thread-1, execution
exception attrapée : java.lang.IllegalArgumentException: ohhh une Exception dans MyRunnable

Exemple avec la possibilité de poursuivre la planification si l'une des exécutions de la tâche rencontre un problème

Il exécute une tâche qui lance une exception aux deux premières exécutions et une erreur à la troisième. Nous pouvons voir que le client des tâches peut choisir d'arrêter ou non la planification : ici je continue en cas d'exception et je m'arrête en cas d'erreur.

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ScheduledExecutorServiceWithException {

  public static void main(String[] args) {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    // variable utilisée pour lancer une erreur à la 3ème invocation de tâche
    AtomicInteger countBeforeError = new AtomicInteger(3);

    // booléen permettant de laisser le client arrêter ou non la tâche de planification après un échec
    boolean mustHalt = true;
    do {
      Future futureA = executor
              .scheduleWithFixedDelay(new MyRunnable(countBeforeError), 1, 2, TimeUnit.SECONDS);
      try {
        futureA.get(); // ne retournera que si annulé
      } catch (InterruptedException e) {
        // gérer cela : arrêter ou ne pas arrêter
      } catch (ExecutionException e) {
        if (e.getCause() instanceof Error) {
          System.out.println("Je m'arrête en cas d'Erreur");
          mustHalt = true;
        } else {
          System.out.println("Je replanifie en cas d'Exception");
          mustHalt = false;
        }
      }
    }
    while (!mustHalt);
    // arrêter le service executorservice
    executor.shutdown();
  }

  private static class MyRunnable implements Runnable {

    private final AtomicInteger invocationDone;

    public MyRunnable(AtomicInteger invocationDone) {
      this.invocationDone = invocationDone;
    }

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + ", exécution");

      if (invocationDone.decrementAndGet() == 0) {
        throw new Error("ohhh une Erreur dans MyRunnable");
      } else {
        throw new IllegalArgumentException("ohhh une Exception dans MyRunnable");
      }
    }
  }
}

Sortie :

pool-1-thread-1, exécution
Je replanifie en cas d'Exception
pool-1-thread-1, exécution
Je replanifie en cas d'Exception
pool-1-thread-2, exécution
Je m'arrête en cas d'Erreur

6voto

MBec Points 1660

Je sais que c'est une ancienne question, mais si quelqu'un utilise un CompletableFuture retardé avec un ScheduledExecutorService, alors il devrait le gérer de cette manière :

private static CompletableFuture delayed(Duration delay) {
    CompletableFuture delayed = new CompletableFuture<>();
    executor.schedule(() -> {
        String value = null;
        try {
            value = mayThrowExceptionOrValue();
        } catch (Throwable ex) {
            delayed.completeExceptionally(ex);
        }
        if (!delayed.isCompletedExceptionally()) {
            delayed.complete(value);
        }
    }, delay.toMillis(), TimeUnit.MILLISECONDS);
    return delayed;
}

et gérer l'exception dans CompletableFuture :

CompletableFuture delayed = delayed(Duration.ofSeconds(5));
delayed.exceptionally(ex -> {
    // gérer l'exception
    return null;
}).thenAccept(value -> {
    // gérer la valeur
});

5voto

yegor256 Points 21737

Une autre solution serait d'ignorer une exception dans le Runnable. Vous pouvez utiliser une classe VerboseRunnable pratique de jcabi-log, par exemple:

import com.jcabi.log.VerboseRunnable;
scheduler.scheduleWithFixedDelay(
  new VerboseRunnable(
    Runnable() {
      public void run() { 
        // Faire la logique métier, une exception peut survenir
      }
    },
    true // cela signifie que toutes les exceptions seront ignorées et enregistrées
  ),
  1, 10, TimeUnit.SECONDS
);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X