Recover poisoned shared-state locks

This commit is contained in:
Georg Bauer
2026-07-25 13:12:49 +02:00
parent 292ee82af5
commit c77ef47cfc
2 changed files with 64 additions and 22 deletions

View File

@@ -362,6 +362,15 @@ fn optional_text(value: &str) -> Option<String> {
(!value.is_empty()).then(|| value.to_owned()) (!value.is_empty()).then(|| value.to_owned())
} }
fn update_runtime_preferences(
runtime_preferences: &RwLock<AppPreferences>,
preferences: &AppPreferences,
) {
*runtime_preferences
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = preferences.clone();
}
impl App { impl App {
pub(super) fn open_preferences(&mut self) { pub(super) fn open_preferences(&mut self) {
if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder { if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder {
@@ -457,9 +466,7 @@ impl App {
Ok(preferences) => { Ok(preferences) => {
self.preferences = preferences; self.preferences = preferences;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if let Ok(mut runtime_preferences) = self.runtime_preferences.write() { update_runtime_preferences(&self.runtime_preferences, &self.preferences);
*runtime_preferences = self.preferences.clone();
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if let Some(endpoint) = pending_endpoint { if let Some(endpoint) = pending_endpoint {
self._endpoint = Some(endpoint); self._endpoint = Some(endpoint);
@@ -474,3 +481,33 @@ impl App {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_preferences_update_after_lock_poisoning() {
let runtime = Arc::new(RwLock::new(AppPreferences::default()));
let poisoned = Arc::clone(&runtime);
let _ = std::thread::spawn(move || {
let _guard = poisoned.write().unwrap();
panic!("poison preference lock");
})
.join();
let preferences = AppPreferences {
endpoint_port: 4567,
..AppPreferences::default()
};
update_runtime_preferences(&runtime, &preferences);
assert_eq!(
runtime
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.endpoint_port,
4567
);
}
}

View File

@@ -410,16 +410,17 @@ pub(super) fn validate_tool_results(
if !matches!(protocol, Protocol::Anthropic | Protocol::Responses) { if !matches!(protocol, Protocol::Anthropic | Protocol::Responses) {
return Ok(()); return Ok(());
} }
let memory = state.tool_memory.lock().ok(); let memory = state
.tool_memory
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
for (index, message) in messages.iter().enumerate() { for (index, message) in messages.iter().enumerate() {
if !matches!(message.role.as_str(), "tool" | "function") || message.tool_call_id.is_empty() if !matches!(message.role.as_str(), "tool" | "function") || message.tool_call_id.is_empty()
{ {
continue; continue;
} }
let id = &message.tool_call_id; let id = &message.tool_call_id;
let live = memory let live = memory.contains_key(id);
.as_ref()
.is_some_and(|memory| memory.contains_key(id));
let replayed = messages[..index].iter().any(|message| { let replayed = messages[..index].iter().any(|message| {
message.role == "assistant" && message.tool_calls.iter().any(|call| call.id == *id) message.role == "assistant" && message.tool_calls.iter().any(|call| call.id == *id)
}); });
@@ -441,13 +442,15 @@ pub(super) fn validate_tool_results(
} }
fn replayed_or_canonical_tools(state: &State, calls: &[ApiToolCall]) -> String { fn replayed_or_canonical_tools(state: &State, calls: &[ApiToolCall]) -> String {
if let Ok(memory) = state.tool_memory.lock() let memory = state
&& let Some(raw) = calls.iter().find_map(|call| { .tool_memory
(!call.id.is_empty()) .lock()
.then(|| memory.get(&call.id)) .unwrap_or_else(|poisoned| poisoned.into_inner());
.flatten() if let Some(raw) = calls.iter().find_map(|call| {
}) (!call.id.is_empty())
{ .then(|| memory.get(&call.id))
.flatten()
}) {
return raw.clone(); return raw.clone();
} }
canonical_tools(calls) canonical_tools(calls)
@@ -568,14 +571,16 @@ pub(super) fn parse_generated_tools_with_ids(
.cloned() .cloned()
.unwrap_or_else(|| random_tool_id(prefix)); .unwrap_or_else(|| random_tool_id(prefix));
} }
if let Ok(mut memory) = state.tool_memory.lock() { let mut memory = state
// ponytail: one process-local replay table; add LRU eviction if 100k live tool ids is measured insufficient. .tool_memory
if memory.len() >= 100_000 { .lock()
memory.clear(); .unwrap_or_else(|poisoned| poisoned.into_inner());
} // ponytail: one process-local replay table; add LRU eviction if 100k live tool ids is measured insufficient.
for call in &calls { if memory.len() >= 100_000 {
memory.insert(call.id.clone(), raw.to_owned()); memory.clear();
} }
for call in &calls {
memory.insert(call.id.clone(), raw.to_owned());
} }
(content.to_owned(), calls) (content.to_owned(), calls)
} }