From 629369b100950412028cbc5dbd0215cb38b462fe Mon Sep 17 00:00:00 2001 From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Date: Thu, 28 May 2026 17:43:51 +0530 Subject: [PATCH] fix(config): retry transient config-read race on Windows (Sentry TAURI-RUST-9R) (#2807) --- src/openhuman/config/schema/load.rs | 29 +++++++++-- src/openhuman/config/schema/load_tests.rs | 62 +++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/openhuman/config/schema/load.rs b/src/openhuman/config/schema/load.rs index 4746c540a..f7924f9f7 100644 --- a/src/openhuman/config/schema/load.rs +++ b/src/openhuman/config/schema/load.rs @@ -1161,9 +1161,32 @@ impl Config { } } - let contents = fs::read_to_string(&config_path) - .await - .context("Failed to read config file")?; + // Sentry OPENHUMAN-TAURI-9R (~8k events, Windows): this read can + // race the atomic-replace in `Config::save` (temp file → + // `fs::rename` over `config_path`). On Windows the in-flight + // rename / a transient AV or indexer handle makes the read fail + // with ERROR_SHARING_VIOLATION (32) / ERROR_ACCESS_DENIED (5) / + // ERROR_DELETE_PENDING (303) even though `config_path.exists()` + // just returned true. `inference_status` polls `load_config` + // frequently, so each coincidence with a save produced one + // "Failed to read config file" event. Retry on the transient + // Windows locking codes (the same class `retry_with_backoff_async` + // already handles for the auth-profile + team_get_usage paths) so + // the read succeeds once the writer releases its handle. + // `is_transient_fs_error` is `false` for every non-Windows error + // (and for NotFound on Windows), so this is a no-op on + // macOS/Linux and never masks a genuinely-unreadable config. + let contents = crate::openhuman::util::retry_with_backoff_async( + "read config file", + 5, + 20, + || async { + fs::read_to_string(&config_path).await.with_context(|| { + format!("Failed to read config file: {}", config_path.display()) + }) + }, + ) + .await?; let (mut config, config_was_corrupted) = parse_config_with_recovery(&config_path, &contents).await; config.config_path = config_path.clone(); diff --git a/src/openhuman/config/schema/load_tests.rs b/src/openhuman/config/schema/load_tests.rs index ff177ee37..ff202fbe6 100644 --- a/src/openhuman/config/schema/load_tests.rs +++ b/src/openhuman/config/schema/load_tests.rs @@ -1468,6 +1468,68 @@ default_temperature = 0.7 ); } +#[tokio::test] +async fn load_or_init_reads_valid_config_through_retry_wrapper() { + // OPENHUMAN-TAURI-9R regression: the config read is wrapped in + // `retry_with_backoff_async`. Confirm the happy path is untouched — + // a present, readable, valid config loads on the first attempt with + // no behavior change from the wrapper. + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + write_file( + &root.join("config.toml"), + r#"default_model = "gpt-through-retry" +default_temperature = 0.5 +"#, + ) + .await; + + let config = load_or_init_for_workspace(root).await; + + assert_eq!( + config.default_model.as_deref(), + Some("gpt-through-retry"), + "valid config must load on first attempt through the retry wrapper" + ); +} + +#[tokio::test] +async fn load_or_init_read_failure_embeds_path_in_error_context() { + // OPENHUMAN-TAURI-9R (~8k events, Windows): the read at the + // `config_path.exists()` branch raced `Config::save`'s atomic rename + // and surfaced the opaque "Failed to read config file" with no path + // or underlying cause. The fix retries transient Windows locking + // errors AND embeds the config path in the context so any residual + // non-transient failure is triageable in Sentry. + // + // Simulate a non-transient read failure portably by placing a + // *directory* at the config path: `exists()` is true (so we enter the + // read branch), but `read_to_string` fails with EISDIR (unix) / + // ERROR_ACCESS_DENIED (windows) — neither is classified transient by + // `is_transient_fs_error`, so the retry bails immediately and returns + // the path-embedded context. + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let config_path = root.join("config.toml"); + std::fs::create_dir(&config_path).unwrap(); + + let env = MapEnv::default().with("OPENHUMAN_WORKSPACE", root.to_str().unwrap()); + let err = Config::load_or_init_with_env_lookup(root, &root.join("workspace"), &env) + .await + .expect_err("reading a directory as config.toml must fail"); + + let msg = format!("{err:#}"); + assert!( + msg.contains("Failed to read config file"), + "error must carry the read-failure context: {msg}" + ); + assert!( + msg.contains("config.toml"), + "error context must embed the config path so Sentry titles are triageable: {msg}" + ); +} + #[test] fn redact_url_strips_basic_auth_and_query() { let out = redact_url_for_log(