1use std::ops::Range;
2use std::sync::{Arc, OnceLock};
3
4use async_trait::async_trait;
5use bytes::Bytes;
6use futures::stream::{self, BoxStream};
7use micromegas_tracing::prelude::*;
8use object_store::{
9 Attributes, CopyOptions, GetOptions, GetRange, GetResult, GetResultPayload, ListResult,
10 MultipartUpload, ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload,
11 PutResult, path::Path,
12};
13
14use crate::backend::RangeCacheBackend;
15use crate::bounded_memory_backend::BoundedMemoryBackend;
16use crate::range_cache::{
17 DEFAULT_BLOCK_SIZE, DEFAULT_MAX_COALESCED_GET_BYTES, DEFAULT_PROMOTE_WHOLE_BATCH, RangeCache,
18};
19
20const ENV_OBJECT_CACHE_L1_MB: &str = "MICROMEGAS_OBJECT_CACHE_L1_MB";
25const DEFAULT_OBJECT_CACHE_L1_MB: u64 = 200;
27
28const L1_TOTAL_FETCH_PERMITS: usize = 16;
37const L1_DEMAND_RESERVED_FETCH_PERMITS: usize = 4;
45const L1_FETCH_MEMORY_BUDGET_BYTES: u64 =
50 L1_TOTAL_FETCH_PERMITS as u64 * DEFAULT_MAX_COALESCED_GET_BYTES;
51
52static SHARED_L1_BACKEND: OnceLock<Option<Arc<BoundedMemoryBackend>>> = OnceLock::new();
55
56fn shared_l1_backend() -> Option<Arc<BoundedMemoryBackend>> {
57 SHARED_L1_BACKEND
58 .get_or_init(|| {
59 let mb = match std::env::var(ENV_OBJECT_CACHE_L1_MB) {
60 Ok(s) => s.parse::<u64>().unwrap_or_else(|_| {
61 warn!(
62 "Invalid {ENV_OBJECT_CACHE_L1_MB} value '{s}', using default {DEFAULT_OBJECT_CACHE_L1_MB} MB"
63 );
64 DEFAULT_OBJECT_CACHE_L1_MB
65 }),
66 Err(_) => DEFAULT_OBJECT_CACHE_L1_MB,
67 };
68 if mb == 0 {
69 info!("{ENV_OBJECT_CACHE_L1_MB}=0, in-process L1 cache disabled");
70 None
71 } else {
72 info!("in-process L1 cache enabled, budget={mb}MB");
73 Some(Arc::new(BoundedMemoryBackend::new(
74 (mb * 1024 * 1024) as usize,
75 )))
76 }
77 })
78 .clone()
79}
80
81pub fn l1_wrap(origin: Arc<dyn ObjectStore>, ns: &str) -> Arc<dyn ObjectStore> {
87 match shared_l1_backend() {
88 Some(backend) => Arc::new(L1CacheStore::new(origin, backend, ns.to_string())),
89 None => origin,
90 }
91}
92
93pub struct L1CacheStore {
101 cache: RangeCache,
102 origin: Arc<dyn ObjectStore>,
103}
104
105impl L1CacheStore {
106 pub fn new(
107 origin: Arc<dyn ObjectStore>,
108 backend: Arc<dyn RangeCacheBackend>,
109 ns: String,
110 ) -> Self {
111 let cache = RangeCache::new(
112 origin.clone(),
113 backend,
114 DEFAULT_BLOCK_SIZE,
115 ns,
116 L1_TOTAL_FETCH_PERMITS,
117 L1_DEMAND_RESERVED_FETCH_PERMITS,
118 L1_FETCH_MEMORY_BUDGET_BYTES,
119 DEFAULT_MAX_COALESCED_GET_BYTES,
120 DEFAULT_PROMOTE_WHOLE_BATCH,
121 );
122 Self { cache, origin }
123 }
124
125 fn key(location: &Path) -> String {
126 location.as_ref().to_string()
127 }
128
129 async fn fallback_get_opts(
130 &self,
131 location: &Path,
132 options: GetOptions,
133 error: anyhow::Error,
134 ) -> object_store::Result<GetResult> {
135 imetric!("l1_cache_fallback", "count", 1_u64);
136 debug!("L1 cache miss for {location}, falling back to origin: {error}");
137 self.origin.get_opts(location, options).await
138 }
139}
140
141impl std::fmt::Debug for L1CacheStore {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 write!(f, "L1CacheStore({})", self.origin)
144 }
145}
146
147impl std::fmt::Display for L1CacheStore {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "L1CacheStore({})", self.origin)
150 }
151}
152
153fn single_chunk_get_result(
155 data: Bytes,
156 range: Range<u64>,
157 object_size: u64,
158 location: &Path,
159) -> GetResult {
160 let meta = ObjectMeta {
161 location: location.clone(),
162 last_modified: chrono::Utc::now(),
163 size: object_size,
164 e_tag: None,
165 version: None,
166 };
167 let payload = GetResultPayload::Stream(Box::pin(stream::once(async move { Ok(data) })));
168 GetResult {
169 payload,
170 meta,
171 range,
172 attributes: Attributes::default(),
173 }
174}
175
176#[async_trait]
177impl ObjectStore for L1CacheStore {
178 async fn put_opts(
179 &self,
180 location: &Path,
181 payload: PutPayload,
182 opts: PutOptions,
183 ) -> object_store::Result<PutResult> {
184 self.origin.put_opts(location, payload, opts).await
185 }
186
187 async fn put_multipart_opts(
188 &self,
189 location: &Path,
190 opts: PutMultipartOptions,
191 ) -> object_store::Result<Box<dyn MultipartUpload>> {
192 self.origin.put_multipart_opts(location, opts).await
193 }
194
195 async fn get_opts(
196 &self,
197 location: &Path,
198 options: GetOptions,
199 ) -> object_store::Result<GetResult> {
200 if options.if_match.is_some()
203 || options.if_none_match.is_some()
204 || options.if_modified_since.is_some()
205 || options.if_unmodified_since.is_some()
206 || options.version.is_some()
207 || options.head
208 {
209 return self.origin.get_opts(location, options).await;
210 }
211
212 let key = Self::key(location);
213 let range = options.range.clone();
214
215 let resolved: anyhow::Result<(Bytes, Range<u64>, u64)> = async {
216 match &range {
217 Some(GetRange::Bounded(r)) => {
218 let r = r.clone();
219 let data = self.cache.get_range(&key, r.clone()).await?;
220 let size = self.cache.size(&key).await?;
227 Ok((data, r, size))
228 }
229 other => {
230 let size = self.cache.size(&key).await?;
231 let resolved_range = match other {
232 None => 0..size,
233 Some(GetRange::Offset(offset)) => *offset..size,
234 Some(GetRange::Suffix(suffix)) => size.saturating_sub(*suffix)..size,
235 Some(GetRange::Bounded(_)) => unreachable!("handled above"),
236 };
237 let data = self.cache.get_range(&key, resolved_range.clone()).await?;
238 Ok((data, resolved_range, size))
239 }
240 }
241 }
242 .await;
243
244 match resolved {
245 Ok((data, range, size)) => Ok(single_chunk_get_result(data, range, size, location)),
246 Err(e) => self.fallback_get_opts(location, options, e).await,
247 }
248 }
249
250 async fn get_ranges(
251 &self,
252 location: &Path,
253 ranges: &[Range<u64>],
254 ) -> object_store::Result<Vec<Bytes>> {
255 if ranges.is_empty() {
256 return Ok(vec![]);
257 }
258 let key = Self::key(location);
259 match self.cache.get_ranges(&key, ranges).await {
260 Ok(results) => Ok(results),
261 Err(e) => {
262 imetric!("l1_cache_fallback", "count", 1_u64);
263 debug!("L1 cache miss for {location} (ranges), falling back to origin: {e}");
264 self.origin.get_ranges(location, ranges).await
265 }
266 }
267 }
268
269 fn delete_stream(
270 &self,
271 locations: BoxStream<'static, object_store::Result<Path>>,
272 ) -> BoxStream<'static, object_store::Result<Path>> {
273 self.origin.delete_stream(locations)
274 }
275
276 fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, object_store::Result<ObjectMeta>> {
277 self.origin.list(prefix)
278 }
279
280 async fn list_with_delimiter(&self, prefix: Option<&Path>) -> object_store::Result<ListResult> {
281 self.origin.list_with_delimiter(prefix).await
282 }
283
284 async fn copy_opts(
285 &self,
286 from: &Path,
287 to: &Path,
288 options: CopyOptions,
289 ) -> object_store::Result<()> {
290 self.origin.copy_opts(from, to, options).await
291 }
292}