Actors are a fantastic way to model concurrency and a much easier mental model than when working with plain threads and shared memory. However let’s say that you’re writing a very simple library that needs to run some simple background tasks. You could pull in one of the popular Actor implementation libraries out there, but they don’t come for free and can sometimes be heavy-weight.

There is a small movement in the JVM communities to move toward completely self-contained libraries. I am personally in favor of this movement, but it does mean it can make writing libraries a touch more difficult. So, if I’m writing a library and I want to do some async stuff and I really like using actors, what do I do? Well I personally like to program in the actor model (primarily) for two things

  • Messages are immutable
  • Message passing as a way of communicating

So the question is, can we get this with zero runtime dependencies? Of course we can, but how much work is it going to be? Let’s find out.

The Exercise

Before we jump right into the meaty bits, we need to setup a problem that our examples will be trying to solve. Let’s say that I’m writing a library that will do some local file-caching. As part of this, the library needs to handle active cache expiry. Active meaning that we don’t wait until the value is read again to do the cleanup, meaning we need some sort of asynchronous monitor.

Immutable Messages

There is nothing stopping from creating immutable data-structures in Java. However it may not be as easy as using say case classes in Scala. However it is most certainly possible. However, this one is pretty easy to get with very little work on our part. Java has long had annotation processors, which is a way to generate code based on annotations at compile time. You can imagine that this is very useful, especially for defining immutable objects. Luckily for us, someone has already went through the work to create a generator for immutable data. If you are not already familiar, meet Immutables.

Let’s define some models that we’ll use later

 1 @Value.Immutable
 2 public interface CacheCreated {
 3   Path getFilePath();
 4   Long getTtl();
 5   TimeUnit getTtlUnit();
 6 }
 7 
 8 @Value.Immutable
 9 public interface CacheExpired {
10   Path getFilePath();
11 }

That’s it! We now have immutable data-objects that are easy to work with. Let’s move on to the Actors.

Actors

Well we already know we have threads for running concurrent operations, but how do we handle the message passing? Simple, we define a mailbox that actors receive messages into and process out of. But what is a mailbox if not just a simple queue. Java has a bunch of those! In fact, there is a nice thread-safe one that works for us, the ConcurrentLinkedQueue. So that means we can define an actor as

 1 final ConcurrentLinkedQueue<Object> mailbox = new ConcurrentLinkedQueue<>();
 2 
 3 Thread actor = new Thread(() -> {
 4 
 5   while (true) {
 6     Thread.sleep(10);
 7 
 8     Object msg = mailbox.poll();
 9     if (msg == null) {
10       continue;
11     }
12 
13     try {
14       // process message
15     } catch (Exception e) {
16       // handle exception(s)
17     }
18   }
19 
20 });
21 actor.start();

Let’s break this down so we understand all that is happening here. First the mailbox

final ConcurrentLinkedQueue<Object> mailbox = new ConcurrentLinkedQueue<>();

All variables used within lambdas or anonymous inner classes must be effective final, thus the final. Note that we are also typing the collection to Object, why? This allows us to send any kind of messages to our actors and the actors can pattern match to find out what the message is. Since we’re in Java, pattern matching really just means doing a bunch of instaceof statements.

while (true) {
  Thread.sleep(10);

To keep the actor alive, the thread cannot end or be “done.” Because of this, we have the thread run in an infinite loop. However we also don’t want our process in a “busy wait.” To get around this we sleep for 10ms at the beginning of each loop. 10ms is arbitrary and is just represents the trade-off between using resources and responding to events in a timely manner.

Object msg = mailbox.poll();
if (msg == null) {
  continue;
}

Attempt to read the first thing in the mailbox (if anything is there). If there is nothing in the mailbox then null is returned. In this case we simply jump back up to the beginning of the infinite loop which causes us to sleep for 10ms before checking again.

Object msg = mailbox.poll();
try {
  // process message
} catch (Exception e) {
  // handle exception(s)
}

For the same reason that we put everything into a while, we also need to catch any errors that might occur. If an exception is thrown, then we need to have code to handle the error and allow the actor to continue processing. Otherwise the exception could crash our thread and effectively kill our actor. Note that supervision is not something our implementation has.

File-Caching Actors

For my system I will define two actors. One actor to receive created events and monitor when cache items should be expired. This actor will send messages to another actor which will take care of the actual file deletion.

Putting what we defined above to good use, I can define my actor as

1 final ConcurrentLinkedQueue<Object> ttlWatcherMailbox = new ConcurrentLinkedQueue<>();
2 final ConcurrentLinkedQueue<Object> cacheDeleterMailbox = new ConcurrentLinkedQueue<>();
 1 Thread cacheWatcher = new Thread(() -> {
 2   Map<Path, DateTime> cacheExpiry = new HashMap<>();
 3   int count = 0;
 4   while (true) {
 5     try {
 6       count++;
 7       Thread.sleep(10);
 8 
 9       // check for new items to store
10       Object msg = ttlWatcherMailbox.poll();
11       if (msg == null) break;
12       if (msg instanceof CacheCreated) {
13         CacheCreated cc = (CacheCreated) msg;
14         cacheExpiry.put(cc.getFilePath(), calculateExpiryDate(cc.getTtl(), cc.getTtlUnit());
15       } else {
16         // log unhandled message
17       }
18 
19       // scan cached objects (every 100ms, roughly)
20       if (count % 10 != 0) continue;
21       for( Map.Entry<Path, DateTime> entry : cacheExpiry.iterator() ) {
22         if (isExpired(entry.getValue()) {
23           cacheDeleterMailbox.offer(ImmutableCacheExpired.builder()
24               .filePath(entry.getValue())
25               .build());
26         }
27       }
28     } catch (Exception e) {
29       // log out errors
30     }
31   }
32 });
 1 Thread cacheDeleter = new Thread(() -> {
 2   while (true) {
 3     try {
 4       Thread.sleep(10);
 5 
 6       Object msg = cacheDeleterMailbox.poll();
 7       if (msg == null) continue;
 8       if (msg instanceof CacheExpired) {
 9         CacheExpired ce = (CacheExpired) msg;
10         deleteFile(ce.getFilePath());
11       } else {
12         // log unhandled message
13       }
14     } catch (Exception) {
15       // log out errors
16     }
17   }
18 });
1 cacheWatcher.start();
2 cacheDeleter.start();

Bam! We now have simple actors performing our background jobs and communicate by message passing. We could take this further if wanted and use some of Java’s concurrent blocking queues which would give us back-pressure as well. And like that, there are many small things you could do to add the features that you most value in a full-blown actor framework / library.

Improving our API

If we want to take our working code a little bit further we can improve the current API to abstract some of the details for how this is working. First let’s define a central place to create our actors and store all of our mailboxes. Also, mailboxes should have an address, let’s do this by naming our actors / mailboxes.

 1 class ActorRef {
 2   private ConcurrentLinkedQueue<Object> mailbox = new ConcurrentLinkedQueue<>();
 3   private String name;
 4 
 5   public ActorRef(String name) {
 6     this.name = name;
 7   }
 8 
 9   public void tell(Object msg) {
10     mailbox.offer(msg);
11   }
12 
13   public Object getLetter() {
14     return mailbox.poll();
15   }
16 
17   public int getLetterCount() {
18     return mailbox.size();
19   }
20 }
 1 class ActorSystem {
 2   private Map<String, ActorRef> registry = new HashMap<>();
 3 
 4   public ActorRef actor(String name, Consumer<Object> f) {
 5     ActorRef ref = new ActorRef(name);
 6     registry.put(name, ref);
 7 
 8     Thread t = new Thread(() -> {
 9       while (true) {
10         if (ref.getLetterCount() == 0) {
11           Thread.sleep(10);
12         }
13         f.accept(ref.getLetter());
14       }
15     });
16     t.start();
17 
18     return ref;
19   }
20 
21   public Optional<ActorRef> lookup(String name) {
22     ActorRef ref = registry.get(name);
23     return Optional.of(ref);
24   }
25 }

Modeling this off of Akka, we now have an ActorRef which wraps the mailbox and an ActorSystem that tracks all of the actors in our system and also creates some easier utilities for creating actors. We can now define actors in our system as

1 ActorSystem system = new ActorSystem();
2 
3 ActorRef echoActor = system.actor("echo", (Object msg) -> {
4   if (msg instanceof String) {
5     System.out.println((String) msg);
6   }
7 });
8 
9 echoActor.tell("Hello, World!");

and we can handle communication between two named actors as

 1 ActorSystem system = new ActorSystem();
 2 
 3 ActorRef ping = system.actor("ping", (Object msg) -> {
 4   if (msg instanceof String) {
 5     String sMsg = (String) msg;
 6     if (sMsg.equals("ping")) {
 7       System.out.println("ping");
 8       system.lookup("pong").ifPresent((ActorRef pong) -> pong.tell("pong"));
 9     }
10   }
11 }
12 ActorRef pong = system.actor("pong", (Object msg) -> {
13   if (msg instanceof String) {
14     String sMsg = (String) msg;
15     if (sMsg.equals("pong")) {
16       System.out.println("pong");
17       system.lookup("ping").ifPresent((ActorRef ping) -> ping.tell("ping"));
18     }
19   }
20 }

Sweet! Now we have two actors that can look each other up and communicate back and forth endlessly. Not that this is a useful example, but it shows out our API improvements work.

Of course you can continue adding little improvements like this until you are eventually building a full actor implementation (which you shouldn’t do). So this brings us to our next question.

Should You Do This?

So we’ve seen how we can create some really bare-bones actors but the question really is, “is it worth it?” The answer to this question depends on what you’re doing. If all you need is some very simple concurrency within your library, then this solution is fantastic as it’s zero-dependency. However if you are performing very complex concurrent tasks that require a lot of coordination or cooperation, then using a more full-featured framework may be to your benefit. The title did say this was a “poor man’s” implementation, which means we’re missing many of the feature that would come in a typical actor framework / library.

And actually, if you take a look at Akka (a popular Actor framework on the JVM), you’ll notice that the dependency tree is surprisingly small. So, if you must require all the power of a full-blown actor implementation, at least it’s not bloating your dependency tree too much.

sbt> akka-actor/dependencyGraph

             +-------------------+
             |akka-actor_2.11 [S]|
             | com.typesafe.akka |
             |   2.4-SNAPSHOT    |
             +-------------------+
                |          |
                |          -------------
                |                      |
                v                      v
  +---------------------------+ +------------+
  |scala-java8-compat_2.11 [S]| |   config   |
  |  org.scala-lang.modules   | |com.typesafe|
  |           0.7.0           | |   1.3.0    |
  +---------------------------+ +------------+