Skip to content

Commit 8050ec9

Browse files
committed
Fix path handling in sqlite and kv
Signed-off-by: Ryan Levick <[email protected]>
1 parent b9ed65a commit 8050ec9

File tree

9 files changed

+169
-140
lines changed

9 files changed

+169
-140
lines changed

crates/factor-key-value-spin/src/lib.rs

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ use spin_key_value_sqlite::{DatabaseLocation, KeyValueSqlite};
1111
/// A key-value store that uses SQLite as the backend.
1212
pub struct SpinKeyValueStore {
1313
/// The base path or directory for the SQLite database file.
14-
base_path: PathBuf,
14+
base_path: Option<PathBuf>,
1515
}
1616

1717
impl SpinKeyValueStore {
1818
/// Create a new SpinKeyValueStore with the given base path.
19-
pub fn new(base_path: PathBuf) -> Self {
19+
///
20+
/// If the database directory is None, the database will always be in-memory.
21+
/// If it's `Some`, the database will be stored at the combined `base_path` and
22+
/// the `path` specified in the runtime configuration.
23+
pub fn new(base_path: Option<PathBuf>) -> Self {
2024
Self { base_path }
2125
}
2226
}
@@ -31,25 +35,14 @@ pub struct SpinKeyValueRuntimeConfig {
3135
impl SpinKeyValueRuntimeConfig {
3236
/// The default filename for the SQLite database.
3337
const DEFAULT_SPIN_STORE_FILENAME: &'static str = "sqlite_key_value.db";
34-
35-
/// Create a new runtime configuration with the given directory.
36-
///
37-
/// If the database directory is None, the database is in-memory.
38-
/// If the database directory is Some, the database is stored in a file in the given directory.
39-
pub fn default(default_database_dir: Option<PathBuf>) -> Self {
40-
let path = default_database_dir.map(|dir| dir.join(Self::DEFAULT_SPIN_STORE_FILENAME));
41-
Self { path }
42-
}
4338
}
4439

45-
/// Resolve a relative path against a base dir.
46-
///
47-
/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir.
48-
fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
49-
if path.is_absolute() {
50-
return path.to_owned();
40+
impl Default for SpinKeyValueRuntimeConfig {
41+
fn default() -> Self {
42+
Self {
43+
path: Some(PathBuf::from(Self::DEFAULT_SPIN_STORE_FILENAME)),
44+
}
5145
}
52-
base_dir.join(path)
5346
}
5447

5548
impl MakeKeyValueStore for SpinKeyValueStore {
@@ -63,16 +56,30 @@ impl MakeKeyValueStore for SpinKeyValueStore {
6356
&self,
6457
runtime_config: Self::RuntimeConfig,
6558
) -> anyhow::Result<Self::StoreManager> {
66-
let location = match runtime_config.path {
67-
Some(path) => {
68-
let path = resolve_relative_path(&path, &self.base_path);
59+
// The base path and the subpath must both be set otherwise, we default to in-memory.
60+
let location =
61+
if let (Some(base_path), Some(path)) = (&self.base_path, &runtime_config.path) {
62+
let path = resolve_relative_path(path, base_path);
6963
// Create the store's parent directory if necessary
70-
fs::create_dir_all(path.parent().unwrap())
71-
.context("Failed to create key value store")?;
64+
let parent = path.parent().unwrap();
65+
if !parent.exists() {
66+
fs::create_dir_all(parent)
67+
.context("Failed to create key value store's parent directory")?;
68+
}
7269
DatabaseLocation::Path(path)
73-
}
74-
None => DatabaseLocation::InMemory,
75-
};
70+
} else {
71+
DatabaseLocation::InMemory
72+
};
7673
Ok(KeyValueSqlite::new(location))
7774
}
7875
}
76+
77+
/// Resolve a relative path against a base dir.
78+
///
79+
/// If the path is absolute, it is returned as is. Otherwise, it is resolved against the base dir.
80+
fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
81+
if path.is_absolute() {
82+
return path.to_owned();
83+
}
84+
base_dir.join(path)
85+
}

crates/factor-key-value/src/runtime_config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub struct RuntimeConfig {
1313

1414
impl RuntimeConfig {
1515
/// Adds a store manager for the store with the given label to the runtime configuration.
16+
///
17+
/// If a store manager already exists for the given label, it will be replaced.
1618
pub fn add_store_manager(&mut self, label: String, store_manager: Arc<dyn StoreManager>) {
1719
self.store_managers.insert(label, store_manager);
1820
}

crates/factor-key-value/src/runtime_config/spin.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,20 @@ impl RuntimeConfigResolver {
6262
///
6363
/// Users must ensure that the store type for `config` has been registered with
6464
/// the resolver using [`Self::register_store_type`].
65-
pub fn add_default_store(&mut self, label: &'static str, config: StoreConfig) {
66-
self.defaults.insert(label, config);
65+
pub fn add_default_store<T>(
66+
&mut self,
67+
label: &'static str,
68+
config: T::RuntimeConfig,
69+
) -> anyhow::Result<()>
70+
where
71+
T: MakeKeyValueStore,
72+
T::RuntimeConfig: Serialize,
73+
{
74+
self.defaults.insert(
75+
label,
76+
StoreConfig::new(T::RUNTIME_CONFIG_TYPE.to_owned(), config)?,
77+
);
78+
Ok(())
6779
}
6880

6981
/// Registers a store type to the resolver.

crates/factor-key-value/tests/factor_test.rs

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,25 @@
1-
use anyhow::Context;
1+
use anyhow::Context as _;
22
use spin_factor_key_value::{
3-
runtime_config::spin::{MakeKeyValueStore, RuntimeConfigResolver, StoreConfig},
3+
runtime_config::spin::{MakeKeyValueStore, RuntimeConfigResolver},
44
KeyValueFactor, RuntimeConfig,
55
};
66
use spin_factor_key_value_redis::RedisKeyValueStore;
7-
use spin_factor_key_value_spin::{SpinKeyValueRuntimeConfig, SpinKeyValueStore};
7+
use spin_factor_key_value_spin::SpinKeyValueStore;
88
use spin_factors::{FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer, RuntimeFactors};
99
use spin_factors_test::{toml, TestEnvironment};
10+
use spin_world::v2::key_value::HostStore;
1011
use std::{collections::HashSet, sync::Arc};
1112

1213
#[derive(RuntimeFactors)]
1314
struct TestFactors {
1415
key_value: KeyValueFactor,
1516
}
1617

17-
fn default_key_value_resolver() -> anyhow::Result<(RuntimeConfigResolver, tempdir::TempDir)> {
18-
let mut test_resolver = RuntimeConfigResolver::new();
19-
test_resolver.register_store_type(SpinKeyValueStore::new(
20-
std::env::current_dir().context("failed to get current directory")?,
21-
))?;
22-
let tmp_dir = tempdir::TempDir::new("example")?;
23-
let path = tmp_dir.path().to_path_buf();
24-
let default_config = SpinKeyValueRuntimeConfig::default(Some(path));
25-
let store_config = StoreConfig::new(
26-
SpinKeyValueStore::RUNTIME_CONFIG_TYPE.to_string(),
27-
default_config,
28-
)?;
29-
test_resolver.add_default_store("default", store_config);
30-
Ok((test_resolver, tmp_dir))
31-
}
32-
3318
#[tokio::test]
3419
async fn default_key_value_works() -> anyhow::Result<()> {
35-
let (test_resolver, dir) = default_key_value_resolver()?;
20+
let mut test_resolver = RuntimeConfigResolver::new();
21+
test_resolver.register_store_type(SpinKeyValueStore::new(None))?;
22+
test_resolver.add_default_store::<SpinKeyValueStore>("default", Default::default())?;
3623
let factors = TestFactors {
3724
key_value: KeyValueFactor::new(test_resolver),
3825
};
@@ -47,16 +34,14 @@ async fn default_key_value_works() -> anyhow::Result<()> {
4734
state.key_value.allowed_stores(),
4835
&["default".into()].into_iter().collect::<HashSet<_>>()
4936
);
50-
// Ensure the database directory is created
51-
assert!(dir.path().exists());
5237
Ok(())
5338
}
5439

5540
async fn run_test_with_config_and_stores_for_label(
5641
runtime_config: Option<toml::Table>,
5742
store_types: Vec<impl MakeKeyValueStore>,
5843
labels: Vec<&str>,
59-
) -> anyhow::Result<()> {
44+
) -> anyhow::Result<TestFactorsInstanceState> {
6045
let mut test_resolver = RuntimeConfigResolver::new();
6146
for store_type in store_types {
6247
test_resolver.register_store_type(store_type)?;
@@ -79,7 +64,7 @@ async fn run_test_with_config_and_stores_for_label(
7964
state.key_value.allowed_stores().iter().collect::<Vec<_>>()
8065
);
8166

82-
Ok(())
67+
Ok(state)
8368
}
8469

8570
#[tokio::test]
@@ -94,7 +79,8 @@ async fn overridden_default_key_value_works() -> anyhow::Result<()> {
9479
vec![RedisKeyValueStore::new()],
9580
vec!["default"],
9681
)
97-
.await
82+
.await?;
83+
Ok(())
9884
}
9985

10086
#[tokio::test]
@@ -105,52 +91,69 @@ async fn custom_spin_key_value_works() -> anyhow::Result<()> {
10591
};
10692
run_test_with_config_and_stores_for_label(
10793
Some(runtime_config),
108-
vec![SpinKeyValueStore::new(
109-
std::env::current_dir().context("failed to get current directory")?,
110-
)],
94+
vec![SpinKeyValueStore::new(None)],
11195
vec!["custom"],
11296
)
113-
.await
97+
.await?;
98+
Ok(())
11499
}
115100

116-
#[tokio::test]
101+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
117102
async fn custom_spin_key_value_works_with_absolute_path() -> anyhow::Result<()> {
118103
let tmp_dir = tempdir::TempDir::new("example")?;
119-
let path = tmp_dir.path().join("custom.db");
120-
let path_str = path.to_str().unwrap();
104+
let db_path = tmp_dir.path().join("foo/custom.db");
105+
// Check that the db does not exist yet - it will exist by the end of the test
106+
assert!(!db_path.exists());
107+
108+
let path_str = db_path.to_str().unwrap();
121109
let runtime_config = toml::toml! {
122110
[key_value_store.custom]
123111
type = "spin"
124112
path = path_str
125113
};
126-
run_test_with_config_and_stores_for_label(
114+
let mut state = run_test_with_config_and_stores_for_label(
127115
Some(runtime_config),
128-
vec![SpinKeyValueStore::new(
116+
vec![SpinKeyValueStore::new(Some(
129117
std::env::current_dir().context("failed to get current directory")?,
130-
)],
118+
))],
131119
vec!["custom"],
132120
)
133121
.await?;
134-
assert!(tmp_dir.path().exists());
122+
123+
// Actually et a key since store creation is lazy
124+
let store = state.key_value.open("custom".to_owned()).await??;
125+
let _ = state.key_value.get(store, "foo".to_owned()).await??;
126+
127+
// Check that the parent has been created
128+
assert!(db_path.exists());
135129
Ok(())
136130
}
137131

138-
#[tokio::test]
132+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
139133
async fn custom_spin_key_value_works_with_relative_path() -> anyhow::Result<()> {
140134
let tmp_dir = tempdir::TempDir::new("example")?;
141-
let path = tmp_dir.path().to_owned();
135+
let db_path = tmp_dir.path().join("custom.db");
136+
// Check that the db does not exist yet - it will exist by the end of the test
137+
assert!(!db_path.exists());
138+
142139
let runtime_config = toml::toml! {
143140
[key_value_store.custom]
144141
type = "spin"
145142
path = "custom.db"
146143
};
147-
run_test_with_config_and_stores_for_label(
144+
let mut state = run_test_with_config_and_stores_for_label(
148145
Some(runtime_config),
149-
vec![SpinKeyValueStore::new(path)],
146+
vec![SpinKeyValueStore::new(Some(tmp_dir.path().to_owned()))],
150147
vec!["custom"],
151148
)
152149
.await?;
153-
assert!(tmp_dir.path().exists());
150+
151+
// Actually et a key since store creation is lazy
152+
let store = state.key_value.open("custom".to_owned()).await??;
153+
let _ = state.key_value.get(store, "foo".to_owned()).await??;
154+
155+
// Check that the correct store in the config was chosen by verifying the existence of the DB
156+
assert!(db_path.exists());
154157
Ok(())
155158
}
156159

@@ -166,64 +169,75 @@ async fn custom_redis_key_value_works() -> anyhow::Result<()> {
166169
vec![RedisKeyValueStore::new()],
167170
vec!["custom"],
168171
)
169-
.await
172+
.await?;
173+
Ok(())
170174
}
171175

172176
#[tokio::test]
173177
async fn misconfigured_spin_key_value_fails() -> anyhow::Result<()> {
178+
let tmp_dir = tempdir::TempDir::new("example")?;
174179
let runtime_config = toml::toml! {
175180
[key_value_store.custom]
176181
type = "spin"
177182
path = "/$$&/bad/path/foo.db"
178183
};
179-
assert!(run_test_with_config_and_stores_for_label(
184+
let result = run_test_with_config_and_stores_for_label(
180185
Some(runtime_config),
181-
vec![SpinKeyValueStore::new(
182-
std::env::current_dir().context("failed to get current directory")?
183-
)],
184-
vec!["custom"]
186+
vec![SpinKeyValueStore::new(Some(tmp_dir.path().to_owned()))],
187+
vec!["custom"],
185188
)
186-
.await
187-
.is_err());
189+
.await;
190+
// TODO(rylev): This only fails on my machine due to a read-only file system error.
191+
// We should consider adding a check for the error message.
192+
assert!(result.is_err());
188193
Ok(())
189194
}
190195

191-
#[tokio::test]
192-
async fn multiple_custom_key_value_uses_first_store() -> anyhow::Result<()> {
196+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
197+
// TODO(rylev): consider removing this test as it is really only a consequence of
198+
// toml deserialization and not a feature of the key-value store.
199+
async fn multiple_custom_key_value_uses_second_store() -> anyhow::Result<()> {
193200
let tmp_dir = tempdir::TempDir::new("example")?;
201+
let db_path = tmp_dir.path().join("custom.db");
202+
// Check that the db does not exist yet - it will exist by the end of the test
203+
assert!(!db_path.exists());
204+
194205
let mut test_resolver = RuntimeConfigResolver::new();
195206
test_resolver.register_store_type(RedisKeyValueStore::new())?;
196-
test_resolver.register_store_type(SpinKeyValueStore::new(tmp_dir.path().to_owned()))?;
207+
test_resolver.register_store_type(SpinKeyValueStore::new(Some(tmp_dir.path().to_owned())))?;
197208
let test_resolver = Arc::new(test_resolver);
198209
let factors = TestFactors {
199210
key_value: KeyValueFactor::new(test_resolver.clone()),
200211
};
212+
let runtime_config = toml::toml! {
213+
[key_value_store.custom]
214+
type = "redis"
215+
url = "redis://localhost:6379"
216+
217+
[key_value_store.custom]
218+
type = "spin"
219+
path = "custom.db"
220+
221+
};
201222
let env = TestEnvironment::new(factors)
202223
.extend_manifest(toml! {
203224
[component.test-component]
204225
source = "does-not-exist.wasm"
205226
key_value_stores = ["custom"]
206227
})
207-
.runtime_config(TomlConfig::new(
208-
test_resolver,
209-
Some(toml::toml! {
210-
[key_value_store.custom]
211-
type = "spin"
212-
path = "custom.db"
228+
.runtime_config(TomlConfig::new(test_resolver, Some(runtime_config)))?;
229+
let mut state = env.build_instance_state().await?;
213230

214-
[key_value_store.custom]
215-
type = "redis"
216-
url = "redis://localhost:6379"
217-
}),
218-
))?;
219-
let state = env.build_instance_state().await?;
231+
// Actually et a key since store creation is lazy
232+
let store = state.key_value.open("custom".to_owned()).await??;
233+
let _ = state.key_value.get(store, "foo".to_owned()).await??;
220234

221235
assert_eq!(
222236
state.key_value.allowed_stores(),
223237
&["custom".into()].into_iter().collect::<HashSet<_>>()
224238
);
225-
// Check that the first store in the config was chosen by verifying the existence of the DB directory
226-
assert!(tmp_dir.path().exists());
239+
// Check that the correct store in the config was chosen by verifying the existence of the DB
240+
assert!(db_path.exists());
227241
Ok(())
228242
}
229243

0 commit comments

Comments
 (0)