How to restart scheduled task on runtime with EnableScheduling annotation in spring?

后端 未结 4 507
南笙
南笙 2021-01-01 21:25

I have been investigating how to change the frequency of a job on runtime with Java 8 and spring. This question was very useful but it did not totally solve my issue.

<
相关标签:
4条回答
  • 2021-01-01 21:59

    The following, an improved version of this code, seems a working POC based on Spring Boot. You can start and stop the scheduled tasks any number of times based on a table configuration. But you can't start a stopped job from where it was stopped.

    1) In the main class, make sure scheduling is enabled, and perhaps configure a ThreadPoolTaskScheduler with size more than one so scheduled tasks may run in parallel.

    @SpringBootApplication
    @EnableScheduling
    
     @Bean
    public TaskScheduler poolScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
        scheduler.setPoolSize(10);
        scheduler.initialize();
        return scheduler;
    }
    

    2) an object that contains the schedule configuration, e.g. a cron like configuration in this case:

    public class ScheduleConfigVo {
    //some constructors, getter/setters
        private String  taskName;
        private String  configValue; // like */10 * * * * * for cron
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ScheduleConfigVo that = (ScheduleConfigVo) o;
            return taskName.equals(that.taskName) &&
                configValue.equals(that.configValue) ;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(taskName, configValue);
        }
    }
    

    equals and hashCode are needed since object comparison will be conducted.

    3) I use mybatis, so the sheduled selection is something like:

    @Mapper
    public interface ScheduleConfigMapper {
        List<ScheduleConfigVo> getAllConfigure();
    }
    

    and

    public class ScheduleConfigMapperImpl implements ScheduleConfigMapper {
        @Override
        public List<ScheduleConfigVo>getAllConfigure() {
            return getAllConfigure();
        }
    }
    

    with a simple companion mybatis xml configuration (not shown here but can find it anywhere in the internet).

    4) create a table and populate it with a record

    CREATE TABLE "SCHEDULER" 
    ( "CLASS_NAME" VARCHAR2(100), --PK
    "VALUE" VARCHAR2(20 BYTE) --not null
    )
    

    and populated it with a record class_name=Task1, value=*/10 * * * * * etc. => run like a cron every ten seconds

    5) the scheduler part:

    @Service
    public class DynamicScheduler implements SchedulingConfigurer {
    
    @Autowired
    private ScheduleConfigMapper repo;
    
    @Autowired
    private Runnable [] tsks;
    
    @Autowired
    private TaskScheduler tsch;
    
    private ScheduledTaskRegistrar scheduledTaskRegistrar;
    private ScheduledFuture future;
    
    private Map<String, ScheduledFuture> futureMap = new ConcurrentHashMap<>(); // for the moment it has only class name
    List<ScheduleConfigVo> oldList = new ArrayList<>();
    List<ScheduleConfigVo> newList;
    List<ScheduleConfigVo> addList = new ArrayList<>();
    List<ScheduleConfigVo> removeList = new ArrayList<>();
    
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        if (scheduledTaskRegistrar == null) {
            scheduledTaskRegistrar = taskRegistrar;
        }
        if (taskRegistrar.getScheduler() == null) {
            taskRegistrar.setScheduler(tsch);
        }
        updateJobList();
    
    }
    
    @Scheduled(fixedDelay = 5000)
    public void updateJobList() {
        newList = repo.getConfigure()== null ? new ArrayList<>() : repo.getConfigure();
        addList.clear();
        removeList.clear();
    
        if (!newList.isEmpty()) {
            //compare new List with oldList
            if (!oldList.isEmpty()) {
                addList = newList.stream().filter(e -> !oldList.contains(e)).collect(Collectors.toList());
                removeList = oldList.stream().filter(e -> !newList.contains(e)).collect(Collectors.toList());
            } else {
                addList = new ArrayList<>(newList); // nothing to remove
            }
        } else { // nothing to add
            if (!oldList.isEmpty()) {
                removeList = new ArrayList<>(oldList);
            } // else removeList = 0
        }
        log.info("addList="+ addList.toString());
        log.info("removeList="+ removeList.toString());
        //re-schedule here
    
        for ( ScheduleConfigVo conf : removeList ) {
            if ( !futureMap.isEmpty()){
                future = futureMap.get(conf.getTaskName());
                if (future != null) {
                    log.info("cancelling task "+conf.getTaskName() +" ...");
                    future.cancel(true);
                    log.info(conf.getTaskName() + " isCancelled = " + future.isCancelled());
                    futureMap.remove(conf.getTaskName());
                }
            }
        }
        for ( ScheduleConfigVo conf : addList ) {
            for (Runnable o: tsks) {
                if (o.getClass().getName().contains(conf.getTaskName())) { // o has fqn whereas conf has class name only
                    log.info("find " + o.getClass().getName() + " to add to scheduler");
                    future = scheduledTaskRegistrar.getScheduler().schedule(o, (TriggerContext a) -> { 
                        CronTrigger crontrigger = new CronTrigger(conf.getConfigValue());
                        return crontrigger.nextExecutionTime(a);
                    });
                    futureMap.put(o.getClass().getName().substring(o.getClass().getName().lastIndexOf('.')+1), future);
                }
            }
        }
    
        oldList.clear();
        oldList= newList;
    }
    

    6) one or more Runnable tasks that actually does the cron work, for instance:

    @Slf4j
    @Service
    public class Task1 implements Runnable {
        @Override
        public void run() {
            log.info("Task1 is running...");
        }
    }
    

    Once the application is started, the cron job will run. The running interval changes as the value in the table changes, and the job stops as the table entry is removed.

    Note that if the job runs longer than the cron interval, the next run is after the previous job finishes. You can simulate this situation by adding, for instance, sleep 15 seconds in Task1 above to test it. Sometimes after being cancelled, a job maybe still run till it's done.

    ***Just edit to add that if folks like lambda to save some lines, the above removeList and addList can be modified as:

    removeList.stream().filter(conf -> {
            future = futureMap.get(conf.getTaskName());
            return future != null;
        }).forEach((conf) -> {
            log.info("cancelling task " + conf.getTaskName() + " ...");
            future.cancel(true);
            log.info(conf.getTaskName() + " isCancelled = " + future.isCancelled());
        });
    

    and

    Arrays.stream(tsks).forEach(task -> {
            addList.stream().filter(conf -> task.getClass().getName().contains(conf.getTaskName())).forEach(conf -> {
                log.info("find " + task.getClass().getName() + " to add to scheduler");
                future = scheduledTaskRegistrar.getScheduler().schedule(task, (TriggerContext a) -> {
                    CronTrigger crontrigger = new CronTrigger(conf.getConfigValue());
                    return crontrigger.nextExecutionTime(a);
                });
                futureMap.put(task.getClass().getName().substring(task.getClass().getName().lastIndexOf('.') + 1), future);
            });
        });
    
    0 讨论(0)
  • 2021-01-01 22:02

    One simple approach is to only ever add new tasks, not to try and cancel or restart the scheduler.

    Each time the configuration changes, just add a new task with its new configuration.

    Then, whenever a task runs, it must first check some state (by querying database, or lookup up in a concurrent map, or whatever) to decide if it is the latest version. If it is, then it should proceed. Otherwise, it should end immediately.

    The only downside is that if you are changing job configuration frequently compared to how often they run, then of course the list of scheduled tasks will keep growing in memory.

    0 讨论(0)
  • 2021-01-01 22:05
    1. Create a singleton bean that gets an injected TaskScheduler. This will hold as state variables all ScheduledFutures, like private ScheduledFuture job1;
    2. On deployment, load from databases all schedule data and start the jobs, filling in all state variables like job1.
    3. On change of scheduling data, cancel the corresponding Future (e.g job1) and then start it again with the new scheduling data.

    The key idea here is to get control on the Futures as they are created, so to save them in some state variables, so that when something in scheduling data changes, you can cancel them.

    Here is the working code:

    applicationContext.xml

    <task:annotation-driven />
    <task:scheduler id="infScheduler" pool-size="10"/>
    

    The singleton bean, that holds the Futures

    @Component
    public class SchedulerServiceImpl implements SchedulerService {
    
            private static final Logger logger = LoggerFactory.getLogger(SchedulerServiceImpl.class);
    
            @Autowired
            @Qualifier(value="infScheduler")
            private TaskScheduler taskScheduler;
    
            @Autowired
            private MyService myService;
    
            private ScheduledFuture job1;//for other jobs you can add new private state variables
    
            //Call this on deployment from the ScheduleDataRepository and everytime when schedule data changes.
            @Override
            public synchronized void scheduleJob(int jobNr, long newRate) {//you are free to change/add new scheduling data, but suppose for now you only want to change the rate
                    if (jobNr == 1) {//instead of if/else you could use a map with all job data
                            if (job1 != null) {//job was already scheduled, we have to cancel it
                                    job1.cancel(true);
                            }
                            //reschedule the same method with a new rate
                            job1 = taskScheduler.scheduleAtFixedRate(new ScheduledMethodRunnable(myService, "methodInMyServiceToReschedule"), newRate);
                    }
            }
    }
    
    0 讨论(0)
  • 2021-01-01 22:09

    What about using Set<ScheduledTask> ScheduledTaskRegistrar.getScheduledTasks() to get all schedules tasks and calling ScheduledTask::cancel() ? or maybe executing ThreadPoolTaskScheduler::shutdown() and recreating ThreadPoolTaskScheduler and setting it again in ScheduledTaskRegistrar ?

    0 讨论(0)
提交回复
热议问题