Akka durable state
Having persistent actors with events journaling can be an excellent choice if used for the right use case, e.g. if you know you’ll benefit from having history of how entity state changed over time. Unfortunately, it comes with a cost and complexity. Traditional event sourcing architectures are not easy in their nature and the same applies to akka persistent actors built on top of these. You have commands, events, state changes, recovery, effects that have to be run in response to an event (but cannot be run from event handlers to avoid side effects on recovery), etc. All that makes any reasonably big application inherently complex, but that’s one of the tradeoffs you need to consider when choosing your weapons.
A few months ago, the Akka team has released a new flavour of actors persistence called Durable State that aims to simplify how actor state is managed and saved. Obviously, not all is gold, so don’t expect it to be a drop-in replacement for event-based persistence. While it’s simpler and easier to use, it also trades off some of the core traits and benefits of the classic event sourcing approach. Nevertheless, it may be worth giving it a look and having it at the back of your head for potential use cases.
Entering durable state
This new actor persistence flavour seems to resemble the classic CRUD approach while maintaining the well-known actor model of handling messages or commands. The difference is the API that conceptually is a function (State, Command) => State, while for event-based persistence it is (State, Event) => State.
At first glance, they look pretty similar. The difference is huge, though. In the event-based version, you need to have an event at hand in order to come up with a new state. That means you have to have a command handler that produces that event when executed. So it’s not the command handling that changes the state, it’s the event that, when persisted and applied to the current state, makes it transition to the new one.
Durable state shortens the cycle by eliminating events from the picture. In the durable state flavour, you just handle a command and as a result of command handling, you produce the new state that gets persisted. No event handlers that modify state, no recovery by replaying sequence of events to arrive at the current state.
This lack of events already exhibits the most important differences when compared to classical persistence:
- No events means no activity log, no history of what happened to the entity and how the state evolved over time.
- No ability to replay and reprocess events as only state snapshot is being stored.
- It's the command that comes up with a new state that gets persisted, which means only the recent state is saved and the next command overrides the previous one with an updated version. You can think of it as always having only one snapshot of state in classical persistence.
That means durable state is meant to be used in less-constrained use cases, where journaling, events replay, complex projections, etc. are not required and what you actually want is an akka-flavoured CRUD approach.
More on the SoftwareMill Tech Blog:
Despite the differences in the way entity/actor state is managed and persisted, durable state actors can benefit from all other goodies of the regular actor model. This is especially important in clustered and sharded environments.
Durable state actors can be sharded across multiple nodes in the same way as regular actors giving you the usual sharding guarantees (that there is at most one instance of a given entity in the cluster in given time). This, complemented with good passivation strategy for entity can be e.g. way simpler and more effective approach to CRUD-like state management than traditional horizontally scaled stateless services that would have to load state on every request, do its work and persist new state taking care of state versioning and concurrent writes.
Quick look at the API
Durable state is built exclusively on akka-typed, there is no way to use it with classical (untyped) akka API. Keep in mind that while some of the API bits have names similar to event-based persistence ones (e.g. Effect, EffectBuilder, some signals), they live in its own package, under akka.persistence.typed.state
.
object Subscription {
def apply(persistenceId: PersistenceId): DurableStateBehavior[SubscriptionCommand[?], SubscriptionState] =
DurableStateBehavior.apply[SubscriptionCommand[?], SubscriptionState](
persistenceId = persistenceId,
emptyState = SubscriptionState(active = false, validUntil = None),
commandHandler = commandHandler
)
def commandHandler(state: SubscriptionState, cmd: SubscriptionCommand[?]): Effect[SubscriptionState] = cmd match {
case Deactivate(replyTo) =>
Effect
.persist(state.copy(active = false))
.thenReply(replyTo)(_ => StatusReply.Ack)
case Extend(to, replyTo) =>
if (!state.active) {
Effect.reply(replyTo)(StatusReply.error("Cannot extend inactive subscription"))
} else {
val newCap = state.validUntil match {
case Some(currentCap) => if (currentCap.isBefore(to)) to else currentCap
case None => to
}
Effect.persist(state.copy(validUntil = newCap.some)).thenReply(replyTo)(_ => StatusReply.Ack)
}
case IsActive(replyTo) =>
Effect.reply(replyTo)(state.active)
}
sealed trait SubscriptionCommand[M] {
def replyTo: ActorRef[M]
}
final case class Deactivate(replyTo: ActorRef[StatusReply[Done]]) extends SubscriptionCommand[StatusReply[Done]]
final case class Extend(to: Instant, replyTo: ActorRef[StatusReply[Done]]) extends SubscriptionCommand[StatusReply[Done]]
final case class IsActive(replyTo: ActorRef[Boolean]) extends SubscriptionCommand[Boolean]
final case class SubscriptionState(active: Boolean, validUntil: Option[Instant])
}
As you can see, it’s not that different from classic event-based persistent actors.
Instead of EventSourcedBehavior
, you create DurableStateBehavior
. You still need to come up with a unique persistenceId
to identify the actor in the system (so that it can e.g. be sharded) and in the persistent storage. Next, you provide an initial state that it will start with when no state was saved before and the core of the behavior in the form of a command handler.
As in event-sourced behavior, this one returns Effect
as well. Again, note this is an Effect
from state
package but it works more or less the same way as the one for classical persistence - you just either persist state, do nothing and/or reply to the command sender. If you need to run some arbitrary side effects after state is persisted, you can chain persist
invocations with thenRun
that will be run after state gets persisted.
Whenever a command arrives when the actor is passivated, the state gets loaded into the memory and the current command is executed against this state.
An entity’s state can easily be implemented in a more FSM-like way as well. It’s just a matter of creating multiple different states that share the same base type and later, in the command handler, let the state handle commands that are allowed to be handled in this particular state.
object Subscription {
def apply(persistenceId: PersistenceId): DurableStateBehavior[SubscriptionCommand[?], SubscriptionState] =
DurableStateBehavior.apply[SubscriptionCommand[?], SubscriptionState](
persistenceId = persistenceId,
emptyState = SubscriptionState.Inactive(validUntil = None),
commandHandler = commandHandler
)
def commandHandler(state: SubscriptionState, cmd: SubscriptionCommand[?]): Effect[SubscriptionState] = state.onCommand(cmd)
sealed trait SubscriptionCommand[M] {
def replyTo: ActorRef[M]
}
final case class Activate(until: Instant, replyTo: ActorRef[StatusReply[Done]]) extends SubscriptionCommand[StatusReply[Done]]
final case class Deactivate(replyTo: ActorRef[StatusReply[Done]]) extends SubscriptionCommand[StatusReply[Done]]
final case class Extend(to: Instant, replyTo: ActorRef[StatusReply[Done]]) extends SubscriptionCommand[StatusReply[Done]]
final case class IsActive(replyTo: ActorRef[Boolean]) extends SubscriptionCommand[Boolean]
sealed trait SubscriptionState {
def onCommand(cmd: SubscriptionCommand[?]): Effect[SubscriptionState]
}
object SubscriptionState {
final case class Active(validUntil: Instant) extends SubscriptionState {
def onCommand(cmd: SubscriptionCommand[?]): Effect[SubscriptionState] = cmd match {
case Activate(_, replyTo) =>
Effect.reply(replyTo)(StatusReply.Ack)
case Deactivate(replyTo) =>
Effect
.persist(SubscriptionState.Inactive(validUntil.some))
.thenReply(replyTo)(_ => StatusReply.Ack)
case Extend(to, replyTo) =>
val newCap = if (validUntil.isBefore(to)) to else validUntil
Effect
.persist(copy(validUntil = newCap))
.thenReply(replyTo)(_ => StatusReply.Ack)
case IsActive(replyTo) => Effect.reply(replyTo)(true)
}
}
final case class Inactive(validUntil: Option[Instant]) extends SubscriptionState {
def onCommand(cmd: SubscriptionCommand[?]): Effect[SubscriptionState] = cmd match {
case Activate(until, replyTo) =>
Effect.persist(SubscriptionState.Active(until)).thenReply(replyTo)(_ => StatusReply.Ack)
case Deactivate(replyTo) =>
Effect.reply(replyTo)(StatusReply.Ack)
case Extend(to, replyTo) =>
Effect.reply(replyTo)(StatusReply.error("Cannot extend inactive subscription"))
case IsActive(replyTo) => Effect.reply(replyTo)(false)
}
}
}
}
As for the state persistence and its schema evolution over time, all usual akka rules apply. It needs to be serializable and you can configure your own serializers (be it protobuf, avro, kryo, whatever you come up with).
As this is a relatively new kid on the block, the only persistence plugin that supports it is the official Akka Persistence JDBC. It’s configured in a way similar to the eventsourced plugin, under the akka.persistence.state.plugin
key.
There is some integration with akka-projections to make use of durable state changes on the read side of the system but it’s relatively simple, so don’t expect it to give you the same amount of power as the eventsourced version, simply because there is no history of changes to build your projection from, just stream of updates to the state.
Closing notes
Akka’s recent persistence addition in the form of durable state is very convenient for use cases where you don’t need the benefits of event sourcing such as event log, by event projections, ability to replay state etc. If you’re good with storing just a recent state snapshot, it may be worth giving it a look as it’s definitely less complex than its event sourced counterpart.