Skip to content

Commit 4920a8a

Browse files
committed
remove created_at field from tracked structs
1 parent 4580a10 commit 4920a8a

File tree

2 files changed

+40
-54
lines changed

2 files changed

+40
-54
lines changed

src/tracked_struct.rs

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -271,15 +271,6 @@ where
271271
/// with different values.
272272
durability: Durability,
273273

274-
/// The revision in which the tracked struct was first created.
275-
///
276-
/// Unlike `updated_at`, which gets bumped on every read,
277-
/// `created_at` is updated whenever an untracked field is updated.
278-
/// This is necessary to detect reused tracked struct ids _after_
279-
/// they've been freed in a prior revision or tracked structs that have been updated
280-
/// in-place because of a bad `Hash` implementation.
281-
created_at: Revision,
282-
283274
/// The revision when this tracked struct was last updated.
284275
/// This field also acts as a kind of "lock". Once it is equal
285276
/// to `Some(current_revision)`, the fields are locked and
@@ -400,7 +391,7 @@ where
400391
tracing::trace!("Reuse tracked struct {id:?}", id = index);
401392
// The struct already exists in the intern map.
402393
zalsa_local.add_output(index);
403-
self.update(zalsa, current_revision, id, &current_deps, fields);
394+
let id = self.update(zalsa, current_revision, id, &current_deps, fields);
404395
FromId::from_id(id)
405396
}
406397

@@ -425,7 +416,6 @@ where
425416
fields: C::Fields<'db>,
426417
) -> Id {
427418
let value = |_| Value {
428-
created_at: current_revision,
429419
updated_at: OptionalAtomicRevision::new(Some(current_revision)),
430420
durability: current_deps.durability,
431421
// lifetime erase for storage
@@ -434,17 +424,11 @@ where
434424
memos: Default::default(),
435425
};
436426

437-
while let Some(id) = self.free_list.pop() {
427+
if let Some(id) = self.free_list.pop() {
438428
// Increment the ID generation before reusing it, as if we have allocated a new
439429
// slot in the table.
440-
//
441-
// If the generation will wrap, we are forced to leak the slot. We reserve enough
442-
// bits for the generation that this should not be a problem in practice.
443-
let Some(generation) = id.generation().checked_add(1) else {
444-
continue;
445-
};
446-
447-
let id = id.with_generation(generation);
430+
let id = id.with_generation(id.generation() + 1);
431+
448432
return Self::data_raw(zalsa.table(), id).with_mut(|data_raw| {
449433
let data_raw = data_raw.cast::<Value<C>>();
450434

@@ -477,10 +461,10 @@ where
477461
&'db self,
478462
zalsa: &'db Zalsa,
479463
current_revision: Revision,
480-
id: Id,
464+
mut id: Id,
481465
current_deps: &StampedValue<()>,
482466
fields: C::Fields<'db>,
483-
) {
467+
) -> Id {
484468
let data_raw = Self::data_raw(zalsa.table(), id);
485469

486470
// The protocol is:
@@ -547,7 +531,7 @@ where
547531
});
548532

549533
if !locked {
550-
return;
534+
return id;
551535
}
552536

553537
data_raw.with_mut(|data| {
@@ -570,22 +554,25 @@ where
570554
),
571555
fields,
572556
) {
573-
// Consider this a new tracked-struct (even though it still uses the same id)
574-
// when any non-tracked field got updated.
575-
// This should be rare and only ever happen if there's a hash collision
576-
// which makes Salsa consider two tracked structs to still be the same
577-
// even though the fields are different.
578-
// See `tracked-struct-id-field-bad-hash` for more details.
579-
data.created_at = current_revision;
557+
// Consider this a new tracked-struct when any non-tracked field got updated.
558+
// This should be rare and only ever happen if there's a hash collision.
559+
//
560+
// Note that we hold the lock and have exclusive access to the tracked struct data,
561+
// so there should be no live instances of IDs from the previous generation. We clear
562+
// the memos and return a new ID here as if we have allocated a new slot.
563+
let mut table = data.take_memo_table();
564+
self.clear_memos(zalsa, &mut table, id);
565+
id = id.with_generation(id.generation() + 1);
580566
}
581567
}
582568
if current_deps.durability < data.durability {
583569
data.revisions = C::new_revisions(current_deps.changed_at);
584-
data.created_at = current_revision;
585570
}
586571
data.durability = current_deps.durability;
587572
let swapped_out = data.updated_at.swap(Some(current_revision));
588573
assert!(swapped_out.is_none());
574+
575+
id
589576
})
590577
}
591578

