mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(config): retry transient config-read race on Windows (Sentry TAURI-RUST-9R) (#2807)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user