Keep the latest Guardian risk score during concurrent sampling (#38580)

## Why

Concurrent Guardian samples can finish out of order, allowing an older sample to
replace the thread's newer in-memory security risk score.

## What changed

- Record when each `SecurityRiskScore` sample starts while preserving compatibility
  with records that have no timestamp.
- Add `ExtensionData::insert_if` so checking and replacing a typed attachment happen
  under the same lock.
- Update Guardian V2 to replace the thread's risk score only when the completed sample
  is newer than the stored one.

## Testing

- Verify concurrent conditional inserts retain the newest value.
- Verify Guardian-generated risk scores include a sampling timestamp.

GitOrigin-RevId: 6543a4e5497da4c623331050789e91613f931c4b
This commit is contained in:
jif
2026-08-14 13:44:51 +00:00
committed by copyberry
parent 42d842a91a
commit 2a452d7dc1
11 changed files with 76 additions and 2 deletions
+19
View File
@@ -104,6 +104,25 @@ impl ExtensionData {
.map(downcast_data)
}
/// Stores `value` only when `should_insert` accepts the current attachment.
///
/// The predicate and insertion happen while this map is locked, so concurrent
/// callers cannot replace a value after checking a stale attachment.
pub fn insert_if<T>(&self, value: T, should_insert: impl FnOnce(Option<&T>) -> bool) -> bool
where
T: Any + Send + Sync,
{
let mut entries = self.entries();
let existing = entries
.get(&TypeId::of::<T>())
.map(|value| downcast_data::<T>(Arc::clone(value)));
if !should_insert(existing.as_deref()) {
return false;
}
entries.insert(TypeId::of::<T>(), Arc::new(value));
true
}
/// Removes and returns the attached value of type `T`, if one exists.
pub fn remove<T>(&self) -> Option<Arc<T>>
where