@@ -5,9 +5,10 @@ use lazy_static::lazy_static;
5
5
use stable_hash:: crypto:: SetHasher ;
6
6
use stable_hash:: prelude:: * ;
7
7
use stable_hash:: utils:: stable_hash;
8
- use std:: collections:: { BTreeMap , HashMap , HashSet } ;
8
+ use std:: collections:: { BTreeMap , HashMap , HashSet , VecDeque } ;
9
9
use std:: iter;
10
10
use std:: sync:: atomic:: AtomicBool ;
11
+ use std:: sync:: RwLock ;
11
12
use std:: time:: Instant ;
12
13
13
14
use graph:: prelude:: * ;
@@ -19,13 +20,18 @@ use crate::prelude::*;
19
20
use crate :: query:: ast as qast;
20
21
use crate :: schema:: ast as sast;
21
22
use crate :: values:: coercion;
22
- use lru_time_cache:: LruCache ;
23
- use std:: sync:: Mutex ;
24
23
25
24
type QueryHash = <SetHasher as StableHasher >:: Out ;
26
25
26
+ #[ derive( Debug ) ]
27
+ struct CacheByBlock {
28
+ block : EthereumBlockPointer ,
29
+ cache : BTreeMap < QueryHash , BTreeMap < String , q:: Value > > ,
30
+ }
31
+
27
32
lazy_static ! {
28
33
// Comma separated subgraph ids to cache queries for.
34
+ // If `*` is present in the list, queries are cached for all subgraphs.
29
35
static ref CACHED_SUBGRAPH_IDS : Vec <String > = {
30
36
std:: env:: var( "GRAPH_CACHED_SUBGRAPH_IDS" )
31
37
. unwrap_or_default( )
@@ -34,18 +40,22 @@ lazy_static! {
34
40
. collect( )
35
41
} ;
36
42
37
- // Cache expiry time, 1 minute by default.
38
- static ref CACHE_EXPIRY : Duration = {
39
- std:: env:: var( "GRAPH_CACHE_EXPIRY_SECS" )
40
- . unwrap_or( "60" . to_string( ) )
41
- . parse:: <u64 >( )
42
- . map( |s| Duration :: from_secs( s) )
43
- . unwrap( )
43
+ static ref CACHE_ALL : bool = CACHED_SUBGRAPH_IDS . contains( & "*" . to_string( ) ) ;
44
+
45
+ // How many blocks should be kept in the query cache. When the limit is reached, older blocks
46
+ // are evicted. This should be kept small since a lookup to the cache is O(n) on this value, and
47
+ // the cache memory usage also increases with larger number. Set to 0 to disable the cache,
48
+ // defaults to disabled.
49
+ static ref QUERY_CACHE_BLOCKS : usize = {
50
+ std:: env:: var( "GRAPH_QUERY_CACHE_BLOCKS" )
51
+ . unwrap_or( "0" . to_string( ) )
52
+ . parse:: <usize >( )
53
+ . expect( "Invalid value for GRAPH_QUERY_CACHE_BLOCKS environment variable" )
44
54
} ;
45
55
46
- // This cache might serve stale data .
47
- static ref QUERY_CACHE : Mutex < LruCache < QueryHash , BTreeMap < String , q :: Value >>>
48
- = Mutex :: new( LruCache :: with_expiry_duration ( * CACHE_EXPIRY ) ) ;
56
+ // News blocks go on the front, so the oldest block will be at the back .
57
+ // This `VecDeque` works as a ring buffer with a capacity of `QUERY_CACHE_BLOCKS`.
58
+ static ref QUERY_CACHE : RwLock < VecDeque < CacheByBlock >> = RwLock :: new( VecDeque :: new ( ) ) ;
49
59
}
50
60
51
61
struct HashableQuery < ' a > {
@@ -178,25 +188,34 @@ pub fn execute_root_selection_set(
178
188
ctx : & ExecutionContext < impl Resolver > ,
179
189
selection_set : & q:: SelectionSet ,
180
190
root_type : & s:: ObjectType ,
191
+ block_ptr : Option < EthereumBlockPointer > ,
181
192
) -> Result < BTreeMap < String , q:: Value > , Vec < QueryExecutionError > > {
182
193
// Cache the cache key to not have to calculate it twice - once for lookup
183
194
// and once for insert.
184
195
let mut key: Option < QueryHash > = None ;
185
196
186
- if CACHED_SUBGRAPH_IDS . contains ( & ctx. query . schema . id ) {
187
- // Calculate the hash outside of the lock
188
- key = Some ( cache_key ( ctx, selection_set) ) ;
189
-
190
- // Check if the response is cached.
191
- let mut cache = QUERY_CACHE . lock ( ) . unwrap ( ) ;
192
-
193
- // Remove expired entries.
194
- cache. notify_iter ( ) ;
197
+ if * CACHE_ALL || CACHED_SUBGRAPH_IDS . contains ( & ctx. query . schema . id ) {
198
+ if let Some ( block_ptr) = block_ptr {
199
+ // JSONB and metadata queries use `BLOCK_NUMBER_MAX`. Ignore this case for two reasons:
200
+ // - Metadata queries are not cacheable.
201
+ // - Caching `BLOCK_NUMBER_MAX` would make this cache think all other blocks are old.
202
+ if block_ptr. number != BLOCK_NUMBER_MAX as u64 {
203
+ // Calculate the hash outside of the lock
204
+ let cache_key = cache_key ( ctx, selection_set) ;
205
+
206
+ // Check if the response is cached.
207
+ let cache = QUERY_CACHE . read ( ) . unwrap ( ) ;
208
+
209
+ // Iterate from the most recent block looking for a block that matches.
210
+ if let Some ( cache_by_block) = cache. iter ( ) . find ( |c| c. block == block_ptr) {
211
+ if let Some ( response) = cache_by_block. cache . get ( & cache_key) {
212
+ ctx. cached . store ( true , std:: sync:: atomic:: Ordering :: SeqCst ) ;
213
+ return Ok ( response. clone ( ) ) ;
214
+ }
215
+ }
195
216
196
- // Peek because we want even hot entries to invalidate after the expiry period.
197
- if let Some ( response) = cache. peek ( key. as_ref ( ) . unwrap ( ) ) {
198
- ctx. cached . store ( true , std:: sync:: atomic:: Ordering :: SeqCst ) ;
199
- return Ok ( response. clone ( ) ) ;
217
+ key = Some ( cache_key) ;
218
+ }
200
219
}
201
220
}
202
221
@@ -244,10 +263,36 @@ pub fn execute_root_selection_set(
244
263
) ?) ;
245
264
}
246
265
247
- if let Some ( key) = key {
248
- // Insert into the cache.
249
- let mut cache = QUERY_CACHE . lock ( ) . unwrap ( ) ;
250
- cache. insert ( key, values. clone ( ) ) ;
266
+ // Check if this query should be cached.
267
+ if let ( Some ( key) , Some ( block_ptr) ) = ( key, block_ptr) {
268
+ let mut cache = QUERY_CACHE . write ( ) . unwrap ( ) ;
269
+
270
+ // If there is already a cache by the block of this query, just add it there.
271
+ if let Some ( cache_by_block) = cache. iter_mut ( ) . find ( |c| c. block == block_ptr) {
272
+ cache_by_block. cache . insert ( key, values. clone ( ) ) ;
273
+ } else if * QUERY_CACHE_BLOCKS > 0 {
274
+ // We're creating a new `CacheByBlock` if:
275
+ // - There are none yet, this is the first query being cached, or
276
+ // - `block_ptr` is of higher or equal number than the most recent block in the cache.
277
+ // Otherwise this is a historical query which will not be cached.
278
+ let should_insert = match cache. iter ( ) . next ( ) {
279
+ None => true ,
280
+ Some ( highest) if highest. block . number <= block_ptr. number => true ,
281
+ Some ( _) => false ,
282
+ } ;
283
+
284
+ if should_insert {
285
+ if cache. len ( ) == * QUERY_CACHE_BLOCKS {
286
+ // At capacity, so pop the oldest block.
287
+ cache. pop_back ( ) ;
288
+ }
289
+
290
+ cache. push_front ( CacheByBlock {
291
+ block : block_ptr,
292
+ cache : BTreeMap :: from_iter ( iter:: once ( ( key, values. clone ( ) ) ) ) ,
293
+ } ) ;
294
+ }
295
+ }
251
296
}
252
297
253
298
Ok ( values)
0 commit comments