-
Notifications
You must be signed in to change notification settings - Fork 181
Fix hang in nested fixpoint iteration #871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
tests/parallel/cycle_provisional_depending_on_itself.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
//! Test a specific cycle scenario: | ||
//! | ||
//! 1. Thread T1 calls `a` which calls `b` | ||
//! 2. Thread T2 calls `c` which calls `b` (blocks on T1 for `b`). The ordering here is important! | ||
//! 3. Thread T1: `b` calls `c` and `a`, both trigger a cycle and Salsa returns a fixpoint initial values (with `c` and `a` as cycle heads). | ||
//! 4. Thread T1: `b` is released (its not in its own cycle heads), `Memo::provisional_retry` blocks blocks on `T2` because `c` is in its cycle heads | ||
//! 5. Thread T2: Iterates `c`, blocks on T1 when reading `a`. | ||
//! 6. Thread T1: Completes the first itaration of `a`, inserting a provisional that depends on `c` and itself (`a`). | ||
//! Starts a new iteration where it executes `b`. Calling `query_a` hits a cycle: | ||
//! | ||
//! 1. `fetch_cold` returns the current provisional for `a` that depends both on `a` (owned by itself) and `c` (has no cycle heads). | ||
//! 2. `Memo::provisional_retry`: Awaits `c` (which has no cycle heads anymore). | ||
//! - Before: it skipped over the dependency key `a` that it is holding itself. It sees that `c` is final, so it retries (which gets us back to 6.1) | ||
//! - Now: Return the provisional memo and allow the outer cycle to resolve. | ||
//! | ||
//! The desired behavior here is that: | ||
//! 1. `t1`: completes the first iteration of b | ||
//! 2. `t2`: completes the cycle `c`, up to where it only depends on `a`, now blocks on `a` | ||
//! 3. `t1`: Iterates on `a`, finalizes the memo | ||
|
||
use crate::sync::thread; | ||
use salsa::CycleRecoveryAction; | ||
|
||
use crate::setup::{Knobs, KnobsDatabase}; | ||
|
||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, salsa::Update)] | ||
struct CycleValue(u32); | ||
|
||
const MIN: CycleValue = CycleValue(0); | ||
const MAX: CycleValue = CycleValue(1); | ||
|
||
#[salsa::tracked(cycle_fn=cycle_fn, cycle_initial=cycle_initial)] | ||
fn query_a(db: &dyn KnobsDatabase) -> CycleValue { | ||
query_b(db) | ||
} | ||
|
||
#[salsa::tracked(cycle_fn=cycle_fn, cycle_initial=cycle_initial)] | ||
fn query_b(db: &dyn KnobsDatabase) -> CycleValue { | ||
// Wait for thread 2 to have entered `query_c`. | ||
tracing::debug!("Wait for signal 1 from thread 2"); | ||
db.wait_for(1); | ||
|
||
// Unblock query_c on thread 2 | ||
db.signal(2); | ||
tracing::debug!("Signal 2 for thread 2"); | ||
|
||
let c_value = query_c(db); | ||
|
||
tracing::debug!("query_b: c = {:?}", c_value); | ||
|
||
let a_value = query_a(db); | ||
|
||
tracing::debug!("query_b: a = {:?}", a_value); | ||
|
||
CycleValue(a_value.0 + 1).min(MAX) | ||
} | ||
|
||
#[salsa::tracked(cycle_fn=cycle_fn, cycle_initial=cycle_initial)] | ||
fn query_c(db: &dyn KnobsDatabase) -> CycleValue { | ||
tracing::debug!("query_c: signaling thread1 to call c"); | ||
db.signal(1); | ||
|
||
tracing::debug!("query_c: waiting for signal"); | ||
// Wait for thread 1 to acquire the lock on query_b | ||
db.wait_for(1); | ||
let b = query_b(db); | ||
tracing::debug!("query_c: b = {:?}", b); | ||
b | ||
} | ||
|
||
fn cycle_fn( | ||
_db: &dyn KnobsDatabase, | ||
_value: &CycleValue, | ||
_count: u32, | ||
) -> CycleRecoveryAction<CycleValue> { | ||
CycleRecoveryAction::Iterate | ||
} | ||
|
||
fn cycle_initial(_db: &dyn KnobsDatabase) -> CycleValue { | ||
MIN | ||
} | ||
|
||
#[test_log::test] | ||
fn the_test() { | ||
crate::sync::check(|| { | ||
let db_t1 = Knobs::default(); | ||
|
||
let db_t2 = db_t1.clone(); | ||
|
||
let t1 = thread::spawn(move || { | ||
let _span = tracing::debug_span!("t1", thread_id = ?thread::current().id()).entered(); | ||
query_a(&db_t1) | ||
}); | ||
let t2 = thread::spawn(move || { | ||
let _span = tracing::debug_span!("t2", thread_id = ?thread::current().id()).entered(); | ||
query_c(&db_t2) | ||
}); | ||
|
||
let (r_t1, r_t2) = (t1.join().unwrap(), t2.join().unwrap()); | ||
|
||
assert_eq!((r_t1, r_t2), (MAX, MAX)); | ||
}); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this to early-return in #861 but I think that was incorrect. Not early-returning fixes the ty panic for
kopf
. I might spend some more time to come up with a test case for it but I might also just leave it at it (I already spent an entire week on this :s)