-
Notifications
You must be signed in to change notification settings - Fork 78
CRDT.ORMap serialization #507 #598
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
Conversation
Motivation: `CRDT.ORMap` does not conform to `ProtobufRepresentable` Modiifications: - Modified `ORMap` so it doesn't require a closure in initializer - Conform `ORMap` and `ORMapDelta` to `ProtobufRepresentable` Result: Part of #507
Protos/CRDT/CRDT.proto
Outdated
|
|
||
| message CRDTORMap { | ||
| message Delta { | ||
| CRDTORMapValue defaultValue = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
carrying the default a bit weird I guess... it's because we want to merge(default, this value) if the other node does not have a value? Would the other node's defaultValue not be usable instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's for merge, update, and reset--more so for update which takes a mutator closure and expects Value as param, so we make a copy of defaultValue in case there is no existing value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can modify the mutator to accept Value? and return Value, so the caller is responsible for providing the default if needed, then we can get rid of this defaultValue I think...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
modifications would look like thing(.empty) { $0.add("hello") I guess then right? That's bearable (AFAIR also how Akka's Map worked)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, not so easy. We need something for merge in case key doesn't have value locally, and we can't just copy other node's value because the replicaID is expected to be different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you meant by your first point now. We don't have to transport defaultValue--we would still require it in the initializer, but we allow it to be nil if we got it from remote. All we care about is local ORMap has a defaultValue. The remote one doesn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right yes; I'm not sure how it complicates end user API though 🤔 Curious what you can figure out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only change for users is they pass in an object instead of closure. Looks nicer actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does look nicer! :)
| /// but it **is** required in the initializer to ensure that **local** `ORMap` has `defaultValue` | ||
| /// for `merge`, `update`, etc. In those methods `defaultValue` is required in case **local** | ||
| /// `ORMap` does not have an existing value for the given `key`. | ||
| let defaultValue: Value? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now optional when constructed from protobuf
| } | ||
|
|
||
| init(replicaID: ReplicaID, valueInitializer: @escaping () -> Value) { | ||
| init(replicaID: ReplicaID, defaultValue: Value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defaultValue is required for local construction
| } | ||
|
|
||
| public mutating func update(key: Key, mutator: (inout Value) -> Void) { | ||
| guard let defaultValue = self.defaultValue else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added these guards as a result of making defaultValue optional
|
|
||
| // Apply `mutator` to the value then save it to state. Create `Value` if needed. | ||
| var value = self._values[key] ?? self.valueInitializer() | ||
| var value = self._values[key] ?? defaultValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming value semantics
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| let defaultValue: Value? | ||
|
|
||
| init(keys: ORSet<Key>.Delta, values: [Key: Value], valueInitializer: @escaping () -> Value) { | ||
| init(keys: ORSet<Key>.Delta, values: [Key: Value], defaultValue: Value?) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delta may be constructed from deserialized ORMap (https://github.com/apple/swift-distributed-actors/pull/598/files#diff-6d3797019f1301f2492ee9aed777e118R77), which has defaultValue as nil, so here we have to allow Value? not Value.
| /// Additional `ORMap` methods when `Value` type conforms to `ResettableCRDT`. | ||
| public protocol ORMapWithResettableValue: ORMapWithUnsafeRemove { | ||
| /// Resets value for `key` to `defaultValue` provided in `init`. | ||
| /// Resets value for `key` by calling `ResettableCRDT.reset()`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected the comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
| // If `k` is not found in `self` then create a new `Value` instance. | ||
| // We must NOT copy `other`'s value directly to `self` because the two should have different replica IDs. | ||
| var lv: Value = self[k] ?? valueInitializer() | ||
| var lv: Value = self[k] ?? defaultValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming value semantics
| let serialized = try system.serialization.serialize(map) | ||
| let deserialized = try system.serialization.deserialize(as: CRDT.ORMap<String, CRDT.ORSet<String>>.self, from: serialized) | ||
|
|
||
| "\(deserialized.replicaID)".shouldContain("actor:sact://CRDTSerializationTests@localhost:9001/user/alpha") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, this is another one of those localhost to 127.0.0.1 change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok no problem, sorry about the change 🙇 In the end it's more consistent though I think..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the end it's more consistent though
+1. 🙏
|
Test failure was #463 |
| /// - SeeAlso: `CRDT.ORMap` | ||
| /// - SeeAlso: `CRDT.LWWRegister` | ||
| public struct LWWMap<Key: Codable & Hashable, Value: ActorMessage>: NamedDeltaCRDT, LWWMapOperations { | ||
| public struct LWWMap<Key: Codable & Hashable, Value: Codable>: NamedDeltaCRDT, LWWMapOperations { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good 👍
ActorMessage really only a marker for us to grep over them when we : ActorMessage things, Codable in most places is the right thing 👍
ktoso
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit iffy if the optional is good enough as I've not dug very deep here but if there's issues we'll hit them during replication work so let's solve then 👍
LGTM
yeah same. worst case we just add it back to the payload... |
Motivation:
CRDT.ORMapdoes not conform toProtobufRepresentableModiifications:
ORMapso it doesn't require a closure in initializerORMapandORMapDeltatoProtobufRepresentableResult:
Part of #507