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
177
178
179
180
181
182
183
//! Module to provide caching support for the relayer.
//!
//! Utilizes the [`moka`](https://docs.rs/moka) crate, which provides full
//! concurrency of retrievals and a high expected concurrency for updates.
use core::fmt::Formatter;
use std::fmt;
use std::time::Duration;

use moka::sync::Cache as MokaCache;

use ibc::core::ics02_client::client_state::AnyClientState;
use ibc::core::ics02_client::height::Height;
use ibc::core::ics03_connection::connection::ConnectionEnd;
use ibc::core::ics04_channel::channel::ChannelEnd;
use ibc::core::ics24_host::identifier::{ClientId, ConnectionId, PortChannelId};

const CHANNEL_CACHE_TTL: Duration = Duration::from_secs(60);
const CONNECTION_CACHE_TTL: Duration = Duration::from_secs(10 * 60);
const CLIENT_STATE_CACHE_TTL: Duration = Duration::from_millis(500);
const LATEST_HEIGHT_CACHE_TTL: Duration = Duration::from_millis(200);

const CHANNEL_CACHE_CAPACITY: u64 = 10_000;
const CONNECTION_CACHE_CAPACITY: u64 = 10_000;
const CLIENT_STATE_CACHE_CAPACITY: u64 = 10_000;

/// Whether or not a result was in cache (ie. a cache hit)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CacheStatus {
    Hit,
    Miss,
}

/// Alias for a result and its cache status.
pub type CacheResult<A, E> = Result<(A, CacheStatus), E>;

/// The main cache data structure, which comprises multiple sub-caches for caching
/// different chain components, each with different time-to-live values.
///
/// There should be one `Cache` instantiated per every chain runtime.
#[derive(Clone)]
pub struct Cache {
    /// Cache storing [`ChannelEnd`]s keyed by their [`PortChannelId`]s.
    channels: MokaCache<PortChannelId, ChannelEnd>,
    /// Cache storing [`ConnectionEnd`]s keyed by their [`ConnectionId`]s.
    connections: MokaCache<ConnectionId, ConnectionEnd>,
    /// Cache storing [`AnyClientState`]s keyed by their [`ClientId`]s.
    client_states: MokaCache<ClientId, AnyClientState>,
    /// The latest `Height` associated with the chain runtime this `Cache` is associated with.
    latest_height: MokaCache<(), Height>,
}

impl Default for Cache {
    fn default() -> Self {
        Self::new()
    }
}

impl Cache {
    /// Initializes a new empty [`Cache`] with default time-to-live values.
    pub fn new() -> Cache {
        let channels = MokaCache::builder()
            .time_to_live(CHANNEL_CACHE_TTL)
            .max_capacity(CHANNEL_CACHE_CAPACITY)
            .build();

        let connections = MokaCache::builder()
            .time_to_live(CONNECTION_CACHE_TTL)
            .max_capacity(CONNECTION_CACHE_CAPACITY)
            .build();

        let client_states = MokaCache::builder()
            .time_to_live(CLIENT_STATE_CACHE_TTL)
            .max_capacity(CLIENT_STATE_CACHE_CAPACITY)
            .build();

        let latest_height = MokaCache::builder()
            .time_to_live(LATEST_HEIGHT_CACHE_TTL)
            .max_capacity(1)
            .build();

        Cache {
            channels,
            connections,
            client_states,
            latest_height,
        }
    }

    /// Return a cached [`ChannelEnd`] via its [`PortChannelId`] if it exists in the cache.
    /// Otherwise, attempts to fetch it via the supplied fetcher function `F`. If `F`
    /// returns successfully with the channel end in an open state, a copy of it is stored in
    /// the cache before it is returned.
    pub fn get_or_try_insert_channel_with<F, E>(
        &self,
        id: &PortChannelId,
        f: F,
    ) -> CacheResult<ChannelEnd, E>
    where
        F: FnOnce() -> Result<ChannelEnd, E>,
    {
        if let Some(chan) = self.channels.get(id) {
            // If cache hit, return it.
            Ok((chan, CacheStatus::Hit))
        } else {
            // Only cache a channel end if the channel is open.
            let chan = f()?;
            if chan.state().is_open() {
                self.channels.insert(id.clone(), chan.clone());
            }
            Ok((chan, CacheStatus::Miss))
        }
    }

    /// Return a cached [`ConnectionEnd`] via its [`ConnectionId`] if it exists in the cache.
    /// Otherwise, attempts to fetch it via the supplied fetcher function `F`. If `F`
    /// returns successfully with the connection end in an open state, a copy of it is
    /// in the cache before it is returned.
    pub fn get_or_try_insert_connection_with<F, E>(
        &self,
        id: &ConnectionId,
        f: F,
    ) -> CacheResult<ConnectionEnd, E>
    where
        F: FnOnce() -> Result<ConnectionEnd, E>,
    {
        if let Some(conn) = self.connections.get(id) {
            Ok((conn, CacheStatus::Hit))
        } else {
            let conn = f()?;
            if conn.state().is_open() {
                self.connections.insert(id.clone(), conn.clone());
            }
            Ok((conn, CacheStatus::Miss))
        }
    }

    /// Return a cached [`AnyClientState`] via its [`ClientId`] if it exists in the cache.
    /// Otherwise, attempts to fetch it via the supplied fetcher function `F`. If `F`
    /// returns successfully with the client state, a copy of it is stored in the cache
    /// before it is returned.
    pub fn get_or_try_insert_client_state_with<F, E>(
        &self,
        id: &ClientId,
        f: F,
    ) -> CacheResult<AnyClientState, E>
    where
        F: FnOnce() -> Result<AnyClientState, E>,
    {
        if let Some(state) = self.client_states.get(id) {
            Ok((state, CacheStatus::Hit))
        } else {
            let state = f()?;
            self.client_states.insert(id.clone(), state.clone());
            Ok((state, CacheStatus::Miss))
        }
    }

    /// Returns the latest [`Height`] value if it exists in the cache.
    /// Otherwise, attempts to fetch it via the supplied fetcher function `F`. If
    /// `F` returns successfully with the latest height, a copy of it is stored in the
    /// cache before it is returned.
    ///
    /// This value is cached with a small time-to-live so that the latest height
    /// query returns the same height if the same query is repeated within a small time frame.
    pub fn get_or_try_update_latest_height_with<F, E>(&self, f: F) -> CacheResult<Height, E>
    where
        F: FnOnce() -> Result<Height, E>,
    {
        if let Some(height) = self.latest_height.get(&()) {
            Ok((height, CacheStatus::Hit))
        } else {
            let height = f()?;
            self.latest_height.insert((), height);
            Ok((height, CacheStatus::Miss))
        }
    }
}

impl fmt::Debug for Cache {
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Cache").finish_non_exhaustive()
    }
}