While looking for an alternative to Quartz for scheduling crons in a distributed environment, I stumbled upon Reddison’s RScheduledExeuctorService which was documented (as linked) for specifically that purpose.
From what I understand, Redisson asks for a serializable runnable which it then serializes and stores into Redis itself. Then, whenever a Redisson node comes online, it deserializes the task and one of the online Redisson nodes will get to execute the task at the scheduled interval.
i.e, I can schedule task Foo, shut down the server, remove Foo entirely from my source code, recompile, deploy, and the Foo task will still run, because it was serialized into Redis.
This seems really powerful and I’d like to make use of it. However, as far as I’m aware, the only way to give the deserialized instances of my task access to any of my necessary application data is to expose a static reference to it.
Moreover, there’s no way to check if a task has already been scheduled without also persisting the task ID with some sort of known reference, so I’ll have to do that too.
Lastly, unless I abstract away the behavior of the task, the serialization will prevent me from modifying the behavior of the task in future versions of my application, which is undesirable, so I’ll do that as well.
Putting this all together, I’ve come up with this:
public final class Scheduler { private static final Map<String, Cron> TASK_MAPPED_CRONS = new HashMap<>(); private static final Cron[] CRONS = new Cron[] { new HealthCheck() // email a digest to everyone, // charge advertisers $$, // register more crons here }; private static App currentApp; private Scheduler() {} public static final void initialize(App app, RedissonClient client) { currentApp = app; Lock lock = client.getLock("cronExecutor"); lock.lock(); // Don’t want to register the same task more than once try { syncronizeChrons(client); } catch (Exception e) { throw new RuntimeException(e); } finally { // Don’t want the lock to fall into limbo lock.unlock(); } } private static final void syncronizeChrons(RedissonClient client) { RScheduledExecutorService es = client.getExecutorService("cronExecutor"); RMap<String, String> tasks = client.getMap("cronExecutorTasks"); for (Cron cron : CRONS) { String taskID; if (!tasks.containsKey(cron.getClass().getName())) { RScheduledFuture<?> future = es.schedule(new Task(), CronSchedule.of(cron.getSchedule())); taskID = future.getTaskId(); tasks.put(cron.getClass().getName(), taskID); } else { // Task is already serialized in redis taskID = tasks.get(cron.getClass().getName()); } TASK_MAPPED_CRONS.put(taskID, cron); } es.registerWorkers(WorkerOptions.defaults().taskTimeout(0, TimeUnit.SECONDS)); } private static final class Task implements Runnable , Serializable { @RInject String taskID; @Override public void run() { execute(taskID); } } public static final void execute(String taskID) { Cron cron = TASK_MAPPED_CRONS.get(taskID); if (cron != null) { cron.accept(currentApp); } }}
public class HealthCheck implements Cron { @Override public void accept(App t) { System.out.println("Health check: OK!"); } @Override public String getSchedule() { return "*/10 * * * * *"; // 10 seconds }}
public interface Cron extends Consumer<App> { public String getSchedule();}
To test this, I booted up two separate JVMs, each spawning five new instances of my App, and initializing the scheduler with each of them.
As expected, only one instance of the HealthCheck cron got serialized into Redis, and as expected, every ten seconds, one and only one of the JVMs would print an OK health check. Shutting down one of the JVMs resulted in all of the health checks being done by the other. Shutting them both down and booting them back up resulted in them continuing where they left off.
(I’m leaving de-scheduling out to reduce complexity in the example)
Being a novice in this area, there’s a good chance I’ve overlooked something. This seems quite hacky of a solution after all. Perhaps even outside of the scope of its intended use.
Hoping to be peer reviewed by those who have experience with the RScheduledExecutorService.