BLOG

Re@Scheduled Services

Nowadays, it is common to store application configuration in an external service. This is considered “best practice” and usually we expect automated actions to happen when the configuration is changed dynamically at runtime. @Scheduled services are no exception. Spring @Scheduled services are automatically executed on an interval defined by a cron expression or a fixed period. It is often used to implement repetitive routines and tasks, for example – data backup, queue polling, notification checks, etc. Scheduled services are commonly used in an asynchronous environment. In this article, we review the most common ways of configuring @Scheduled services and how to change the scheduling parameters at runtime. 

The problem 

How to define a re-scheduled Spring service that is based on a dynamically changing schedule? We want to be able to reconfigure the service’s execution schedule at runtime by changing its cron expression or its fixed execution interval. Rescheduling should take effect immediately and it should not require: 

  • Code recompilation / project re-build 
  • Application redeployment 
  • Application restart 

The obvious solution 

Let’s pour some Spring magic to our code and we have what we want – scheduled services. The @Scheduled annotation allows us to configure our service executions very conveniently. We can  set 

  • Cron 
  • FixedRate 
  • Delays 

For example: @Scheduled(cron = “0/10 * * * * *”). This is good enough for most of the use cases, but it does not satisfy our requirements. Since Java annotations require String constants or String literals as parameters, changing the cron expression will require recompilation, redeployment and restart

The Spring expressions magic 

 These limitations are not only valid for @Scheduled annotation. It is well known that Spring is an annotation-based sorcery, so there must be a standard solution. 

This is where Spring expression language comes to the rescue. It allows us to externalise the configuration. It is moved out from the source code and stored in the application.{yaml/properties} file. It is easy to use and anyone who has ever used Spring is aware of the application-profile.{yaml/properties} files. 

How does it work? 

  • Java source code: @Scheduled(cron = “my.amazong.cron”)  
  • Application.properties file: {code:properties} my.amazong.cron=“0/10 * * * * *” {code} 

This solves the problem with one of our requirements – recompilation. However, in order to provide changes to the application.{yaml/properties} file we must replace at least the file itself (i.e. kind of redeployment) and restart the application. 

This approach may be helpful if we use Spring Cloud Config. But in order to refresh the ApplicationContext and make use of configuration changes at runtime, we need to integrate Spring Cloud Bus. 

Spring Cloud Bus requires a message broker like RabbitMQ or Kafka. 

This is a useful design, but it implies a lot of dependencies for a simple task, like getting notified for changes in a key-value store. 

The Scheduling Configurer 

There is another “standard” option provided by Spring, interface called SchedulingConfigurer

Its documentation clearly states the use cases it is designed for: 

  • Typically used for setting a specific TaskScheduler (abstracts the scheduling of Runnables based on different kinds of triggers)
  • to be used … for registering scheduled tasks in a programmatic fashion as opposed to the declarative approach 
  • this may be necessary when implementing Trigger-based tasks, which are not supported by the @Scheduled annotation 

It feels just like what we need. Let’s see how it is used and dig deeper into the pros and cons of this approach. 

{code:java} 
@Configuration 
@EnableScheduling 
public class DynamicSchedulingConfig implements SchedulingConfigurer { 
 
    @Autowired 
    private TriggeredService myService; 
 
 
    @Override 
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { 
    taskRegistrar.setScheduler(getTaskExecutor()); 
            taskRegistrar.addTriggerTask( 
                        myService, // Runnable because TriggeredServive extends Runnable  
                        myService.constructTrigger() // Trigger) 
        	); 
	} 
 
} 
{code} 

We must implement only SchedulingConfigurer.configureTasks(ScheduledTaskRegistrar taskRegistrar) method. It allows us to set the TaskScheduler using the taskRegistrar parameter. 

In order to schedule our service – TriggeredService myService, we need to call addTriggerTask and provide the Runnable myService’s business logic. It also requires a Trigger object, which will determine the next execution of myService. 

So how is the actual re-scheduling done? 

It is required that either myService.constructTrigger()  or myService.run() to pull the latest configuration. Either way, this is a pull-based approach, and our service is overloaded with extra responsibilities to fetch configuration from an external source. Also, if the cron expression related to our service has not been changed at all and re-scheduling is not required, this configuration pull may be just another useless request. 

Besides that, if the service executes frequently, the same number of pull requests will be made as well. This may flood the network with unnecessary requests. 

Additionally, there are some other technical issues. We need to add one more @Configuration class. Our code is compile-time dependent on a Spring interface. And we know this is an anti-pattern – it may cause compile issues if the interface is changed in the next Spring version. Also, we have to @Autowire our TriggeredService into the @Configuration class. And this may cause cyclic dependency issues – if myService requires some @Bean defined in this @Configuration class. 

Sometimes the cyclic dependency chain may be very long. It also may be happening intermittently with different Spring versions if the order of @Configuration classes initialisation is changed. 

