3#ifndef LIBZARR_SHARDING_HPP
4#define LIBZARR_SHARDING_HPP
16#include "libzarr/detail/common.hpp"
30namespace zarr::detail_shard {
32inline constexpr std::uint64_t kSentinel = std::numeric_limits<std::uint64_t>::max();
35 std::uint64_t offset = kSentinel;
36 std::uint64_t nbytes = kSentinel;
37 [[nodiscard]]
bool missing()
const {
return offset == kSentinel && nbytes == kSentinel; }
43inline std::uint64_t index_encoded_size(
const std::vector<CodecSpec>& index_codecs,
44 std::uint64_t entry_count,
const std::string& ctx) {
45 std::uint64_t size = entry_count * 2 * 8;
46 bool have_bytes =
false;
47 for (
const CodecSpec& codec : index_codecs) {
48 if (codec.name ==
"bytes") {
50 }
else if (codec.name ==
"crc32c") {
54 ": index_codecs may only contain 'bytes' and 'crc32c' (the index must "
55 "have a fixed encoded size), got '" +
60 throw error(ctx +
": index_codecs must contain the 'bytes' codec");
69 std::string chunk_prefix;
75 std::vector<std::uint64_t> per_shard;
77 std::vector<std::uint64_t> inner_grid;
79 std::vector<CodecSpec> index_codecs;
81 bool index_at_end =
true;
87inline ShardParams params_for_level(
const ArrayMeta& meta, std::size_t level,
88 const std::string& prefix) {
89 const ShardLevel& lvl = meta.shard_levels[level];
90 const std::vector<std::uint64_t>& inner_shape = level + 1 < meta.shard_levels.size()
91 ? meta.shard_levels[level + 1].shard_shape
94 params.chunk_prefix = prefix;
95 params.key_encoding = meta.key_encoding;
96 params.separator = meta.dimension_separator;
97 params.index_codecs = lvl.index_codecs;
98 params.index_at_end = lvl.index_at_end;
99 params.per_shard.resize(inner_shape.size());
100 params.inner_grid.resize(inner_shape.size());
101 for (std::size_t d = 0; d < inner_shape.size(); ++d) {
102 params.per_shard[d] = lvl.shard_shape[d] / inner_shape[d];
103 params.inner_grid[d] = detail::ceil_div(meta.shape[d], inner_shape[d]);
110inline void locate_index(
const ShardParams& params,
const std::vector<std::uint64_t>& index,
111 std::string& shard_key, std::uint64_t& slot) {
112 const std::size_t rank = params.per_shard.size();
113 std::vector<std::uint64_t> outer(rank, 0);
115 for (std::size_t d = 0; d < rank; ++d) {
116 outer[d] = index[d] / params.per_shard[d];
117 slot = slot * params.per_shard[d] + index[d] % params.per_shard[d];
119 shard_key = params.chunk_prefix + (params.key_encoding == ChunkKeyKind::v3_default
120 ? v3::chunk_key(outer, params.separator)
121 : v2::
chunk_key(outer, params.separator));
125inline ArrayMeta index_array_meta(std::uint64_t entry_count,
126 const std::vector<CodecSpec>& index_codecs) {
128 meta.
shape = {entry_count * 2};
129 meta.chunk_shape = {entry_count * 2};
131 meta.codecs = index_codecs;
138inline std::vector<IndexEntry> decode_index(
const CodecPipeline& pipeline,
139 std::uint64_t entry_count, Bytes stored,
140 const std::string& ctx) {
141 const Bytes decoded = pipeline.decode(std::move(stored));
142 if (decoded.size() != entry_count * 16) {
143 throw error(ctx +
": corrupt shard index");
145 std::vector<IndexEntry> entries(
static_cast<std::size_t
>(entry_count));
146 for (std::size_t i = 0; i < entries.size(); ++i) {
147 std::memcpy(&entries[i].offset, decoded.data() + i * 16, 8);
148 std::memcpy(&entries[i].nbytes, decoded.data() + i * 16 + 8, 8);
149 const bool sentinel_mismatch =
150 (entries[i].offset == kSentinel) != (entries[i].nbytes == kSentinel);
151 const bool overflow =
152 !entries[i].missing() && entries[i].nbytes > kSentinel - entries[i].offset;
153 if (sentinel_mismatch || overflow) {
154 throw error(ctx +
": corrupt shard index");
165inline Bytes assemble_shard(
const std::vector<std::optional<Bytes>>& entries,
166 const std::vector<CodecSpec>& index_codecs,
bool index_at_end,
167 const std::string& ctx) {
169 for (
const auto& entry : entries) {
170 any = any || entry.has_value();
175 const std::uint64_t entry_count = entries.size();
176 const std::uint64_t index_size = index_encoded_size(index_codecs, entry_count, ctx);
177 const CodecPipeline index_pipeline =
181 std::vector<std::uint64_t> raw_index(entries.size() * 2, kSentinel);
182 const std::uint64_t base = index_at_end ? 0 : index_size;
183 for (std::size_t i = 0; i < entries.size(); ++i) {
184 const std::optional<Bytes>& entry = entries[i];
188 raw_index[i * 2] = base + body.size();
189 raw_index[i * 2 + 1] = entry->size();
190 body.insert(body.end(), entry->begin(), entry->end());
192 Bytes index_bytes(raw_index.size() * 8);
193 std::memcpy(index_bytes.data(), raw_index.data(), index_bytes.size());
194 index_bytes = index_pipeline.encode(std::move(index_bytes));
197 shard.reserve(body.size() + index_bytes.size());
199 shard = std::move(body);
200 shard.insert(shard.end(), index_bytes.begin(), index_bytes.end());
202 shard = std::move(index_bytes);
203 shard.insert(shard.end(), body.begin(), body.end());
216class ShardStore final :
public Store {
219 ShardStore(std::shared_ptr<Store> source, ShardParams params)
220 : source_(std::move(source)),
221 params_(std::move(params)),
222 entry_count_(detail::checked_product(params_.per_shard,
"shard grid")),
223 index_size_(detail_shard::index_encoded_size(params_.index_codecs, entry_count_,
224 "sharding_indexed")),
225 index_pipeline_(CodecPipeline::resolve(index_meta())) {
227 throw error(
"ShardStore: null store");
231 ~ShardStore()
override =
default;
232 ShardStore(
const ShardStore&) =
delete;
233 ShardStore& operator=(
const ShardStore&) =
delete;
234 ShardStore(ShardStore&&) =
delete;
235 ShardStore& operator=(ShardStore&&) =
delete;
237 [[nodiscard]] std::optional<Bytes>
read(std::string_view key)
override {
241 [[nodiscard]] std::optional<Bytes>
read_range(std::string_view key, ByteRange range)
override {
242 const Location loc = locate(key);
243 if (assembly_ && assembly_->shard_key == loc.shard_key) {
245 const auto& entry = assembly_->entries[loc.intra];
249 return slice_value(*entry, range, key);
251 const auto* index = load_index(loc.shard_key);
252 if (index ==
nullptr || (*index)[loc.intra].missing()) {
255 const detail_shard::IndexEntry entry = (*index)[loc.intra];
256 std::uint64_t begin = 0;
257 std::uint64_t count = entry.nbytes;
258 resolve_range(range, entry.nbytes, begin, count, key);
259 return source_->read_range(loc.shard_key,
ByteRange::slice(entry.offset + begin, count));
262 [[nodiscard]] std::optional<std::uint64_t> size(std::string_view key)
override {
263 const Location loc = locate(key);
264 if (assembly_ && assembly_->shard_key == loc.shard_key) {
265 const auto& entry = assembly_->entries[loc.intra];
269 return entry->size();
271 const auto* index = load_index(loc.shard_key);
272 if (index ==
nullptr || (*index)[loc.intra].missing()) {
275 return (*index)[loc.intra].nbytes;
278 [[nodiscard]]
bool exists(std::string_view key)
override {
return size(key).has_value(); }
280 void write(std::string_view key, Bytes value)
override {
281 put(locate(key), std::optional<Bytes>(std::move(value)));
284 void erase(std::string_view key)
override { put(locate(key), std::nullopt); }
288 void flush()
override {
293 [[nodiscard]] std::vector<std::string>
list_prefix(std::string_view )
override {
294 throw error(
"ShardStore is an internal adapter; listing is not supported");
296 [[nodiscard]] DirListing
list_dir(std::string_view )
override {
297 throw error(
"ShardStore is an internal adapter; listing is not supported");
302 std::string shard_key;
303 std::size_t intra = 0;
307 std::string shard_key;
308 std::vector<std::optional<Bytes>> entries;
313 [[nodiscard]] ArrayMeta index_meta()
const {
314 return index_array_meta(entry_count_, params_.index_codecs);
317 static void resolve_range(ByteRange range, std::uint64_t size, std::uint64_t& begin,
318 std::uint64_t& count, std::string_view key) {
320 if (range.length > size || range.offset > size - range.length) {
321 throw error(
"read_range: slice out of bounds for inner chunk '" + std::string(key) +
"' (" +
322 std::to_string(size) +
" bytes)");
324 begin = range.offset;
325 count = range.length;
327 if (range.length > size) {
328 throw error(
"read_range: suffix out of bounds for inner chunk '" + std::string(key) +
329 "' (" + std::to_string(size) +
" bytes)");
331 begin = size - range.length;
332 count = range.length;
336 [[nodiscard]]
static std::optional<Bytes> slice_value(
const Bytes& value, ByteRange range,
337 std::string_view key) {
338 std::uint64_t begin = 0;
339 std::uint64_t count = value.size();
340 resolve_range(range, value.size(), begin, count, key);
341 const auto first = value.begin() +
static_cast<std::ptrdiff_t
>(begin);
342 return Bytes(first, first +
static_cast<std::ptrdiff_t
>(count));
347 [[nodiscard]] Location locate(std::string_view key)
const {
348 const std::size_t rank = params_.per_shard.size();
349 std::string_view rest = key;
350 if (!detail::starts_with(rest, params_.chunk_prefix)) {
351 throw error(
"ShardStore: key '" + std::string(key) +
"' is outside the array's chunks");
353 rest = rest.substr(params_.chunk_prefix.size());
354 if (params_.key_encoding == ChunkKeyKind::v3_default) {
355 if (rest.empty() || rest[0] !=
'c' || (rest.size() > 1 && rest[1] != params_.separator)) {
356 throw error(
"ShardStore: malformed chunk key '" + std::string(key) +
"'");
358 rest = rest.size() > 1 ? rest.substr(2) : rest.substr(1);
360 std::vector<std::uint64_t> index(rank, 0);
362 for (std::size_t d = 0; d < rank; ++d) {
363 std::uint64_t value = 0;
364 const std::size_t start = pos;
365 while (pos < rest.size() && rest[pos] >=
'0' && rest[pos] <=
'9') {
366 value = value * 10 +
static_cast<std::uint64_t
>(rest[pos] -
'0');
369 if (pos == start || value >= params_.inner_grid[d]) {
370 throw error(
"ShardStore: malformed chunk key '" + std::string(key) +
"'");
374 if (pos >= rest.size() || rest[pos] != params_.separator) {
375 throw error(
"ShardStore: malformed chunk key '" + std::string(key) +
"'");
380 if (pos != rest.size()) {
381 throw error(
"ShardStore: malformed chunk key '" + std::string(key) +
"'");
385 std::uint64_t slot = 0;
386 locate_index(params_, index, loc.shard_key, slot);
387 loc.intra = detail::checked_size(slot,
"shard entry");
393 [[nodiscard]]
const std::vector<detail_shard::IndexEntry>* load_index(
394 const std::string& shard_key) {
395 for (std::size_t i = 0; i < cache_.size(); ++i) {
396 if (cache_[i].first == shard_key) {
398 std::rotate(cache_.begin(), cache_.begin() +
static_cast<std::ptrdiff_t
>(i),
399 cache_.begin() +
static_cast<std::ptrdiff_t
>(i) + 1);
401 return &cache_.front().second;
405 source_->read_range(shard_key, params_.index_at_end ?
ByteRange::suffix(index_size_)
406 : ByteRange::slice(0, index_size_));
410 std::vector<detail_shard::IndexEntry> entries =
411 decode_index(index_pipeline_, entry_count_, std::move(*stored), shard_key);
412 cache_.insert(cache_.begin(), {shard_key, std::move(entries)});
413 if (cache_.size() > kCacheCapacity) {
416 return &cache_.front().second;
419 void put(
const Location& loc, std::optional<Bytes> value) {
420 if (assembly_ && assembly_->shard_key != loc.shard_key) {
424 assembly_ = load_assembly(loc.shard_key);
426 assembly_->entries[loc.intra] = std::move(value);
427 assembly_->dirty =
true;
432 [[nodiscard]] Assembly load_assembly(
const std::string& shard_key) {
434 assembly.shard_key = shard_key;
435 assembly.entries.assign(
static_cast<std::size_t
>(entry_count_), std::nullopt);
436 const auto stored = source_->read(shard_key);
438 const auto* index = load_index(shard_key);
439 assert(index !=
nullptr);
440 for (std::size_t i = 0; i < index->size(); ++i) {
441 const detail_shard::IndexEntry entry = (*index)[i];
442 if (entry.missing()) {
445 if (entry.offset > stored->size() || entry.nbytes > stored->size() - entry.offset) {
446 throw error(shard_key +
": shard index points outside the shard");
448 const auto first = stored->begin() +
static_cast<std::ptrdiff_t
>(entry.offset);
449 assembly.entries[i] =
Bytes(first, first +
static_cast<std::ptrdiff_t
>(entry.nbytes));
455 void flush_assembly() {
459 const Assembly assembly = *std::move(assembly_);
461 if (!assembly.dirty) {
464 drop_cached(assembly.shard_key);
465 Bytes shard = assemble_shard(assembly.entries, params_.index_codecs, params_.index_at_end,
468 source_->erase(assembly.shard_key);
470 source_->write(assembly.shard_key, std::move(shard));
474 void drop_cached(
const std::string& shard_key) {
475 for (std::size_t i = 0; i < cache_.size(); ++i) {
476 if (cache_[i].first == shard_key) {
477 cache_.erase(cache_.begin() +
static_cast<std::ptrdiff_t
>(i));
483 static constexpr std::size_t kCacheCapacity = 16;
485 std::shared_ptr<Store> source_;
487 std::uint64_t entry_count_;
488 std::uint64_t index_size_;
489 CodecPipeline index_pipeline_;
490 std::vector<std::pair<std::string, std::vector<detail_shard::IndexEntry>>> cache_;
491 std::optional<Assembly> assembly_;
536 const std::vector<std::uint64_t>& inner_index,
537 std::size_t level = 0) {
539 throw error(
"zarr::shard::place: level " + std::to_string(level) +
" is not a shard level (" +
542 const std::string prefix = path.empty() ?
"" : path +
"/";
543 const detail_shard::ShardParams params = detail_shard::params_for_level(meta, level, prefix);
544 if (inner_index.size() != params.per_shard.size()) {
545 throw error(
"zarr::shard::place: inner_index rank " + std::to_string(inner_index.size()) +
546 " != array rank " + std::to_string(params.per_shard.size()));
548 for (std::size_t d = 0; d < inner_index.size(); ++d) {
549 if (inner_index[d] >= params.inner_grid[d]) {
550 throw error(
"zarr::shard::place: inner_index[" + std::to_string(d) +
551 "] = " + std::to_string(inner_index[d]) +
" out of range");
555 detail_shard::locate_index(params, inner_index, out.
shard_key, out.
slot);
556 const std::uint64_t entry_count = detail::checked_product(params.per_shard,
"shard grid");
558 detail_shard::index_encoded_size(params.index_codecs, entry_count,
"sharding_indexed");
567 std::uint64_t slot, std::size_t level = 0) {
569 throw error(
"zarr::shard::extent: level " + std::to_string(level) +
" is not a shard level");
571 const detail_shard::ShardParams params = detail_shard::params_for_level(meta, level,
"");
572 const std::uint64_t entry_count = detail::checked_product(params.per_shard,
"shard grid");
573 if (slot >= entry_count) {
574 throw error(
"zarr::shard::extent: slot " + std::to_string(slot) +
" out of range (" +
575 std::to_string(entry_count) +
" entries)");
579 const std::vector<detail_shard::IndexEntry> entries =
580 detail_shard::decode_index(pipeline, entry_count, index_bytes,
"sharding_indexed");
581 const detail_shard::IndexEntry& e = entries[
static_cast<std::size_t
>(slot)];
586 out.offset = e.offset;
587 out.nbytes = e.nbytes;
603 const std::vector<std::optional<Bytes>>& entries,
604 std::size_t level = 0) {
606 throw error(
"zarr::shard::pack: level " + std::to_string(level) +
" is not a shard level");
608 const detail_shard::ShardParams params = detail_shard::params_for_level(meta, level,
"");
609 const std::uint64_t entry_count = detail::checked_product(params.per_shard,
"shard grid");
610 if (entries.size() != entry_count) {
611 throw error(
"zarr::shard::pack: got " + std::to_string(entries.size()) +
612 " entries but shard has " + std::to_string(entry_count) +
" slots");
614 return detail_shard::assemble_shard(entries, params.index_codecs, params.index_at_end,
static CodecPipeline resolve(const ArrayMeta &meta)
Definition codecs.hpp:43
virtual bool exists(std::string_view key)=0
True if key holds a value.
virtual void flush()
Definition store.hpp:139
virtual void write(std::string_view key, Bytes value)=0
Create or replace the value at key.
virtual DirListing list_dir(std::string_view prefix)=0
Immediate children under prefix ("" or ending in '/').
virtual std::optional< Bytes > read_range(std::string_view key, ByteRange range)
Definition store.hpp:230
virtual std::vector< std::string > list_prefix(std::string_view prefix)=0
All keys starting with prefix ("" or ending in '/'), sorted.
virtual void erase(std::string_view key)=0
Remove key; removing an absent key is a no-op.
virtual std::optional< Bytes > read(std::string_view key)=0
Full value at key, or std::nullopt if the key is absent.
Definition sharding.hpp:503
Placement place(const ArrayMeta &meta, const std::string &path, const std::vector< std::uint64_t > &inner_index, std::size_t level=0)
Definition sharding.hpp:535
Extent extent(const ArrayMeta &meta, const Bytes &index_bytes, std::uint64_t slot, std::size_t level=0)
Definition sharding.hpp:566
Bytes pack(const ArrayMeta &meta, const std::vector< std::optional< Bytes > > &entries, std::size_t level=0)
Definition sharding.hpp:602
@ suffix
the final length bytes
@ slice
length bytes starting at offset
static constexpr ByteRange full()
The whole value.
Definition store.hpp:44
static constexpr DataType of(DType kind)
Definition types.hpp:131
Definition sharding.hpp:522
std::uint64_t nbytes
Encoded byte length of the chunk.
Definition sharding.hpp:526
std::uint64_t offset
Byte offset of the chunk within the shard object.
Definition sharding.hpp:524
bool missing
True if the chunk is all-fill (not stored); offset/nbytes are unset.
Definition sharding.hpp:528
Definition sharding.hpp:507
std::uint64_t slot
C-order slot of the chunk within the shard (pass to extent()).
Definition sharding.hpp:511
std::string shard_key
Store key of the owning shard object.
Definition sharding.hpp:509
bool index_at_end
Definition sharding.hpp:517
std::uint64_t index_size
Definition sharding.hpp:514
std::vector< std::uint8_t > Bytes
Owned byte buffer used throughout the value-based public API.
Definition types.hpp:42
std::string chunk_key(const std::vector< std::uint64_t > &index, char separator)
Definition v2.hpp:582