Repository: salsa-rs/salsa (rust)

Issue:
Crash with BadHash input and multiple changes
```rust 
#[salsa::input]
    struct TmpInput {
        pub id: u64,
    }

    #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
    struct BadHash {
        field: u64,
    }

    impl From<u64> for BadHash {
        fn from(value: u64) -> Self {
            Self { field: value }
        }
    }

    impl std::hash::Hash for BadHash {
        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
            state.write_i16(0);
        }
    }

    #[salsa::tracked(debug)]
    struct Tmp<'db> {
        id: BadHash,
    }

    #[salsa::tracked]
    fn tmp_getter<'db>(db: &'db dyn base_db::BaseDb, input: TmpInput) -> Tmp<'db> {
        Tmp::new(db, input.id(db).into())
    }

    #[test]
    fn generations_test() {
        tracing_subscriber::fmt().init();
        let mut db = TestDatabase::default();
        let input = TmpInput::new(&db, 9);
        dbg!(tmp_getter(&db, input).0);
        input.set_id(&mut db).to(10);
        dbg!(tmp_getter(&db, input).0);
        input.set_id(&mut db).to(9);
        dbg!(tmp_getter(&db, input).0);
    }
```

logs:
```
---- tests::eq_ord_test stdout ----
[test.rs:1077:9] tmp_getter(&db, input).0 = Id(800)
[test.rs:1079:9] tmp_getter(&db, input).0 = Id(800g1)

thread 'resolver::tests::eq_ord_test' panicked at C:\Users\user\.cargo\git\checkouts\salsa-e6f3bb7c2a062968\0414d89\src\tracked_struct.rs:652:53:
cannot delete read-locked id `Id(800)`; value leaked across threads or user functions not deterministic
stack backtrace:
...
```

Pull request:
Don't report stale outputs if there is newer generation in new_outputs
Fixes #878

Patch:
diff --git a/components/salsa-macro-rules/src/setup_tracked_fn.rs b/components/salsa-macro-rules/src/setup_tracked_fn.rs
index 64e0c7d6..37cf48e4 100644
--- a/components/salsa-macro-rules/src/setup_tracked_fn.rs
+++ b/components/salsa-macro-rules/src/setup_tracked_fn.rs
@@ -28,6 +28,9 @@ macro_rules! setup_tracked_fn {
         // Types of the function arguments (may reference `$generics`).
         input_tys: [$($input_ty:ty),*],
 
+        // Types of the function arguments as should be put in interned struct.
+        interned_input_tys: [$($interned_input_ty:ty),*],
+
         // Return type of the function (may reference `$generics`).
         output_ty: $output_ty:ty,
 
@@ -135,12 +138,12 @@ macro_rules! setup_tracked_fn {
                         };
                         const DEBUG_NAME: &'static str = concat!(stringify!($fn_name), "::interned_arguments");
 
-                        type Fields<$db_lt> = ($($input_ty),*);
+                        type Fields<$db_lt> = ($($interned_input_ty),*);
 
                         type Struct<$db_lt> = $InternedData<$db_lt>;
                     }
                 } else {
-                    type $InternedData<$db_lt> = ($($input_ty),*);
+                    type $InternedData<$db_lt> = ($($interned_input_ty),*);
                 }
             }
 
