fix(worker): bound configurable control request deadlines

This commit is contained in:
2026-10-10 02:15:52 +08:00
parent 47f535ea79
commit 029585739c
2 changed files with 147 additions and 5 deletions
+3
View File
@@ -131,6 +131,9 @@ GitHub 私有仓库承载,并设置有限并发;Gitea 只保存逐阶段工
再恢复工厂/Coordinator/API/publisher/Worker。生产启用需单独安排,仓库不自动部署。
请求不在线安装 Python/Java 依赖,不运行 pip/uvx,不接收自定义解释器参数/JAR/agents。
Worker 私有配置可设置 `control_timeout_ms`(1000–5000,缺省2000)作为 Coordinator
单次控制请求的期限。资源受限节点应依据实际领取/续租/停止耗时选择,并核对租约和
停止恢复;这不改变学生阶段/作业期限、重试次数或工件下载的固定2秒期限,连接仍限1秒。
Python main.py 仅标准库;Java Main.java 使用默认包 Main,最多256个有界编译class。
题目语言与预算以公开版本配置为准,旧题目和草稿保留,Rust继续明确不支持。
新版检查阶段CE/运行RE/真实限额与SystemError的区别见 [判题说明](../judge/README.md#python--java-与明确协议版本)。
+144 -5
View File
@@ -89,8 +89,19 @@ pub struct Config {
pub socket: PathBuf,
pub staging: PathBuf,
pub admission: NodeAdmission,
#[serde(default = "default_control_timeout_ms")]
pub control_timeout_ms: u64,
}
fn default_control_timeout_ms() -> u64 {
2000
}
impl Config {
fn control_timeout(&self) -> io::Result<Duration> {
(1000..=5000)
.contains(&self.control_timeout_ms)
.then(|| Duration::from_millis(self.control_timeout_ms))
.ok_or_else(error)
}
pub fn load(path: &Path) -> io::Result<Self> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let m = fs::symlink_metadata(path)?;
@@ -130,6 +141,7 @@ impl Config {
return Err(error());
}
c.admission.validate().map_err(|_| error())?;
c.control_timeout()?;
amqp_uri(&c.amqp_url)?;
Ok(c)
}
@@ -156,19 +168,32 @@ fn amqp_uri(raw: &str) -> io::Result<lapin::uri::AMQPUri> {
#[derive(Clone)]
struct Remote {
client: Client,
artifact_client: Client,
base: String,
service: String,
}
impl Remote {
fn new(c: &Config) -> io::Result<Self> {
Ok(Self {
client: Client::builder()
let timeout = c.control_timeout()?;
let build = |timeout| {
Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
.connect_timeout(Duration::from_secs(1))
.timeout(Duration::from_secs(2))
.timeout(timeout)
.build()
.map_err(|_| error())?,
.map_err(|_| error())
};
let client = build(timeout)?;
// Operator control-plane allowance never extends artifact fetches or execution budgets.
let artifact_client = if timeout == Duration::from_secs(2) {
client.clone()
} else {
build(Duration::from_secs(2))?
};
Ok(Self {
client,
artifact_client,
base: c.coordinator_url.trim_end_matches('/').into(),
service: c.service_token.expose_secret().into(),
})
@@ -348,7 +373,7 @@ fn execute(c: &Config, r: &Remote, g: Grant, stop: &AtomicBool) -> io::Result<()
return Err(error());
}
let artifacts = RemoteArtifacts {
client: r.client.clone(),
client: r.artifact_client.clone(),
base: c.platform_url.trim_end_matches('/').into(),
grant: g.clone(),
};
@@ -668,6 +693,120 @@ pub fn run(c: Config, stop: Arc<AtomicBool>) -> io::Result<()> {
#[cfg(test)]
mod tests {
use super::*;
fn config_value() -> serde_json::Value {
serde_json::json!({
"worker_id": "11111111-1111-4111-8111-111111111111",
"service_token": "a".repeat(64),
"coordinator_url": "http://127.0.0.1/",
"platform_url": "http://127.0.0.1/",
"amqp_url": "amqp://fixture:temporary@127.0.0.1/",
"socket": "/private/control.sock",
"staging": "/private/staging",
"admission": serde_json::from_slice::<serde_json::Value>(include_bytes!(
"../../../../tests/fixtures/contracts/judge-admission.valid.json"
)).unwrap()
})
}
#[test]
fn trusted_control_allowance_defaults_and_refuses_unbounded_values() {
let value = config_value();
let c: Config = serde_json::from_value(value.clone()).unwrap();
assert_eq!(c.control_timeout().unwrap(), Duration::from_secs(2));
for ms in [1000, 5000] {
let mut v = value.clone();
v["control_timeout_ms"] = ms.into();
let c: Config = serde_json::from_value(v).unwrap();
assert_eq!(c.control_timeout().unwrap(), Duration::from_millis(ms));
}
for ms in [0, 999, 5001, u64::MAX] {
let mut v = value.clone();
v["control_timeout_ms"] = ms.into();
let c: Config = serde_json::from_value(v).unwrap();
assert!(Remote::new(&c).is_err());
}
for invalid in [serde_json::Value::Null, "5000".into(), (-1).into()] {
let mut v = value.clone();
v["control_timeout_ms"] = invalid;
assert!(serde_json::from_value::<Config>(v).is_err());
}
}
#[test]
fn private_config_loader_enforces_control_allowance_before_startup() {
use std::io::{Seek, Write};
use std::os::unix::fs::OpenOptionsExt;
let path = std::env::temp_dir().join(format!("kf-worker-config-{}", uuid::Uuid::new_v4()));
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&path)
.unwrap();
for ms in [None, Some(5000), Some(0), Some(5001)] {
let mut value = config_value();
if let Some(ms) = ms {
value["control_timeout_ms"] = ms.into();
}
file.set_len(0).unwrap();
file.rewind().unwrap();
file.write_all(&serde_json::to_vec(&value).unwrap())
.unwrap();
file.flush().unwrap();
let loaded = Config::load(&path);
assert_eq!(loaded.is_ok(), matches!(ms, None | Some(5000)));
}
drop(file);
fs::remove_file(path).unwrap();
}
#[test]
fn real_delayed_control_response_does_not_extend_artifact_transport() {
use std::io::Write;
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let url = format!("http://{}/", listener.local_addr().unwrap());
let server = std::thread::spawn(move || {
for _ in 0..3 {
let end = Instant::now() + Duration::from_secs(12);
let mut connection = loop {
match listener.accept() {
Ok((connection, _)) => break connection,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
assert!(Instant::now() < end);
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => panic!("test listener: {e}"),
}
};
connection
.set_read_timeout(Some(Duration::from_secs(1)))
.unwrap();
let mut request = [0u8; 4096];
assert!(connection.read(&mut request).unwrap() > 0);
std::thread::sleep(Duration::from_secs(3));
let _ = connection.write_all(
b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}",
);
}
});
let mut value = config_value();
value["coordinator_url"] = url.clone().into();
let c: Config = serde_json::from_value(value.clone()).unwrap();
let default = Remote::new(&c).unwrap();
assert!(
default
.post("/internal/claim", &serde_json::json!({}), None)
.is_err()
);
value["control_timeout_ms"] = 5000.into();
let c: Config = serde_json::from_value(value).unwrap();
let configured = Remote::new(&c).unwrap();
let response = configured
.post("/internal/claim", &serde_json::json!({}), None)
.unwrap();
assert_eq!(Remote::bytes(response, 2).unwrap(), b"{}");
assert!(configured.artifact_client.get(url).send().is_err());
server.join().unwrap();
}
#[test]
fn versioned_manifest_is_bound_to_lease_generation_digest_and_identity() {
for (raw, version) in [