The Cron and The Scheduler
Its very common for a business to require certain processes to be run on a repeated schedule, or to be able to react to a change by scheduling an action to take place some time in the future. The most common solution to this problem is to use some form of cron job or scheduler within the service that needs to carry out the work. Whilst these approaches can work fine they are not always the optimal solution.
Cron jobs tend to only work well with repeatable processes and require locking or some form of leader election process for every job where redundancy is involved. These can also be challenging to control from the outside without extra work. External schedulers can solve some of these problems but tend to require leakage of internal processes from other components.
Alternative — Passage of Time Events
An alternative approach is to represent the passage of time as an event. This approach isn't ideal for a real time process but for the majority of time based actions the following would suffice:
- At 00:00 every day, a ‘day-has-passed’ event is triggered in the system
- An ‘hour-has-passed’ event is triggered on the hour, every hour
- A ‘minute-has-passed’ event is triggered on the minute, every minute.
These events can simply contain the unit of time that has passed.
Benefits of This Pattern
A good example of the usefulness of this pattern is a particular use case we have at INSHUR. An insurance policy will be valid for a particular length of time, usually in days or months. We may want to expire a policy after this period has elapsed, as well as sending renewal information at a certain point before this expiry occurs.
Lets say we have a policy that lasts for 5 days (just for a simple example) the event log may look something like this:
policy-created
day-has-passed
day-has-passed
day-has-passed
renewal-reminder-email-sent
day-has-passed
day-has-passed
policy-expired
Benefits of this approach include:
- All domain logic can be contained to where it should be.
- We can utilise competing consumers in common message brokers for easy redundancy
- Trigger no longer needs to know if subscriber is up, providing temporal decoupling.
- Easy to control, its a simple event.
Don't Use The Clock
To get the most of this pattern I’d recommend that you don't use the clock but hold a count (up or down) of how many units of time have passed relative to your requirements. So rather than something like this:
class Policy {
void onDayPassed(@Observes DayHasPassed evt){
if(expiryDate().isBefore(now())){
domainEvents.fire(new PolicyExpired());
}
}
}
Do something like this:
class Policy { isExpired(){
return daysSinceInception > lengthInDays;
} void onDayPassed(@Observes DayHasPassed evt){
daysSinceInception++;
if(isExpired())
domainEvents.fire(new PolicyExpired());
}
}
}
With a test looking something like
Scenario: If policy length elapsed, expire poilcy
Given PolicyValidForFiveDays
And DayHasPassed
And DayHasPassed
And DayHasPassed
And DayHasPassed
When DayHasPassed
Then PolicyExpired
Using this sort of counter over checking the clock will allow a test scenario to effectively fast forward time throughout an entire system, if used consistently, as all timed jobs will react to the exact same triggers. This means no more clock or data fudging needed to test your timed processes. A nice little win in my opinion.
Other Things To Think About
- Margin of error — If a process that has to react to a certain number of days passing has a small margin of error it may be more desirable to count hours passing than days, for example
- Source of events — cloud schedulers etc. may be a good fit. If creating a simple cron service, you will need to think about redundancy, can be solved with simple leader election or locking — at least you only have to do this once!