It is common to have jobs running periodically, especially in asynchronous and distributed systems. If the service is scaled horizontally (i.e. there are multiple instances of the same service), you often only want a single node to handle the task.
In this session I will demonstrate how to manually setup Spring to have custom logic in scheduling configuration and perform recurring tasks only on a single node. This requires keeping notation of the leader and persisting the selection.
The key takeaway of this session is how to implement distributed locking and how simple it is to run Spring application on top of it. In this talk you will learn how to mitigate challenges that arise when you use traditional declarative approach for scheduling and how to switch to a more flexible programmatic approach.
15. @Aspect
public class RunIfLeaderAspect {
@Around("@annotation(com.n26.RunIfLeader) && execution(void *(..))")
public void annotatedMethod(ProceedingJoinPoint joinPoint)
throws Throwable {
if (isLeader()) {
joinPoint.proceed();
}
// do not execute
}
16. Why we didn’t
like it?
一 No clear separation between
business and scheduling logic
一 Hard to test
一 Scheduled jobs spread across
the application
26. @RunWith(MockitoJUnitRunner.class)
public class SchedulingConfigTest {
@InjectMocks
private SchedulingConfig underTest;
@Mock
private ScheduledTaskRegistrar taskRegistrarMock;
@Test
public void usesScheduledThreadPoolExecutor() {
ArgumentCaptor<ScheduledThreadPoolExecutor> captor =
forClass(ScheduledThreadPoolExecutor.class);
underTest.configureTasks(taskRegistrarMock);
verify(taskRegistrarMock).setScheduler(captor.capture());
assertThat(captor.getValue().getCorePoolSize()).isEqualTo(4);
}
27. @Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(
new CronTask(() -> {
if (isLeader()) {
// process events
}
}, "0 * * * * *"));
}
28. @Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Autowired
private Runnable processEventsTask;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
Runnable leaderAwareTask =
new LeaderAwareTaskDecorator(processEventsTask);
taskRegistrar.addCronTask(
new CronTask(leaderAwareTask, "0 * * * * *"));
}
29. public final class LeaderAwareTaskDecorator implements Runnable {
private Runnable delegate;
public LeaderAwareTaskDecorator(Runnable delegate) {
this.delegate = delegate;
}
@Override
public void run() {
if (isLeader()) {
delegate.run();
}
}
30.
31. Resiliency
一 What if the response didn’t come?
一 Can we safely repeat?
• Duplicate entries created
一 Is the action idempotent?
• One or multiple identical
requests give the same result
34. What have we
learned?
一 Annotation-driven development is hard
一 Keep (code) consistency
一 Increase resilience & predictability
一 Think about observability
At first sight scheduling a job seems like a trivial task, especially when there are not that many tasks that are performed by the scheduler and performance is not a huge factor. But is there a simple solution available that could enable handling background jobs in a distributed environment? We want to keep the amount of dependencies low and avoid problem when there is a higher chance of failure.
Resilience as a main requirement.
Benefits: fast/safe processing of the events stream and better visibility (amount of items to process).
Service received event, it was stored into database as a reaction and job was taking these items to process them and send them to partner.
Job scheduling in Unix environments
How we can handle growing amount of work?
Same application on multiple nodes.
Without going into details: we can use different machines or containerization.
Spring philosophy is to keep servers stateless, which means they are not aware of each other.
Microservices are not aware about the scaling method, so how they could know where to look for another node? Another machine? Another container? Or a pod in Kubernetes cluster?
We need third-party service to keep the notation of the leader. We can call it orchestrator.
Services can register themselves.
Let’s introduce our own annotation - it will be used to verify if the method should run.
Retention - annotations can be read from source files, class files, or reflectively at run time.
Target - declaration contexts, in our case a method will be marked with this annotation.
Let’s see how to “teach” Spring to understand this annotation.
To enable this annotation AOP is used - this looks like cross-cutting concern similar to logging or monitoring.
AOP framework - independent from IoC.
@Around is needed because inside we need to decide if the method should be called (@Before would be not enough - it is unconditional)
Spring can interpret aspects defined in XML configuration or defined with use of AspectJ annotation. It’s developer’s informed decision which one to use.
Execution expression has two required patterns: returning type and name/params.
Simple, but there is no separation between business logic and scheduling. Adding additional class that is responsible for scheduling and just calls this method looks like overdoing things.
There is no easy way to verify if something is scheduled.
Let’s try a naive approach.
Used for setting a specific task scheduler (i.e. executor service - next slide).
And of course - for registering scheduled task in programmatic fashion.
An object that executes submitted Runnable tasks, requirement is that it needs to be of type scheduled.
@Bean(destroyMethod="shutdown") - ensures that the task executor is properly shut down when the Spring application context itself is closed.
We can control pool size of this executor - the number of threads to keep in the pool.
(most importantly) We can specify the naming of the threads, which increases the visibility in the logs.
(builder is from Guava library)
Network partition = split brain.
If we are able to provide ID from outside (from the action performed by the scheduler) it will be safe.
Items are
Maybe more complex setup, but with Spring-type consistency.
Multiple instances, automatic switch to a new leader in case of a failure.
Good visibility in logs and we can test it quite well.