Skip to content

Commit 25a5fb8

Browse files
committed
feat(left-right-tlcache): cache entry invalidation
The existing cache relies on a single criteria to invalidate an entry: if the ReadHandle<T> cannot be "entered", then it is invalid and the provider needs to be asked for a refresh; which may fail or not, depending on whether a factory for the key exists or not. This behavior suffices to invalidate entries (read handles) when the wrapped objects are removed or created in the provider. However, providers (and hence caches) may have "alias" keys. An example is a Fib object that can be identified by both an Id (master key) but also a Vni. In such a case, the cache may itself have two cached entries, pointing to the same readhandle. So far so good. However, if such aliases get exchanged without the objects being torn down, they will remain in the cache indefinitely, pointing to the wrong object, because they will not be invalidated. In the example of a Fib, suppose 2 Fibs with ids A and B. A is accessible via vni 10 and B only with its Id. At some point, there is a configuration change such that Fib A no longer uses vni 10, but B is assigned vni 10, and both Fibs continue to exist.Without a mechanism to invalidate the entry with key vni = 10, the cache would return a readhandle for A instead of B. Invalidating such entries (vni 10 -> Rhandle(A)) can be tricky because we don't want to poll the provider for a handle every time (which would defeat the whole point of the cache) and because the provider itself may be wrapped in left-right, meaning that changes may not immediately appear in readers. Some mechanism is needed to be able to tell if a cached entry points to the "right" object, or has to be otherwise invalidated / refreshed. This patch augments the implementation to address that case by: - defining the identity of a T (in ReadHandle<T>) with a trait - augmenting cached entries to store that identity, which may differ from the key used to access them - invalidating entries by detecting if their identity has changed. This is achieved by comparing the "cached identity" (the identity the cache believes the object accessed via the readhandle has) against the identity it sees when accessing the object. The only additional requirement is implementing trait Identity for T. Signed-off-by: Fredi Raspall <[email protected]>
1 parent 7589590 commit 25a5fb8

File tree

1 file changed

+57
-23
lines changed

1 file changed

+57
-23
lines changed

left-right-tlcache/src/lib.rs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ pub trait ReadHandleProvider {
4747
fn get_factory(&self, key: &Self::Key) -> Option<&ReadHandleFactory<Self::Data>>;
4848
}
4949

50+
/// Trait to determine the real identity of a `T` wrapped in left-right. That is,
51+
/// the identity of `T` in a `ReadHandle<T>`. This is needed to invalidate cache entries
52+
/// with keys that are alias of their identity.
53+
pub trait Identity<K> {
54+
fn identity(&self) -> K;
55+
}
56+
5057
#[derive(Debug, PartialEq, Error)]
5158
pub enum ReadHandleCacheError<K> {
5259
#[error("Reader not found for key {0}")]
@@ -55,12 +62,44 @@ pub enum ReadHandleCacheError<K> {
5562
NotAccessible(K),
5663
}
5764

58-
pub struct ReadHandleCache<K: Hash + Eq, T> {
59-
handles: RefCell<HashMap<K, Rc<ReadHandle<T>>, RandomState>>,
65+
/// An entry in a thread-local `ReadHandleCache<K,T>` to hold a `ReadHandle<T>`
66+
/// along with its identity, which may match the key to find it or not.
67+
struct ReadHandleEntry<T, K> {
68+
rhandle: Rc<ReadHandle<T>>,
69+
identity: K,
70+
}
71+
impl<T: Identity<K>, K: PartialEq> ReadHandleEntry<T, K> {
72+
fn new(identity: K, rhandle: Rc<ReadHandle<T>>) -> Self {
73+
Self { rhandle, identity }
74+
}
75+
/// A cached entry is valid if the ReadHandle<T> can be entered and the
76+
/// object we'd enter into has the identity we believe it has.
77+
fn is_valid(&self) -> bool {
78+
if self.rhandle.was_dropped() {
79+
return false;
80+
}
81+
match self.rhandle.enter() {
82+
Some(t) => t.as_ref().identity() == self.identity,
83+
None => false,
84+
}
85+
}
86+
}
87+
impl<T, K: Clone> Clone for ReadHandleEntry<T, K> {
88+
fn clone(&self) -> Self {
89+
Self {
90+
rhandle: self.rhandle.clone(),
91+
identity: self.identity.clone(),
92+
}
93+
}
94+
}
95+
96+
pub struct ReadHandleCache<K: Hash + Eq + Clone, T> {
97+
handles: RefCell<HashMap<K, ReadHandleEntry<T, K>, RandomState>>,
6098
}
6199
impl<K, T> ReadHandleCache<K, T>
62100
where
63101
K: Hash + Eq + Clone,
102+
T: Identity<K>,
64103
{
65104
#[allow(clippy::new_without_default)]
66105
pub fn new() -> Self {
@@ -77,37 +116,32 @@ where
77116
let mut map = local.handles.borrow_mut();
78117

79118
// cache has a valid handle for that key
80-
if let Some(rhandle) = map.get(&key)
81-
&& !rhandle.was_dropped()
119+
if let Some(entry) = map.get(&key)
120+
&& entry.is_valid()
82121
{
83-
return Ok(Rc::clone(rhandle));
122+
return Ok(Rc::clone(&entry.rhandle));
84123
}
85124

86-
// Either we've found a handle but was invalid or we did not find it.
87-
// In either case, request a fresh handle to the provider, which may:
88-
// 1) fail to provide one with that key
89-
// 2) provide a valid one
90-
// 3) provide an invalid one (e.g. if the `WriteHandle<T>` was dropped).
91-
// We don't require providers to provide good readhandles/factories, nor
92-
// want to return invalid handles to callers, so we validate them too.
93-
// A valid readhandle is one where, at least now, can be 'entered'.
94-
95125
let result = {
126+
// get a factory for the key from the provider and build a fresh handle from it
96127
let fresh = provider
97128
.get_factory(&key)
98129
.map(|factory| factory.handle())
99130
.ok_or_else(|| ReadHandleCacheError::NotFound(key.clone()))?;
100131

101-
if fresh.was_dropped() {
102-
Err(ReadHandleCacheError::NotAccessible(key.clone()))
103-
} else {
104-
let fresh = Rc::new(fresh);
105-
map.entry(key.clone())
106-
.and_modify(|e| *e = Rc::clone(&fresh))
107-
.or_insert_with(|| Rc::clone(&fresh));
132+
// get master identity
133+
let identity = fresh
134+
.enter()
135+
.as_ref()
136+
.ok_or_else(|| ReadHandleCacheError::NotAccessible(key.clone()))?
137+
.as_ref()
138+
.identity();
108139

109-
Ok(fresh)
110-
}
140+
// store a new entry locally with a handle, its identity and the key
141+
let rhandle = Rc::new(fresh);
142+
let entry = ReadHandleEntry::new(identity, Rc::clone(&rhandle));
143+
map.insert(key.clone(), entry);
144+
Ok(rhandle)
111145
};
112146
if result.is_err() {
113147
// clean-up cache on failure

0 commit comments

Comments
 (0)