Skip to content

Conversation

@Veykril
Copy link
Member

@Veykril Veykril commented Oct 17, 2025

Introduces a CancellationToken that can be used to cancel a specific database computation opposed to cancelling the whole runtime. This is traced by storing a second cancellation state on ZalsaLocal itself opposed to the runtime cancel flag.

When a thread gets cancelled, it will unwind as usual but with a different payload than pending write cancellation, threads blocked on such a cancelled thread will instead of propagating the cancellation run the computation they are blocked on themselves.

The cancellation state of the database gets reset when we exit the database TLS.

@netlify
Copy link

netlify bot commented Oct 17, 2025

Deploy Preview for salsa-rs canceled.

Name Link
🔨 Latest commit 2f99827
🔍 Latest deploy log https://app.netlify.com/projects/salsa-rs/deploys/695545814804290008c0aa3e

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 17, 2025

CodSpeed Performance Report

Merging #1007 will degrade performance by 8.36%

Comparing Veykril:push-kwpwsmmosonq (2f99827) with master (77c296f)

Summary

⚡ 1 improvement
❌ 3 (👁 3) regressions
✅ 9 untouched

Benchmarks breakdown

Benchmark BASE HEAD Efficiency
👁 amortized[SupertypeInput] 2.9 µs 3 µs -4.61%
👁 many_tracked_structs 8.7 µs 9.1 µs -4.06%
👁 amortized[InternedInput] 2 µs 2.2 µs -8.36%
new[Input] 10.5 µs 10.1 µs +4.55%

@Veykril
Copy link
Member Author

Veykril commented Oct 17, 2025

Looks like allocator noise from the new arc alloc

@MichaReiser
Copy link
Contributor

Interesting. Is the idea to cancel a single thread rather than all threads? Won't this cancell all threads that currently block on any query executing on the thread being cancelled?

@Veykril
Copy link
Member Author

Veykril commented Oct 17, 2025

Is the idea to cancel a single thread rather than all threads?

Yes, the thought is that this could allow implementing client side request cancellation for LSP requests for example.

Won't this cancell all threads that currently block on any query executing on the thread being cancelled?

That is the thing I will have to investigate. We unwind with the payload, we don't panic so I believe this does not set the panicking state of the thread actually. So this might actually just work (though I doubt it).

@MichaReiser
Copy link
Contributor

That is the thing I will have to investigate. We unwind with the payload, we don't panic so I believe this does not set the panicking state of the thread actually. So this might actually just work (though I doubt it).

Oh, so other threads would reclaim and re-execute the queries then. Interesting.

@Veykril
Copy link
Member Author

Veykril commented Oct 17, 2025

That would be the ideal I think

@Veykril Veykril force-pushed the push-kwpwsmmosonq branch 3 times, most recently from db715b7 to 47b7a7d Compare October 19, 2025 16:45
@Veykril
Copy link
Member Author

Veykril commented Oct 19, 2025

I was wrong, unwinding does set the panicking flag after all (which probably makes more sense ...) hmm

@Veykril Veykril force-pushed the push-kwpwsmmosonq branch 12 times, most recently from f97346e to de32cab Compare October 26, 2025 15:52
@Veykril Veykril marked this pull request as ready for review October 26, 2025 15:52
@Veykril Veykril force-pushed the push-kwpwsmmosonq branch 3 times, most recently from ba0f832 to 2318de4 Compare October 26, 2025 16:32
@Veykril Veykril requested a review from MichaReiser October 26, 2025 17:14
@MichaReiser
Copy link
Contributor

Would mind explaining the approach in the pr summary. I'm mainly interested in how it works if other threads are blocked on a query that gets cancelled (including if they participate in a cycle)

- name: Test with Miri
run: cargo miri nextest run --no-fail-fast --tests
env:
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-retag-fields
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a default now

@Veykril Veykril force-pushed the push-kwpwsmmosonq branch 6 times, most recently from 6e14f3d to 7d927c2 Compare December 26, 2025 18:43
@Veykril Veykril requested a review from Copilot December 31, 2025 14:00
Copy link

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 introduces a CancellationToken that allows cancelling specific database computations instead of the entire runtime. The token is stored on ZalsaLocal and enables fine-grained cancellation control, with special handling during cycle fixpoint iteration where local cancellation is disabled to ensure cycles complete successfully.

