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

Fix path handling in sqlite and kv #2756

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
59 changes: 33 additions & 26 deletions crates/factor-key-value-spin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ use spin_key_value_sqlite::{DatabaseLocation, KeyValueSqlite};
/// A key-value store that uses SQLite as the backend.
pub struct SpinKeyValueStore {
/// The base path or directory for the SQLite database file.
base_path: PathBuf,
base_path: Option<PathBuf>,
}

impl SpinKeyValueStore {
/// Create a new SpinKeyValueStore with the given base path.
pub fn new(base_path: PathBuf) -> Self {
///
/// If the database directory is None, the database will always be in-memory.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this isn't quite right. If the directory is None and there is an absolute path I would expect that to work fine. This might be important for SpinKube.

When the directory is None and the path is relative I'm not sure what the current behavior is but it seems like it would be least surprising for that to be a fatal error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would mean that runtimes where --state-dir="" will error when running apps that have runtime configs with key-value stores with relative paths. Do we want that to be the case? It seems like we would want to err on the side of running apps even if the exact behavior is slightly more confusing when considering all the possible combinations.

Copy link
Collaborator Author

@rylev rylev Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed it so that absolute paths are still allowed when --state-dir is unset. However, this does mean a runtime can never fully force in-memory databases. 3d4fd06?w=0

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err on the side of running apps even if the exact behavior is slightly more confusing

I'm fine with that for spin up but probably not for other Spin runtimes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'll error for now - probably easier in the future to make it less restrictive than it is to make it more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a runtime can never fully force in-memory databases

If I'm reading main correctly it looks like you can omit path to force an in-memory database.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but that's in the runtime config not in the actual running of the runtime engine. Are we assuming that whoever is controlling the runtime is the same as whoever is setting the runtime config? We're requiring coordination between whoever is setting the runtime config and whoever is actually running the runtime binary.

/// If it's `Some`, the database will be stored at the combined `base_path` and
/// the `path` specified in the runtime configuration.
pub fn new(base_path: Option<PathBuf>) -> Self {
Self { base_path }
}
}
Expand All @@ -31,25 +35,14 @@ pub struct SpinKeyValueRuntimeConfig {
impl SpinKeyValueRuntimeConfig {
/// The default filename for the SQLite database.
const DEFAULT_SPIN_STORE_FILENAME: &'static str = "sqlite_key_value.db";

/// Create a new runtime configuration with the given directory.
///
/// If the database directory is None, the database is in-memory.
/// If the database directory is Some, the database is stored in a file in the given directory.
pub fn default(default_database_dir: Option<PathBuf>) -> Self {
let path = default_database_dir.map(|dir| dir.join(Self::DEFAULT_SPIN_STORE_FILENAME));
Self { path }
}
}

/// Resolve a relative path against a base dir.
///
/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir.
fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
if path.is_absolute() {
return path.to_owned();
impl Default for SpinKeyValueRuntimeConfig {
fn default() -> Self {
Self {
path: Some(PathBuf::from(Self::DEFAULT_SPIN_STORE_FILENAME)),
}
}
base_dir.join(path)
}

impl MakeKeyValueStore for SpinKeyValueStore {
Expand All @@ -63,16 +56,30 @@ impl MakeKeyValueStore for SpinKeyValueStore {
&self,
runtime_config: Self::RuntimeConfig,
) -> anyhow::Result<Self::StoreManager> {
let location = match runtime_config.path {
Some(path) => {
let path = resolve_relative_path(&path, &self.base_path);
// The base path and the subpath must both be set otherwise, we default to in-memory.
let location =
if let (Some(base_path), Some(path)) = (&self.base_path, &runtime_config.path) {
let path = resolve_relative_path(path, base_path);
// Create the store's parent directory if necessary
fs::create_dir_all(path.parent().unwrap())
.context("Failed to create key value store")?;
let parent = path.parent().unwrap();
if !parent.exists() {
fs::create_dir_all(parent)
.context("Failed to create key value store's parent directory")?;
}
DatabaseLocation::Path(path)
}
None => DatabaseLocation::InMemory,
};
} else {
DatabaseLocation::InMemory
};
Ok(KeyValueSqlite::new(location))
}
}

/// Resolve a relative path against a base dir.
///
/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir.
fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
if path.is_absolute() {
return path.to_owned();
}
base_dir.join(path)
}
2 changes: 2 additions & 0 deletions crates/factor-key-value/src/runtime_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct RuntimeConfig {

impl RuntimeConfig {
/// Adds a store manager for the store with the given label to the runtime configuration.
///
/// If a store manager already exists for the given label, it will be replaced.
pub fn add_store_manager(&mut self, label: String, store_manager: Arc<dyn StoreManager>) {
self.store_managers.insert(label, store_manager);
}
Expand Down
16 changes: 14 additions & 2 deletions crates/factor-key-value/src/runtime_config/spin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,20 @@ impl RuntimeConfigResolver {
///
/// Users must ensure that the store type for `config` has been registered with
/// the resolver using [`Self::register_store_type`].
pub fn add_default_store(&mut self, label: &'static str, config: StoreConfig) {
self.defaults.insert(label, config);
pub fn add_default_store<T>(
&mut self,
label: &'static str,
config: T::RuntimeConfig,
) -> anyhow::Result<()>
where
T: MakeKeyValueStore,
T::RuntimeConfig: Serialize,
{
self.defaults.insert(
label,
StoreConfig::new(T::RUNTIME_CONFIG_TYPE.to_owned(), config)?,
);
Ok(())
}

/// Registers a store type to the resolver.
Expand Down
146 changes: 80 additions & 66 deletions crates/factor-key-value/tests/factor_test.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
use anyhow::Context;
use anyhow::Context as _;
use spin_factor_key_value::{
runtime_config::spin::{MakeKeyValueStore, RuntimeConfigResolver, StoreConfig},
runtime_config::spin::{MakeKeyValueStore, RuntimeConfigResolver},
KeyValueFactor, RuntimeConfig,
};
use spin_factor_key_value_redis::RedisKeyValueStore;
use spin_factor_key_value_spin::{SpinKeyValueRuntimeConfig, SpinKeyValueStore};
use spin_factor_key_value_spin::SpinKeyValueStore;
use spin_factors::{FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer, RuntimeFactors};
use spin_factors_test::{toml, TestEnvironment};
use spin_world::v2::key_value::HostStore;
use std::{collections::HashSet, sync::Arc};

#[derive(RuntimeFactors)]
struct TestFactors {
key_value: KeyValueFactor,
}

fn default_key_value_resolver() -> anyhow::Result<(RuntimeConfigResolver, tempdir::TempDir)> {
let mut test_resolver = RuntimeConfigResolver::new();
test_resolver.register_store_type(SpinKeyValueStore::new(
std::env::current_dir().context("failed to get current directory")?,
))?;
let tmp_dir = tempdir::TempDir::new("example")?;
let path = tmp_dir.path().to_path_buf();
let default_config = SpinKeyValueRuntimeConfig::default(Some(path));
let store_config = StoreConfig::new(
SpinKeyValueStore::RUNTIME_CONFIG_TYPE.to_string(),
default_config,
)?;
test_resolver.add_default_store("default", store_config);
Ok((test_resolver, tmp_dir))
}

#[tokio::test]
async fn default_key_value_works() -> anyhow::Result<()> {
let (test_resolver, dir) = default_key_value_resolver()?;
let mut test_resolver = RuntimeConfigResolver::new();
test_resolver.register_store_type(SpinKeyValueStore::new(None))?;
test_resolver.add_default_store::<SpinKeyValueStore>("default", Default::default())?;
let factors = TestFactors {
key_value: KeyValueFactor::new(test_resolver),
};
Expand All @@ -47,16 +34,14 @@ async fn default_key_value_works() -> anyhow::Result<()> {
state.key_value.allowed_stores(),
&["default".into()].into_iter().collect::<HashSet<_>>()
);
// Ensure the database directory is created
assert!(dir.path().exists());
Ok(())
}

async fn run_test_with_config_and_stores_for_label(
runtime_config: Option<toml::Table>,
store_types: Vec<impl MakeKeyValueStore>,
labels: Vec<&str>,
) -> anyhow::Result<()> {
) -> anyhow::Result<TestFactorsInstanceState> {
let mut test_resolver = RuntimeConfigResolver::new();
for store_type in store_types {
test_resolver.register_store_type(store_type)?;
Expand All @@ -79,7 +64,7 @@ async fn run_test_with_config_and_stores_for_label(
state.key_value.allowed_stores().iter().collect::<Vec<_>>()
);

Ok(())
Ok(state)
}

#[tokio::test]
Expand All @@ -94,7 +79,8 @@ async fn overridden_default_key_value_works() -> anyhow::Result<()> {
vec![RedisKeyValueStore::new()],
vec!["default"],
)
.await
.await?;
Ok(())
}

#[tokio::test]
Expand All @@ -105,52 +91,69 @@ async fn custom_spin_key_value_works() -> anyhow::Result<()> {
};
run_test_with_config_and_stores_for_label(
Some(runtime_config),
vec![SpinKeyValueStore::new(
std::env::current_dir().context("failed to get current directory")?,
)],
vec![SpinKeyValueStore::new(None)],
vec!["custom"],
)
.await
.await?;
Ok(())
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn custom_spin_key_value_works_with_absolute_path() -> anyhow::Result<()> {
let tmp_dir = tempdir::TempDir::new("example")?;
let path = tmp_dir.path().join("custom.db");
let path_str = path.to_str().unwrap();
let db_path = tmp_dir.path().join("foo/custom.db");
// Check that the db does not exist yet - it will exist by the end of the test
assert!(!db_path.exists());

let path_str = db_path.to_str().unwrap();
let runtime_config = toml::toml! {
[key_value_store.custom]
type = "spin"
path = path_str
};
run_test_with_config_and_stores_for_label(
let mut state = run_test_with_config_and_stores_for_label(
Some(runtime_config),
vec![SpinKeyValueStore::new(
vec![SpinKeyValueStore::new(Some(
std::env::current_dir().context("failed to get current directory")?,
)],
))],
vec!["custom"],
)
.await?;
assert!(tmp_dir.path().exists());

// Actually et a key since store creation is lazy
let store = state.key_value.open("custom".to_owned()).await??;
let _ = state.key_value.get(store, "foo".to_owned()).await??;

// Check that the parent has been created
assert!(db_path.exists());
Ok(())
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn custom_spin_key_value_works_with_relative_path() -> anyhow::Result<()> {
let tmp_dir = tempdir::TempDir::new("example")?;
let path = tmp_dir.path().to_owned();
let db_path = tmp_dir.path().join("custom.db");
// Check that the db does not exist yet - it will exist by the end of the test
assert!(!db_path.exists());

let runtime_config = toml::toml! {
[key_value_store.custom]
type = "spin"
path = "custom.db"
};
run_test_with_config_and_stores_for_label(
let mut state = run_test_with_config_and_stores_for_label(
Some(runtime_config),
vec![SpinKeyValueStore::new(path)],
vec![SpinKeyValueStore::new(Some(tmp_dir.path().to_owned()))],
vec!["custom"],
)
.await?;
assert!(tmp_dir.path().exists());

// Actually et a key since store creation is lazy
let store = state.key_value.open("custom".to_owned()).await??;
let _ = state.key_value.get(store, "foo".to_owned()).await??;

// Check that the correct store in the config was chosen by verifying the existence of the DB
assert!(db_path.exists());
Ok(())
}

Expand All @@ -166,64 +169,75 @@ async fn custom_redis_key_value_works() -> anyhow::Result<()> {
vec![RedisKeyValueStore::new()],
vec!["custom"],
)
.await
.await?;
Ok(())
}

#[tokio::test]
async fn misconfigured_spin_key_value_fails() -> anyhow::Result<()> {
let tmp_dir = tempdir::TempDir::new("example")?;
let runtime_config = toml::toml! {
[key_value_store.custom]
type = "spin"
path = "/$$&/bad/path/foo.db"
};
assert!(run_test_with_config_and_stores_for_label(
let result = run_test_with_config_and_stores_for_label(
Some(runtime_config),
vec![SpinKeyValueStore::new(
std::env::current_dir().context("failed to get current directory")?
)],
vec!["custom"]
vec![SpinKeyValueStore::new(Some(tmp_dir.path().to_owned()))],
vec!["custom"],
)
.await
.is_err());
.await;
// TODO(rylev): This only fails on my machine due to a read-only file system error.
// We should consider adding a check for the error message.
assert!(result.is_err());
Ok(())
}

#[tokio::test]
async fn multiple_custom_key_value_uses_first_store() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
// TODO(rylev): consider removing this test as it is really only a consequence of
// toml deserialization and not a feature of the key-value store.
async fn multiple_custom_key_value_uses_second_store() -> anyhow::Result<()> {
let tmp_dir = tempdir::TempDir::new("example")?;
let db_path = tmp_dir.path().join("custom.db");
// Check that the db does not exist yet - it will exist by the end of the test
assert!(!db_path.exists());

let mut test_resolver = RuntimeConfigResolver::new();
test_resolver.register_store_type(RedisKeyValueStore::new())?;
test_resolver.register_store_type(SpinKeyValueStore::new(tmp_dir.path().to_owned()))?;
test_resolver.register_store_type(SpinKeyValueStore::new(Some(tmp_dir.path().to_owned())))?;
let test_resolver = Arc::new(test_resolver);
let factors = TestFactors {
key_value: KeyValueFactor::new(test_resolver.clone()),
};
let runtime_config = toml::toml! {
[key_value_store.custom]
type = "redis"
url = "redis://localhost:6379"

[key_value_store.custom]
type = "spin"
path = "custom.db"

};
let env = TestEnvironment::new(factors)
.extend_manifest(toml! {
[component.test-component]
source = "does-not-exist.wasm"
key_value_stores = ["custom"]
})
.runtime_config(TomlConfig::new(
test_resolver,
Some(toml::toml! {
[key_value_store.custom]
type = "spin"
path = "custom.db"
.runtime_config(TomlConfig::new(test_resolver, Some(runtime_config)))?;
let mut state = env.build_instance_state().await?;

[key_value_store.custom]
type = "redis"
url = "redis://localhost:6379"
}),
))?;
let state = env.build_instance_state().await?;
// Actually et a key since store creation is lazy
let store = state.key_value.open("custom".to_owned()).await??;
let _ = state.key_value.get(store, "foo".to_owned()).await??;

assert_eq!(
state.key_value.allowed_stores(),
&["custom".into()].into_iter().collect::<HashSet<_>>()
);
// Check that the first store in the config was chosen by verifying the existence of the DB directory
assert!(tmp_dir.path().exists());
// Check that the correct store in the config was chosen by verifying the existence of the DB
assert!(db_path.exists());
Ok(())
}

Expand Down
Loading
Loading