From 3fa4243b4e7df46064d270291e65b70122fbe195 Mon Sep 17 00:00:00 2001 From: Ben Young Date: Fri, 27 Sep 2024 10:31:08 -0600 Subject: [PATCH] Added hostcalls for the new builder api hostcalls (#427) * bring over changes from old branch * Fix the declaration for lookup-result methods in trappable_imports * start fleshing out builder api * complete port from the old branch * more kv store hostcalls, although linkage seems to be failing * open, list, insert, more or less working, need more work on error handling * fix up errors for inserts, compiles clean * finished error reporting for deletes * everything but ttl and regression tests * all tests pass, but infinite recursion when deleting in delete * fixed list ttl deletion, all tests pass * fix for old sdk on new hostcalls * clippy * add base64 to the cargo.lock * Added in the OOB check * write out metadata length, even on failure * cleanups as per @ulyssa * added unit tests for kv store hostcalls * Implement the component-side hostcalls for KV Store (#430) * match wit and component to trevor's working branch in xqd * lookup_wait impl for component * insert and delete * list impl * re-make adapter --------- Co-authored-by: Trevor Elliott --- Cargo.lock | 1 + cli/tests/trap-test/Cargo.lock | 1 + crates/adapter/src/fastly/core.rs | 85 +- lib/Cargo.toml | 1 + lib/compute-at-edge-abi/compute-at-edge.witx | 74 ++ lib/compute-at-edge-abi/typenames.witx | 114 ++- lib/data/viceroy-component-adapter.wasm | Bin 200636 -> 200467 bytes lib/src/component/error.rs | 52 +- lib/src/component/kv_store.rs | 282 +++++-- lib/src/component/mod.rs | 6 +- lib/src/component/object_store.rs | 50 +- lib/src/component/types.rs | 6 + lib/src/config/object_store.rs | 5 + lib/src/error.rs | 10 + lib/src/linking.rs | 1 + lib/src/object_store.rs | 791 ++++++++++++++++++- lib/src/session.rs | 183 ++++- lib/src/session/async_item.rs | 53 +- lib/src/wiggle_abi.rs | 74 +- lib/src/wiggle_abi/entity.rs | 6 +- lib/src/wiggle_abi/kv_store_impl.rs | 335 ++++++++ lib/src/wiggle_abi/obj_store_impl.rs | 37 +- lib/wit/deps/fastly/compute.wit | 28 +- 23 files changed, 1999 insertions(+), 196 deletions(-) create mode 100644 lib/src/wiggle_abi/kv_store_impl.rs diff --git a/Cargo.lock b/Cargo.lock index 4649f71a..1b5d5663 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2431,6 +2431,7 @@ version = "0.12.1" dependencies = [ "anyhow", "async-trait", + "base64", "bytes", "bytesize", "cfg-if", diff --git a/cli/tests/trap-test/Cargo.lock b/cli/tests/trap-test/Cargo.lock index c57429c8..d1ec759f 100644 --- a/cli/tests/trap-test/Cargo.lock +++ b/cli/tests/trap-test/Cargo.lock @@ -2344,6 +2344,7 @@ version = "0.12.1" dependencies = [ "anyhow", "async-trait", + "base64", "bytes", "bytesize", "cfg-if", diff --git a/crates/adapter/src/fastly/core.rs b/crates/adapter/src/fastly/core.rs index 5f6bc58e..ffbd1114 100644 --- a/crates/adapter/src/fastly/core.rs +++ b/crates/adapter/src/fastly/core.rs @@ -2677,6 +2677,21 @@ pub mod fastly_kv_store { PreconditionFailed, PayloadTooLarge, InternalError, + TooManyRequests, + } + + impl From for KvError { + fn from(value: kv_store::KvStatus) -> Self { + match value { + kv_store::KvStatus::Ok => Self::Ok, + kv_store::KvStatus::BadRequest => Self::BadRequest, + kv_store::KvStatus::NotFound => Self::NotFound, + kv_store::KvStatus::PreconditionFailed => Self::PreconditionFailed, + kv_store::KvStatus::PayloadTooLarge => Self::PayloadTooLarge, + kv_store::KvStatus::InternalError => Self::InternalError, + kv_store::KvStatus::TooManyRequests => Self::TooManyRequests, + } + } } #[export_name = "fastly_kv_store#open"] @@ -2735,22 +2750,25 @@ pub mod fastly_kv_store { pending_handle: PendingObjectStoreLookupHandle, body_handle_out: *mut BodyHandle, metadata_out: *mut u8, - metadata_len: *mut usize, + metadata_len: usize, + nwritten_out: *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) => { + Ok((res, status)) => { unsafe { - *kv_error_out = KvError::NotFound; + *kv_error_out = status.into(); } - return FastlyStatus::OK; + let Some(res) = res else { + return FastlyStatus::OK; + }; + + res } Err(e) => { unsafe { - // TODO: the wit interface doesn't return any KvError values *kv_error_out = KvError::Uninitialized; } @@ -2758,27 +2776,27 @@ pub mod fastly_kv_store { } }; - let max_len = unsafe { *metadata_len }; - with_buffer!( metadata_out, - max_len, - { res.metadata(u64::try_from(max_len).trapping_unwrap()) }, + metadata_len, + { res.metadata(u64::try_from(metadata_len).trapping_unwrap()) }, |res| { - let buf = handle_buffer_len!(res, metadata_len); + let buf = handle_buffer_len!(res, nwritten_out); unsafe { - *metadata_len = buf.as_ref().map(Vec::len).unwrap_or(0); + *nwritten_out = buf.as_ref().map(Vec::len).unwrap_or(0); } std::mem::forget(buf); } ); + let body = res.body(); + let generation = res.generation(); + unsafe { - *body_handle_out = res.body(); - *generation_out = res.generation(); - *kv_error_out = KvError::Ok; + *body_handle_out = body; + *generation_out = generation; } FastlyStatus::OK @@ -2839,18 +2857,16 @@ pub mod fastly_kv_store { kv_error_out: *mut KvError, ) -> FastlyStatus { match kv_store::insert_wait(pending_body_handle) { - Ok(_) => { + Ok(status) => { unsafe { - *kv_error_out = KvError::Ok; + *kv_error_out = status.into(); } 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; } @@ -2890,9 +2906,9 @@ pub mod fastly_kv_store { kv_error_out: *mut KvError, ) -> FastlyStatus { match kv_store::delete_wait(pending_body_handle) { - Ok(_) => { + Ok(status) => { unsafe { - *kv_error_out = KvError::Ok; + *kv_error_out = status.into(); } FastlyStatus::OK @@ -2900,7 +2916,6 @@ pub mod fastly_kv_store { Err(e) => { unsafe { - // TODO: the wit interface doesn't return any KvError values *kv_error_out = KvError::Uninitialized; } @@ -2916,19 +2931,27 @@ pub mod fastly_kv_store { list_config: *const ListConfig, pending_body_handle_out: *mut PendingObjectStoreListHandle, ) -> FastlyStatus { - let mask = list_config_mask.into(); + let mask = kv_store::ListConfigOptions::from(list_config_mask); let config = unsafe { kv_store::ListConfig { mode: (*list_config).mode.into(), - cursor: { + cursor: if mask.contains(kv_store::ListConfigOptions::CURSOR) { let len = usize::try_from((*list_config).cursor_len).trapping_unwrap(); Vec::from_raw_parts((*list_config).cursor as *mut _, len, len) + } else { + Vec::new() + }, + limit: if mask.contains(kv_store::ListConfigOptions::LIMIT) { + (*list_config).limit + } else { + 0 }, - limit: (*list_config).limit, - prefix: { + prefix: if mask.contains(kv_store::ListConfigOptions::PREFIX) { let len = usize::try_from((*list_config).prefix_len).trapping_unwrap(); - Vec::from_raw_parts((*list_config).cursor as *mut _, len, len) + Vec::from_raw_parts((*list_config).prefix as *mut _, len, len) + } else { + Vec::new() }, } }; @@ -2957,10 +2980,10 @@ pub mod fastly_kv_store { kv_error_out: *mut KvError, ) -> FastlyStatus { match kv_store::list_wait(pending_body_handle) { - Ok(res) => { + Ok((res, status)) => { unsafe { - *kv_error_out = KvError::Ok; - *body_handle_out = res; + *kv_error_out = status.into(); + *body_handle_out = res.unwrap_or(INVALID_HANDLE); } FastlyStatus::OK @@ -2968,8 +2991,8 @@ pub mod fastly_kv_store { Err(e) => { unsafe { - // TODO: the wit interface doesn't return any KvError values *kv_error_out = KvError::Uninitialized; + *body_handle_out = INVALID_HANDLE; } e.into() diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 670141de..1361b6c1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -64,6 +64,7 @@ wasmtime-wasi = { workspace = true } wasmtime-wasi-nn = { workspace = true } wat = { workspace = true } wiggle = { workspace = true } +base64 = { workspace = true } [dev-dependencies] tempfile = "3.6.0" diff --git a/lib/compute-at-edge-abi/compute-at-edge.witx b/lib/compute-at-edge-abi/compute-at-edge.witx index 231a402b..85d7e1b3 100644 --- a/lib/compute-at-edge-abi/compute-at-edge.witx +++ b/lib/compute-at-edge-abi/compute-at-edge.witx @@ -761,6 +761,7 @@ ) ) +;; NOTE: These are deprecated, use the fastly_kv_store hostcalls (module $fastly_object_store (@interface func (export "open") (param $name string) @@ -820,6 +821,79 @@ ) ) +(module $fastly_kv_store + (@interface func (export "open") + (param $name string) + (result $err (expected $kv_store_handle (error $fastly_status))) + ) + + (@interface func (export "lookup") + (param $store $kv_store_handle) + (param $key string) + (param $lookup_config_mask $kv_lookup_config_options) + (param $lookup_configuration (@witx pointer $kv_lookup_config)) + (param $handle_out (@witx pointer $kv_store_lookup_handle)) + (result $err (expected (error $fastly_status))) + ) + + (@interface func (export "lookup_wait") + (param $handle $kv_store_lookup_handle) + (param $body_handle_out (@witx pointer $body_handle)) + (param $metadata_buf (@witx pointer (@witx char8))) + (param $metadata_buf_len (@witx usize)) + (param $nwritten_out (@witx pointer (@witx usize))) + (param $generation_out (@witx pointer u32)) + (param $kv_error_out (@witx pointer $kv_error)) + (result $err (expected (error $fastly_status))) + ) + + (@interface func (export "insert") + (param $store $kv_store_handle) + (param $key string) + (param $body_handle $body_handle) + (param $insert_config_mask $kv_insert_config_options) + (param $insert_configuration (@witx pointer $kv_insert_config)) + (param $handle_out (@witx pointer $kv_store_insert_handle)) + (result $err (expected (error $fastly_status))) + ) + + (@interface func (export "insert_wait") + (param $handle $kv_store_insert_handle) + (param $kv_error_out (@witx pointer $kv_error)) + (result $err (expected (error $fastly_status))) + ) + + (@interface func (export "delete") + (param $store $kv_store_handle) + (param $key string) + (param $delete_config_mask $kv_delete_config_options) + (param $delete_configuration (@witx pointer $kv_delete_config)) + (param $handle_out (@witx pointer $kv_store_delete_handle)) + (result $err (expected (error $fastly_status))) + ) + + (@interface func (export "delete_wait") + (param $handle $kv_store_delete_handle) + (param $kv_error_out (@witx pointer $kv_error)) + (result $err (expected (error $fastly_status))) + ) + + (@interface func (export "list") + (param $store $kv_store_handle) + (param $list_config_mask $kv_list_config_options) + (param $list_configuration (@witx pointer $kv_list_config)) + (param $handle_out (@witx pointer $kv_store_list_handle)) + (result $err (expected (error $fastly_status))) + ) + + (@interface func (export "list_wait") + (param $handle $kv_store_list_handle) + (param $body_handle_out (@witx pointer $body_handle)) + (param $kv_error_out (@witx pointer $kv_error)) + (result $err (expected (error $fastly_status))) + ) +) + (module $fastly_secret_store (@interface func (export "open") (param $name string) diff --git a/lib/compute-at-edge-abi/typenames.witx b/lib/compute-at-edge-abi/typenames.witx index 7d192415..a8e18fba 100644 --- a/lib/compute-at-edge-abi/typenames.witx +++ b/lib/compute-at-edge-abi/typenames.witx @@ -91,14 +91,26 @@ (typename $endpoint_handle (handle)) ;;; A handle to an Edge Dictionary. (typename $dictionary_handle (handle)) -;;; A handle to an Object Store. +;;; (DEPRECATED) A handle to an Object Store. (typename $object_store_handle (handle)) -;;; A handle to a pending KV lookup request. +;;; (DEPRECATED) A handle to a pending KV lookup request. (typename $pending_kv_lookup_handle (handle)) -;;; A handle to a pending KV insert request. +;;; (DEPRECATED) A handle to a pending KV insert request. (typename $pending_kv_insert_handle (handle)) -;;; A handle to a pending KV delete request. +;;; (DEPRECATED) A handle to a pending KV delete request. (typename $pending_kv_delete_handle (handle)) +;;; (DEPRECATED) A handle to a pending KV list. +(typename $pending_kv_list_handle (handle)) +;;; A handle to an KV Store. +(typename $kv_store_handle (handle)) +;;; A handle to a KV Store lookup. +(typename $kv_store_lookup_handle (handle)) +;;; A handle to a KV Store insert. +(typename $kv_store_insert_handle (handle)) +;;; A handle to a KV Store delete. +(typename $kv_store_delete_handle (handle)) +;;; A handle to a KV Store list. +(typename $kv_store_list_handle (handle)) ;;; A handle to a Secret Store. (typename $secret_store_handle (handle)) ;;; A handle to an individual secret. @@ -385,3 +397,97 @@ (field $workspace_len u32) ) ) + +(typename $kv_lookup_config_options + (flags (@witx repr u32) + $reserved + )) + +(typename $kv_lookup_config + (record + (field $reserved u32) + )) + +(typename $kv_delete_config_options + (flags (@witx repr u32) + $reserved + )) + +(typename $kv_delete_config + (record + (field $reserved u32) + )) + +(typename $kv_insert_config_options + (flags (@witx repr u32) + $reserved + $background_fetch + $if_generation_match + $metadata + $time_to_live_sec + )) + +(typename $kv_insert_mode + (enum (@witx tag u32) + $overwrite + $add + $append + $prepend)) + +(typename $kv_insert_config + (record + (field $mode $kv_insert_mode) + (field $if_generation_match u32) + (field $metadata (@witx pointer (@witx char8))) + (field $metadata_len u32) + (field $time_to_live_sec u32) + )) + +(typename $kv_list_config_options + (flags (@witx repr u32) + $reserved + $cursor + $limit + $prefix + )) + +(typename $kv_list_mode + (enum (@witx tag u32) + $strong + $eventual)) + +(typename $kv_list_config + (record + (field $mode $kv_list_mode) + (field $cursor (@witx pointer (@witx char8))) + (field $cursor_len u32) + (field $limit u32) + (field $prefix (@witx pointer (@witx char8))) + (field $prefix_len u32) + )) + +(typename $kv_error + (enum (@witx tag u32) + ;;; The $kv_error has not been set. + $uninitialized + ;;; There was no error. + $ok + ;;; KV store cannot or will not process the request due to something that is perceived to be a client error + ;;; This will map to the api's 400 codes + $bad_request + ;;; KV store cannot find the requested resource + ;;; This will map to the api's 404 codes + $not_found + ;;; KV store cannot fulfill the request, as definied by the client's prerequisites (ie. if-generation-match) + ;;; This will map to the api's 412 codes + $precondition_failed + ;;; The size limit for a KV store key was exceeded. + ;;; This will map to the api's 413 codes + $payload_too_large + ;;; The system encountered an unexpected internal error. + ;;; This will map to all remaining http error codes + $internal_error + ;;; Too many requests have been made to the KV store. + ;;; This will map to the api's 429 codes + $too_many_requests + )) diff --git a/lib/data/viceroy-component-adapter.wasm b/lib/data/viceroy-component-adapter.wasm index d0c37ee7546d0311c836399a2ec099ccea69dbf4..23c5936c1cc1c90c270f58d545e32248d084ae94 100755 GIT binary patch delta 34936 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 Q)X}0oS`yiXMZ(zs2aS@Epa1{> delta 35048 zcmdtL2b>kv*#~^jnY(=#E_*L!3vhSoDlok$C`&SGq9T?=jVR2WLT71Wi3$r?DFUM$ zJH_5X3`A^TO)QkyBPzC-C@9!9QNRD0x!Z`Oyx;f!elOpeaPQ1HbINm`{ycM^@W%3o zSCp?=L^^et-R_Uf4i}R8b(_d_>oRd);Ts-7~mYnu*k$-PdcWy1C!Xv^%VY;W%m*TG``XB$+<_|aVm;X1M z)N{O*NF@^0g@x6rWU@oDLx)sRQK~~KQQfz`y1Kfa|HJD49qWrBW502ON~_ zP!s;GXZPNNYiMCTt?%1ctf?-nDePNQQxiYIKWKe@J?$%oM`UYV34JhJAv)F5LR#Io zZ||DEHHCOckZbsYFzKKH2jee^$1q-fHLb>I_`@fbGhk4%!@OHG1bxaVC#<6E_IqTC+;HQOJgS&2+fa>_AuWTbbgijnii)RtkNh`#r5RT_rG4 z5C#Z|)j~MN8c1&v!Yiz<^k#vr5`a@gdLbo=EGu?Mw zAWg3s{`6UXVxy4GuiJIpael8n&OH>K*}IGIu#gEi^~od?j|e^Sr88UkE-j^rN5k#C zYYKCX-nog#@HH!JYHd!26@7|?%|fr{{>f16Qzkqv$SpV6i1%n!GVz4aqvck~jG4|% zKk=l{H@@{A?L-q#g^%{>L7ryY-lKIi@yxQm{mCC#_4~9BP5dz&*|%PJRu~+<-KQ#C z+_x)v4tHwUeeVNX63_GR#jZ2O^(Q4>2zT|Z?z%;&Y`0B!QsWFiu{G@8uZnDA*$-%z zCbqMN4`|o!i9ZQ>LK+=|1k^Knl0Rka%yEeq!$tjikeArf59lE@@iKe#16oBBukZ)c zHRHz5oRN4{NaZIw%bD71sxuwaye5P_)hhbBz()T-JJ%-uEOgHI88>t0)X^M}y>KdC z;thc<{(&!HhtTDpdc;e9Q(&8ZpxI>NEuo%m{()A7JJeF)?fgZ>EN9YL{%B6`JB40R zdwgp~bnhJ@)U=-ByDb}>)Ht>maCK_q_$f1^9`Cg*pEsR7eLPO(eRjyl9Oxf}Zynr+ z{DmF9i}s_555s|n98CVomhPel)5PESXLt7JU9>|o@sS|1IlF01xb2WG!pFiP%@;*% z*lt>yOzdiHFJg_mX$P9v%`V(cRhsxDY#Mlw@TqVxeoPCfqE7ismM};!EekS#n}t zvKn8jg~a@1xJm3wFG_}g6?+L6Cl6+8_R`w$ASoqWk{r~0(XnVW9=bHym*40sBrZ#` z`}Wd~U~!dTo+*_#5plB(6)ck$=O1gfFiet-HjgPj&O)k zCES^;2@f~Agj0+{au=IJgl;r(cX$Qb?+NcUx{`a@79!NtChki%lV-HK(c>GzNhkU4 z%*2}T6Q!o}+GNN4q)u*puklmH&rIB(43F&JTX-VbBV60RTWKB@pNua{#!mq!o|$+m z$=3dbR+U32_%XFU9o?xH63-;V?0{?`$H_nNuf2so#?OYM2Ncm~lWf?Bw6l(8R5#z;VUHH&JJqyN9O}vy0Hy+f7yc{l6s>mzs-p^^*Lldti zJLT5`4X0OL+Ps!59X;A}W;&xmqk>R2noCYFMrVdEBuigjU-XdhRCvVU4Iw-95V9bA z^3c_#^XAQ&lW164o|+#{J*+dC7ali6!M{yI()f4FVWaaMmiqUy z%lR0pK2ecKT{k!Smij7OKjgShm-8vILY6ipCp0X~r2ZZ{r(fD5p7^gAH)^I0rH3Yx zaUYVJ7j7InI^Sj2{<;{4k0lGk6^Acvy9qLkZ(`&TO6O>TEaBtU;G&`f~=l}K`ZC1Hf;6UX|UspPE(*OFzmz;D6 z`Fh<0C-wMWpQ-gAnv%_149A=@l0YRmGS{Wfn&Y#WKp=@YscSD@HyQN3YuXY$&DJwjXZz9&u_;C{$2N3!9enLcysU$ zeRqC%$=K)qm!aG={=9=9;9@dcfd6O%ompR$if+XJ))!}sC)Af@i;vHi;Lf_f6IK%P z-MVKcmXrL-<`%aHwu$pM5LI=rD&TawLX4Z>kJX)wt4(NIl0^aCy(zus~2VS!h%_+=h0_R^C%={AEjJD ziqU7ztTXd_h;*iW3rC+p7j$7;E+j=^>g-eV;QG3CoJb2G&04_4XAcHJtv~z2wv6## z$>RT^ux=wr!_pRM-c}#Ls>21pZPp@hMyH6Y?W>3I&8_%=l!X1x|8;(I>zex(#wt!> z^X7Qp`_`fF#+JUDoBI~TeG6Lqo|Ol|ANcy#zQP6lX%ChXx_*AR`J!%g-TbibjtSlO zVe9#GN#I;KPj^;|m2CtZzs*mi?&eYX2V2Y3p2GiL;7u^$fdS(fI7y=5v%DidzG_YS%N>gc+2I6=I#KoP1HV z_ybM`htNmEU7MoCA9&2?gxB6ZfCbp514n@Ug!3WdIXe#wp0j`X`&>BfSvy?4-K3w- z56RslIXWG9-jrwVT|@se|G*Gm`#+2KxJ4P>_m_h3@k=9Z=z!y<>t27jAaW(QEcYP23aq0E5+WNUyzRZJDJ#OG}xX9rf z{_t|UaMV9_hz-+qw;!PT2$aEa;n&eo9-yjVS*Bo3UQWXOX{XWv&D{@65*^N<4$^x>bcJ}|=pG4Vcn;1KUm zS(U&Z`a6W!FN{2K2ORRSib|60@HNjSYM@6|BpQU&H|(%VQboRDzp5lR=QBRmHs^mO z;>c22Ho;RA5L{waY0{iAl!&tzq9z2GHXwKX&i4WbuaCvq*Qs4jo0ZbnhHCXe(YjNo^s$*@Neh*I4;h z-2Obiz2WlnNG)L-N02P}lI^V`o!O_qCXcggFDC=~e>Df2!b1Dga=RkIKU4LIhS^8* z@2Rh&#nr ziNi=aK8Q|J_$Z%{%J=vt>cLaQ@;zvq9<1lBBp`oh7vD;zlDX{bTgh=uK$CTa*<@-iJN-8DbsiIzla>^iC;_QFD`3V56QsaYdWdNLbCzs#%4|@ zFEH^Bq&NM?9QN>AGUE0Lf{t{bmfA_Nd-BS+&R$d-I7XfVn2imLbm=vui3)$PH^cnW! zi=+ox#Cp6$zNL!+hAAYoVc^R|AmoY&!t9W(ctdQ@s{rXz*6B4YWEmguuO!^yPSbQwx7&d4v8Q6Upe;=TZq%Pv*1Dwv2 z9gp%Sj$+wmS8jOUv$3~ z7v>dhF0N^N@m>Dn;3dT2-4`zb!7SMD;}TL9%~8G#2!Z?OFN??ico{hp*Bg!r;|?26 zy^?f^dQQCxutLv?%gJ&4bLag)fi264+4iEAnT*u7+?l}QzI;~t_a=7m)j&HuiycHe ziWYy+HEkBZpzX!3){D`Zb)3BGz!Td5k8OO1oJSv@&-8am6a7aMd+=SNfW_^8m#BlT zx3^H< zHrz{w)2qR;?j;Ru-o04X)U{*;XkpKNkbz$`vAy>ZhlKIvp8RD~eEC^)nPvmml0oD< zHfk-&fwK66?9=;*%YL^8!#(gGsb=f$Cj*KQ2rTB{282%LQ{QtvG1-)LG14qtM=mP+ zG%sNa8bo&Idyt&N9v}nQrWK@s_s4S?*7N|GO+IJjcVs&aZQm@QjR zM)I!Gqv-n0dYst&xoqbnL}8b0i06q~E?9vzy}V(cu}LGFvj_Z8`#sU?3gg9c#1qQ0 zg{@E)%5JBW%of;XzbDsLW{VKrtSh0zfNc1hs4r!M9)uQ9%8q@I4DV2a{pN=#!k&tE zf!P1>Am-h+h!m8zv=y+zU1ShDcw@eWZ{gI9WCZ_AJ&CP6ypbH*a&hlNB+CwYsLg)P ze5if%kY*UHf0#^+NO1=k?CUP_k6}EH}sjK5-<+}qz3mUS`y-C^Qlu}H)Z4>z=?_Rr^c$mI8ixcUVhcv#X z0O`e>BS_!9nGEZLwDH#Q3foLi%5J4e;LL?|Hf5b2Cs$TNL%^98M;pP{M12Wc^EgH- zVOt(2!>gLd0l!H$h|TLtl%2tvmP3*^JV6d>i^>rvtYs^nh|eQJ((O->q50!_3-ajG zCqV}%-UOy{`#d)HrkJMJ+ywr4M=PT!ycySbf{*OM>5jRXl=A_m@SAt-bMqB`^X}Fs z54i;z)IIaqh-G;4xQ(PJ!mh5Z*wq$!&tW6KB;D!ah`FTMu#dpKFj2%|F6JEOiipEh zvCSWm8s75OM?|GdVz$GthkuMGmd;~jFX_q_e2lSh^UjZn-eXy$2!UQe3lUrkhIcpt zS}U^saWw1)^r7Lt%j8dcyD;RH?O14rJ-rKyxvF*EF1yib`97V_*-czNLbUw7yJL!J z_=IG@fsfrmy3H=k3OY?@3$sum6A>9hAO1TR57F^Qh*UJZ!-*C7>;AMGUvUGTN^l`j zm@Vv2Yjq0r%L>phbz08v;blIZ)<)lo@vU3*tsLJn{2SL+Imbv{=0<`jOZX_osa;&J z#j?=K_IUM7ng9N^1>*IsuyXjB+V;pRj;t+dgi#R^^&qFN|nk_J*Ws^BY zR12x5h$K1>iO+)Y<1UT|i6It)Dw)MkC$wP^LMw44%FbnveN1`?sR!q?Sx=Lp)tI%p z>%O9T?`lxaMqutsgO&(8kLqKuNx#iAfWn@xS;ZIf&dCtJL0n(uc0d>*Oou6H4gH zc2}#~U9FC<7<#_Rx=$hX^rq+z3?e9Bh#4{N_OOWu&38 zNJE3hUwtFFgN?nT*LEd9q-;2%Wi1EL%g7mB2 z%|$+VApebx`u04qb|skDC()^QW&Lh~*$#3$epzjGyK{!A(}GPK1I^;?`J`xeMFMOfSx{J1TvA%r zv1~Z0OO{uZ4X5}hFB>l4qpWOr608W&>INGcR|U9PQZ~F0AH`+Ei||oYHXPilys&I| z2|fzShL_?aSvI^3A41via(sZkC@0xeOtOUEhnFE;1}PrNA5K?b=mMM1&DfDPc;bIySULCc0$)D^OZ zKa=Yuz5Q-ZJ=1kQAA}}{6 zWRr#f_Q6+!eX*W%K@+z|7HWnK8V|}DJQ0+0Tb^>bi-O!95pmIjRN#6OTe=#EAvThd z(mNvH;E+?xy8$-hFj8L2VFRlbyQ6v7Ky~4;fplR_Q%Slf2M&pVBSgT776yTr1aL@0 zF-yHk`W8mVPuU57C5Kf(0L0sXkE#wL4eOqerPu~rdnwWAo%yv!B!5?Y(R_S&|Ns2> z9=HtJ<2_C6-OI@N_%(^qmy^TTk6THP4P(JAD>uBFravQWT$(mkcHoCjvNZKAXGdj; z)l=Cao#|Nm&Xf&Tb*3*2gt<4nH|@*1A5AOSbw8sc*y%r~!%0VWiJkC8T|}K zq8ENcivMH>Fna>`^I(>O=!ZNCgo*ySGZKB0iTtl*!Ob?l1DV(l{rX2E6pnMIc$561 z!~njJ=$Etx@*g*W%$h(-c$b2L8MFDXXT@Wc@t@%t7(XN=Cp$AHRu;xP@0>Uo)5Jg1 zK@K4k5^cvXNesmJabrCGAdFx3&r2PQG2$O7AtE*yFYk1}EP6g}h@Y46dt|V!eWEVBYD=OPbJF(whgk=pofmWpzD<^7-CRw^@n2s*m#pkrFya!u* z098z>;%-hLkg9-!6wwjYIkRljzV+eqb4rs#vb4xuRrOOo}J$ zbTU=bs;elvZmC)zs*a+%c8C1=%sLt8V_Ak0$dVXHo}x;o%T}FCowN}+vKd&KC*sJo zoMNY@^AU%z6MjYe;Aniy(H+O~L`!mWf!%5Oujwz?#p`HCw)0o`AvM*I91NlAx`zL9 z8vEf_bX?lEL`Bj~SHqN+C!2Qq0=E5h40_)wG|ejTqfP9UU(=42vaiaTE{UG%>49j< zc7|;W=;7>-576#C#Ix~ZLgOd%-!YSnalYY6o}1GgUr}YtGTGEqXk*IA1)gMTn(Rxm z;=3;U^b|TRWdxq3`Z^Zi=$hhbZ0e~rNShL7G9)a`lYPZ>?QZ9=6V}ty_L)eROjFfO zO>uMRD@kn3Y4og&DO!pg7&%R|076Hzt1luPPKUAHzoAyz)h#2?UDcCi14k#?H8Wx; z-u)Ze7vQiYQM3Zl#i^N+XxBy4_bI$C@vDhA!(K24#as(a|Z%@ihq`(LKk|RXN9Q9EI~xR5cJq zMe{AyL08SL%OCQo8xeAtbO!BEDY}yD$p%islyXvF+VxBtMQyhI4BD;Iw3NWhxw3;+ zJpd{6;+U+kISOo+>GZq#F*FDcQYSdUR(CucsZ-rXT3Os)(j&x~}1Q zcAp5X4`XW{rX?&pns!dhrfj(Yujk@OP2IHnw(YVIT~bm`$+@C#%91G=VoqdhN7GSh zS<1O?pcyI#_ne$#_a7S{PU9mO+Z#hG)A;EqQFbgmt2jX}r`ZEqyHv2R#silErw ze3X_lnRfx18is1Al56F1uH)MSTf1~%jXA*H1M-3qD5B%)4v@|s#H1Gh-jxAXf5c<7 za32(PWze~r(_~Bb@l3!z&Cy?_eVHSuk`q-^1~)NmF~7q-kAbDFaB&7Q=dzk4i@v6- zF7QB_7NhC$n`oDPCh>BT?wXEc`if!(IiHCh%~cwnuYrMp>J&xPMBi38uZ}RGlr?&o z=o622NlR8BgJt_VIG^izuC3-Ve*R|KnT_>1J{aJ9K@gZ=f;m-FY%PzIA8!UuzJV^4 zx|9=hfhwt@=xTwi+IpMG%mB0#0L9(F@IkARYI=%oFzM_Vay?RkW@(0=%lTSPG96E0 zTLL<#Q{q$TQiROm68&;AVgV|EXTlyJrqp! zaORJv{VQER5FN<{v1o>`S&Dr)+j{|3*`_Cf3M-$Y)$Et602yDFbVV{nAighJnrt7@ zrd#>b=r(x*x+y+nlkfS8fe9_ivxjlq9m<}aK)a+2*>}LqT+foNoFsc}&jflxN)f?2 zKtzG0NWQOo?1YK*#7-9eR1@dmfH)l4{uxJ5i|w8W=HPp}0*HF5CUbso|9l*4nuNxY zlOULV-Hy7H8II#iJ&bLAftIt8 zQ)!nq6fER)LR$ zL!Q`Xj3v_nS>GxC;7(CJ5HrX?=F}_MzsY0uV`tGGX-hP64rCRyK4=q=ho_$%@8q6s_}$5) zXJLv!25x~$Jt!-(YMAz@HdEAY$Kl`0yIBy4iV2P+0ysIG?-?x%PO)1jbEyR7%z}QF z(@X_e?X$sW)05I%#WqFXRV^s7vgg>N1J221mi-eL?2fYmsT?$M1u_>h!Ug~H?J=!g zD%g;7fE2zgDme%X&GH=s;?H*2=9ht-D_;bpZaxQ;W`OhSP$-sTj)n%CyEZPC@HRJyDbBivJ227?Rz+M4~*Ist(>fiwimH`nQc3l4o+eB zKrK~==mHjc66< h*jX(*gGsT}J~uQ0ySmz@ukM*tVCT6OTI&AaW#C)1eF~Ds*t! z)tCPk5EAvkP+dp>$AH;ksP?!>1Vs=kVc)+D`Fz)Jq2{}i;z6Yj982{4z}M{Y`}FI~ z`kjw0nx3hG(jBOkiVqU8C&WT}*ad*Zyz^^Y05E-e0lM zgF%3!*I~F!iyj%uX3nP7l|U`s%4tR*J5VZI-JafNBRgh8zKh^h1}rFO${<<*W5zhP z_BBj7Vh%v1Kv<}OZ$McF_mmBLW*bo5IfrIaVB0`jkg4T4;C(XNGKZd09e5hlG!w|^ zXyDF)J?nll`ZpsaRc3QO24Nj>5-nZM|9>$ZTM2CklrH8R-!lwFQtY$Yc^A{0!RU0D zeNg=M0G0w&k8_xG2|cNjle-B9=}UoTg4x>V@M==N`Ygvub9SAGFno_ZP0q%~E9vv7=*?UwQ3E}&FJzt0;|%F5+KoMOIT#WK zc3@DNvgksL0!?SP4qm|ySpW_SBiS?qsIL~Jr(v1)oaL9(pR>chq7~VFxU-mpp@3l& z$IxWMv76Ym%jvN!wUD2_Z>mtKG#6kJRmrmFM#IcmNNXxh)c{7Q9!ww~7+~0lo$v?0 z#e*3ihN6z~pmI1+%M5!y`3+}E{Wy&4Ypk3hyI^3R4hI3)vwacS7$2|Edc5qLYv{R^ z00E?gC~J=58aXhFi`k~7fOYm7@FmIs4hfnau-6CN;CAqJ`x268OxMsPyD=$rV;3?2 zTvwrE8aYT{SSY^Y+LtmC#v>)!D~t~41Zid(Q1D%t)w1H)m$B5<)X9JisV-+(Ce&Cw zZ(mLVw)AbB$@2w5hCP2Z?U3J=|4U)l?0V37`Z0 z%l1O%TuKM9>{2l8NlUom43C5f3kl+33I16dQK~ zzz%PL1xqyWbKnJ_Z#(2*A5&>S=ami9vlQ5Pj;h#K^7}`#he9Zlo+E>HtN>~z=jN7u z72C5IXVtp7MgIVtjyV(J1egYO(tvoMtng@4+bZB$y!(V4wu# z@Wh9=VmT&qRrp+>k*T5rcar4WSF@pu>8cu6l;MQnY{1eK7|ixHWJ5a)?Rh^;lgAg) z@s*+kMywc`<-iaH4%*ko7`mki++_C-TFidigk?%O90MEzs;R4@57vAgVef3hp0+m8 zEbJRdESMTD&I>BIV_(lV?4-f}ASdC%0h}Osrxl0GdORelfvfADePbT$ zFS~{wl);WPmm{lzCI#?YLB{f3>uk+ikaivB(JmPn=NekUBvlP+oMo?!Q8wz+iCs7k zXpNP4@Wp}Ez{)YSoV}_|pK|ujJetmM$srrM;%Ye=DvoBaZquiX9X+3BGKz=|XwZ}l z;3_Or`=Bk43WmD#8z^iyT>*3zUDW_#`e2<} z;K`DGOAP6^zk#=|!x8`)*eJtY15F#cil}+^t#HlErN?D_13I||CTGG0^-5= z%Hfa#&AywjNN3+eJY_7{A&xJJs^NMH3<~=m5;>Xrafjl-eK@=WDG^u@1{xf)vMk&8 zvWAQ3w2tr}1qwvF2WJxSpEX?z(J0D#PSjNij!F)8*GiL9@}J*@EK~&#OMX1D5?T$M*<|Np|8& zzIZsXEKdh+JD%(+hW!Yc#U~lUMwJUSk%J%wH30?{JdWTz03nR~m2_OC4mqeRIT-wA z00)F?Kh}JNvv#fUdx*dhmHYM$u!hguwJMsd>rhg3P-NiB_80A1 zVI3>n^n(>E!SJxZY}abZpkT+tz$*NGvi()NRxAmVil!l|IjGn6*UhbcZlkHnTrLL} zDL|&{Ku-Icmd4v@r%upKwHzWG(DI=@ecRmT<9DztWt=t)9R;c-1g!mCbK@`xlk_*1L{pvbEAI;whJ!@d>FtpR}Kbwn9Tt*=2H3=~td=QX!}a3=?q z0+Eg|2<#Jx1jnA=z7=bNRReZo$T>;1FKXASB2*&l8n}%KnF6J?U26`bB81|ZTs?MO z`;z9?K6l}4(Fm?@fQ72yznA9Q_5rd5160;9fujM7ApS4gPd^QU4gOFUHaVE1W?#Oa zem+cp9exb3SW|#CR)8P&Q>s#8c6{1PSx_rYjd=+8BW$jwMK=lkTB{;Rg*i}1hX}t&YLif}~$&7F( z=jry9?OIJsf~LnE0G^M)gMC$V>j(E@LzZc%T>4>IE=Y5Eb7P-1yipW$kmc}nDzFx8 z*4*f?;V_0-=OSLEf|Yqt$F6Q}y@fvtH!u7Wn#?(^sM^;wH%5;_ut9%R0{Hwia6@lAMBL54R_?m-^H-aZ)yr!fuzDFy0Tpa3f1@&Vg^KRqo2hZel<@NB`}ft9P; zD`*tRIhqF!J7o~*gYt~%8eHRGlbXFUZa$?8!hBHmV3TN$3MUA-x4nu^+b49`1?yI! zaH;UnDsUJ@4!qTrn}0*trr#sBc<64L>dIq=o(s1ptSAj$E)QpVQ=7r+F`Hx}TEeLg zQUr2Kvu~y|;@JkBN_(S&3M@vL#e%`ABdQAJlSa`SL^4mMRR93=4^J};9WLA;7nmkH zZap2H;aa6D1u7^S@fgdsZ=eJVrLkuz3M3v;EEJ}5P;KF~Ghwe z*-?Y|ly&%QxT_P95b+T@B87M@qPjkOg1&?h2DJZ2=r7^hf?n$C2Gn;EM6cQpvL%nu z;ngU00*r&Ig8(q3KBTaVAd0<_j*8Ef2L!vI2n#xS2r%IYP570eydtu;>QQ=p3U(d@ z5&|Et0qrVRIpyQjY<0g743RvNgxOj%z3kRzU&2Hx=dJH??qs73T9DMHJB%03cAsDLc ztxfclwC2OiM-UzHmV$N1r9SC}+Ive9GZW|zWiU5)YYb<9! zou}~fv9u@Jb;PKkLQ8O*BWw-T@R?RTiQ;&bm;=6XF@S;xEsm3;X8(a5zaC5Y+Y_`$ z1`HB0WB8=G+=g4){v&N{iR9I1Kzpi-*rW)zJO`cqEREu+L)fR|Xg5wBjtGOmmEhoj z$g*VnxtNU&<&nH<4pXSP+}J^Y3kYGjY|K-1RK~?-0A@sEJor8lrhS2q=O{9SA#T?r zrJDiNI2Z1I1j$W%OLX2nkRmh{Rf2##U}SJ+!$^enqVpV&t@NZg_ORt?TAD!++L3+N zG2qRCXUeg+H8(Gu7#%d?6$s730qw%dhpNdAe}*2Fff#_ws{zzd5W|i|xQYhgdjr9^eI3pvu~B@Kqko5YsEd+!BH% zzJj2Ks|F4aneG5_M9(9xR|D+8CRNLDVI?T=iRK)(_c?k>rHoVp`2FC5g%cU>9Q&>l48Q^77KJ1gQni1IGdn=Yfho~~$J3@G>m~Gn*(o$jXsTRB-h*%&~#ISeM$h{p!l)7e+Pv8JsV3ESmf-!7= z(xy3zw-p0xbl=bUFlsFLXQ5py_NSb{RUQQ^j>2wT#x>61bPkXxAT^$TCM!9(fc}K2 z4TuX&#YMUka>n2iv-h@tx;z87xes0rtq%@TsJZrMF;9+SW_7?zU=f7H&h^DeP&!;JD1?P?MJUkqJjwcnt>cw7LB4)MbWPc`H zRWo62jDwpJ3=h6&1T}IRG69FaN{>&2ZlMyH@HTt+8p!rPS|=!B9cR-boESU=;NDRB zJr8m==i1-1BVL6J(O={KU7l8CL3xM0kK`TQ{-Ny%Z(;;ck)nW)Iox3kj}rVx?4H*! zIb6dsmmmhBI&hoF_8fsVJOeV~FsM~|HYOGh&ncvA4l!uBZ2>>KNjQydpF@v~j#7lD z1~L@sY9@l~_FQ2udvYf|Z6KT|V2>_LT`#%2vC5HIw&%m$#;pc+*E?_?BdiBMi!4EEahn46uMovbqvi}N zdY7gLLNaNPBk;SzLj)Jg{-?fJXsMry!l}#5ZWL8hvnZcRFN>?E3YQBbHd_nCg=R=fIp(AB4h|uKm;Wa9kdq%{;hGpkzdf_3~~;TQ!KYF{Co-qNQ_8cYy42VWwhA;_VFMJvQg@6?3|my~}=J7!!+W$q<|s-#1%i1t!p ze9H*stno|Q4YPoEBdN+kUMeOD>}73csbD+4q$O#@b)ezv(BFVB@T1vb+o=!;>eGc#{}Bc(hl~|r`9q0-x@$loMeOXV*3r5H|9}n<(NH%e33LQr zv%K9L?}{se#Z};1jVBD@M+k1q(Q;m@8Zp7tH0g zG*Kp(*UP2X#pQAj{mFv3`D(d0{EK3_H}b-`-Gmh_wQ?)y%BWUu5zjGQ)lw?AlCF+R zHU1M^N~K92zlh`Jy;S&dVGc7bTJjvP?v zC}0@&T^Of8I5`6xhxy2LUq^%@8Q6CVziEk3cFDlo2XShE#p+O@b=SV9O>?S>2Pbia zK@RkSSQ(@&JcNdQufUc*f)%vJ+z^&5#xg)q@B^r(!Py4#QSJK#w!KiWqNrmtZF&ye z

Ny@8u3`-(Ca4LTGSEwK^TiOPCNMheww{3qDf&EXQ6O!>xI~0(R_o&{L6b0?rpe zMnK{zQh-oG1m5Ou6=@I^EID`xpw{GM1c29}w(WOxL>fM6h&2x>XWSo($kYP@kD!iB zJ220IU^(ESh!DSM|1J;h=0TI}h>&KmS9t$nfvZUMfTTpehQ0o$dY1C2WhtiM!LUHypp$a}Ci?!PqiLqA!W`Qd%;##-egaiM16(bhuD zrTF!ClxZ&DuQhDKx1K1<+)~6jSrQjCeq3l*r@2&kLfEHHv+!h_GR?&^xWo9VHdUI* z(-_o*&m`=Jl92iGAqByexA)DlxZ|W$q|k z+NLaXAxb4*maoc8QHz-uWu^<_n#^%6=5BORRFb&>i$Eb}UXhtz5f@|@mL#Kk%&7Ao z)ni_Ya?HKSGL&QPCxrQ?d^zSTL!$Jlx@tewn3tm%^B}@djJb+lokTI_F2Xf!iZP=I zGiovSqSwW>m}y*oxoZIw-y7me%y^5J7h)zWTIw*9l`UnMi{hfnt6Hislhx=}OK##t zn90q!J%HT8dsnor!F=mgD8XEP+fP?uzCA9$jLOU1jqc=5)L-r<+!fbfE*#Cbe0NlU z8I_l>K>NL@zubk~7u8>`Uei)}c%SmiYf*iZB)Z@B{$c@nT=Um*$<+&^A36>j6EF|HJ{uI zU<0f6#^kA7xl(v~NhKF#2o>j$2jJoZ2}h#+P?Ad~mG%4v`44cSB0ggw*$v6Z$SJZP zW=$nXq1p8&t*yk%Ai9NjEnLWZo{kOA;|Ybri3mO;xPqJxIFJGpYqcLup58vmq!_9p znvk8U!3_bC2o=PBEE(x0QJP6Q;~@A4A0*PR@j{LQC+em)&E+-Yr!;ci8?IaE!-({G zd?KiZo7?m%tC{M|bb06zX%vp-z|VqLPw<{uU_YK@OWvl#(y-iM!@vy+UlJ0^bo+@U zN@@2JnrmmrOyjAz8g_9k3!jb)X_vJW(takcqfMjQ*->ftKk_Qty@Y35s%RI|=i(~b zsGf}?+I1AQvw0D1x+SimJ?vkV$Obps_OB~wqi!}YpiTc2*Uzqfu?30uDWCmPTs=EE ze(KA-cy@2{3W{fUgB0V%v%9?Z6ScEnj}S>=$fe2P4H-D}5!u%Rgn|(?{B!bzSUb7q zO3s0J$60Z&q z8iD!7BgTP_*T3M&HSND~Qa`FDCINInrXzgS2r0sIhRBF(f5bbCOoO>$QwVlIb44Ue zwLebs+^R^oEvuK>6oTi2z_`R~9muTUsi1gUa95kobuFFa6~k4e$Vdi7LIUEvj%)Ac z3?|C5E(E(& z;00b?f^XgaEE(kt4PoM{$Wp_rkRrnPu8FWUf4kKFy!Chsd45eNtcAPu5Zs0j1!m9XEZG%0cuPz`QmxbF$9Vi+DWr@AtK^Xl109urMEdT%j diff --git a/lib/src/component/error.rs b/lib/src/component/error.rs index 98e69cc6..64b7f4b9 100644 --- a/lib/src/component/error.rs +++ b/lib/src/component/error.rs @@ -1,9 +1,9 @@ use { - super::fastly::api::{http_req, types}, + super::fastly::api::{http_req, kv_store::KvStatus, types}, crate::{ config::ClientCertError, error::{self, HandleError}, - object_store::{KeyValidationError, ObjectStoreError}, + object_store::{KeyValidationError, KvStoreError, ObjectStoreError}, wiggle_abi::{DictionaryError, SecretStoreError}, }, http::{ @@ -12,6 +12,7 @@ use { status::InvalidStatusCode, uri::InvalidUri, }, + wasmtime_wasi::ResourceTableError, }; impl types::Error { @@ -95,6 +96,12 @@ impl From for types::Error { } } +impl From for types::Error { + fn from(_: std::string::FromUtf8Error) -> Self { + types::Error::InvalidArgument + } +} + impl From for types::Error { fn from(err: wiggle::GuestError) -> Self { use wiggle::GuestError::*; @@ -130,6 +137,46 @@ impl From for types::Error { } } +impl From for types::Error { + fn from(err: KvStoreError) -> Self { + use KvStoreError::*; + match err { + Uninitialized => panic!("{}", err), + Ok => panic!("{err} should never be converted to an error"), + BadRequest => types::Error::InvalidArgument, + NotFound => types::Error::OptionalNone, + PreconditionFailed => types::Error::InvalidArgument, + PayloadTooLarge => types::Error::InvalidArgument, + InternalError => types::Error::InvalidArgument, + TooManyRequests => types::Error::InvalidArgument, + } + } +} + +impl From for types::Error { + fn from(err: ResourceTableError) -> Self { + match err { + _ => panic!("{}", err), + } + } +} + +impl From for KvStatus { + fn from(err: KvStoreError) -> Self { + use KvStoreError::*; + match err { + Uninitialized => panic!("{}", err), + Ok => KvStatus::Ok, + BadRequest => KvStatus::BadRequest, + NotFound => KvStatus::NotFound, + PreconditionFailed => KvStatus::PreconditionFailed, + PayloadTooLarge => KvStatus::PayloadTooLarge, + InternalError => KvStatus::InternalError, + TooManyRequests => KvStatus::TooManyRequests, + } + } +} + impl From for types::Error { fn from(_: KeyValidationError) -> Self { types::Error::GenericError @@ -177,6 +224,7 @@ impl From for types::Error { // We delegate to some error types' own implementation of `to_fastly_status`. Error::DictionaryError(e) => e.into(), Error::ObjectStoreError(e) => e.into(), + Error::KvStoreError(e) => e.into(), Error::SecretStoreError(e) => e.into(), // All other hostcall errors map to a generic `ERROR` value. Error::AbiVersionMismatch diff --git a/lib/src/component/kv_store.rs b/lib/src/component/kv_store.rs index 28e8729c..b67e0020 100644 --- a/lib/src/component/kv_store.rs +++ b/lib/src/component/kv_store.rs @@ -1,103 +1,297 @@ use { - super::fastly::api::{http_body, kv_store, types}, - crate::linking::ComponentCtx, + super::{ + fastly::api::{ + http_body, + kv_store::{self, InsertMode}, + types, + }, + types::TrappableError, + }, + crate::{ + linking::ComponentCtx, + object_store::{ObjectKey, ObjectStoreError}, + session::{ + PeekableTask, PendingKvDeleteTask, PendingKvInsertTask, PendingKvListTask, + PendingKvLookupTask, + }, + wiggle_abi::types::KvInsertMode, + }, + wasmtime_wasi::WasiView, }; -pub struct LookupResult; +pub struct LookupResult { + body: http_body::BodyHandle, + metadata: Option>, + generation: u32, +} #[async_trait::async_trait] impl kv_store::HostLookupResult for ComponentCtx { async fn body( &mut self, - _self_: wasmtime::component::Resource, - ) -> http_body::BodyHandle { - todo!() + rep: wasmtime::component::Resource, + ) -> wasmtime::Result { + Ok(self.table().get(&rep)?.body) } async fn metadata( &mut self, - _self_: wasmtime::component::Resource, - _max_len: u64, - ) -> Result>, types::Error> { - todo!() + rep: wasmtime::component::Resource, + max_len: u64, + ) -> Result>, TrappableError> { + let res = self.table().get(&rep)?; + let Some(md) = res.metadata.as_ref() else { + return Ok(None); + }; + + if md.len() > max_len as usize { + return Err(types::Error::BufferLen(md.len() as u64).into()); + } + + Ok(self.table().get_mut(&rep)?.metadata.take()) } async fn generation( &mut self, - _self_: wasmtime::component::Resource, - ) -> u32 { - todo!() + rep: wasmtime::component::Resource, + ) -> wasmtime::Result { + Ok(self.table().get(&rep)?.generation) } async fn drop( &mut self, - _rep: wasmtime::component::Resource, + rep: wasmtime::component::Resource, ) -> wasmtime::Result<()> { - todo!() + self.table().delete(rep)?; + Ok(()) } } #[async_trait::async_trait] impl kv_store::Host for ComponentCtx { - async fn open(&mut self, _name: String) -> Result, types::Error> { - todo!() + async fn open(&mut self, name: Vec) -> Result, types::Error> { + let name = String::from_utf8(name)?; + if self.session.kv_store.store_exists(&name)? { + // todo (byoung), handle optional/none/error case + let h = self.session.kv_store_handle(&name)?; + Ok(Some(h.into())) + } else { + Err(ObjectStoreError::UnknownObjectStore(name.to_owned()).into()) + } } async fn lookup( &mut self, - _store: kv_store::Handle, - _key: String, - ) -> Result { - todo!() + store: kv_store::Handle, + key: Vec, + ) -> Result { + let store = self.session.get_kv_store_key(store.into()).unwrap(); + let key = String::from_utf8(key)?; + // just create a future that's already ready + let fut = futures::future::ok(self.session.obj_lookup(store.clone(), ObjectKey::new(key)?)); + let task = PeekableTask::spawn(fut).await; + let lh = self + .session + .insert_pending_kv_lookup(PendingKvLookupTask::new(task)); + Ok(lh.into()) } async fn lookup_wait( &mut self, - _handle: kv_store::LookupHandle, - ) -> Result>, types::Error> { - todo!() + handle: kv_store::LookupHandle, + ) -> Result< + ( + Option>, + kv_store::KvStatus, + ), + types::Error, + > { + let resp = self + .session + .take_pending_kv_lookup(handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(value) => { + let lr = kv_store::LookupResult { + body: self.session.insert_body(value.body.into()).into(), + metadata: match value.metadata_len { + 0 => None, + _ => Some(value.metadata), + }, + generation: value.generation, + }; + + let res = self.table().push(lr)?; + + Ok((Some(res), kv_store::KvStatus::Ok)) + } + Err(e) => Ok((None, e.into())), + } } async fn insert( &mut self, - _store: kv_store::Handle, - _key: String, - _body_handle: kv_store::BodyHandle, - _mask: kv_store::InsertConfigOptions, - _config: kv_store::InsertConfig, + store: kv_store::Handle, + key: Vec, + body_handle: kv_store::BodyHandle, + mask: kv_store::InsertConfigOptions, + config: kv_store::InsertConfig, ) -> Result { - todo!() + let body = self + .session + .take_body(body_handle.into())? + .read_into_vec() + .await?; + let store = self.session.get_kv_store_key(store.into()).unwrap(); + let key = String::from_utf8(key)?; + + let mode = match config.mode { + InsertMode::Overwrite => KvInsertMode::Overwrite, + InsertMode::Add => KvInsertMode::Add, + InsertMode::Append => KvInsertMode::Append, + InsertMode::Prepend => KvInsertMode::Prepend, + }; + + let meta = if mask.contains(kv_store::InsertConfigOptions::METADATA) { + Some(config.metadata) + } else { + None + }; + + let igm = if mask.contains(kv_store::InsertConfigOptions::IF_GENERATION_MATCH) { + Some(config.if_generation_match) + } else { + None + }; + + let ttl = if mask.contains(kv_store::InsertConfigOptions::TIME_TO_LIVE_SEC) { + Some(std::time::Duration::from_secs( + config.time_to_live_sec as u64, + )) + } else { + None + }; + + let fut = futures::future::ok(self.session.kv_insert( + store.clone(), + ObjectKey::new(key)?, + body, + Some(mode), + igm, + meta, + ttl, + )); + let task = PeekableTask::spawn(fut).await; + let handle = self + .session + .insert_pending_kv_insert(PendingKvInsertTask::new(task)); + Ok(handle.into()) } - async fn insert_wait(&mut self, _handle: kv_store::InsertHandle) -> Result<(), types::Error> { - todo!() + async fn insert_wait( + &mut self, + handle: kv_store::InsertHandle, + ) -> Result { + let resp = self + .session + .take_pending_kv_insert(handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(()) => Ok(kv_store::KvStatus::Ok), + Err(e) => Ok(e.into()), + } } async fn delete( &mut self, - _store: kv_store::Handle, - _key: String, + store: kv_store::Handle, + key: Vec, ) -> Result { - todo!() + let store = self.session.get_kv_store_key(store.into()).unwrap(); + let key = String::from_utf8(key)?; + // just create a future that's already ready + let fut = futures::future::ok(self.session.kv_delete(store.clone(), ObjectKey::new(key)?)); + let task = PeekableTask::spawn(fut).await; + let lh = self + .session + .insert_pending_kv_delete(PendingKvDeleteTask::new(task)); + Ok(lh.into()) } - async fn delete_wait(&mut self, _handle: kv_store::DeleteHandle) -> Result<(), types::Error> { - todo!() + async fn delete_wait( + &mut self, + handle: kv_store::DeleteHandle, + ) -> Result { + let resp = self + .session + .take_pending_kv_delete(handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(()) => Ok(kv_store::KvStatus::Ok), + Err(e) => Ok(e.into()), + } } async fn list( &mut self, - _store: kv_store::Handle, - _mask: kv_store::ListConfigOptions, - _options: kv_store::ListConfig, + store: kv_store::Handle, + mask: kv_store::ListConfigOptions, + options: kv_store::ListConfig, ) -> Result { - todo!() + let store = self.session.get_kv_store_key(store.into()).unwrap(); + + let cursor = if mask.contains(kv_store::ListConfigOptions::CURSOR) { + Some(String::from_utf8(options.cursor)?) + } else { + None + }; + + let prefix = if mask.contains(kv_store::ListConfigOptions::PREFIX) { + Some(String::from_utf8(options.prefix)?) + } else { + None + }; + + let limit = if mask.contains(kv_store::ListConfigOptions::LIMIT) { + Some(options.limit) + } else { + None + }; + + let fut = futures::future::ok(self.session.kv_list(store.clone(), cursor, prefix, limit)); + let task = PeekableTask::spawn(fut).await; + let handle = self + .session + .insert_pending_kv_list(PendingKvListTask::new(task)); + Ok(handle.into()) } async fn list_wait( &mut self, - _handle: kv_store::ListHandle, - ) -> Result { - todo!() + handle: kv_store::ListHandle, + ) -> Result<(Option, kv_store::KvStatus), types::Error> { + let resp = self + .session + .take_pending_kv_list(handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(value) => Ok(( + Some(self.session.insert_body(value.into()).into()), + kv_store::KvStatus::Ok, + )), + Err(e) => Ok((None, e.into())), + } } } diff --git a/lib/src/component/mod.rs b/lib/src/component/mod.rs index 4dcac2c5..f137840a 100644 --- a/lib/src/component/mod.rs +++ b/lib/src/component/mod.rs @@ -6,6 +6,7 @@ component::bindgen!({ async: true, with: { "fastly:api/uap/user-agent": uap::UserAgent, + "fastly:api/kv-store/lookup-result": kv_store::LookupResult, "wasi:clocks": wasmtime_wasi::bindings::clocks, "wasi:random": wasmtime_wasi::bindings::random, @@ -18,7 +19,10 @@ component::bindgen!({ }, trappable_imports: [ - "header-values-get" + "header-values-get", + "[method]lookup-result.body", + "[method]lookup-result.metadata", + "[method]lookup-result.generation" ], }); diff --git a/lib/src/component/object_store.rs b/lib/src/component/object_store.rs index 95eb1177..79e48326 100644 --- a/lib/src/component/object_store.rs +++ b/lib/src/component/object_store.rs @@ -3,7 +3,7 @@ use { crate::{ body::Body, linking::ComponentCtx, - object_store::{ObjectKey, ObjectStoreError}, + object_store::{KvStoreError, ObjectKey}, session::{PeekableTask, PendingKvDeleteTask, PendingKvInsertTask, PendingKvLookupTask}, }, }; @@ -11,8 +11,8 @@ use { #[async_trait::async_trait] impl object_store::Host for ComponentCtx { async fn open(&mut self, name: String) -> Result, types::Error> { - if self.session.object_store.store_exists(&name)? { - let handle = self.session.obj_store_handle(&name)?; + if self.session.kv_store.store_exists(&name)? { + let handle = self.session.kv_store_handle(&name)?; Ok(Some(handle.into())) } else { Ok(None) @@ -24,17 +24,17 @@ impl object_store::Host for ComponentCtx { store: object_store::Handle, key: String, ) -> Result, types::Error> { - let store = self.session.get_obj_store_key(store.into()).unwrap(); + let store = self.session.get_kv_store_key(store.into()).unwrap(); let key = ObjectKey::new(&key)?; - match self.session.obj_lookup(store, &key) { + match self.session.obj_lookup(store.clone(), key) { Ok(obj) => { - let new_handle = self.session.insert_body(Body::from(obj)); + let new_handle = self.session.insert_body(Body::from(obj.body)); 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(KvStoreError::NotFound) => Ok(None), Err(err) => Err(err.into()), } } @@ -44,10 +44,10 @@ impl object_store::Host for ComponentCtx { store: object_store::Handle, key: String, ) -> Result { - let store = self.session.get_obj_store_key(store.into()).unwrap(); + let store = self.session.get_kv_store_key(store.into()).unwrap(); let key = ObjectKey::new(key)?; // just create a future that's already ready - let fut = futures::future::ok(self.session.obj_lookup(store, &key)); + let fut = futures::future::ok(self.session.obj_lookup(store.clone(), key)); let task = PendingKvLookupTask::new(PeekableTask::spawn(fut).await); Ok(self.session.insert_pending_kv_lookup(task).into()) } @@ -64,8 +64,8 @@ impl object_store::Host for ComponentCtx { .await?; // proceed with the normal match from lookup() match pending_obj { - Ok(obj) => Ok(Some(self.session.insert_body(Body::from(obj)).into())), - Err(ObjectStoreError::MissingObject) => Ok(None), + Ok(obj) => Ok(Some(self.session.insert_body(Body::from(obj.body)).into())), + Err(KvStoreError::NotFound) => Ok(None), Err(err) => Err(err.into()), } } @@ -76,18 +76,15 @@ impl object_store::Host for ComponentCtx { key: String, body_handle: http_types::BodyHandle, ) -> Result<(), types::Error> { - let store = self - .session - .get_obj_store_key(store.into()) - .unwrap() - .clone(); + let store = self.session.get_kv_store_key(store.into()).unwrap().clone(); let key = ObjectKey::new(&key)?; let bytes = self .session .take_body(body_handle.into())? .read_into_vec() .await?; - self.session.obj_insert(store, key, bytes)?; + self.session + .kv_insert(store, key, bytes, None, None, None, None)?; Ok(()) } @@ -98,18 +95,17 @@ impl object_store::Host for ComponentCtx { key: String, body_handle: http_types::BodyHandle, ) -> Result { - let store = self - .session - .get_obj_store_key(store.into()) - .unwrap() - .clone(); + let store = self.session.get_kv_store_key(store.into()).unwrap().clone(); let key = ObjectKey::new(&key)?; let bytes = self .session .take_body(body_handle.into())? .read_into_vec() .await?; - let fut = futures::future::ok(self.session.obj_insert(store, key, bytes)); + let fut = futures::future::ok( + self.session + .kv_insert(store, key, bytes, None, None, None, None), + ); let task = PeekableTask::spawn(fut).await; Ok(self @@ -135,13 +131,9 @@ impl object_store::Host for ComponentCtx { store: object_store::Handle, key: String, ) -> Result { - let store = self - .session - .get_obj_store_key(store.into()) - .unwrap() - .clone(); + let store = self.session.get_kv_store_key(store.into()).unwrap().clone(); let key = ObjectKey::new(&key)?; - let fut = futures::future::ok(self.session.obj_delete(store, key)); + let fut = futures::future::ok(self.session.kv_delete(store, key)); let task = PeekableTask::spawn(fut).await; Ok(self diff --git a/lib/src/component/types.rs b/lib/src/component/types.rs index bb35934a..ed066a92 100644 --- a/lib/src/component/types.rs +++ b/lib/src/component/types.rs @@ -21,6 +21,12 @@ impl types::Host for ComponentCtx { } } +impl From for TrappableError { + fn from(e: wasmtime::component::ResourceTableError) -> Self { + Self::Trap(e.into()) + } +} + impl From for TrappableError { fn from(e: types::Error) -> Self { Self::Error(e) diff --git a/lib/src/config/object_store.rs b/lib/src/config/object_store.rs index 5f156b53..707245dc 100644 --- a/lib/src/config/object_store.rs +++ b/lib/src/config/object_store.rs @@ -5,6 +5,7 @@ use { crate::{ error::{FastlyConfigError, ObjectStoreConfigError}, object_store::{ObjectKey, ObjectStoreKey, ObjectStores}, + wiggle_abi::types::KvInsertMode, }, std::fs, toml::value::Table, @@ -167,6 +168,10 @@ impl TryFrom for ObjectStoreConfig { } })?, bytes, + KvInsertMode::Overwrite, + None, + None, + None, ) .expect("Lock was not poisoned"); } diff --git a/lib/src/error.rs b/lib/src/error.rs index 765399a8..a7cd2fc0 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -95,6 +95,9 @@ pub enum Error { #[error(transparent)] ObjectStoreError(#[from] crate::object_store::ObjectStoreError), + #[error(transparent)] + KvStoreError(#[from] crate::object_store::KvStoreError), + #[error(transparent)] SecretStoreError(#[from] crate::wiggle_abi::SecretStoreError), @@ -182,6 +185,7 @@ impl Error { Error::DictionaryError(e) => e.to_fastly_status(), Error::DeviceDetectionError(e) => e.to_fastly_status(), Error::ObjectStoreError(e) => e.into(), + Error::KvStoreError(e) => e.into(), Error::SecretStoreError(e) => e.into(), Error::Again => FastlyStatus::Again, // All other hostcall errors map to a generic `ERROR` value. @@ -272,6 +276,10 @@ pub enum HandleError { #[error("Invalid pending KV delete handle: {0}")] InvalidPendingKvDeleteHandle(crate::wiggle_abi::types::PendingKvDeleteHandle), + /// A list handle was not valid. + #[error("Invalid pending KV list handle: {0}")] + InvalidPendingKvListHandle(crate::wiggle_abi::types::PendingKvListHandle), + /// A dictionary handle was not valid. #[error("Invalid dictionary handle: {0}")] InvalidDictionaryHandle(crate::wiggle_abi::types::DictionaryHandle), @@ -645,6 +653,8 @@ pub enum ObjectStoreConfigError { NotATable, #[error("There was an error when manipulating the ObjectStore: {0}.")] ObjectStoreError(#[from] crate::object_store::ObjectStoreError), + #[error("There was an error when manipulating the KvStore: {0}.")] + KvStoreError(#[from] crate::object_store::KvStoreError), #[error("Invalid `key` value used: {0}.")] KeyValidationError(#[from] crate::object_store::KeyValidationError), #[error("'{0}' is not a valid format for the config store. Supported format(s) are: 'json'.")] diff --git a/lib/src/linking.rs b/lib/src/linking.rs index 22c7858c..41f01835 100644 --- a/lib/src/linking.rs +++ b/lib/src/linking.rs @@ -298,6 +298,7 @@ pub fn link_host_functions( wiggle_abi::fastly_http_resp::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_log::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_object_store::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_kv_store::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_purge::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_secret_store::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_uap::add_to_linker(linker, WasmCtx::session)?; diff --git a/lib/src/object_store.rs b/lib/src/object_store.rs index 11a92125..9696567f 100644 --- a/lib/src/object_store.rs +++ b/lib/src/object_store.rs @@ -1,15 +1,27 @@ use { - crate::wiggle_abi::types::FastlyStatus, + crate::wiggle_abi::types::{FastlyStatus, KvError, KvInsertMode}, + base64::prelude::*, + serde::Serialize, std::{ collections::BTreeMap, sync::{Arc, RwLock}, + time::SystemTime, }, }; +#[derive(Debug, Clone)] +pub struct ObjectValue { + pub body: Vec, + pub metadata: Vec, + pub metadata_len: usize, + pub generation: u32, + pub expiration: Option, +} + #[derive(Clone, Debug, Default)] pub struct ObjectStores { #[allow(clippy::type_complexity)] - stores: Arc>>>>, + stores: Arc>>>, } impl ObjectStores { @@ -30,15 +42,32 @@ impl ObjectStores { pub fn lookup( &self, - obj_store_key: &ObjectStoreKey, - obj_key: &ObjectKey, - ) -> Result, ObjectStoreError> { + obj_store_key: ObjectStoreKey, + obj_key: ObjectKey, + ) -> Result { + let mut res = Err(KvStoreError::Uninitialized); + self.stores - .read() - .map_err(|_| ObjectStoreError::PoisonedLock)? - .get(obj_store_key) - .and_then(|map| map.get(obj_key).cloned()) - .ok_or(ObjectStoreError::MissingObject) + .write() + .map_err(|_| KvStoreError::InternalError)? + .entry(obj_store_key) + .and_modify(|store| match store.get(&obj_key) { + Some(val) => { + res = Ok(val.clone()); + // manages ttl + if let Some(exp) = val.expiration { + if SystemTime::now() >= exp { + store.remove(&obj_key); + res = Err(KvStoreError::NotFound); + } + } + } + None => { + res = Err(KvStoreError::NotFound); + } + }); + + res } pub(crate) fn insert_empty_store( @@ -60,17 +89,94 @@ impl ObjectStores { obj_store_key: ObjectStoreKey, obj_key: ObjectKey, obj: Vec, - ) -> Result<(), ObjectStoreError> { + mode: KvInsertMode, + generation: Option, + metadata: Option>, + ttl: Option, + ) -> Result<(), KvStoreError> { + // manages ttl + let existing = self.lookup(obj_store_key.clone(), obj_key.clone()); + + if let Some(g) = generation { + if let Ok(val) = &existing { + if val.generation != g { + return Err(KvStoreError::PreconditionFailed); + } + } + } + + let out_obj = match mode { + KvInsertMode::Overwrite => obj, + KvInsertMode::Add => { + if existing.is_ok() { + // key exists, add fails + return Err(KvStoreError::PreconditionFailed); + } + obj + } + KvInsertMode::Append => { + let mut out_obj; + match existing { + Err(KvStoreError::NotFound) => { + out_obj = obj; + } + Err(_) => return Err(KvStoreError::InternalError), + Ok(v) => { + out_obj = v.body; + out_obj.append(&mut obj.clone()); + } + } + out_obj + } + KvInsertMode::Prepend => { + let mut out_obj; + match existing { + Err(KvStoreError::NotFound) => { + out_obj = obj; + } + Err(_) => return Err(KvStoreError::InternalError), + Ok(mut v) => { + out_obj = obj; + out_obj.append(&mut v.body); + } + } + out_obj + } + }; + + let exp = ttl.map(|t| SystemTime::now() + t); + + let mut obj_val = ObjectValue { + body: out_obj, + metadata: vec![], + metadata_len: 0, + generation: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_nanos() as u32, + expiration: exp, + }; + + // magic number hack to ensure a case for integration tests + if obj_val.generation == 1337 { + obj_val.generation = 1338; + } + + if let Some(m) = metadata { + obj_val.metadata_len = m.len(); + obj_val.metadata = m; + } + self.stores .write() - .map_err(|_| ObjectStoreError::PoisonedLock)? + .map_err(|_| KvStoreError::InternalError)? .entry(obj_store_key) .and_modify(|store| { - store.insert(obj_key.clone(), obj.clone()); + store.insert(obj_key.clone(), obj_val.clone()); }) .or_insert_with(|| { let mut store = BTreeMap::new(); - store.insert(obj_key, obj); + store.insert(obj_key, obj_val); store }); @@ -81,16 +187,131 @@ impl ObjectStores { &self, obj_store_key: ObjectStoreKey, obj_key: ObjectKey, - ) -> Result<(), ObjectStoreError> { + ) -> Result<(), KvStoreError> { + let mut res = Ok(()); + self.stores .write() - .map_err(|_| ObjectStoreError::PoisonedLock)? + .map_err(|_| KvStoreError::InternalError)? .entry(obj_store_key) - .and_modify(|store| { - store.remove(&obj_key); + .and_modify(|store| match store.get(&obj_key) { + // 404 if the key doesn't exist, otherwise delete + Some(val) => { + // manages ttl + if let Some(exp) = val.expiration { + if SystemTime::now() >= exp { + res = Err(KvStoreError::NotFound); + } + } + store.remove(&obj_key); + } + None => { + res = Err(KvStoreError::NotFound); + } }); - Ok(()) + res + } + + pub fn list( + &self, + obj_store_key: ObjectStoreKey, + cursor: Option, + prefix: Option, + limit: u32, + ) -> Result, KvStoreError> { + let mut res = Err(KvStoreError::InternalError); + + let cursor = match cursor { + Some(c) => { + let cursor_bytes = BASE64_STANDARD + .decode(c) + .map_err(|_| KvStoreError::BadRequest)?; + let decoded = + String::from_utf8(cursor_bytes).map_err(|_| KvStoreError::BadRequest)?; + Some(decoded) + } + None => None, + }; + + self.stores + .write() + .map_err(|_| KvStoreError::InternalError)? + .entry(obj_store_key.clone()) + .and_modify(|store| { + // manages ttl + // a bit wasteful to run this loop twice, but we need mutable access to store, + // and it's already claimed in the filters below + let ttl_list = store.iter_mut().map(|(k, _)| k.clone()).collect::>(); + ttl_list.into_iter().for_each(|k| { + let val = store.get(&k); + if let Some(v) = val { + if let Some(exp) = v.expiration { + if SystemTime::now() >= exp { + store.remove(&k); + } + } + } + }); + + let mut list = store + .iter_mut() + .filter(|(k, _)| { + if let Some(c) = &cursor { + &k.0 > c + } else { + true + } + }) + .filter(|(k, _)| { + if let Some(p) = &prefix { + k.0.starts_with(p) + } else { + true + } + }) + .map(|(k, _)| String::from_utf8(k.0.as_bytes().to_vec()).unwrap()) + .collect::>(); + + // limit + let old_len = list.len(); + list.truncate(limit as usize); + let new_len = list.len(); + + let next_cursor = match old_len != new_len { + true => Some(BASE64_STANDARD.encode(&list[new_len - 1])), + false => None, + }; + + #[derive(Serialize)] + struct Metadata { + limit: u32, + #[serde(skip_serializing_if = "Option::is_none")] + prefix: Option, + #[serde(skip_serializing_if = "Option::is_none")] + next_cursor: Option, + } + #[derive(Serialize)] + struct JsonOutput { + data: Vec, + meta: Metadata, + } + + let body = JsonOutput { + data: list, + meta: Metadata { + limit, + prefix, + next_cursor, + }, + }; + + match serde_json::to_string(&body).map_err(|_| KvStoreError::InternalError) { + Ok(s) => res = Ok(s.as_bytes().to_vec()), + Err(e) => res = Err(e), + }; + }); + res } } @@ -136,6 +357,81 @@ impl From<&ObjectStoreError> for FastlyStatus { } } +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, thiserror::Error)] +pub enum KvStoreError { + #[error("The error was not set")] + Uninitialized, + #[error("There was no error")] + Ok, + #[error("KV store cannot or will not process the request due to something that is perceived to be a client error")] + BadRequest, + #[error("KV store cannot find the requested resource")] + NotFound, + #[error("KV store cannot fulfill the request, as definied by the client's prerequisites (ie. if-generation-match)")] + PreconditionFailed, + #[error("The size limit for a KV store key was exceeded")] + PayloadTooLarge, + #[error("The system encountered an unexpected internal error")] + InternalError, + #[error("Too many requests have been made to the KV store")] + TooManyRequests, +} + +impl From<&KvError> for KvStoreError { + fn from(e: &KvError) -> Self { + match e { + KvError::Uninitialized => KvStoreError::Uninitialized, + KvError::Ok => KvStoreError::Ok, + KvError::BadRequest => KvStoreError::BadRequest, + KvError::NotFound => KvStoreError::NotFound, + KvError::PreconditionFailed => KvStoreError::PreconditionFailed, + KvError::PayloadTooLarge => KvStoreError::PayloadTooLarge, + KvError::InternalError => KvStoreError::InternalError, + KvError::TooManyRequests => KvStoreError::TooManyRequests, + } + } +} + +impl From<&KvStoreError> for KvError { + fn from(e: &KvStoreError) -> Self { + match e { + KvStoreError::Uninitialized => KvError::Uninitialized, + KvStoreError::Ok => KvError::Ok, + KvStoreError::BadRequest => KvError::BadRequest, + KvStoreError::NotFound => KvError::NotFound, + KvStoreError::PreconditionFailed => KvError::PreconditionFailed, + KvStoreError::PayloadTooLarge => KvError::PayloadTooLarge, + KvStoreError::InternalError => KvError::InternalError, + KvStoreError::TooManyRequests => KvError::TooManyRequests, + } + } +} + +impl From<&KvStoreError> for ObjectStoreError { + fn from(e: &KvStoreError) -> Self { + match e { + // the only real one + KvStoreError::NotFound => ObjectStoreError::MissingObject, + _ => ObjectStoreError::UnknownObjectStore("".to_string()), + } + } +} + +impl From<&KvStoreError> for FastlyStatus { + fn from(e: &KvStoreError) -> Self { + match e { + KvStoreError::Uninitialized => panic!("{}", e), + KvStoreError::Ok => FastlyStatus::Ok, + KvStoreError::BadRequest => FastlyStatus::Inval, + KvStoreError::NotFound => FastlyStatus::None, + KvStoreError::PreconditionFailed => FastlyStatus::Inval, + KvStoreError::PayloadTooLarge => FastlyStatus::Inval, + KvStoreError::InternalError => FastlyStatus::Inval, + KvStoreError::TooManyRequests => FastlyStatus::Inval, + } + } +} + /// Keys in the Object Store must follow the following rules: /// /// * Keys can contain any sequence of valid Unicode characters, of length 1-1024 bytes when @@ -193,3 +489,460 @@ pub enum KeyValidationError { #[error("Keys for objects cannot contain a `{0}`")] Contains(String), } + +#[cfg(test)] +mod tests { + use super::*; + + const STORE_NAME: &'static str = "test_store"; + + #[test] + fn test_kv_store_exists() { + let stores = ObjectStores::default(); + stores + .insert_empty_store(ObjectStoreKey(STORE_NAME.to_string())) + .unwrap(); + + let res = stores.store_exists(STORE_NAME); + match res { + Ok(true) => {} + _ => panic!("should have been OK(true)"), + } + } + + #[test] + fn test_kv_store_basics() { + let stores = ObjectStores::default(); + stores + .insert_empty_store(ObjectStoreKey(STORE_NAME.to_string())) + .unwrap(); + + let key = "insert_key".to_string(); + let val1 = "val1".to_string(); + + // insert + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val1.clone().into(), + KvInsertMode::Overwrite, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + + // lookup + let res = stores.lookup( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + ); + match res { + Ok(ov) => { + assert_eq!(ov.body, val1.as_bytes().to_vec()) + } + Err(_) => panic!("should have been OK"), + } + + // list + let limit = 1000; + let res = stores.list(ObjectStoreKey(STORE_NAME.to_string()), None, None, limit); + match res { + Ok(ov) => { + let val = format!(r#"{{"data":["{key}"],"meta":{{"limit":{limit}}}}}"#); + assert_eq!(std::str::from_utf8(&ov).unwrap(), val) + } + Err(_) => panic!("should have been OK"), + } + + // delete + let res = stores.delete( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + ); + match res { + Ok(_) => {} + Err(_) => panic!("should have been OK"), + } + } + + #[test] + fn test_kv_store_item_404s() { + let stores = ObjectStores::default(); + stores + .insert_empty_store(ObjectStoreKey(STORE_NAME.to_string())) + .unwrap(); + + let res = stores.lookup( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey("bad_key".to_string()), + ); + match res { + Ok(_) => panic!("should not have been OK"), + Err(e) => assert_eq!(e, KvStoreError::NotFound), + } + + let res = stores.delete( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey("bad_key".to_string()), + ); + match res { + Ok(_) => panic!("should not have been OK"), + Err(e) => assert_eq!(e, KvStoreError::NotFound), + } + } + + #[test] + fn test_kv_store_item_insert_modes() { + let stores = ObjectStores::default(); + stores + .insert_empty_store(ObjectStoreKey(STORE_NAME.to_string())) + .unwrap(); + + let key = "insert_key".to_string(); + let val1 = "val1".to_string(); + let val2 = "val2".to_string(); + let val3 = "val3".to_string(); + + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val1.clone().into(), + KvInsertMode::Add, + None, + None, + None, + ); + assert!(res.is_ok()); + // fail on Add, because key already exists + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val1.clone().into(), + KvInsertMode::Add, + None, + None, + None, + ); + match res { + Ok(_) => panic!("should not have been OK"), + Err(e) => assert_eq!(e, KvStoreError::PreconditionFailed), + } + // prepend val2 + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val2.clone().into(), + KvInsertMode::Prepend, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + // append val3 + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val3.clone().into(), + KvInsertMode::Append, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + let res = stores.lookup( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + ); + match res { + Ok(ov) => { + let val = format!("{val2}{val1}{val3}"); + assert_eq!(ov.body, val.as_bytes().to_vec()) + } + Err(_) => panic!("should have been OK"), + } + + // overwrite val3 + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val3.clone().into(), + KvInsertMode::Overwrite, + None, + Some(val2.as_bytes().to_vec()), + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + + // test overwrite + let res = stores.lookup( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + ); + match res { + Ok(ov) => { + assert_eq!(ov.body, val3.as_bytes().to_vec()); + assert_eq!(ov.metadata, val2.as_bytes().to_vec()); + } + Err(_) => panic!("should have been OK"), + } + } + + #[test] + fn test_kv_store_item_insert_generation() { + let stores = ObjectStores::default(); + stores + .insert_empty_store(ObjectStoreKey(STORE_NAME.to_string())) + .unwrap(); + + let key = "insert_key".to_string(); + let val1 = "val1".to_string(); + + // insert val1 + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val1.clone().into(), + KvInsertMode::Overwrite, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + + // test overwrite, get gen + let generation; + let res = stores.lookup( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + ); + match res { + Ok(ov) => { + assert_eq!(ov.body, val1.as_bytes().to_vec()); + generation = ov.generation; + } + Err(_) => panic!("should have been OK"), + } + + // test generation match failure + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val1.clone().into(), + KvInsertMode::Overwrite, + Some(1337), + None, + None, + ); + match res { + Err(KvStoreError::PreconditionFailed) => {} + _ => panic!("should have been Err(KvStoreError::PreconditionFailed)"), + } + + // test generation match positive + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val1.clone().into(), + KvInsertMode::Overwrite, + Some(generation), + None, + None, + ); + match res { + Ok(_) => {} + _ => panic!("should have been OK"), + } + + // check result + let res = stores.lookup( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + ); + match res { + Ok(ov) => { + assert_eq!(ov.body, val1.as_bytes().to_vec()); + } + Err(_) => panic!("should have been OK"), + } + } + + #[test] + fn test_kv_store_item_list_advanced() { + let stores = ObjectStores::default(); + stores + .insert_empty_store(ObjectStoreKey(STORE_NAME.to_string())) + .unwrap(); + + let key = "insert_key".to_string(); + let prefix = "key".to_string(); + let key1 = format!("{prefix}1").to_string(); + let key2 = format!("{prefix}2").to_string(); + let key3 = format!("{prefix}3").to_string(); + let val1 = "val1".to_string(); + let val2 = "val2".to_string(); + let val3 = "val3".to_string(); + + // insert insert_key + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key.clone()), + val1.clone().into(), + KvInsertMode::Overwrite, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + + // insert val1 + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key1.clone()), + val1.clone().into(), + KvInsertMode::Overwrite, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + // insert val2 + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key2.clone()), + val2.clone().into(), + KvInsertMode::Overwrite, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + // insert val3 + let res = stores.insert( + ObjectStoreKey(STORE_NAME.to_string()), + ObjectKey(key3.clone()), + val3.clone().into(), + KvInsertMode::Overwrite, + None, + None, + None, + ); + match res { + Err(_) => panic!("should have been OK"), + _ => {} + } + + // list + let limit = 1000; + let res = stores.list(ObjectStoreKey(STORE_NAME.to_string()), None, None, limit); + match res { + Ok(ov) => { + let val = format!( + r#"{{"data":["{key}","{key1}","{key2}","{key3}"],"meta":{{"limit":{limit}}}}}"# + ); + assert_eq!(std::str::from_utf8(&ov).unwrap(), val) + } + Err(_) => panic!("should have been OK"), + } + + // list w/prefix + let limit = 1000; + let res = stores.list( + ObjectStoreKey(STORE_NAME.to_string()), + None, + Some(prefix.clone()), + limit, + ); + match res { + Ok(ov) => { + let val = format!( + r#"{{"data":["{key1}","{key2}","{key3}"],"meta":{{"limit":{limit},"prefix":"{prefix}"}}}}"# + ); + assert_eq!(std::str::from_utf8(&ov).unwrap(), val) + } + Err(_) => panic!("should have been OK"), + } + + // list w/prefix&limit + let limit = 1; + let res = stores.list( + ObjectStoreKey(STORE_NAME.to_string()), + None, + Some(prefix.clone()), + limit, + ); + match res { + Ok(ov) => { + let next_cursor = BASE64_STANDARD.encode(key1.clone()); + let val = format!( + r#"{{"data":["{key1}"],"meta":{{"limit":{limit},"prefix":"{prefix}","next_cursor":"{next_cursor}"}}}}"# + ); + assert_eq!(std::str::from_utf8(&ov).unwrap(), val) + } + Err(_) => panic!("should have been OK"), + } + + // list w/prefix&limit&cursor + let limit = 1; + let last_cursor = BASE64_STANDARD.encode(key1.clone()); + let res = stores.list( + ObjectStoreKey(STORE_NAME.to_string()), + Some(last_cursor), + Some(prefix.clone()), + limit, + ); + match res { + Ok(ov) => { + let next_cursor = BASE64_STANDARD.encode(key2.clone()); + let val = format!( + r#"{{"data":["{key2}"],"meta":{{"limit":{limit},"prefix":"{prefix}","next_cursor":"{next_cursor}"}}}}"# + ); + assert_eq!(std::str::from_utf8(&ov).unwrap(), val) + } + Err(_) => panic!("should have been OK"), + } + + // list w/prefix&limit&cursor + let limit = 1; + let last_cursor = BASE64_STANDARD.encode(key2.clone()); + let res = stores.list( + ObjectStoreKey(STORE_NAME.to_string()), + Some(last_cursor), + Some(prefix.clone()), + limit, + ); + match res { + Ok(ov) => { + let val = format!( + r#"{{"data":["{key3}"],"meta":{{"limit":{limit},"prefix":"{prefix}"}}}}"# + ); + assert_eq!(std::str::from_utf8(&ov).unwrap(), val) + } + Err(_) => panic!("should have been OK"), + } + } +} diff --git a/lib/src/session.rs b/lib/src/session.rs index b6378c04..edade7fd 100644 --- a/lib/src/session.rs +++ b/lib/src/session.rs @@ -4,7 +4,8 @@ mod async_item; mod downstream; pub use async_item::{ - AsyncItem, PeekableTask, PendingKvDeleteTask, PendingKvInsertTask, PendingKvLookupTask, + AsyncItem, PeekableTask, PendingKvDeleteTask, PendingKvInsertTask, PendingKvListTask, + PendingKvLookupTask, }; use std::collections::HashMap; @@ -14,6 +15,9 @@ use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use std::sync::atomic::AtomicU64; use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use crate::object_store::KvStoreError; use { self::downstream::DownstreamResponse, @@ -22,14 +26,16 @@ use { config::{Backend, Backends, DeviceDetection, Dictionaries, Geolocation, LoadedDictionary}, error::{Error, HandleError}, logging::LogEndpoint, - object_store::{ObjectKey, ObjectStoreError, ObjectStoreKey, ObjectStores}, + object_store::{ObjectKey, ObjectStoreKey, ObjectStores, ObjectValue}, secret_store::{SecretLookup, SecretStores}, streaming_body::StreamingBody, upstream::{SelectTarget, TlsConfig}, wiggle_abi::types::{ - self, BodyHandle, ContentEncodings, DictionaryHandle, EndpointHandle, - ObjectStoreHandle, PendingKvDeleteHandle, PendingKvInsertHandle, PendingKvLookupHandle, - PendingRequestHandle, RequestHandle, ResponseHandle, SecretHandle, SecretStoreHandle, + self, BodyHandle, ContentEncodings, DictionaryHandle, EndpointHandle, KvInsertMode, + KvStoreDeleteHandle, KvStoreHandle, KvStoreInsertHandle, KvStoreListHandle, + KvStoreLookupHandle, PendingKvDeleteHandle, PendingKvInsertHandle, PendingKvListHandle, + PendingKvLookupHandle, PendingRequestHandle, RequestHandle, ResponseHandle, + SecretHandle, SecretStoreHandle, }, ExecuteCtx, }, @@ -122,11 +128,11 @@ pub struct Session { /// The ObjectStore configured for this execution. /// /// Populated prior to guest execution and can be modified during requests. - pub(crate) object_store: ObjectStores, + pub(crate) kv_store: ObjectStores, /// The object stores configured for this execution. /// /// Populated prior to guest execution. - object_store_by_name: PrimaryMap, + kv_store_by_name: PrimaryMap, /// The secret stores configured for this execution. /// /// Populated prior to guest execution, and never modified. @@ -164,7 +170,7 @@ impl Session { tls_config: TlsConfig, dictionaries: Arc, config_path: Arc>, - object_store: ObjectStores, + kv_store: ObjectStores, secret_stores: Arc, ) -> Session { let (parts, body) = req.into_parts(); @@ -199,8 +205,8 @@ impl Session { tls_config, dictionaries, loaded_dictionaries: PrimaryMap::new(), - object_store, - object_store_by_name: PrimaryMap::new(), + kv_store, + kv_store_by_name: PrimaryMap::new(), secret_stores, secret_stores_by_name: PrimaryMap::new(), secrets_by_name: PrimaryMap::new(), @@ -678,23 +684,33 @@ impl Session { ) } - // ----- Object Store API ----- - pub fn obj_store_handle(&mut self, key: &str) -> Result { + // ----- KV Store API ----- + pub fn kv_store_handle(&mut self, key: &str) -> Result { let obj_key = ObjectStoreKey::new(key); - Ok(self.object_store_by_name.push(obj_key)) + Ok(self.kv_store_by_name.push(obj_key)) } - pub fn get_obj_store_key(&self, handle: ObjectStoreHandle) -> Option<&ObjectStoreKey> { - self.object_store_by_name.get(handle) + pub fn get_kv_store_key(&self, handle: KvStoreHandle) -> Option<&ObjectStoreKey> { + self.kv_store_by_name.get(handle) } - pub fn obj_insert( + pub fn kv_insert( &self, obj_store_key: ObjectStoreKey, obj_key: ObjectKey, obj: Vec, - ) -> Result<(), ObjectStoreError> { - self.object_store.insert(obj_store_key, obj_key, obj) + mode: Option, + generation: Option, + metadata: Option>, + ttl: Option, + ) -> Result<(), KvStoreError> { + let mode = match mode { + None => KvInsertMode::Overwrite, + Some(m) => m, + }; + + self.kv_store + .insert(obj_store_key, obj_key, obj, mode, generation, metadata, ttl) } /// Insert a [`PendingKvInsert`] into the session. @@ -704,7 +720,7 @@ impl Session { pub fn insert_pending_kv_insert( &mut self, pending: PendingKvInsertTask, - ) -> PendingKvInsertHandle { + ) -> KvStoreInsertHandle { self.async_items .push(Some(AsyncItem::PendingKvInsert(pending))) .into() @@ -743,12 +759,12 @@ impl Session { .ok_or(HandleError::InvalidPendingKvInsertHandle(handle)) } - pub fn obj_delete( + pub fn kv_delete( &self, obj_store_key: ObjectStoreKey, obj_key: ObjectKey, - ) -> Result<(), ObjectStoreError> { - self.object_store.delete(obj_store_key, obj_key) + ) -> Result<(), KvStoreError> { + self.kv_store.delete(obj_store_key, obj_key) } /// Insert a [`PendingKvDelete`] into the session. @@ -799,10 +815,10 @@ impl Session { pub fn obj_lookup( &self, - obj_store_key: &ObjectStoreKey, - obj_key: &ObjectKey, - ) -> Result, ObjectStoreError> { - self.object_store.lookup(obj_store_key, obj_key) + obj_store_key: ObjectStoreKey, + obj_key: ObjectKey, + ) -> Result { + self.kv_store.lookup(obj_store_key, obj_key) } /// Insert a [`PendingLookup`] into the session. @@ -851,6 +867,61 @@ impl Session { .ok_or(HandleError::InvalidPendingKvLookupHandle(handle)) } + pub fn kv_list( + &self, + obj_store_key: ObjectStoreKey, + cursor: Option, + prefix: Option, + limit: Option, + ) -> Result, KvStoreError> { + let limit = limit.unwrap_or(1000); + + self.kv_store.list(obj_store_key, cursor, prefix, limit) + } + + /// Insert a [`PendingList`] into the session. + /// + /// This method returns a new [`PendingKvListHandle`], which can then be used to access + /// and mutate the pending list. + pub fn insert_pending_kv_list(&mut self, pending: PendingKvListTask) -> PendingKvListHandle { + self.async_items + .push(Some(AsyncItem::PendingKvList(pending))) + .into() + } + + /// Take ownership of a [`PendingList`], given its [`PendingKvListHandle`]. + /// + /// Returns a [`HandleError`] if the handle is not associated with a pending list in the + /// session. + pub fn take_pending_kv_list( + &mut self, + handle: PendingKvListHandle, + ) -> Result { + // check that this is a pending request before removing it + let _ = self.pending_kv_list(handle)?; + + self.async_items + .get_mut(handle.into()) + .and_then(Option::take) + .and_then(AsyncItem::into_pending_kv_list) + .ok_or(HandleError::InvalidPendingKvListHandle(handle)) + } + + /// Get a reference to a [`PendingList`], given its [`PendingKvListHandle`]. + /// + /// Returns a [`HandleError`] if the handle is not associated with a list in the + /// session. + pub fn pending_kv_list( + &self, + handle: PendingKvListHandle, + ) -> Result<&PendingKvListTask, HandleError> { + self.async_items + .get(handle.into()) + .and_then(Option::as_ref) + .and_then(AsyncItem::as_pending_kv_list) + .ok_or(HandleError::InvalidPendingKvListHandle(handle)) + } + // ----- Secret Store API ----- pub fn secret_store_handle(&mut self, name: &str) -> Option { @@ -1165,3 +1236,63 @@ impl From for PendingKvDeleteHandle { PendingKvDeleteHandle::from(h.as_u32()) } } + +impl From for AsyncItemHandle { + fn from(h: PendingKvListHandle) -> AsyncItemHandle { + AsyncItemHandle::from_u32(h.into()) + } +} + +impl From for PendingKvListHandle { + fn from(h: AsyncItemHandle) -> PendingKvListHandle { + PendingKvListHandle::from(h.as_u32()) + } +} + +impl From for AsyncItemHandle { + fn from(h: KvStoreLookupHandle) -> AsyncItemHandle { + AsyncItemHandle::from_u32(h.into()) + } +} + +impl From for KvStoreLookupHandle { + fn from(h: AsyncItemHandle) -> KvStoreLookupHandle { + KvStoreLookupHandle::from(h.as_u32()) + } +} + +impl From for AsyncItemHandle { + fn from(h: KvStoreInsertHandle) -> AsyncItemHandle { + AsyncItemHandle::from_u32(h.into()) + } +} + +impl From for KvStoreInsertHandle { + fn from(h: AsyncItemHandle) -> KvStoreInsertHandle { + KvStoreInsertHandle::from(h.as_u32()) + } +} + +impl From for AsyncItemHandle { + fn from(h: KvStoreDeleteHandle) -> AsyncItemHandle { + AsyncItemHandle::from_u32(h.into()) + } +} + +impl From for KvStoreDeleteHandle { + fn from(h: AsyncItemHandle) -> KvStoreDeleteHandle { + KvStoreDeleteHandle::from(h.as_u32()) + } +} + +impl From for AsyncItemHandle { + fn from(h: KvStoreListHandle) -> AsyncItemHandle { + AsyncItemHandle::from_u32(h.into()) + } +} + +impl From for KvStoreListHandle { + fn from(h: AsyncItemHandle) -> KvStoreListHandle { + KvStoreListHandle::from(h.as_u32()) + } +} diff --git a/lib/src/session/async_item.rs b/lib/src/session/async_item.rs index c6600049..000ee100 100644 --- a/lib/src/session/async_item.rs +++ b/lib/src/session/async_item.rs @@ -1,4 +1,4 @@ -use crate::object_store::ObjectStoreError; +use crate::object_store::{KvStoreError, ObjectValue}; use crate::{body::Body, error::Error, streaming_body::StreamingBody}; use anyhow::anyhow; use futures::Future; @@ -7,34 +7,45 @@ use http::Response; use tokio::sync::oneshot; #[derive(Debug)] -pub struct PendingKvLookupTask(PeekableTask, ObjectStoreError>>); +pub struct PendingKvLookupTask(PeekableTask>); impl PendingKvLookupTask { - pub fn new(t: PeekableTask, ObjectStoreError>>) -> PendingKvLookupTask { + pub fn new(t: PeekableTask>) -> PendingKvLookupTask { PendingKvLookupTask(t) } - pub fn task(self) -> PeekableTask, ObjectStoreError>> { + pub fn task(self) -> PeekableTask> { self.0 } } #[derive(Debug)] -pub struct PendingKvInsertTask(PeekableTask>); +pub struct PendingKvInsertTask(PeekableTask>); impl PendingKvInsertTask { - pub fn new(t: PeekableTask>) -> PendingKvInsertTask { + pub fn new(t: PeekableTask>) -> PendingKvInsertTask { PendingKvInsertTask(t) } - pub fn task(self) -> PeekableTask> { + pub fn task(self) -> PeekableTask> { self.0 } } #[derive(Debug)] -pub struct PendingKvDeleteTask(PeekableTask>); +pub struct PendingKvDeleteTask(PeekableTask>); impl PendingKvDeleteTask { - pub fn new(t: PeekableTask>) -> PendingKvDeleteTask { + pub fn new(t: PeekableTask>) -> PendingKvDeleteTask { PendingKvDeleteTask(t) } - pub fn task(self) -> PeekableTask> { + pub fn task(self) -> PeekableTask> { + self.0 + } +} + +#[derive(Debug)] +pub struct PendingKvListTask(PeekableTask, KvStoreError>>); +impl PendingKvListTask { + pub fn new(t: PeekableTask, KvStoreError>>) -> PendingKvListTask { + PendingKvListTask(t) + } + pub fn task(self) -> PeekableTask, KvStoreError>> { self.0 } } @@ -51,6 +62,7 @@ pub enum AsyncItem { PendingKvLookup(PendingKvLookupTask), PendingKvInsert(PendingKvInsertTask), PendingKvDelete(PendingKvDeleteTask), + PendingKvList(PendingKvListTask), } impl AsyncItem { @@ -149,6 +161,20 @@ impl AsyncItem { } } + pub fn as_pending_kv_list(&self) -> Option<&PendingKvListTask> { + match self { + Self::PendingKvList(req) => Some(req), + _ => None, + } + } + + pub fn into_pending_kv_list(self) -> Option { + match self { + Self::PendingKvList(req) => Some(req), + _ => None, + } + } + pub fn as_pending_req(&self) -> Option<&PeekableTask>> { match self { Self::PendingReq(req) => Some(req), @@ -178,6 +204,7 @@ impl AsyncItem { Self::PendingKvLookup(req) => req.0.await_ready().await, Self::PendingKvInsert(req) => req.0.await_ready().await, Self::PendingKvDelete(req) => req.0.await_ready().await, + Self::PendingKvList(req) => req.0.await_ready().await, } } @@ -210,6 +237,12 @@ impl From for AsyncItem { } } +impl From for AsyncItem { + fn from(task: PendingKvListTask) -> Self { + Self::PendingKvList(task) + } +} + #[derive(Debug)] pub enum PeekableTask { Waiting(oneshot::Receiver>), diff --git a/lib/src/wiggle_abi.rs b/lib/src/wiggle_abi.rs index bd76b3b0..bcae2721 100644 --- a/lib/src/wiggle_abi.rs +++ b/lib/src/wiggle_abi.rs @@ -60,6 +60,7 @@ mod erl_impl; mod fastly_purge_impl; mod geo_impl; mod headers; +mod kv_store_impl; mod log_impl; mod obj_store_impl; mod req_impl; @@ -75,7 +76,8 @@ wiggle::from_witx!({ errors: { fastly_status => Error }, async: { fastly_async_io::{select}, - fastly_object_store::{delete_async, pending_delete_wait, insert, insert_async, pending_insert_wait, lookup_async, pending_lookup_wait}, + fastly_object_store::{delete_async, pending_delete_wait, insert, insert_async, pending_insert_wait, lookup_async, pending_lookup_wait, list}, + fastly_kv_store::{lookup, lookup_wait, insert, insert_wait, delete, delete_wait, list, list_wait}, fastly_http_body::{append, read, write}, fastly_http_req::{ pending_req_select, pending_req_select_v2, pending_req_poll, pending_req_poll_v2, @@ -84,6 +86,76 @@ wiggle::from_witx!({ } }); +impl From for types::KvStoreHandle { + fn from(h: types::ObjectStoreHandle) -> types::KvStoreHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::ObjectStoreHandle { + fn from(h: types::KvStoreHandle) -> types::ObjectStoreHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::PendingKvLookupHandle { + fn from(h: types::KvStoreLookupHandle) -> types::PendingKvLookupHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::KvStoreLookupHandle { + fn from(h: types::PendingKvLookupHandle) -> types::KvStoreLookupHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::PendingKvInsertHandle { + fn from(h: types::KvStoreInsertHandle) -> types::PendingKvInsertHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::KvStoreInsertHandle { + fn from(h: types::PendingKvInsertHandle) -> types::KvStoreInsertHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::PendingKvDeleteHandle { + fn from(h: types::KvStoreDeleteHandle) -> types::PendingKvDeleteHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::KvStoreDeleteHandle { + fn from(h: types::PendingKvDeleteHandle) -> types::KvStoreDeleteHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::PendingKvListHandle { + fn from(h: types::KvStoreListHandle) -> types::PendingKvListHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + +impl From for types::KvStoreListHandle { + fn from(h: types::PendingKvListHandle) -> types::KvStoreListHandle { + let s = unsafe { h.inner() }; + s.into() + } +} + impl From for http::version::Version { fn from(v: types::HttpVersion) -> http::version::Version { match v { diff --git a/lib/src/wiggle_abi/entity.rs b/lib/src/wiggle_abi/entity.rs index 1641ade8..5b11940d 100644 --- a/lib/src/wiggle_abi/entity.rs +++ b/lib/src/wiggle_abi/entity.rs @@ -3,8 +3,9 @@ //! [ref]: https://docs.rs/cranelift-entity/latest/cranelift_entity/trait.EntityRef.html use super::types::{ - AsyncItemHandle, BodyHandle, DictionaryHandle, EndpointHandle, ObjectStoreHandle, - PendingRequestHandle, RequestHandle, ResponseHandle, SecretHandle, SecretStoreHandle, + AsyncItemHandle, BodyHandle, DictionaryHandle, EndpointHandle, KvStoreHandle, + ObjectStoreHandle, PendingRequestHandle, RequestHandle, ResponseHandle, SecretHandle, + SecretStoreHandle, }; /// Macro which implements a 32-bit entity reference for handles generated by Wiggle. @@ -46,6 +47,7 @@ wiggle_entity!(EndpointHandle); wiggle_entity!(PendingRequestHandle); wiggle_entity!(DictionaryHandle); wiggle_entity!(ObjectStoreHandle); +wiggle_entity!(KvStoreHandle); wiggle_entity!(SecretStoreHandle); wiggle_entity!(SecretHandle); wiggle_entity!(AsyncItemHandle); diff --git a/lib/src/wiggle_abi/kv_store_impl.rs b/lib/src/wiggle_abi/kv_store_impl.rs new file mode 100644 index 00000000..96f3ca0d --- /dev/null +++ b/lib/src/wiggle_abi/kv_store_impl.rs @@ -0,0 +1,335 @@ +//! fastly_obj_store` hostcall implementations. + +use crate::object_store::KvStoreError; +use crate::session::PeekableTask; +use crate::session::{ + PendingKvDeleteTask, PendingKvInsertTask, PendingKvListTask, PendingKvLookupTask, +}; + +use { + crate::{ + error::Error, + object_store::{ObjectKey, ObjectStoreError}, + session::Session, + wiggle_abi::{ + fastly_kv_store::FastlyKvStore, + types::{ + BodyHandle, KvDeleteConfig, KvDeleteConfigOptions, KvError, KvInsertConfig, + KvInsertConfigOptions, KvListConfig, KvListConfigOptions, KvLookupConfig, + KvLookupConfigOptions, KvStoreDeleteHandle, KvStoreHandle, KvStoreInsertHandle, + KvStoreListHandle, KvStoreLookupHandle, + }, + }, + }, + wiggle::{GuestMemory, GuestPtr}, +}; + +#[wiggle::async_trait] +impl FastlyKvStore for Session { + fn open( + &mut self, + memory: &mut GuestMemory<'_>, + name: GuestPtr, + ) -> Result { + let name = memory.as_str(name)?.ok_or(Error::SharedMemory)?; + if self.kv_store.store_exists(&name)? { + self.kv_store_handle(&name) + } else { + Err(Error::ObjectStoreError( + ObjectStoreError::UnknownObjectStore(name.to_owned()), + )) + } + } + + async fn lookup( + &mut self, + memory: &mut GuestMemory<'_>, + store: KvStoreHandle, + key: GuestPtr, + _lookup_config_mask: KvLookupConfigOptions, + _lookup_configuration: GuestPtr, + handle_out: GuestPtr, + ) -> Result<(), Error> { + let store = self.get_kv_store_key(store).unwrap(); + let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string()) + .map_err(|_| KvStoreError::BadRequest)?; + // just create a future that's already ready + let fut = futures::future::ok(self.obj_lookup(store.clone(), key)); + let task = PeekableTask::spawn(fut).await; + memory.write( + handle_out, + self.insert_pending_kv_lookup(PendingKvLookupTask::new(task)) + .into(), + )?; + Ok(()) + } + + async fn lookup_wait( + &mut self, + memory: &mut GuestMemory<'_>, + pending_kv_lookup_handle: KvStoreLookupHandle, + body_handle_out: GuestPtr, + metadata_buf: GuestPtr, + metadata_buf_len: u32, + nwritten_out: GuestPtr, + generation_out: GuestPtr, + kv_error_out: GuestPtr, + ) -> Result<(), Error> { + let resp = self + .take_pending_kv_lookup(pending_kv_lookup_handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(value) => { + let body_handle = self.insert_body(value.body.into()); + + memory.write(body_handle_out, body_handle)?; + match value.metadata_len { + 0 => memory.write(nwritten_out, 0)?, + len => { + let meta_len_u32 = + u32::try_from(len).expect("metadata len is outside the bounds of u32"); + memory.write(nwritten_out, meta_len_u32)?; + if meta_len_u32 > metadata_buf_len { + return Err(Error::BufferLengthError { + buf: "metadata", + len: "specified length", + }); + } + memory.copy_from_slice( + &value.metadata, + metadata_buf.as_array(meta_len_u32), + )?; + } + } + memory.write(generation_out, value.generation)?; + memory.write(kv_error_out, KvError::Ok)?; + Ok(()) + } + Err(e) => { + memory.write(kv_error_out, (&e).into())?; + Ok(()) + } + } + } + + async fn insert( + &mut self, + memory: &mut GuestMemory<'_>, + store: KvStoreHandle, + key: GuestPtr, + body_handle: BodyHandle, + insert_config_mask: KvInsertConfigOptions, + insert_configuration: GuestPtr, + pending_handle_out: GuestPtr, + ) -> Result<(), Error> { + let store = self.get_kv_store_key(store).unwrap().clone(); + let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string()) + .map_err(|_| KvStoreError::BadRequest)?; + let body = self.take_body(body_handle)?.read_into_vec().await?; + + let config = memory.read(insert_configuration)?; + + let config_str_or_none = |flag, str_field: GuestPtr, len_field| { + if insert_config_mask.contains(flag) { + if len_field == 0 { + return Err(Error::InvalidArgument); + } + + Ok(Some(memory.to_vec(str_field.as_array(len_field))?)) + } else { + Ok(None) + } + }; + + let mode = config.mode; + + // won't actually do anything in viceroy + // let bgf = insert_config_mask.contains(KvInsertConfigOptions::BACKGROUND_FETCH); + + let igm = if insert_config_mask.contains(KvInsertConfigOptions::IF_GENERATION_MATCH) { + Some(config.if_generation_match) + } else { + None + }; + + let meta = config_str_or_none( + KvInsertConfigOptions::METADATA, + config.metadata, + config.metadata_len, + )?; + + let ttl = if insert_config_mask.contains(KvInsertConfigOptions::TIME_TO_LIVE_SEC) { + Some(std::time::Duration::from_secs( + config.time_to_live_sec as u64, + )) + } else { + None + }; + + let fut = futures::future::ok(self.kv_insert(store, key, body, Some(mode), igm, meta, ttl)); + let task = PeekableTask::spawn(fut).await; + memory.write( + pending_handle_out, + self.insert_pending_kv_insert(PendingKvInsertTask::new(task)), + )?; + + Ok(()) + } + + async fn insert_wait( + &mut self, + memory: &mut GuestMemory<'_>, + pending_insert_handle: KvStoreInsertHandle, + kv_error_out: GuestPtr, + ) -> Result<(), Error> { + let resp = self + .take_pending_kv_insert(pending_insert_handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(_) => { + memory.write(kv_error_out, KvError::Ok)?; + Ok(()) + } + Err(e) => { + memory.write(kv_error_out, (&e).into())?; + Ok(()) + } + } + } + + async fn delete( + &mut self, + memory: &mut GuestMemory<'_>, + store: KvStoreHandle, + key: GuestPtr, + _delete_config_mask: KvDeleteConfigOptions, + _delete_configuration: GuestPtr, + pending_handle_out: GuestPtr, + ) -> Result<(), Error> { + let store = self.get_kv_store_key(store).unwrap().clone(); + let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string()) + .map_err(|_| KvStoreError::BadRequest)?; + let fut = futures::future::ok(self.kv_delete(store, key)); + let task = PeekableTask::spawn(fut).await; + memory.write( + pending_handle_out, + self.insert_pending_kv_delete(PendingKvDeleteTask::new(task)) + .into(), + )?; + Ok(()) + } + + async fn delete_wait( + &mut self, + memory: &mut GuestMemory<'_>, + pending_delete_handle: KvStoreDeleteHandle, + kv_error_out: GuestPtr, + ) -> Result<(), Error> { + let resp = self + .take_pending_kv_delete(pending_delete_handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(_) => { + memory.write(kv_error_out, KvError::Ok)?; + Ok(()) + } + Err(e) => { + memory.write(kv_error_out, (&e).into())?; + Ok(()) + } + } + } + + async fn list( + &mut self, + memory: &mut GuestMemory<'_>, + store: KvStoreHandle, + list_config_mask: KvListConfigOptions, + list_configuration: GuestPtr, + pending_handle_out: GuestPtr, + ) -> Result<(), Error> { + let store = self.get_kv_store_key(store).unwrap().clone(); + + let config = memory.read(list_configuration)?; + + let config_string_or_none = |flag, str_field: GuestPtr, len_field| { + if list_config_mask.contains(flag) { + if len_field == 0 { + return Err(Error::InvalidArgument); + } + + let byte_vec = memory.to_vec(str_field.as_array(len_field))?; + + Ok(Some( + String::from_utf8(byte_vec).map_err(|_| Error::InvalidArgument)?, + )) + } else { + Ok(None) + } + }; + + let cursor = config_string_or_none( + KvListConfigOptions::CURSOR, + config.cursor, + config.cursor_len, + )?; + + let prefix = config_string_or_none( + KvListConfigOptions::PREFIX, + config.prefix, + config.prefix_len, + )?; + + let limit = match list_config_mask.contains(KvListConfigOptions::LIMIT) { + true => Some(config.limit), + false => None, + }; + + let fut = futures::future::ok(self.kv_list(store, cursor, prefix, limit)); + let task = PeekableTask::spawn(fut).await; + memory.write( + pending_handle_out, + self.insert_pending_kv_list(PendingKvListTask::new(task)) + .into(), + )?; + Ok(()) + } + + async fn list_wait( + &mut self, + memory: &mut GuestMemory<'_>, + pending_kv_list_handle: KvStoreListHandle, + body_handle_out: GuestPtr, + kv_error_out: GuestPtr, + ) -> Result<(), Error> { + let resp = self + .take_pending_kv_list(pending_kv_list_handle.into())? + .task() + .recv() + .await?; + + match resp { + Ok(value) => { + let body_handle = self.insert_body(value.into()).into(); + + memory.write(body_handle_out, body_handle)?; + + memory.write(kv_error_out, KvError::Ok)?; + Ok(()) + } + Err(e) => { + memory.write(kv_error_out, (&e).into())?; + Ok(()) + } + } + } +} diff --git a/lib/src/wiggle_abi/obj_store_impl.rs b/lib/src/wiggle_abi/obj_store_impl.rs index ce8211d3..493f0c94 100644 --- a/lib/src/wiggle_abi/obj_store_impl.rs +++ b/lib/src/wiggle_abi/obj_store_impl.rs @@ -8,7 +8,7 @@ use { crate::{ body::Body, error::Error, - object_store::{ObjectKey, ObjectStoreError}, + object_store::{KvStoreError, ObjectKey, ObjectStoreError}, session::Session, wiggle_abi::{ fastly_object_store::FastlyObjectStore, @@ -26,8 +26,8 @@ impl FastlyObjectStore for Session { name: GuestPtr, ) -> Result { let name = memory.as_str(name)?.ok_or(Error::SharedMemory)?; - if self.object_store.store_exists(&name)? { - self.obj_store_handle(&name) + if self.kv_store.store_exists(name)? { + Ok(self.kv_store_handle(name)?.into()) } else { Err(Error::ObjectStoreError( ObjectStoreError::UnknownObjectStore(name.to_owned()), @@ -42,18 +42,18 @@ impl FastlyObjectStore for Session { key: GuestPtr, opt_body_handle_out: GuestPtr, ) -> Result<(), Error> { - let store = self.get_obj_store_key(store).unwrap(); + let store = self.get_kv_store_key(store.into()).unwrap(); let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string())?; - match self.obj_lookup(store, &key) { + match self.obj_lookup(store.clone(), key) { Ok(obj) => { - let new_handle = self.insert_body(Body::from(obj)); + let new_handle = self.insert_body(Body::from(obj.body)); memory.write(opt_body_handle_out, new_handle)?; Ok(()) } // 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(()), + Err(KvStoreError::NotFound) => Ok(()), Err(err) => Err(err.into()), } } @@ -65,10 +65,10 @@ impl FastlyObjectStore for Session { key: GuestPtr, opt_pending_body_handle_out: GuestPtr, ) -> Result<(), Error> { - let store = self.get_obj_store_key(store).unwrap(); + let store = self.get_kv_store_key(store.into()).unwrap(); let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string())?; // just create a future that's already ready - let fut = futures::future::ok(self.obj_lookup(store, &key)); + let fut = futures::future::ok(self.obj_lookup(store.clone(), key)); let task = PeekableTask::spawn(fut).await; memory.write( opt_pending_body_handle_out, @@ -91,11 +91,11 @@ impl FastlyObjectStore for Session { // proceed with the normal match from lookup() match pending_obj { Ok(obj) => { - let new_handle = self.insert_body(Body::from(obj)); + let new_handle = self.insert_body(Body::from(obj.body)); memory.write(opt_body_handle_out, new_handle)?; Ok(()) } - Err(ObjectStoreError::MissingObject) => Ok(()), + Err(KvStoreError::NotFound) => Ok(()), Err(err) => Err(err.into()), } } @@ -107,10 +107,10 @@ impl FastlyObjectStore for Session { key: GuestPtr, body_handle: BodyHandle, ) -> Result<(), Error> { - let store = self.get_obj_store_key(store).unwrap().clone(); + let store = self.get_kv_store_key(store.into()).unwrap().clone(); let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string())?; let bytes = self.take_body(body_handle)?.read_into_vec().await?; - self.obj_insert(store, key, bytes)?; + self.kv_insert(store, key, bytes, None, None, None, None)?; Ok(()) } @@ -123,14 +123,15 @@ impl FastlyObjectStore for Session { body_handle: BodyHandle, opt_pending_body_handle_out: GuestPtr, ) -> Result<(), Error> { - let store = self.get_obj_store_key(store).unwrap().clone(); + let store = self.get_kv_store_key(store.into()).unwrap().clone(); let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string())?; let bytes = self.take_body(body_handle)?.read_into_vec().await?; - let fut = futures::future::ok(self.obj_insert(store, key, bytes)); + let fut = futures::future::ok(self.kv_insert(store, key, bytes, None, None, None, None)); let task = PeekableTask::spawn(fut).await; memory.write( opt_pending_body_handle_out, - self.insert_pending_kv_insert(PendingKvInsertTask::new(task)), + self.insert_pending_kv_insert(PendingKvInsertTask::new(task)) + .into(), )?; Ok(()) } @@ -154,9 +155,9 @@ impl FastlyObjectStore for Session { key: GuestPtr, opt_pending_delete_handle_out: GuestPtr, ) -> Result<(), Error> { - let store = self.get_obj_store_key(store).unwrap().clone(); + let store = self.get_kv_store_key(store.into()).unwrap().clone(); let key = ObjectKey::new(memory.as_str(key)?.ok_or(Error::SharedMemory)?.to_string())?; - let fut = futures::future::ok(self.obj_delete(store, key)); + let fut = futures::future::ok(self.kv_delete(store, key)); let task = PeekableTask::spawn(fut).await; memory.write( opt_pending_delete_handle_out, diff --git a/lib/wit/deps/fastly/compute.wit b/lib/wit/deps/fastly/compute.wit index 1006d781..cc1076dc 100644 --- a/lib/wit/deps/fastly/compute.wit +++ b/lib/wit/deps/fastly/compute.wit @@ -726,12 +726,22 @@ interface kv-store { type delete-handle = u32; type list-handle = u32; - open: func(name: string) -> result, error>; + enum kv-status { + ok, + bad-request, + not-found, + precondition-failed, + payload-too-large, + internal-error, + too-many-requests, + } + + open: func(name: list) -> result, error>; lookup: func( store: handle, - key: string, - ) -> result; + key: list, + ) -> result; resource lookup-result { body: func() -> body-handle; @@ -741,7 +751,7 @@ interface kv-store { lookup-wait: func( handle: lookup-handle, - ) -> result, error>; + ) -> result, kv-status>, error>; enum insert-mode { overwrite, @@ -767,7 +777,7 @@ interface kv-store { insert: func( store: handle, - key: string, + key: list, body-handle: body-handle, mask: insert-config-options, config: insert-config, @@ -775,16 +785,16 @@ interface kv-store { insert-wait: func( handle: insert-handle, - ) -> result<_, error>; + ) -> result; delete: func( store: handle, - key: string, + key: list, ) -> result; delete-wait: func( handle: delete-handle, - ) -> result<_, error>; + ) -> result; enum list-mode { strong, @@ -813,7 +823,7 @@ interface kv-store { list-wait: func( handle: list-handle, - ) -> result; + ) -> result, kv-status>, error>; } /*