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

Service chaining #2305

Merged
merged 7 commits into from
Mar 20, 2024
Merged

Service chaining #2305

merged 7 commits into from
Mar 20, 2024

Conversation

itowlson
Copy link
Contributor

@itowlson itowlson commented Feb 29, 2024

Extremely WIP and not ready for review! (This initial commit, for example, is just the core dispatch, none of the validation or precautions, and only minimally tested.)

Tasks:

  • Testing and (inevitably) fixing
    • Paths ✔️
    • Headers ✔️
    • Streaming bodies ✔️
    • Concurrency ✔️
  • host_requirements section and validation
    • spin.toml [N/A]
    • Lockfile ✔️
    • Propagation ✔️
  • Strip any Host: *.spin.internal header from routed requests ✔️
  • Headers to provide on chained requests
    • Standard Spin request headers ✔️
    • Additional headers for chaining (TBD) [N/A]

@itowlson itowlson force-pushed the service-chaining branch 2 times, most recently from 859021d to e8aad87 Compare February 29, 2024 22:25
@itowlson
Copy link
Contributor Author

itowlson commented Mar 4, 2024

@lann This has a proposal for the lockfile stuff, which declares v0 and v1 serialisation formats plus a new wrapper that handles deserialisation from either format and serialisation to the v0 or v1 format depending on whether v1 features are needed. The wrapper format is no longer Serialize or Deserialize - this was to get around code that performed serialisation or deserialisation directly, which would have bypassed the version understander, although it could be restored as a custom implementation. The PR as a whole isn't ready yet (and, in particular, the "must-understand" stuff isn't processed), but it would be good to know whether this aligns with your thinking and whether it's going to be friendly to Cloud. Thanks!

@lann
Copy link
Collaborator

lann commented Mar 5, 2024

I had been thinking that the version field could just be relaxed to allow 0 or 1 since the format is (syntactically) backward-compatible.

@itowlson
Copy link
Contributor Author

itowlson commented Mar 5, 2024

@lann That would work for deserialising, but would require custom serialisation. Is that your preference?

@lann
Copy link
Collaborator

lann commented Mar 5, 2024

Ugh serde does not make this easy does it?

@lann
Copy link
Collaborator

lann commented Mar 5, 2024

After a closer look it does seem like custom serialization might be the least-bad option here from a maintenance perspective.

@itowlson
Copy link
Contributor Author

@lann Okay I have been exploring the custom serialisation (in a different branch, which I can share if it would help) and I am measurably dubious because it feels like either:

  1. Everywhere that deserialises one of these things has to check it understands the must-understand stuff; or
  2. We also have to do custom deserialisation and check the must-understand stuff within that

I don't trust myself or anybody else to do 1 correctly. So this approach seems to leave us with custom deserialisation and serde errors, which feels like brittle work for a worse UX. I am happy to press on with custom serialisation/deserialisation if you feel it's the right thing, but I wanted to touch base and make sure you still felt it was better than the wrapper type.

@lann
Copy link
Collaborator

lann commented Mar 12, 2024

I think deserialization can be handled with a serde enum; something like:

// LockedApp {
    must_understand: Vec<MustUnderstand>,
// }

#[derive(Deserialize)
#[serde(rename_all = "snake_case")]
enum MustUnderstand {
    HostRequirements,
}

It also could make sense to have a custom #[serde(deserialize_with = "deserialize_must_understand")] for nicer error messages at some point.

@itowlson
Copy link
Contributor Author

Okay this has mostly come together except for streaming. I have a test case where the front end streams its request to the back end, and streams the response from the back. When I route the front-to-back request, it works. When I chain the two, it deadlocks.

I was SO CLOSE.

@itowlson
Copy link
Contributor Author

Okay, I have two-way streaming working. I understand why my first attempt to test it deadlocked when chained, but I don't understand why it worked when routed - so I'm still concerned, because there is a behavioural difference, and that could trip folks up if they have a working streaming interaction and attempt to chain it. Still, unblocked for now I think.

@itowlson itowlson marked this pull request as ready for review March 18, 2024 03:19
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'd be happy for someone with a better mental model of how wasi http works to review this (chain_request in particular). @dicej?

crates/loader/src/local.rs Show resolved Hide resolved
@@ -347,7 +347,8 @@ impl HandlerType {

fn set_http_origin_from_request(
store: &mut Store,
engine: &TriggerAppEngine<HttpTrigger>,
engine: &Arc<TriggerAppEngine<HttpTrigger>>,
handler: &HttpHandlerExecutor,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is an empty struct. Any particular reason to pass it around?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could instantiate it within the function I guess, but conceptually what this is saying is "I want to use the exact same engine and executor that the HTTP trigger has set up." That the executor has no state today is an implementation detail that this function shouldn't have to care about.

Really I should probably Arc it rather than cloning it, in case changes are made to the executor after I capture it...

crates/trigger-http/src/handler.rs Outdated Show resolved Hide resolved
crates/trigger-http/src/lib.rs Outdated Show resolved Hide resolved
crates/trigger-http/src/lib.rs Outdated Show resolved Hide resolved
crates/trigger-http/src/lib.rs Outdated Show resolved Hide resolved
crates/trigger-http/src/lib.rs Show resolved Hide resolved
crates/trigger-http/src/lib.rs Show resolved Hide resolved
@itowlson
Copy link
Contributor Author

@lann I'm still not happy with the LockedApp stuff. At the moment I check host requirements during deserialisation and that just doesn't work (because the spin_locked_app crate doesn't know what the caller of the deserialiser actually supports). So I am back to either:

  • A wrapper type, such that consumers of LockedApp can only get at it via a function that checks requirements; or
  • All consumers making sure to check that they can meet the host requirements after deserialisation.

We've shied away from the first option, which leaves us with the second. (Or I'm missing something. Always plausible.) For triggers I can encapsulate the check in the trigger loader - which is probably the 90% case - but anything else will rely on the programmer knowing they need to do the check. Are we okay with that? Or did you already have another plan in mind for this? grin

Signed-off-by: itowlson <[email protected]>
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.

I mostly skimmed the loader bits (assuming Lann knows those best) and focused on the trigger-http parts. Looks good to me, and it's actually simpler than I would have expected. Nice work!

@itowlson
Copy link
Contributor Author

Added a "check that the app doesn't need anything I don't provide" function and implemented it in the base trigger code. Example message: Error: This application requires the following features that are not available in this version of the 'redis' trigger: local_service_chaining

Note that because capabilities are checked at application level, this can misfire in mixed-trigger apps. A mixed app whose HTTP component allows spin.internal but whose Redis component does not will still fail saying the app as a whole needs service chaining. This isn't ideal: we need to decide if the compatibility risk of iterating on it later justifies holding this up.

cc @lann for opinioning

@itowlson
Copy link
Contributor Author

(The alternative would be for the Redis trigger not to check, which would enable the "HTTP component chains but Redis does not" scenario, but would mean the "Redis component chains" scenario would sneak through and fail when the request was made.
If we think that's better then I will just back out the last commit!)

@itowlson itowlson merged commit 46a9135 into fermyon:main Mar 20, 2024
15 checks passed
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.

4 participants