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

Add spin up --key-value option #1442

Merged
merged 1 commit into from May 4, 2023
Merged

Conversation

itowlson
Copy link
Contributor

@itowlson itowlson commented May 2, 2023

Fixes #1404.

Note this currently assumes the answer to my questions there is "default store suffices." The core logic is easy enough to extend to support non-default stores; the difficulty is only in the CLI syntax.

This PR contains (in key-value/src/host_component.rs) the greatest Rust crime I have ever committed. @dicej it would be great to get your thoughts on the right way to do this. The trouble I kept running into was that StoreManagerManager would only dispense a Store if you gave it an AppComponent. My guess is I need to add a get_for_app method to StoreManagerManager but then we will not longer be able to implement it via a function...

Also @lann does the proposed entry point for carrying out before-run host component initialisation seem reasonable? I was wondering about using builder.hooks to set an app_loaded hook. The place where hooks are currently set didn't seem to have access to the host component itself, but would it be reasonable to set a hook where we instantiate and add the key/value DynamicHostComponent (TriggerExecutorBuilder::build)?

Or are these questions two sides of the same coin, and if I set the right hook in the right place then I wouldn't need to go through StoreManagerManager at all?

@itowlson
Copy link
Contributor Author

itowlson commented May 2, 2023

Before I added the is_empty check, this made a couple of tests (spin-trigger-http tests::test_spin_httptests::test_{spin|wagi}_http and spin-redis-engine tests::test_pubsub) fail. The problem was that the SQLite provider's StoreManager::get(name) calls tokio::task::block_in_place, which is forbidden in the Tokio test runtime. This occurs because they instantiate and invoke trigger objects directly in the unit tests, not hosted in main (which uses the multi-threaded runtime and is perfectly happy). This makes me worry that there is some hidden brittleness here, but I am not sure if it is in the way the initialisation code uses the StoreManager, or in the way the tests load the trigger.

@dicej
Copy link
Contributor

dicej commented May 2, 2023

FYI, you can annotate Tokio tests using #[tokio::test(flavor = "multi_thread")], which should allow task::block_in_place to work.

@itowlson itowlson marked this pull request as ready for review May 3, 2023 01:44
@itowlson
Copy link
Contributor Author

itowlson commented May 3, 2023

This has been manually tested, but doesn't have automated tests - I'm not sure how best to go about those.

Copy link
Contributor

@dicej dicej left a comment

Choose a reason for hiding this comment

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

LGTM. Regarding testing: perhaps we could add a variation on the key_value_works test in tests/spinup_tests.rs, tests/testcases/mod.rs, and tests/testcases/key-value.

@itowlson
Copy link
Contributor Author

itowlson commented May 4, 2023

#1448 is thwarting my ability to implement the suggested test. I am thinking I will merge this as-is and open a new issue to get it under e2e test coverage.

fn get_for_init(&self) -> Option<Arc<dyn StoreManager>>; // Some stores might not support init
}

