A lightweight job scheduling library for Java 21+. It provides a simple, modular, and dependency-free core domain for job scheduling. It can run in-memory for small applications or use a durable backend (like PostgreSQL) for distributed environments.
If you (or an AI agent) are extending, using, or contributing to this library, you must adhere to the following strict rules:
- At-Least-Once Delivery: The system guarantees that a job will run at least once. Due to lease expirations or worker crashes, a job might run more than once. All
Job.act()implementations MUST be idempotent. - No Time Drift: When calculating the next execution time, the system passes the original scheduled time (
Ticket.dueTime()) toRoutine.next(after), never the actual completion time (Instant.now()). - Distributed Locking: The JDBC implementation relies on
SELECT ... FOR UPDATE SKIP LOCKED. This ensures atomic claims across multiple pods without requiring a separate consensus system (like ZooKeeper). - Timezone Transparency: Routines must not rely on system default timezones. For instance,
CronRoutinerequires an explicitZoneIdin its constructor.
- Virtual Threads: The scheduler loop (
eos-ticker) uses Java 21+Thread.ofVirtual(). It spawns a new virtual thread for every claimed ticket and a separate background virtual thread to continuously renew the lease while the job is running. Do not use traditional thread pools.
The project is divided into distinct Maven modules. You only import what you need.
eos-core: The pure Java domain model (7 interfaces). Zero external dependencies.eos-ram: In-memory implementations (RamAgenda,RamQueue,IntervalRoutine, etc.).eos-decorators: Standard decorators (LoggedJob,SafeJob,RetryJob,DeadLetterJob).eos-ticker: The scheduler loop utilizing Java 21 Virtual Threads.eos-jdbc: Durable persistence using JDBC.
This project is deployed via JitPack.
First, add the JitPack repository to your pom.xml:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>For lightweight, single-instance applications:
<dependencies>
<dependency>
<groupId>com.github.maniac4j</groupId>
<artifactId>eos-core</artifactId>
<version>Tag</version>
</dependency>
<dependency>
<groupId>com.github.maniac4j</groupId>
<artifactId>eos-ram</artifactId>
<version>Tag</version>
</dependency>
<dependency>
<groupId>com.github.maniac4j</groupId>
<artifactId>eos-ticker</artifactId>
<version>Tag</version>
</dependency>
</dependencies>For clustered environments requiring persistence:
<dependencies>
<dependency>
<groupId>com.github.maniac4j</groupId>
<artifactId>eos-core</artifactId>
<version>Tag</version>
</dependency>
<dependency>
<groupId>com.github.maniac4j</groupId>
<artifactId>eos-jdbc</artifactId>
<version>Tag</version>
</dependency>
<dependency>
<groupId>com.github.maniac4j</groupId>
<artifactId>eos-ticker</artifactId>
<version>Tag</version>
</dependency>
</dependencies>(Remember to include your specific JDBC driver).
import uz.maniac4j.eos.core.*;
import uz.maniac4j.eos.ram.*;
import uz.maniac4j.eos.ticker.VirtualThreadTicker;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void main(String[] args) {
// 1. Define an idempotent Job
final Job myJob = input -> System.out.println("Processing: " + input);
// 2. Register it in a Catalog
final MapCatalog catalog = new MapCatalog(
java.util.Map.of("print-job", myJob)
);
// 3. Setup Memory Structures (or use JDBC alternatives)
final ConcurrentHashMap<String, Ticket> tickets = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, Routine> routines = new ConcurrentHashMap<>();
final ConcurrentHashMap<String, java.time.Instant> leases = new ConcurrentHashMap<>();
final Agenda agenda = new RamAgenda(tickets, routines);
final Queue queue = new RamQueue(tickets, routines, leases);
// 4. Schedule the Task
agenda.append(
"report-series",
"print-job",
"Data",
new IntervalRoutine(Duration.ofSeconds(60))
);
// 5. Start the Scheduler Loop
final Sink sink = new Sink() {
public void info(String msg) { System.out.println(msg); }
public void error(String msg, Throwable cause) { cause.printStackTrace(); }
};
final VirtualThreadTicker ticker = new VirtualThreadTicker(
queue, catalog, sink,
Duration.ofSeconds(5), // Polling interval
Duration.ofMinutes(5) // Lease timeout
);
Thread.ofVirtual().start(ticker);
}
}Use the eos-decorators module to add behavior without modifying the job itself.
// Example: A job that retries 3 times, catches fatal errors, and logs activity.
final Job robustJob = new LoggedJob(
new SafeJob(
new RetryJob(originJob, 3),
systemSink
),
systemSink,
"MyImportantJob"
);