The power of Akka

Krzysztof Borowski

Akka Paint -requirements

  • Simple
  • Real time changes
  • Multiuser
  • Scalable

Simple

case class Pixel(x: Int, y: Int)

case class DrawEvent(
    changes: Seq[Pixel],
    color:   String
)

//Board state
type Color = String
var drawballBoard = Map.empty[Pixel, Color]

Changes as events

Simple

class DrawballActorSimple() extends PersistentActor {

  var drawballBoard = Map.empty[Pixel, String]

  override def persistenceId: String = "drawballActor"

  override def receiveRecover: Receive = {
    case d: DrawEvent => updateState(d)
  }

  override def receiveCommand: Receive = {
    case Draw(changes, color) =>
      persistAsync(DrawEvent(changes, color)) { de =>
        updateState(de)
      }
  }

  private def updateState(drawEvent: DrawEvent) = {
    drawballBoard = drawEvent.changes.foldLeft(drawballBoard) {
      case (newBoard, pixel) =>
        newBoard.updated(pixel, drawEvent.color)
    }
  }

}

Board as a Persistent Actor

Multiuser

Multiuser

class DrawballActor() extends PersistentActor {

  var drawballBoard = Map.empty[Pixel, String]
  var registeredClients = Set.empty[ActorRef]

  override def persistenceId: String = "drawballActor"

  override def receiveRecover: Receive = {
    case d: DrawEvent => updateState(d)
  }

  override def receiveCommand: Receive = {
    case Draw(changes, color) =>
      persistAsync(DrawEvent(changes, color)) { de =>
        updateState(de)
        (registeredClients - sender())
          .foreach(_ ! Changes(de.changes, de.color))
      }
    case r: RegisterClient => {
      registeredClients = registeredClients + r.client
      convertBoardToUpdates(drawballBoard, Changes.apply)
        .foreach(r.client ! _)
    }
    case ur: UnregisterClient => {
      registeredClients = registeredClients - ur.client
    }
  }

  private def updateState(drawEvent: DrawEvent) = {
    ...
  } 
}

Multiuser and real time


class ClientConnectionSimple(
  browser: ActorRef,
  drawBoardActor: ActorRef
) extends Actor {

  drawBoardActor ! RegisterClient(self)
  var recentChanges = Map.empty[Pixel, String]

  override def receive: Receive = {
    case d: Draw =>
      drawBoardActor ! d
    case c @ Changes =>
      browser ! c
  }

  override def postStop(): Unit = {
    drawBoardActor ! UnregisterClient(self)
  }
}
//Play! controller
def socket = WebSocket.accept[Draw, Changes](requestHeader => {
    ActorFlow.actorRef[Draw, Changes](browser =>
      ClientConnection.props(
        browser,
        drawballActor
      ))
})

Scalable

Akka Sharding to the rescue !

 

Akka Sharding to the rescue !

 

Akka Sharding to the rescue !

 

Scalable

Scaling - architecture

Sharding

def shardingPixels(changes: Iterable[Pixel], color: String): Iterable[DrawEntity] = {
      changes.groupBy { pixel =>
        (pixel.y / entitySize, pixel.x / entitySize)
      }.map {
        case ((shardId, entityId), pixels) =>
          DrawEntity(shardId, entityId, pixels.toSeq, color)
      }
}

private val extractEntityId: ShardRegion.ExtractEntityId = {
    case DrawEntity(_, entityId, pixels, color) ⇒
      (entityId.toString, Draw(pixels, color))
    case ShardingRegister(_, entityId, client) ⇒
      (entityId.toString, RegisterClient(Serialization.serializedActorPath(client)))
    case ShardingUnregister(_, entityId, client) ⇒
      (entityId.toString, UnregisterClient(Serialization.serializedActorPath(client)))
}

private val extractShardId: ShardRegion.ExtractShardId = {
    case DrawEntity(shardId, _, _, _) ⇒
      shardId.toString
    case ShardingRegister(shardId, _, _) ⇒
      shardId.toString
    case ShardingUnregister(shardId, _, _) ⇒
      shardId.toString
}

Sharding Cluster

 

def initializeCluster(): ActorSystem = {
    // Create an Akka system
    val system = ActorSystem("DrawballSystem")

    ClusterSharding(system).start(
      typeName = entityName,
      entityProps = Props[DrawballActor],
      settings = ClusterShardingSettings(system),
      extractEntityId = extractEntityId,
      extractShardId = extractShardId
    )
    system
}

def shardRegion()(implicit actorSystem: ActorSystem) = {
    ClusterSharding(actorSystem).shardRegion(entityName)
}

Akka - snapshoting

 

 override def receiveRecover: Receive = {
    ...
    case SnapshotOffer(_, snapshot: DrawSnapshot) => {
      snapshot.changes.foreach(updateState)
      snapshot.clients.foreach(c => registerClient(RegisterClient(c)))
    }
    case RecoveryCompleted => {
      registeredClients.foreach(c => c ! ReRegisterClient())
      registeredClients = Set.empty
    }
}
override def receiveCommand: Receive = {
    case Draw(changes, color) =>
      persistAsync(DrawEvent(changes, color)) { de =>
        updateState(de)
        changesNumber += 1
        if (changesNumber > 1000) {
          changesNumber = 0
          self ! "snap"
        }
        (registeredClients - sender())
          .foreach(_ ! Changes(de.changes, de.color))
      }
    case "snap" => saveSnapshot(DrawSnapshot(
      convertBoardToUpdates(drawballBoard, DrawEvent.apply).toSeq,
      registeredClients.map(Serialization.serializedActorPath).toSeq
    ))
    ...
}

DEMO

Tips and tricks

  • Use `ClusterSharding.startProxy` to not hold any entities on the node.
  • Distributed coordinator - akka.extensions += "akka.cluster.ddata.DistributedData" (Experimental).
  • Use Protocol Buffer.
  • Be careful about message buffering.  

Summary:

  • lines of Code: 275!,
  • multiuser,
  • scalable,
  • fault tolerant.

Bibliography

  • http://www.slideshare.net/bantonsson/akka-persistencescaladays2014
  • http://doc.akka.io/docs/akka/current/scala/cluster-sharding.html
  • https://github.com/trueaccord/ScalaPB
Made with Slides.com