1use std::io;
18use std::mem;
19use std::ops::Range;
20use std::sync::Arc;
21use std::time::Duration;
22
23use bytes::Bytes;
24use common_base::range_read::RangeReader;
25use common_cache::paged::{PageFetchStats, PagedCache};
26
27use crate::metrics;
28use crate::sst::file::RegionFileId;
29
30pub(crate) const PAGE_SIZE: u64 = 64 * 1024;
32
33const PAGE_SIZE_ENV: &str = "GREPTIME_PARQUET_PAGE_SIZE";
35const PAGE_TTL: Duration = Duration::from_secs(3600);
37
38fn parse_page_size(value: Option<&str>) -> u64 {
39 match value {
40 None => PAGE_SIZE,
41 Some(value) => match value.parse::<u64>() {
42 Ok(size) if (1..=MAX_PAGE_SIZE).contains(&size) => size,
43 _ => {
44 common_telemetry::warn!(
45 "Invalid {PAGE_SIZE_ENV}: expected bytes in 1..={MAX_PAGE_SIZE}; using {PAGE_SIZE}"
46 );
47 PAGE_SIZE
48 }
49 },
50 }
51}
52
53pub(crate) const MAX_PAGE_SIZE: u64 = u32::MAX as u64
55 - (mem::size_of::<(RegionFileId, u64)>() + mem::size_of::<Bytes>() + mem::size_of::<u64>())
56 as u64;
57
58pub(crate) struct ParquetPageCache {
60 page_size: u64,
61 cache: PagedCache<RegionFileId>,
62}
63
64impl ParquetPageCache {
65 pub(crate) fn from_env(capacity: u64) -> Arc<Self> {
67 let value = std::env::var(PAGE_SIZE_ENV).ok();
68 Self::new(capacity, parse_page_size(value.as_deref()))
69 }
70
71 pub(crate) fn new(capacity: u64, page_size: u64) -> Arc<Self> {
73 Self::with_ttl(capacity, page_size, PAGE_TTL)
74 }
75
76 fn with_ttl(capacity: u64, page_size: u64, ttl: Duration) -> Arc<Self> {
77 Arc::new(Self {
78 page_size,
79 cache: PagedCache::new_with_ttl(
80 "parquet_pages",
81 capacity,
82 page_size,
83 |_: &(RegionFileId, u64), bytes: &Bytes| {
84 (mem::size_of::<(RegionFileId, u64)>()
85 + mem::size_of::<Bytes>()
86 + mem::size_of::<u64>()
87 + bytes.len()) as u32
88 },
89 metrics::cache_page_observer("page", "page", |hits, misses| {
90 (usize::from(hits > 0), usize::from(misses > 0))
91 }),
92 Some(ttl),
93 ),
94 })
95 }
96
97 pub(crate) async fn known_file_size(
99 &self,
100 file: RegionFileId,
101 ranges: &[Range<u64>],
102 ) -> Option<u64> {
103 self.cache.known_file_size(&file, ranges).await
104 }
105
106 pub(crate) async fn fetch<R: RangeReader + ?Sized>(
109 &self,
110 file: RegionFileId,
111 file_size: u64,
112 ranges: &[Range<u64>],
113 reader: &R,
114 ) -> Result<(Vec<Bytes>, PageFetchStats), Arc<io::Error>> {
115 if self.page_size == 0 || self.page_size > MAX_PAGE_SIZE {
117 return Err(Arc::new(io::Error::new(
118 io::ErrorKind::InvalidInput,
119 format!(
120 "Parquet cache page size {} is outside 1..={MAX_PAGE_SIZE} bytes",
121 self.page_size
122 ),
123 )));
124 }
125 let result = self.cache.fetch(file, file_size, ranges, reader).await?;
126 metrics::cache_page_fetch_metrics("page", &result.1);
127 Ok(result)
128 }
129
130 #[cfg(test)]
132 pub(crate) async fn contains(&self, file: RegionFileId, page_id: u64) -> bool {
133 self.cache.contains(&file, page_id).await
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use async_trait::async_trait;
141 use common_base::range_read::Metadata;
142 use std::sync::Mutex;
143 use store_api::storage::{FileId, RegionId};
144
145 #[derive(Default)]
146 struct Reader(Mutex<Vec<Range<u64>>>);
147
148 #[async_trait]
149 impl RangeReader for Reader {
150 async fn metadata(&self) -> io::Result<Metadata> {
151 Ok(Metadata {
152 content_length: PAGE_SIZE * 2 + 10,
153 })
154 }
155 async fn read(&self, range: Range<u64>) -> io::Result<Bytes> {
156 self.0.lock().unwrap().push(range.clone());
157 tokio::task::yield_now().await;
158 Ok(Bytes::from(range.map(|n| n as u8).collect::<Vec<_>>()))
159 }
160 }
161
162 #[tokio::test]
163 async fn test_aligns_and_reuses_partial_overlaps_without_merging_reads() {
164 let cache = ParquetPageCache::new(1024 * 1024, PAGE_SIZE);
165 let file = RegionFileId::new(RegionId::new(1, 1), FileId::random());
166 let size = PAGE_SIZE * 2 + 10;
167 let reader = Reader::default();
168 let (result, stats) = cache
169 .fetch(
170 file,
171 size,
172 &[1..4, PAGE_SIZE - 2..PAGE_SIZE + 2, size - 3..size],
173 &reader,
174 )
175 .await
176 .unwrap();
177 assert_eq!(&result[0][..], &[1, 2, 3]);
178 assert_eq!(&result[1][..], &[254, 255, 0, 1]);
179 assert_eq!(&result[2][..], &[7, 8, 9]);
180 assert_eq!(
181 (
182 stats.requested_bytes,
183 stats.unique_requested_bytes,
184 stats.aligned_bytes
185 ),
186 (10, 10, size)
187 );
188 assert_eq!(
189 reader.0.lock().unwrap().as_slice(),
190 &[0..PAGE_SIZE, PAGE_SIZE..PAGE_SIZE * 2, PAGE_SIZE * 2..size]
191 );
192 let (_, stats) = cache
193 .fetch(file, size, &[2..6, 4..8], &reader)
194 .await
195 .unwrap();
196 assert_eq!(
197 (stats.cache_hit, stats.cache_miss, stats.cached_bytes),
198 (1, 0, 8)
199 );
200 assert_eq!(reader.0.lock().unwrap().len(), 3);
201 }
202
203 #[tokio::test]
204 async fn file_identity_includes_region_and_concurrent_reads_share() {
205 let cache = ParquetPageCache::new(1024 * 1024, PAGE_SIZE);
206 let file_id = FileId::random();
207 let first = RegionFileId::new(RegionId::new(1, 1), file_id);
208 let second = RegionFileId::new(RegionId::new(2, 1), file_id);
209 let reader = Reader::default();
210 let ranges = [0..4, 2..6];
211 let (a, b) = tokio::join!(
212 cache.fetch(first, 100, &ranges, &reader),
213 cache.fetch(first, 100, &ranges, &reader)
214 );
215 assert_eq!(a.unwrap().0, b.unwrap().0);
216 assert_eq!(reader.0.lock().unwrap().len(), 1);
217 cache.fetch(second, 100, &ranges, &reader).await.unwrap();
218 assert_eq!(reader.0.lock().unwrap().len(), 2);
219 assert!(cache.contains(first, 0).await);
220 assert!(cache.contains(second, 0).await);
221 }
222
223 #[tokio::test]
224 async fn configurable_blocks_preserve_cross_block_and_tail_reads() {
225 let file = RegionFileId::new(RegionId::new(1, 1), FileId::random());
226 let size = PAGE_SIZE * 2 + 10;
227 for page_size in [1, 37, 4096, PAGE_SIZE, PAGE_SIZE * 4] {
229 let cache = ParquetPageCache::new(16 * 1024 * 1024, page_size);
230 let reader = Reader::default();
231 let boundary = page_size.min(size - 1);
232 let ranges = [
233 0..3,
234 boundary - 1..boundary + 1,
235 PAGE_SIZE - 2..PAGE_SIZE + 2,
236 size - 3..size,
237 size..size,
238 ];
239 let expected: Vec<_> = ranges
240 .iter()
241 .map(|r| Bytes::from(r.clone().map(|n| n as u8).collect::<Vec<_>>()))
242 .collect();
243 let (actual, _) = cache.fetch(file, size, &ranges, &reader).await.unwrap();
244 assert_eq!(actual, expected);
245 let reads = reader.0.lock().unwrap().len();
246 assert!(reader.0.lock().unwrap().iter().all(|r| r.end <= size));
247 let (cached, stats) = cache.fetch(file, size, &ranges, &reader).await.unwrap();
248 assert_eq!(cached, expected);
249 assert_eq!(stats.cache_miss, 0);
250 assert_eq!(reader.0.lock().unwrap().len(), reads);
251 }
252 }
253
254 #[tokio::test]
255 async fn builder_uses_fixed_block_size() {
256 use crate::cache::{CacheManager, CacheStrategy};
257
258 let strategy = CacheStrategy::EnableAll(Arc::new(
259 CacheManager::builder().page_cache_size(1024 * 1024).build(),
260 ));
261 let cache = strategy.parquet_page_cache().unwrap();
262 let reader = Reader::default();
263 let file = RegionFileId::new(RegionId::new(1, 1), FileId::random());
264 cache
265 .fetch(file, PAGE_SIZE * 2, std::slice::from_ref(&(0..1)), &reader)
266 .await
267 .unwrap();
268 assert_eq!(
269 reader.0.lock().unwrap().as_slice(),
270 std::slice::from_ref(&(0..PAGE_SIZE))
271 );
272 let disabled = CacheStrategy::EnableAll(Arc::new(CacheManager::builder().build()));
273 assert!(disabled.parquet_page_cache().is_none());
274 }
275
276 #[test]
277 fn block_size_override_uses_valid_bytes_or_default() {
278 for (value, expected) in [
279 (None, PAGE_SIZE),
280 (Some(""), PAGE_SIZE),
281 (Some("64KiB"), PAGE_SIZE),
282 (Some("0"), PAGE_SIZE),
283 (Some("-1"), PAGE_SIZE),
284 (Some("37"), 37),
285 (Some("131072"), 131072),
286 ] {
287 assert_eq!(parse_page_size(value), expected);
288 }
289 assert_eq!(
290 parse_page_size(Some(&MAX_PAGE_SIZE.to_string())),
291 MAX_PAGE_SIZE
292 );
293 assert_eq!(
294 parse_page_size(Some(&(MAX_PAGE_SIZE + 1).to_string())),
295 PAGE_SIZE
296 );
297 }
298
299 #[tokio::test]
300 async fn expired_pages_reload_without_file_invalidation() {
301 let cache = ParquetPageCache::with_ttl(1024 * 1024, PAGE_SIZE, Duration::from_millis(50));
302 let retained = ParquetPageCache::new(1024 * 1024, PAGE_SIZE);
303 let file = RegionFileId::new(RegionId::new(1, 1), FileId::random());
304 let reader = Reader::default();
305 let ranges = [0..4, 4..8];
306 let expected = cache.fetch(file, 100, &ranges, &reader).await.unwrap().0;
307 retained.fetch(file, 100, &ranges, &reader).await.unwrap();
308 cache.cache.run_pending_tasks().await;
309 retained.cache.run_pending_tasks().await;
310 assert!(retained.contains(file, 0).await);
311 tokio::time::timeout(Duration::from_secs(5), async {
312 while cache.contains(file, 0).await {
313 tokio::time::sleep(Duration::from_millis(5)).await;
314 }
315 })
316 .await
317 .unwrap();
318 assert!(retained.contains(file, 0).await);
319 let (actual, stats) = cache.fetch(file, 100, &ranges, &reader).await.unwrap();
320 assert_eq!(actual, expected);
321 assert_eq!(stats.cache_miss, 1);
322 assert_eq!(reader.0.lock().unwrap().len(), 3);
323 }
324
325 #[tokio::test]
326 async fn invalid_block_size_is_rejected_before_io() {
327 let file = RegionFileId::new(RegionId::new(1, 1), FileId::random());
328 for page_size in [0, MAX_PAGE_SIZE + 1, u64::MAX] {
329 let cache = ParquetPageCache::new(1024, page_size);
330 let reader = Reader::default();
331 let error = cache
332 .fetch(file, 100, std::slice::from_ref(&(0..4)), &reader)
333 .await
334 .unwrap_err();
335 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
336 assert!(reader.0.lock().unwrap().is_empty());
337 }
338 }
339
340 #[tokio::test]
341 async fn oversized_page_is_returned_without_retention() {
342 let cache = ParquetPageCache::new(1, PAGE_SIZE);
343 let file = RegionFileId::new(RegionId::new(1, 1), FileId::random());
344 let reader = Reader::default();
345 let result = cache
346 .fetch(file, 100, &[0..4, 4..8], &reader)
347 .await
348 .unwrap();
349 assert_eq!(&result.0[0][..], &[0, 1, 2, 3]);
350 cache.cache.run_pending_tasks().await;
351 assert!(!cache.contains(file, 0).await);
352 }
353}