Skip to content

Commit 1a9d85e

Browse files
committed
refactor: split rust fast-time transports
Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent c9b11ef commit 1a9d85e

11 files changed

Lines changed: 1428 additions & 1336 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2025
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use axum::Router;
5+
use axum::serve::ListenerExt;
6+
use serde_json::json;
7+
use std::env;
8+
use tracing::info;
9+
use tracing::trace;
10+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
11+
12+
use crate::config::{APP_NAME, APP_VERSION, DEFAULT_BIND_ADDRESS, MCP_PROTOCOL_VERSION};
13+
use crate::rest;
14+
use crate::transports::{sse, streamable_http};
15+
16+
pub async fn run() -> anyhow::Result<()> {
17+
init_logging();
18+
19+
let bind_address =
20+
env::var("BIND_ADDRESS").unwrap_or_else(|_| DEFAULT_BIND_ADDRESS.to_string());
21+
22+
info!("{} v{} starting...", APP_NAME, APP_VERSION);
23+
info!("Binding to: {}", bind_address);
24+
25+
let tcp_listener = tokio::net::TcpListener::bind(&bind_address)
26+
.await?
27+
.tap_io(|tcp_stream| {
28+
if let Err(err) = tcp_stream.set_nodelay(true) {
29+
trace!("failed to set TCP_NODELAY on incoming connection: {err:#}");
30+
}
31+
});
32+
33+
info!("MCP endpoint: http://{}/mcp", bind_address);
34+
info!("MCP SSE: http://{}/sse", bind_address);
35+
info!(
36+
"REST API: http://{}/api/echo (POST), /api/time (GET)",
37+
bind_address
38+
);
39+
info!("Health check: http://{}/health", bind_address);
40+
info!("Version info: http://{}/version", bind_address);
41+
info!("");
42+
info!("Benchmark with:");
43+
info!(" hey -n 1000000 -c 200 -m POST -T 'application/json' \\");
44+
info!(
45+
" -d '{{\"message\":\"hello\"}}' http://{}/api/echo",
46+
bind_address
47+
);
48+
49+
axum::serve(tcp_listener, router())
50+
.with_graceful_shutdown(async move {
51+
tokio::signal::ctrl_c().await.unwrap();
52+
info!("Shutting down...");
53+
})
54+
.await?;
55+
56+
Ok(())
57+
}
58+
59+
fn init_logging() {
60+
tracing_subscriber::registry()
61+
.with(
62+
tracing_subscriber::EnvFilter::try_from_default_env()
63+
.unwrap_or_else(|_| "info".to_string().into()),
64+
)
65+
.with(tracing_subscriber::fmt::layer())
66+
.init();
67+
}
68+
69+
fn router() -> Router {
70+
Router::new()
71+
.route("/health", axum::routing::get(health_handler))
72+
.route("/version", axum::routing::get(version_handler))
73+
.route("/api/echo", axum::routing::post(rest::echo_handler))
74+
.route("/api/time", axum::routing::get(rest::time_handler))
75+
.route(
76+
"/mcp",
77+
axum::routing::post(streamable_http::handler).delete(streamable_http::delete_handler),
78+
)
79+
.route("/sse", axum::routing::get(sse::handler))
80+
.route("/messages", axum::routing::post(sse::message_handler))
81+
.route("/message", axum::routing::post(sse::message_handler))
82+
}
83+
84+
async fn health_handler() -> axum::Json<serde_json::Value> {
85+
axum::Json(json!({
86+
"status": "healthy",
87+
"server": APP_NAME,
88+
"version": APP_VERSION
89+
}))
90+
}
91+
92+
async fn version_handler() -> axum::Json<serde_json::Value> {
93+
axum::Json(json!({
94+
"name": APP_NAME,
95+
"version": APP_VERSION,
96+
"mcp_version": MCP_PROTOCOL_VERSION
97+
}))
98+
}
99+
100+
#[cfg(test)]
101+
mod tests {
102+
use super::*;
103+
104+
#[tokio::test]
105+
async fn test_version_endpoint_advertises_latest_protocol() {
106+
let version = version_handler().await;
107+
assert_eq!(version.0["mcp_version"], MCP_PROTOCOL_VERSION);
108+
}
109+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2025
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pub(crate) const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:9080";
5+
pub(crate) const APP_NAME: &str = "fast-time-server";
6+
pub(crate) const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
7+
pub(crate) const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
8+
pub(crate) const SESSION_HEADER: &str = "mcp-session-id";
9+
pub(crate) const MAX_ACTIVE_SESSIONS: usize = 10_000;
10+
pub(crate) const SSE_CHANNEL_CAPACITY: usize = 64;
11+
pub(crate) const MAX_DELAY_MS: u64 = 60_000;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2025
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use rand_distr::Distribution;
5+
use rand_distr::Normal;
6+
7+
use crate::config::MAX_DELAY_MS;
8+
9+
/// Compute the actual delay in ms, optionally sampling from a normal distribution.
10+
/// Returns the mean unchanged when stddev is None, zero, or negative.
11+
pub(crate) fn compute_delay(mean_ms: u64, stddev: Option<f64>) -> u64 {
12+
match stddev {
13+
Some(sd) if sd > 0.0 => {
14+
let dist = Normal::new(mean_ms as f64, sd)
15+
.unwrap_or_else(|_| Normal::new(mean_ms as f64, 0.0).unwrap());
16+
let sample = dist.sample(&mut rand::rng());
17+
sample.round().clamp(0.0, MAX_DELAY_MS as f64) as u64
18+
}
19+
_ => mean_ms,
20+
}
21+
}
22+
23+
pub(crate) fn validate_delay(delay: Option<u64>) -> Result<Option<u64>, &'static str> {
24+
match delay {
25+
Some(ms) if ms > MAX_DELAY_MS => Err("delay exceeds the 60000 ms limit"),
26+
value => Ok(value),
27+
}
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
34+
#[test]
35+
fn test_delay_validation_rejects_values_above_limit() {
36+
assert_eq!(validate_delay(Some(MAX_DELAY_MS)), Ok(Some(MAX_DELAY_MS)));
37+
assert!(validate_delay(Some(MAX_DELAY_MS + 1)).is_err());
38+
}
39+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// fast-time-server - Ultra-fast MCP server for performance testing
2+
//
3+
// Copyright 2025
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
mod app;
7+
mod config;
8+
mod delay;
9+
mod mcp;
10+
mod rest;
11+
mod time;
12+
mod transports;
13+
14+
pub use app::run;

0 commit comments

Comments
 (0)