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
249    fn info(&self) -> ServiceInfo {
250        self.info.clone()
251    }
252
253    fn capability(&self) -> Capability {
254        self.capability
255    }
256
257    async fn create_dir(
258        &self,
259        _ctx: &OperationContext,
260        _path: &str,
261        _args: OpCreateDir,
262    ) -> Result<RpCreateDir> {
263        Err(Error::new(
264            ErrorKind::Unsupported,
265            "operation is not supported",
266        ))
267    }
268
269    async fn stat(&self, _ctx: &OperationContext, path: &str, _: OpStat) -> Result<RpStat> {
270        let p = build_abs_path(&self.root, path);
271
272        if p == build_abs_path(&self.root, "") {
273            Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
274        } else {
275            match self.core.get(&p).await? {
276                Some(bs) => Ok(RpStat::new(
277                    Metadata::new(EntryMode::FILE).with_content_length(bs.len() as u64),
278                )),
279                None => Err(Error::new(ErrorKind::NotFound, "key not found in foyer")),
280            }
281        }
282    }
283    fn read(&self, _ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
284        let output: oio::StreamReader<FoyerReader> = {
285            Ok(oio::StreamReader::new(FoyerReader::new(
286                self.clone(),
287                path,
288                args,
289            )))
290        }?;
291
292        Ok(output)
293    }
294
295    fn write(&self, _ctx: &OperationContext, path: &str, _: OpWrite) -> Result<Self::Writer> {
296        let output: FoyerWriter = {
297            let p = build_abs_path(&self.root, path);
298            Ok(FoyerWriter::new(self.core.clone(), p))
299        }?;
300
301        Ok(output)
302    }
303
304    fn delete(&self, _ctx: &OperationContext) -> Result<Self::Deleter> {
305        let output: oio::OneShotDeleter<FoyerDeleter> = {
306            Ok(oio::OneShotDeleter::new(FoyerDeleter::new(
307                self.core.clone(),
308                self.root.clone(),
309            )))
310        }?;
311
312        Ok(output)
313    }
314
315    fn list(&self, _ctx: &OperationContext, _path: &str, _args: OpList) -> Result<Self::Lister> {
316        Err(Error::new(
317            ErrorKind::Unsupported,
318            "operation is not supported",
319        ))
320    }
321
322    fn copy(
323        &self,
324        _ctx: &OperationContext,
325        _from: &str,
326        _to: &str,
327        _args: OpCopy,
328        _opts: OpCopier,
329    ) -> Result<Self::Copier> {
330        Err(Error::new(
331            ErrorKind::Unsupported,
332            "operation is not supported",
333        ))
334    }
335
336    async fn rename(
337        &self,
338        _ctx: &OperationContext,
339        _from: &str,
340        _to: &str,
341        _args: OpRename,
342    ) -> Result<RpRename> {
343        Err(Error::new(
344            ErrorKind::Unsupported,
345            "operation is not supported",
346        ))
347    }
348
349    async fn presign(
350        &self,
351        _ctx: &OperationContext,
352        _path: &str,
353        _args: OpPresign,
354    ) -> Result<RpPresign> {
355        Err(Error::new(
356            ErrorKind::Unsupported,
357            "operation is not supported",
358        ))
359    }
360}