pub struct SingletonStoreManagerManager {
Copy link
Collaborator

Choose a reason for hiding this comment

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

😱

Comment on lines 120 to 121
runtime_config::key_value::build_key_value_component(&runtime_config)?,
init_data.kv,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unless I'm missing something you should be able to pass this init data into the function that creates the component here:

Suggested change
runtime_config::key_value::build_key_value_component(&runtime_config)?,
init_data.kv,
runtime_config::key_value::build_key_value_component(&runtime_config, init_data.kv)?,

and then in build_key_value_component do something like:

pub fn build_key_value_component(runtime_config: &RuntimeConfig, initial_values: Vec<(String, String)>) -> Result<KeyValueComponent> {
    let mut stores = runtime_config
        .key_value_stores()
        .context("Failed to build key-value component")?
        .collect::<HashMap<_, _>>();
    if !initial_values.is_empty() {
        if let Some(manager) = stores.get("default") {
            let store = manager.get("default").unwrap();
            for (key, value) in initial_values {
                store.set(&key, value.as_bytes())?;
            }
        } else {
            // bail? log warning?
        }
    }
    ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was kind of thinking it would be nice to have the initialisation login in the component itself rather than as a side effect in a function in the runtime_config module. But maybe I should get over myself!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator

@lann lann left a comment

Choose a reason for hiding this comment

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

I don't think the DynamicHostComponent::initialize method is necessary.

Comment on lines 117 to 121
self.loader
.add_dynamic_host_component(
&mut builder,
runtime_config::key_value::build_key_value_component(&runtime_config)?,
init_data.kv,
Copy link
Collaborator

@lann lann May 4, 2023

Choose a reason for hiding this comment

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

I was kind of thinking it would be nice to have the initialisation logic in the component itself

Sure!

Suggested change
self.loader
.add_dynamic_host_component(
&mut builder,
runtime_config::key_value::build_key_value_component(&runtime_config)?,
init_data.kv,
let component = runtime_config::key_value::build_key_value_component(&runtime_config)?;
component.set_initial_values(init_data.kv)?;
self.loader
.add_dynamic_host_component(
&mut builder,
component
impl KeyValueComponent {
  pub fn set_intitial_values ...
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

...or some variation thereof... maybe the set_initial_values call would be better off inside build_key_value_component 🤷

@@ -79,7 +79,7 @@ impl AppLoader {
///
/// This calls [`EngineBuilder::add_host_component`] for you; it should not
/// be called separately.
pub fn add_dynamic_host_component<T: Send + Sync, DHC: DynamicHostComponent>(
pub async fn add_dynamic_host_component<T: Send + Sync, DHC: DynamicHostComponent>(
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 can be removed now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aargh, good spot, thanks!

@@ -243,6 +254,16 @@ async fn warn_slow_wasm_build() {
println!();
}

// Parse the key/values passed in as `key=value` pairs.
// TODO: `spin-utils` WHEN
Copy link
Collaborator

Choose a reason for hiding this comment

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

👀

Comment on lines 260 to 264
let parts: Vec<_> = s.splitn(2, '=').collect();
if parts.len() != 2 {
bail!("Key/Values must be of the form `key=value`");
}
Ok((parts[0].to_owned(), parts[1].to_owned()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Slightly nicer:

Suggested change
let parts: Vec<_> = s.splitn(2, '=').collect();
if parts.len() != 2 {
bail!("Key/Values must be of the form `key=value`");
}
Ok((parts[0].to_owned(), parts[1].to_owned()))
let Some((key, value)) = s.split_once('=') else {
bail!("Key/Values must be of the form `key=value`");
}
Ok((key.to_owned(), value.to_owned()))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not only nicer, but more correct (if a value contains an = sign e.g. base64). But I wanted to keep it exactly the same as the deploy one (and didn't want to change deploy) so that when spin-utils came along we wouldn't need to eyeball them for subtle differences.

But yeah now I can (rebase and) move to common.

Copy link
Collaborator

@lann lann May 4, 2023

Choose a reason for hiding this comment

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

I think splitn(2, ...) makes it equivalent. (I had the exact same thought on first look)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And you are, of course, correct. Can I start today again please, this time with more tea...

Copy link
Collaborator

Choose a reason for hiding this comment

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

move to common

Ooo yeah we're probably duplicating a few of these arg parsers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How similar this is to the environment variable parser will shock you.

@@ -8,6 +8,7 @@ use crate::AppComponent;
///
/// This extends [`HostComponent`] to support per-[`AppComponent`] dynamic
/// runtime configuration.
#[async_trait::async_trait]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove?

@@ -54,6 +54,7 @@ impl HostComponent for KeyValueComponent {
}
}

#[async_trait]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove?

Signed-off-by: itowlson <ivan.towlson@fermyon.com>
@itowlson itowlson merged commit ab801b0 into fermyon:main May 4, 2023
9 checks passed
@Mossaka
Copy link
Contributor

Mossaka commented May 18, 2023

I saw there is a comment in HostComponentInitData saying that this is only for testing purposes, are you planning to remove it @itowlson ?

The new v1.2.0 release of spin has this struct as a argument to builder.build().

@itowlson
Copy link
Contributor Author

@Mossaka HostComponentInitData is for real, but the #[derive(Default)] macro is only there to support the testing infrastructure. There's no meaningful default for HostComponentInitData, it has to be determined by the CLI; but there's a bunch of testing infra that relies on being able to default things. I'll clarify the comment - thanks for flagging this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for spin up --key-value KEY,VALUE
4 participants