1use std::sync::Arc;
26
27use futures_util::{Stream, StreamExt};
28use objectstore_types::metadata::Metadata;
29
30use crate::backend::common::Backend;
31use crate::concurrency::ConcurrencyLimiter;
32use crate::error::{Error, Result};
33use crate::id::{ObjectContext, ObjectId, ObjectKey};
34use crate::service::GetResponse;
35
36#[derive(Debug)]
38pub struct Insert {
39 pub key: Option<ObjectKey>,
41 pub metadata: Metadata,
43 pub payload: bytes::Bytes,
45}
46
47#[derive(Debug)]
49pub struct Get {
50 pub key: ObjectKey,
52}
53
54#[derive(Debug)]
56pub struct Delete {
57 pub key: ObjectKey,
59}
60
61#[derive(Debug)]
63pub struct Head {
64 pub key: ObjectKey,
66}
67
68#[derive(Debug)]
70pub enum Operation {
71 Insert(Box<Insert>),
73 Get(Get),
75 Delete(Delete),
77 Head(Head),
79}
80
81impl Operation {
82 pub fn key(&self) -> Option<&ObjectKey> {
84 match self {
85 Operation::Insert(op) => op.key.as_ref(),
86 Operation::Get(op) => Some(&op.key),
87 Operation::Delete(op) => Some(&op.key),
88 Operation::Head(op) => Some(&op.key),
89 }
90 }
91
92 pub fn permission(&self) -> objectstore_types::auth::Permission {
94 match self {
95 Operation::Get(_) | Operation::Head(_) => {
96 objectstore_types::auth::Permission::ObjectRead
97 }
98 Operation::Insert(_) => objectstore_types::auth::Permission::ObjectWrite,
99 Operation::Delete(_) => objectstore_types::auth::Permission::ObjectDelete,
100 }
101 }
102
103 pub fn kind(&self) -> &'static str {
105 match self {
106 Operation::Insert(_) => "insert",
107 Operation::Get(_) => "get",
108 Operation::Delete(_) => "delete",
109 Operation::Head(_) => "head",
110 }
111 }
112}
113
114pub enum OpResponse {
119 Inserted {
121 id: ObjectId,
123 },
124 Got {
126 key: ObjectKey,
128 response: GetResponse,
130 },
131 Deleted {
133 key: ObjectKey,
135 },
136 Head {
138 key: ObjectKey,
140 metadata: Option<Metadata>,
142 },
143}
144
145impl OpResponse {
146 pub fn kind(&self) -> &'static str {
148 match self {
149 OpResponse::Inserted { .. } => "insert",
150 OpResponse::Got { .. } => "get",
151 OpResponse::Deleted { .. } => "delete",
152 OpResponse::Head { .. } => "head",
153 }
154 }
155
156 pub fn key(&self) -> &ObjectKey {
158 match self {
159 OpResponse::Inserted { id } => &id.key,
160 OpResponse::Got { key, .. } => key,
161 OpResponse::Deleted { key } => key,
162 OpResponse::Head { key, .. } => key,
163 }
164 }
165}
166
167impl std::fmt::Debug for OpResponse {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match self {
170 OpResponse::Inserted { id } => f.debug_struct("Inserted").field("id", id).finish(),
171 OpResponse::Got {
172 key,
173 response: Some(_),
174 } => f
175 .debug_struct("Got")
176 .field("key", key)
177 .field("response", &format_args!("Some(<stream>)"))
178 .finish(),
179 OpResponse::Got {
180 key,
181 response: None,
182 } => f
183 .debug_struct("Got")
184 .field("key", key)
185 .field("response", &format_args!("None"))
186 .finish(),
187 OpResponse::Deleted { key } => f.debug_struct("Deleted").field("key", key).finish(),
188 OpResponse::Head { key, metadata } => f
189 .debug_struct("Head")
190 .field("key", key)
191 .field("metadata", &metadata.is_some())
192 .finish(),
193 }
194 }
195}
196
197#[derive(Debug)]
205pub struct StreamExecutor {
206 backend: Arc<dyn Backend>,
207 concurrency: ConcurrencyLimiter,
208}
209
210impl StreamExecutor {
211 pub fn new(backend: Arc<dyn Backend>, concurrency: ConcurrencyLimiter) -> Self {
213 Self {
214 backend,
215 concurrency,
216 }
217 }
218
219 pub fn execute<E>(
235 self,
236 context: ObjectContext,
237 operations: impl Stream<Item = (usize, Result<Operation, E>)> + Send + 'static,
238 ) -> impl Stream<Item = (usize, Result<OpResponse, E>)> + Send + 'static
239 where
240 E: From<Error> + Send + 'static,
241 {
242 let StreamExecutor {
243 backend,
244 concurrency,
245 } = self;
246
247 let buffer = concurrency.total_bulk().max(1) as usize;
248
249 operations
250 .then(move |(idx, item)| {
255 let concurrency = concurrency.clone();
256 async move {
257 let op = match item {
258 Ok(op) => op,
259 Err(e) => return (idx, Err(e)),
260 };
261 match concurrency.acquire_bulk().await {
262 Ok(permit) => (idx, Ok((op, permit))),
263 Err(e) => {
264 objectstore_metrics::count!(
265 "service.concurrency.rejected",
266 class = "bulk"
267 );
268 objectstore_log::warn!("Bulk operation rejected: service at capacity");
269 (idx, Err(E::from(e)))
270 }
271 }
272 }
273 })
274 .map(move |(idx, result)| {
275 let backend = Arc::clone(&backend);
276 let context = context.clone();
277 async move {
278 let (op, permit) = match result {
279 Ok(pair) => pair,
280 Err(e) => return (idx, Err(e)),
281 };
282
283 let spawn = crate::concurrency::spawn_metered(op.kind(), permit, {
284 execute_operation(backend, context, op)
285 });
286 (idx, spawn.await.map_err(E::from))
287 }
288 })
289 .buffer_unordered(buffer)
290 }
291}
292
293async fn execute_operation(
294 backend: Arc<dyn Backend>,
295 context: ObjectContext,
296 op: Operation,
297) -> Result<OpResponse> {
298 match op {
299 Operation::Get(get) => {
300 let id = ObjectId::new(context, get.key);
301 let response = backend.get_object(&id, None).await?;
302 Ok(OpResponse::Got {
303 key: id.key,
304 response,
305 })
306 }
307 Operation::Insert(insert) => {
308 let id = ObjectId::optional(context, insert.key);
309 let stream = crate::stream::single(insert.payload);
310 backend.put_object(&id, &insert.metadata, stream).await?;
311 Ok(OpResponse::Inserted { id })
312 }
313 Operation::Delete(delete) => {
314 let id = ObjectId::new(context, delete.key);
315 backend.delete_object(&id).await?;
316 Ok(OpResponse::Deleted { key: id.key })
317 }
318 Operation::Head(head) => {
319 let id = ObjectId::new(context, head.key);
320 let metadata = backend.get_metadata(&id).await?;
321 Ok(OpResponse::Head {
322 key: id.key,
323 metadata,
324 })
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use std::sync::Arc;
332 use std::sync::atomic::{AtomicUsize, Ordering};
333 use std::time::Duration;
334
335 use bytes::Bytes;
336 use futures_util::StreamExt;
337 use objectstore_types::metadata::Metadata;
338 use objectstore_types::scope::{Scope, Scopes};
339
340 use super::*;
341 use crate::backend::common::PutResponse;
342 use crate::backend::in_memory::InMemoryBackend;
343 use crate::backend::testing::{Hooks, TestBackend};
344 use crate::concurrency::ConcurrencyLimiter;
345 use crate::error::Error;
346 use crate::service::StorageService;
347 use crate::stream::{self, ClientStream};
348
349 fn make_context() -> ObjectContext {
350 ObjectContext {
351 usecase: "testing".into(),
352 scopes: Scopes::from_iter([Scope::create("testing", "value").unwrap()]),
353 }
354 }
355
356 fn make_service_with_limit(limit: u32) -> StorageService {
357 StorageService::new(Box::new(InMemoryBackend::new("in-memory")))
358 .with_concurrency(ConcurrencyLimiter::new(limit))
359 }
360
361 fn make_service() -> StorageService {
362 make_service_with_limit(500)
363 }
364
365 fn indexed_ok(ops: Vec<Operation>) -> impl Stream<Item = (usize, Result<Operation, Error>)> {
367 futures_util::stream::iter(ops.into_iter().enumerate().map(|(i, op)| (i, Ok(op))))
368 }
369
370 #[tokio::test]
373 async fn execute_empty_stream() {
374 let service = make_service();
375 let executor = service.stream();
376 let outcomes: Vec<_> = executor
377 .execute(
378 make_context(),
379 futures_util::stream::empty::<(usize, Result<Operation, Error>)>(),
380 )
381 .collect()
382 .await;
383 assert!(outcomes.is_empty());
384 }
385
386 #[tokio::test]
387 async fn execute_runs_all_operations() {
388 let service = make_service();
389 let context = make_context();
390
391 service
393 .insert_object(
394 context.clone(),
395 Some("key1".into()),
396 Metadata::default(),
397 stream::single("hello"),
398 )
399 .await
400 .unwrap();
401
402 let ops = vec![
403 Operation::Get(Get { key: "key1".into() }),
404 Operation::Get(Get {
405 key: "nonexistent".into(),
406 }),
407 Operation::Insert(Box::new(Insert {
408 key: Some("key2".into()),
409 metadata: Metadata::default(),
410 payload: Bytes::from("world"),
411 })),
412 Operation::Delete(Delete { key: "key1".into() }),
413 ];
414
415 let executor = service.stream();
416 let outcomes: Vec<_> = executor.execute(context, indexed_ok(ops)).collect().await;
417
418 assert_eq!(outcomes.len(), 4);
419
420 for (_, result) in &outcomes {
421 let response = result
422 .as_ref()
423 .unwrap_or_else(|e| panic!("unexpected error: {e:?}"));
424 assert!(
425 !response.key().as_str().is_empty(),
426 "response must have a non-empty key"
427 );
428 }
429 }
430
431 #[tokio::test]
432 async fn execute_head_operation() {
433 let service = make_service();
434 let context = make_context();
435
436 service
437 .insert_object(
438 context.clone(),
439 Some("exists".into()),
440 Metadata::default(),
441 stream::single("data"),
442 )
443 .await
444 .unwrap();
445
446 let ops = vec![
447 Operation::Head(Head {
448 key: "exists".into(),
449 }),
450 Operation::Head(Head {
451 key: "missing".into(),
452 }),
453 ];
454
455 let executor = service.stream();
456 let mut outcomes: Vec<_> = executor.execute(context, indexed_ok(ops)).collect().await;
457 outcomes.sort_by_key(|(idx, _)| *idx);
458
459 assert_eq!(outcomes.len(), 2);
460
461 match &outcomes[0].1 {
462 Ok(OpResponse::Head {
463 key,
464 metadata: Some(_),
465 }) => assert_eq!(key.as_str(), "exists"),
466 other => panic!("expected Head with metadata, got: {other:?}"),
467 }
468
469 match &outcomes[1].1 {
470 Ok(OpResponse::Head {
471 key,
472 metadata: None,
473 }) => assert_eq!(key.as_str(), "missing"),
474 other => panic!("expected Head with None, got: {other:?}"),
475 }
476 }
477
478 struct GateOnPut {
481 paused_tx: tokio::sync::mpsc::Sender<()>,
482 resume: Arc<tokio::sync::Notify>,
483 in_flight: Arc<AtomicUsize>,
484 }
485
486 impl std::fmt::Debug for GateOnPut {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 f.debug_struct("GateOnPut").finish()
489 }
490 }
491
492 #[async_trait::async_trait]
493 impl Hooks for GateOnPut {
494 async fn put_object(
495 &self,
496 inner: &InMemoryBackend,
497 id: &ObjectId,
498 metadata: &Metadata,
499 stream: ClientStream,
500 ) -> Result<PutResponse> {
501 self.in_flight.fetch_add(1, Ordering::SeqCst);
502 let _ = self.paused_tx.send(()).await;
503 self.resume.notified().await;
504 let result = inner.put_object(id, metadata, stream).await;
505 self.in_flight.fetch_sub(1, Ordering::SeqCst);
506 result
507 }
508 }
509
510 #[tokio::test]
511 async fn concurrent_execution() {
512 let (paused_tx, mut paused_rx) = tokio::sync::mpsc::channel::<()>(20);
513 let resume = Arc::new(tokio::sync::Notify::new());
514 let in_flight = Arc::new(AtomicUsize::new(0));
515
516 let gated = TestBackend::new(GateOnPut {
517 paused_tx,
518 resume: Arc::clone(&resume),
519 in_flight: Arc::clone(&in_flight),
520 });
521 let service =
522 StorageService::new(Box::new(gated)).with_concurrency(ConcurrencyLimiter::new(100));
523
524 let ops: Vec<Operation> = (0..10)
525 .map(|i| {
526 Operation::Insert(Box::new(Insert {
527 key: Some(format!("key{i}")),
528 metadata: Metadata::default(),
529 payload: Bytes::from(format!("data{i}")),
530 }))
531 })
532 .collect();
533
534 let executor = service.stream();
535 let exec_handle = tokio::spawn(async move {
536 executor
537 .execute(make_context(), indexed_ok(ops))
538 .collect::<Vec<_>>()
539 .await
540 });
541
542 for _ in 0..10 {
544 paused_rx.recv().await.unwrap();
545 }
546 assert_eq!(in_flight.load(Ordering::SeqCst), 10);
547
548 resume.notify_waiters();
550
551 let outcomes = exec_handle.await.unwrap();
552 assert_eq!(outcomes.len(), 10);
553 for (_, result) in &outcomes {
554 assert!(
555 matches!(result, Ok(OpResponse::Inserted { .. })),
556 "unexpected result: {result:?}",
557 );
558 }
559 }
560
561 #[tokio::test]
562 async fn bulk_respects_budget() {
563 let service = StorageService::new(Box::new(InMemoryBackend::new("in-memory")))
566 .with_concurrency(
567 ConcurrencyLimiter::new(1)
568 .with_queue(0)
569 .with_timeout(Duration::from_millis(1))
570 .with_bulk(100),
571 );
572
573 let _held = service.concurrency_limiter().acquire().await.unwrap();
574
575 let ops = vec![Operation::Insert(Box::new(Insert {
576 key: Some("blocked".into()),
577 metadata: Metadata::default(),
578 payload: Bytes::from("data"),
579 }))];
580
581 let executor = service.stream();
582 let outcomes: Vec<_> = executor
583 .execute(make_context(), indexed_ok(ops))
584 .collect()
585 .await;
586
587 assert_eq!(outcomes.len(), 1);
588 assert!(
589 matches!(&outcomes[0].1, Err(Error::AtCapacity)),
590 "expected AtCapacity, got {:?}",
591 outcomes[0].1,
592 );
593 }
594}