use std::path::Path;
use super::{
IndexOptions, IndexRegistration, best_effort_create_index, registered_root_from_response,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CreateOutcome {
Confirmed,
Conflict { existing_id: Option<String> },
NotConfirmed,
}
pub(super) fn classify_create_response(
status: reqwest::StatusCode,
body: &str,
index_id: &str,
root: &Path,
root_display: &str,
) -> CreateOutcome {
if status == reqwest::StatusCode::CONFLICT {
tracing::warn!(
"trusty-search index registration for '{index_id}' at {root_display} \
conflicts (HTTP 409); looking for the index already serving that tree"
);
return CreateOutcome::Conflict {
existing_id: colliding_index_id_from_response(body),
};
}
if !status.is_success() {
tracing::warn!("trusty-search index registration for '{index_id}' returned HTTP {status}");
return CreateOutcome::NotConfirmed;
}
match registered_root_from_response(body) {
Some(registered) if !crate::identifies_same_path(Path::new(®istered), root) => {
tracing::warn!(
"trusty-search index '{index_id}' is registered at {registered}, not at the \
requested {root_display}; withholding confirmation so the caller cannot pin \
an index that searches a different tree"
);
CreateOutcome::Conflict { existing_id: None }
}
_ => {
tracing::debug!("registered trusty-search index '{index_id}' (root={root_display})");
CreateOutcome::Confirmed
}
}
}
pub(super) fn create_and_reconcile(
base: &str,
index_id: &str,
root: &Path,
opts: IndexOptions,
) -> (String, IndexRegistration) {
match best_effort_create_index(base, index_id, root, opts) {
CreateOutcome::Confirmed => (index_id.to_string(), IndexRegistration::Confirmed),
CreateOutcome::NotConfirmed => (index_id.to_string(), IndexRegistration::NotConfirmed),
CreateOutcome::Conflict { existing_id } => {
match resolve_colliding_id(base, index_id, root, opts, existing_id) {
Some(resolved) => (resolved, IndexRegistration::Confirmed),
None => (index_id.to_string(), IndexRegistration::NotConfirmed),
}
}
}
}
fn resolve_colliding_id(
base: &str,
derived: &str,
root: &Path,
opts: IndexOptions,
existing_id: Option<String>,
) -> Option<String> {
if let Some(id) = existing_id {
tracing::info!(
"trusty-search already serves {} as index '{id}'; pinning that instead of \
the derived '{derived}' (#6864)",
root.display()
);
return Some(id);
}
if let Some(body) = fetch_index_list(base)
&& let Some(id) = index_id_serving_root(&body, root)
{
tracing::info!(
"trusty-search index '{derived}' identifies another tree; {} is registered \
as '{id}' and that is what this session pins (#6864)",
root.display()
);
return Some(id);
}
let fresh = crate::derive_checkout_index_id(root)?;
tracing::info!(
"no trusty-search index is registered for {}; registering it under the \
collision-resistant id '{fresh}' because '{derived}' names another tree (#6864)",
root.display()
);
match best_effort_create_index(base, &fresh, root, opts) {
CreateOutcome::Confirmed => Some(fresh),
CreateOutcome::Conflict { existing_id } => existing_id,
CreateOutcome::NotConfirmed => None,
}
}
pub(super) fn colliding_index_id_from_response(body: &str) -> Option<String> {
let value: serde_json::Value = serde_json::from_str(body).ok()?;
value.get("existing_id")?.as_str().map(str::to_string)
}
pub(super) fn index_id_serving_root(body: &str, root: &Path) -> Option<String> {
let value: serde_json::Value = serde_json::from_str(body).ok()?;
for entry in value.get("indexes")?.as_array()? {
let Some(registered) = entry.get("root_path").and_then(|v| v.as_str()) else {
continue;
};
if !crate::identifies_same_path(Path::new(registered), root) {
continue;
}
if let Some(id) = entry.get("id").and_then(|v| v.as_str()) {
return Some(id.to_string());
}
}
None
}
fn fetch_index_list(base: &str) -> Option<String> {
let url = format!("{}/indexes?details=true", base.trim_end_matches('/'));
let result = std::thread::spawn(move || {
let client = crate::http_client::blocking_loopback_client_builder()
.timeout(std::time::Duration::from_secs(1))
.connect_timeout(std::time::Duration::from_millis(750))
.build()?;
let resp = client.get(&url).send()?;
let status = resp.status();
let text = resp.text().unwrap_or_default();
Ok::<(reqwest::StatusCode, String), reqwest::Error>((status, text))
})
.join();
match result {
Ok(Ok((status, body))) if status.is_success() => Some(body),
Ok(Ok((status, _))) => {
tracing::warn!("trusty-search index list returned HTTP {status}");
None
}
Ok(Err(e)) => {
tracing::warn!("trusty-search index list failed: {e}");
None
}
Err(_) => {
tracing::warn!("trusty-search index list thread panicked");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_root_collision_409_carries_the_existing_id() {
let body =
r#"{"error":"root_path is already registered","existing_id":"trusty-tools-checkout"}"#;
assert_eq!(
colliding_index_id_from_response(body),
Some("trusty-tools-checkout".to_string())
);
}
#[test]
fn a_root_mismatch_409_names_no_existing_index() {
let body = r#"{"error":"index 'trusty-tools' is registered at ...","index_id":"trusty-tools",
"registered_root_path":"/Users/masa/Projects/trusty-tools",
"requested_root_path":"/Users/masa/checkout/trusty-tools"}"#;
assert_eq!(colliding_index_id_from_response(body), None);
assert_eq!(colliding_index_id_from_response("not json"), None);
}
#[test]
fn index_id_serving_root_matches_on_the_tree_not_the_id() {
let mine = std::env::temp_dir();
let body = format!(
r#"{{"indexes":[{{"id":"trusty-tools","root_path":"/nonexistent/other/trusty-tools"}},
{{"id":"trusty-tools-checkout","root_path":"{}"}}]}}"#,
mine.display()
);
assert_eq!(
index_id_serving_root(&body, &mine),
Some("trusty-tools-checkout".to_string()),
"the entry rooted at this tree is the one to pin, whatever its id"
);
}
#[test]
fn index_id_serving_root_is_none_when_no_entry_matches() {
let body = r#"{"indexes":[{"id":"api","root_path":"/nonexistent/work/api"}]}"#;
assert_eq!(
index_id_serving_root(body, Path::new("/nonexistent/work/other")),
None
);
}
#[test]
fn index_id_serving_root_tolerates_a_malformed_body() {
let root = std::env::temp_dir();
assert_eq!(index_id_serving_root("not json", &root), None);
assert_eq!(index_id_serving_root("{}", &root), None);
assert_eq!(index_id_serving_root(r#"{"indexes":"nope"}"#, &root), None);
assert_eq!(
index_id_serving_root(r#"{"indexes":[{"id":"x","root_path":null}]}"#, &root),
None
);
assert_eq!(
index_id_serving_root(
&format!(r#"{{"indexes":[{{"root_path":"{}"}}]}}"#, root.display()),
&root
),
None,
"an entry with no id cannot be pinned"
);
}
}