Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce StorageCodec which helps with encoding/decoding of (codec, value) pairs #1389

Merged
merged 8 commits into from
Apr 17, 2024

Conversation

tillrohrmann
Copy link
Contributor

Attempt to generalize the serialization of versioned values by introducing the VersionedSerializer trait. The versioned serializer is aware of the serialization format version and always writes it first (u16) before writing the actual value. When deserializing, the serializer first reads the version and then gives it to the deserialize method for the actual value. Thereby, the deserialization step can be conditioned on the actual version. One thing that becomes apparent is that by decoupling the domain object from the storage object, one needs to provide at the storage layer boundary information about which serializer to use for a given domain object.

Happy to hear what you think about this approach @AhmedSoliman.

@tillrohrmann
Copy link
Contributor Author

tillrohrmann commented Apr 12, 2024

Some open questions:

  • How fine-grained do we want to version the data?
    • We could have a version for PP state, logs records and every metadata entity (as it is in this PR)
    • We could bundle the metadata entities into a single version meaning that the overall version needs to be bumped if any entity changes
    • Having a single storage version (probably not very practical)
  • How to couple the cluster marker version to the individual storage versions? Should the storage format versions be combined into the cluster marker version or be recorded separately? Probably just recording the actual Restate version is fine.

@tillrohrmann
Copy link
Contributor Author

I've pushed an update to this PR which reflects our offline discussion @AhmedSoliman. The VersionedSerializer has now been replaced with StorageSerde which encodes for a given type how it is stored in storage (including its codec). PTAL.

Making the domain object aware of its storage type requires that we co-locate both. This is currently not the case for the pp storage objects where we have the storage-api crate and the storage-proto crate. Merging those two crates, I will do as a follow-up. I am a bit unhappy about this coupling but there is currently no clear value for keeping these crates separate other than from a purist point of view.

crates/storage-proto/src/lib.rs Outdated Show resolved Hide resolved
crates/storage-proto/src/lib.rs Outdated Show resolved Hide resolved
crates/storage-rocksdb/src/codec.rs Outdated Show resolved Hide resolved
crates/types/src/storage.rs Outdated Show resolved Hide resolved
crates/types/src/storage.rs Outdated Show resolved Hide resolved
crates/types/src/storage.rs Outdated Show resolved Hide resolved
crates/types/src/storage.rs Outdated Show resolved Hide resolved
crates/types/src/storage.rs Outdated Show resolved Hide resolved
crates/types/src/storage.rs Outdated Show resolved Hide resolved
crates/wal-protocol/src/lib.rs Outdated Show resolved Hide resolved
@tillrohrmann tillrohrmann changed the title Introduce VersionedSerializer which helps with serde of versioned values Introduce StorageCodec which helps with encoding/decoding of (codec, values) pairs Apr 16, 2024
@tillrohrmann tillrohrmann marked this pull request as ready for review April 16, 2024 16:46
@tillrohrmann tillrohrmann changed the title Introduce StorageCodec which helps with encoding/decoding of (codec, values) pairs Introduce StorageCodec which helps with encoding/decoding of (codec, value) pairs Apr 16, 2024
@tillrohrmann tillrohrmann force-pushed the serialization branch 3 times, most recently from 382d9ea to a8a2b31 Compare April 16, 2024 17:11
Copy link
Contributor

@AhmedSoliman AhmedSoliman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolute legend! Thanks for bearing with me on this one.

// todo add proper format version
let value = bincode::serde::encode_to_vec(value, bincode::config::standard())
.map_err(|err| WriteError::Codec(err.into()))?;
let mut buf = BytesMut::default();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential future improvement would be to follow a design similar to storage-rocksdb's codec, if types can return an estimate of the buffer size needed for serialization, the buffer can be created with enough capacity to reduce the number of allocations.

Copy link
Contributor Author

@tillrohrmann tillrohrmann Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, indeed. This has become a bit harder now because before we already had a Protobuf type which easily gave us this information. Right now we only create the protobuf types for the protobuf-encoded values when actually serializing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Schema {
pub version: Version,
pub components: HashMap<String, ComponentSchemas>,
// flexbuffers only supports string-keyed maps :-( --> so we store it as vector of kv pairs
#[serde_as(as = "serde_with::Seq<(_, _)>")]
pub deployments: HashMap<DeploymentId, DeploymentSchemas>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if one forgot to add the serde_as here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would fail at runtime with a KeyMustBeString error if the map contained an entry.

Comment on lines +46 to 47
#[serde_as(as = "serde_with::Seq<(_, _)>")]
pub subscriptions: HashMap<SubscriptionId, Subscription>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potential future improvement would be to wrap serializable maps into a new type to DRY.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this makes sense. Good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use crate::Schema;
use restate_types::flexbuffers_storage_encode_decode;

flexbuffers_storage_encode_decode!(Schema);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turning this into a proc-macro (derive) would be a nice exercise :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I didn't want to press my luck with macros yesterday ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// To support codec evolution, this trait implementation needs to be able to decode values encoded
/// with any previously used codec.
pub trait StorageDecode {
fn decode(buf: &[u8], kind: StorageCodecKind) -> Result<Self, StorageDecodeError>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential future improvement might to change the input to &mut T where T: Buf to make it easier to decode from cursors (and to advance it for incremental decoding).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Let me check whether I can quickly change it as part of this PR.

Making the storage-api crate aware of the storage types will allow us
to implement for each API type how it will be serialized. This increases
coupling but simplifies things until we truely need to have multiple
storage representations.
StorageSerde encodes how a given type is encoded for
storing in storage.
We move the dedup types into the storage-api crate so that we can
implement StorageSerde for them based on the protobuf types defined
in this crate.
This commit removes the partial storage of the VirtualObjectStatus to
simplify transition to the StorageSerde. Optimizations for what to not
store can be done later.
We now require that stored values implement the StorageEncode trait
to encode them together with their codec. When reading a value, the
value needs to implement StorageDecode. Alternatively, one can also
store raw bytes into RocksDB as it is needed for state values, for
example.
Flexbuffers does not support non-string keyed maps. Therefore, we have to
serialize maps that have a non-string key as a vector of key-value pairs.
@tillrohrmann
Copy link
Contributor Author

Thanks for all your valuable feedback @AhmedSoliman 🙏 Merging this PR once GHA gives green light.

@tillrohrmann tillrohrmann merged commit d32f3ef into restatedev:main Apr 17, 2024
6 checks passed
@tillrohrmann tillrohrmann deleted the serialization branch April 17, 2024 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants