修复本地回环端口池耗尽
让Agent Runner与本地预览复用Linux非临时端口回退 让MCP HTTP测试夹具预留可用端口并有限重试 补充端口池耗尽排障与验证约束
This commit is contained in:
@@ -289,8 +289,8 @@ pub(crate) fn start_local_game_preview_for_project(
|
||||
));
|
||||
}
|
||||
|
||||
let listener =
|
||||
TcpListener::bind(("127.0.0.1", 0)).map_err(|error| format!("启动预览失败:{error}"))?;
|
||||
let listener = bind_loopback_listener_with_linux_fallback(root.to_string_lossy().as_ref())
|
||||
.map_err(|error| format!("启动预览失败:{error}"))?;
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.map_err(|error| format!("读取预览端口失败:{error}"))?
|
||||
|
||||
@@ -3009,18 +3009,18 @@ fn read_external_agent_runner_linux_fallback_ports(boot_id: &str) -> Option<Vec<
|
||||
))
|
||||
}
|
||||
|
||||
fn bind_external_agent_runner_listener(boot_id: &str) -> io::Result<TcpListener> {
|
||||
pub(crate) fn bind_loopback_listener_with_linux_fallback(seed: &str) -> io::Result<TcpListener> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return bind_external_agent_runner_listener_with(
|
||||
|| read_external_agent_runner_linux_fallback_ports(boot_id).unwrap_or_default(),
|
||||
|| read_external_agent_runner_linux_fallback_ports(seed).unwrap_or_default(),
|
||||
|port| TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = boot_id;
|
||||
let _ = seed;
|
||||
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
|
||||
}
|
||||
}
|
||||
@@ -3038,7 +3038,7 @@ pub(crate) fn run_external_agent_runner_server(config_dir: impl AsRef<Path>) ->
|
||||
&external_agent_runner_lock_path(&config_dir),
|
||||
&boot_id,
|
||||
)?;
|
||||
let listener = bind_external_agent_runner_listener(&boot_id)
|
||||
let listener = bind_loopback_listener_with_linux_fallback(&boot_id)
|
||||
.map_err(|error| format!("绑定 Agent Runner loopback 端口失败:{error}"))?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
|
||||
@@ -2768,26 +2768,46 @@ impl Drop for McpHttpFixtureChild {
|
||||
}
|
||||
|
||||
fn spawn_mcp_http_fixture() -> (McpHttpFixtureChild, u16) {
|
||||
let mut child = std::process::Command::new("node")
|
||||
.arg(mcp_fixture_script_path())
|
||||
.arg("http")
|
||||
.arg("0")
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.spawn()
|
||||
.expect("spawn MCP HTTP fixture");
|
||||
let stdout = child.stdout.take().expect("MCP HTTP fixture stdout");
|
||||
let mut line = String::new();
|
||||
BufReader::new(stdout)
|
||||
.read_line(&mut line)
|
||||
.expect("read MCP HTTP fixture address");
|
||||
let port = serde_json::from_str::<serde_json::Value>(&line)
|
||||
.expect("parse MCP HTTP fixture address")["port"]
|
||||
.as_u64()
|
||||
.and_then(|value| u16::try_from(value).ok())
|
||||
.expect("MCP HTTP fixture port");
|
||||
(McpHttpFixtureChild(child), port)
|
||||
const MCP_HTTP_FIXTURE_BIND_ATTEMPTS: usize = 8;
|
||||
let mut last_error = "未尝试启动".to_string();
|
||||
for _ in 0..MCP_HTTP_FIXTURE_BIND_ATTEMPTS {
|
||||
let reservation = bind_test_tcp_listener("reserve MCP HTTP fixture port");
|
||||
let requested_port = reservation
|
||||
.local_addr()
|
||||
.expect("read reserved MCP HTTP fixture port")
|
||||
.port();
|
||||
drop(reservation);
|
||||
|
||||
let mut child = std::process::Command::new("node")
|
||||
.arg(mcp_fixture_script_path())
|
||||
.arg("http")
|
||||
.arg(requested_port.to_string())
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.spawn()
|
||||
.expect("spawn MCP HTTP fixture");
|
||||
let stdout = child.stdout.take().expect("MCP HTTP fixture stdout");
|
||||
let mut line = String::new();
|
||||
match BufReader::new(stdout).read_line(&mut line) {
|
||||
Ok(0) => last_error = "Node 在报告 MCP HTTP 地址前退出".to_string(),
|
||||
Ok(_) => {
|
||||
let reported_port = serde_json::from_str::<serde_json::Value>(&line)
|
||||
.ok()
|
||||
.and_then(|value| value["port"].as_u64())
|
||||
.and_then(|value| u16::try_from(value).ok());
|
||||
if reported_port == Some(requested_port) {
|
||||
return (McpHttpFixtureChild(child), requested_port);
|
||||
}
|
||||
last_error =
|
||||
format!("Node 报告的 MCP HTTP 端口与预留端口不一致:{reported_port:?}");
|
||||
}
|
||||
Err(error) => last_error = format!("读取 MCP HTTP fixture 地址失败:{error}"),
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
panic!("启动 MCP HTTP fixture 失败:{last_error}")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -3162,11 +3162,11 @@
|
||||
|
||||
## loopback port 0 也会被临时端口池耗尽阻断
|
||||
|
||||
- 现象:旧 Runner 已停止、endpoint 连接拒绝,但新 Runner 在 `TcpListener::bind(127.0.0.1:0)` 直接返回 `Address already in use`,所有 Agent 写命令随后报“Runner 在就绪前退出”。
|
||||
- 现象:旧 Runner 已停止、endpoint 连接拒绝,但新 Runner 在 `TcpListener::bind(127.0.0.1:0)` 直接返回 `Address already in use`,所有 Agent 写命令随后报“Runner 在就绪前退出”;同一主机上的本地预览和 Node HTTP 测试夹具也会以相同方式失败。
|
||||
- 原因:port 0 仍需要内核从 `ip_local_port_range` 分配监听端口;本机 api-server 与 SpacetimeDB 的约 2.8 万双向连接占满 32768-60999 后,即使目标端口不是旧 endpoint 端口,自动分配也会失败。只看 `ss -ltn` 会漏掉占用本地端口的 established client socket。
|
||||
- 处理:先保留 port 0 正常路径;Linux 只在 `AddrInUse` 后懒读取 `ip_local_port_range / ip_unprivileged_port_start / ip_local_reserved_ports`,把候选限制在 61000-65535 高位段并排除临时范围、实际特权范围和 reserved ranges,再按随机起点尝试。不要停止用户 dev 栈,不要扫描常见服务低端口,不要使用非 loopback fallback,也不要用固定公开端口或无 token 协议绕过。
|
||||
- 验证:除纯 bind 回退、懒加载、非默认特权起点、reserved ranges、候选耗尽和范围解析单测外,还要在端口池真实耗尽的主机上启动 Runner,确认 endpoint 端口位于临时范围外、heartbeat 可读,并完成真实 Provider 任务。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/runner.rs`、`agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
|
||||
- 处理:先保留 port 0 正常路径;Linux 只在 `AddrInUse` 后懒读取 `ip_local_port_range / ip_unprivileged_port_start / ip_local_reserved_ports`,把候选限制在 61000-65535 高位段并排除临时范围、实际特权范围和 reserved ranges,再按随机起点尝试。Runner 与本地预览复用同一 loopback binder;必须交给外部测试进程监听时,先用有同等回退能力的测试 listener 预留端口并有限重试交接。不要停止用户 dev 栈,不要扫描常见服务低端口,不要使用非 loopback fallback,也不要用固定公开端口或无 token 协议绕过。
|
||||
- 验证:除纯 bind 回退、懒加载、非默认特权起点、reserved ranges、候选耗尽和范围解析单测外,还要在端口池真实耗尽的主机上启动 Runner 和本地预览,确认监听端口位于临时范围外、heartbeat 与预览内容可读,并完成真实 Provider 任务及 MCP HTTP fixture 全量回归。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/runner.rs`、`preview.rs`、`tests.rs`、`agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
|
||||
|
||||
## 旧 process record 惰性迁移不能替代 resume 主动投影
|
||||
|
||||
|
||||
Reference in New Issue
Block a user