Recover poisoned shared-state locks
This commit is contained in:
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
if let Some(raw) = calls.iter().find_map(|call| {
|
||||||
(!call.id.is_empty())
|
(!call.id.is_empty())
|
||||||
.then(|| memory.get(&call.id))
|
.then(|| memory.get(&call.id))
|
||||||
.flatten()
|
.flatten()
|
||||||
})
|
}) {
|
||||||
{
|
|
||||||
return raw.clone();
|
return raw.clone();
|
||||||
}
|
}
|
||||||
canonical_tools(calls)
|
canonical_tools(calls)
|
||||||
@@ -568,7 +571,10 @@ 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
|
||||||
|
.tool_memory
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
// ponytail: one process-local replay table; add LRU eviction if 100k live tool ids is measured insufficient.
|
// ponytail: one process-local replay table; add LRU eviction if 100k live tool ids is measured insufficient.
|
||||||
if memory.len() >= 100_000 {
|
if memory.len() >= 100_000 {
|
||||||
memory.clear();
|
memory.clear();
|
||||||
@@ -576,7 +582,6 @@ pub(super) fn parse_generated_tools_with_ids(
|
|||||||
for call in &calls {
|
for call in &calls {
|
||||||
memory.insert(call.id.clone(), raw.to_owned());
|
memory.insert(call.id.clone(), raw.to_owned());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
(content.to_owned(), calls)
|
(content.to_owned(), calls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user