objectstore_service/backend/
counting.rs1use std::num::NonZeroU64;
18use std::sync::Arc;
19
20use objectstore_types::metadata::Metadata;
21use objectstore_types::range::ByteRange;
22use objectstore_types::resumable::UploadProgress;
23use objectstore_types::time::Timestamp;
24
25use crate::backend::common::{
26 Backend, DeleteResponse, GetResponse, MetadataResponse, MultipartUploadBackend, PutResponse,
27};
28use crate::error::Result;
29use crate::id::ObjectId;
30use crate::multipart::{
31 AbortMultipartResponse, CompleteMultipartResponse, CompletedPart, InitiateMultipartResponse,
32 ListPartsResponse, PartNumber, UploadId, UploadPartResponse,
33};
34use crate::resumable::BackendToken;
35use crate::stream::ClientStream;
36
37fn count(usecase: &str) {
42 objectstore_metrics::count!("cogs.usage" += 1, app_feature = usecase.to_owned());
43}
44
45#[derive(Debug)]
54pub struct CountingBackend {
55 inner: Arc<dyn Backend>,
56}
57
58impl CountingBackend {
59 pub fn new(inner: Box<dyn Backend>) -> Self {
62 let inner: Arc<dyn Backend> = Arc::from(inner);
63 Self { inner }
64 }
65}
66
67#[async_trait::async_trait]
68impl Backend for CountingBackend {
69 fn name(&self) -> &'static str {
70 self.inner.name()
71 }
72
73 async fn put_object(
74 &self,
75 id: &ObjectId,
76 metadata: &Metadata,
77 stream: ClientStream,
78 access_time: Timestamp,
79 ) -> Result<PutResponse> {
80 count(&id.context.usecase);
81 self.inner
82 .put_object(id, metadata, stream, access_time)
83 .await
84 }
85
86 async fn get_object(
87 &self,
88 id: &ObjectId,
89 access_time: Timestamp,
90 range: Option<ByteRange>,
91 ) -> Result<GetResponse> {
92 count(&id.context.usecase);
93 self.inner.get_object(id, access_time, range).await
94 }
95
96 async fn get_metadata(
97 &self,
98 id: &ObjectId,
99 access_time: Timestamp,
100 ) -> Result<MetadataResponse> {
101 count(&id.context.usecase);
102 self.inner.get_metadata(id, access_time).await
103 }
104
105 async fn set_expiry(
106 &self,
107 id: &ObjectId,
108 expire_at: Timestamp,
109 access_time: Timestamp,
110 ) -> Result<bool> {
111 count(&id.context.usecase);
112 self.inner.set_expiry(id, expire_at, access_time).await
113 }
114
115 async fn delete_object(&self, id: &ObjectId, access_time: Timestamp) -> Result<DeleteResponse> {
116 count(&id.context.usecase);
117 self.inner.delete_object(id, access_time).await
118 }
119
120 async fn join(&self) {
121 self.inner.join().await;
122 }
123
124 fn as_multipart_upload_backend(&self) -> Result<&dyn MultipartUploadBackend> {
125 self.inner.as_multipart_upload_backend()?;
126 Ok(self)
127 }
128
129 async fn create_upload_session(
130 &self,
131 id: &ObjectId,
132 metadata: &Metadata,
133 total_length: NonZeroU64,
134 ) -> Result<Option<BackendToken>> {
135 count(&id.context.usecase);
136 self.inner
137 .create_upload_session(id, metadata, total_length)
138 .await
139 }
140
141 async fn put_chunk(
142 &self,
143 id: &ObjectId,
144 token: &BackendToken,
145 offset: u64,
146 content_length: u64,
147 stream: ClientStream,
148 ) -> Result<UploadProgress> {
149 count(&id.context.usecase);
150 self.inner
151 .put_chunk(id, token, offset, content_length, stream)
152 .await
153 }
154
155 async fn upload_offset(&self, id: &ObjectId, token: &BackendToken) -> Result<UploadProgress> {
156 count(&id.context.usecase);
157 self.inner.upload_offset(id, token).await
158 }
159
160 async fn cancel_upload(&self, id: &ObjectId, token: &BackendToken) -> Result<()> {
161 count(&id.context.usecase);
162 self.inner.cancel_upload(id, token).await
163 }
164}
165
166#[async_trait::async_trait]
167impl MultipartUploadBackend for CountingBackend {
168 async fn initiate_multipart(
169 &self,
170 id: &ObjectId,
171 metadata: &Metadata,
172 ) -> Result<InitiateMultipartResponse> {
173 count(&id.context.usecase);
174 self.inner
175 .as_multipart_upload_backend()?
176 .initiate_multipart(id, metadata)
177 .await
178 }
179
180 async fn upload_part(
181 &self,
182 id: &ObjectId,
183 upload_id: &UploadId,
184 part_number: PartNumber,
185 content_length: u64,
186 content_md5: Option<&str>,
187 body: ClientStream,
188 ) -> Result<UploadPartResponse> {
189 count(&id.context.usecase);
190 self.inner
191 .as_multipart_upload_backend()?
192 .upload_part(
193 id,
194 upload_id,
195 part_number,
196 content_length,
197 content_md5,
198 body,
199 )
200 .await
201 }
202
203 async fn list_parts(
204 &self,
205 id: &ObjectId,
206 upload_id: &UploadId,
207 max_parts: Option<u32>,
208 part_number_marker: Option<PartNumber>,
209 ) -> Result<ListPartsResponse> {
210 count(&id.context.usecase);
211 self.inner
212 .as_multipart_upload_backend()?
213 .list_parts(id, upload_id, max_parts, part_number_marker)
214 .await
215 }
216
217 async fn abort_multipart(
218 &self,
219 id: &ObjectId,
220 upload_id: &UploadId,
221 ) -> Result<AbortMultipartResponse> {
222 count(&id.context.usecase);
223 self.inner
224 .as_multipart_upload_backend()?
225 .abort_multipart(id, upload_id)
226 .await
227 }
228
229 async fn complete_multipart(
230 &self,
231 id: &ObjectId,
232 upload_id: &UploadId,
233 parts: Vec<CompletedPart>,
234 access_time: Timestamp,
235 ) -> Result<CompleteMultipartResponse> {
236 count(&id.context.usecase);
237 self.inner
238 .as_multipart_upload_backend()?
239 .complete_multipart(id, upload_id, parts, access_time)
240 .await
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use objectstore_types::scope::{Scope, Scopes};
247
248 use super::*;
249 use crate::backend::in_memory::InMemoryBackend;
250 use crate::id::ObjectContext;
251 use crate::stream;
252
253 fn object_id(usecase: &str) -> ObjectId {
254 ObjectId::new(
255 ObjectContext {
256 usecase: usecase.into(),
257 scopes: Scopes::from_iter([Scope::create("org", "1").unwrap()]),
258 },
259 "key".into(),
260 )
261 }
262
263 fn capture(f: impl std::future::Future<Output = ()>) -> Vec<String> {
268 objectstore_metrics::with_capturing_test_client(|| {
269 tokio::runtime::Builder::new_current_thread()
270 .enable_all()
271 .build()
272 .unwrap()
273 .block_on(f);
274 })
275 }
276
277 #[test]
278 fn counts_each_core_operation_once() {
279 let captured = capture(async {
280 let backend = CountingBackend::new(Box::new(InMemoryBackend::new("in-memory")));
281 let id = object_id("attachments");
282
283 backend
284 .put_object(
285 &id,
286 &Metadata::default(),
287 stream::single("hi"),
288 Timestamp::now(),
289 )
290 .await
291 .unwrap();
292 backend
293 .get_object(&id, Timestamp::now(), None)
294 .await
295 .unwrap();
296 backend.get_metadata(&id, Timestamp::now()).await.unwrap();
297 backend.delete_object(&id, Timestamp::now()).await.unwrap();
298 });
299
300 let cogs = captured
301 .iter()
302 .filter(|m| m.starts_with("cogs.usage"))
303 .count();
304 assert_eq!(
305 cogs, 4,
306 "expected one count per operation, captured: {captured:?}"
307 );
308 assert!(
309 captured
310 .iter()
311 .all(|m| !m.starts_with("cogs.usage")
312 || m == "cogs.usage:+1|c|#app_feature:attachments"),
313 "captured: {captured:?}"
314 );
315 }
316
317 #[test]
318 fn counts_missing_reads_on_dispatch() {
319 let captured = capture(async {
320 let backend = CountingBackend::new(Box::new(InMemoryBackend::new("in-memory")));
321 let result = backend
323 .get_object(&object_id("attachments"), Timestamp::now(), None)
324 .await
325 .unwrap();
326 assert!(result.is_none());
327 });
328
329 assert_eq!(
330 captured
331 .iter()
332 .filter(|m| m.starts_with("cogs.usage"))
333 .count(),
334 1,
335 "captured: {captured:?}"
336 );
337 }
338
339 #[test]
340 fn new_usecase_is_app_feature() {
341 let captured = capture(async {
342 let backend = CountingBackend::new(Box::new(InMemoryBackend::new("in-memory")));
343 backend
344 .get_object(&object_id("new_usecase"), Timestamp::now(), None)
345 .await
346 .unwrap();
347 });
348
349 assert!(
350 captured
351 .iter()
352 .any(|m| m == "cogs.usage:+1|c|#app_feature:new_usecase"),
353 "captured: {captured:?}"
354 );
355 }
356
357 #[test]
358 fn counts_each_multipart_operation() {
359 let captured = capture(async {
360 let backend: Arc<dyn Backend> = Arc::new(CountingBackend::new(Box::new(
361 InMemoryBackend::new("in-memory"),
362 )));
363 let multipart = backend.as_multipart_upload_backend().unwrap();
364 let id = object_id("attachments");
365
366 let upload_id = multipart
367 .initiate_multipart(&id, &Metadata::default())
368 .await
369 .unwrap();
370 multipart
371 .upload_part(
372 &id,
373 &upload_id,
374 PartNumber::new(1).unwrap(),
375 2,
376 None,
377 stream::single("hi"),
378 )
379 .await
380 .unwrap();
381 multipart
382 .list_parts(&id, &upload_id, None, None)
383 .await
384 .unwrap();
385 multipart
386 .complete_multipart(&id, &upload_id, vec![], Timestamp::now())
387 .await
388 .unwrap();
389 let _ = multipart.abort_multipart(&id, &upload_id).await;
392 });
393
394 assert_eq!(
395 captured
396 .iter()
397 .filter(|m| m == &"cogs.usage:+1|c|#app_feature:attachments")
398 .count(),
399 5,
400 "expected one count per multipart operation, captured: {captured:?}"
401 );
402 }
403}