Key changes:

  • Added CancellationToken type with atomic state management for local cancellation
  • Differentiated Cancelled::Local from Cancelled::PendingWrite to distinguish cancellation types
  • Modified blocking behavior so threads blocked on locally-cancelled threads recompute instead of propagating cancellation

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/zalsa_local.rs Implements CancellationToken with atomic state management and integrates it into ZalsaLocal
src/zalsa.rs Updates cancellation checks to handle both local and pending-write cancellation
src/database.rs Adds public cancellation_token() API method
src/cancelled.rs Adds Cancelled::Local variant for local cancellation
src/attach.rs Resets cancellation state when exiting database TLS
src/storage.rs Ensures proper cleanup of zalsa_local when converting to handle
src/runtime.rs Adds WaitResult::Cancelled, renames revision_canceled to revision_cancelled
src/function/sync.rs Reorders ClaimResult variants, adds zalsa_local parameter, handles cancelled release
src/function/fetch.rs Returns None on local cancellation to trigger recomputation
src/function/memo.rs Converts local cancellation to panic in fixpoint, fixes comment typo
src/function/execute.rs Disables local cancellation during fixpoint iteration
src/function/maybe_changed_after.rs Updates blocking to discard return value
src/interned.rs Adds #[allow(dead_code)] for feature-gated field
tests/cancellation_token.rs Basic cancellation token test
tests/parallel/cancellation_token_*.rs Comprehensive parallel cancellation tests
tests/dataflow.rs Refactors Box<[usize]> to BTreeSet<usize>
tests/interned-revisions.rs Adjusts iteration counts for miri/non-miri
.github/workflows/test.yml Removes -Zmiri-retag-fields flag
tests/parallel/main.rs Adds new test module declarations

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

Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

It probably makes sense to have this in Salsa but is an implementation outside Salsa considerably more work?

Comment on lines 184 to 188
// We cannot handle local cancellations in fixpoints
// so we treat it as a general cancellation / panic.
//
// We shouldn't hit this though as we disable local cancellation
// in cycles.
Copy link
Contributor

Choose a reason for hiding this comment

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

block_on_heads isn't used by fixpoint anymore. It's only used by the FallbackImmediate cycle recovery strategy

Copy link
Member Author

Choose a reason for hiding this comment

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

Is FallbackImmediate even necessary anymore? iirc you've removed the requirement of fixpoint having to converge right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Whether it's still necessary depends on r-a. We aren't using it in ty

iirc you've removed the requirement of fixpoint having to converge right?

No, fixpoint queries still have to converge. I only changed how we run nested queries.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah i mixed it up with #991

if let Some(attached) = self.state {
if let Some(prev) = attached.database.replace(self.prev) {
// SAFETY: `prev` is a valid pointer to a database.
unsafe { prev.as_ref().zalsa_local().uncancel() };
Copy link
Contributor

Choose a reason for hiding this comment

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

What if prev was cancelled too? Which is possible because CancellationToken is Clone.

Should we instead simply consider that db as "dead". Meaning, you have to call Clone to get a new Db afterwards (What you do is probably the opposite, you clone the Db before passing it to a request handler, and the db is later dropped)

Copy link
Member Author

Choose a reason for hiding this comment

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

If prev was cancelled too then the query that initially attached it will reset the state once it exits its scope. We always put the databases back that we replaced in attached when the scope ends due to this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I think I got confused by the variable being named prev. I assumed it's DbGuard.prev when it is the database stored in attached.database (which isn't prev but the current db up to when we started dropping).

I think there's still a risk that we uncancel if we have something like a -> b -> a and a gets cancelled. I understand that we call a.uncancel() when we drop the guard for a -> b -> a (the last db).

Should we make this a method on attached instead, saying that it resets the database state (e.g. .attached.exit`). This also avoids duplicating the same logic.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I believe this should be fine though. This could basically cause cancellations to be .. well cancelled. But only if you interleave a bunch of different databases (which ideally we wouldn't even allow imo, but is kind of a necessary evil given the lack of speculative execution).

@Veykril
Copy link
Member Author

Veykril commented Dec 31, 2025

I am not sure how we can make this work as an implementation outside of salsa, given it needs to special case around fixpoint executions. And to me this feels very much like something salsa should offer natively anyways.

@Veykril Veykril force-pushed the push-kwpwsmmosonq branch 2 times, most recently from 15c4e98 to c5b15ae Compare December 31, 2025 15:45
if let Some(attached) = self.state {
if let Some(prev) = attached.database.replace(self.prev) {
// SAFETY: `prev` is a valid pointer to a database.
unsafe { prev.as_ref().zalsa_local().uncancel() };
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I think I got confused by the variable being named prev. I assumed it's DbGuard.prev when it is the database stored in attached.database (which isn't prev but the current db up to when we started dropping).

I think there's still a risk that we uncancel if we have something like a -> b -> a and a gets cancelled. I understand that we call a.uncancel() when we drop the guard for a -> b -> a (the last db).

Should we make this a method on attached instead, saying that it resets the database state (e.g. .attached.exit`). This also avoids duplicating the same logic.

@MichaReiser
Copy link
Contributor

Curious to see how you use this in r-a.

@Veykril
Copy link
Member Author

Veykril commented Jan 1, 2026

Basically like rust-lang/rust-analyzer#21380. Right now rust-analyzer will run LSP requests to completion even if they were cancelled (unless a user types/changes an input of course)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants