feat(TASK-061): fix reconnect-flag race and completed-after-save hook ordering
- McpFactory::acquire now reads the live probe and the reconnect flag under one lock section so a racing first connect cannot be misreported as COYOTE_MCP_RECONNECT=true; the residual window across the spawn await (no singleflight) is documented. try_get_active, now unused, is removed. - All three RAG sync funnels (init, init_with_config, refresh_document_paths) fire rag.sync.completed only after a successful save(), matching the documented contract; a save error routes through rag.sync.failed with COYOTE_ERROR before propagating. - New funnel-level tests pin init_with_config's started-then-completed ordering, the failed event on a save error, and the failed event on the sync Err arm that a user abort surfaces through.
This commit is contained in:
2 files changed
+198
-39
No files matched your search
+15
-30
@@ -81,11 +81,6 @@ pub struct McpFactory {
|
||||
}
|
||||
|
||||
impl McpFactory {
|
||||
pub fn try_get_active(&self, key: &McpServerKey) -> Option<Arc<ConnectedServer>> {
|
||||
let map = self.active.lock();
|
||||
map.get(key).and_then(|weak| weak.upgrade())
|
||||
}
|
||||
|
||||
pub fn insert_active(&self, key: McpServerKey, handle: &Arc<ConnectedServer>) {
|
||||
let mut map = self.active.lock();
|
||||
map.insert(key, Arc::downgrade(handle));
|
||||
@@ -101,15 +96,21 @@ impl McpFactory {
|
||||
let key = McpServerKey::from_spec(name, spec);
|
||||
|
||||
// Reuse of a live server fires no hooks; only a real spawn below
|
||||
// reports anything.
|
||||
if let Some(existing) = self.try_get_active(&key) {
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
// The live probe failed, so an entry still present for this key is a
|
||||
// dead weak: the key was connected earlier in this process and the
|
||||
// spawn below is a reconnect.
|
||||
let reconnect = self.active.lock().contains_key(&key);
|
||||
// reports anything. The live probe and the reconnect read share one
|
||||
// lock section so a racing first connect that inserts between them
|
||||
// cannot be misreported as a reconnect. The spawn await below stays
|
||||
// outside the lock, so two racing first connects may still both
|
||||
// spawn (no singleflight); the loser then reports a reconnect.
|
||||
let reconnect = {
|
||||
let map = self.active.lock();
|
||||
if let Some(existing) = map.get(&key).and_then(|weak| weak.upgrade()) {
|
||||
return Ok(existing);
|
||||
}
|
||||
// The live probe failed, so an entry still present for this key
|
||||
// is a dead weak: the key was connected earlier in this process
|
||||
// and the spawn below is a reconnect.
|
||||
map.contains_key(&key)
|
||||
};
|
||||
|
||||
let transport = transport_label(&spec.transport_type);
|
||||
let handle = match spawn_server(name, spec, log_path).await {
|
||||
@@ -357,22 +358,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_try_get_active_returns_none_when_empty() {
|
||||
let factory = McpFactory::default();
|
||||
let spec = stdio_spec("cmd", None, None);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
assert!(factory.try_get_active(&key).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_try_get_active_returns_none_for_unknown_key() {
|
||||
let factory = McpFactory::default();
|
||||
let spec = stdio_spec("cmd", None, None);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
assert!(factory.try_get_active(&key).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_default_has_empty_active_map() {
|
||||
let factory = McpFactory::default();
|
||||
|
||||
Reference in new issue
Block a user