# Customization

As demonstrated in the Getting Started, there are several parts of the system that are entirely customizable. For each of them, Shardcake provides at least a fake implementation for testing and a prod-ready implementation using common technologies.

Feel free to implement your own!

architecture diagram

# Storage

The Storage trait defines how to store and access pods and shards assignments. It contains 5 methods: getAssignments/saveAssignments to store assignments, getPods/savePods to store pods, and assignmentsStream to receive assignment updates.

trait Storage {
  def getAssignments: Task[Map[ShardId, Option[PodAddress]]]
  def saveAssignments(assignments: Map[ShardId, Option[PodAddress]]): Task[Unit]
  
  def assignmentsStream: ZStream[Any, Throwable, Map[Int, Option[PodAddress]]]
  
  def getPods: Task[Map[PodAddress, Pod]]
  def savePods(pods: Map[PodAddress, Pod]): Task[Unit]
}

For testing, you can use the Storage.memory layer that keeps data in memory.

Shardcake provides an implementation of Storage using Redis with the Redis4cats library (there's also an alternative using Redisson). To use it, add the following dependency:

libraryDependencies += "com.devsisters" %% "shardcake-storage-redis" % "2.1.0"

You can then simply use the StorageRedis.live layer.

It requires a RedisConfig with the following options:

  • assignmentsKey: the key to store shard assignments in Redis
  • podsKey: the key to store registered pods in Redis

It also requires a Redis object, which is an alias to RedisCommands[Task, String, String] with PubSubCommands[fs2Stream, String, String] from the redis4cats (opens new window) library used under the hood. Here's an example how to build it:

import com.devsisters.shardcake.StorageRedis.Redis
import dev.profunktor.redis4cats.Redis
import dev.profunktor.redis4cats.connection.RedisClient
import dev.profunktor.redis4cats.data.RedisCodec
import dev.profunktor.redis4cats.effect.Log
import dev.profunktor.redis4cats.pubsub.PubSub
import zio.interop.catz._
import zio.{ Task, ZEnvironment, ZIO, ZLayer }

val redis: ZLayer[Any, Throwable, Redis] =
  ZLayer.scopedEnvironment {
    implicit val runtime: zio.Runtime[Any] = zio.Runtime.default
    implicit val logger: Log[Task]         = new Log[Task] {
      override def debug(msg: => String): Task[Unit] = ZIO.unit
      override def error(msg: => String): Task[Unit] = ZIO.logError(msg)
      override def info(msg: => String): Task[Unit]  = ZIO.logDebug(msg)
    }

    (for {
      client   <- RedisClient[Task].from("redis://foobared@localhost")
      commands <- Redis[Task].fromClient(client, RedisCodec.Utf8)
      pubSub   <- PubSub.mkPubSubConnection[Task, String, String](client, RedisCodec.Utf8)
    } yield ZEnvironment(commands, pubSub)).toScopedZIO
  }

# Messaging Protocol

The Pods trait defines how to communicate with remote pods. It is used both by the Shard Manager for assigning and unassigning shards, and by pods for internal communication (forward messages to each other).

trait Pods {
  def assignShards(pod: PodAddress, shards: Set[ShardId]): Task[Unit]
  def unassignShards(pod: PodAddress, shards: Set[ShardId]): Task[Unit]
  def ping(pod: PodAddress): Task[Unit]
  
  def sendMessage(pod: PodAddress, message: BinaryMessage): Task[Option[Array[Byte]]]
  def sendMessageStreaming(pod: PodAddress, message: BinaryMessage): ZStream[Any, Throwable, Array[Byte]]
}

For testing, you can use the Pods.noop layer that does nothing.

Shardcake provides an implementation of Pods using the gRPC protocol. To use it, add the following dependency:

libraryDependencies += "com.devsisters" %% "shardcake-protocol-grpc" % "2.1.0"

You can then simply use the GrpcPods.live layer.

On pods, you also need expose the gRPC API. This is done by adding the GrpcShardingService.live layer to your environment. You don't need this one on the Shard Manager.

# Serialization

The Serialization trait defines how to serialize user messages that will be sent between pods. It contains 2 methods encode and decode that define how to transform a give type from and to bytes.

trait Serialization {
  def encode(message: Any): Task[Array[Byte]]
  def decode[A](bytes: Array[Byte]): Task[A]
}

For testing, you can use the Serialization.javaSerialization layer that uses Java Serialization (not recommended in production).

Shardcake provides an implementation of Serialization using the Kryo (opens new window) binary serialization library. To use it, add the following dependency:

libraryDependencies += "com.devsisters" %% "shardcake-serialization-kryo" % "2.1.0"

You can then simply use the KryoSerialization.live layer.

Server updates and message versioning

  • Messages are not persisted, which means that if you stop and restart the whole system, you can change anything in the messages format.
  • On the other hand, if you wish to do rolling updates (update servers progressively without downtime), you need to be careful with changes in the messages format.
  • What you can do largely depends on your serialization mechanism, some solutions allow changes while some others are very restrictive. Kryo (opens new window) by default is pretty strict and won't support most changes, but there are settings to support more (at the cost of some performance or message size).
  • When you can't modify existing messages, an option is to create new messages that won't be used until the rolling update is finished (so you won't have cases where old nodes receive new messages).

# Health

The PodsHealth trait defines how to know if a pod is still alive or is dead (in which case, we should reassign all its shards).

trait PodsHealth {
  def isAlive(podAddress: PodAddress): UIO[Boolean]
}

For testing, you can use the PodsHealth.noop layer that always returns true, or the PodsHealth.local layer that uses ping from the Messaging Protocol to check if a pod is alive.

Shardcake provides an implementation of PodsHealth using the Kubernetes (opens new window) API. To use it, add the following dependency:

libraryDependencies += "com.devsisters" %% "shardcake-health-k8s" % "2.1.0"

You can then simply use the K8sPodsHealth.live layer. This is requiring a Pods layer that comes from zio-k8s (opens new window).

Examples

Check the examples (opens new window) folder that contains a full example using Redis, gRPC and Kryo seralization.