State versioning in Orleans
The documentation on Grain versioning in Microsoft Orleans starts off with this
"👉WARNING: This page describes how to use grain interface versioning. The versioning of Grain state is out of scope."
With a stateful system handling multiple state versions is a must if we want to deploy new versions without taking the whole system offline. Working with F# we have some powertools in our belt, and for dealing with multiple versions we decided to try out Discriminated Unions, and they worked our great. One can of course apply the same technique in C# with a bit more checks.
tl;dr;
The short version (phun intented) is:
Discriminated union with one entry per version and a module which is capable of upgrading old versions as they are loaded from storage. Writing state always write the latest version.
Example
Let's say we have a state where we wish to go from a single image to an array if images, just changing the field type would not work. Lets call this ImageState
The goal is for our grain implementation not to worry too much about versions, to do this we separate the in memory representation, state, in our case called ImageState
from the DTO representation called ImageStateDto
.
The in memory representation which the grain works with ImageState
has a simple list of images uri's
type ImageState = { images: Uri list }
Our stored representation of this can be both the new version and old version
type ImageStateDtoV1 = { image: Uri option }
type ImageStateDtoV2 = { images: Uri list}
type ImageVersionedStateDto =
| V1 of ImageStateDtoV1
| V2 of ImageStateDtoV2
Be sure to start with at least two cases and test with your binary serializer. With Orleans default binary serializer, single case discriminated unions will yield different format, hence fail deserialization
Since Orlans must be able to create new, empty versions of the state object we'll wrap this with a class and also add a module which can convert from Dto to State and from State to Dto.
type ImageStateDto(state: ImageVersionedStateDto) =
member val State = state with get, set
new () = ImageStateDto({ images = []} |> V2)
module ImageStateDto =
let fromDto (dto:ImageStateDto) =
match dto.State with
| V1 dto ->
match dto.image with
| Some i -> { ImageState.images = [i]}
| None _ -> { ImageState.images = []}
| V2 dto ->
{ ImageState.images = dto.images}
let toDto (state:ImageState) : ImageStateDto =
{ ImageStateDtoV2.images = state.images} |> V2 |> ImageStateDto
The ImageStateDto
is the class which will be injected into the grain constructor as IPersistentState<ImageStateDto>
. Inside the grain we wrap the IPersistentState to read the state using the fromDto
method. Instead of calling WriteStateAsync
directly we have our own internal method using ImageStateDto.toDto
before calling WriteStateAsync. A sample grain may look like this
type ProfileImage =
inherit Grain
val private persistence : IPersistentState<ImageStateDto>
new (persistence) = {
persistence = persistence
}
member this.State
with get() = this.persistence.State |> ImageStateDto.fromDto
member private this.WriteStateAsync(state) =
this.persistence.State <- state |> ImageStateDto.toDto
this.persistence.WriteStateAsync()
It may seem a bit tedious, but we've found this to work very well for our needs.