Storage internals¶
This document explains the internal design of tileverse-storage, with a focus on the RangeReader byte-range path: how it achieves a unified API across diverse storage backends and where the seams are for adding a new one.
Core Design¶
The library is built around a single, synchronous interface: RangeReader.
public interface RangeReader extends Closeable {
// The fundamental atomic operation
int readRange(long offset, int length, ByteBuffer target) throws IOException;
// Metadata
long size() throws IOException;
String getSourceIdentifier();
}
Design Decisions¶
- Synchronous API: We chose a blocking API over
CompletableFutureor Reactor. This simplifies the implementation of complex logic (like caching and retries) and aligns with Java'sFileChanneland standardInputStreampatterns, which are what most format parsers expect. - ByteBuffers: All data transfer happens via
java.nio.ByteBuffer. This allows for off-heap storage, direct memory mapping, and efficient I/O operations without unnecessary array copying. - Thread Safety: All implementations must be thread-safe. State (like connection pools) is shared, but individual read operations are isolated.
Implementation Hierarchy¶
Base Layer: AbstractRangeReader¶
This abstract class handles the boilerplate:
- Argument validation (bounds checks).
- Buffer handling (position management, slicing).
- Template pattern: delegates the actual byte fetching to
readRangeNoFlip.
Backend Layer¶
These classes implement the actual network/disk I/O:
FileRangeReader: WrapsFileChannel. Uses OS page cache.HttpRangeReader: Usesjava.net.http.HttpClientto issueGETrequests withRangeheaders. TheHttpClientis refcounted at theHttpStoragelevel (viaHttpClientCache) and shared across sibling readers; per-readerclose()is a no-op, mirroringS3/Azure/GCS. The client shuts down only when the lastHttpStorageholding a lease closes.S3RangeReader: Wraps AWS SDK v2. Maps exceptions to standardIOException. Uses anS3Clientrefcounted byS3ClientCacheat theS3Storagelevel.Azure/GCS: Similar wrappers for their respective SDKs, with matching refcounted client caches.
Decorator Layer¶
We use the Decorator pattern to add behaviors without modifying backends.
CachingRangeReader: InterceptsreadRange. Checks in-memory Caffeine cache. If miss, calls delegate, caches result, returns data.BlockAlignedRangeReader: Expands arbitrary read requests (e.g., "bytes 100-150") to align with specific block boundaries (e.g., "bytes 0-4096"), optimizing cache hit rates.
Runtime View¶
The runtime view describes the dynamic behavior of the library.
Basic File Range Reading¶
HTTP Range Reading with Authentication¶
Cache Miss Scenario¶
Service Provider Interface (SPI)¶
To support dynamic loading (e.g., for configuration-driven applications), we expose a StorageFactory.
- Discovery: Uses
java.util.ServiceLoaderto find registeredStorageProviderimplementations. - Resolution:
StorageFactory.open(uri)(orfindProvider(StorageConfig)) iterates providers. The first one returningtrueforcanProcess(StorageConfig)is instantiated; ambiguoushttp(s)://URIs go through a HEAD-probe disambiguation step. - Per-key reads:
Storage.openRangeReader(String key)on the returnedStorageproduces aRangeReaderfor a single object under the storage root. - Extensibility: Users can write their own backend (e.g.,
FtpStorageProvider) and register it viaMETA-INF/serviceswithout forking the codebase.
Dependency Structure¶
To avoid "dependency hell" (e.g., conflicting Netty versions between Azure and AWS SDKs), the core module has zero heavy dependencies.
tileverse-storage-core: Lightweight. Only depends on SLF4J and Caffeine.tileverse-storage-s3: Pulls in AWS SDK.tileverse-storage-azure: Pulls in Azure SDK.
This allows consumers to pick exactly the providers they need.