1use std::{
7 collections::{HashMap, HashSet},
8 sync::Arc,
9};
10
11const WASI_KEYVALUE_ID: &str = "wasi-keyvalue";
12use tokio::sync::RwLock;
13use wasmtime::component::Resource;
14
15use crate::{
16 engine::{
17 ctx::Ctx,
18 workload::{ResolvedWorkload, WorkloadComponent},
19 },
20 plugin::HostPlugin,
21 wit::{WitInterface, WitWorld},
22};
23
24mod bindings {
25 wasmtime::component::bindgen!({
26 world: "keyvalue",
27 trappable_imports: true,
28 async: true,
29 with: {
30 "wasi:keyvalue/store/bucket": crate::plugin::wasi_keyvalue::BucketHandle,
31 },
32 });
33}
34
35use bindings::wasi::keyvalue::store::{Error as StoreError, KeyResponse};
36
37#[derive(Clone, Debug)]
39pub struct BucketData {
40 pub name: String,
41 pub data: HashMap<String, Vec<u8>>,
42 pub created_at: u64,
43}
44
45pub type BucketHandle = String;
47
48#[derive(Clone, Default)]
50pub struct WasiKeyvalue {
51 storage: Arc<RwLock<HashMap<String, HashMap<String, BucketData>>>>,
53}
54
55impl WasiKeyvalue {
56 pub fn new() -> Self {
57 Self {
58 storage: Arc::new(RwLock::new(HashMap::new())),
59 }
60 }
61
62 fn get_timestamp() -> u64 {
63 std::time::SystemTime::now()
64 .duration_since(std::time::SystemTime::UNIX_EPOCH)
65 .unwrap_or_default()
66 .as_secs()
67 }
68}
69
70impl bindings::wasi::keyvalue::store::Host for Ctx {
72 async fn open(
73 &mut self,
74 identifier: String,
75 ) -> anyhow::Result<Result<Resource<BucketHandle>, StoreError>> {
76 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
77 return Ok(Err(StoreError::Other(
78 "keyvalue plugin not available".to_string(),
79 )));
80 };
81
82 let mut storage = plugin.storage.write().await;
83 let workload_storage = storage.entry(self.id.clone()).or_default();
84
85 if !workload_storage.contains_key(&identifier) {
87 let bucket_data = BucketData {
88 name: identifier.clone(),
89 data: HashMap::new(),
90 created_at: WasiKeyvalue::get_timestamp(),
91 };
92 workload_storage.insert(identifier.clone(), bucket_data);
93 }
94
95 let resource = self.table.push(identifier)?;
96 Ok(Ok(resource))
97 }
98}
99
100impl bindings::wasi::keyvalue::store::HostBucket for Ctx {
102 async fn get(
103 &mut self,
104 bucket: Resource<BucketHandle>,
105 key: String,
106 ) -> anyhow::Result<Result<Option<Vec<u8>>, StoreError>> {
107 let bucket_name = self.table.get(&bucket)?;
108
109 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
110 return Ok(Err(StoreError::Other(
111 "keyvalue plugin not available".to_string(),
112 )));
113 };
114
115 let storage = plugin.storage.read().await;
116 let empty_map = HashMap::new();
117 let workload_storage = storage.get(&self.id).unwrap_or(&empty_map);
118
119 match workload_storage.get(bucket_name) {
120 Some(bucket_data) => {
121 let value = bucket_data.data.get(&key).cloned();
122 Ok(Ok(value))
123 }
124 None => Ok(Err(StoreError::Other(format!(
125 "bucket '{bucket_name}' does not exist"
126 )))),
127 }
128 }
129
130 async fn set(
131 &mut self,
132 bucket: Resource<BucketHandle>,
133 key: String,
134 value: Vec<u8>,
135 ) -> anyhow::Result<Result<(), StoreError>> {
136 let bucket_name = self.table.get(&bucket)?;
137
138 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
139 return Ok(Err(StoreError::Other(
140 "keyvalue plugin not available".to_string(),
141 )));
142 };
143
144 let mut storage = plugin.storage.write().await;
145 let workload_storage = storage.entry(self.id.clone()).or_default();
146
147 match workload_storage.get_mut(bucket_name) {
148 Some(bucket_data) => {
149 bucket_data.data.insert(key, value);
150 Ok(Ok(()))
151 }
152 None => Ok(Err(StoreError::Other(format!(
153 "bucket '{bucket_name}' does not exist"
154 )))),
155 }
156 }
157
158 async fn delete(
159 &mut self,
160 bucket: Resource<BucketHandle>,
161 key: String,
162 ) -> anyhow::Result<Result<(), StoreError>> {
163 let bucket_name = self.table.get(&bucket)?;
164
165 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
166 return Ok(Err(StoreError::Other(
167 "keyvalue plugin not available".to_string(),
168 )));
169 };
170
171 let mut storage = plugin.storage.write().await;
172 let workload_storage = storage.entry(self.id.clone()).or_default();
173
174 match workload_storage.get_mut(bucket_name) {
175 Some(bucket_data) => {
176 bucket_data.data.remove(&key);
177 Ok(Ok(()))
178 }
179 None => Ok(Err(StoreError::Other(format!(
180 "bucket '{bucket_name}' does not exist"
181 )))),
182 }
183 }
184
185 async fn exists(
186 &mut self,
187 bucket: Resource<BucketHandle>,
188 key: String,
189 ) -> anyhow::Result<Result<bool, StoreError>> {
190 let bucket_name = self.table.get(&bucket)?;
191
192 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
193 return Ok(Err(StoreError::Other(
194 "keyvalue plugin not available".to_string(),
195 )));
196 };
197
198 let storage = plugin.storage.read().await;
199 let empty_map = HashMap::new();
200 let workload_storage = storage.get(&self.id).unwrap_or(&empty_map);
201
202 match workload_storage.get(bucket_name) {
203 Some(bucket_data) => Ok(Ok(bucket_data.data.contains_key(&key))),
204 None => Ok(Err(StoreError::Other(format!(
205 "bucket '{bucket_name}' does not exist"
206 )))),
207 }
208 }
209
210 async fn list_keys(
211 &mut self,
212 bucket: Resource<BucketHandle>,
213 cursor: Option<u64>,
214 ) -> anyhow::Result<Result<KeyResponse, StoreError>> {
215 let bucket_name = self.table.get(&bucket)?;
216
217 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
218 return Ok(Err(StoreError::Other(
219 "keyvalue plugin not available".to_string(),
220 )));
221 };
222
223 let storage = plugin.storage.read().await;
224 let empty_map = HashMap::new();
225 let workload_storage = storage.get(&self.id).unwrap_or(&empty_map);
226
227 match workload_storage.get(bucket_name) {
228 Some(bucket_data) => {
229 let mut keys: Vec<String> = bucket_data.data.keys().cloned().collect();
230 keys.sort(); let start_index = cursor.unwrap_or(0) as usize;
234
235 const PAGE_SIZE: usize = 100;
237 let end_index = std::cmp::min(start_index + PAGE_SIZE, keys.len());
238 let page_keys = keys[start_index..end_index].to_vec();
239
240 let next_cursor = if end_index < keys.len() {
242 Some(end_index as u64)
243 } else {
244 None
245 };
246
247 Ok(Ok(KeyResponse {
248 keys: page_keys,
249 cursor: next_cursor,
250 }))
251 }
252 None => Ok(Err(StoreError::Other(format!(
253 "bucket '{bucket_name}' does not exist"
254 )))),
255 }
256 }
257
258 async fn drop(&mut self, rep: Resource<BucketHandle>) -> anyhow::Result<()> {
259 tracing::debug!(
260 workload_id = self.id,
261 resource_id = ?rep,
262 "Dropping bucket resource"
263 );
264 self.table.delete(rep)?;
265 Ok(())
266 }
267}
268
269impl bindings::wasi::keyvalue::atomics::Host for Ctx {
271 async fn increment(
272 &mut self,
273 bucket: Resource<BucketHandle>,
274 key: String,
275 delta: u64,
276 ) -> anyhow::Result<Result<u64, StoreError>> {
277 let bucket_name = self.table.get(&bucket)?;
278
279 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
280 return Ok(Err(StoreError::Other(
281 "keyvalue plugin not available".to_string(),
282 )));
283 };
284
285 let mut storage = plugin.storage.write().await;
286 let workload_storage = storage.entry(self.id.clone()).or_default();
287
288 match workload_storage.get_mut(bucket_name) {
289 Some(bucket_data) => {
290 let current_bytes = bucket_data.data.get(&key);
292 let current_value = if let Some(bytes) = current_bytes {
293 if bytes.len() == 8 {
295 u64::from_le_bytes(bytes.clone().try_into().unwrap_or([0; 8]))
296 } else {
297 String::from_utf8_lossy(bytes).parse::<u64>().unwrap_or(0)
299 }
300 } else {
301 0
302 };
303
304 let new_value = current_value.saturating_add(delta);
305
306 bucket_data
308 .data
309 .insert(key, new_value.to_le_bytes().to_vec());
310
311 Ok(Ok(new_value))
312 }
313 None => Ok(Err(StoreError::Other(format!(
314 "bucket '{bucket_name}' does not exist"
315 )))),
316 }
317 }
318}
319
320impl bindings::wasi::keyvalue::batch::Host for Ctx {
322 async fn get_many(
323 &mut self,
324 bucket: Resource<BucketHandle>,
325 keys: Vec<String>,
326 ) -> anyhow::Result<Result<Vec<Option<(String, Vec<u8>)>>, StoreError>> {
327 let bucket_name = self.table.get(&bucket)?;
328
329 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
330 return Ok(Err(StoreError::Other(
331 "keyvalue plugin not available".to_string(),
332 )));
333 };
334
335 let storage = plugin.storage.read().await;
336 let empty_map = HashMap::new();
337 let workload_storage = storage.get(&self.id).unwrap_or(&empty_map);
338
339 match workload_storage.get(bucket_name) {
340 Some(bucket_data) => {
341 let results: Vec<Option<(String, Vec<u8>)>> = keys
342 .into_iter()
343 .map(|key| {
344 bucket_data
345 .data
346 .get(&key)
347 .cloned()
348 .map(|value| (key, value))
349 })
350 .collect();
351 Ok(Ok(results))
352 }
353 None => Ok(Err(StoreError::Other(format!(
354 "bucket '{bucket_name}' does not exist"
355 )))),
356 }
357 }
358
359 async fn set_many(
360 &mut self,
361 bucket: Resource<BucketHandle>,
362 key_values: Vec<(String, Vec<u8>)>,
363 ) -> anyhow::Result<Result<(), StoreError>> {
364 let bucket_name = self.table.get(&bucket)?;
365
366 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
367 return Ok(Err(StoreError::Other(
368 "keyvalue plugin not available".to_string(),
369 )));
370 };
371
372 let mut storage = plugin.storage.write().await;
373 let workload_storage = storage.entry(self.id.clone()).or_default();
374
375 match workload_storage.get_mut(bucket_name) {
376 Some(bucket_data) => {
377 for (key, value) in key_values {
378 bucket_data.data.insert(key, value);
379 }
380 Ok(Ok(()))
381 }
382 None => Ok(Err(StoreError::Other(format!(
383 "bucket '{bucket_name}' does not exist"
384 )))),
385 }
386 }
387
388 async fn delete_many(
389 &mut self,
390 bucket: Resource<BucketHandle>,
391 keys: Vec<String>,
392 ) -> anyhow::Result<Result<(), StoreError>> {
393 let bucket_name = self.table.get(&bucket)?;
394
395 let Some(plugin) = self.get_plugin::<WasiKeyvalue>(WASI_KEYVALUE_ID) else {
396 return Ok(Err(StoreError::Other(
397 "keyvalue plugin not available".to_string(),
398 )));
399 };
400
401 let mut storage = plugin.storage.write().await;
402 let workload_storage = storage.entry(self.id.clone()).or_default();
403
404 match workload_storage.get_mut(bucket_name) {
405 Some(bucket_data) => {
406 for key in keys {
407 bucket_data.data.remove(&key);
408 }
409 Ok(Ok(()))
410 }
411 None => Ok(Err(StoreError::Other(format!(
412 "bucket '{bucket_name}' does not exist"
413 )))),
414 }
415 }
416}
417
418#[async_trait::async_trait]
419impl HostPlugin for WasiKeyvalue {
420 fn id(&self) -> &'static str {
421 WASI_KEYVALUE_ID
422 }
423
424 fn world(&self) -> WitWorld {
425 WitWorld {
426 imports: HashSet::from([WitInterface::from(
427 "wasi:keyvalue/store,atomics,batch@0.2.0-draft",
428 )]),
429 ..Default::default()
430 }
431 }
432
433 async fn on_component_bind(
434 &self,
435 component: &mut WorkloadComponent,
436 interfaces: std::collections::HashSet<crate::wit::WitInterface>,
437 ) -> anyhow::Result<()> {
438 let has_keyvalue = interfaces
440 .iter()
441 .any(|i| i.namespace == "wasi" && i.package == "keyvalue");
442
443 if !has_keyvalue {
444 tracing::warn!(
445 "WasiKeyvalue plugin requested for non-wasi:keyvalue interface(s): {:?}",
446 interfaces
447 );
448 return Ok(());
449 }
450
451 tracing::debug!(
452 workload_id = component.id(),
453 "Adding keyvalue interfaces to linker for workload"
454 );
455 let linker = component.linker();
456
457 bindings::wasi::keyvalue::store::add_to_linker(linker, |ctx| ctx)?;
458 bindings::wasi::keyvalue::atomics::add_to_linker(linker, |ctx| ctx)?;
459 bindings::wasi::keyvalue::batch::add_to_linker(linker, |ctx| ctx)?;
460
461 let id = component.id();
462 tracing::debug!(
463 workload_id = id,
464 "Successfully added keyvalue interfaces to linker for workload"
465 );
466
467 let mut storage = self.storage.write().await;
469 storage.insert(id.to_string(), HashMap::new());
470
471 tracing::debug!("WasiKeyvalue plugin bound to workload '{id}'");
472
473 Ok(())
474 }
475
476 async fn on_workload_unbind(
477 &self,
478 workload_handle: &ResolvedWorkload,
479 _interfaces: std::collections::HashSet<crate::wit::WitInterface>,
480 ) -> anyhow::Result<()> {
481 let id = workload_handle.id();
482 let mut storage = self.storage.write().await;
484 storage.remove(id);
485
486 tracing::debug!("WasiKeyvalue plugin unbound from workload '{id}'");
487
488 Ok(())
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn test_wasi_keyvalue_creation() {
498 let keyvalue = WasiKeyvalue::new();
499 assert!(keyvalue.storage.try_read().is_ok());
500 }
501
502 #[test]
503 fn test_get_timestamp() {
504 let timestamp = WasiKeyvalue::get_timestamp();
505 assert!(timestamp > 0);
506 }
507
508 #[test]
509 fn test_bucket_data_creation() {
510 let bucket = BucketData {
511 name: "test-bucket".to_string(),
512 data: HashMap::new(),
513 created_at: WasiKeyvalue::get_timestamp(),
514 };
515
516 assert_eq!(bucket.name, "test-bucket");
517 assert!(bucket.data.is_empty());
518 assert!(bucket.created_at > 0);
519 }
520
521 #[tokio::test]
522 async fn test_storage_operations() {
523 let keyvalue = WasiKeyvalue::new();
524
525 {
527 let mut storage = keyvalue.storage.write().await;
528 storage.insert("workload1".to_string(), HashMap::new());
529 }
530
531 {
533 let storage = keyvalue.storage.read().await;
534 assert!(storage.contains_key("workload1"));
535 }
536 }
537
538 #[test]
539 fn test_batch_operations_data_structures() {
540 let key_values = [
542 ("key1".to_string(), b"value1".to_vec()),
543 ("key2".to_string(), b"value2".to_vec()),
544 ];
545 assert_eq!(key_values.len(), 2);
546
547 let keys = ["key1".to_string(), "key2".to_string()];
548 assert_eq!(keys.len(), 2);
549
550 let results: Vec<Option<(String, Vec<u8>)>> = vec![
551 Some(("key1".to_string(), b"value1".to_vec())),
552 None, ];
554 assert_eq!(results.len(), 2);
555 assert!(results[0].is_some());
556 assert!(results[1].is_none());
557 }
558}