1 Commits

Author SHA1 Message Date
3097a16465 feat: 实现配置热重载功能
- 引入热加载机制:集成 `notify-debouncer-mini` 监听 `./mocks` 目录,实现 YAML 变动自动重载。
- 优化并发控制:将 `MockRouter` 包装在 `RwLock` 中,确保 Web 线程(读)与监控线程(写)的数据安全。
- 增强系统健壮性:增加防抖处理防止频繁 IO 触发重载。
2026-01-04 11:44:18 +08:00
5 changed files with 158 additions and 45 deletions

98
Cargo.lock generated
View File

@@ -60,6 +60,12 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.10.0" version = "2.10.0"
@@ -109,6 +115,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@@ -271,12 +286,52 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inotify"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.10.0",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.16" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -335,6 +390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [ dependencies = [
"libc", "libc",
"log",
"wasi", "wasi",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -345,6 +401,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"futures-util", "futures-util",
"notify",
"notify-debouncer-mini",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
@@ -356,6 +414,42 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "notify"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.10.0",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.60.2",
]
[[package]]
name = "notify-debouncer-mini"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8"
dependencies = [
"log",
"notify",
"notify-types",
"tempfile",
]
[[package]]
name = "notify-types"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -442,7 +536,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
] ]
[[package]] [[package]]
@@ -451,7 +545,7 @@ version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.10.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",

View File

@@ -26,7 +26,8 @@ tracing-subscriber = "0.3.22"
# 性能优化:快速哈希(可选,用于路由匹配) # 性能优化:快速哈希(可选,用于路由匹配)
#dashmap = "7.0.0-rc2" #dashmap = "7.0.0-rc2"
# 热加载支持(扩展功能) # 热加载支持(扩展功能)
#notify = "8.2.0" notify = "8.2.0"
notify-debouncer-mini = "0.6.0"
# 路径处理 # 路径处理
#pathdiff = "0.2.3" #pathdiff = "0.2.3"

View File

@@ -7,6 +7,8 @@ request:
Content-Type: "application/json" Content-Type: "application/json"
Authorization: "111" Authorization: "111"
host: "127.0.0.1:8080" host: "127.0.0.1:8080"
body: >
{}
response: response:
status: 200 status: 200
headers: headers:
@@ -20,4 +22,4 @@ response:
"msg": "success" "msg": "success"
} }
settings: settings:
delay_ms: 200 # 模拟真实网络延迟 delay_ms: 2000 # 模拟真实网络延迟

View File

