WebAssembly / WebAssembly/wasi-keyvalue
Avoid resources for atomic compare-and-swap operations?
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 54
- Forks
- 18
- PR merge metrics
- No merged PRs in 30d
Description
Right now, compare-and-swap operations are represented as resources:
Creating a resource is a relatively expensive operation in the component model as it involves resource table manipulation and potentially growing the resource table and its underlying allocation. Furthermore, engines/embedders may have implementation limits on the size of their resource tables to limit guest-controlled memory consumption, so creating a resource can fail non-deterministically from the Wasm guest's point of view (similar to the memory.grow instruction in core Wasm).
Furthermore, because swap takes ownership of the cas resource and, when the operation fails, the cas resource is returned again as part of the error, we are required to do even more resource table manipulation: removing the resource from the table on call and then adding it back in again on error return. These bits could potentially be avoided if the swap operation took a borrow<cas> instead of taking the cas by ownership.
But even better would be to avoid resources completely for compare-and-swap operations. A (perhaps naive) initial sketch could look something like this:
interface atomics {
use store.{bucket, error};
/// The error returned by a CAS operation.
variant cas-error {
/// A store error occurred when performing the operation.
store-error(error),
/// The CAS operation failed because `swap`'s `current-value` argument
/// did not match the entry's current value.
///
/// This returns the entry's current value.
cas-failed(option<list<u8>>),
}
/// Perform the swap on a CAS operation.
///
/// If the entry's current value does not match `current-value` then
/// an error is returned with the current value, so that the `swap`
/// operation can be retried.
swap: func(
bucket: borrow<bucket>,
key: string,
new-value: list<u8>,
current-value: option<list<u8>>,
) -> result<_, cas-error>;
}
This does imply copying the new and current values on every call, which may end up being more expensive than the resource table manipulation for large values. (FWIW, the existing API also exhibits this to some degree, since swap takes value: list<u8>, rather than the cas resource owning/containing the value so that if you are trying to swap in a loop you don't have to keep copying it.)
For large keys and values, we would want to move the API in the other direction: towards using resources more. (Right now, the API is in a weird middle ground where it isn't a great match for either scenario.) The maximally-resourced version of the API might look something like this:
interface atomics {
use store.{bucket, error};
/// The error returned by a CAS operation.
variant cas-error {
/// A store error occurred when performing the operation.
store-error(error),
/// The CAS operation failed because the value was too old.
cas-failed,
}
resource cas {
new: static func(
bucket: borrow<bucket>,
key: string,
new-value: list<u8>,
current-value: option<list<u8>>,
) -> result<cas, error>;
/// Getters and setters for this `cas` resource's internal
/// fields so that one `cas` resource can be reused for
/// multiple compare-and-swap operations.
get-bucket: func() -> bucket;
set-bucket: func(bucket: borrow<bucket>);
get-key: func() -> string;
set-key: func(key: string);
get-new-value: func() -> list<u8>;
set-new-value: func(new-valu: list<u8>);
get-current-value: func() -> option<list<u8>>;
set-current-value: func(current-value: option<list<u8>>);
/// Perform the configured compare-and-swap operation.
///
/// On success, clears the inner `key`, `new-value`, and
/// `current-value` so that this `cas` resource can be
/// reused for other compare-and-swap operations.
///
/// On error, updates this `cas` resource's `current-value`
/// with the entry's current value. This avoids copying the
/// current value in and out of the guest, which can be
/// expensive for large values.
swap: func() -> result<_, cas-error>;
}
}
It may actually make sense to have both variants of the API.
Thoughts?
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with wit/atomic.wit, especially the existing CAS resource and swap signature linked in the issue. Compare the resource-free and maximally-resourced sketches against the stated small- and large-value tradeoffs. Done means an agreed API direction, or an updated atomic interface if maintainers choose to proceed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- wasm
- Domain
- api, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100