@@ -645,21 +632,26 @@ where
645632
unsafe { (*data).assume_init_mut() }.take_memo_table()
646633
});
647634

635+
self.clear_memos(zalsa, &mut memo_table, id);
636+
}
637+
638+
/// Clears the given memo table.
639+
pub(crate) fn clear_memos(&self, zalsa: &Zalsa, memo_table: &mut MemoTable, id: Id) {
648640
// SAFETY: We use the correct types table.
649-
let table = unsafe { self.memo_table_types.attach_memos_mut(&mut memo_table) };
641+
let table = unsafe { self.memo_table_types.attach_memos_mut(memo_table) };
650642

651643
// `Database::salsa_event` is a user supplied callback which may panic
652644
// in that case we need a drop guard to free the memo table
653645
struct TableDropGuard<'a>(MemoTableWithTypesMut<'a>);
654646
impl Drop for TableDropGuard<'_> {
655647
fn drop(&mut self) {
656-
// SAFETY: We have verified that no more references to these memos exist and so we are good
648+
// SAFETY: We have `&mut MemoTable`, so no more references to these memos exist and we are good
657649
// to drop them.
658650
unsafe { self.0.drop() };
659651
}
660652
}
661653
let mut table_guard = TableDropGuard(table);
662-
// SAFETY: We have verified that no more references to these memos exist and so we are good
654+
// SAFETY: We have `&mut MemoTable`, so no more references to these memos exist and we are good
663655
// to drop them.
664656
unsafe {
665657
table_guard.0.take_memos(|memo_ingredient_index, memo| {
@@ -767,15 +759,13 @@ where
767759

768760
unsafe fn maybe_changed_after(
769761
&self,
770-
db: &dyn Database,
771-
input: Id,
772-
revision: Revision,
762+
_db: &dyn Database,
763+
_input: Id,
764+
_revision: Revision,
773765
_cycle_heads: &mut CycleHeads,
774766
) -> VerifyResult {
775-
let zalsa = db.zalsa();
776-
let data = Self::data(zalsa.table(), input);
777-
778-
VerifyResult::changed_if(data.created_at > revision)
767+
// Any change to a tracked struct results in a new ID generation.
768+
VerifyResult::unchanged()
779769
}
780770

781771
fn mark_validated_output(
@@ -799,7 +789,7 @@ where
799789
// `executor` creates a tracked struct `salsa_output_key`,
800790
// but it did not in the current revision.
801791
// In that case, we can delete `stale_output_key` and any data associated with it.
802-
self.delete_entity(zalsa, stale_output_key);
792+
self.delete_entity(zalsa, stale_output_key)
803793
}
804794

805795
fn debug_name(&self) -> &'static str {

tests/tracked-struct-id-field-bad-hash.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! Test for a tracked struct where an untracked field has a
22
//! very poorly chosen hash impl (always returns 0).
3-
//! This demonstrates that the untracked fields on a struct
4-
//! can change values and yet the struct can have the same
5-
//! id (because struct ids are based on the *hash* of the
6-
//! untracked fields).
3+
//!
4+
//! This demonstrates that tracked struct ids will always change if
5+
//! untracked fields on a struct change values, because although struct
6+
//! ids are based on the *hash* of the untracked fields, ids are generational
7+
//! based on the field values.
78
89
use salsa::{Database as Db, Setter};
910
use test_log::test;
@@ -62,10 +63,6 @@ fn with_tracked<'db>(db: &'db dyn Db, tracked: MyTracked<'db>) -> bool {
6263
}
6364

6465
#[test]
65-
// Because tracked struct IDs are generational, the ID of the second tracked struct ends
66-
// up being different than the ID of the first. However, this test still has precedence
67-
// and represents valid behavior for IDs that get leaked.
68-
#[ignore]
6966
fn dependent_query() {
7067
let mut db = salsa::DatabaseImpl::new();
7168

@@ -76,10 +73,9 @@ fn dependent_query() {
7673
input.set_field(&mut db).to(false);
7774

7875
// We now re-run the query that creates the tracked struct.
79-
// Salsa will re-use the `MyTracked` struct from the previous revision
80-
// because it thinks it is unchanged because of `BadHash`'s bad hash function.
81-
// That's why Salsa updates the `MyTracked` struct in-place and the struct
82-
// should be considered re-created even though it still has the same id.
76+
//
77+
// Salsa will re-use the `MyTracked` struct from the previous revision,
78+
// but practically it has been re-created due to generational ids.
8379
let tracked = create_tracked(&db, input);
8480
assert!(!with_tracked(&db, tracked));
8581
}

0 commit comments

Comments
 (0)