feat(msgpack): add ASAPv1 self-describing envelope to wire format#63
feat(msgpack): add ASAPv1 self-describing envelope to wire format#63GordonYuanyc wants to merge 11 commits into
Conversation
|
One thought - a 2 digit hex magic ID restricts us to 256 IDs. While this seems reasonable for the short term, it's not out of the question that the number of sketches may go beyond that. We can make it 3 digits, or if we want to ensure complete future proofness (at the cost of some complexity), the magic ID can be prepended by a number that denotes the number of digits in the magic ID. Are there any references in other frameworks as to how this is handled? |
|
I agree that a single-byte sketch ID gives us an unnecessary 256-ID ceiling. Prometheus and OTAP both use lightweight metadata before the raw binary payload so the receiver can interpret the following bytes from the bytes themselves. Prometheus stores a chunk encoding before each chunk payload; OTAP wraps raw Arrow IPC bytes with schema_id/type metadata. For us, a thin wrapper around the existing MessagePack payload should be enough:
|
|
With this wrapper, the serialized bytes are self-describing enough for the deserializer to determine which sketch decoder to use. |
|
Sounds good. This encoding scheme makes sense. |
|
This might also need changes to https://github.com/ProjectASAP/asap_sketchlib/blob/main/proto/sketchlib.proto? |
|
Protobuf is intentionally left untouched. protobuf's |
…mpls This is a proof-of-concept / starting point for making serialized sketch binaries self-describing. Every `to_msgpack()` output now starts with a single type-discriminant byte (analogous to Prometheus magic bytes for gauge/histogram), and `from_msgpack()` validates that byte before deserialising the payload. Magic-ID table (stable, never reuse a value): 0x01 HllSketch 0x02 CountMinSketch 0x03 CountMinSketchWithHeap 0x04 CountSketch 0x05 DdSketch 0x06 KllSketch 0x07 HydraKllSketch 0x08 SetAggregator 0x09 DeltaResult Wire format after this change: [ magic_id: u8 | <rmp_serde msgpack payload> ] API is unchanged: callers still call `to_msgpack()` / `from_msgpack()`. Round-trips still pass. The `BadMagicId` error variant surfaces mismatches at decode time instead of producing a confusing type error. All 426 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…es on all sketch types All native sketch serialization methods now also carry a magic-ID prefix (range 0x81–0x89, separate from the portable 0x01–0x09 range): 0x81 CountMin<_, _> (native map-format) 0x82 Count<_, _, _> (native map-format) 0x83 CountL2HH / CMSHeap (native map-format) 0x84 HyperLogLogImpl<_,_,_> 0x85 HyperLogLogHIPImpl<_> 0x86 DDSketch (native map-format) 0x87 KLL<T> 0x88 KLLDynamic<T> 0x89 KMV The portable KllSketch and HydraKllSketch wire formats embed KLL bytes via KLL::serialize_to_bytes / deserialize_from_bytes; those call sites pick up the magic byte automatically and the portable round-trips continue to work. The native MessagePackCodec shims in message_pack_format/native/ delegate to these methods, so they too get the magic byte without any further changes. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- cargo fmt --all to fix formatting diffs caught by CI - Add docs/msgpack-magic-ids.md documenting the full magic-ID table, the portable vs native distinction, the embedded-KLL relationship, and instructions for adding future sketch types Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Completes the magic-ID rollout across all serialize_to_bytes / deserialize_from_bytes call sites in the codebase. The EH files (eh.rs, eh_sketch_list.rs, eh_univ_optimized.rs) have no serialization methods and need no changes. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The native header grows from 1 byte to 2 bytes: [ family+mode byte | hasher byte | <rmp_serde named payload> ] Key changes: serialize_to_bytes / deserialize_from_bytes are now in Mode-specialized impl blocks instead of a single generic impl, so the first byte encodes both the sketch family and the phantom-type parameter: CountMin<_, RegularPath, _> → 0x81 CountMin<_, FastPath, _> → 0x82 Count<_, RegularPath, _> → 0x83 Count<_, FastPath, _> → 0x84 HyperLogLogImpl<Classic, _, _> → 0x86 HyperLogLogImpl<ErtlMLE, _, _> → 0x87 A new hasher_magic_id() method on SketchHasher (default = 0xff = HASHER_UNKNOWN) fills the second byte. DefaultXxHasher returns 0x01 (HASHER_DEFAULT_XX). check_hasher_id<H>(stored) skips the check when either side is 0xff, so custom hashers interoperate without registering. Types without an H parameter (DDSketch, KLL, KLLDynamic, KMV, Hydra, UnivMon, HyperLogLogHIPImpl) always write 0xff or HASHER_DEFAULT_XX as the second byte for a consistent 2-byte header across all native blobs. The native MessagePackCodec shims in message_pack_format/native/ are updated to match the specialized impl split. Docs updated: docs/msgpack-magic-ids.md now shows the 2-byte native header layout and the full updated ID table. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Closes the coverage gap: the earlier split-impl work only had RegularPath
round-trip tests. New tests:
count_min_fast_path_round_trip_serialization — verifies magic bytes
0x82 / HASHER_DEFAULT_XX and data survives serialize→deserialize
count_min_mode_mismatch_is_rejected — FastPath bytes →
RegularPath decoder must error
count_sketch_fast_path_round_trip_serialization — same for Count
count_sketch_mode_mismatch_is_rejected
hll_magic_bytes_are_variant_specific — Classic=0x86, ErtlMLE=0x87
hll_variant_mismatch_is_rejected — ErtlMLE bytes → Classic
decoder must error
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Implements the wire format proposed in PR #63 review thread. Every serialized sketch binary is now wrapped in the ASK1 envelope: [ b"ASK1" | version:u8 | kind_id_len:u8 | kind_id:bytes | msgpack_payload ] The `kind_id` replaces the old bare magic-byte header: - Portable (cross-language): 1-byte kind_id — same ID constants as before - Native Rust: 2-byte kind_id — [type_discriminant, hasher_id] The ASK1 framing lifts the 256-sketch-type ceiling while keeping the same self-describing semantics. The 4-byte sentinel (b"ASK1") is unambiguous since no msgpack value starts with 0x41. All three original constraints hold: - API unchanged (same serialize_to_bytes/from_msgpack signatures) - Round trips verified (432 tests, all pass) - Different sketch types/modes produce different kind_ids Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
803bc22 to
add3935
Compare
- kmv.rs: encode H::hasher_magic_id() instead of HASHER_UNKNOWN; verify
stored hasher on deserialize via check_hasher_id::<H>
- hll.rs: validate stored hasher byte in HyperLogLogHIPImpl::deserialize_from_bytes
(accept HASHER_DEFAULT_XX or HASHER_UNKNOWN, reject anything else)
- portable/*/from_msgpack: propagate decode_wrapper structural failures as
Error::Decode rather than silently swallowing them into a misleading
BadMagicId { got: bytes[0] } (bytes[0] is always 0x41 'A' for ASK1 input)
- docs/msgpack-magic-ids.md: fix copy-paste error — KLL inner bytes carry
prefix 0x8a (NATIVE_KLL), not 0x87 (NATIVE_HLL_ERTL_MLE)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Auto-fixed uninlined format args (clippy::uninlined_format_args) and applied cargo fmt across the tree. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
Is there a description of what the sketch payload is, for each sketch family? |
Not yet. The payload that can work for cross-language ser/de needs modification. On the other hand, something that works currently is under the portable subdirectory. |
|
The PR is marked as draft, instead of close for one reason: it's easier to find reference for possible metadata and envelope design in PR description. That means, for self-describing, cross-language ser/de, this PR will be broken into several small PRs, where each PR will contain one sketch algorithm. Portable and payload will be redesigned and will gradually be unified into one place. |
|
Sure. I would say though, it is better to put the envelope and metadata description in a design doc markdown file and add that to the repo. Similary, for the serialization formats for each sketch family (whether it supports cross-language serde or not) |
Overview
Adds the ASAPv1 envelope to every serialized sketch binary, making the wire
format self-describing and carrying the hash metadata needed by out-of-process
consumers.
b"ASAPv1"— 6-byte ASCII sentinel, encoded as0x41 0x53 0x41 0x50 0x76 0x31version— envelope layout version; currently0x02kind_id_len + kind_id— variable-length sketch discriminantmetadata_len + metadata— compact MessagePack metadata blockThe sketch payload structure is unchanged; metadata is wrapper-level.
Metadata
Current metadata is a compact MessagePack array in this field order:
For the standard ProjectASAP hash profile:
metadata_version = 1hash_profile_id = "projectasap.xxh3.seedlist.v1"hash_algorithm = "xxh3_64_128"seed_list = [0xcafe3553, 0xade3415118, 0x8cc70208, 0x2f024b2b, 0x451a3df5, 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, 0xcbbb9d5d, 0x629a292a, 0x9159015a, 0x152fecd8, 0x67332667, 0x8eb44a87, 0xdb0c2e0d]canonical_seed_index = 5matrix_seed_index = 0hydra_seed_index = 6univmon_bottom_layer_seed_index = 19seed_derivation = "seed_list_index_wrap"input_encoding = "projectasap.input.v1"Decoders validate both the metadata contents and that the metadata matches the
decoded
kind_id.Breaking change
Bytes produced before this PR (bare msgpack, or earlier draft ASAPv1 wrapper
without metadata) will be rejected. No backward-compatibility shim; callers must
re-serialize stored blobs.
Verification
cargo test msgpackcargo testGo mirror
sketchlib-goPR #68 is updated in lock-step.