@@ -5,27 +5,27 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::{Arc, RwLock}; // 必须引入 RwLock
use tokio_util::io::ReaderStream; // 需在 Cargo.toml 确认有 tokio-util use tokio_util::io::ReaderStream;
use crate::router::MockRouter; use crate::router::MockRouter;
/// 共享的应用状态 /// 共享的应用状态router 现在由 RwLock 保护以支持热重载
pub struct AppState { pub struct AppState {
pub router: MockRouter, pub router: RwLock<MockRouter>,
} }
/// 全局统一请求处理函数 /// 全局统一请求处理函数
pub async fn mock_handler( pub async fn mock_handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>, // State 必须是第一个或靠前的参数
method: Method, method: Method,
headers: HeaderMap, headers: HeaderMap,
Query(params): Query<HashMap<String, String>>, Query(params): Query<HashMap<String, String>>,
req: Request<Body>, req: Request<Body>, // Request<Body> 必须是最后一个参数
) -> impl IntoResponse { ) -> impl IntoResponse {
let path = req.uri().path(); let path = req.uri().path().to_string(); // 先提取 path
// 1. 将 Axum HeaderMap 转换为简单的 HashMap 供 Router 使用 // 1. 将 Axum HeaderMap 转换为简单的 HashMap
let mut req_headers = HashMap::new(); let mut req_headers = HashMap::new();
for (name, value) in headers.iter() { for (name, value) in headers.iter() {
if let Ok(v) = value.to_str() { if let Ok(v) = value.to_str() {
@@ -33,9 +33,14 @@ pub async fn mock_handler(
} }
} }
// 2. 执行匹配逻辑 // 2. 执行匹配逻辑:先获取读锁 (Read Lock)
if let Some(rule) = state.router.match_rule(method.as_str(), path, &params, &req_headers) { let maybe_rule = {
let router = state.router.read().expect("Failed to acquire read lock");
router.match_rule(method.as_str(), &path, &params, &req_headers).cloned()
// 此处使用 .cloned() 以便尽早释放读锁,避免阻塞热重载写锁
};
if let Some(rule) = maybe_rule {
// 3. 处理模拟延迟 // 3. 处理模拟延迟
if let Some(ref settings) = rule.settings { if let Some(ref settings) = rule.settings {
if let Some(delay) = settings.delay_ms { if let Some(delay) = settings.delay_ms {
@@ -43,42 +48,33 @@ pub async fn mock_handler(
} }
} }
// 4. 构建响应基础信息 // 4. 构建响应
let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK); let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
let mut response_builder = Response::builder().status(status); let mut response_builder = Response::builder().status(status);
// 注入 YAML 定义的 Header
if let Some(ref h) = rule.response.headers { if let Some(ref h) = rule.response.headers {
for (k, v) in h { for (k, v) in h {
// println!("{}:{}",k.clone(), v.clone());
response_builder = response_builder.header(k, v); response_builder = response_builder.header(k, v);
} }
} }
// 5. 执行 Smart Body 协议逻辑 // 5. Smart Body 逻辑
if let Some(file_path) = rule.response.get_file_path() { if let Some(file_path) = rule.response.get_file_path() {
// A. 文件模式:异步打开文件并转换为流,实现低内存占用
match tokio::fs::File::open(file_path).await { match tokio::fs::File::open(file_path).await {
Ok(file) => { Ok(file) => {
let stream = ReaderStream::new(file); let stream = ReaderStream::new(file);
let body = Body::from_stream(stream); response_builder.body(Body::from_stream(stream)).unwrap()
response_builder.body(body).unwrap()
}
Err(_) => {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Mock Error: File not found at {}", file_path)))
.unwrap()
} }
Err(_) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("File not found: {}", file_path)))
.unwrap(),
} }
} else { } else {
// B. 内联模式:直接返回字符串内容
response_builder.body(Body::from(rule.response.body.clone())).unwrap() response_builder.body(Body::from(rule.response.body.clone())).unwrap()
} }
} else { } else {
println!("请求头{:?}",req_headers.clone()); // 匹配失败
// 匹配失败返回 404
Response::builder() Response::builder()
.status(StatusCode::NOT_FOUND) .status(StatusCode::NOT_FOUND)
.body(Body::from("No mock rule matched this request")) .body(Body::from("No mock rule matched this request"))

View File

@@ -1,43 +1,63 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::{Arc, RwLock};
use std::time::Duration;
use axum::{routing::any, Router}; use axum::{routing::any, Router};
use notify_debouncer_mini::{new_debouncer, notify::*};
use mock_server::loader::MockLoader; use mock_server::loader::MockLoader;
use mock_server::router::MockRouter; use mock_server::router::MockRouter;
use mock_server::handler::{mock_handler, AppState}; use mock_server::handler::{mock_handler, AppState};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// 1. 初始化日志(建议添加,方便观察加载情况)
// 需要在 Cargo.toml 添加 tracing-subscriber = "0.3"
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// 2. 递归加载所有的 YAML 配置文件
let mocks_dir = Path::new("./mocks"); let mocks_dir = Path::new("./mocks");
if !mocks_dir.exists() { if !mocks_dir.exists() {
println!("Warning: 'mocks/' directory not found. Creating it...");
std::fs::create_dir_all(mocks_dir).unwrap(); std::fs::create_dir_all(mocks_dir).unwrap();
} }
// 1. 初始加载
println!("Scanning mocks directory..."); println!("Scanning mocks directory...");
let index = MockLoader::load_all_from_dir(mocks_dir); let index = MockLoader::load_all_from_dir(mocks_dir);
// 3. 构建路由引擎并包装为共享状态
let router_engine = MockRouter::new(index);
let shared_state = Arc::new(AppState { let shared_state = Arc::new(AppState {
router: router_engine, router: RwLock::new(MockRouter::new(index)),
}); });
// 4. 配置 Axum 路由 // 2. 设置热加载监听器
// 使用 any(mock_handler) 意味着它会接管所有 HTTP 方法和所有路径的请求 let state_for_watcher = shared_state.clone();
let watch_path = mocks_dir.to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
// 200ms 防抖,防止编辑器保存文件时产生多次干扰
let mut debouncer = new_debouncer(Duration::from_millis(200), tx).unwrap();
debouncer.watcher().watch(&watch_path, RecursiveMode::Recursive).unwrap();
// 启动异步任务监听文件变动
tokio::spawn(async move {
while let Ok(res) = rx.recv() {
match res {
Ok(_) => {
println!("🔄 Detecting changes in mocks/, reloading...");
let new_index = MockLoader::load_all_from_dir(&watch_path);
// 获取写锁 (Write Lock) 更新索引
let mut writer = state_for_watcher.router.write().expect("Failed to acquire write lock");
*writer = MockRouter::new(new_index);
println!("✅ Mocks reloaded successfully.");
}
Err(e) => eprintln!("Watcher error: {:?}", e),
}
}
});
// 3. 配置 Axum 路由
let app = Router::new() let app = Router::new()
.fallback(any(mock_handler)) .fallback(any(mock_handler))
.with_state(shared_state); .with_state(shared_state);
// 5. 启动服务
let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("🚀 Rust Mock Server is running on http://{}", addr); println!("🚀 Server running at http://{}", addr);
println!("Ready to handle requests based on your YAML definitions.");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();