Cron Starter
Overview
This starter provides a distributed cron scheduler. It loads SysCron records from the database, elects a single
leader via Redis, and publishes cron triggers to MQ (Pulsar) as CronTaskMessage. Your application consumes those
messages and executes the real business logic.
The scheduling side and the execution side are intentionally decoupled:
cron-starterdecides when to trigger and what context to send.- Your MQ consumer decides what business code to run after the trigger arrives.
Dependency
<dependency>
<groupId>io.softa</groupId>
<artifactId>cron-starter</artifactId>
<version>${softa.version}</version>
</dependency>Requirements
- Redis is required for leader election (
StringRedisTemplate). - Pulsar is required for task delivery (
PulsarTemplate). - Database contains the
SysCron/SysCronLogmetadata tables. - If you run in shared-db multi-tenancy mode, the runtime must also provide
TenantInfoService(normally withsystem.enable-multi-tenancy=trueand thetenant_infomodel/table available).
Configuration
MQ topic
mq:
topics:
cron-task:
topic: dev_demo_cron_task
flow-sub: dev_demo_cron_task_flow_subScheduler thread pool size
cron:
threads:
number: 1Multi-tenancy switch
cron-starter itself does not add a separate cron-specific tenant switch. It follows the ORM/runtime tenant mode:
system:
enable-multi-tenancy: trueWhen multi-tenancy is disabled, TenantInfoService is not expected and tenant-aware fan-out is not available.
Data Model
SysCron
Key fields:
name: cron job name.cronExpression: Quartz cron expression.cronSemantic: human readable description (seeCronUtils.cronSemantic).limitExecution:trueto enable limited executions.remainingCount: remaining executions whenlimitExecution=true.nextExecTime,lastExecTime: timestamps for reference or UI.redoMisfire,priority: reserved for future use in scheduler.description: free text.tenantJobMode: optional tenant dispatch mode. Option values arePerTenantandCrossTenant.active: active flag.
SysCronLog
Execution log for jobs. The Flow starter writes logs when it consumes cron tasks.
CronTaskMessage
Fields published to MQ:
cronId,cronNametriggerTime,lastExecTimecontext(request context snapshot used by the consumer)
Scheduling Flow
- One node becomes leader by holding Redis key
cron:scheduler:leader. - The leader loads all
SysCronrecords from metadata storage. - Each job is converted into a Quartz schedule and registered in the local scheduler pool.
- When a trigger fires, the scheduler publishes one or more
CronTaskMessagemessages. - Consumers restore
message.contextand execute the real business logic.
Implementation notes:
- Only one scheduler runs across the cluster at a time.
- Lease duration is 60s, renewal interval is 30s.
- The scheduler uses
ZoneId.systemDefault(). - A cron with
limitExecution=falseis scheduled repeatedly. - A cron with
limitExecution=trueis scheduled with an in-memory counter only.remainingCountis not persisted by the scheduler. - On startup, the current implementation registers jobs where
activeis NOTtrue(null/false). If your expectation is “active means enabled”, align data or adjust the condition in code.
Tenant Modes
Non-multi-tenant mode
Use this when system.enable-multi-tenancy=false or your application does not provide TenantInfoService.
Recommended SysCron setup:
- leave
tenantJobModeempty / null
Execution behavior:
- one MQ message is published for each trigger
message.context.tenantIdis not expandedmessage.context.crossTenant=false
Notes:
tenantJobMode=PerTenantis not supported here. The scheduler logs an error and skips dispatch because it cannot list active tenants.tenantJobMode=CrossTenantis technically harmless, but it has no practical meaning when the runtime is not in multi-tenant mode.
Shared-db multi-tenant mode
Use this when system.enable-multi-tenancy=true and TenantInfoService is available.
Mode 1: default single-dispatch
Recommended SysCron setup:
- leave
tenantJobModeempty / null
Execution behavior:
- one MQ message is published
- the current scheduler context is copied as-is
Use this only when you intentionally want “single dispatch with inherited context”.
For tenant-wide batch work, prefer PerTenant or CrossTenant explicitly.
Mode 2: per-tenant dispatch
Recommended SysCron setup:
tenantJobMode=PerTenant
Execution behavior:
- the scheduler loads all active tenant IDs from
TenantInfoService - one MQ message is published per active tenant
- each message gets its own
context.tenantId - each message keeps
context.crossTenant=false
Typical use cases:
- send monthly reports tenant by tenant
- run tenant-local cleanup jobs
- synchronize tenant-isolated business data
Mode 3: cross-tenant dispatch
Recommended SysCron setup:
tenantJobMode=CrossTenant
Execution behavior:
- one MQ message is published
context.tenantIdis left emptycontext.crossTenant=true
Typical use cases:
- global reconciliation
- system-wide statistics
- admin maintenance that must scan across all tenants
Consumer-side Execution Logic
The scheduler already decides the tenant context and puts it into CronTaskMessage.context.
Consumers should restore that context before calling ORM-backed services.
Example:
@Component
@ConditionalOnProperty(name = "mq.topics.cron-task.topic")
public class CustomCronConsumer {
@PulsarListener(
topics = "${mq.topics.cron-task.topic}",
subscriptionName = "${mq.topics.cron-task.flow-sub}"
)
public void onMessage(CronTaskMessage message) {
Runnable task = () -> {
// TODO: run your business logic here
// message.getCronName(), message.getTriggerTime(), message.getLastExecTime()
};
if (message.getContext() != null) {
ContextHolder.runWith(message.getContext(), task);
} else {
task.run();
}
}
}Important rule:
- If
SysCron.tenantJobMode=PerTenant, do not also annotate the downstream business entry method with@PerTenant, otherwise the job will be expanded twice.
How To Use
1. Create a cron job
Use one of the following ways to create cron jobs. UI creation is the primary recommended path.
-
UI (Recommended) Create a
SysCronrecord through your admin UI or metadata console. SettenantJobModeonly when you need multi-tenant fan-out or cross-tenant execution. -
Predefined data Seed
SysCronrecords during system initialization or migrations, then activate them when the service starts. -
SQL insert Insert into the
SysCrontable directly (fields may vary by schema).
INSERT INTO sys_cron
(name, cron_expression, cron_semantic, tenant_job_mode, limit_execution, remaining_count, active)
VALUES
('DailyReport', '0 0 2 ? * *', 'At 02:00 AM', 'PerTenant', false, 0, true);2. Execute once immediately
The controller exposes endpoints for immediate execution:
POST /SysCron/executeNow?id=123POST /SysCron/executeMultipleNow?ids=1&ids=2
Quartz Cron Expression
Format:
* * * ? * * [*]
- - - - - - -
| | | | | | |
| | | | | | +- Year (OPTIONAL)
| | | | | +---- Day of the Week (range: 1-7 or SUN-SAT, 1 standing for Monday)
| | | | +------ Month (range: 1-12 or JAN-DEC)
| | | +-------- Day of the Month (range: 1-31)
| | +---------- Hour (range: 0-23)
| +------------ Minute (range: 0-59)
+-------------- Second (range: 0-59)Special characters:
*match any?no specific value,list separator-range/increments
Examples:
0 0 2 ? * *run every day at 02:000 */5 * ? * *run every 5 minutes0 30 9 ? * MON-FRIrun on weekdays at 09:30