Файли
openidea/crates/ide-host/src/requests.rs
T
l0ng-ai 1c46a1cd04 fix(tasks): Make every background task cancellable, queued or running (#177)
Background tasks often couldn't be cancelled, or cancelling didn't stop them:
- A cancel that arrived before the backend registered the request was dropped (build while saving files, multi-repository fetch/pull between repositories). The host now remembers early cancels (bounded, 2 min), and request ids start at a random value per page.
- There was no queue and no common cancel path: backend-reported tasks (LSP progress, deployment transfers) couldn't be cancelled, and tool installs, VCS operations and update checks had no cancel button in Background Tasks.
- Only the direct child process was killed, and JDK / tool archives were extracted without checking for cancellation.

Tool installs and JDK downloads now share a downloads queue; cancelling a queued task removes it without starting any work. Every task cancels through one path that shows "Cancelling…" and then removes it; tasks that can't be interrupted show a disabled cancel button with the reason. IdeEvent::Task carries `cancellable` with a cancel_task command, cancellable LSP progress sends window/workDoneProgress/cancel, installers, curl and tar run in their own process group and are killed as a group, and extraction checks for cancellation between entries.

Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
2026-10-29 09:05:12 +08:00

117 рядки
3.6 KiB
Rust

//! Cancellable requests: the UI allocates ids, `cancel_request` cancels.
//!
//! A cancel can arrive before the command that owns the id has begun: the UI shows
//! a task (with Cancel) and then awaits other work first (saving documents, a
//! queue, a branch lookup), or the command waits for something before it gets to
//! `begin`. Such a cancel is remembered for a while so the request starts out
//! cancelled instead of running to the end.
use ide_core::{CancellationToken, RequestId};
use parking_lot::Mutex;
use std::collections::{HashMap, VecDeque};
use std::time::{Duration, Instant};
/// How long a cancel for a request that has not begun is remembered.
const EARLY_CANCEL_TTL: Duration = Duration::from_secs(120);
/// At most this many are remembered (ids of finished requests end up here too).
const EARLY_CANCEL_MAX: usize = 1024;
#[derive(Default)]
pub(crate) struct Requests {
inner: Mutex<Inner>,
}
#[derive(Default)]
struct Inner {
running: HashMap<RequestId, CancellationToken>,
/// Cancelled before they began, oldest first.
early: VecDeque<(RequestId, Instant)>,
}
impl Inner {
fn prune(&mut self, now: Instant) {
while self.early.front().is_some_and(|(_, at)| now.duration_since(*at) > EARLY_CANCEL_TTL)
|| self.early.len() > EARLY_CANCEL_MAX
{
self.early.pop_front();
}
}
}
impl Requests {
pub(crate) fn begin(&self, id: RequestId) -> CancellationToken {
let token = CancellationToken::new();
let mut inner = self.inner.lock();
inner.prune(Instant::now());
if let Some(i) = inner.early.iter().position(|(early, _)| *early == id) {
inner.early.remove(i);
token.cancel();
return token;
}
if let Some(old) = inner.running.insert(id, token.clone()) {
old.cancel();
}
token
}
pub(crate) fn end(&self, id: RequestId) {
self.inner.lock().running.remove(&id);
}
pub(crate) fn cancel(&self, id: RequestId) {
let mut inner = self.inner.lock();
match inner.running.remove(&id) {
Some(token) => token.cancel(),
None => {
let now = Instant::now();
inner.early.push_back((id, now));
inner.prune(now);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cancel_reaches_a_running_request() {
let r = Requests::default();
let token = r.begin(RequestId(1));
assert!(!token.is_cancelled());
r.cancel(RequestId(1));
assert!(token.is_cancelled());
}
#[test]
fn a_cancel_before_begin_cancels_the_request_when_it_begins() {
let r = Requests::default();
r.cancel(RequestId(7));
assert!(r.begin(RequestId(7)).is_cancelled());
// Only once: the id is not cancelled for good.
assert!(!r.begin(RequestId(7)).is_cancelled());
assert!(!r.begin(RequestId(8)).is_cancelled());
}
#[test]
fn early_cancels_are_bounded() {
let r = Requests::default();
for i in 0..(EARLY_CANCEL_MAX as u32 + 10) {
r.cancel(RequestId(i));
}
assert_eq!(r.inner.lock().early.len(), EARLY_CANCEL_MAX);
assert!(!r.begin(RequestId(0)).is_cancelled(), "the oldest were dropped");
assert!(r.begin(RequestId(EARLY_CANCEL_MAX as u32 + 9)).is_cancelled());
}
#[test]
fn a_finished_request_is_not_cancelled_again() {
let r = Requests::default();
let token = r.begin(RequestId(3));
r.end(RequestId(3));
r.cancel(RequestId(3));
assert!(!token.is_cancelled());
}
}