@@ -185,7 +188,7 @@ macro_rules! setup_tracked_fn {
 
                 type SalsaStruct<$db_lt> = $InternedData<$db_lt>;
 
-                type Input<$db_lt> = ($($input_ty),*);
+                type Input<$db_lt> = ($($interned_input_ty),*);
 
                 type Output<$db_lt> = $output_ty;
 
@@ -193,7 +196,7 @@ macro_rules! setup_tracked_fn {
 
                 $($values_equal)+
 
-                fn execute<$db_lt>($db: &$db_lt Self::DbView, ($($input_id),*): ($($input_ty),*)) -> Self::Output<$db_lt> {
+                fn execute<$db_lt>($db: &$db_lt Self::DbView, ($($input_id),*): ($($interned_input_ty),*)) -> Self::Output<$db_lt> {
                     $($assert_return_type_is_update)*
 
                     $($inner_fn)*
@@ -201,7 +204,7 @@ macro_rules! setup_tracked_fn {
                     $inner($db, $($input_id),*)
                 }
 
-                fn cycle_initial<$db_lt>(db: &$db_lt Self::DbView, ($($input_id),*): ($($input_ty),*)) -> Self::Output<$db_lt> {
+                fn cycle_initial<$db_lt>(db: &$db_lt Self::DbView, ($($input_id),*): ($($interned_input_ty),*)) -> Self::Output<$db_lt> {
                     $($cycle_recovery_initial)*(db, $($input_id),*)
                 }
 
@@ -209,7 +212,7 @@ macro_rules! setup_tracked_fn {
                     db: &$db_lt dyn $Db,
                     value: &Self::Output<$db_lt>,
                     count: u32,
-                    ($($input_id),*): ($($input_ty),*)
+                    ($($input_id),*): ($($interned_input_ty),*)
                 ) -> $zalsa::CycleRecoveryAction<Self::Output<$db_lt>> {
                     $($cycle_recovery_fn)*(db, value, count, $($input_id),*)
                 }
@@ -305,7 +308,7 @@ macro_rules! setup_tracked_fn {
             impl $fn_name {
                 pub fn accumulated<$db_lt, A: salsa::Accumulator>(
                     $db: &$db_lt dyn $Db,
-                    $($input_id: $input_ty,)*
+                    $($input_id: $interned_input_ty,)*
                 ) -> Vec<&$db_lt A> {
                     use salsa::plumbing as $zalsa;
                     let key = $zalsa::macro_if! {
@@ -322,7 +325,7 @@ macro_rules! setup_tracked_fn {
                 $zalsa::macro_if! { $is_specifiable =>
                     pub fn specify<$db_lt>(
                         $db: &$db_lt dyn $Db,
-                        $($input_id: $input_ty,)*
+                        $($input_id: $interned_input_ty,)*
                         value: $output_ty,
                     ) {
                         let key = $zalsa::AsId::as_id(&($($input_id),*));
diff --git a/components/salsa-macros/src/tracked_fn.rs b/components/salsa-macros/src/tracked_fn.rs
index caa08330..cb892b45 100644
--- a/components/salsa-macros/src/tracked_fn.rs
+++ b/components/salsa-macros/src/tracked_fn.rs
@@ -80,6 +80,16 @@ impl Macro {
         let db_lt = db_lifetime::db_lifetime(&item.sig.generics);
         let input_ids = self.input_ids(&item);
         let input_tys = self.input_tys(&item)?;
+        let interned_input_tys = input_tys.iter().map(|&ty| {
+            let mut ty = ty.clone();
+            syn::visit_mut::visit_type_mut(
+                &mut ToDbLifetimeVisitor {
+                    db_lifetime: db_lt.clone(),
+                },
+                &mut ty,
+            );
+            ty
+        });
         let output_ty = self.output_ty(&db_lt, &item)?;
         let (cycle_recovery_fn, cycle_recovery_initial, cycle_recovery_strategy) =
             self.cycle_recovery()?;
@@ -196,6 +206,7 @@ impl Macro {
                 db: #db_ident,
                 input_ids: [#(#input_ids),*],
                 input_tys: [#(#input_tys),*],
+                interned_input_tys: [#(#interned_input_tys),*],
                 output_ty: #output_ty,
                 inner_fn: { #inner_fn },
                 cycle_recovery_fn: #cycle_recovery_fn,
@@ -286,6 +297,16 @@ impl Macro {
     }
 }
 
+struct ToDbLifetimeVisitor {
+    db_lifetime: syn::Lifetime,
+}
+
+impl syn::visit_mut::VisitMut for ToDbLifetimeVisitor {
+    fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) {
+        i.clone_from(&self.db_lifetime);
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, Hash)]
 enum FunctionType {
     Constant,
diff --git a/src/active_query.rs b/src/active_query.rs
index 6c9fb3df..08d46bed 100644
--- a/src/active_query.rs
+++ b/src/active_query.rs
@@ -48,7 +48,7 @@ pub(crate) struct ActiveQuery {
 
     /// Map from tracked struct keys (which include the hash + disambiguator) to their
     /// final id.
-    pub(crate) tracked_struct_ids: IdentityMap,
+    tracked_struct_ids: IdentityMap,
 
     /// Stores the values accumulated to the given ingredient.
     /// The type of accumulated value is erased but known to the ingredient.
@@ -151,6 +151,14 @@ impl ActiveQuery {
     pub(super) fn iteration_count(&self) -> u32 {
         self.iteration_count
     }
+
+    pub(crate) fn tracked_struct_ids(&self) -> &IdentityMap {
+        &self.tracked_struct_ids
+    }
+
+    pub(crate) fn tracked_struct_ids_mut(&mut self) -> &mut IdentityMap {
+        &mut self.tracked_struct_ids
+    }
 }
 
 impl ActiveQuery {
diff --git a/src/function.rs b/src/function.rs
index 6c2ad545..22a13554 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -188,9 +188,10 @@ where
         &'db self,
         zalsa: &'db Zalsa,
         id: Id,
-        memo: memo::Memo<C::Output<'db>>,
+        mut memo: memo::Memo<C::Output<'db>>,
         memo_ingredient_index: MemoIngredientIndex,
     ) -> &'db memo::Memo<C::Output<'db>> {
+        memo.revisions.tracked_struct_ids.shrink_to_fit();
         // We convert to a `NonNull` here as soon as possible because we are going to alias
         // into the `Box`, which is a `noalias` type.
         // FIXME: Use `Box::into_non_null` once stable
diff --git a/src/function/diff_outputs.rs b/src/function/diff_outputs.rs
index b00dc414..319e8d4e 100644
--- a/src/function/diff_outputs.rs
+++ b/src/function/diff_outputs.rs
@@ -3,7 +3,7 @@ use crate::function::{Configuration, IngredientImpl};
 use crate::hash::FxIndexSet;
 use crate::zalsa::Zalsa;
 use crate::zalsa_local::QueryRevisions;
-use crate::{DatabaseKeyIndex, Event, EventKind};
+use crate::{DatabaseKeyIndex, Event, EventKind, Id};
 
 impl<C> IngredientImpl<C>
 where
@@ -14,9 +14,6 @@ where
     ///
     /// This function takes a `&mut` reference to `revisions` to remove outputs
     /// that no longer exist in this revision from [`QueryRevisions::tracked_struct_ids`].
-    ///
-    /// If `provisional` is true, the new outputs are from a cycle-provisional result. In
-    /// that case, we won't panic if we see outputs from the current revision become stale.
     pub(super) fn diff_outputs(
         &self,
         zalsa: &Zalsa,
@@ -25,7 +22,15 @@ where
         revisions: &mut QueryRevisions,
     ) {
         // Iterate over the outputs of the `old_memo` and put them into a hashset
-        let mut old_outputs: FxIndexSet<_> = old_memo.revisions.origin.outputs().collect();
+        //
+        // Ignore key_generation here, because we use the same tracked struct allocation for
+        // all generations with the same key_index and can't report it as stale
+        let mut old_outputs: FxIndexSet<_> = old_memo
+            .revisions
+            .origin
+            .outputs()
+            .map(|a| (a.ingredient_index(), a.key_index().index()))
+            .collect();
 
         if old_outputs.is_empty() {
             return;
@@ -34,7 +39,10 @@ where
         // Iterate over the outputs of the current query
         // and remove elements from `old_outputs` when we find them
         for new_output in revisions.origin.outputs() {
-            old_outputs.swap_remove(&new_output);
+            old_outputs.swap_remove(&(
+                new_output.ingredient_index(),
+                new_output.key_index().index(),
+            ));
         }
 
         if old_outputs.is_empty() {
@@ -43,12 +51,14 @@ where
 
         // Remove the outputs that are no longer present in the current revision
         // to prevent that the next revision is seeded with an id mapping that no longer exists.
-        revisions.tracked_struct_ids.retain(|&k, &mut value| {
-            !old_outputs.contains(&DatabaseKeyIndex::new(k.ingredient_index(), value))
-        });
+        revisions
+            .tracked_struct_ids
+            .retain(|&k, &mut value| !old_outputs.contains(&(k.ingredient_index(), value.index())));
 
-        for old_output in old_outputs {
-            Self::report_stale_output(zalsa, key, old_output);
+        for (ingredient_index, key_index) in old_outputs {
+            // SAFETY: key_index acquired from valid output
+            let id = unsafe { Id::from_index(key_index) };
+            Self::report_stale_output(zalsa, key, DatabaseKeyIndex::new(ingredient_index, id));
         }
     }
 
diff --git a/src/function/fetch.rs b/src/function/fetch.rs
index d1a15822..b855a412 100644
--- a/src/function/fetch.rs
+++ b/src/function/fetch.rs
@@ -14,6 +14,10 @@ where
         let (zalsa, zalsa_local) = db.zalsas();
         zalsa.unwind_if_revision_cancelled(zalsa_local);
 
+        let database_key_index = self.database_key_index(id);
+        #[cfg(debug_assertions)]
+        let _span = tracing::debug_span!("fetch", query = ?database_key_index).entered();
+
         let memo = self.refresh_memo(db, zalsa, id);
         // SAFETY: We just refreshed the memo so it is guaranteed to contain a value now.
         let memo_value = unsafe { memo.value.as_ref().unwrap_unchecked() };
@@ -21,7 +25,7 @@ where
         self.lru.record_use(id);
 
         zalsa_local.report_tracked_read(
-            self.database_key_index(id),
+            database_key_index,
             memo.revisions.durability,
             memo.revisions.changed_at,
             memo.revisions.accumulated.is_some(),
diff --git a/src/function/memo.rs b/src/function/memo.rs
index 0b1ca703..9b8e2bb8 100644
--- a/src/function/memo.rs
+++ b/src/function/memo.rs
@@ -150,14 +150,11 @@ impl<V> Memo<V> {
             cycle_heads: &CycleHeads,
         ) -> bool {
             let mut retry = false;
+            let mut hit_cycle = false;
 
             for head in cycle_heads {
                 let head_index = head.database_key_index;
 
-                if head_index == database_key_index {
-                    continue;
-                }
-
                 let ingredient = zalsa.lookup_ingredient(head_index.ingredient_index());
                 let cycle_head_kind = ingredient.cycle_head_kind(zalsa, head_index.key_index());
                 if matches!(
@@ -167,7 +164,9 @@ impl<V> Memo<V> {
                     // This cycle is already finalized, so we don't need to wait on it;
                     // keep looping through cycle heads.
                     retry = true;
+                    tracing::trace!("Dependent cycle head {head_index:?} has been finalized.");
                 } else if ingredient.wait_for(zalsa, head_index.key_index()) {
+                    tracing::trace!("Dependent cycle head {head_index:?} has been released (there's a new memo)");
                     // There's a new memo available for the cycle head; fetch our own
                     // updated memo and see if it's still provisional or if the cycle
                     // has resolved.
@@ -176,7 +175,10 @@ impl<V> Memo<V> {
                     // We hit a cycle blocking on the cycle head; this means it's in
                     // our own active query stack and we are responsible to resolve the
                     // cycle, so go ahead and return the provisional memo.
-                    return false;
+                    tracing::debug!(
+                        "Waiting for {head_index:?} results in a cycle, return {database_key_index:?} once all other cycle heads completed to allow the outer cycle to make progress."
+                    );
+                    hit_cycle = true;
                 }
             }
 
@@ -186,7 +188,14 @@ impl<V> Memo<V> {
             // the cycle head (either initial value, or from a later iteration) and should be
             // returned to caller to allow fixpoint iteration to proceed. (All cases in the loop
             // above other than "cycle head is self" are either terminal or set `retry`.)
-            retry
+            if hit_cycle {
+                false
+            } else if retry {
+                tracing::debug!("Retrying {database_key_index:?}");
+                true
+            } else {
+                false
+            }
         }
     }
 
diff --git a/src/tracked_struct.rs b/src/tracked_struct.rs
index cb15a7d9..44d0a606 100644
--- a/src/tracked_struct.rs
+++ b/src/tracked_struct.rs
@@ -205,55 +205,59 @@ pub struct IdentityHash {
     hash: u64,
 }
 
+/// A map from tracked struct keys (which include the hash + [Disambiguator]) to their
+/// final [Id].
 #[derive(Default, Debug)]
 pub(crate) struct IdentityMap {
-    // we use a non-hasher hashmap here as our key contains its own hash (`Identity::hash`)
-    // so we use the raw entry api instead to avoid the overhead of hashing unnecessarily
-    map: hashbrown::HashMap<Identity, Id, ()>,
+    // we use a hashtable here as our key contains its own hash (`Identity::hash`)
+    // so we do the hash wrangling ourselves
+    table: hashbrown::HashTable<(Identity, Id)>,
 }
 
 impl Clone for IdentityMap {
     fn clone(&self) -> Self {
         Self {
-            map: self.map.clone(),
+            table: self.table.clone(),
         }
     }
     fn clone_from(&mut self, source: &Self) {
-        self.map.clone_from(&source.map);
+        self.table.clone_from(&source.table);
     }
 }
 
 impl IdentityMap {
     pub(crate) fn insert(&mut self, key: Identity, id: Id) -> Option<Id> {
-        use hashbrown::hash_map::RawEntryMut;
-
-        let entry = self.map.raw_entry_mut().from_hash(key.hash, |k| *k == key);
+        let entry = self.table.find_mut(key.hash, |&(k, _)| k == key);
         match entry {
-            RawEntryMut::Occupied(mut occupied) => Some(occupied.insert(id)),
-            RawEntryMut::Vacant(vacant) => {
-                vacant.insert_with_hasher(key.hash, key, id, |k| k.hash);
+            Some(occupied) => Some(mem::replace(&mut occupied.1, id)),
+            None => {
+                self.table
+                    .insert_unique(key.hash, (key, id), |(k, _)| k.hash);
                 None
             }
         }
     }
 
     pub(crate) fn get(&self, key: &Identity) -> Option<Id> {
-        self.map
-            .raw_entry()
-            .from_hash(key.hash, |k| *k == *key)
-            .map(|(_, &v)| v)
+        self.table
+            .find(key.hash, |&(k, _)| k == *key)
+            .map(|&(_, v)| v)
     }
 
     pub(crate) fn is_empty(&self) -> bool {
-        self.map.is_empty()
+        self.table.is_empty()
     }
 
-    pub(crate) fn retain(&mut self, f: impl FnMut(&Identity, &mut Id) -> bool) {
-        self.map.retain(f);
+    pub(crate) fn retain(&mut self, mut f: impl FnMut(&Identity, &mut Id) -> bool) {
+        self.table.retain(|(k, v)| f(k, v));
     }
 
-    pub fn clear(&mut self) {
-        self.map.clear()
+    pub(crate) fn clear(&mut self) {
+        self.table.clear()
+    }
+
+    pub(crate) fn shrink_to_fit(&mut self) {
+        self.table.shrink_to_fit(|(k, _)| k.hash);
     }
 }
 
diff --git a/src/zalsa.rs b/src/zalsa.rs
index 9800a44d..4b15f1bc 100644
--- a/src/zalsa.rs
+++ b/src/zalsa.rs
@@ -355,6 +355,7 @@ impl Zalsa {
     #[doc(hidden)]
     pub fn new_revision(&mut self) -> Revision {
         let new_revision = self.runtime.new_revision();
+        let _span = tracing::debug_span!("new_revision", ?new_revision).entered();
 
         for (_, index) in self.ingredients_requiring_reset.iter() {
             let index = index.as_usize();
@@ -372,6 +373,7 @@ impl Zalsa {
     /// **NOT SEMVER STABLE**
     #[doc(hidden)]
     pub fn evict_lru(&mut self) {
+        let _span = tracing::debug_span!("evict_lru").entered();
         for (_, index) in self.ingredients_requiring_reset.iter() {
             let index = index.as_usize();
             self.ingredients_vec
diff --git a/src/zalsa_local.rs b/src/zalsa_local.rs
index fed25478..0278e2b2 100644
--- a/src/zalsa_local.rs
+++ b/src/zalsa_local.rs
@@ -285,7 +285,7 @@ impl ZalsaLocal {
             let top_query = stack
                 .last()
                 .expect("cannot create a tracked struct ID outside of a tracked function");
-            top_query.tracked_struct_ids.get(identity)
+            top_query.tracked_struct_ids().get(identity)
         })
     }
 
@@ -295,7 +295,7 @@ impl ZalsaLocal {
             let top_query = stack
                 .last_mut()
                 .expect("cannot store a tracked struct ID outside of a tracked function");
-            top_query.tracked_struct_ids.insert(identity, id);
+            top_query.tracked_struct_ids_mut().insert(identity, id);
         })
     }
 
@@ -509,8 +509,10 @@ impl ActiveQueryGuard<'_> {
             #[cfg(debug_assertions)]
             assert_eq!(stack.len(), self.push_len);
             let frame = stack.last_mut().unwrap();
-            assert!(frame.tracked_struct_ids.is_empty());
-            frame.tracked_struct_ids.clone_from(tracked_struct_ids);
+            assert!(frame.tracked_struct_ids().is_empty());
+            frame
+                .tracked_struct_ids_mut()
+                .clone_from(tracked_struct_ids);
         })
     }
 


Test patch:
diff --git a/tests/parallel/cycle_provisional_depending_on_itself.rs b/tests/parallel/cycle_provisional_depending_on_itself.rs
new file mode 100644
index 00000000..ba3645fd
--- /dev/null
+++ b/tests/parallel/cycle_provisional_depending_on_itself.rs
@@ -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));
+    });
+}
diff --git a/tests/parallel/main.rs b/tests/parallel/main.rs
index 2309ad27..bd9d5658 100644
--- a/tests/parallel/main.rs
+++ b/tests/parallel/main.rs
@@ -6,6 +6,7 @@ mod cycle_a_t1_b_t2_fallback;
 mod cycle_ab_peeping_c;
 mod cycle_nested_three_threads;
 mod cycle_panic;
+mod cycle_provisional_depending_on_itself;
 mod parallel_cancellation;
 mod parallel_join;
 mod parallel_map;
diff --git a/tests/tracked-struct-id-field-bad-hash.rs b/tests/tracked-struct-id-field-bad-hash.rs
index 746a7cee..f60aa479 100644
--- a/tests/tracked-struct-id-field-bad-hash.rs
+++ b/tests/tracked-struct-id-field-bad-hash.rs
@@ -11,16 +11,16 @@ use test_log::test;
 
 #[salsa::input]
 struct MyInput {
-    field: bool,
+    field: u64,
 }
 
 #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
 struct BadHash {
-    field: bool,
+    field: u64,
 }
 
-impl From<bool> for BadHash {
-    fn from(value: bool) -> Self {
+impl From<u64> for BadHash {
+    fn from(value: u64) -> Self {
         Self { field: value }
     }
 }
@@ -46,9 +46,9 @@ fn the_fn(db: &dyn Db, input: MyInput) {
 fn execute() {
     let mut db = salsa::DatabaseImpl::new();
 
-    let input = MyInput::new(&db, true);
+    let input = MyInput::new(&db, 1);
     the_fn(&db, input);
-    input.set_field(&mut db).to(false);
+    input.set_field(&mut db).to(0);
     the_fn(&db, input);
 }
 
@@ -58,7 +58,7 @@ fn create_tracked<'db>(db: &'db dyn Db, input: MyInput) -> MyTracked<'db> {
 }
 
 #[salsa::tracked]
-fn with_tracked<'db>(db: &'db dyn Db, tracked: MyTracked<'db>) -> bool {
+fn with_tracked<'db>(db: &'db dyn Db, tracked: MyTracked<'db>) -> u64 {
     tracked.field(db).field
 }
 
@@ -66,16 +66,19 @@ fn with_tracked<'db>(db: &'db dyn Db, tracked: MyTracked<'db>) -> bool {
 fn dependent_query() {
     let mut db = salsa::DatabaseImpl::new();
 
-    let input = MyInput::new(&db, true);
+    let input = MyInput::new(&db, 1);
     let tracked = create_tracked(&db, input);
-    assert!(with_tracked(&db, tracked));
+    assert_eq!(with_tracked(&db, tracked), 1);
 
-    input.set_field(&mut db).to(false);
+    input.set_field(&mut db).to(0);
 
     // We now re-run the query that creates the tracked struct.
     //
     // Salsa will re-use the `MyTracked` struct from the previous revision,
     // but practically it has been re-created due to generational ids.
     let tracked = create_tracked(&db, input);
-    assert!(!with_tracked(&db, tracked));
+    assert_eq!(with_tracked(&db, tracked), 0);
+    input.set_field(&mut db).to(2);
+    let tracked = create_tracked(&db, input);
+    assert_eq!(with_tracked(&db, tracked), 2);
 }
diff --git a/tests/tracked_fn_interned_lifetime.rs b/tests/tracked_fn_interned_lifetime.rs
new file mode 100644
index 00000000..99b33af4
--- /dev/null
+++ b/tests/tracked_fn_interned_lifetime.rs
@@ -0,0 +1,14 @@
+#[salsa::interned]
+struct Interned<'db> {
+    field: i32,
+}
+
+#[salsa::tracked]
+fn foo<'a>(_db: &'a dyn salsa::Database, _: Interned<'_>, _: Interned<'a>) {}
+
+#[test]
+fn the_test() {
+    let db = salsa::DatabaseImpl::new();
+    let i = Interned::new(&db, 123);
+    foo(&db, i, i);
+}