From ab2388a65d516a12058e271cbd8148f1368ee28c Mon Sep 17 00:00:00 2001 From: Trevor Elliott Date: Tue, 13 Aug 2024 11:02:42 -0700 Subject: [PATCH] Add the object-store interface, and rework kv-store to match the hostcalls --- crates/adapter/src/fastly/core.rs | 526 +++++++++++++++++++++++- lib/data/viceroy-component-adapter.wasm | Bin 180590 -> 190129 bytes lib/src/component/kv_store.rs | 179 ++++---- lib/src/component/mod.rs | 2 + lib/src/component/object_store.rs | 140 +++++++ lib/wit/deps/fastly/compute.wit | 109 ++++- 6 files changed, 835 insertions(+), 121 deletions(-) create mode 100644 lib/src/component/object_store.rs diff --git a/crates/adapter/src/fastly/core.rs b/crates/adapter/src/fastly/core.rs index 0d8820ff..e2430b45 100644 --- a/crates/adapter/src/fastly/core.rs +++ b/crates/adapter/src/fastly/core.rs @@ -89,6 +89,7 @@ pub enum HttpKeepaliveMode { pub type PendingObjectStoreLookupHandle = u32; pub type PendingObjectStoreInsertHandle = u32; pub type PendingObjectStoreDeleteHandle = u32; +pub type PendingObjectStoreListHandle = u32; pub type BodyHandle = u32; pub type PendingRequestHandle = u32; pub type RequestHandle = u32; @@ -2310,9 +2311,9 @@ pub mod fastly_erl { } } -pub mod fastly_kv_store { +pub mod fastly_object_store { use super::*; - use crate::bindings::fastly::api::kv_store; + use crate::bindings::fastly::api::object_store; use core::slice; #[export_name = "fastly_object_store#open"] @@ -2322,7 +2323,7 @@ pub mod fastly_kv_store { kv_store_handle_out: *mut KVStoreHandle, ) -> FastlyStatus { let name = unsafe { slice::from_raw_parts(name_ptr, name_len) }; - match kv_store::open(name) { + match object_store::open(name) { Ok(None) => { unsafe { *kv_store_handle_out = INVALID_HANDLE; @@ -2348,7 +2349,7 @@ pub mod fastly_kv_store { body_handle_out: *mut BodyHandle, ) -> FastlyStatus { let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; - match kv_store::lookup(kv_store_handle, key) { + match object_store::lookup(kv_store_handle, key) { Ok(res) => { unsafe { *body_handle_out = res.unwrap_or(INVALID_HANDLE); @@ -2367,7 +2368,7 @@ pub mod fastly_kv_store { pending_body_handle_out: *mut PendingObjectStoreLookupHandle, ) -> FastlyStatus { let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; - match kv_store::lookup_async(kv_store_handle, key) { + match object_store::lookup_async(kv_store_handle, key) { Ok(res) => { unsafe { *pending_body_handle_out = res; @@ -2383,7 +2384,7 @@ pub mod fastly_kv_store { pending_body_handle: PendingObjectStoreLookupHandle, body_handle_out: *mut BodyHandle, ) -> FastlyStatus { - match kv_store::pending_lookup_wait(pending_body_handle) { + match object_store::pending_lookup_wait(pending_body_handle) { Ok(res) => { unsafe { *body_handle_out = res.unwrap_or(INVALID_HANDLE); @@ -2402,7 +2403,7 @@ pub mod fastly_kv_store { body_handle: BodyHandle, ) -> FastlyStatus { let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; - convert_result(kv_store::insert(kv_store_handle, key, body_handle)) + convert_result(object_store::insert(kv_store_handle, key, body_handle)) } #[export_name = "fastly_object_store#insert_async"] @@ -2414,7 +2415,7 @@ pub mod fastly_kv_store { pending_body_handle_out: *mut PendingObjectStoreInsertHandle, ) -> FastlyStatus { let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; - match kv_store::insert_async(kv_store_handle, key, body_handle) { + match object_store::insert_async(kv_store_handle, key, body_handle) { Ok(res) => { unsafe { *pending_body_handle_out = res; @@ -2429,7 +2430,7 @@ pub mod fastly_kv_store { pub fn pending_insert_wait( pending_body_handle: PendingObjectStoreInsertHandle, ) -> FastlyStatus { - convert_result(kv_store::pending_insert_wait(pending_body_handle)) + convert_result(object_store::pending_insert_wait(pending_body_handle)) } #[export_name = "fastly_object_store#delete_async"] @@ -2440,7 +2441,7 @@ pub mod fastly_kv_store { pending_body_handle_out: *mut PendingObjectStoreDeleteHandle, ) -> FastlyStatus { let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; - match kv_store::delete_async(kv_store_handle, key) { + match object_store::delete_async(kv_store_handle, key) { Ok(res) => { unsafe { *pending_body_handle_out = res; @@ -2455,7 +2456,510 @@ pub mod fastly_kv_store { pub fn pending_delete_wait( pending_body_handle: PendingObjectStoreDeleteHandle, ) -> FastlyStatus { - convert_result(kv_store::pending_delete_wait(pending_body_handle)) + convert_result(object_store::pending_delete_wait(pending_body_handle)) + } +} + +pub mod fastly_kv_store { + use super::*; + use crate::bindings::fastly::api::kv_store; + use core::slice; + + /// Modes of KV Store insertion. + /// + /// This type serves to facilitate alternative methods of key insertion. + #[repr(C)] + #[derive(Default, Clone, Copy)] + pub enum InsertMode { + /// The default method of insertion. Create a key, or overwrite an existing one + #[default] + Overwrite, + /// Only insert if the key does not currently exist + Add, + /// Append this insertion's body onto a key's value if it exists (or create a new key if there is none) + Append, + /// Prepend this insertion's body onto a key's value if it exists (or create a new key if there is none) + Prepend, + } + + impl From for kv_store::InsertMode { + fn from(value: InsertMode) -> Self { + match value { + InsertMode::Overwrite => Self::Overwrite, + InsertMode::Add => Self::Add, + InsertMode::Append => Self::Append, + InsertMode::Prepend => Self::Prepend, + } + } + } + + #[repr(C)] + pub struct InsertConfig { + pub mode: InsertMode, + pub if_generation_match: u32, + pub metadata: *const u8, + pub metadata_len: u32, + pub time_to_live_sec: u32, + } + + impl Default for InsertConfig { + fn default() -> Self { + InsertConfig { + mode: InsertMode::Overwrite, + if_generation_match: 0, + metadata: std::ptr::null(), + metadata_len: 0, + time_to_live_sec: 0, + } + } + } + + #[repr(C)] + #[derive(Default, Copy, Clone)] + pub enum ListModeInternal { + #[default] + Strong, + Eventual, + } + + impl From for kv_store::ListMode { + fn from(value: ListModeInternal) -> Self { + match value { + ListModeInternal::Strong => Self::Strong, + ListModeInternal::Eventual => Self::Eventual, + } + } + } + + #[repr(C)] + pub struct ListConfig { + pub mode: ListModeInternal, + pub cursor: *const u8, + pub cursor_len: u32, + pub limit: u32, + pub prefix: *const u8, + pub prefix_len: u32, + } + + impl Default for ListConfig { + fn default() -> Self { + ListConfig { + mode: ListModeInternal::Strong, + cursor: std::ptr::null(), + cursor_len: 0, + limit: 0, + prefix: std::ptr::null(), + prefix_len: 0, + } + } + } + + #[repr(C)] + pub struct LookupConfig { + // reserved is just a placeholder, + // can be removed when somethin real is added + reserved: u32, + } + + impl Default for LookupConfig { + fn default() -> Self { + LookupConfig { reserved: 0 } + } + } + + #[repr(C)] + pub struct DeleteConfig { + // reserved is just a placeholder, + // can be removed when somethin real is added + reserved: u32, + } + + impl Default for DeleteConfig { + fn default() -> Self { + DeleteConfig { reserved: 0 } + } + } + + bitflags::bitflags! { + /// `InsertConfigOptions` codings. + #[derive(Default)] + #[repr(transparent)] + pub struct InsertConfigOptions: u32 { + const RESERVED = 1 << 0; + const BACKGROUND_FETCH = 1 << 1; + const IF_GENERATION_MATCH = 1 << 2; + const METADATA = 1 << 3; + const TIME_TO_LIVE_SEC = 1 << 4; + } + /// `ListConfigOptions` codings. + #[derive(Default)] + #[repr(transparent)] + pub struct ListConfigOptions: u32 { + const RESERVED = 1 << 0; + const CURSOR = 1 << 1; + const LIMIT = 1 << 2; + const PREFIX = 1 << 3; + } + /// `LookupConfigOptions` codings. + #[derive(Default)] + #[repr(transparent)] + pub struct LookupConfigOptions: u32 { + const RESERVED = 1 << 0; + } + /// `DeleteConfigOptions` codings. + #[derive(Default)] + #[repr(transparent)] + pub struct DeleteConfigOptions: u32 { + const RESERVED = 1 << 0; + } + } + + impl From for kv_store::InsertConfigOptions { + fn from(value: InsertConfigOptions) -> Self { + let mut res = Self::empty(); + res.set( + Self::RESERVED, + value.contains(InsertConfigOptions::RESERVED), + ); + res.set( + Self::BACKGROUND_FETCH, + value.contains(InsertConfigOptions::BACKGROUND_FETCH), + ); + res.set( + Self::IF_GENERATION_MATCH, + value.contains(InsertConfigOptions::IF_GENERATION_MATCH), + ); + res.set( + Self::METADATA, + value.contains(InsertConfigOptions::METADATA), + ); + res.set( + Self::TIME_TO_LIVE_SEC, + value.contains(InsertConfigOptions::TIME_TO_LIVE_SEC), + ); + res + } + } + + impl From for kv_store::ListConfigOptions { + fn from(value: ListConfigOptions) -> Self { + let mut res = Self::empty(); + res.set(Self::RESERVED, value.contains(ListConfigOptions::RESERVED)); + res.set(Self::CURSOR, value.contains(ListConfigOptions::CURSOR)); + res.set(Self::LIMIT, value.contains(ListConfigOptions::LIMIT)); + res.set(Self::PREFIX, value.contains(ListConfigOptions::PREFIX)); + res + } + } + + #[repr(u32)] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum KvError { + Uninitialized, + Ok, + BadRequest, + NotFound, + PreconditionFailed, + PayloadTooLarge, + InternalError, + } + + #[export_name = "fastly_kv_store#open"] + pub fn open_v2( + name_ptr: *const u8, + name_len: usize, + kv_store_handle_out: *mut KVStoreHandle, + ) -> FastlyStatus { + let name = unsafe { slice::from_raw_parts(name_ptr, name_len) }; + match kv_store::open(name) { + Ok(None) => { + unsafe { + *kv_store_handle_out = INVALID_HANDLE; + } + + FastlyStatus::INVALID_ARGUMENT + } + + Ok(Some(res)) => { + unsafe { + *kv_store_handle_out = res; + } + + FastlyStatus::OK + } + + Err(e) => e.into(), + } + } + + #[export_name = "fastly_kv_store#lookup"] + pub fn lookup_v2( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + // NOTE: mask and config are ignored in the wit definition while they're empty + _lookup_config_mask: LookupConfigOptions, + _lookup_config: *const LookupConfig, + pending_body_handle_out: *mut PendingObjectStoreLookupHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + match kv_store::lookup(kv_store_handle, key) { + Ok(res) => { + unsafe { + *pending_body_handle_out = res; + } + + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_kv_store#lookup_wait"] + pub fn pending_lookup_wait_v2( + pending_handle: PendingObjectStoreLookupHandle, + body_handle_out: *mut BodyHandle, + metadata_out: *mut u8, + metadata_len: *mut usize, + generation_out: *mut u32, + kv_error_out: *mut KvError, + ) -> FastlyStatus { + let res = match kv_store::lookup_wait(pending_handle) { + Ok(Some(res)) => res, + Ok(None) => { + unsafe { + *kv_error_out = KvError::NotFound; + } + + return FastlyStatus::OK; + } + Err(e) => { + unsafe { + // TODO: the wit interface doesn't return any KvError values + *kv_error_out = KvError::Uninitialized; + } + + return e.into(); + } + }; + + let max_len = unsafe { *metadata_len }; + + with_buffer!( + metadata_out, + max_len, + { res.metadata(u64::try_from(max_len).trapping_unwrap()) }, + |res| { + let buf = handle_buffer_len!(res, metadata_len); + + unsafe { + *metadata_len = buf.as_ref().map(Vec::len).unwrap_or(0); + } + + std::mem::forget(buf); + } + ); + + unsafe { + *body_handle_out = res.body(); + *generation_out = res.generation(); + *kv_error_out = KvError::Ok; + } + + FastlyStatus::OK + } + + #[export_name = "fastly_kv_store#insert"] + pub fn insert_v2( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + body_handle: BodyHandle, + insert_config_mask: InsertConfigOptions, + insert_config: *const InsertConfig, + pending_body_handle_out: *mut PendingObjectStoreInsertHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + + let insert_config_mask = insert_config_mask.into(); + let insert_config = unsafe { + kv_store::InsertConfig { + mode: (*insert_config).mode.into(), + if_generation_match: (*insert_config).if_generation_match, + metadata: { + let len = usize::try_from((*insert_config).metadata_len).trapping_unwrap(); + Vec::from_raw_parts((*insert_config).metadata as *mut _, len, len) + }, + time_to_live_sec: (*insert_config).time_to_live_sec, + } + }; + + let res = kv_store::insert( + kv_store_handle, + key, + body_handle, + insert_config_mask, + &insert_config, + ); + + // We don't own the memory in metadata, so forget the vector that the insert config holds. + std::mem::forget(insert_config); + + match res { + Ok(res) => { + unsafe { + *pending_body_handle_out = res; + } + + FastlyStatus::OK + } + + Err(e) => e.into(), + } + } + + #[export_name = "fastly_kv_store#insert_wait"] + pub fn pending_insert_wait_v2( + pending_body_handle: PendingObjectStoreInsertHandle, + kv_error_out: *mut KvError, + ) -> FastlyStatus { + match kv_store::insert_wait(pending_body_handle) { + Ok(_) => { + unsafe { + *kv_error_out = KvError::Ok; + } + + FastlyStatus::OK + } + + // TODO: the wit interface doesn't return any KvError values + Err(e) => { + unsafe { + // TODO: the wit interface doesn't return any KvError values + *kv_error_out = KvError::Uninitialized; + } + + e.into() + } + } + } + + #[export_name = "fastly_kv_store#delete"] + pub fn delete_v2( + kv_store_handle: KVStoreHandle, + key_ptr: *const u8, + key_len: usize, + // These are ignored in the wit interface for the time being, as they don't pass any + // meaningful values. + _delete_config_mask: DeleteConfigOptions, + _delete_config: *const DeleteConfig, + pending_body_handle_out: *mut PendingObjectStoreDeleteHandle, + ) -> FastlyStatus { + let key = unsafe { slice::from_raw_parts(key_ptr, key_len) }; + match kv_store::delete(kv_store_handle, key) { + Ok(res) => { + unsafe { + *pending_body_handle_out = res; + } + + FastlyStatus::OK + } + + Err(e) => e.into(), + } + } + + #[export_name = "fastly_kv_store#delete_wait"] + pub fn pending_delete_wait_v2( + pending_body_handle: PendingObjectStoreDeleteHandle, + kv_error_out: *mut KvError, + ) -> FastlyStatus { + match kv_store::delete_wait(pending_body_handle) { + Ok(_) => { + unsafe { + *kv_error_out = KvError::Ok; + } + + FastlyStatus::OK + } + + Err(e) => { + unsafe { + // TODO: the wit interface doesn't return any KvError values + *kv_error_out = KvError::Uninitialized; + } + + e.into() + } + } + } + + #[export_name = "fastly_kv_store#list"] + pub fn list_v2( + kv_store_handle: KVStoreHandle, + list_config_mask: ListConfigOptions, + list_config: *const ListConfig, + pending_body_handle_out: *mut PendingObjectStoreListHandle, + ) -> FastlyStatus { + let mask = list_config_mask.into(); + + let config = unsafe { + kv_store::ListConfig { + mode: (*list_config).mode.into(), + cursor: { + let len = usize::try_from((*list_config).cursor_len).trapping_unwrap(); + Vec::from_raw_parts((*list_config).cursor as *mut _, len, len) + }, + limit: (*list_config).limit, + prefix: { + let len = usize::try_from((*list_config).prefix_len).trapping_unwrap(); + Vec::from_raw_parts((*list_config).cursor as *mut _, len, len) + }, + } + }; + + let res = kv_store::list(kv_store_handle, mask, &config); + + std::mem::forget(config); + + match res { + Ok(res) => { + unsafe { + *pending_body_handle_out = res; + } + + FastlyStatus::OK + } + + Err(e) => e.into(), + } + } + + #[export_name = "fastly_kv_store#list_wait"] + pub fn pending_list_wait_v2( + pending_body_handle: PendingObjectStoreListHandle, + body_handle_out: *mut BodyHandle, + kv_error_out: *mut KvError, + ) -> FastlyStatus { + match kv_store::list_wait(pending_body_handle) { + Ok(res) => { + unsafe { + *kv_error_out = KvError::Ok; + *body_handle_out = res; + } + + FastlyStatus::OK + } + + Err(e) => { + unsafe { + // TODO: the wit interface doesn't return any KvError values + *kv_error_out = KvError::Uninitialized; + } + + e.into() + } + } } } diff --git a/lib/data/viceroy-component-adapter.wasm b/lib/data/viceroy-component-adapter.wasm index 84e57249610fb41bd8af6d3b4fe70587655df898..1d7ff261ad3d4f84630e4707e3c3144fccd3d25b 100755 GIT binary patch delta 40115 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 delta 35074 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 Pk%Bf-6xjTa=;;3g7Q#P` diff --git a/lib/src/component/kv_store.rs b/lib/src/component/kv_store.rs index 2ebc4172..2cc6e1b2 100644 --- a/lib/src/component/kv_store.rs +++ b/lib/src/component/kv_store.rs @@ -1,140 +1,103 @@ use { - super::fastly::api::{http_types, kv_store, types}, - crate::{ - body::Body, - object_store::{ObjectKey, ObjectStoreError}, - session::{ - PeekableTask, PendingKvDeleteTask, PendingKvInsertTask, PendingKvLookupTask, Session, - }, - }, + super::fastly::api::{http_body, kv_store, types}, + crate::session::Session, }; +pub struct LookupResult; + #[async_trait::async_trait] -impl kv_store::Host for Session { - async fn open(&mut self, name: String) -> Result, types::Error> { - if self.object_store.store_exists(&name)? { - let handle = self.obj_store_handle(&name)?; - Ok(Some(handle.into())) - } else { - Ok(None) - } +impl kv_store::HostLookupResult for Session { + async fn body( + &mut self, + _self_: wasmtime::component::Resource, + ) -> http_body::BodyHandle { + todo!() } - async fn lookup( + async fn metadata( &mut self, - store: kv_store::Handle, - key: String, - ) -> Result, types::Error> { - let store = self.get_obj_store_key(store.into()).unwrap(); - let key = ObjectKey::new(&key)?; - match self.obj_lookup(store, &key) { - Ok(obj) => { - let new_handle = self.insert_body(Body::from(obj)); - Ok(Some(new_handle.into())) - } - // Don't write to the invalid handle as the SDK will return Ok(None) - // if the object does not exist. We need to return `Ok(())` here to - // make sure Viceroy does not crash - Err(ObjectStoreError::MissingObject) => Ok(None), - Err(err) => Err(err.into()), - } + _self_: wasmtime::component::Resource, + _max_len: u64, + ) -> Result>, types::Error> { + todo!() } - async fn lookup_async( + async fn generation( &mut self, - store: kv_store::Handle, - key: String, - ) -> Result { - let store = self.get_obj_store_key(store.into()).unwrap(); - let key = ObjectKey::new(key)?; - // just create a future that's already ready - let fut = futures::future::ok(self.obj_lookup(store, &key)); - let task = PendingKvLookupTask::new(PeekableTask::spawn(fut).await); - Ok(self.insert_pending_kv_lookup(task).into()) + _self_: wasmtime::component::Resource, + ) -> u32 { + todo!() } - async fn pending_lookup_wait( + fn drop( &mut self, - pending: kv_store::PendingLookupHandle, - ) -> Result, types::Error> { - let pending_obj = self - .take_pending_kv_lookup(pending.into())? - .task() - .recv() - .await?; - // proceed with the normal match from lookup() - match pending_obj { - Ok(obj) => Ok(Some(self.insert_body(Body::from(obj)).into())), - Err(ObjectStoreError::MissingObject) => Ok(None), - Err(err) => Err(err.into()), - } + _rep: wasmtime::component::Resource, + ) -> wasmtime::Result<()> { + todo!() } +} - async fn insert( +#[async_trait::async_trait] +impl kv_store::Host for Session { + async fn open(&mut self, _name: String) -> Result, types::Error> { + todo!() + } + + async fn lookup( &mut self, - store: kv_store::Handle, - key: String, - body_handle: http_types::BodyHandle, - ) -> Result<(), types::Error> { - let store = self.get_obj_store_key(store.into()).unwrap().clone(); - let key = ObjectKey::new(&key)?; - let bytes = self.take_body(body_handle.into())?.read_into_vec().await?; - self.obj_insert(store, key, bytes)?; + _store: kv_store::Handle, + _key: String, + ) -> Result { + todo!() + } - Ok(()) + async fn lookup_wait( + &mut self, + _handle: kv_store::LookupHandle, + ) -> Result>, types::Error> { + todo!() } - async fn insert_async( + async fn insert( &mut self, - store: kv_store::Handle, - key: String, - body_handle: http_types::BodyHandle, - ) -> Result { - let store = self.get_obj_store_key(store.into()).unwrap().clone(); - let key = ObjectKey::new(&key)?; - let bytes = self.take_body(body_handle.into())?.read_into_vec().await?; - let fut = futures::future::ok(self.obj_insert(store, key, bytes)); - let task = PeekableTask::spawn(fut).await; + _store: kv_store::Handle, + _key: String, + _body_handle: kv_store::BodyHandle, + _mask: kv_store::InsertConfigOptions, + _config: kv_store::InsertConfig, + ) -> Result { + todo!() + } - Ok(self - .insert_pending_kv_insert(PendingKvInsertTask::new(task)) - .into()) + async fn insert_wait(&mut self, _handle: kv_store::InsertHandle) -> Result<(), types::Error> { + todo!() } - async fn pending_insert_wait( + async fn delete( &mut self, - handle: kv_store::PendingInsertHandle, - ) -> Result<(), types::Error> { - Ok((self - .take_pending_kv_insert(handle.into())? - .task() - .recv() - .await?)?) + _store: kv_store::Handle, + _key: String, + ) -> Result { + todo!() } - async fn delete_async( - &mut self, - store: kv_store::Handle, - key: String, - ) -> Result { - let store = self.get_obj_store_key(store.into()).unwrap().clone(); - let key = ObjectKey::new(&key)?; - let fut = futures::future::ok(self.obj_delete(store, key)); - let task = PeekableTask::spawn(fut).await; + async fn delete_wait(&mut self, _handle: kv_store::DeleteHandle) -> Result<(), types::Error> { + todo!() + } - Ok(self - .insert_pending_kv_delete(PendingKvDeleteTask::new(task)) - .into()) + async fn list( + &mut self, + _store: kv_store::Handle, + _mask: kv_store::ListConfigOptions, + _options: kv_store::ListConfig, + ) -> Result { + todo!() } - async fn pending_delete_wait( + async fn list_wait( &mut self, - handle: kv_store::PendingDeleteHandle, - ) -> Result<(), types::Error> { - Ok((self - .take_pending_kv_delete(handle.into())? - .task() - .recv() - .await?)?) + _handle: kv_store::ListHandle, + ) -> Result { + todo!() } } diff --git a/lib/src/component/mod.rs b/lib/src/component/mod.rs index c667056c..f8c7e62e 100644 --- a/lib/src/component/mod.rs +++ b/lib/src/component/mod.rs @@ -53,6 +53,7 @@ pub fn link_host_functions(linker: &mut component::Linker) -> anyh fastly::api::http_resp::add_to_linker(linker, |x| x.session())?; fastly::api::http_types::add_to_linker(linker, |x| x.session())?; fastly::api::log::add_to_linker(linker, |x| x.session())?; + fastly::api::object_store::add_to_linker(linker, |x| x.session())?; fastly::api::kv_store::add_to_linker(linker, |x| x.session())?; fastly::api::purge::add_to_linker(linker, |x| x.session())?; fastly::api::secret_store::add_to_linker(linker, |x| x.session())?; @@ -81,6 +82,7 @@ pub mod http_resp; pub mod http_types; pub mod kv_store; pub mod log; +pub mod object_store; pub mod purge; pub mod secret_store; pub mod types; diff --git a/lib/src/component/object_store.rs b/lib/src/component/object_store.rs new file mode 100644 index 00000000..49a423d2 --- /dev/null +++ b/lib/src/component/object_store.rs @@ -0,0 +1,140 @@ +use { + super::fastly::api::{http_types, object_store, types}, + crate::{ + body::Body, + object_store::{ObjectKey, ObjectStoreError}, + session::{ + PeekableTask, PendingKvDeleteTask, PendingKvInsertTask, PendingKvLookupTask, Session, + }, + }, +}; + +#[async_trait::async_trait] +impl object_store::Host for Session { + async fn open(&mut self, name: String) -> Result, types::Error> { + if self.object_store.store_exists(&name)? { + let handle = self.obj_store_handle(&name)?; + Ok(Some(handle.into())) + } else { + Ok(None) + } + } + + async fn lookup( + &mut self, + store: object_store::Handle, + key: String, + ) -> Result, types::Error> { + let store = self.get_obj_store_key(store.into()).unwrap(); + let key = ObjectKey::new(&key)?; + match self.obj_lookup(store, &key) { + Ok(obj) => { + let new_handle = self.insert_body(Body::from(obj)); + Ok(Some(new_handle.into())) + } + // Don't write to the invalid handle as the SDK will return Ok(None) + // if the object does not exist. We need to return `Ok(())` here to + // make sure Viceroy does not crash + Err(ObjectStoreError::MissingObject) => Ok(None), + Err(err) => Err(err.into()), + } + } + + async fn lookup_async( + &mut self, + store: object_store::Handle, + key: String, + ) -> Result { + let store = self.get_obj_store_key(store.into()).unwrap(); + let key = ObjectKey::new(key)?; + // just create a future that's already ready + let fut = futures::future::ok(self.obj_lookup(store, &key)); + let task = PendingKvLookupTask::new(PeekableTask::spawn(fut).await); + Ok(self.insert_pending_kv_lookup(task).into()) + } + + async fn pending_lookup_wait( + &mut self, + pending: object_store::PendingLookupHandle, + ) -> Result, types::Error> { + let pending_obj = self + .take_pending_kv_lookup(pending.into())? + .task() + .recv() + .await?; + // proceed with the normal match from lookup() + match pending_obj { + Ok(obj) => Ok(Some(self.insert_body(Body::from(obj)).into())), + Err(ObjectStoreError::MissingObject) => Ok(None), + Err(err) => Err(err.into()), + } + } + + async fn insert( + &mut self, + store: object_store::Handle, + key: String, + body_handle: http_types::BodyHandle, + ) -> Result<(), types::Error> { + let store = self.get_obj_store_key(store.into()).unwrap().clone(); + let key = ObjectKey::new(&key)?; + let bytes = self.take_body(body_handle.into())?.read_into_vec().await?; + self.obj_insert(store, key, bytes)?; + + Ok(()) + } + + async fn insert_async( + &mut self, + store: object_store::Handle, + key: String, + body_handle: http_types::BodyHandle, + ) -> Result { + let store = self.get_obj_store_key(store.into()).unwrap().clone(); + let key = ObjectKey::new(&key)?; + let bytes = self.take_body(body_handle.into())?.read_into_vec().await?; + let fut = futures::future::ok(self.obj_insert(store, key, bytes)); + let task = PeekableTask::spawn(fut).await; + + Ok(self + .insert_pending_kv_insert(PendingKvInsertTask::new(task)) + .into()) + } + + async fn pending_insert_wait( + &mut self, + handle: object_store::PendingInsertHandle, + ) -> Result<(), types::Error> { + Ok((self + .take_pending_kv_insert(handle.into())? + .task() + .recv() + .await?)?) + } + + async fn delete_async( + &mut self, + store: object_store::Handle, + key: String, + ) -> Result { + let store = self.get_obj_store_key(store.into()).unwrap().clone(); + let key = ObjectKey::new(&key)?; + let fut = futures::future::ok(self.obj_delete(store, key)); + let task = PeekableTask::spawn(fut).await; + + Ok(self + .insert_pending_kv_delete(PendingKvDeleteTask::new(task)) + .into()) + } + + async fn pending_delete_wait( + &mut self, + handle: object_store::PendingDeleteHandle, + ) -> Result<(), types::Error> { + Ok((self + .take_pending_kv_delete(handle.into())? + .task() + .recv() + .await?)?) + } +} diff --git a/lib/wit/deps/fastly/compute.wit b/lib/wit/deps/fastly/compute.wit index efcad638..bde3e5ec 100644 --- a/lib/wit/deps/fastly/compute.wit +++ b/lib/wit/deps/fastly/compute.wit @@ -657,9 +657,9 @@ interface erl { } /* - * Fastly KV Store + * Fastly Object Store */ -interface kv-store { +interface object-store { use types.{error}; use http-types.{body-handle}; @@ -711,6 +711,110 @@ interface kv-store { ) -> result<_, error>; } +/* + * Fastly KV Store + */ +interface kv-store { + + use types.{error}; + use http-types.{body-handle}; + + type handle = u32; + type lookup-handle = u32; + type insert-handle = u32; + type delete-handle = u32; + type list-handle = u32; + + open: func(name: string) -> result, error>; + + lookup: func( + store: handle, + key: string, + ) -> result; + + resource lookup-result { + body: func() -> body-handle; + metadata: func(max-len: u64) -> result>, error>; + generation: func() -> u32; + } + + lookup-wait: func( + handle: lookup-handle, + ) -> result, error>; + + enum insert-mode { + overwrite, + add, + append, + prepend, + } + + flags insert-config-options { + reserved, + background-fetch, + if-generation-match, + metadata, + time-to-live-sec, + } + + record insert-config { + mode: insert-mode, + if-generation-match: u32, + metadata: list, + time-to-live-sec: u32, + } + + insert: func( + store: handle, + key: string, + body-handle: body-handle, + mask: insert-config-options, + config: insert-config, + ) -> result; + + insert-wait: func( + handle: insert-handle, + ) -> result<_, error>; + + delete: func( + store: handle, + key: string, + ) -> result; + + delete-wait: func( + handle: delete-handle, + ) -> result<_, error>; + + enum list-mode { + strong, + eventual, + } + + flags list-config-options { + reserved, + cursor, + limit, + prefix, + } + + record list-config { + mode: list-mode, + cursor: list, + limit: u32, + prefix: list, + } + + %list: func( + store: handle, + mask: list-config-options, + options: list-config, + ) -> result; + + list-wait: func( + handle: list-handle, + ) -> result; +} + /* * Fastly Secret Store */ @@ -1144,6 +1248,7 @@ world compute { import http-resp; import log; import kv-store; + import object-store; import purge; import secret-store; import config-store;