Do you want to change the Scheduled tasks in runtime with Spring Cloud Config?
Hello to everybody that is reading to me. This is my very first article in this page and I’m so excited to show you a “little” way to resolve a “big” problem that we all have with the relationship between our Spring Cloud Config Server and our scheduled tasks.
First at all, I want to ask... Can you change the executed time of your scheduled methods without restart your server? A good response to explain why I’m writing this article, is because I didn’t find anything on Google explaining how to make work the @RefreshScope synergy with @Scheduled methods.
Creating the Spring Cloud Config Server
Ok, so if this life was a good life I wouldn’t need to explain how to make them work. If this life were fair, we need to start creating a Spring Cloud Config Server project like this:
With a default application.properties configuration:
The normal scheduling class
We have our Spring Config Server created! Ok, so let’s start with our Server Client. We will need the actuator tool and our RefreshScope. I assume that you already know how to connect the Spring Client with the Spring Config Server. After configure all the settings of our client (telling the config server port, exposing the actuator…), we are going to have the class of our Scheduled methods. Something like this:
As you could imagine, this cron expressions won’t be refreshed with the /actuator/refresh service, they only will be refreshed until we restart the client server. We can’t change them with our actuator refresh, so… if we are in a production environment… we will need to redeploy the server! And that is not a good thing.
What do I propose to resolve this?
Ok, so… the logic is clear. We want to cancel the old scheduled task and create a new one with the new crontab. It’s easy. But… how could we know when the configuration has been updated? Easy too! We will use the ApplicationListener interface, with the EnvironmentChangeEvent class. We will have something similar to this:
This new way to create scheduling tasks will force us to Override the method: “onApplicationEvent”. This method will have a EnvironmentChangeEvent parameter that will provide us the name of the values refreshed. We will have only two values right now: “scheduler.tips” and “scheduler.refresh”; so if we update the “scheduler.tips” only, the event.getKeys() method will only have the scheduler.tips name.
Ok, so we are going to create a stream for the flow of the keys retrieved from the event and filter them by “scheduler.” (as you see we must create every scheduled task with this format, you can change it if you want), this filter is because we don’t want to do nothing with other type of values.
For the next logic that you see in the “.forEach()” method, I need to explain the reason for the functions and getters hash maps.
We need to know which method is related with the crontab expression that was updated, and obviously the new trigger value that is set. So… in our functions hash map we will save the method associated to the crontab, and in our getters hash map we will save the getter method associated to the crontab, too.
We can see it by a simple image:
Ok, so maybe right now is not clear what I’m doing. If you read the code, I’m initializing the hash maps of the @Service class, but why? We need to save the ScheduledFuture object that is already scheduled, because we need to cancel it when the refresh event happen.
And.. why do we need a hash map for the functions? We need it, because… how do you know which logic is associated with the scheduled? We need to save it too!
And.. why do we need a hash map for the getters methods? We need it, because… how do you know the new injected value associated to the scheduled crontab retrieved from the event?
We need to save them all! A ring to govern them all!
Let’s finish it now!
So, we can finish the explanation of this method. As you see we have a forEach with a computeIfPresent function in our pool hash map. If you can remember, few lines up we create this hash map with the scheduled thread objects.
So… the key of this hash map is the event key! Obviously it will be present, but we need to validate everything as a good back end engineer!
Ok… it is present… we need need to cancel the ScheduledFuture object, and create a new one! But… which function and which CronTigger value? Mmm… hash maps collections are good guys! They are good.
So… I hope you like it guys and that this article were useful for your projects. I saved a lot of time with this approach, so if I you can save it too.. I will be happy!
Two important updates!
Getters hash map is not working
When I improving my code, I realized that the getters values were not updated with the new configuration value, this is because I was saving a copy of the get function (not a reference) so it always retrieve the very first value assigned.
How can you resolve that? It is ugly, but functional. You only need to create a switch/case with each the name of the cron (something like scheduler.refresh) and the get invocation associated to that cron (this.getRefreshConnections()).
So, you only need to call to this private method instead of call to:
Duplicated threads
I didn’t remember that I wrote this article several months ago, and editing my profile this morning I realized that I didn’t explain a little fix.
If we mark a class with the @RefreshScope annotation, this class pass to be a lazy class (no matter if the class is a @Service or a @Component). What does this result in? If we maintain the pool hash map in this class, we are going to replicate the threads if we want to refresh the cron values.
Each time that we refresh the configuration with the actuator endpoint, the server will restart each class (with the new injected values) that is marked with @RefreshScope annotation. So, the pool with the whole threads created will be empty (but not cancelled) and the @PostConstruct annotation will start again and create another time each scheduled function.
How can we fix this problem?
We only need to separate every hash map that we were “persisting” in our @RefreshScope class, we’ll create an auxiliary class (not marked with @RefreshScope) where we are going to create these collections.
We just need to create the getters methods to retrieve the collections.
Follow me in my LinkedIn if you want! https://www.linkedin.com/in/ropuertop/