Using the Scheduler in EzyPlatform

Updated at 1784130698000
EzyPlatform provides background task scheduling through the shared Scheduler class, then specializes it into three runtime-specific schedulers:
  • AdminScheduler: used in the admin runtime.
  • WebScheduler: used in the web runtime.
  • SocketScheduler: used in the socket runtime.
These three classes do not define separate scheduling algorithms. They all inherit the same mechanism from Scheduler, giving each runtime its own independent scheduler so background tasks from admin, web, and socket do not get mixed together.

General Architecture

At its core, Scheduler has two main parts:
  • an inspector thread that scans the task list at a short interval, 5ms by default;
  • an executor pool that runs tasks whose scheduled time has arrived.
The flow looks like this:
flowchart TD
    A[Admin/Web/Socket Runtime] --> B[Inject the matching Scheduler]
    B --> C[start scheduler]
    C --> D[Inspector scans tasks periodically]
    D --> E{Is the task due?}
    E -- No --> D
    E -- Yes --> F[Submit task to executor pool]
    F --> G[Run Runnable]
    G --> H{One-time task?}
    H -- Yes --> I[Remove from task list]
    H -- No --> D
A task is not executed directly on the inspector thread. The inspector only checks timing; the actual work is submitted to the executor pool.

Scheduler Types

AdminScheduler is the scheduler for the admin runtime. It is declared as a singleton bean and started during the final admin configuration phase. Current admin background tasks, such as caching the latest platform version or checking whether to send platform update emails, are registered on this scheduler.
WebScheduler is also a singleton bean and is started when the web runtime completes configuration. It is suitable for web background tasks such as caching settings, watching configuration changes, or periodically reloading data.
SocketScheduler is slightly different: it is implemented as a static singleton instance through getInstance(), then registered into the bean context before the socket runtime finishes initialization. It is started during the final socket configuration phase.
From business code, all three schedulers are used in the same way. Inject the scheduler that matches the runtime you are working in.

Starting the Scheduler

In normal business services, you usually do not need to call start() yourself. EzyPlatform starts the scheduler at the runtime configuration layer:
  • admin runtime starts AdminScheduler;
  • web runtime starts WebScheduler;
  • socket runtime starts SocketScheduler.
The start() method is idempotent: calling it multiple times only starts the scheduler once.

Scheduling a Repeated Task

To run a task repeatedly, use:
scheduler.scheduleAtFixRate(
    () -> {
        // logic to run periodically
    },
    0,
    15,
    TimeUnit.SECONDS
);
Parameters:
  • parameter 1: the Runnable to execute;
  • parameter 2: the delay before the first run;
  • parameter 3: the period between runs;
  • parameter 4: the time unit.
Example in the admin runtime:
@EzySingleton
@AllArgsConstructor
public class MyAdminJob {

    private final AdminScheduler scheduler;

    public void start() {
        scheduler.scheduleAtFixRate(
            this::syncData,
            1,
            1,
            TimeUnit.HOURS
        );
    }

    private void syncData() {
        // sync data
    }
}
Example in the web runtime:
@Service
@AllArgsConstructor
public class MyWebCacheService {

    private final WebScheduler scheduler;

    public void start() {
        scheduler.scheduleAtFixRate(
            this::reloadCache,
            0,
            30,
            TimeUnit.SECONDS
        );
    }

    private void reloadCache() {
        // reload cache
    }
}
Example in the socket runtime:
@EzySingleton
@AllArgsConstructor
public class MySocketMonitor {

    private final SocketScheduler scheduler;

    public void start() {
        scheduler.scheduleAtFixRate(
            this::checkConnections,
            5,
            5,
            TimeUnit.SECONDS
        );
    }

    private void checkConnections() {
        // check connections
    }
}
Note that the current API name is scheduleAtFixRate, not scheduleAtFixedRate.

Scheduling a One-Time Task

If a task only needs to run once after a delay, use scheduleOneTime:
scheduler.scheduleOneTime(
    () -> {
        // logic to run only once
    },
    10,
    TimeUnit.SECONDS
);
After the task completes, the scheduler automatically removes it from the managed task list.

Canceling a Scheduled Task

A task is canceled using the same Runnable instance that was registered:
Runnable job = this::reloadCache;

scheduler.scheduleAtFixRate(
    job,
    0,
    15,
    TimeUnit.SECONDS
);

// Cancel later
scheduler.cancelSchedule(job);
Since the task key is the Runnable object itself, keep a reference to the Runnable if you need to cancel it later. Passing a new lambda to cancelSchedule will not cancel the old task.

Preventing Overlapping Runs

Scheduler keeps a runningTasks set to track tasks currently being executed. If a task is due but its previous run has not finished yet, the scheduler will not start another copy of the same task.
This prevents the same Runnable from running concurrently when the period is too short. However, if a task takes longer than its period, the next run may happen almost immediately after the previous one finishes, because the next scheduled time is already overdue.
For heavy tasks, choose a reasonable period or add your own load-control logic inside the task.

Error Handling

If a Runnable throws an exception, the scheduler catches it and logs a warning. The exception does not stop the scheduler and does not remove a repeated task from the schedule.
Example:
scheduler.scheduleAtFixRate(
    () -> {
        try {
            doSomething();
        } catch (Exception e) {
            // Handle business-specific error context if needed
        }
    },
    0,
    1,
    TimeUnit.MINUTES
);
Even though the scheduler already protects itself from task exceptions, important jobs should still handle exceptions explicitly so the logs contain useful business context.

Using a Named Executor

Besides scheduled tasks, Scheduler can also create a named single-thread ScheduledExecutorService:
ScheduledExecutorService executor =
    scheduler.getOrCreateSingleThreadScheduledExecutor("my-worker");

executor.scheduleWithFixedDelay(
    this::doWork,
    0,
    5,
    TimeUnit.SECONDS
);
When it is no longer needed, shut it down and remove it by name:
scheduler.removeAndShutdownScheduledExecutorService("my-worker");
This is useful when you need direct control over ScheduledExecutorService, such as using scheduleWithFixedDelay or isolating a group of tasks in its own worker.

Usage Notes

Do not pass a negative initialDelay, because the scheduler will throw IllegalArgumentException.
Do not call stop() on the default admin, web, or socket schedulers. These schedulers are created with the default constructor and are not stoppable; calling stop() will throw IllegalStateException.
Avoid very small periods for heavy tasks. The scheduler scans frequently, but the executor pool is still limited. By default, the execution pool size equals the number of available processors.
If a task may need to be canceled later, keep the same Runnable instance.
For a task that should only run once, use scheduleOneTime instead of canceling it manually inside the task.

Conclusion

AdminScheduler, WebScheduler, and SocketScheduler are runtime-specific schedulers built on top of EzyPlatform’s shared Scheduler mechanism. The most common usage is to inject the matching scheduler, register a Runnable with scheduleAtFixRate or scheduleOneTime, and let the runtime manage the scheduler lifecycle.
This design gives the admin, web, and socket modules separate spaces for background work while keeping a simple shared API for tasks such as periodic caching, setting watchers, system checks, and lightweight background jobs.

Table Of Contents