We don’t want to deal with problems that are hard to debug. 

So, although this approach is handy there are some obvious drawbacks. Can we do better? 

Yes, we can, but it will require some extra coding. 

The Push based approach 

First, let us define the responsibilities of our auto re-scheduled services. 

{code:java} 
public interface TriggeredService extends Runnable { 
 
String NO_CONFIG_VALUE = "noConfigValue"; 
 
Optional<String> getKey(); 
 
Trigger constructTrigger(String config); 
 
} 
{code} 

A TriggeredService has to be able to answer/do: 

  • What is the configuration key that the service relies on? 
  • Construct a Trigger from a configuration value (cron/period) 
  • run() – the business logic 

Let’s abstract our configuration service with the following interface: 

{code:java} 
public interface ConfigurationService { 
 
String get(String key); 
Map<String, String> getAll(); 
void set(String key, String value); 
void addListener(ConfigurationChangeListener listener); 
 
} 
{code} 

Of course, this may be backed by a database table, HTTP service or just an in-memory map. Notice that one can register change listeners to this service. 

So, what does a ConfigurationChangeListener do? 

{code:java} 
public interface ConfigurationChangeListener { 
 
Optional<String> keyToReactOn(); 
void onConfigurationChange(String newValue); 
 
} 
{code} 

It simply needs the key it listens to and it reacts to a new value for this key. 

Here is an example implementation that will suit our needs 

{code:java} 
public class TriggeringConfigurationChangeListener implements ConfigurationChangeListener { 
 
private volatile ScheduledFuture<?> future; 
 
private final TriggeredService service; 
private final TaskScheduler taskScheduler; 
 
public TriggeringConfigurationChangeListener(TriggeredService service, TaskScheduler taskScheduler) { 
    this.service = service; 
    this.taskScheduler = taskScheduler; 
} 
 
@Override 
public Optional<String> keyToReactOn() { 
    return service.getKey(); 
} 
 
@Override 
public synchronized void onConfigurationChange(String newValue) { 
    if (future != null) { 
  	  future.cancel(false); 
    } 
 
    Trigger trigger = service.constructTrigger(newValue); 
    future = taskScheduler.schedule(service, trigger); 
    } 
} 
{code} 

We will use one listener per TriggeredService. All it does: 

  • Delegates the answer for key of interest to the service 
  • On new config value asks the triggered service to construct a Trigger 
  • Cancels previously scheduled executions of the service and re-schedules it based on the new Trigger 

How is all of this glued together? 

{code:java} 
public abstract class SpringEventTriggeredServiceSchedulerBase<T extends TriggeredService> implements ApplicationListener<ContextRefreshedEvent> { 
 
private final ApplicationContext applicationContextToReactOn; 
 
private final ConfigurationService configurationService; 
 
protected SpringEventTriggeredServiceSchedulerBase( 
    ApplicationContext applicationContextToReactOn, 
    ConfigurationService configurationService) { 
    this.applicationContextToReactOn = applicationContextToReactOn; 
    this.configurationService = configurationService; 
} 
 
@Override 
public void onApplicationEvent(ContextRefreshedEvent event) { 
    if (!this.applicationContextToReactOn.getId().equals(event.getApplicationContext().getId())) { 
  	  return; 
    } 
 
    getServices().forEach(this::startService); 
} 
 
private final void startService(T service) { 
 
    TriggeringConfigurationChangeListener listener = new TriggeringConfigurationChangeListener(service, getScheduler()); 
 
    service.getKey().ifPresent(key -> configurationService.addListener(listener)); 
 
      String configValue = service.getKey() 
  	  .map(configurationService::get) 
  	  .orElse(TriggeredService.NO_CONFIG_VALUE); 
 
    // initial start 
    listener.onConfigurationChange(configValue); 
} 
 
protected abstract Set<T> getServices(); 
protected abstract TaskScheduler getScheduler(); 
} 
{code} 

This class defines two abstract methods for its descendants to implement: 

  • getServices() – which defines the specific TriggeredService sub-type of interest 
  • getScheduler() – should return the TaskScheduler to be used for all services returned from getServices() 

The class is bound to work only on TriggeredServices sub-types. It implements ApplicationListener<ContextRefreshedEvent>, which allows it to handle the initial ContextRefreshedEvent event and start the scheduled services. Since Spring application contexts are hierarchical, our SpringEventTriggeredServiceSchedulerBase will receive the event from its own ApplicationContext and from its parents. That is why we filter only the events from our own ApplicationContext. 

 
{code:java} 
@Service 
public class CapitalCaseBlueService implements BlueService { 
 
@Override 
public Optional<String> getKey() { 
    return Optional.of("capital-blue"); 
} 
 
@Override 
public Trigger constructTrigger(String cron) { 
    return BlueService.super.constructTrigger(cron); 
} 
 
@Override 
public void run() { 
    System.out.println("BLUE SERVICE "); 
} 
 
} 
 
