Skip to content

Commit 82fcbd1

Browse files
committed
It compiles, bag it and ship it
Signed-off-by: itowlson <[email protected]>
1 parent 22f9f98 commit 82fcbd1

File tree

8 files changed

+347
-1
lines changed

8 files changed

+347
-1
lines changed

crates/key-value/src/host_component.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ impl HostComponent for KeyValueComponent {
4848
get: impl Fn(&mut spin_core::Data<T>) -> &mut Self::Data + Send + Sync + Copy + 'static,
4949
) -> anyhow::Result<()> {
5050
super::key_value::add_to_linker(linker, get)?;
51+
super::wasi_keyvalue::store::add_to_linker(linker, get)?;
5152
spin_world::v1::key_value::add_to_linker(linker, get)
5253
}
5354

crates/key-value/src/lib.rs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::{Context, Result};
22
use spin_app::MetadataKey;
33
use spin_core::{async_trait, wasmtime::component::Resource};
44
use spin_world::v2::key_value;
5+
use spin_world::wasi::keyvalue as wasi_keyvalue;
56
use std::{collections::HashSet, sync::Arc};
67
use table::Table;
78

@@ -56,7 +57,7 @@ impl KeyValueDispatch {
5657
self.manager = manager;
5758
}
5859

59-
pub fn get_store(&self, store: Resource<key_value::Store>) -> anyhow::Result<&Arc<dyn Store>> {
60+
pub fn get_store<T: 'static>(&self, store: Resource<T>) -> anyhow::Result<&Arc<dyn Store>> {
6061
self.stores.get(store.rep()).context("invalid store")
6162
}
6263
}
@@ -138,6 +139,100 @@ impl key_value::HostStore for KeyValueDispatch {
138139
}
139140
}
140141

