libzarr
Header-only C++17 Zarr v2/v3, WASM-compatible
Loading...
Searching...
No Matches
sharding.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2
3#ifndef LIBZARR_SHARDING_HPP
4#define LIBZARR_SHARDING_HPP
5
6#include <cstdint>
7#include <limits>
8#include <memory>
9#include <optional>
10#include <string>
11#include <string_view>
12#include <utility>
13#include <vector>
14
15#include "libzarr/codecs.hpp"
16#include "libzarr/detail/common.hpp"
17#include "libzarr/metadata.hpp"
18#include "libzarr/store.hpp"
19#include "libzarr/types.hpp"
20#include "libzarr/v2.hpp"
21#include "libzarr/v3.hpp"
22
29
30namespace zarr::detail_shard {
31
32inline constexpr std::uint64_t kSentinel = std::numeric_limits<std::uint64_t>::max();
33
34struct IndexEntry {
35 std::uint64_t offset = kSentinel;
36 std::uint64_t nbytes = kSentinel;
37 [[nodiscard]] bool missing() const { return offset == kSentinel && nbytes == kSentinel; }
38};
39
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") {
49 have_bytes = true;
50 } else if (codec.name == "crc32c") {
51 size += 4;
52 } else {
53 throw error(ctx +
54 ": index_codecs may only contain 'bytes' and 'crc32c' (the index must "
55 "have a fixed encoded size), got '" +
56 codec.name + "'");
57 }
58 }
59 if (!have_bytes) {
60 throw error(ctx + ": index_codecs must contain the 'bytes' codec");
61 }
62 return size;
63}
64
66struct ShardParams {
69 std::string chunk_prefix;
71 ChunkKeyKind key_encoding = ChunkKeyKind::v3_default;
73 char separator = '/';
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;
82};
83
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
92 : meta.chunk_shape;
93 ShardParams params;
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]);
104 }
105 return params;
106}
107
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);
114 slot = 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];
118 }
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));
122}
123
125inline ArrayMeta index_array_meta(std::uint64_t entry_count,
126 const std::vector<CodecSpec>& index_codecs) {
127 ArrayMeta meta;
128 meta.shape = {entry_count * 2};
129 meta.chunk_shape = {entry_count * 2};
130 meta.dtype = DataType::of(DType::uint64);
131 meta.codecs = index_codecs;
132 return meta;
133}
134
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");
144 }
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");
155 }
156 }
157 return entries;
158}
159
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) {
168 bool any = false;
169 for (const auto& entry : entries) {
170 any = any || entry.has_value();
171 }
172 if (!any) {
173 return {}; // all-fill shard: nothing to store
174 }
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 =
178 CodecPipeline::resolve(index_array_meta(entry_count, index_codecs));
179
180 Bytes body;
181 std::vector<std::uint64_t> raw_index(entries.size() * 2, kSentinel);
182 const std::uint64_t base = index_at_end ? 0 : index_size; // chunks follow a leading index
183 for (std::size_t i = 0; i < entries.size(); ++i) {
184 const std::optional<Bytes>& entry = entries[i];
185 if (!entry) {
186 continue;
187 }
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());
191 }
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));
195
196 Bytes shard;
197 shard.reserve(body.size() + index_bytes.size());
198 if (index_at_end) {
199 shard = std::move(body);
200 shard.insert(shard.end(), index_bytes.begin(), index_bytes.end());
201 } else {
202 shard = std::move(index_bytes);
203 shard.insert(shard.end(), body.begin(), body.end());
204 }
205 return shard;
206}
207
216class ShardStore final : public Store {
217 public:
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())) {
226 if (!source_) {
227 throw error("ShardStore: null store");
228 }
229 }
230
231 ~ShardStore() override = default;
232 ShardStore(const ShardStore&) = delete;
233 ShardStore& operator=(const ShardStore&) = delete;
234 ShardStore(ShardStore&&) = delete;
235 ShardStore& operator=(ShardStore&&) = delete;
236
237 [[nodiscard]] std::optional<Bytes> read(std::string_view key) override {
238 return read_range(key, ByteRange::full());
239 }
240
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) {
244 // Serve pending writes so read-modify-write sequences stay coherent.
245 const auto& entry = assembly_->entries[loc.intra];
246 if (!entry) {
247 return std::nullopt;
248 }
249 return slice_value(*entry, range, key);
250 }
251 const auto* index = load_index(loc.shard_key);
252 if (index == nullptr || (*index)[loc.intra].missing()) {
253 return std::nullopt;
254 }
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));
260 }
261
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];
266 if (!entry) {
267 return std::nullopt;
268 }
269 return entry->size();
270 }
271 const auto* index = load_index(loc.shard_key);
272 if (index == nullptr || (*index)[loc.intra].missing()) {
273 return std::nullopt;
274 }
275 return (*index)[loc.intra].nbytes;
276 }
277
278 [[nodiscard]] bool exists(std::string_view key) override { return size(key).has_value(); }
279
280 void write(std::string_view key, Bytes value) override {
281 put(locate(key), std::optional<Bytes>(std::move(value)));
282 }
283
284 void erase(std::string_view key) override { put(locate(key), std::nullopt); }
285
288 void flush() override {
289 flush_assembly();
290 source_->flush();
291 }
292
293 [[nodiscard]] std::vector<std::string> list_prefix(std::string_view /*prefix*/) override {
294 throw error("ShardStore is an internal adapter; listing is not supported");
295 }
296 [[nodiscard]] DirListing list_dir(std::string_view /*prefix*/) override {
297 throw error("ShardStore is an internal adapter; listing is not supported");
298 }
299
300 private:
301 struct Location {
302 std::string shard_key;
303 std::size_t intra = 0; // C-order position within the shard
304 };
305
306 struct Assembly {
307 std::string shard_key;
308 std::vector<std::optional<Bytes>> entries;
309 bool dirty = false;
310 };
311
313 [[nodiscard]] ArrayMeta index_meta() const {
314 return index_array_meta(entry_count_, params_.index_codecs);
315 }
316
317 static void resolve_range(ByteRange range, std::uint64_t size, std::uint64_t& begin,
318 std::uint64_t& count, std::string_view key) {
319 if (range.kind == ByteRange::Kind::slice) {
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)");
323 }
324 begin = range.offset;
325 count = range.length;
326 } else if (range.kind == ByteRange::Kind::suffix) {
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)");
330 }
331 begin = size - range.length;
332 count = range.length;
333 }
334 }
335
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));
343 }
344
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");
352 }
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) + "'");
357 }
358 rest = rest.size() > 1 ? rest.substr(2) : rest.substr(1);
359 }
360 std::vector<std::uint64_t> index(rank, 0);
361 std::size_t pos = 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');
367 ++pos;
368 }
369 if (pos == start || value >= params_.inner_grid[d]) {
370 throw error("ShardStore: malformed chunk key '" + std::string(key) + "'");
371 }
372 index[d] = value;
373 if (d + 1 < rank) {
374 if (pos >= rest.size() || rest[pos] != params_.separator) {
375 throw error("ShardStore: malformed chunk key '" + std::string(key) + "'");
376 }
377 ++pos;
378 }
379 }
380 if (pos != rest.size()) {
381 throw error("ShardStore: malformed chunk key '" + std::string(key) + "'");
382 }
383
384 Location loc;
385 std::uint64_t slot = 0;
386 locate_index(params_, index, loc.shard_key, slot);
387 loc.intra = detail::checked_size(slot, "shard entry");
388 return loc;
389 }
390
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) {
397 if (i != 0) {
398 std::rotate(cache_.begin(), cache_.begin() + static_cast<std::ptrdiff_t>(i),
399 cache_.begin() + static_cast<std::ptrdiff_t>(i) + 1);
400 }
401 return &cache_.front().second;
402 }
403 }
404 auto stored =
405 source_->read_range(shard_key, params_.index_at_end ? ByteRange::suffix(index_size_)
406 : ByteRange::slice(0, index_size_));
407 if (!stored) {
408 return nullptr;
409 }
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) {
414 cache_.pop_back();
415 }
416 return &cache_.front().second;
417 }
418
419 void put(const Location& loc, std::optional<Bytes> value) {
420 if (assembly_ && assembly_->shard_key != loc.shard_key) {
421 flush_assembly(); // early flush: writes moved on to another shard
422 }
423 if (!assembly_) {
424 assembly_ = load_assembly(loc.shard_key);
425 }
426 assembly_->entries[loc.intra] = std::move(value);
427 assembly_->dirty = true;
428 }
429
432 [[nodiscard]] Assembly load_assembly(const std::string& shard_key) {
433 Assembly assembly;
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);
437 if (stored) {
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()) {
443 continue;
444 }
445 if (entry.offset > stored->size() || entry.nbytes > stored->size() - entry.offset) {
446 throw error(shard_key + ": shard index points outside the shard");
447 }
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));
450 }
451 }
452 return assembly;
453 }
454
455 void flush_assembly() {
456 if (!assembly_) {
457 return;
458 }
459 const Assembly assembly = *std::move(assembly_);
460 assembly_.reset();
461 if (!assembly.dirty) {
462 return;
463 }
464 drop_cached(assembly.shard_key);
465 Bytes shard = assemble_shard(assembly.entries, params_.index_codecs, params_.index_at_end,
466 assembly.shard_key);
467 if (shard.empty()) {
468 source_->erase(assembly.shard_key); // all-fill shards are not stored
469 } else {
470 source_->write(assembly.shard_key, std::move(shard));
471 }
472 }
473
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));
478 return;
479 }
480 }
481 }
482
483 static constexpr std::size_t kCacheCapacity = 16;
484
485 std::shared_ptr<Store> source_;
486 ShardParams params_;
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_;
492};
493
494} // namespace zarr::detail_shard
495
503namespace zarr::shard {
504
507struct Placement {
509 std::string shard_key;
511 std::uint64_t slot = 0;
514 std::uint64_t index_size = 0;
517 bool index_at_end = true;
518};
519
522struct Extent {
524 std::uint64_t offset = 0;
526 std::uint64_t nbytes = 0;
528 bool missing = false;
529};
530
535[[nodiscard]] inline Placement place(const ArrayMeta& meta, const std::string& path,
536 const std::vector<std::uint64_t>& inner_index,
537 std::size_t level = 0) {
538 if (level >= meta.shard_levels.size()) {
539 throw error("zarr::shard::place: level " + std::to_string(level) + " is not a shard level (" +
540 std::to_string(meta.shard_levels.size()) + " levels)");
541 }
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()));
547 }
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");
552 }
553 }
554 Placement out;
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");
557 out.index_size =
558 detail_shard::index_encoded_size(params.index_codecs, entry_count, "sharding_indexed");
559 out.index_at_end = params.index_at_end;
560 return out;
561}
562
566[[nodiscard]] inline Extent extent(const ArrayMeta& meta, const Bytes& index_bytes,
567 std::uint64_t slot, std::size_t level = 0) {
568 if (level >= meta.shard_levels.size()) {
569 throw error("zarr::shard::extent: level " + std::to_string(level) + " is not a shard level");
570 }
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)");
576 }
577 const CodecPipeline pipeline =
578 CodecPipeline::resolve(detail_shard::index_array_meta(entry_count, params.index_codecs));
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)];
582 Extent out;
583 if (e.missing()) {
584 out.missing = true;
585 } else {
586 out.offset = e.offset;
587 out.nbytes = e.nbytes;
588 }
589 return out;
590}
591
602[[nodiscard]] inline Bytes pack(const ArrayMeta& meta,
603 const std::vector<std::optional<Bytes>>& entries,
604 std::size_t level = 0) {
605 if (level >= meta.shard_levels.size()) {
606 throw error("zarr::shard::pack: level " + std::to_string(level) + " is not a shard level");
607 }
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");
613 }
614 return detail_shard::assemble_shard(entries, params.index_codecs, params.index_at_end,
615 "sharding_indexed");
616}
617
618} // namespace zarr::shard
619
620#endif // LIBZARR_SHARDING_HPP
Definition codecs.hpp:37
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 types.hpp:36
ChunkKeyKind
Definition metadata.hpp:78
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
Definition metadata.hpp:98
std::vector< std::uint64_t > shape
Array shape; empty = 0-dimensional.
Definition metadata.hpp:102
std::vector< ShardLevel > shard_levels
Sharding levels (empty = unsharded); see ShardLevel.
Definition metadata.hpp:121
@ 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