Skip to main content

opendal_service_foyer/
backend.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Debug;
19use std::sync::Arc;
20
21use foyer::HybridCache;
22use log::debug;
23
24use super::FOYER_SCHEME;
25use super::FoyerKey;
26use super::FoyerValue;
27use super::config::FoyerConfig;
28use super::core::FoyerCore;
29use super::deleter::FoyerDeleter;
30use super::reader::*;
31use super::writer::FoyerWriter;
32use opendal_core::raw::*;
33use opendal_core::*;
34
35/// [foyer](https://github.com/foyer-rs/foyer) backend support.
36#[doc = include_str!("docs.md")]
37#[derive(Default)]
38pub struct FoyerBuilder {
39    pub(super) config: FoyerConfig,
40    pub(super) cache: Option<Arc<HybridCache<FoyerKey, FoyerValue>>>,
41}
42
43impl Debug for FoyerBuilder {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.debug_struct("FoyerBuilder")
46            .field("config", &self.config)
47            .finish_non_exhaustive()
48    }
49}
50
51impl FoyerBuilder {
52    /// Create a new [`FoyerBuilder`] with default settings.
53    ///
54    /// The cache will be lazily initialized when first accessed if not provided via [`Self::cache`].
55    ///
56    /// # Example
57    ///
58    /// ```no_run
59    /// use opendal_service_foyer::Foyer;
60    ///
61    /// let builder = Foyer::new()
62    ///     .memory(64 * 1024 * 1024)
63    ///     .root("/cache");
64    /// ```
65    pub fn new() -> Self {
66        Self {
67            ..Default::default()
68        }
69    }
70
71    /// Set the name of this cache instance.
72    pub fn name(mut self, name: &str) -> Self {
73        if !name.is_empty() {
74            self.config.name = Some(name.to_owned());
75        }
76        self
77    }
78
79    /// Set a pre-built [`foyer::HybridCache`] instance.
80    ///
81    /// If provided, this cache will be used directly. Otherwise, a cache will be
82    /// lazily initialized using the configured memory size.
83    ///
84    /// # Example
85    ///
86    /// ```no_run
87    /// use opendal_service_foyer::Foyer;
88    /// use foyer::HybridCacheBuilder;
89    ///
90    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
91    /// let cache = HybridCacheBuilder::new()
92    ///     .memory(64 * 1024 * 1024)
93    ///     .storage()
94    ///     .build()
95    ///     .await?;
96    ///
97    /// let builder = Foyer::new().cache(cache);
98    /// # Ok(())
99    /// # }
100    /// ```
101    pub fn cache(mut self, cache: HybridCache<FoyerKey, FoyerValue>) -> Self {
102        self.cache = Some(Arc::new(cache));
103        self
104    }
105
106    /// Set the root path of this backend.
107    ///
108    /// All operations will be relative to this root path.
109    pub fn root(mut self, path: &str) -> Self {
110        self.config.root = if path.is_empty() {
111            None
112        } else {
113            Some(path.to_string())
114        };
115        self
116    }
117
118    /// Set the memory capacity in bytes for the cache.
119    ///
120    /// This is used when the cache is lazily initialized (i.e., when no pre-built cache
121    /// is provided via [`Self::cache`]).
122    ///
123    /// Default is 1 GiB (1024 * 1024 * 1024 bytes).
124    pub fn memory(mut self, size: usize) -> Self {
125        self.config.memory = Some(size);
126        self
127    }
128
129    /// Set the disk cache directory path.
130    ///
131    /// Enables hybrid cache with disk storage. When memory cache is full, data will
132    /// be persisted to this directory.
133    pub fn disk_path(mut self, path: &str) -> Self {
134        self.config.disk_path = if path.is_empty() {
135            None
136        } else {
137            Some(path.to_string())
138        };
139        self
140    }
141
142    /// Set the disk cache total capacity in bytes.
143    ///
144    /// Only used when `disk_path` is set.
145    pub fn disk_capacity(mut self, size: usize) -> Self {
146        self.config.disk_capacity = Some(size);
147        self
148    }
149
150    /// Set the individual cache file size in bytes.
151    ///
152    /// Default is 1 MiB (1024 * 1024 bytes).
153    /// Only used when `disk_path` is set.
154    pub fn disk_file_size(mut self, size: usize) -> Self {
155        self.config.disk_file_size = Some(size);
156        self
157    }
158
159    /// Set the recovery mode when starting the cache.
160    ///
161    /// Valid values: "none" (default), "quiet", "strict".
162    /// - "none": Don't recover from disk
163    /// - "quiet": Recover and skip errors
164    /// - "strict": Recover and panic on errors
165    pub fn recover_mode(mut self, mode: &str) -> Self {
166        if !mode.is_empty() {
167            self.config.recover_mode = Some(mode.to_string());
168        }
169        self
170    }
171
172    /// Set the number of shards for concurrent access.
173    ///
174    /// Default is 1. Higher values improve concurrency but increase overhead.
175    pub fn shards(mut self, count: usize) -> Self {
176        self.config.shards = Some(count);
177        self
178    }
179}
180
181impl Builder for FoyerBuilder {
182    type Config = FoyerConfig;
183
184    fn build(self) -> Result<impl Service> {
185        debug!("backend build started: {:?}", self);
186
187        let root = normalize_root(
188            self.config
189                .root
190                .clone()
191                .unwrap_or_else(|| "/".to_string())
192                .as_str(),
193        );
194
195        let mut core = FoyerCore::new(self.config.clone());
196        if let Some(cache) = self.cache {
197            core = core.with_cache(cache.clone());
198        }
199
200        debug!("backend build finished: {:?}", self.config);
201
202        Ok(FoyerBackend::new(core).with_normalized_root(root))
203    }
204}
205
206#[derive(Debug, Clone)]
207pub struct FoyerBackend {
208    pub(crate) core: Arc<FoyerCore>,
209    pub(crate) root: String,
210    pub(crate) info: ServiceInfo,
211    pub(crate) capability: Capability,
212}
213
214impl FoyerBackend {
215    fn new(core: FoyerCore) -> Self {
216        let info = ServiceInfo::new(FOYER_SCHEME, "/", core.name().unwrap_or("foyer"));
217        let capability = Capability {
218            read: true,
219            write: true,
220            write_can_empty: true,
221            delete: true,
222            stat: true,
223            shared: true,
224            ..Default::default()
225        };
226
227        Self {
228            core: Arc::new(core),
229            root: "/".to_string(),
230            info,
231            capability,
232        }
233    }
234
235    fn with_normalized_root(mut self, root: String) -> Self {
236        self.info = self.info.with_root(&root);
237        self.root = root;
238        self
239    }
240}
241
242impl Service for FoyerBackend {
243    type Reader = oio::StreamReader<FoyerReader>;
244    type Writer = FoyerWriter;
245    type Lister = ();
246    type Deleter = oio::OneShotDeleter<FoyerDeleter>;
247    type Copier = ();
248    type Composer = ();
249
250    fn info(&self) -> ServiceInfo {
251        self.info.clone()
252    }
253
254    fn capability(&self) -> Capability {
255        self.capability
256    }
257
258    async fn create_dir(
259        &self,
260        _ctx: &OperationContext,
261        _path: &str,
262        _args: OpCreateDir,
263    ) -> Result<RpCreateDir> {
264        Err(Error::new(
265            ErrorKind::Unsupported,
266            "operation is not supported",
267        ))
268    }
269
270    async fn stat(&self, _ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
271        let p = build_abs_path(&self.root, path);
272
273        if p == build_abs_path(&self.root, "") {
274            Ok(RpStat::new(MetadataBuilder::dir().build()))
275        } else {
276            match self.core.get(&p).await? {
277                Some(bs) => Ok(RpStat::new({
278                    let metadata = MetadataBuilder::file(bs.len() as u64);
279                    metadata.build()
280                })),
281                None => Err(Error::new(ErrorKind::NotFound, "key not found in foyer")),
282            }
283        }
284    }
285    fn read(&self, _ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
286        let output: oio::StreamReader<FoyerReader> = {
287            Ok(oio::StreamReader::new(FoyerReader::new(
288                self.clone(),
289                path,
290                args,
291            )))
292        }?;
293
294        Ok(output)
295    }
296
297    fn write(&self, _ctx: &OperationContext, path: &str, _: OpWrite) -> Result<Self::Writer> {
298        let output: FoyerWriter = {
299            let p = build_abs_path(&self.root, path);
300            Ok(FoyerWriter::new(self.core.clone(), p))
301        }?;
302
303        Ok(output)
304    }
305
306    fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
307        let output: oio::OneShotDeleter<FoyerDeleter> = {
308            Ok(oio::OneShotDeleter::new(FoyerDeleter::new(
309                self.core.clone(),
310                self.root.clone(),
311            )))
312        }?;
313
314        Ok(output)
315    }
316
317    fn list(&self, _ctx: &OperationContext, _path: &str, _args: OpList) -> Result<Self::Lister> {
318        Err(Error::new(
319            ErrorKind::Unsupported,
320            "operation is not supported",
321        ))
322    }
323
324    fn copy(
325        &self,
326        _ctx: &OperationContext,
327        _from: &str,
328        _to: &str,
329        _args: OpCopy,
330    ) -> Result<Self::Copier> {
331        Err(Error::new(
332            ErrorKind::Unsupported,
333            "operation is not supported",
334        ))
335    }
336
337    async fn rename(
338        &self,
339        _ctx: &OperationContext,
340        _from: &str,
341        _to: &str,
342        _args: OpRename,
343    ) -> Result<RpRename> {
344        Err(Error::new(
345            ErrorKind::Unsupported,
346            "operation is not supported",
347        ))
348    }
349
350    async fn presign(
351        &self,
352        _ctx: &OperationContext,
353        _path: &str,
354        _args: OpPresign,
355    ) -> Result<RpPresign> {
356        Err(Error::new(
357            ErrorKind::Unsupported,
358            "operation is not supported",
359        ))
360    }
361}