Skip to content

Commit 5ce6735

Browse files
committed
remove created_at field from tracked structs
1 parent 4580a10 commit 5ce6735

File tree

2 files changed

+44
-53
lines changed

2 files changed

+44
-53
lines changed

src/tracked_struct.rs

Lines changed: 36 additions & 41 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,27 @@ 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+
564+
// SAFETY: We already set `updated_at` to `None` to acquire the lock.
565+
self.delete_entity(zalsa, id, true);
566+
567+
id = id.with_generation(id.generation() + 1);
580568
}
581569
}
582570
if current_deps.durability < data.durability {
583571
data.revisions = C::new_revisions(current_deps.changed_at);
584-
data.created_at = current_revision;
585572
}
586573
data.durability = current_deps.durability;
587574
let swapped_out = data.updated_at.swap(Some(current_revision));
588575
assert!(swapped_out.is_none());
576+
577+
id
589578
})
590579
}
591580

@@ -609,7 +598,11 @@ where
609598
/// Using this method on an entity id that MAY be used in the current revision will lead to
610599
/// unspecified results (but not UB). See [`InternedIngredient::delete_index`] for more
611600
/// discussion and important considerations.
612-
pub(crate) fn delete_entity(&self, zalsa: &Zalsa, id: Id) {
601+
///
602+
/// # Safety
603+
///
604+
/// If `locked == true` is passed, the `updated_at` field must already be set to `None`.
605+
pub(crate) unsafe fn delete_entity(&self, zalsa: &Zalsa, id: Id, locked: bool) {
613606
zalsa.event(&|| {
614607
Event::new(crate::EventKind::DidDiscard {
615608
key: self.database_key_index(id),
@@ -619,7 +612,8 @@ where
619612
let current_revision = zalsa.current_revision();
620613
let data_raw = Self::data_raw(zalsa.table(), id);
621614

622-
data_raw.with(|data| {
615+
if !locked {
616+
data_raw.with(|data| {
623617
let data = data.cast::<Value<C>>();
624618

625619
// We want to set `updated_at` to `None`, signalling that other field values
@@ -639,6 +633,7 @@ where
639633
}
640634
}
641635
});
636+
}
642637

643638
let mut memo_table = data_raw.with_mut(|data| {
644639
// SAFETY: We have acquired the write lock
@@ -767,15 +762,13 @@ where
767762

768763
unsafe fn maybe_changed_after(
769764
&self,
770-
db: &dyn Database,
771-
input: Id,
772-
revision: Revision,
765+
_db: &dyn Database,
766+
_input: Id,
767+
_revision: Revision,
773768
_cycle_heads: &mut CycleHeads,
774769
) -> VerifyResult {
775-
let zalsa = db.zalsa();
776-
let data = Self::data(zalsa.table(), input);
777-
778-
VerifyResult::changed_if(data.created_at > revision)
770+
// Any change to a tracked struct results in a new ID generation.
771+
VerifyResult::unchanged()
779772
}
780773

781774
fn mark_validated_output(
@@ -799,7 +792,9 @@ where
799792
// `executor` creates a tracked struct `salsa_output_key`,
800793
// but it did not in the current revision.
801794
// In that case, we can delete `stale_output_key` and any data associated with it.
802-
self.delete_entity(zalsa, stale_output_key);
795+
//
796+
// SAFETY: `delete_entity` is always sound to call with `locked == false`.
797+
unsafe { self.delete_entity(zalsa, stale_output_key, false) };
803798
}
804799

805800
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)