psylend_cpi/state/
cache.rs

1// Various PsyLend structs use a cache to store data on-chain. The cache is valid for some TTL,
2// expressed in slots, which varies by the data being stored.
3use bytemuck::{Pod, Zeroable};
4
5#[derive(Clone, Copy)]
6#[repr(C)]
7pub struct Cache<T, const TTL: u64> {
8    /// The value being cached
9    value: T,
10
11    /// The last slot that this information was updated in
12    last_updated: u64,
13
14    /// Whether the value has been manually invalidated
15    invalidated: u8,
16
17    _reserved: [u8; 7],
18}
19
20// Since the `Cache` type uses generic parameters we can't use the derive macros
21// which help with some validation. The fields used are all pod-safe types, so
22// a `Cache` should also be pod-safe if the value stored within it is.
23unsafe impl<T, const TTL: u64> Pod for Cache<T, TTL> where T: Pod {}
24unsafe impl<T, const TTL: u64> Zeroable for Cache<T, TTL> where T: Zeroable {}
25
26/// Store calculated values that can be manually invalidated or expire after some number of slots
27/// Methods expect a "current_slot" argument which should indicate which slot the calculation is
28/// relevant for. This is usually the actual current slot but may be an older slot if the value is
29/// used or calculated for a previous slot, for example a partial refresh of the reserve.
30impl<T, const TTL: u64> Cache<T, TTL> {
31    pub fn new(value: T, current_slot: u64) -> Self {
32        Self {
33            value,
34            invalidated: 0,
35            last_updated: current_slot,
36            _reserved: [0; 7],
37        }
38    }
39
40    pub fn validate_fresh(&self, current_slot: u64) -> Result<(), CacheInvalidError> {
41        if current_slot < self.last_updated {
42            return Err(CacheInvalidError::TooNew {
43                msg: self.time_msg(current_slot),
44            });
45        }
46        if current_slot - self.last_updated > TTL {
47            return Err(CacheInvalidError::Expired {
48                msg: self.time_msg(current_slot),
49            });
50        }
51        if self.invalidated != 0 {
52            return Err(CacheInvalidError::Invalidated);
53        }
54        Ok(())
55    }
56
57    fn time_msg(&self, current_slot: u64) -> String {
58        format!(
59            "last_updated = {}, time_to_live = {}, current_slot = {}",
60            self.last_updated, TTL, current_slot
61        )
62    }
63
64    /// If the cache is neither expired nor marked invalid, return the value,
65    /// otherwise return an error indicating why it is stale
66    pub fn try_get(&self, current_slot: u64) -> Result<&T, CacheInvalidError> {
67        self.validate_fresh(current_slot)?;
68        Ok(&self.value)
69    }
70
71    /// If the cache is neither expired nor marked invalid, return the value mutably,
72    /// otherwise return an error indicating why it is stale
73    pub fn try_get_mut(&mut self, current_slot: u64) -> Result<&mut T, CacheInvalidError> {
74        self.validate_fresh(current_slot)?;
75        Ok(&mut self.value)
76    }
77
78    /// If the cache is neither expired nor marked invalid, return the value,
79    /// otherwise panic with an error message describing the item and why it is stale
80    pub fn expect(&self, current_slot: u64, description: &str) -> &T {
81        self.try_get(current_slot).expect(description)
82    }
83
84    /// If the cache is neither expired nor marked invalid, return the value mutably,
85    /// otherwise panic with an error message describing the item and why it is stale
86    pub fn expect_mut(&mut self, current_slot: u64, description: &str) -> &mut T {
87        self.try_get_mut(current_slot).expect(description)
88    }
89
90    /// Returns the current value, regardless of whether or not it is stale
91    pub fn get_stale(&self) -> &T {
92        &self.value
93    }
94
95    /// Returns the current value mutably, regardless of whether or not it is stale.
96    pub fn get_stale_mut(&mut self) -> &mut T {
97        &mut self.value
98    }
99
100    /// Replace the current value and reset the state to the current slot
101    pub fn refresh_as(&mut self, value: T, current_slot: u64) {
102        self.value = value;
103        self.invalidated = 0;
104        self.last_updated = current_slot;
105    }
106
107    /// Marks the data as stale
108    pub fn invalidate(&mut self) {
109        self.invalidated = 1;
110    }
111
112    /// Returns the slot when this data was last updated
113    pub fn last_updated(&self) -> u64 {
114        self.last_updated
115    }
116
117    // TODO this is identical to `refresh_to` and the comment is incorrect (it is mutating)
118    /// Updates the cache to be valid and at current_slot without mutating the value.
119    pub fn refresh(&mut self, current_slot: u64) {
120        self.invalidated = 0;
121        self.last_updated = current_slot;
122    }
123
124    /// Updates the cache to be valid and increments the last updated slot
125    pub fn refresh_additional(&mut self, additional_slots: u64) {
126        self.invalidated = 0;
127        self.last_updated += additional_slots;
128    }
129
130    /// Updates the cache to be valid and sets the last updated slot
131    pub fn refresh_to(&mut self, current_slot: u64) {
132        self.invalidated = 0;
133        self.last_updated = current_slot;
134    }
135}
136
137#[derive(Debug)]
138pub enum CacheInvalidError {
139    /// The cache is too old to use for the current slot.
140    Expired { msg: String },
141
142    /// A calculation was attempted for a slot that is too old to use the cache,
143    /// since the cache was created more recently than the relevant slot.
144    TooNew { msg: String },
145
146    /// The cache has been manually invalidated and may no longer be used.
147    Invalidated,
148}