diff --git a/Cargo.lock b/Cargo.lock index 497ff9721..2044f0e62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1847,6 +1847,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -3713,6 +3725,20 @@ dependencies = [ "digest", ] +[[package]] +name = "memcache" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396084c5d5e2ef2c480a51933b56e9673898f14657212eebbffcbcc12c7153a" +dependencies = [ + "byteorder", + "enum_dispatch", + "openssl", + "r2d2", + "rand 0.8.5", + "url", +] + [[package]] name = "memchr" version = "2.7.1" @@ -4949,6 +4975,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.7.3" @@ -5598,6 +5635,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -6205,6 +6251,19 @@ dependencies = [ "url", ] +[[package]] +name = "spin-key-value-memcached" +version = "2.4.0-pre0" +dependencies = [ + "anyhow", + "memcache", + "spin-core", + "spin-key-value", + "spin-world", + "tokio", + "url", +] + [[package]] name = "spin-key-value-redis" version = "2.4.0-pre0" @@ -6566,6 +6625,7 @@ dependencies = [ "spin-expressions", "spin-key-value", "spin-key-value-azure", + "spin-key-value-memcached", "spin-key-value-redis", "spin-key-value-sqlite", "spin-llm", diff --git a/crates/key-value-memcached/Cargo.toml b/crates/key-value-memcached/Cargo.toml new file mode 100644 index 000000000..9347b60c3 --- /dev/null +++ b/crates/key-value-memcached/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "spin-key-value-memcached" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = "1" +spin-key-value = { path = "../key-value" } +spin-core = { path = "../core" } +spin-world = { path = "../world" } +tokio = "1" +url = "2" +memcache = "0.17" diff --git a/crates/key-value-memcached/src/lib.rs b/crates/key-value-memcached/src/lib.rs new file mode 100644 index 000000000..613715b1f --- /dev/null +++ b/crates/key-value-memcached/src/lib.rs @@ -0,0 +1,90 @@ +use anyhow::Result; +use memcache::Client; +use spin_core::async_trait; +use spin_key_value::{log_error, Error, Store, StoreManager}; +use std::sync::Arc; +use tokio::sync::OnceCell; + +const NEVER_EXPIRE: u32 = 0; + +pub struct KeyValueMemcached { + urls: Vec, + pool_size: u32, + client: OnceCell>, +} + +impl KeyValueMemcached { + pub fn new(addresses: Vec, pool_size: Option) -> Result { + Ok(Self { + pool_size: pool_size.unwrap_or(32), + urls: addresses, + client: OnceCell::new(), + }) + } +} + +#[async_trait] +impl StoreManager for KeyValueMemcached { + async fn get(&self, _name: &str) -> Result, Error> { + let client = self + .client + .get_or_try_init(|| async { + Client::with_pool_size(self.urls.clone(), self.pool_size).map(Arc::new) + }) + .await + .map_err(log_error)?; + + Ok(Arc::new(MemcacheStore { + client: client.clone(), + })) + } + + fn is_defined(&self, _store_name: &str) -> bool { + true + } +} + +struct MemcacheStore { + client: Arc, +} + +#[async_trait] +impl Store for MemcacheStore { + async fn get(&self, key: &str) -> Result>, Error> { + self.client.get(key).map_err(log_error) + } + + async fn set(&self, key: &str, value: &[u8]) -> Result<(), Error> { + self.client.set(key, value, NEVER_EXPIRE).map_err(log_error) + } + + async fn delete(&self, key: &str) -> Result<(), Error> { + self.client.delete(key).map(|_| ()).map_err(log_error) + } + + async fn exists(&self, _key: &str) -> Result { + // memcache doesn't implement an "exists" api because it isn't actually + // to check without getting the value. We require it, so implement via cas. + // memcache uses a global incrementing value for `cas` so by setting the cas + // value to zero, this should be safe in close to all cases without having + // to worry about needlessly allocating memory for the response. + // + // TODO: test how this actually interacts with the rust lib and finish + // let result = self.client.cas(key, 0, 0, 0); + // match result { + // Ok(_) => Result::Ok(true), + // Err(err) => { + // match err { + // _ => Result::Err(log_error(err)) + // } + // } + // } + Result::Err(Error::Other("not yet implemented".into())) + } + + async fn get_keys(&self) -> Result, Error> { + // memcached is a distributed store with sharded keys. It can't reasonably + // implement a `get_keys` function. + Result::Err(Error::Other("get_keys unimplemented for memcached".into())) + } +} diff --git a/crates/trigger/Cargo.toml b/crates/trigger/Cargo.toml index 47b2a708e..0f11dea2f 100644 --- a/crates/trigger/Cargo.toml +++ b/crates/trigger/Cargo.toml @@ -33,6 +33,7 @@ spin-key-value = { path = "../key-value" } spin-key-value-azure = { path = "../key-value-azure" } spin-key-value-redis = { path = "../key-value-redis" } spin-key-value-sqlite = { path = "../key-value-sqlite" } +spin-key-value-memcached = { path = "../key-value-memcached" } spin-outbound-networking = { path = "../outbound-networking" } spin-sqlite = { path = "../sqlite" } spin-sqlite-inproc = { path = "../sqlite-inproc" } @@ -60,4 +61,4 @@ wasmtime-wasi = { workspace = true } wasmtime-wasi-http = { workspace = true } [dev-dependencies] -tempfile = "3.8.0" \ No newline at end of file +tempfile = "3.8.0" diff --git a/crates/trigger/src/runtime_config/key_value.rs b/crates/trigger/src/runtime_config/key_value.rs index b186c4819..b028d3053 100644 --- a/crates/trigger/src/runtime_config/key_value.rs +++ b/crates/trigger/src/runtime_config/key_value.rs @@ -62,6 +62,7 @@ pub enum KeyValueStoreOpts { Spin(SpinKeyValueStoreOpts), Redis(RedisKeyValueStoreOpts), AzureCosmos(AzureCosmosConfig), + Memcached(MemcachedKeyValueStoreOpts), } impl KeyValueStoreOpts { @@ -74,6 +75,7 @@ impl KeyValueStoreOpts { Self::Spin(opts) => opts.build_store(config_opts), Self::Redis(opts) => opts.build_store(), Self::AzureCosmos(opts) => opts.build_store(), + Self::Memcached(opts) => opts.build_store(), } } } @@ -140,6 +142,20 @@ impl AzureCosmosConfig { } } +#[derive(Clone, Debug, Deserialize)] +pub struct MemcachedKeyValueStoreOpts { + pub servers: Vec, + pub pool_size: Option, +} + +impl MemcachedKeyValueStoreOpts { + fn build_store(&self) -> Result { + let kv = + spin_key_value_memcached::KeyValueMemcached::new(self.servers.clone(), self.pool_size)?; + Ok(Arc::new(kv)) + } +} + // Prints startup messages about the default key value store config. pub struct KeyValuePersistenceMessageHook; @@ -173,6 +189,12 @@ impl TriggerHooks for KeyValuePersistenceMessageHook { KeyValueStoreOpts::AzureCosmos(store_opts) => { println!("Storing default key-value data to Azure CosmosDB: account: {}, database: {}, container: {}", store_opts.account, store_opts.database, store_opts.container); } + KeyValueStoreOpts::Memcached(store_opts) => { + println!( + "Storing default key-value data to memcached servers: {:?}", + store_opts.servers.clone() + ) + } } Ok(()) }