Crate objectstore_service

Crate objectstore_service 

Source
Expand description

The service layer is the core storage abstraction for objectstore. It provides durable access to blobs through a dual-backend architecture that balances cost, latency, and reliability. The service is designed as a library crate consumed by the objectstore-server.

§Object Identification

Every object is uniquely identified by an ObjectId, a logical address that is backend-independent — the same ObjectId refers to the same object regardless of which physical backend currently stores its data. This allows objects to be transparently moved between backends (e.g. during migrations or rebalancing) without changing their identity.

Identifiers are also designed to be self-contained: given an ObjectId, you can always determine which usecase and organizational scope the object belongs to. This makes references into objectstore meaningful on their own, without requiring a lookup.

An ObjectId consists of an ObjectContext (the where) and a key (the what). The context contains:

  • A usecase — a top-level namespace (e.g. "attachments", "debug-files") that groups related objects. A usecase can have its own server-level configuration such as rate limits or killswitches.
  • Scopes — ordered key-value pairs that form a hierarchy within a usecase, such as organization=17, project=42. They act as both an organizational structure and an authorization boundary.

See the id module for details on storage path formatting, scope ordering, and key generation.

§Stateless Design

The service layer has no caches or local state beyond what is needed for a single request. This is intentional:

  • Object sizes vary wildly — caching large objects is impractical.
  • Access patterns are write-once, read-few — the hit rate for a cache would be low.
  • The high-volume backend already provides low latency for the common case of small objects.
  • Horizontal scaling — without shared caches, any service instance can handle any request. There is no need to shard requests for read-after-write consistency or to replicate cache state.

The service orchestrates mature, battle-tested backends and keeps its own footprint minimal.

Each storage operation runs to completion even if the caller is cancelled (e.g., due to a client disconnect). This ensures that multi-step operations such as writing redirect tombstones are never left partially applied. Post-commit cleanup of unreferenced long-term blobs runs in background tasks so it does not block the caller. Operations are also panic-isolated — a failure in one request does not bring down the service.

§Two-Tier Backend System

TieredStorage is the Backend implementation that provides the two-tier system. It is the typical backend passed to StorageService::new, though any Backend implementation can be used. The two-tier split exists because no single storage system optimally handles both small, frequently-accessed objects and large, infrequently-accessed ones:

  • High-volume backend (typically BigTable): optimized for low-latency reads and writes of small objects. Objects in practice are small (metadata blobs, event attachments, etc.), so this path handles the majority of traffic by volume.
  • Long-term backend (typically GCS): optimized for large objects and long retention periods where per-byte storage cost matters more than access latency.

The threshold is 1 MiB. TieredStorage routes objects at or below this size to the high-volume backend; objects exceeding it go to the long-term backend.

See backend::StorageConfig for available backend implementations.

§Redirect Tombstones

For large objects, TieredStorage stores a redirect tombstone in the high-volume backend — a marker that carries the target ObjectId where the real payload lives in the long-term backend. Reads check only the high-volume backend: they either find the object directly (small) or follow the tombstone’s target to long-term storage (large), without probing both backends.

How tombstones are physically stored is determined by the HighVolumeBackend implementation. Refer to the backend’s own documentation for storage format details.

§Cross-Tier Consistency

Because a single logical object may span both backends (tombstone in HV, payload in LT), mutations must keep them in sync without distributed locks. The high-volume backend must implement HighVolumeBackend, which provides compare-and-swap operations that TieredStorage uses to atomically commit cross-tier state changes — rolling back on conflict so that concurrent writers never corrupt each other’s data. After the commit point, cleanup of the now-unreferenced LT blob is performed in the background so the caller is not blocked by cross-backend I/O. Backend::join waits for outstanding cleanup during graceful shutdown.

See the backend::tiered module documentation for the per-operation sequences.

§Metadata and Payload

Every object consists of structured metadata and a binary payload. Metadata contains a set of built-in keys with special semantics (such as expiration policies) as well as arbitrary user-defined key-value pairs. Metadata is always stored alongside the payload in the same backend — never in a separate data store. This ensures that inspecting a backend directly is sufficient to resolve an object together with its metadata, without joining across stores.

Metadata is small and always fully loaded into memory, while the payload streams. Backends can serve metadata independently of the payload (e.g. BigTable uses separate column families; GCS stores metadata as object headers), which enables efficient metadata-only reads.

Individual metadata keys are mutable — they can be updated without rewriting the payload. Payloads, however, can only be replaced in their entirety.

§Streaming and Buffering

Data flows in streams throughout the API to keep memory consumption low. See the stream module for the stream types and related utilities.

On writes, the incoming request body arrives as a ClientStream. The service buffers it only up to the 1 MiB threshold to determine which backend to use. Once exceeded, the buffered bytes are prepended to the remaining stream and everything flows through to the long-term backend without further accumulation.

On reads, the backend returns a PayloadStream that the service forwards to the caller. Not all backends stream small payloads (e.g. BigTable returns them in a single response), but for large objects in the long-term backend, data is streamed end-to-end.

§Expiration

Expiration policies are part of the built-in object metadata and can carry special semantics. The service delegates expiry entirely to the backend implementation, allowing each backend to leverage its underlying system’s native capabilities. For example, BigTable has built-in TTL via garbage collection policies, and GCS supports object lifecycle management. The service does not perform active garbage collection.

§Backpressure

The service applies backpressure to protect backends from overload. Rather than queueing work when capacity is exhausted, the service rejects operations immediately so the caller can shed load or retry.

§Concurrency Limit

A semaphore caps the total number of in-flight backend operations across all callers. A permit is acquired before each operation is spawned and held until the task completes — including on panic — so the limit counts running operations, not queued ones. When no permits are available, the operation fails with Error::AtCapacity.

The default limit is DEFAULT_CONCURRENCY_LIMIT. Callers can override it via StorageService::with_concurrency_limit.

§Streaming Concurrency

The streaming module provides StreamExecutor for running a stream of operations concurrently within a bounded window. It is intended for efficient handling of batch requests, where multiple operations arrive together and should be dispatched in parallel rather than sequentially. See the module documentation for the window formula, permit reservation, lazy pulling, memory bounds, and concurrency model.

§Further Plans

More backpressure mechanisms (e.g. per-backend limits, adaptive throttling) may be added here in the future.

Re-exports§

pub use service::StorageService;
pub use stream::ClientStream;
pub use stream::PayloadStream;

Modules§

backend
Storage backend implementations.
error
Error types for service and backend operations.
id
Definitions for object identifiers, including usecases and scopes.
service
Core storage service and configuration.
stream
Stream types and buffering utilities for object data.
streaming
Streaming operation types and concurrent executor.