Skip to content

Commit 9cf01d0

Browse files
committed
sync: update from the development repository
Content snapshot from the private development repository. The two histories are independent by design; this commit carries the file contents, not the upstream commits.
1 parent 9537678 commit 9cf01d0

10 files changed

Lines changed: 201 additions & 25 deletions

File tree

‎CHANGELOG.en.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
---
99

10+
## [0.3.1] - 2026-08-30
11+
12+
### Fixed
13+
14+
- **A stale `/_internal/scan-roots-changed` notification could overwrite newer
15+
scan roots.** yu-server assigns a monotonic `generation` in config-write order,
16+
but the receiver never read it and overwrote its roots on every call. yu-server's
17+
own send lock orders only the sends it starts; one it gave up on (5s timeout)
18+
can still land afterwards and clobber a newer one. The handler now remembers the
19+
highest generation it applied and drops anything not strictly newer, answering
20+
`{"ok":true,"applied":false,"stale":true}`. The generation lives under the same
21+
`RwLock` as the roots so the compare-and-apply cannot interleave with another
22+
request. A request carrying no `generation` (a yu-server predating the field) is
23+
still applied unconditionally and leaves the stored generation alone.
24+
25+
---
26+
1027
## [0.3.0] - 2026-08-16
1128

1229
Verified on Hailo-10H hardware. Wires native tool-call support (HailoRT genai

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@
77

88
---
99

10+
## [0.3.1] - 2026-08-30
11+
12+
### Fixed
13+
14+
- **`/_internal/scan-roots-changed` が古い世代の通知で新しい scan roots を上書きし得た問題
15+
を修正。** yu-server は書込順に単調増加する `generation` を払っているが、受信側はこれを
16+
読まず毎回無条件に上書きしていた。yu-server 側の送信ロックは自ら開始した送信しか順序化
17+
できず、5 秒の timeout で諦めた送信は後から到着して新しい状態を潰し得る。受信側で
18+
`generation` を記憶し、既に適用した値以下の要求を落とすようにした(応答は
19+
`{"ok":true,"applied":false,"stale":true}`)。`generation` は roots と同一の
20+
`RwLock` 下に置き、比較と適用が他要求と交錯しないようにしてある。`generation` を持たない
21+
要求(本フィールド以前の yu-server)は従来どおり無条件に適用し、記憶した世代も動かさない。
22+
23+
---
24+
1025
## [0.3.0] - 2026-08-16
1126

1227
Hailo-10H 実機で検証済み。`/v1/infer/llm/generate/stream` にネイティブ tool 呼出し対応

‎Cargo.lock‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["yu-hailo-auth", "yu-hailo-infer-core", "yu-hailo-infer"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.3.0"
6+
version = "0.3.1"
77
edition = "2021"
88
license = "MIT"
99
repository = "https://github.com/eauesque/yu-hailo-infer"

‎yu-hailo-infer-core/src/yolo_postprocess.rs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@ pub fn validate_yolo_outputs(outputs: &[YoloOutputBuffer]) -> Result<(), String>
9090

9191
pub fn dequantize(data: &[u8], scale: f32, zero_point: f32, is_float32: bool) -> Vec<f32> {
9292
if is_float32 {
93-
data.chunks_exact(4)
94-
.map(|bytes| f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
93+
data.as_chunks::<4>()
94+
.0
95+
.iter()
96+
.map(|&bytes| f32::from_le_bytes(bytes))
9597
.collect()
9698
} else {
9799
data.iter()
@@ -173,7 +175,7 @@ pub fn parse_nms_output(buf: &YoloOutputBuffer) -> (Vec<[f64; 4]>, Vec<f64>, Vec
173175
let mut scores = Vec::new();
174176
let mut class_ids = Vec::new();
175177

176-
for row in values.chunks_exact(6) {
178+
for row in values.as_chunks::<6>().0 {
177179
let score = row[4] as f64;
178180
if score > 0.0 {
179181
boxes.push([row[1] as f64, row[0] as f64, row[3] as f64, row[2] as f64]);

‎yu-hailo-infer/build.rs‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ fn main() {
1919
.as_deref()
2020
.map(|d| std::path::Path::new(d).join("hailo/hailort.hpp").exists())
2121
.unwrap_or_else(|| {
22-
["/usr/local", "/usr"]
23-
.iter()
24-
.any(|prefix| std::path::Path::new(prefix).join("include/hailo/hailort.hpp").exists())
22+
["/usr/local", "/usr"].iter().any(|prefix| {
23+
std::path::Path::new(prefix)
24+
.join("include/hailo/hailort.hpp")
25+
.exists()
26+
})
2527
});
2628

2729
if header_exists {
@@ -43,10 +45,11 @@ fn main() {
4345
.unwrap_or_else(|| dir.clone())
4446
});
4547
println!("cargo:rustc-link-search=native={lib_dir}");
46-
} else if let Some(prefix) = ["/usr/local", "/usr"]
47-
.iter()
48-
.find(|prefix| std::path::Path::new(prefix).join("include/hailo/hailort.hpp").exists())
49-
{
48+
} else if let Some(prefix) = ["/usr/local", "/usr"].iter().find(|prefix| {
49+
std::path::Path::new(prefix)
50+
.join("include/hailo/hailort.hpp")
51+
.exists()
52+
}) {
5053
build.include(format!("{prefix}/include"));
5154
println!("cargo:rustc-link-search=native={prefix}/lib");
5255
}

‎yu-hailo-infer/src/hailort/speech2text.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,8 @@ impl Speech2Text {
129129
};
130130
check_status("s2t_generate_segments", status)?;
131131
let json = take_c_string(out)?;
132-
serde_json::from_str(&json).map_err(|_| {
133-
HailoRtError::InvalidMetadata("shim returned malformed segments JSON")
134-
})
132+
serde_json::from_str(&json)
133+
.map_err(|_| HailoRtError::InvalidMetadata("shim returned malformed segments JSON"))
135134
}
136135

137136
pub(crate) fn tokenize(&mut self, text: &str) -> HailoRtResult<Vec<i32>> {

‎yu-hailo-infer/src/lib.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ pub async fn run() {
4242
let state = router::AppState {
4343
started_at: std::time::Instant::now(),
4444
instance_id: contract.instance_id,
45-
scan_roots: Arc::new(RwLock::new(contract.scan_roots)),
45+
scan_roots: Arc::new(RwLock::new(router::ScanRoots {
46+
roots: contract.scan_roots,
47+
generation: 0,
48+
})),
4649
auth_token: contract.auth_token,
4750
wd_cache_dir: cli.wd_cache_dir,
4851
wd_infer: Arc::new(RwLock::new(HashMap::new())),

‎yu-hailo-infer/src/router.rs‎

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,23 @@ fn media_preprocessing_permits(reservation_bytes: u64) -> u32 {
8989
.expect("media preprocessing reservation exceeds semaphore capacity type")
9090
}
9191

92+
/// The scan roots yu-server last told us about, paired with the `generation`
93+
/// that delivered them. Both live under one lock on purpose: the
94+
/// compare-and-apply in `scan_roots_changed` must not interleave with another
95+
/// request's, and a separate counter would let two calls both pass the
96+
/// staleness check and then land in the wrong order.
97+
pub struct ScanRoots {
98+
pub roots: Vec<PathBuf>,
99+
/// Highest generation applied so far. yu-server's counter starts at 1, so
100+
/// 0 means nothing but the startup contract has been applied yet.
101+
pub generation: u64,
102+
}
103+
92104
#[derive(Clone)]
93105
pub struct AppState {
94106
pub started_at: std::time::Instant,
95107
pub instance_id: String,
96-
pub scan_roots: Arc<RwLock<Vec<PathBuf>>>,
108+
pub scan_roots: Arc<RwLock<ScanRoots>>,
97109
pub auth_token: String,
98110
pub wd_cache_dir: PathBuf,
99111
pub wd_infer: Arc<RwLock<HashMap<String, Arc<infer_core::WdInferEngine>>>>,
@@ -151,6 +163,16 @@ async fn healthz(State(state): State<AppState>) -> Json<serde_json::Value> {
151163
#[derive(Deserialize)]
152164
pub struct ScanRootsChangedRequest {
153165
pub scan_roots: Vec<String>,
166+
/// Monotonic counter yu-server assigns in config-write order. Its own send
167+
/// lock only orders the sends it starts; a send it gave up on (5s timeout)
168+
/// can still reach us after a newer one, so anything not strictly newer
169+
/// than what we already applied is dropped here.
170+
///
171+
/// Absent means a yu-server predating the field: apply unconditionally,
172+
/// which is exactly what we did before, and leave the stored generation
173+
/// alone so a mixed deployment cannot wedge us.
174+
#[serde(default)]
175+
pub generation: Option<u64>,
154176
}
155177

156178
async fn scan_roots_changed(
@@ -166,13 +188,23 @@ async fn scan_roots_changed(
166188
.into_response();
167189
}
168190

169-
let roots = body
191+
// Canonicalize before taking the lock -- it hits the filesystem.
192+
let roots: Vec<PathBuf> = body
170193
.scan_roots
171194
.into_iter()
172195
.filter_map(|path| std::fs::canonicalize(path).ok())
173196
.collect();
174-
*state.scan_roots.write().unwrap() = roots;
175-
Json(json!({"ok": true})).into_response()
197+
198+
let mut scan_roots = state.scan_roots.write().unwrap();
199+
if let Some(generation) = body.generation {
200+
if generation <= scan_roots.generation {
201+
return Json(json!({"ok": true, "applied": false, "stale": true})).into_response();
202+
}
203+
scan_roots.generation = generation;
204+
}
205+
scan_roots.roots = roots;
206+
drop(scan_roots);
207+
Json(json!({"ok": true, "applied": true})).into_response()
176208
}
177209

178210
#[derive(Deserialize)]
@@ -250,7 +282,11 @@ fn check_auth(state: &AppState, headers: &HeaderMap) -> bool {
250282

251283
fn check_scan_roots(state: &AppState, real_path: &std::path::Path) -> bool {
252284
let scan_roots = state.scan_roots.read().unwrap();
253-
!scan_roots.is_empty() && scan_roots.iter().any(|root| real_path.starts_with(root))
285+
!scan_roots.roots.is_empty()
286+
&& scan_roots
287+
.roots
288+
.iter()
289+
.any(|root| real_path.starts_with(root))
254290
}
255291

256292
fn check_model_id(model_id: &str) -> bool {
@@ -1771,7 +1807,10 @@ mod tests {
17711807
AppState {
17721808
started_at: std::time::Instant::now(),
17731809
instance_id: "test-instance".to_string(),
1774-
scan_roots: Arc::new(RwLock::new(scan_roots)),
1810+
scan_roots: Arc::new(RwLock::new(ScanRoots {
1811+
roots: scan_roots,
1812+
generation: 0,
1813+
})),
17751814
auth_token: "test-token".to_string(),
17761815
wd_cache_dir: std::env::temp_dir().join("yu-infer-test-cache"),
17771816
wd_infer: Arc::new(RwLock::new(HashMap::new())),
@@ -2083,6 +2122,100 @@ mod tests {
20832122
assert_eq!(decoded.height(), 64);
20842123
}
20852124

2125+
async fn scan_roots_changed_request(
2126+
app: axum::Router,
2127+
roots: &[&std::path::Path],
2128+
generation: Option<u64>,
2129+
) -> StatusCode {
2130+
let mut payload = json!({
2131+
"scan_roots": roots.iter().map(|p| p.to_str().unwrap()).collect::<Vec<_>>(),
2132+
});
2133+
if let Some(generation) = generation {
2134+
payload["generation"] = json!(generation);
2135+
}
2136+
app.oneshot(
2137+
Request::builder()
2138+
.method("POST")
2139+
.uri("/_internal/scan-roots-changed")
2140+
.header("content-type", "application/json")
2141+
.header("authorization", "Bearer test-token")
2142+
.body(Body::from(payload.to_string()))
2143+
.unwrap(),
2144+
)
2145+
.await
2146+
.unwrap()
2147+
.status()
2148+
}
2149+
2150+
#[tokio::test]
2151+
async fn scan_roots_changed_drops_a_generation_it_already_passed() {
2152+
let base = std::env::temp_dir().join("yu-infer-scan-roots-generation-test");
2153+
let newer = base.join("newer");
2154+
let older = base.join("older");
2155+
std::fs::create_dir_all(&newer).unwrap();
2156+
std::fs::create_dir_all(&older).unwrap();
2157+
let canonical_newer = std::fs::canonicalize(&newer).unwrap();
2158+
2159+
let state = test_state(vec![]);
2160+
2161+
// Generation 2 lands first; the send that carried generation 1 timed
2162+
// out on yu-server's side and only reaches us afterwards.
2163+
let status =
2164+
scan_roots_changed_request(build_router(state.clone()), &[&newer], Some(2)).await;
2165+
assert_eq!(status, StatusCode::OK);
2166+
let status =
2167+
scan_roots_changed_request(build_router(state.clone()), &[&older], Some(1)).await;
2168+
assert_eq!(status, StatusCode::OK);
2169+
assert_eq!(
2170+
state.scan_roots.read().unwrap().roots,
2171+
vec![canonical_newer.clone()],
2172+
"a stale generation must not clobber the newer roots"
2173+
);
2174+
2175+
// An equal generation is a replay, not news, so it is dropped too.
2176+
let status =
2177+
scan_roots_changed_request(build_router(state.clone()), &[&older], Some(2)).await;
2178+
assert_eq!(status, StatusCode::OK);
2179+
assert_eq!(
2180+
state.scan_roots.read().unwrap().roots,
2181+
vec![canonical_newer]
2182+
);
2183+
2184+
// A strictly newer generation still applies.
2185+
let status =
2186+
scan_roots_changed_request(build_router(state.clone()), &[&older], Some(3)).await;
2187+
assert_eq!(status, StatusCode::OK);
2188+
assert_eq!(
2189+
state.scan_roots.read().unwrap().roots,
2190+
vec![std::fs::canonicalize(&older).unwrap()]
2191+
);
2192+
assert_eq!(state.scan_roots.read().unwrap().generation, 3);
2193+
}
2194+
2195+
#[tokio::test]
2196+
async fn scan_roots_changed_applies_a_request_without_a_generation() {
2197+
// A yu-server predating the field must keep working, and must not
2198+
// move the stored generation.
2199+
let base = std::env::temp_dir().join("yu-infer-scan-roots-legacy-test");
2200+
let first = base.join("first");
2201+
let later = base.join("later");
2202+
std::fs::create_dir_all(&first).unwrap();
2203+
std::fs::create_dir_all(&later).unwrap();
2204+
let state = test_state(vec![]);
2205+
2206+
let status =
2207+
scan_roots_changed_request(build_router(state.clone()), &[&first], Some(7)).await;
2208+
assert_eq!(status, StatusCode::OK);
2209+
let status = scan_roots_changed_request(build_router(state.clone()), &[&later], None).await;
2210+
assert_eq!(status, StatusCode::OK);
2211+
assert_eq!(
2212+
state.scan_roots.read().unwrap().roots,
2213+
vec![std::fs::canonicalize(&later).unwrap()],
2214+
"a request without a generation must still apply"
2215+
);
2216+
assert_eq!(state.scan_roots.read().unwrap().generation, 7);
2217+
}
2218+
20862219
#[tokio::test]
20872220
async fn infer_wd_rejects_missing_auth_header() {
20882221
let app = build_router(test_state(vec![]));

‎yu-hailo-infer/src/speech2text_route.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,16 @@ mod tests {
304304

305305
use super::*;
306306
use crate::router::build_router;
307+
use crate::router::ScanRoots;
307308

308309
fn test_state(scan_roots: Vec<PathBuf>) -> AppState {
309310
AppState {
310311
started_at: std::time::Instant::now(),
311312
instance_id: "test-instance".to_string(),
312-
scan_roots: Arc::new(RwLock::new(scan_roots)),
313+
scan_roots: Arc::new(RwLock::new(ScanRoots {
314+
roots: scan_roots,
315+
generation: 0,
316+
})),
313317
auth_token: "test-token".to_string(),
314318
wd_cache_dir: std::env::temp_dir().join("yu-infer-test-cache"),
315319
wd_infer: Arc::new(RwLock::new(HashMap::new())),

0 commit comments

Comments
 (0)