Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spin-kv-memcached: init #2405

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions crates/key-value-memcached/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
90 changes: 90 additions & 0 deletions crates/key-value-memcached/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pool_size: u32,
client: OnceCell<Arc<Client>>,
}

impl KeyValueMemcached {
pub fn new(addresses: Vec<String>, pool_size: Option<u32>) -> Result<Self> {
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<Arc<dyn Store>, 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<Client>,
}

#[async_trait]
impl Store for MemcacheStore {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, 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<bool, Error> {
// 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<Vec<String>, 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()))
}
}
3 changes: 2 additions & 1 deletion crates/trigger/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -60,4 +61,4 @@ wasmtime-wasi = { workspace = true }
wasmtime-wasi-http = { workspace = true }

[dev-dependencies]
tempfile = "3.8.0"
tempfile = "3.8.0"
22 changes: 22 additions & 0 deletions crates/trigger/src/runtime_config/key_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub enum KeyValueStoreOpts {
Spin(SpinKeyValueStoreOpts),
Redis(RedisKeyValueStoreOpts),
AzureCosmos(AzureCosmosConfig),
Memcached(MemcachedKeyValueStoreOpts),
}

impl KeyValueStoreOpts {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -140,6 +142,20 @@ impl AzureCosmosConfig {
}
}

#[derive(Clone, Debug, Deserialize)]
pub struct MemcachedKeyValueStoreOpts {
pub servers: Vec<String>,
pub pool_size: Option<u32>,
}

impl MemcachedKeyValueStoreOpts {
fn build_store(&self) -> Result<KeyValueStore> {
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;

Expand Down Expand Up @@ -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(())
}
Expand Down
Loading