Skip to content
Closed
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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ jobs:
tool: cargo-nextest
- run: cargo nextest run --locked --workspace

loom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2.9.1
- run: cargo test --locked -p contextforge-gateway-rs-lib --features loom gateway::session_manager::concurrency

miri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri
- uses: Swatinem/rust-cache@v2.9.1
- run: cargo miri setup
- run: cargo miri test --locked -p contextforge-gateway-rs-lib miri_checks

build:
runs-on: ubuntu-latest
steps:
Expand Down
35 changes: 35 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions crates/contextforge-gateway-rs-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ typed-builder.workspace = true
[features]
default = []
with_tools = []
loom = []


[dev-dependencies]
Expand All @@ -64,6 +65,7 @@ axum-test = "20.0.0"
test-log = "0.2.20"
axum-server = { version = "0.8.0", features = ["tls-rustls"] }
futures.workspace = true
loom = "0.7.2"

[lints]
workspace = true
12 changes: 9 additions & 3 deletions crates/contextforge-gateway-rs-lib/src/gateway/mcp_gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::{
SessionId,
gateway::{
mcp_call_validator::InitializeCallValidator,
session_manager::SessionManager,
session_manager::{SessionManager, return_transport_entry},
session_store::{UserSession, UserSessionStore},
},
};
Expand Down Expand Up @@ -252,7 +252,8 @@ where

let mut transports = self.transports.lock().await;
for (name, svc) in backend_services {
transports.entry(BackendTransportKey::from((&name, session_id))).and_modify(|e| e.service = svc);
let key = BackendTransportKey::from((&name, session_id));
return_transport_entry(&mut transports, &key, svc, |entry| &mut entry.service);
}
drop(transports);

Expand Down Expand Up @@ -399,7 +400,8 @@ where

let mut transports = self.transports.lock().await;
for (name, svc) in backend_services {
transports.entry(BackendTransportKey::from((&name, session_id))).and_modify(|e| e.service = svc);
let key = BackendTransportKey::from((&name, session_id));
return_transport_entry(&mut transports, &key, svc, |entry| &mut entry.service);
}
drop(transports);

Expand Down Expand Up @@ -806,3 +808,7 @@ mod tests {
assert_eq!(Some(pair), split_tool_name(&tool_name, &backend_names));
}
}

#[cfg(all(test, miri))]
#[path = "miri_checks/namespace_routing.rs"]
mod miri_checks;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::sync::Arc;

use serde::Deserialize;

use super::{SessionMapping, UserSession};

#[derive(Debug, Deserialize, PartialEq, Eq)]
struct OwnedUserSession {
name: String,
principal: String,
downstream_session_id: Arc<str>,
}

fn round_trip_session_mapping(entries: &[(&str, Option<&str>)]) -> Vec<(String, Option<String>)> {
let mut mapping = SessionMapping::new();
for (backend_name, upstream_session_id) in entries {
let upstream_session_id = upstream_session_id.map(Arc::<str>::from);
mapping.push((*backend_name).to_owned(), upstream_session_id.as_ref());
}

let encoded = rmp_serde::encode::to_vec(&mapping).expect("session mapping should encode");
let decoded: SessionMapping = rmp_serde::decode::from_slice(&encoded).expect("session mapping should decode");

entries
.iter()
.map(|(backend_name, _)| {
let upstream_session_id =
decoded.get(backend_name).and_then(super::SessionMap::session).map(|id| id.to_string());
((*backend_name).to_owned(), upstream_session_id)
})
.collect()
}

fn user_session_msgpack_round_trip(principal: &str, downstream_session_id: &str) -> bool {
let session = UserSession::new(principal.to_owned(), Arc::<str>::from(downstream_session_id));
let encoded = rmp_serde::encode::to_vec(&session).expect("user session should encode");
let decoded: OwnedUserSession = rmp_serde::decode::from_slice(&encoded).expect("user session should decode");
decoded
== OwnedUserSession {
name: "UserSession".to_owned(),
principal: principal.to_owned(),
downstream_session_id: Arc::<str>::from(downstream_session_id),
}
}

#[test]
fn session_mapping_msgpack_round_trip() {
let entries = [("backend-a", Some("upstream-a")), ("backend-b", None)];
let round_tripped = round_trip_session_mapping(&entries);

assert_eq!(
round_tripped,
vec![("backend-a".to_owned(), Some("upstream-a".to_owned())), ("backend-b".to_owned(), None)]
);
}

#[test]
fn user_session_key_msgpack_round_trip() {
assert!(user_session_msgpack_round_trip("principal-a", "downstream-session-a"));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use super::{split_resource_name, split_tool_name};

fn split_tool_name_owned(tool_name: &str, backend_names: &[&str]) -> Option<(String, String)> {
split_tool_name(&tool_name, backend_names).map(|pair| (pair.backend_name.to_owned(), pair.tool_name.to_owned()))
}

fn split_resource_name_owned(resource_uri: &str, backend_names: &[&str]) -> Option<(String, String)> {
split_resource_name(&resource_uri, backend_names)
.map(|pair| (pair.backend_name.to_owned(), pair.resource_uri.to_owned()))
}

#[test]
fn longest_backend_prefix_wins_for_tools() {
let backend_names = ["counter", "counter-one"];
let parsed = split_tool_name_owned("counter-one-increment", &backend_names);
assert_eq!(parsed, Some(("counter-one".to_owned(), "increment".to_owned())));
}

#[test]
fn longest_backend_prefix_wins_for_resources() {
let backend_names = ["counter", "counter-one"];
let parsed = split_resource_name_owned("counter-one-memo://insights", &backend_names);
assert_eq!(parsed, Some(("counter-one".to_owned(), "memo://insights".to_owned())));
}

#[test]
fn missing_separator_does_not_match() {
let backend_names = ["counter-one"];
let parsed = split_tool_name_owned("counter-oneincrement", &backend_names);
assert_eq!(parsed, None);
}

#[test]
fn hyphen_and_underscore_backend_names_are_distinct() {
let backend_names = ["counter-one", "counter_one"];
let hyphenated = split_tool_name_owned("counter-one-increment", &backend_names);
let underscored = split_tool_name_owned("counter_one-increment", &backend_names);

assert_eq!(hyphenated, Some(("counter-one".to_owned(), "increment".to_owned())));
assert_eq!(underscored, Some(("counter_one".to_owned(), "increment".to_owned())));
}
Loading
Loading