Skip to content

A pluggable actor system written in java leveraging modern features from JDK21+

License

Notifications You must be signed in to change notification settings

CajunSystems/cajun

Repository files navigation

Cajun

A pluggable actor system written in java leveraging modern features from JDK21+

Alt Text

An actor is a concurrent unit of computation which guarantees serial processing of messages with no need for state synchronization and coordination. This guarantee of actors mainly comes from the way actors communicate with each other, each actor send asynchronous messages to other actors and each actor only reads messages from its mailbox.

Actor architecture

Prerequisites

  • Java 21+ (with --enable-preview flag)

Usage

Creating actors

There are two styles of creating actors, one is the Object-oriented style and Functional style

Caution: When dealing with state of actors, be sure not to allow mutable objects to escape to external actors, this could cause unwanted state mutations, this is mainly due to the nature of referential objects in Java.

  1. Object-oriented style

This is the default way of creating actors, we extend from the Actor<M> class and we implement the receive method where we add logic to handle the message and state mutations.

public sealed interface GreetingMessage permits HelloMessage, ByeMessage, GetHelloCount, Shutdown {
}

public record HelloMessage() implements GreetingMessage {
}

public record ByeMessage() implements GreetingMessage {
}

public record Shutdown() implements GreetingMessage {
}

public record GetHelloCount(Pid replyTo) implements GreetingMessage {
}

public class GreetingActor extends Actor<GreetingMessage> {

    private int helloCount;

    public GreetingActor(ActorSystem system, String actorId) {
        super(system, actorId);
        this.helloCount = 0;
    }

    @Override
    public void receive(GreetingMessage message) {
        switch (message) {
            case HelloMessage ignored -> {
                // Updating state of the actor
                helloCount++;
            }
            case GetHelloCount ghc -> {
                // Replying back to calling actor
                ghc.replyTo().tell(new HelloCount(helloCount));
            }
            case ByeMessage ignored -> {
                // Sending a message to self
                self().tell(new Shutdown());
            }
            case Shutdown ignored -> {
                // Stopping actor after processing all messages
                stop();
            }
        }
    }
}
  1. Functional style actor

When creating a FunctionalActor we need to know the State and Message that the actor is going to be using, then we define call receiveMessage on the FunctionalActor to program the state changes and message handling logic.

sealed interface CounterProtocol {

    record CountUp() implements CounterProtocol {
    }

    record GetCount(Pid replyTo) implements CounterProtocol {
    }
}

public static void main(String[] args) {
    var counterActor = new FunctionalActor<Integer, CounterProtocol>();
    var counter = actorSystem.register(counterActor.receiveMessage((state, message) -> {
        switch (message) {
            case CounterProtocol.CountUp ignored -> {
                return state + 1;
            }
            case CounterProtocol.GetCount gc -> gc.replyTo().tell(new HelloCount(i));
        }
        return state;
    }, 0), "Counter-Actor");
    var receiverActor = actorSystem.register(CountReceiver.class, "count-receiver-1");
    counter.tell(new CounterProtocol.CountUp());
    counter.tell(new CounterProtocol.CountUp());
    counter.tell(new CounterProtocol.CountUp());
    counter.tell(new CounterProtocol.CountUp());
    counter.tell(new CounterProtocol.GetCount(receiverActor));
}

Using the actor system

After creating the actor we have to use the actor system to spawn them and send messages.

class CountReceiver extends Actor<HelloCount> {

    public CountReceiver(ActorSystem system, String actorId) {
        super(system, actorId);
    }

    @Override
    protected void receive(HelloCount helloCount) {
        System.out.println("Count" + helloCount);
    }
}

public static void main(String[] args) {
    var actorSystem = new ActorSystem();
    var pid1 = actorSystem.register(GreetingActor.class, "greeting-actor-1");
    var receiverActor = actorSystem.register(CountReceiver.class, "count-receiver");
    pid1.tell(new HelloMessage());
    pid1.tell(new GetHelloCount(receiverActor)); // Count: 1
}

Running examples

To run examples in the project, you can leverage the gradle task runner (--enable-preview flag is already enabled for gradle tasks)

./gradlew -PmainClass=examples.TimedCounter run

Feature roadmap

  1. Actor system and actor lifecycle
    • Create Actor and Actor System
    • Support message to self for actor
    • Support hooks for start and shutdown of actor
    • Stateful functional style actor
    • Timed messages
  2. Actor metadata management with etcd
  3. Actor supervision hierarchy and fault tolerance
  4. Persistent state and messaging for actors
  5. Partitioned state and sharding strategy

About

A pluggable actor system written in java leveraging modern features from JDK21+

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages