-
Notifications
You must be signed in to change notification settings - Fork 248
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
WASI Key Value 0.2.0-draft2 support #2895
Conversation
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.
Thanks for working on this David. Fingers crossed that the SQLite was the worst of it. I left a few comments but it's brilliant how it's coming along!
|
||
// The assumption with increment is that if the value for the key does not exist, it will be | ||
// assumed to be zero. In the case that we are unable to unmarshal the value into an i64 an error will be returned. | ||
async fn increment(&self, key: String, delta: i64) -> Result<i64, Error> { |
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 an interesting design niggle - we have not previously defined an encoding for integers, and I don't think wasi-keyvalue
defines one either. All the "set" APIs are byte arrays. So how do I set an initial value to which I can later apply increment
? Hopefully this won't be an issue in practice, but might be worth considering in the wasi-keyvalue specification.
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.
Nice catch! I'm just going with little endian encoding of i64s. I think the guidance on using increment right now is to start with a key/value that doesn't exist and "increment" it to your value of choice. Then use increment or get from there on out.
I do believe this is something that we'll need to consider for future interface revisions.
let tx = binding.transaction().map_err(log_error)?; | ||
for kv in key_values { | ||
tx.prepare_cached( | ||
"INSERT INTO spin_key_value (store, key, value) VALUES ($1, $2, $3) |
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.
You're right, this is pretty unlovely, but I have no other ideas. SQLite does provide for bulk inserts, but I don't think they let you do an update where the row already existed. Hopefully it will not be too bad because in-process...!
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.
thankfully single-transaction-many-inserts is equally performant to single-transaction-bulk-insert - and this is easier to read (SQLite bulk inserts require the data source to be a SELECT
, which is... unintuitive), so I wouldn't personally be too worried about perf here
Signed-off-by: itowlson <[email protected]>
Signed-off-by: David Justice <[email protected]>
Signed-off-by: David Justice <[email protected]>
Signed-off-by: David Justice <[email protected]>
Signed-off-by: David Justice <[email protected]>
@michelleN, please take a look when you have a minute. Thank you! |
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 looks really good, David, and I'm so grateful to you for taking it on. I have some comments which I don't think are blockers but would appreciate if you have time to address them. If not then no worries, I can work on them after the RC.
.incr(key, delta) | ||
.await | ||
.map_err(log_error) | ||
} |
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.
Heh, I look at these implementations compared to the others, and it becomes very clear what the atomics
and batch
interfaces were modelled on...!
ensure_ok!(store.delete("qux")); | ||
ensure_matches!(store.exists("bar"), Ok(false)); | ||
ensure_matches!(store.get("qux"), Ok(None)); | ||
ensure_matches!(keys(&store.list_keys(None)), Ok(&[])); |
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.
Can we add some atomics and batch tests here, please, even if they're only basic smoke tests for now? I know you have tests in the implementations, but just something that verifies that the APIs work from the guest even if we can't practically test their atomicity or batchitude. (Particularly CAS because resources.)
) -> Result<wasi_keyvalue::store::KeyResponse, wasi_keyvalue::store::Error> { | ||
match cursor { | ||
Some(_) => Err(wasi_keyvalue::store::Error::Other( | ||
"list_keys: cursor not supported".to_owned(), |
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.
Is there ever a plan to support a cursor?
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 for data stores that support paging. You could imagine a case where too many keys are in the store and the client would like to fetch them a page at a time.
Personally, I don't like this. I think listing all keys, possibly across partitions, is a really bad idea.
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 an artefact of the original Spin key-value interface not supporting a cursor. There's no technical reason we couldn't implement one in the WASI implementation (and have the Spin interface sit over that, rather than the other way round). It's just down to time and effort.
delta: i64, | ||
) -> Result<i64, wasi_keyvalue::store::Error> { | ||
let store = self.get_store_wasi(bucket)?; | ||
store.increment(key, delta).await.map_err(to_wasi_err) |
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.
Is Store::increment
documented as requiring to be atomic?
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 what sense? If I give a delta and then ask for it to be applied it will be without regard to the underlying value. The only issue with atomicity would be in the case where one fetches and increments while another has mutated the underlying value. In that case, there should be isolation from each mutation. In the case of Redis, this is handled with the INCR
instruction. In other cases, it should be done within a transaction or optimistically.
I guest the short answer is that increment should be serializable and consistent.
@@ -40,6 +41,7 @@ impl Factor for KeyValueFactor { | |||
fn init<T: Send + 'static>(&mut self, mut ctx: InitContext<T, Self>) -> anyhow::Result<()> { | |||
ctx.link_bindings(spin_world::v1::key_value::add_to_linker)?; | |||
ctx.link_bindings(spin_world::v2::key_value::add_to_linker)?; | |||
ctx.link_bindings(spin_world::wasi::keyvalue::store::add_to_linker)?; |
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.
Just a note: We probably don't want to force all embedders of this factor to support wasi key_value. We can leave it as is for now, but we'll probably want to come up with some sort of feature system that allows embedders to choose which interfaces get linked in.
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.
@rylev - would you mind adding an issue so we can track that if we don't already have one?
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.
As Ryan says, this seems like a system-wide consideration and principle which we should approach from a system-wide perspective. For example it would apply equally to "embedders shouldn't be forced to support the v1 WITs." I agree with Michelle, let's capture this but let's not ad-hoc it into each PR.
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 added the linker bindings for batch and atomic.
.await | ||
.map_err(log_error)?; | ||
let pair = self.get_pair(key.as_ref()).await?; | ||
match pair { |
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.
Isn't there a race condition here? If after patching, another request either successfully deletes the key or changes it in some way, pair
here will contain the wrong value. Is there no way to get the value back from the patch operation?
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 going to dig a little deeper. It wasn't clear initially that patch supported that.
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.
Looks like the REST API returns the original document and the Rust SDK throws it all away except the document attributes. (https://github.com/Azure/azure-sdk-for-rust/blob/e64d27c710ea3a9a55c9e044b388d0fc4acd0979/sdk/data_cosmos/src/operations/patch_document.rs#L150)
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.
@itowlson I think this would be the only one of me comments where we might consider blocking the PR on. What are your thoughts on whether the race here is worth blocking the PR?
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.
@rylev My preference is to merge as is. We can document it as a known issue in the RC, continue investigating between now and release, and assuming no joy, we can either:
- Keep documenting it as a known issue in the Cosmos back end; or
- Change it to always error with a "not supported on this back end" message
before release.
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 not sure what we can do about the race condition in Cosmos. The rest looks good to me but I'll leave this with you and Ryan to make a decision on the Cosmos woe. Thanks again for all your hard work on this!
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.
Impls seem sound to me - I'd take the cosmos race as a future improvement if the rust sdk makes it difficult (because it's already a net improvement over our current interfaces)
6ad74fc
to
680e5ab
Compare
79c85bf
to
2fdcb0d
Compare
Signed-off-by: David Justice <[email protected]>
There's a test failure, but I think it might be unrelated to the PR since it is |
It's not you. MQTT is a known flake. Thanks for battling through this! |
This pull request adds support for the
[email protected]
.I'm opening this as a draft to get some initial feedback as I implement the remaining providers. Currently, the only provider implemented is the SQLite provider.
fixes #2486