1use std::{
2 io,
3 ops::Range,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use async_trait::async_trait;
9use block_on_place::HandleExt;
10use deadpool_redis::{Connection, Pool, redis::AsyncCommands};
11use eyre::{OptionExt, Result};
12use gxhash::GxBuildHasher;
13use scc::HashIndex;
14use tantivy::{
15 Directory, HasLen,
16 directory::{
17 DirectoryLock, FileHandle, FileSlice, Lock, WatchCallback, WatchHandle, WritePtr,
18 error::{DeleteError, LockError, OpenReadError, OpenWriteError},
19 },
20};
21use tantivy_common::OwnedBytes;
22use tokio::runtime::Handle;
23
24use self::{error::WrapIoErrorExt, footer::Footer};
25
26pub use self::warmer::Warmer;
27
28mod error;
29mod footer;
30mod keys;
31mod warmer;
32
33#[cfg(test)]
34mod tests;
35
36#[derive(Clone, Debug)]
39pub struct CachingDirectory<D> {
40 rt: Handle,
41 redis: Pool,
42 dir: D,
43
44 cache: Cache,
47}
48
49type Cache = Arc<HashIndex<PathBuf, Footer, GxBuildHasher>>;
50
51#[derive(Debug)]
52struct CachingHandle {
53 file: FileSlice,
54 footer: Footer,
55
56 rt: Handle,
57 redis: Pool,
58 path: PathBuf,
59}
60
61impl<D> CachingDirectory<D> {
62 pub fn new(dir: D, redis: Pool) -> Self {
71 let rt = Handle::current();
72 let cache = Arc::default();
73
74 Self {
75 rt,
76 redis,
77 cache,
78 dir,
79 }
80 }
81
82 async fn open(&self, path: impl Into<PathBuf>, file: FileSlice) -> Result<CachingHandle> {
83 let path = path.into();
84
85 loop {
86 if let Some(footer) = self.cache.get_async(&path).await {
87 let footer = footer.get().clone();
88 let rt = self.rt.clone();
89 let redis = self.redis.clone();
90
91 return Ok(CachingHandle {
92 file,
93 footer,
94 rt,
95 redis,
96 path,
97 });
98 }
99
100 let footer = if let Some(offset) = offset(&path, &self.redis).await {
101 Footer::with_offset(offset)
102 } else {
103 let footer = Footer::read(&path, &file).await?;
104 update(&path, &footer, &self.redis).await;
105
106 footer
107 };
108
109 if self
110 .cache
111 .insert_async(path.clone(), footer.clone())
112 .await
113 .is_err()
114 {
115 continue;
118 }
119
120 let rt = self.rt.clone();
121 let redis = self.redis.clone();
122
123 return Ok(CachingHandle {
124 file,
125 footer,
126 rt,
127 redis,
128 path,
129 });
130 }
131 }
132}
133
134impl<D: Directory + Clone> CachingDirectory<D> {
135 pub fn warmer(&self) -> Warmer<D> {
142 Warmer::new(self.clone())
143 }
144
145 pub(crate) async fn warm(&self, path: &Path) -> Result<()> {
153 let dir = self.dir.clone();
154 let owned = path.to_path_buf();
155
156 let opened = self
159 .rt
160 .spawn_blocking(move || dir.open_read(&owned))
161 .await?;
162
163 let file = match opened {
164 Ok(file) => file,
165 Err(OpenReadError::FileDoesNotExist(_)) => return Ok(()),
166 Err(error) => return Err(error.into()),
167 };
168
169 let handle = self.open(path, file).await?;
171
172 handle.footer().await;
175
176 Ok(())
177 }
178}
179
180impl CachingHandle {
181 async fn footer(&self) -> Option<OwnedBytes> {
187 let fetch = async {
188 if let Some(data) = footer(&self.path, &self.redis).await {
189 return Ok(OwnedBytes::new(data));
190 }
191
192 let footer = Footer::read(&self.path, &self.file).await?;
193 update(&self.path, &footer, &self.redis).await;
194
195 footer.data().ok_or_eyre("missing data")
196 };
197
198 match self.footer.get_or_fetch(fetch).await {
199 Ok(footer) => Some(footer),
200 Err(error) => {
201 tracing::warn!("Failed to fetch footer: {error:?}");
202
203 None
204 }
205 }
206 }
207}
208
209impl<D: Clone + Directory> Directory for CachingDirectory<D> {
210 #[inline]
211 fn get_file_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>, OpenReadError> {
212 let file = self.dir.open_read(path)?;
213 let handle = self
214 .rt
215 .block_on_place(self.open(path, file))
216 .map_err(OpenReadError::wrapper(path))?;
217
218 Ok(Arc::new(handle))
219 }
220
221 #[inline]
222 fn delete(&self, path: &Path) -> Result<(), DeleteError> {
223 self.dir.delete(path)
225 }
226
227 #[inline]
228 fn exists(&self, path: &Path) -> Result<bool, OpenReadError> {
229 self.dir.exists(path)
231 }
232
233 #[inline]
234 fn open_read(&self, path: &Path) -> Result<FileSlice, OpenReadError> {
235 self.get_file_handle(path).map(FileSlice::new)
236 }
237
238 #[inline]
239 fn open_write(&self, path: &Path) -> Result<WritePtr, OpenWriteError> {
240 self.dir.open_write(path)
242 }
243
244 #[inline]
245 fn atomic_read(&self, path: &Path) -> Result<Vec<u8>, OpenReadError> {
246 self.dir.atomic_read(path)
248 }
249
250 #[inline]
251 fn atomic_write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
252 self.dir.atomic_write(path, data)
254 }
255
256 #[inline]
257 fn sync_directory(&self) -> io::Result<()> {
258 self.dir.sync_directory()
259 }
260
261 #[inline]
262 fn watch(&self, cb: WatchCallback) -> tantivy::Result<WatchHandle> {
263 self.dir.watch(cb)
264 }
265
266 #[inline]
267 fn acquire_lock(&self, lock: &Lock) -> Result<DirectoryLock, LockError> {
268 self.dir.acquire_lock(lock)
269 }
270}
271
272#[async_trait]
273impl FileHandle for CachingHandle {
274 fn read_bytes(&self, range: Range<usize>) -> io::Result<OwnedBytes> {
275 if range.end <= self.footer.offset {
276 return self.file.read_bytes_slice(range);
277 }
278
279 let Some(footer) = self.rt.block_on_place(self.footer()) else {
280 return self.file.read_bytes_slice(range);
281 };
282
283 if range.start >= self.footer.offset {
284 let start = range.start - self.footer.offset;
285 let end = range.end - self.footer.offset;
286 let slice = footer.slice(start..end);
287
288 return Ok(slice);
289 }
290
291 let start = range.start;
292 let end = self.footer.offset;
293 let data = self.file.read_bytes_slice(start..end)?;
294
295 let start = 0;
296 let end = range.end - self.footer.offset;
297 let footer = footer.slice(start..end);
298
299 let mut combined = Vec::with_capacity(data.len() + footer.len());
300 combined.extend_from_slice(data.as_ref());
301 combined.extend_from_slice(footer.as_ref());
302
303 Ok(OwnedBytes::new(combined))
304 }
305
306 async fn read_bytes_async(&self, range: Range<usize>) -> io::Result<OwnedBytes> {
307 if range.end <= self.footer.offset {
308 return self.file.read_bytes_slice_async(range).await;
309 }
310
311 let Some(footer) = self.footer().await else {
312 return self.file.read_bytes_slice_async(range).await;
313 };
314
315 if range.start >= self.footer.offset {
316 let start = range.start - self.footer.offset;
317 let end = range.end - self.footer.offset;
318 let slice = footer.slice(start..end);
319
320 return Ok(slice);
321 }
322
323 let start = range.start;
324 let end = self.footer.offset;
325 let data = self.file.read_bytes_slice_async(start..end).await?;
326
327 let start = 0;
328 let end = range.end - self.footer.offset;
329 let footer = footer.slice(start..end);
330
331 let mut combined = Vec::with_capacity(data.len() + footer.len());
332 combined.extend_from_slice(data.as_ref());
333 combined.extend_from_slice(footer.as_ref());
334
335 Ok(OwnedBytes::new(combined))
336 }
337}
338
339impl HasLen for CachingHandle {
340 #[inline]
341 fn len(&self) -> usize {
342 self.file.len()
343 }
344
345 #[inline]
346 fn is_empty(&self) -> bool {
347 self.file.is_empty()
348 }
349}
350
351async fn conn(redis: &Pool) -> Option<Connection> {
355 match redis.get().await {
356 Ok(conn) => Some(conn),
357 Err(error) => {
358 tracing::warn!("Failed to get Redis connection: {error:?}");
359 None
360 }
361 }
362}
363
364async fn offset(path: &Path, redis: &Pool) -> Option<usize> {
368 let key = keys::offset(path).ok()?;
369 let mut conn = conn(redis).await?;
370
371 match conn.get(&key).await {
372 Ok(offset) => offset,
373 Err(error) => {
374 tracing::warn!("Failed to fetch footer offset: {error:?}");
375 None
376 }
377 }
378}
379
380async fn footer(path: &Path, redis: &Pool) -> Option<Vec<u8>> {
384 let key = keys::footer(path).ok()?;
385 let mut conn = conn(redis).await?;
386
387 match conn.get(&key).await {
388 Ok(offset) => offset,
389 Err(error) => {
390 tracing::warn!("Failed to fetch footer: {error:?}");
391 None
392 }
393 }
394}
395
396async fn update(path: &Path, footer: &Footer, redis: &Pool) {
400 let Some(mut conn) = conn(redis).await else {
401 return;
402 };
403
404 let Ok(key) = keys::offset(path) else { return };
405 match conn.set(&key, footer.offset).await {
406 Ok(()) => (),
407 Err(error) => {
408 tracing::warn!("Failed to update footer offset: {error:?}");
409 }
410 }
411
412 let Ok(key) = keys::footer(path) else { return };
413 let Some(data) = footer.data() else { return };
414 match conn.set(&key, data.as_slice()).await {
415 Ok(()) => (),
416 Err(error) => {
417 tracing::warn!("Failed to update footer: {error:?}");
418 }
419 }
420}