Summary
There is a pre-existing race condition in PendingRequests::drop() (in crates/biome_cli/src/service/mod.rs) that was present with DashMap and carries over to the current papaya::HashMap implementation.
Identified during: #10407 (comment: #10407 (comment))
Reported by: @ematipico
Scenario
Two live PendingRequests handles share the same Arc<HashMap>: one in SocketTransport, one in read_task.
read_task terminates (error or shutdown) → its PendingRequests handle is dropped → Drop::drop() calls self.inner.pin().clear(). ✅
- A concurrent
request() call on the still-alive SocketTransport inserts a new sender after the clear completes.
- No one ever clears that entry. The request silently waits the full 15-second timeout instead of failing fast with
TransportError::ChannelClosed.
This is not a hard deadlock — it is a slow failure path.
Proposed Fix
Add an AtomicBool shutdown flag to PendingRequests:
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Clone, Default)]
struct PendingRequests {
closed: Arc<AtomicBool>,
inner: Arc<HashMap<u64, PendingChannel>>,
}
impl Drop for PendingRequests {
fn drop(&mut self) {
// Signal closure before clearing so any concurrent insert
// that races with clear() can detect it and bail.
self.closed.store(true, Ordering::Release);
self.inner.pin().clear();
}
}
And in WorkspaceTransport::request(), after inserting, check the flag and cancel immediately if already closed:
self.pending_requests
.pin()
.insert(request.id, Mutex::new(Some(send)));
// Guard against the drop having run concurrently.
if self.pending_requests.closed.load(Ordering::Acquire) {
self.pending_requests.pin().remove(&request.id);
return Err(TransportError::ChannelClosed);
}
This keeps the fast-cancel guarantee intact without adding heavier synchronisation.
Summary
There is a pre-existing race condition in
PendingRequests::drop()(incrates/biome_cli/src/service/mod.rs) that was present withDashMapand carries over to the currentpapaya::HashMapimplementation.Identified during: #10407 (comment: #10407 (comment))
Reported by: @ematipico
Scenario
Two live
PendingRequestshandles share the sameArc<HashMap>: one inSocketTransport, one inread_task.read_taskterminates (error or shutdown) → itsPendingRequestshandle is dropped →Drop::drop()callsself.inner.pin().clear(). ✅request()call on the still-aliveSocketTransportinserts a new sender after the clear completes.TransportError::ChannelClosed.This is not a hard deadlock — it is a slow failure path.
Proposed Fix
Add an
AtomicBoolshutdown flag toPendingRequests:And in
WorkspaceTransport::request(), after inserting, check the flag and cancel immediately if already closed:This keeps the fast-cancel guarantee intact without adding heavier synchronisation.