Migrating from Shardcake
Shard 3.0 is the successor of Shardcake 2.x. Most concepts are unchanged (entities, topics, singletons, shards, the Shard Manager), but the messaging API is now typed, a Shard Manager can manage several groups of pods, and the protocol between pods and with the Shard Manager has changed.
This section lists what to change in your code, then how to upgrade a running cluster.
Dependencies and packages
The organization, the artifacts and the package were renamed:
"com.devsisters" %% "shardcake-xxx"becomes"com.github.ghostdogpr" %% "shard-xxx"(e.g.shard-entities,shard-manager,shard-protocol-grpc).- The package
com.devsisters.shardcakebecomesshard.
Shard requires Scala 3.9 LTS or later: Scala 2.13 is no longer supported.
Messages
In Shardcake, a message that expects a reply carries a Replier[A], and errors are usually returned as part of the reply (e.g. with a Try). In Shard, the message type has two type parameters: each message declares the type E of the errors it can fail with and the type A of its reply. Replier and StreamReplier are gone.
Before:
sealed trait GuildMessage
object GuildMessage {
case class Join(userId: String, replier: Replier[Try[Set[String]]]) extends GuildMessage
case class Leave(userId: String) extends GuildMessage
}
object Guild extends EntityType[GuildMessage]("guild")After:
case class GuildFull(maxMembers: Int)
enum GuildMessage[E, A] {
case Join(userId: String) extends GuildMessage[GuildFull, Set[String]]
case Leave(userId: String) extends GuildMessage[Nothing, Unit]
}
object Guild extends EntityType[GuildMessage]("guild")A message that replied with a StreamReplier[A] now declares a Streamed[E2, A] reply type (see Streaming).
Serialization
The Serialization layer is replaced by a MessageCodec[Msg] given, required where the entity or topic type is declared. Instead of providing KryoSerialization.live, import the Kryo given:
import shard.kryo.given
object Guild extends EntityType[GuildMessage]("guild")Remove Serialization from the layers given to Sharding.live. See Message Codec for the other backends, including Protobuf.
Behaviors
In Shardcake, the behavior takes a Queue of messages and consumes it forever, replying through the Replier of each message. In Shard, the behavior is called when an entity starts and returns a handler, whose handle method returns the reply of each message. Failing with E is how a message replies with an error.
Before:
def behavior(entityId: String, messages: Queue[GuildMessage]): RIO[Sharding, Nothing] =
Ref.make(Set.empty[String]).flatMap { state =>
messages.take.flatMap {
case Join(userId, replier) =>
state.get.flatMap(members =>
if (members.size >= 5) replier.reply(Failure(new Exception("Guild is full")))
else state.updateAndGet(_ + userId).flatMap(members => replier.reply(Success(members)))
)
case Leave(userId) =>
state.update(_ - userId)
}.forever
}After:
def behavior(entityId: String): ZIO[Scope, Nothing, Guild.Handler] =
Ref.make(Set.empty[String]).map { state =>
new Guild.Handler {
def handle[E, A](message: GuildMessage[E, A]): IO[E, A] =
message match {
case Join(userId) =>
state.get.flatMap(members =>
if (members.size >= 5) ZIO.fail(GuildFull(5))
else state.updateAndGet(_ + userId)
)
case Leave(userId) =>
state.update(_ - userId)
}
}
}A message can no longer be left without a reply: the sender always gets the result of handle, or EntityDefect if it died.
Some patterns of the queue-based behaviors have a different equivalent:
terminateMessageis replaced by theScopegiven to the behavior: useZIO.addFinalizerto save the entity state when it stops. The finalizers run after the last message was processed, withinentityTerminationTimeout.- Messages were processed one at a time unless the behavior forked them. Use the
concurrencyoption to process several messages at the same time, or theConcurrentmarker trait for messages that can run alongside the others (see Messages). - Work running in the background of an entity (timers, subscriptions) can be forked in its scope with
forkScoped: it is interrupted when the entity stops.
Registration
The optional parameters of registerEntity and registerTopic move to EntityOptions, and the behavior is given in a second parameter list:
entityMaxIdleTimebecomesEntityOptions.maxIdleTime.loadEntitybecomesEntityOptions.loadEntity. It is a polymorphic function, since messages have type parameters.terminateMessageis gone (see above).
// before
Sharding.registerEntity(Guild, behavior, entityMaxIdleTime = Some(5.minutes))
// after
Sharding.registerEntity(Guild, EntityOptions(maxIdleTime = Some(5.minutes)))(behavior)EntityOptions also configures the new concurrency, mailboxCapacity and interceptor options (see Entity options).
Sending messages
Messages are sent without the Replier callback, and the Messenger methods were renamed:
| Shardcake 2.x | Shard 3.0 |
|---|---|
send(id)(Join("user1", _)) | send(id)(Join("user1")) |
sendDiscard(id)(msg) | sendDiscard(id)(msg) |
sendAndReceiveStream(id)(Msg(_)) | stream(id)(Msg()) |
sendStreamAndReceiveStream(id)(r => stream) | duplex(id)(Msg(), inputs) |
sendStream(id)(messages) | sendStream(id)(messages) |
sendAndReceiveStreamAutoRestart | streamAutoRestart |
sendStreamAndReceiveStreamAutoRestart | duplexAutoRestart |
broadcast(topic)(Msg(_)) | broadcast(topic)(Msg()) |
A few behaviors changed:
- Errors are typed:
sendreturns anIO[E | ClusterError, A]instead of aTask.ClusterErrorcovers the errors of the cluster itself (see Errors), e.g.SendTimeoutExceptionis nowSendTimeout. streamandduplexreturn aZStreamdirectly, instead of aTaskof a stream.duplexsends a single message along with a stream of inputs, instead of a stream of messages that each share the sameStreamReplier.sendStreamonly takes messages that reply with nothing (Msg[Nothing, Unit]).broadcastreturns aMap[PodAddress, Exit[E | ClusterError, A]]instead of aMap[PodAddress, Try[A]].- When the processing of a message times out, it is interrupted (unless it is
Uninterruptible). - When an entity is full, messages fail with
MailboxFull(seemailboxCapacity).
Roles
A single Shard Manager can now manage several groups of pods, called roles. Where Shardcake needed one Shard Manager (and one storage) per group of pods, e.g. per service, Shard can use one for all of them. Each pod has a role, and each role is managed independently, as if it had its own Shard Manager: it has its own shards, assignments and rebalancing. Pods only send messages to entities hosted by pods of the same role, and a singleton runs on one pod of each role that registers it (see Configuration).
If you only have one group of pods, they all have the default role and behave as before, but a few APIs changed:
Config.rolesets the role of a pod (Role.defaultby default).ManagerConfig.numberOfShardsis a functionRole => Int. Use_ => 300to keep a single number of shards.RedisConfig.assignmentsKeyis a functionRole => String(shard_assignments:<role>by default).K8sConfig.namespaceandK8sConfig.labelSelectorare functions of the role.- Metrics that depend on a role are tagged with
role.
Configuration
On pods, Config.shardManagerUri is replaced by shardManagerHost, shardManagerPort and shardManagerTimeout, since the Shard Manager API now uses gRPC instead of GraphQL. ShardManagerClient.liveWithSttp becomes ShardManagerClient.live, which requires a GrpcConfig. Pods no longer depend on sttp and caliban-client: the client uses the same gRPC library as the communication between pods.
// before
Config.default.copy(shardManagerUri = uri"http://shard-manager:8080/api/graphql")
// after
Config.default.copy(shardManagerHost = "shard-manager", shardManagerPort = 8080)Config also has a new role (see Roles) and entityInterceptor (see Interceptors).
Shard Manager
Server.run()becomesServer.run, and requires aGrpcConfig. It serves a gRPC API onapiPortinstead of a GraphQL API over HTTP, along with the standard gRPC health service. Kubernetes probes should usegrpcinstead ofhttpGeton/health.- The Shard Manager no longer depends on zio-http and Caliban.
Server.rundoesn't take a zio-httpHandlerAspectanymore: useGrpcConfig.serverInterceptorsto wrap the API calls instead. - Tools that called the GraphQL API can use the gRPC API instead, or
Sharding.getAssignmentsandSharding.getRegisteredPodsfrom a pod.
Storage
The Redis storage has a new default implementation, shard-storage-sage, which uses the Sage client. The redis4cats (shard-storage-redis) and Redisson (shard-storage-redisson) implementations are still available (see Storage).
RedisConfig.assignmentsKey takes a role (see Roles). The format of the stored pods changed, so Shardcake and Shard can't share the same data (see below).
Custom implementations
If you implemented the pluggable interfaces yourself:
Storagemethods for assignments take aRole, andPodhas arole(see Roles).PodssendsBinaryMessageand receivesBinaryReply, which carries the outcome of the message (success, typed failure, defect, interruption, etc.) instead of optional bytes.Serializationis replaced byMessageCodec[Msg], which also encodes the replies and errors of each message.
Metrics
Metrics are prefixed with shard. instead of shardcake. (e.g. shard.entities). Update your dashboards and alerts accordingly (see Metrics).
Upgrading a running cluster
Shardcake pods and Shard pods can't be part of the same cluster: the protocol between pods, the Shard Manager API and the stored data are all different. A rolling update from Shardcake to Shard is therefore not possible. Instead, run the new version as a separate cluster next to the old one (a blue/green deployment), then switch to it.
The new cluster needs:
- Its own Shard Manager, with a different address than the old one. Point the new pods to it with
shardManagerHostandshardManagerPort. - Its own storage. Either use a separate Redis instance (or database), or use different keys: the assignment keys are different by default (
shard_assignmentsandshard_assignments:<role>), but both versions usepodsto store pods and overwrite it entirely, so the two Shard Managers would erase each other's pods. Set a differentpodsKeyin theRedisConfigof the new Shard Manager and pods. - Every pod that sends messages to the entities, not only those that host them, since a pod can only send messages to the entities of its own cluster.
As long as both clusters run, an entity can be running in both of them, and a singleton runs once in each. Choose how to switch depending on whether that is acceptable:
- Switch with an overlap: deploy the new cluster, move the traffic to it, then stop the old one. This has no downtime, but an entity started in the new cluster can run while the old one is still running and saving its state, so it may load a state that is not up to date.
- Switch with a pause: deploy the new cluster without traffic, stop sending messages to the old one, stop the old pods so that their entities finish their messages and run their finalizers, then move the traffic to the new cluster. Entities always start from their last saved state, at the cost of a short pause. Keep singletons from doing anything in the new cluster before the switch, since they start as soon as its pods are registered.
Keep the old cluster deployable until the switch is done, to be able to move the traffic back to it.
Next upgrades
Once on Shard, rolling updates work again between Shard versions, as long as your messages remain compatible (see the tip in Message Codec).