solana_accounts_db/epoch_accounts_hash/
manager.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
use {
    super::EpochAccountsHash,
    solana_sdk::clock::Slot,
    std::sync::{Condvar, Mutex},
};

/// Manage the epoch accounts hash
///
/// Handles setting when an EAH calculation is requested and when it completes.  Also handles
/// waiting for in-flight calculations to complete when the "stop" Bank must include it.
#[derive(Debug)]
pub struct Manager {
    /// Current state of the epoch accounts hash
    state: Mutex<State>,
    /// This condition variable is used to wait for an in-flight EAH calculation to complete
    cvar: Condvar,
}

impl Manager {
    #[must_use]
    fn _new(state: State) -> Self {
        Self {
            state: Mutex::new(state),
            cvar: Condvar::new(),
        }
    }

    /// Create a new epoch accounts hash manager, with the initial state set to Invalid
    #[must_use]
    pub fn new_invalid() -> Self {
        Self::_new(State::Invalid)
    }

    /// Create a new epoch accounts hash manager, with the initial state set to Valid
    #[must_use]
    pub fn new_valid(epoch_accounts_hash: EpochAccountsHash, slot: Slot) -> Self {
        Self::_new(State::Valid(epoch_accounts_hash, slot))
    }

    /// An epoch accounts hash calculation has been requested; update our state
    pub fn set_in_flight(&self, slot: Slot) {
        let mut state = self.state.lock().unwrap();
        if let State::InFlight(old_slot) = &*state {
            panic!("An epoch accounts hash calculation is already in-flight from slot {old_slot}!");
        }
        *state = State::InFlight(slot);
    }

    /// An epoch accounts hash calculation has completed; update our state
    pub fn set_valid(&self, epoch_accounts_hash: EpochAccountsHash, slot: Slot) {
        let mut state = self.state.lock().unwrap();
        if let State::Valid(old_epoch_accounts_hash, old_slot) = &*state {
            panic!(
                "The epoch accounts hash is already valid! \
                \nold slot: {old_slot}, epoch accounts hash: {old_epoch_accounts_hash:?} \
                \nnew slot: {slot}, epoch accounts hash: {epoch_accounts_hash:?}"
            );
        }
        *state = State::Valid(epoch_accounts_hash, slot);
        self.cvar.notify_all();
    }

    /// Get the epoch accounts hash
    ///
    /// If an EAH calculation is in-flight, then this call will block until it completes.
    pub fn wait_get_epoch_accounts_hash(&self) -> EpochAccountsHash {
        let mut state = self.state.lock().unwrap();
        loop {
            match &*state {
                State::Valid(epoch_accounts_hash, _slot) => break *epoch_accounts_hash,
                State::InFlight(_slot) => state = self.cvar.wait(state).unwrap(),
                State::Invalid => panic!("The epoch accounts hash cannot be awaited when Invalid!"),
            }
        }
    }

    /// Get the epoch accounts hash
    ///
    /// This fn does not block, and will only yield an EAH if the state is `Valid`
    pub fn try_get_epoch_accounts_hash(&self) -> Option<EpochAccountsHash> {
        let state = self.state.lock().unwrap();
        match &*state {
            State::Valid(epoch_accounts_hash, _slot) => Some(*epoch_accounts_hash),
            _ => None,
        }
    }
}

/// The EpochAccountsHash is calculated in the background via AccountsBackgroundService.  This enum
/// is used to track the state of that calculation, and queried when saving the EAH into a Bank or
/// Snapshot.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
    /// The initial state of the EAH.  This can occur when loading from a snapshot that does not
    /// include an EAH, or when starting from genesis (before an EAH calculation is requested).
    Invalid,
    /// An EAH calculation has been requested (for `Slot`) and is in flight.  The Bank that should
    /// save the EAH must wait until the calculation has completed.
    InFlight(Slot),
    /// The EAH calculation is complete (for `Slot`) and the EAH value is valid to read/use.
    Valid(EpochAccountsHash, Slot),
}

#[cfg(test)]
mod tests {
    use {super::*, solana_sdk::hash::Hash, std::time::Duration};

    #[test]
    fn test_new_valid() {
        let epoch_accounts_hash = EpochAccountsHash::new(Hash::new_unique());
        let manager = Manager::new_valid(epoch_accounts_hash, 5678);
        assert_eq!(
            manager.try_get_epoch_accounts_hash(),
            Some(epoch_accounts_hash),
        );
        assert_eq!(manager.wait_get_epoch_accounts_hash(), epoch_accounts_hash);
    }

    #[test]
    fn test_new_invalid() {
        let manager = Manager::new_invalid();
        assert!(manager.try_get_epoch_accounts_hash().is_none());
    }

    #[test]
    fn test_try_get_epoch_accounts_hash() {
        let epoch_accounts_hash = EpochAccountsHash::new(Hash::new_unique());
        for (state, expected) in [
            (State::Invalid, None),
            (State::InFlight(123), None),
            (
                State::Valid(epoch_accounts_hash, 5678),
                Some(epoch_accounts_hash),
            ),
        ] {
            let manager = Manager::_new(state);
            let actual = manager.try_get_epoch_accounts_hash();
            assert_eq!(actual, expected);
        }
    }

    #[test]
    fn test_wait_epoch_accounts_hash() {
        // Test: State is Valid, no need to wait
        {
            let epoch_accounts_hash = EpochAccountsHash::new(Hash::new_unique());
            let manager = Manager::new_valid(epoch_accounts_hash, 5678);
            assert_eq!(manager.wait_get_epoch_accounts_hash(), epoch_accounts_hash);
        }

        // Test: State is InFlight, must wait
        {
            let epoch_accounts_hash = EpochAccountsHash::new(Hash::new_unique());
            let manager = Manager::new_invalid();
            manager.set_in_flight(123);

            std::thread::scope(|s| {
                s.spawn(|| {
                    std::thread::sleep(Duration::from_secs(1));
                    manager.set_valid(epoch_accounts_hash, 5678)
                });
                assert!(manager.try_get_epoch_accounts_hash().is_none());
                assert_eq!(manager.wait_get_epoch_accounts_hash(), epoch_accounts_hash);
                assert!(manager.try_get_epoch_accounts_hash().is_some());
            });
        }
    }

    #[test]
    #[should_panic]
    fn test_wait_epoch_accounts_hash_invalid() {
        // Test: State is Invalid, should panic
        let manager = Manager::new_invalid();
        let _epoch_accounts_hash = manager.wait_get_epoch_accounts_hash();
    }
}