spring-scheduled

Spring schedule - dynamically start and stop a task


A user wants to start or stop tasks via a user interface. The tasks are stored in a database in case a restart is needed.

When investigating a few alternatives, I came across a solution with ScheduledTaskRegistrar.

My question is: how can I stop any started job in a ScheduledTaskRegistrar context?

Using Quartz is another solution, but I would prefer a lightweight, simple solution.

Is there a better simple framework or is Quartz the right way to go?


Solution

  • When investigating some existing packages and especially Quartz, Quartz was not appropriate in this case. We needed here a lightweight solution. In other cases we used it.

    Inspired by a number of posts and articles I devised the following simple but effective package. It works well for scheduling tasks with cron expressions. It could easily be enhanced with other triggers. After restarting the app (or a crash) the tasks are automatically rescheduled again.

    I hope you also benefit from it.

    The config:

    @Configuration
    @EnableScheduling
    public class TaskSchedulingConfig implements SchedulingConfigurer {
    
      public static final int SCHEDULER_JOB_POOL_SIZE = 5;
    
      @Override
      public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setTaskScheduler(taskScheduler());
      }
    
      @Bean
      public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(SCHEDULER_JOB_POOL_SIZE);
        scheduler.setThreadNamePrefix("SomeThreadScheduler__");
        scheduler.initialize();
        return scheduler;
      }
    }
    

    The TaskDefinition:

    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @Entity
    @Table(name = "LTST_SCHEDULED_TASK")
    public class TaskDefinition {
      @Id
      @GeneratedValue(strategy = GenerationType.AUTO)
      @Column(name = "LTST_ID", updatable = false, nullable = false)
      private Long id;
    
      @Column(name = "LTST_TASK_NAME")
      private String taskName;
    
      @Column(name = "LTST_CRON_EXPR")
      private String cronExpression;
    }
    

    A sample service. Working with prototype beans would of course be better, but in my case this is good enough. You can easily enhance this.

    @Service
    public class TaskDefinitionBean1 implements Runnable {
      private final Logger logger = LoggerFactory.getLogger(TaskDefinitionBean1.class);
    
      @Override
      public void run() {
        logger.info("Running job task-1");
      }
    }
    

    The scheduling service, based on the above config class:

    @Service
    public class TaskSchedulingService {
      private static final Logger logger = LoggerFactory.getLogger(TaskSchedulingService.class);
    
      private final TaskScheduler taskScheduler;
      private final TaskDefinitionBean1 taskBean1;
      private final TaskDefinitionBean2 taskBean2;
      private final TaskDefinitionRepository taskDefinitionRepository;
      private final Map<String, ScheduledFuture<?>> schedulerMap = new HashMap<>();
    
      public TaskSchedulingService(TaskScheduler taskScheduler, TaskDefinitionBean1 taskBean1,
                                   TaskDefinitionBean2 taskBean2, TaskDefinitionRepository taskDefinitionRepository) {
        this.taskScheduler = taskScheduler;
        this.taskBean1 = taskBean1;
        this.taskBean2 = taskBean2;
        this.taskDefinitionRepository = taskDefinitionRepository;
        startSchedulingExistingTasks();
      }
    
      private void startSchedulingExistingTasks() {
        taskDefinitionRepository.findAll().forEach(t -> {
          logger.info("Start existing task '{}' with cron expression {}", t.getTaskName(), t.getCronExpression());
          // Simplistic way to start different services ... good enough for prototyping
          if( t.getTaskName().matches( "one")) {
            schedulerMap.put(t.getTaskName(), taskScheduler.schedule(taskBean1, new CronTrigger(t.getCronExpression())));
          } else {
            schedulerMap.put(t.getTaskName(), taskScheduler.schedule(taskBean2, new CronTrigger(t.getCronExpression())));
          }
        });
      }
    
      public TaskDefinition addCronTask(String taskName, String cronExpression) {
        if (schedulerMap.containsKey(taskName)) {
          return null;
        }
        TaskDefinition taskDefinition = TaskDefinition.builder().taskName(taskName).cronExpression(cronExpression).build();
        logger.info("Start new task '{}' with cron expression {}", taskDefinition.getTaskName(), taskDefinition.getCronExpression());
        // Simplistic way ... good enough for prototyping
        if( taskDefinition.getTaskName().matches( "one")) {
          schedulerMap.put(taskDefinition.getTaskName(), taskScheduler.schedule(taskBean1, new CronTrigger(taskDefinition.getCronExpression())));
        } else {
          schedulerMap.put(taskDefinition.getTaskName(), taskScheduler.schedule(taskBean2, new CronTrigger(taskDefinition.getCronExpression())));
        }
        taskDefinitionRepository.save(taskDefinition);
        return taskDefinition;
      }
    
      public void stopCronTask(String taskName) {
        ScheduledFuture<?> scheduleJob = schedulerMap.get(taskName);
        if (scheduleJob == null) {
          return; // unknow job, so don't stop
        }
        scheduleJob.cancel(false);
        taskDefinitionRepository.deleteByTaskName( taskName);
        schedulerMap.remove(taskName);
      }
    
      public List<TaskDefinition> getScheduledTasks() {
        return StreamSupport.stream(taskDefinitionRepository.findAll().spliterator(), false).toList();
      }
    
    }
    

    Play time: putting it all behind an API so you can create a demo yourself with eg. Postman:

    @RestController
    @RequestMapping("/scheduledtasks")
    public class ScheduleTaskController {
      private static final Logger logger = LoggerFactory.getLogger(ScheduleTaskController.class);
      private final TaskSchedulingService taskSchedulingService;
    
      public ScheduleTaskController(TaskSchedulingService taskSchedulingService) {
        this.taskSchedulingService = taskSchedulingService;
      }
    
      @GetMapping(value = "")
      public ResponseEntity<List<TaskRequest>> getScheduleTasks() {
        return ok().body( taskSchedulingService.getScheduledTasks().stream().map(this::convertTaskDefinitionToTaskRequest).toList());
      }
    
      @PostMapping(value = "", consumes = "application/json")
      public ResponseEntity<TaskDefinition> startTaskWithName(@RequestBody TaskRequest taskRequest) {
        logger.info("Start task with name {} and cronexpression {}", taskRequest.getTaskName(),
          taskRequest.getCronExpression());
        return status(HttpStatus.OK).body( taskSchedulingService.addCronTask( taskRequest.getTaskName(),
          taskRequest.getCronExpression()));
      }
    
      @DeleteMapping(value = "/{taskname}")
      public ResponseEntity<TaskRequest> deleteTaskWithName(@PathVariable("taskname") String taskname) {
        logger.info("Delete task with name {}", taskname);
        taskSchedulingService.stopCronTask( taskname);
        return status(HttpStatus.OK).body( new TaskRequest( taskname, ""));
      }
    
      private TaskRequest convertTaskDefinitionToTaskRequest( TaskDefinition taskDefinition) {
        return new TaskRequest(taskDefinition.getTaskName(), taskDefinition.getCronExpression());
      }
    
    }
    

    And a POJO for a task request:

    @AllArgsConstructor
    @Data
    public class TaskRequest {
      private String taskName;
      private String cronExpression;
    }
    

    Enjoy!