142+
fn to_wasi_err(e: Error) -> wasi_keyvalue::store::Error {
143+
match e {
144+
Error::AccessDenied => wasi_keyvalue::store::Error::AccessDenied,
145+
Error::NoSuchStore => wasi_keyvalue::store::Error::NoSuchStore,
146+
Error::StoreTableFull => wasi_keyvalue::store::Error::Other("store table full".to_string()),
147+
Error::Other(msg) => wasi_keyvalue::store::Error::Other(msg),
148+
}
149+
}
150+
151+
#[async_trait]
152+
impl wasi_keyvalue::store::Host for KeyValueDispatch {
153+
async fn open(
154+
&mut self,
155+
identifier: String,
156+
) -> anyhow::Result<Result<Resource<wasi_keyvalue::store::Bucket>, wasi_keyvalue::store::Error>>
157+
{
158+
Ok(async {
159+
if self.allowed_stores.contains(&identifier) {
160+
let store = self
161+
.stores
162+
.push(self.manager.get(&identifier).await.map_err(to_wasi_err)?)
163+
.map_err(|()| {
164+
wasi_keyvalue::store::Error::Other("store table full".to_string())
165+
})?;
166+
Ok(Resource::new_own(store))
167+
} else {
168+
Err(wasi_keyvalue::store::Error::AccessDenied)
169+
}
170+
}
171+
.await)
172+
}
173+
}
174+
175+
use wasi_keyvalue::store::Bucket;
176+
#[async_trait]
177+
impl wasi_keyvalue::store::HostBucket for KeyValueDispatch {
178+
async fn get(
179+
&mut self,
180+
self_: Resource<Bucket>,
181+
key: String,
182+
) -> anyhow::Result<Result<Option<Vec<u8>>, wasi_keyvalue::store::Error>> {
183+
let store = self.get_store(self_)?;
184+
Ok(store.get(&key).await.map_err(to_wasi_err))
185+
}
186+
187+
async fn set(
188+
&mut self,
189+
self_: Resource<Bucket>,
190+
key: String,
191+
value: Vec<u8>,
192+
) -> anyhow::Result<Result<(), wasi_keyvalue::store::Error>> {
193+
let store = self.get_store(self_)?;
194+
Ok(store.set(&key, &value).await.map_err(to_wasi_err))
195+
}
196+
197+
async fn delete(
198+
&mut self,
199+
self_: Resource<Bucket>,
200+
key: String,
201+
) -> anyhow::Result<Result<(), wasi_keyvalue::store::Error>> {
202+
let store = self.get_store(self_)?;
203+
Ok(store.delete(&key).await.map_err(to_wasi_err))
204+
}
205+
206+
async fn exists(
207+
&mut self,
208+
self_: Resource<Bucket>,
209+
key: String,
210+
) -> anyhow::Result<Result<bool, wasi_keyvalue::store::Error>> {
211+
let store = self.get_store(self_)?;
212+
Ok(store.exists(&key).await.map_err(to_wasi_err))
213+
}
214+
215+
async fn list_keys(
216+
&mut self,
217+
self_: Resource<Bucket>,
218+
cursor: Option<u64>,
219+
) -> anyhow::Result<Result<wasi_keyvalue::store::KeyResponse, wasi_keyvalue::store::Error>>
220+
{
221+
if cursor.is_some() {
222+
anyhow::bail!("list_keys: cursor not supported");
223+
}
224+
225+
let store = self.get_store(self_)?;
226+
let keys = store.get_keys().await.map_err(to_wasi_err)?;
227+
Ok(Ok(wasi_keyvalue::store::KeyResponse { keys, cursor: None }))
228+
}
229+
230+
fn drop(&mut self, rep: Resource<Bucket>) -> anyhow::Result<()> {
231+
self.stores.remove(rep.rep());
232+
Ok(())
233+
}
234+
}
235+
141236
pub fn log_error(err: impl std::fmt::Debug) -> Error {
142237
tracing::warn!("key-value error: {err:?}");
143238
Error::Other(format!("{err:?}"))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// A keyvalue interface that provides atomic operations.
2+
///
3+
/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to
4+
/// fail, it will appear to the invoker of the atomic operation that the action either completed
5+
/// successfully or did nothing at all.
6+
///
7+
/// Please note that this interface is bare functions that take a reference to a bucket. This is to
8+
/// get around the current lack of a way to "extend" a resource with additional methods inside of
9+
/// wit. Future version of the interface will instead extend these methods on the base `bucket`
10+
/// resource.
11+
interface atomics {
12+
use store.{bucket, error};
13+
14+
/// Atomically increment the value associated with the key in the store by the given delta. It
15+
/// returns the new value.
16+
///
17+
/// If the key does not exist in the store, it creates a new key-value pair with the value set
18+
/// to the given delta.
19+
///
20+
/// If any other error occurs, it returns an `Err(error)`.
21+
increment: func(bucket: borrow<bucket>, key: string, delta: u64) -> result<u64, error>;
22+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/// A keyvalue interface that provides batch operations.
2+
///
3+
/// A batch operation is an operation that operates on multiple keys at once.
4+
///
5+
/// Batch operations are useful for reducing network round-trip time. For example, if you want to
6+
/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1
7+
/// batch get operation. The batch operation is faster because it only needs to make 1 network call
8+
/// instead of 100.
9+
///
10+
/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some
11+
/// of the keys may have been modified and some may not.
12+
///
13+
/// This interface does has the same consistency guarantees as the `store` interface, meaning that
14+
/// you should be able to "read your writes."
15+
///
16+
/// Please note that this interface is bare functions that take a reference to a bucket. This is to
17+
/// get around the current lack of a way to "extend" a resource with additional methods inside of
18+
/// wit. Future version of the interface will instead extend these methods on the base `bucket`
19+
/// resource.
20+
interface batch {
21+
use store.{bucket, error};
22+
23+
/// Get the key-value pairs associated with the keys in the store. It returns a list of
24+
/// key-value pairs.
25+
///
26+
/// If any of the keys do not exist in the store, it returns a `none` value for that pair in the
27+
/// list.
28+
///
29+
/// MAY show an out-of-date value if there are concurrent writes to the store.
30+
///
31+
/// If any other error occurs, it returns an `Err(error)`.
32+
get-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<list<option<tuple<string, list<u8>>>>, error>;
33+
34+
/// Set the values associated with the keys in the store. If the key already exists in the
35+
/// store, it overwrites the value.
36+
///
37+
/// Note that the key-value pairs are not guaranteed to be set in the order they are provided.
38+
///
39+
/// If any of the keys do not exist in the store, it creates a new key-value pair.
40+
///
41+
/// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
42+
/// rollback the key-value pairs that were already set. Thus, this batch operation does not
43+
/// guarantee atomicity, implying that some key-value pairs could be set while others might
44+
/// fail.
45+
///
46+
/// Other concurrent operations may also be able to see the partial results.
47+
set-many: func(bucket: borrow<bucket>, key-values: list<tuple<string, list<u8>>>) -> result<_, error>;
48+
49+
/// Delete the key-value pairs associated with the keys in the store.
50+
///
51+
/// Note that the key-value pairs are not guaranteed to be deleted in the order they are
52+
/// provided.
53+
///
54+
/// If any of the keys do not exist in the store, it skips the key.
55+
///
56+
/// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not
57+
/// rollback the key-value pairs that were already deleted. Thus, this batch operation does not
58+
/// guarantee atomicity, implying that some key-value pairs could be deleted while others might
59+
/// fail.
60+
///
61+
/// Other concurrent operations may also be able to see the partial results.
62+
delete-many: func(bucket: borrow<bucket>, keys: list<string>) -> result<_, error>;
63+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/// A keyvalue interface that provides eventually consistent key-value operations.
2+
///
3+
/// Each of these operations acts on a single key-value pair.
4+
///
5+
/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is
6+
/// the common denominator for all data types defined by different key-value stores to handle data,
7+
/// ensuring compatibility between different key-value stores. Note: the clients will be expecting
8+
/// serialization/deserialization overhead to be handled by the key-value store. The value could be
9+
/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects.
10+
///
11+
/// Data consistency in a key value store refers to the guarantee that once a write operation
12+
/// completes, all subsequent read operations will return the value that was written.
13+
///
14+
/// Any implementation of this interface must have enough consistency to guarantee "reading your
15+
/// writes." In particular, this means that the client should never get a value that is older than
16+
/// the one it wrote, but it MAY get a newer value if one was written around the same time. These
17+
/// guarantees only apply to the same client (which will likely be provided by the host or an
18+
/// external capability of some kind). In this context a "client" is referring to the caller or
19+
/// guest that is consuming this interface. Once a write request is committed by a specific client,
20+
/// all subsequent read requests by the same client will reflect that write or any subsequent
21+
/// writes. Another client running in a different context may or may not immediately see the result
22+
/// due to the replication lag. As an example of all of this, if a value at a given key is A, and
23+
/// the client writes B, then immediately reads, it should get B. If something else writes C in
24+
/// quick succession, then the client may get C. However, a client running in a separate context may
25+
/// still see A or B
26+
interface store {
27+
/// The set of errors which may be raised by functions in this package
28+
variant error {
29+
/// The host does not recognize the store identifier requested.
30+
no-such-store,
31+
32+
/// The requesting component does not have access to the specified store
33+
/// (which may or may not exist).
34+
access-denied,
35+
36+
/// Some implementation-specific error has occurred (e.g. I/O)
37+
other(string)
38+
}
39+
40+
/// A response to a `list-keys` operation.
41+
record key-response {
42+
/// The list of keys returned by the query.
43+
keys: list<string>,
44+
/// The continuation token to use to fetch the next page of keys. If this is `null`, then
45+
/// there are no more keys to fetch.
46+
cursor: option<u64>
47+
}
48+
49+
/// Get the bucket with the specified identifier.
50+
///
51+
/// `identifier` must refer to a bucket provided by the host.
52+
///
53+
/// `error::no-such-store` will be raised if the `identifier` is not recognized.
54+
open: func(identifier: string) -> result<bucket, error>;
55+
56+
/// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the
57+
/// bucket, and the bucket itself acts as a collection of all these entries.
58+
///
59+
/// It is worth noting that the exact terminology for bucket in key-value stores can very
60+
/// depending on the specific implementation. For example:
61+
///
62+
/// 1. Amazon DynamoDB calls a collection of key-value pairs a table
63+
/// 2. Redis has hashes, sets, and sorted sets as different types of collections
64+
/// 3. Cassandra calls a collection of key-value pairs a column family
65+
/// 4. MongoDB calls a collection of key-value pairs a collection
66+
/// 5. Riak calls a collection of key-value pairs a bucket
67+
/// 6. Memcached calls a collection of key-value pairs a slab
68+
/// 7. Azure Cosmos DB calls a collection of key-value pairs a container
69+
///
70+
/// In this interface, we use the term `bucket` to refer to a collection of key-value pairs
71+
resource bucket {
72+
/// Get the value associated with the specified `key`
73+
///
74+
/// The value is returned as an option. If the key-value pair exists in the
75+
/// store, it returns `Ok(value)`. If the key does not exist in the
76+
/// store, it returns `Ok(none)`.
77+
///
78+
/// If any other error occurs, it returns an `Err(error)`.
79+
get: func(key: string) -> result<option<list<u8>>, error>;
80+
81+
/// Set the value associated with the key in the store. If the key already
82+
/// exists in the store, it overwrites the value.
83+
///
84+
/// If the key does not exist in the store, it creates a new key-value pair.
85+
///
86+
/// If any other error occurs, it returns an `Err(error)`.
87+
set: func(key: string, value: list<u8>) -> result<_, error>;
88+
89+
/// Delete the key-value pair associated with the key in the store.
90+
///
91+
/// If the key does not exist in the store, it does nothing.
92+
///
93+
/// If any other error occurs, it returns an `Err(error)`.
94+
delete: func(key: string) -> result<_, error>;
95+
96+
/// Check if the key exists in the store.
97+
///
98+
/// If the key exists in the store, it returns `Ok(true)`. If the key does
99+
/// not exist in the store, it returns `Ok(false)`.
100+
///
101+
/// If any other error occurs, it returns an `Err(error)`.
102+
exists: func(key: string) -> result<bool, error>;
103+
104+
/// Get all the keys in the store with an optional cursor (for use in pagination). It
105+
/// returns a list of keys. Please note that for most KeyValue implementations, this is a
106+
/// can be a very expensive operation and so it should be used judiciously. Implementations
107+
/// can return any number of keys in a single response, but they should never attempt to
108+
/// send more data than is reasonable (i.e. on a small edge device, this may only be a few
109+
/// KB, while on a large machine this could be several MB). Any response should also return
110+
/// a cursor that can be used to fetch the next page of keys. See the `key-response` record
111+
/// for more information.
112+
///
113+
/// Note that the keys are not guaranteed to be returned in any particular order.
114+
///
115+
/// If the store is empty, it returns an empty list.
116+
///
117+
/// MAY show an out-of-date list of keys if there are concurrent writes to the store.
118+
///
119+
/// If any error occurs, it returns an `Err(error)`.
120+
list-keys: func(cursor: option<u64>) -> result<key-response, error>;
121+
}
122+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// A keyvalue interface that provides watch operations.
2+
///
3+
/// This interface is used to provide event-driven mechanisms to handle
4+
/// keyvalue changes.
5+
interface watcher {
6+
/// A keyvalue interface that provides handle-watch operations.
7+
use store.{bucket};
8+
9+
/// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket`
10+
/// that can be used to interact with the store.
11+
on-set: func(bucket: bucket, key: string, value: list<u8>);
12+
13+
/// Handle the `delete` event for the given bucket and key. It includes a reference to the
14+
/// `bucket` that can be used to interact with the store.
15+
on-delete: func(bucket: bucket, key: string);
16+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package wasi:keyvalue@0.2.0-draft;
2+
3+
/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores.
4+
/// Components targeting this world will be able to do:
5+
///
6+
/// 1. CRUD (create, read, update, delete) operations on key-value stores.
7+
/// 2. Atomic `increment` and CAS (compare-and-swap) operations.
8+
/// 3. Batch operations that can reduce the number of round trips to the network.
9+
world imports {
10+
/// The `store` capability allows the component to perform eventually consistent operations on
11+
/// the key-value store.
12+
import store;
13+
14+
/// The `atomic` capability allows the component to perform atomic / `increment` and CAS
15+
/// (compare-and-swap) operations.
16+
import atomics;
17+
18+
/// The `batch` capability allows the component to perform eventually consistent batch
19+
/// operations that can reduce the number of round trips to the network.
20+
import batch;
21+
}
22+
23+
world watch-service {
24+
include imports;
25+
export watcher;
26+
}

wit/world.wit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ world http-trigger-rc20231018 {
1616
world platform {
1717
include wasi:cli/imports@0.2.0;
1818
import wasi:http/outgoing-handler@0.2.0;
19+
import wasi:keyvalue/store@0.2.0-draft;
1920
import llm;
2021
import redis;
2122
import mqtt;

0 commit comments

Comments
 (0)