Joy and pain with @Scheduled and @RefreshScope in SpringBoot
| TLDR; @Scheduled
and @RefreshScope
are powerful tools but do not work out of the box together causing dangerous inconsistencies. Find out how to get them to play nicely and more advanced scheduling opporunities.
So I'm a fan of the SpringBoot framework and it's adoption of the Rails "convention over configuration" as well as it's powerful and simple annotations. All of which make a developers life more productive and allow the team to focus on delivering functional value to end users.
One of those annotations is @Scheduled
which can be applied to any bean method. Only two pieces of code are required;
1. the enabling of scheduling in the application, typically achieved with something like the following; (note the use of @EnableScheduling
@SpringBootApplication
@EnableScheduling
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
2. And the actual method you want repeated. For instance;
@Slf4j
@Configuration
public class Followers {
@Value("${follower.count:10}")
private int followers;
@Scheduled(fixedRate = 4000)
public void outputFollowers() {
log.info("===========> Followers " + followers);
}
}
In this example a count of followers is autowired from the spring property hierarchy and outputted to the log every 4 seconds as per the @Scheduled
declaration. If you're wondering where the log
object is defined, this is part of the magic of Project Lombok which abstracts a lot of standard Java boiler-plating and is provided by the @Slf4j
annotation on the class.
@Scheduled
is a very powerful and simple annotation. Common forms of it include;
- Fixed rate - Repeat every x milliseconds.
@Scheduled(fixedRate=1000)
- Fixed delay - Repeat every x milliseconds between previous completion and next start.
@Scheduled(fixedDelay=1000)
- Crontab - Defines a cadence using the same expressions as *nix crontab definitions.
@Scheduled(cron="0 0 * * * *")
A great introduction to this topic can be found here.
So at this part in the article we change track over to automatic property updating. As previously mentioned, SpringBoot has a extensive property hierarchy but no way natively to refresh properties should they change. Enter Spring Cloud Config. The main focus of this project is to establish a centralized repository of configuration for a distributed set of applications and have those applications update automatically if properties change. A great introduction to externalized properties and centralized property management with Spring Cloud Config can be found here.
Spring Cloud Config automatically provides a JMX interface and a HTTP interface (\refresh
) to refresh all properties in the application in classes marked with the @RefreshScope
annotation. Meaning if the external property source changes, all you have to do is hit \refresh
on your application and the configuration changes are automatically pulled in.
And for the most part this works pretty nice and seamlessly as you would expect... except it doesn't work with the original @Scheduled
code example at the start of the article. In fact everything but @Scheduled
annotated code will refresh causing system inconsistencies within an application. The problem here is that the scheduled method (outputFollowers()
) has a dependency on a property injected by the Spring framework and even when refreshed the property change is not propagated down into the scheduled code. A discussion on this can be found in common Spring Cloud issues .
The solution, rather than relying on the magic of the @Scheduled
annotation, is to specify the scheduling configuration manually. For example;
@SpringBootApplication
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
@Slf4j
@RefreshScope
@Configuration
public class Followers {
@Value("${follower.count:10}")
private int followers;
public void outputFollowers() {
log.info("===========> Followers " + followers);
}
}
@Configuration
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {
@Autowired
Followers followers;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
() -> followers.outputFollowers(),
triggerContext -> {
Instant nextTriggerTime = Instant.now().plus(4, SECONDS);
return Date.from(nextTriggerTime);
});
}
}
Tackling the problem this way now yields an application that refreshes properties throughout on demand and consistently.
While initially this is somewhat of a pain, it is a solution first and foremost but it also enables more complex scheduling.
For instance you could build a trigger context that dynamically calculates the next time to run, potentially throttling an action per time period. See here for an example.
Scheduled code by default is limited to a single thread meaning there is an assumption that the previous call should be finished before the next execution. When this assumption is incorrect a execution thread pool is necessary which again can be manually configured such as;
@Configuration
@EnableScheduling
public class AppConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(100);
}
}
Further reading can be found on the @EnableScheduling java docs.
You can of course do far more than just the simple examples above but this should be enough to firstly resolve any problems between the SpringBoot @Scheduled
annotation and the live configuration updates you can attain by incorporating the @RefreshScope
annotation from the Spring Cloud Config project.
Enjoy.