Scala Protocol Buffer
Latest commit 207353d Mar 16, 2017 @notxcain Bump version in README.md
Permalink
Failed to load latest commit information.
core/src/main Add `StateT` based runtime (#24) Mar 16, 2017
example/src/main Fix memory leak in scheduler (#18) Feb 21, 2017
project Remove aecor-bench Dec 8, 2016
schedule/src/main
tests/src/test/scala/aecor/tests Add `StateT` based runtime (#24) Mar 16, 2017
.gitignore Rename, refactor, break things Jul 20, 2016
.scalafmt.conf Behavior overhaul (#11) Jan 31, 2017
.travis.yml Build for Scala 2.12.0 (#9) Dec 8, 2016
LICENSE Welcome, Aecor! Jun 5, 2016
README.md Bump version in README.md Mar 16, 2017
build.sbt Use Bump.Minor in release Mar 15, 2017
version.sbt Set version to 0.16.0-SNAPSHOT Mar 16, 2017

README.md

Build Status Maven Central Join the chat at https://gitter.im/notxcain/aecor

Aecor

Typeful runtime for eventsourced behaviors

Aecor is an opinionated library to help building scalable, distributed eventsourced services written in Scala. It uses Akka for distribution and fault tolerance. With the help of Cats and Shapeless to reach type safety.

Aecor works on Scala 2.11 and 2.12 with Java 8.

The name Aecor (lat. ocean) is inspired by a vision of modern distributed applications, as an ocean of messages with pure behaviors floating in it.

Installing Aecor

To start using Aecor Runtime add the following to your build.sbt file:

scalaOrganization := "org.typelevel"
libraryDependencies += "io.aecor" %% "aecor-core" % "0.15.0"
scalacOptions += "-Ypartial-unification"

Defining and running behavior

Let's start with entity operations:

sealed trait SubscriptionOp[A] {
  def subscriptionId: String
}
object SubscriptionOp {
  case class CreateSubscription(subscriptionId: String, userId: String, productId: String, planId: String) extends SubscriptionOp[Unit]
  case class PauseSubscription(subscriptionId: String) extends SubscriptionOp[Unit]
  case class ResumeSubscription(subscriptionId: String) extends SubscriptionOp[Unit]
  case class CancelSubscription(subscriptionId: String) extends SubscriptionOp[Unit]
}

Entity events with persistent encoder and decoder:

import aecor.data.Folded.syntax._
import cats.syntax.option._

sealed trait SubscriptionEvent
object SubscriptionEvent {
  case class SubscriptionCreated(subscriptionId: String, userId: String, productId: String, planId: String) extends SubscriptionEvent
  case class SubscriptionPaused(subscriptionId: String) extends SubscriptionEvent
  case class SubscriptionResumed(subscriptionId: String) extends SubscriptionEvent
  case class SubscriptionCancelled(subscriptionId: String) extends SubscriptionEvent

  implicit val persistentEncoder: PersistentEncoder[SubscriptionEvent] = `define it as you wish`
  implicit val persistentDecoder: PersistentDecoder[SubscriptionEvent] = `and this one too`
}

Folder[F, E, S] instance represents the ability to fold Es into S, with effect F on each step. Aecor runtime uses Folded[A] data type, with two possible states: Next(a) - says that a should be used as a state for next folding step Impossible - says that folding should be aborted (underlying runtime actor throws IllegalStateException)

sealed trait SubscriptionStatus
object SubscriptionStatus {
  case object Active extends SubscriptionStatus
  case object Paused extends SubscriptionStatus
  case object Cancelled extends SubscriptionStatus
}

case class Subscription(status: SubscriptionStatus)
object Subscription {
  import SubscriptionStatus._

  implicit def folder: Folder[Folded, SubscriptionEvent, Option[Subscription]] =
    Folder.instance(Option.empty[Subscription]) {
      case Some(subscription) => {
        case e: SubscriptionCreated =>
          impossible
        case e: SubscriptionPaused =>
          subscription.copy(status = Paused).some.next
        case e: SubscriptionResumed =>
          subscription.copy(status = Active).some.next
        case e: SubscriptionCancelled =>
          subscription.copy(status = Cancelled).some.next
      }
      case None => {
        case SubscriptionCreated(subscriptionId, userId, productId, planId) =>
          Subscription(Active).some.next
        case _ =>
          impossible
      }
    }
}

Now let's define a behavior that converts operation to its handler. A Handler[State, Event, Reply] is just a wrapper around State => (Seq[Event], Reply), you can think of it as Kleisli[(Seq[Event], ?), State, Reply] i.e. a side-effecting function with a side effect being a sequence of events representing state change caused by operation.

val behavior = Lambda[SubscriptionOp ~> Handler[Option[Subscription], SubscriptionEvent, ?]] {
  case CreateSubscription(subscriptionId, userId, productId, planId) => {
    case Some(subscription) =>
      // Do nothing reply with ()
      Seq.empty -> ()
    case None =>
      // Produce event and reply with ()
      Seq(SubscriptionCreated(subscriptionId, userId, productId, planId)) -> ()
  }
  case PauseSubscription(subscriptionId) => {
    case Some(subscription) if subscription.status == Active =>
      Seq(SubscriptionPaused(subscriptionId)) -> ()
    case _ =>
      Seq.empty -> ()
  }
  case ResumeSubscription(subscriptionId) => {
    case Some(subscription) if subscription.status == Paused =>
      Seq(SubscriptionResumed(subscriptionId)) -> ()
    case _ =>
      Seq.empty -> ()
  }
  case CancelSubscription(subscriptionId) => {
    case Some(subscription) =>
      Seq(SubscriptionCancelled(subscriptionId)) -> ()
    case _ =>
      Seq.empty -> ()
  }
}

Then you define a correlation function, entity name and a value provided by correlation function form unique primary key for aggregate. It should not be changed in the future, at least without prior event migration.

def correlation: Correlation[SubscriptionOp] = {
  def mk[A](op: SubscriptionOp[A]): CorrelationF[A] = op.subscriptionId
  FunctionK.lift(mk _)
}

After that we are ready to launch.

implicit val system = ActorSystem("foo")

val subscriptions: SubscriptionOp ~> Future =
  AkkaRuntime(system).start(
    entityName = "Subscription",
    behavior,
    correlation,
    Tagging(EventTag("Payment")
  )