javamultithreadingexecutorserviceshutdown-hook

Difficulty to understand when is shutdown hook executed and how to terminate an ExecutorService


I'm trying to run a program based on a scheduleAtFixedRate ExecutorService. I use this service to replace a dummy while loop. The global idea is to have first an ExecutorService (the scheduler) executing a Runnable (the runnable). then, for some reason, the runnable may or may not schedule a task on an other ExecutorService (the tasker). Now the question is :

I have experimented a bit and can't find a proper solution. I have tried two things : DaemonThread, ShutdownHook Daemon threads are not what I'm looking for, I want the scheduler to keep running until I stop the program. A ShutdownHook was not a good solution at first sight but after some researches it seems to work.

The thing is, the ShutdownHook is runned if I execute the code with the command mvn exec:java -Dexec.mainClass="com.goodbook.App" and stop it with Ctrl+C, but it's not executed if I run the java code in my IDE (VScode) and stop it with the debugger tool. So there are two more question :

First I will give you two pieces of code : the App and the Task class.


App.java

package com.goodbook;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class ShutdownHook {
  private static final Logger logger = LoggerFactory.getLogger(ShutdownHook.class);
  public void attachShutDownHook(ScheduledExecutorService scheduler, ScheduledExecutorService tasker) {
    logger.info("attaching shutdown hook");
    Runtime.getRuntime().addShutdownHook(new Thread() {
      @Override
      public void run() {
        scheduler.shutdown();
        tasker.shutdown();
        logger.info("shutdown hook runned");
        logger.info("scheduler is down : "+scheduler.isShutdown());
        logger.info("tasker is down : "+tasker.isShutdown());
      }
    });
  }
}

public class App 
{
  private static final Logger logger = LoggerFactory.getLogger(App.class);
  public static void main( String[] args )
  {   
    logger.info("starting program");
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(0);
    ScheduledExecutorService tasker = Executors.newScheduledThreadPool(0);

    ShutdownHook shutdown = new ShutdownHook();
    shutdown.attachShutDownHook(scheduler, tasker);

    Task t = new Task("task1");

    Runnable  runnable = () -> {
      logger.info("running Tasker 1");
      if(!t.isPlanified()){
        logger.info("unplanified task found "+t.getName());
        logger.info("planning...");
        tasker.schedule(t, 1000, TimeUnit.MILLISECONDS);
        t.setPlanified(true);
      }
    };
    logger.info("scheduling tasks");
    scheduler.scheduleAtFixedRate(runnable, 0, 1000, TimeUnit.MILLISECONDS);
  } 
}

Task.java

package com.goodbook;

import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Task implements Runnable {
  private static final Logger logger = LoggerFactory.getLogger(App.class);

  String name;
  AtomicBoolean planified = new AtomicBoolean(false);

  public Task(String name){
    this.name = name;
  }

  @Override
  public void run(){
    logger.info(name+" run");
    planified.set(false);
  }

  public Boolean isPlanified(){
    return planified.get();
  }

  public void setPlanified(Boolean b){
    planified.set(b);
  }

  public String getName(){
    return name;
  }
}

The end of the log file in the case of running with VScode and stopping execution with the debugger tool:

INFO    2019-04-06 16:35:54,121 [pool-1-thread-1] com.goodbook.App  - planning...
INFO    2019-04-06 16:35:55,121 [pool-1-thread-1] com.goodbook.App  - Running Tasker 1
INFO    2019-04-06 16:35:55,121 [pool-2-thread-4] com.goodbook.TestTask  - task1 run
INFO    2019-04-06 16:35:56,121 [pool-1-thread-1] com.goodbook.App  - Running Tasker 1
INFO    2019-04-06 16:35:56,121 [pool-1-thread-1] com.goodbook.App  - Unplanified task found task1
INFO    2019-04-06 16:35:56,121 [pool-1-thread-1] com.goodbook.App  - planning...

The end of the log file in the case of running with mvn exec:java -Dexec.mainClass="com.goodbook.App" and stopping execution with Ctrl+C:

INFO    2019-04-06 16:59:09,686 [pool-1-thread-1] com.goodbook.App  - running Tasker 1
INFO    2019-04-06 16:59:09,688 [pool-2-thread-1] com.goodbook.Task  - task1 run
INFO    2019-04-06 16:59:10,686 [pool-1-thread-1] com.goodbook.App  - running Tasker 1
INFO    2019-04-06 16:59:10,686 [pool-1-thread-1] com.goodbook.App  - unplanified task found task1
INFO    2019-04-06 16:59:10,687 [pool-1-thread-1] com.goodbook.App  - planning...
INFO    2019-04-06 16:59:11,686 [pool-1-thread-1] com.goodbook.App  - running Tasker 1
INFO    2019-04-06 16:59:11,687 [pool-2-thread-2] com.goodbook.Task  - task1 run
INFO    2019-04-06 16:59:12,641 [Thread-1] com.goodbook.ShutdownHook$1  - shutdown hook runned
INFO    2019-04-06 16:59:12,642 [Thread-1] com.goodbook.ShutdownHook$1  - scheduler is down : true
INFO    2019-04-06 16:59:12,642 [Thread-1] com.goodbook.ShutdownHook$1  - tasker is down : true

I really need help with these problems, I'm not a java dev :s


Solution

  • Shutdown hooks are called when the JVM is shutting down; see the class JavaDoc at Runtime.addShutdownHook. But terminating a run from a debugger is not a shutdown.

    Usual exit

    When either the last non-daemon thread exits, or System.exit() is called via code, or CTRL-C is pressed on the command line, (or the program is sent a kill signal, eg. using task manager), then the shutdown hooks are executed, and once they have completed, the JVM shuts down.

    Exit from debugger

    If you terminate the program during debugging, and press "stop", the IDE aborts your app immediately. There is no chance to run the shutdown hooks. This is usually the right thing to do; when debugging, you normally want to just kill whatever is running.

    Exiting gracefully while debugging

    If you want a way to run the shutdown hooks while debugging, there are a number of ways; one is calling System.exit() via code; that will trigger the hooks. In this related question, they wait for a keypress event before calling system.exit.


    Regarding

    how am I supposed to stop all these Threads when I stop the program ?

    again, you can call System.exit() so that the program will terminate even if non-deamon threads are in the middle of an execution (which is not really clean); but a better way is to give every thread a chanche to terminate normally- eg. if a thread is running an infinite loop, it could periodically check a (volatile) shutdown variable, and return (via code) when the variable becomes set. (the shutdown variable itself should be set by a shutdown hook)