Skip to content

fix: make manual query cancellable via dedicated CancellationTokenSource#70

Open
xiaocang wants to merge 1 commit intomasterfrom
claude/fix-manual-query-cancellation-ISAsI
Open

fix: make manual query cancellable via dedicated CancellationTokenSource#70
xiaocang wants to merge 1 commit intomasterfrom
claude/fix-manual-query-cancellation-ISAsI

Conversation

@xiaocang
Copy link
Owner

@xiaocang xiaocang commented Feb 6, 2026

Manual queries (OnServiceQueryRequested) previously used CancellationToken.None,
making them impossible to cancel. This caused three issues:

  1. Unresponsive services (e.g. Ollama) could not be cancelled by the user
  2. Page/window close did not stop in-flight manual queries
  3. New main queries did not cancel stale manual queries for old text

Add _manualQueryCts field (scheme B) with clear ownership:

  • OnServiceQueryRequested: creates CTS, cancels previous, disposes in finally
  • StartQueryAsync: cancels manual queries when new main query starts
  • CleanupResourcesAsync: cancels manual queries on page/window close

https://claude.ai/code/session_01GXde8bGRRhP3p6X9LVCVuW

Manual queries (OnServiceQueryRequested) previously used CancellationToken.None,
making them impossible to cancel. This caused three issues:
1. Unresponsive services (e.g. Ollama) could not be cancelled by the user
2. Page/window close did not stop in-flight manual queries
3. New main queries did not cancel stale manual queries for old text

Add _manualQueryCts field (scheme B) with clear ownership:
- OnServiceQueryRequested: creates CTS, cancels previous, disposes in finally
- StartQueryAsync: cancels manual queries when new main query starts
- CleanupResourcesAsync: cancels manual queries on page/window close

https://claude.ai/code/session_01GXde8bGRRhP3p6X9LVCVuW
@xiaocang xiaocang requested a review from Copilot February 6, 2026 23:45
@xiaocang xiaocang added the verification required manual verification required label Feb 6, 2026
@claude
Copy link

claude bot commented Feb 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes manual per-service queries (triggered by OnServiceQueryRequested) cancellable by introducing a dedicated CancellationTokenSource for manual queries, and ensuring manual queries are canceled on new main queries and during window/page cleanup.

Changes:

  • Add _manualQueryCts to track and cancel in-flight manual queries.
  • Plumb the manual cancellation token through language detection and translation calls.
  • Cancel manual queries when starting a new main query and during cleanup/shutdown.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 12 comments.

File Description
dotnet/src/Easydict.WinUI/Views/MiniWindow.xaml.cs Introduces _manualQueryCts, passes cancellation tokens into manual query execution, and cancels manual queries on new main queries/cleanup.
dotnet/src/Easydict.WinUI/Views/MainPage.xaml.cs Mirrors the same manual-query cancellation model for the main page workflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +382 to +383
oldCts?.Dispose();

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposing the previous manual query CTS here can break an in-flight manual query that is still using its CancellationToken (many APIs register callbacks and may throw ObjectDisposedException if the source is disposed). Since the previous OnServiceQueryRequested invocation owns/disposes its CTS in its finally block, prefer to only Cancel() the old CTS here and let the owning invocation dispose it.

Suggested change
oldCts?.Dispose();

Copilot uses AI. Check for mistakes.
// Cancel any in-flight manual queries (stale text)
var oldManualCts = Interlocked.Exchange(ref _manualQueryCts, null);
oldManualCts?.Cancel();
oldManualCts?.Dispose();
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposing the manual CTS here can race with the in-flight manual query that’s still using the token, which can surface as ObjectDisposedException rather than a clean cancellation. Recommend only Cancel()ing the exchanged CTS and letting the owning OnServiceQueryRequested invocation dispose it in its finally.

Suggested change
oldManualCts?.Dispose();
// Do not dispose here; the owning OnServiceQueryRequested invocation
// is responsible for disposing the CTS in its finally block.

Copilot uses AI. Check for mistakes.
// Cancel any in-flight manual queries
var manualCts = Interlocked.Exchange(ref _manualQueryCts, null);
manualCts?.Cancel();
manualCts?.Dispose();
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposing the manual CTS during cleanup can race with a still-running manual query that’s using the token, potentially causing ObjectDisposedException inside awaited operations. Prefer to exchange+Cancel() but leave disposal to the owning OnServiceQueryRequested invocation’s finally block.

