Cluster Singleton

Dependency

To use Cluster Singleton, you must add the following dependency in your project:

sbt
libraryDependencies += "com.typesafe.akka" %% "akka-cluster-typed" % "2.5.14"
Maven
<dependency>
  <groupId>com.typesafe.akka</groupId>
  <artifactId>akka-cluster-typed_2.12</artifactId>
  <version>2.5.14</version>
</dependency>
Gradle
dependencies {
  compile group: 'com.typesafe.akka', name: 'akka-cluster-typed_2.12', version: '2.5.14'
}

Introduction

Warning

This module is currently marked as may change in the sense of being the subject of active research. This means that API or semantics can change without warning or deprecation period and it is not recommended to use this module in production just yet—you have been warned.

For some use cases it is convenient and sometimes also mandatory to ensure that you have exactly one actor of a certain type running somewhere in the cluster.

Some examples:

  • single point of responsibility for certain cluster-wide consistent decisions, or coordination of actions across the cluster system
  • single entry point to an external system
  • single master, many workers
  • centralized naming service, or routing logic

Using a singleton should not be the first design choice. It has several drawbacks, such as single-point of bottleneck. Single-point of failure is also a relevant concern, but for some cases this feature takes care of that by making sure that another singleton instance will eventually be started.

Example

Any Behavior can be run as a singleton. E.g. a basic counter:

Scala
trait CounterCommand
case object Increment extends CounterCommand
final case class GetValue(replyTo: ActorRef[Int]) extends CounterCommand
case object GoodByeCounter extends CounterCommand

def counter(entityId: String, value: Int): Behavior[CounterCommand] = Behaviors.receiveMessage[CounterCommand] {
  case Increment ⇒
    counter(entityId, value + 1)
  case GetValue(replyTo) ⇒
    replyTo ! value
    Behaviors.same
}
Java
interface CounterCommand {}
public static class Increment implements CounterCommand { }
public static class GoodByeCounter implements CounterCommand { }

public static class GetValue implements CounterCommand {
  private final ActorRef<Integer> replyTo;
  public GetValue(ActorRef<Integer> replyTo) {
    this.replyTo = replyTo;
  }
}

public static Behavior<CounterCommand> counter(String entityId, Integer value) {
  return Behaviors.receive(CounterCommand.class)
    .onMessage(Increment.class, (ctx, msg) -> {
      return counter(entityId,value + 1);
    })
    .onMessage(GetValue.class, (ctx, msg) -> {
      msg.replyTo.tell(value);
      return Behaviors.same();
    })
    .build();
}

Then on every node in the cluster, or every node with a given role, use the ClusterSingleton extension to spawn the singleton. An instance will per data centre of the cluster:

Scala
val singletonManager = ClusterSingleton(system)
// Start if needed and provide a proxy to a named singleton
val proxy: ActorRef[CounterCommand] = singletonManager.spawn(
  behavior = counter("TheCounter", 0),
  "GlobalCounter",
  Props.empty,
  ClusterSingletonSettings(system),
  terminationMessage = GoodByeCounter
)

proxy ! Increment
Java
ClusterSingleton singleton = ClusterSingleton.get(system);
// Start if needed and provide a proxy to a named singleton
ActorRef<CounterCommand> proxy = singleton.spawn(
  counter("TheCounter", 0),
  "GlobalCounter",
  Props.empty(),
  ClusterSingletonSettings.create(system),
  new GoodByeCounter()
);

proxy.tell(new Increment());

Accessing singleton of another data centre

TODO

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.