{code} 

Example implementation of SpringEventTriggeredServiceSchedulerBase. 

{code:java} 
@Service 
public class BlueServiceScheduler extends SpringEventTriggeredServiceSchedulerBase<BlueService> { 
 
private final ThreadPoolTaskScheduler scheduler; 
 
private final Set<BlueService> services; 
 
protected BlueServiceScheduler( 
    ApplicationContext applicationContextToReactOn, 
    ThreadPoolTaskScheduler scheduler, 
    ConfigurationService configurationService, 
    Set<BlueService> services) { 
 
    super(applicationContextToReactOn, configurationService); 
    this.scheduler = scheduler; 
    this.services = services; 
} 
 
@Override 
protected Set<BlueService> getServices() { 
    return services; 
} 
 
@Override 
protected TaskScheduler getScheduler() { 
    return scheduler; 
} 
 
} 
 
{code} 

Example implementation of TriggeredService 

{code:java} 
public interface BlueService extends TriggeredService { 
 
@Override 
default Trigger constructTrigger(String cron) { 
    if (isNull(cron) || !CronExpression.isValidExpression(cron)) { 
        return new CronTrigger(defaultCron()); 
    } 
    return new CronTrigger(cron); 
} 
 
default String defaultCron() { 
    return "0/20 * * * * *"; 
} 
 
} 
{code} 

The pros of this approach: 

  • It allows us to define multiple implementors of SpringEventTriggeredServiceSchedulerBase,so that we can schedule sets of TriggeredServices implementations on different thread-pools by using different ThreadPoolTaskSchedulers; 
  • Every service is responsible to define its own configuration key; 
  • Rescheduling is push-based. The appropriate ConfigurationChangeListener reacts on configuration changes and the network is not flooded with unnecessary requests; 
  • We can use any kind of configuration service that is already available within the project 
  • It does not require any additional components like: Spring Cloud Config, Spring Cloud Bus, RabbitMQ, Kafka, etc.; 
  • It does not enforce compile-time dependencies on framework interfaces; 
  • It does not require recompilation for the configuration changes to take effect; 
  • It does not require redeployment; 
  • It does not require a restart of the application. 

Conclusion 

The problem presented in this article may be solved using different methods. And if any of the previous solutions does the job and we don’t have all the requirements related to recompiling/redeploying/restarting, it is perfectly fine to use it. It is always a good idea to rely on well tested mature implementations that work out of the box. Besides solving the problem in an efficient and performant manner, it is important to keep the code readable and maintainable. Keep it simple stupid – the KISS principle! 

Appendix: The Push based approach 

Some more useful implementations. 

{code:java} 
@Service 
public class ConfigurationServiceImpl implements ConfigurationService { 
 
private Map<String, String> configs; 
private Map<String, List<ConfigurationChangeListener>> listeners; 
 
public ConfigurationServiceImpl() { 
    configs = new HashMap<>(); 
    listeners = new HashMap<>(); 
} 
 
@Override 
public String get(String key) { 
    return configs.get(key); 
} 
 
@Override 
public Map<String, String> getAll() { 
    return new HashMap<>(configs); 
} 
 
@Override 
public void set(String key, String value) { 
    configs.put(key, value); 
 
    listeners.getOrDefault(key, Collections.emptyList()) 
        .forEach(listener -> listener.onConfigurationChange(value)); 
} 
 
@Override 
public void addListener(ConfigurationChangeListener listener) { 
    listener.keyToReactOn() 
        .ifPresent(key -> listeners.computeIfAbsent(key, k -> new ArrayList<>()).add(listener)); 
} 
 
} 
{code} 
{code:java} 
@RestController 
@RequestMapping("/") 
public class ConfigurationRestController implements ConfigurationController { 
 
private final ConfigurationService configurationService; 
 
public ConfigurationRestController(ConfigurationService configurationService) { 
    this.configurationService = configurationService; 
} 
 
@GetMapping 
public String home() { 
    return "Configuration Service Controller"; 
} 
 
@Override 
@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT}, path = "/keys", consumes = { MediaType.APPLICATION_JSON_VALUE}) 
public void update(@RequestBody Map<String, String> keyValues) { 
    keyValues.forEach((k, v) -> configurationService.set(k, v)); 
} 
 
@Override 
@GetMapping(path = "/keys/{key}", produces = MediaType.APPLICATION_JSON_VALUE) 
public Map<String, String> get(@PathVariable String key) { 
    return Collections.singletonMap(key, configurationService.get(key)); 
} 
 
@Override 
@GetMapping(path = "/keys", produces = MediaType.APPLICATION_JSON_VALUE) 
public Map<String, String> get() { 
    return configurationService.getAll(); 
} 
 
} 
 
{code}