Suggested change
manualCts?.Dispose();

Copilot uses AI. Check for mistakes.
Comment on lines +624 to +625
oldManualCts?.Cancel();
oldManualCts?.Dispose();
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposing the exchanged manual CTS here can race with the in-flight manual query that is still using its token, producing ObjectDisposedException instead of a clean cancellation. Recommend only Cancel() here and let OnServiceQueryRequested dispose its CTS in finally.

Suggested change
oldManualCts?.Cancel();
oldManualCts?.Dispose();
// Only cancel here; disposal is handled in the manual query's finally block
oldManualCts?.Cancel();

Copilot uses AI. Check for mistakes.
@@ -43,6 +43,7 @@ private struct POINT
// Owned by StartQueryAsync() - only that method creates and disposes via its finally block.
// Other code may Cancel() but must NOT Dispose().
private CancellationTokenSource? _currentQueryCts;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_currentQueryCts has an ownership comment explaining who may Dispose(); _manualQueryCts doesn’t. Since multiple methods Cancel()/Exchange this field, please add a short ownership/lifetime comment (e.g., owned by OnServiceQueryRequested, others must not Dispose) to prevent future misuse.

Suggested change
private CancellationTokenSource? _currentQueryCts;
private CancellationTokenSource? _currentQueryCts;
// Owned by the manual query workflow (creator method owns Dispose()).
// Other code may Cancel()/exchange this field but must NOT Dispose().

Copilot uses AI. Check for mistakes.
Comment on lines +612 to +615
// Cancel any in-flight manual queries
var manualCts = Interlocked.Exchange(ref _manualQueryCts, null);
manualCts?.Cancel();
manualCts?.Dispose();
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposing the manual CTS during cleanup can race with the still-running manual query, potentially causing ObjectDisposedException inside awaited operations that use the token. Prefer to exchange+Cancel() to signal shutdown, but leave disposal to the owning OnServiceQueryRequested finally block (similar to the _currentQueryCts ownership pattern).

Suggested change
// Cancel any in-flight manual queries
var manualCts = Interlocked.Exchange(ref _manualQueryCts, null);
manualCts?.Cancel();
manualCts?.Dispose();
// Cancel any in-flight manual queries; disposal is handled by the owner
var manualCts = Interlocked.Exchange(ref _manualQueryCts, null);
manualCts?.Cancel();

Copilot uses AI. Check for mistakes.
Comment on lines +322 to +327
// Create new CTS, cancel any previous manual query
var cts = new CancellationTokenSource();
var oldCts = Interlocked.Exchange(ref _manualQueryCts, cts);
oldCts?.Cancel();
oldCts?.Dispose();

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposing the previous manual query CTS here can break an in-flight manual query that is still using its CancellationToken (ObjectDisposedException during token registrations). Since the previous OnServiceQueryRequested invocation disposes its CTS in finally, only Cancel() the old CTS here and let the owner dispose it.

Suggested change
// Create new CTS, cancel any previous manual query
var cts = new CancellationTokenSource();
var oldCts = Interlocked.Exchange(ref _manualQueryCts, cts);
oldCts?.Cancel();
oldCts?.Dispose();
// Create new CTS, cancel any previous manual query (owner will dispose in its own finally)
var cts = new CancellationTokenSource();
var oldCts = Interlocked.Exchange(ref _manualQueryCts, cts);
oldCts?.Cancel();

Copilot uses AI. Check for mistakes.
catch (OperationCanceledException)
{
serviceResult.IsLoading = false;
serviceResult.IsStreaming = false;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On cancellation, MarkQueried() has already run but no Result/Error is set, so manual-query services can end up stuck (HasQueried=true prevents QueryRequested from firing again on re-expand). Consider resetting the ServiceQueryResult on cancellation (or restoring HasQueried=false) so users can retry the manual query.

Suggested change
serviceResult.IsStreaming = false;
serviceResult.IsStreaming = false;
serviceResult.HasQueried = false;

Copilot uses AI. Check for mistakes.
}

// Create new CTS, cancel any previous manual query
var cts = new CancellationTokenSource();
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.

Copilot uses AI. Check for mistakes.
}

// Create new CTS, cancel any previous manual query
var cts = new CancellationTokenSource();
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

verification required manual verification required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants