A zero* dependency EventLoop for async Java applications like servers.
zero* - I use Log4J for logging purposes
Explore the docs »
Report Bug
·
Request Feature
I created this project because I needed it for my JNet and EnderSync projects. Everything else I found online might have worked but would result in too much boilerplate code or just had an API that was pretty big. I just wanted a simple EventLoop without* any other dependencies which could process my events.
- Events
- Queue events
- Handle events with multiple custom handler functions
- Prioritize specific events if necessary
- Callbacks
- Queue events with callbacks that can be completed from inside event handlers.
- Multithreaded
<project>
...
<repositories>
<repository>
<id>maximilianheidenreich</id>
<name>GitHub MaximilianHeidenreich Apache Maven Packages</name>
<url>https://maven.pkg.github.com/maximilianheidenreich/*</url>
</repository>
</repositories>
...
<dependencies>
<dependency>
<groupId>de.maximilian-heidenreich</groupId>
<artifactId>jeventloop</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
....
</project>
Custom events can be used to execute specific handlers for specific events. Custom events can store any data or logic you want them to. Just extends the default Event class and implement anything you need.
The Event
class has a generic argument D
which indicates the type of data that can be consumed inside callbacks.
If your Event does not need callbacks, feel free to use the Void
type.
super()
inside any custom constructor! Otherwise, your Event won't be handled!
class MyCoolEvent extends AbstractEvent<D> { // "D" is the type of data your callbacks can consume.
// You can store custom data associated with your Event
public String message;
// You can use a custom constructor to initialize your custom Event
public MyCoolEvent(String message) {
super(); // ! Call super() ! Otherwise the Event is corrupted and will be ignored !
this.message = message;
}
// You can add custom logic to your Event
public void print() {
System.out.println(this.message);
}
}
class Main {
public static void main(String[] args) {
// Create a default Event loop (dispatcher: SingleThreadedExecutor, task executor: workStealingExecutor)
EventLoop eventLoop = new EventLoop();
// OR: Provide custom Executors
EventLoop eventLoop = new EventLoop(dispatchExecutor, taskExecutor);
// Start the Event loop (You probably want to register some handlers first tho!)
eventLoop.start();
}
}
You can register as many as you need, and they will be executed in order (first registered -> first executed).
class Main {
public static void main(String[] args) {
// ...
// You can add a simple lambda function for small handlers
eventLoop.addEventHandler(MyAwesomeEvent.class, (event) -> {
event.print();
// Do some other stuff
});
// You can add a function reference for bigger tasks
eventLoop.addEventHandler(MyAwesomeEvent.class, Main::bigHandleLogic);
}
public static void bigHandleLogic(MyAwesomeEvent event) {
// Do some work e.g. process data
String data = event.message.toUpperCase();
// If you want to break the chain of execution (stop executing handlers registered after this one):
Event.cancel();
// If there was an uncaught exception, it will get logged and succeeding handler
// will still get executed!
throw new RuntimeException("Oops, something went wrong!");
// Optionally complete callbacks when the data is ready
event.complete(data);
// Optionally except callbacks which gives you the opportunity to handle exceptions
event.except(new Exception("You should really use a custom exception to know ehat went wrong!"));
}
}
If you enqueue an event, it will be handled some time in the future. If you depend on some returning data,
you can use the returned CompletableFuture<D>
to handle your data / any exceptions.
A possible usecase could be inside a networking application where you need to send a request and handle a response for it you will receive some time in the future.
class Main {
public static void main(String[] args) {
// Queue an event and ignore any callbacks
eventLoop.dispatch(new MyAwesomeEvent("Hello World"));
// Queue an event and handle callback data / exceptions
eventLoop.dispatch(new MyAwesomeEvent("Hello Async World"))
.thenApply(data -> {
System.out.println("Got my processed data: " + data);
})
.exceptionally(ex -> { ex.printStackTrace(); });
//...
}
}
Iterations | Average time / event | System | Description |
---|---|---|---|
200 000 | < 0.5ms | Mac Mini M1 (2020) - 16GB ram | No Thread.sleep() |
20 000 | 1 ms | Mac Mini M1 (2020) - 16GB ram | Random Thread.sleep(t) after each dispatch (0 < t < 6) & inside handler (0 < t < 4) |
Note: These benchmarks do not reflect real application usage but try to simulate it somewhat using Thread.sleep() with random intervals.
- Time measured from creating the event instance till callback execution.
- The code was executed while my system was at ~50% CPU load.
- This is just a rough benchmark to test whether it can handle a good amount of basic throughput. If you want to benchmark it yourself, feel free to do so and create a pull request afterwards to update the README.
Benchmark code:
class Main {
static Random ran = new Random();
public static void main(String[] args) {
ConsoleAppender console = new ConsoleAppender();
String PATTERN = "%d %5p | %20C{1} | %m%n";
console.setLayout(new PatternLayout(PATTERN));
console.activateOptions();
Logger.getRootLogger().addAppender(console);
Logger.getRootLogger().setLevel(Level.ERROR);
Logger.getRootLogger().info("Starting ...");
EventLoop eventLoop = new EventLoop();
eventLoop.addEventHandler(TimingEvent.class, Test::testHandler);
eventLoop.start();
AtomicLong sum = new AtomicLong();
int iterations = 200000;
int i;
AtomicInteger completed = new AtomicInteger();
for (i = 0; i < iterations; i++) {
System.out.println("Current iteration: " + i);
TimingEvent e = new TimingEvent(System.currentTimeMillis());
eventLoop.dispatch(e)
.thenAccept(s -> {
System.out.println("\t\t" + s + " ms");
sum.addAndGet(s.intValue());
completed.addAndGet(1);
});
Thread.sleep(ran.nextInt(7));
}
while (completed != iterations) {}
System.out.println("Average time: " + sum.longValue() / iterations + "ms");
}
public static void testHandler(TimingEvent event) {
try {
int s = ran.nextInt(4);
Thread.sleep(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
event.complete(event.getCurrentTimeDiff());
}
}
Feel free to contribute to this project if you find something that is missing or can be optimized. I want to retain the original vision of a simple yet usable library, so please keep that in mind when proposing new features. If you do so, please follow the following steps:
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
Maximilian Heidenreich - github@maximilian-heidenreich.de
Project Link: https://github.com/MaximilianHeidenreich/JEventLoop
Project Icon: https://github.com/MaximilianHeidenreich/JEventLoop/blob/master/assets/Icon-1024.png