Storage Serialization#
Overview#
HugeGraph uses a custom binary encoding layer (BytesBuffer) as the foundation for all persistent data written to storage backends (RocksDB, HBase, Cassandra, HStore). BytesBuffer handles vertex/edge IDs, property values, index keys, and pagination state. For a small set of non-primitive property types not covered by first-class encodings, Kryo is used as a fallback, embedded inline within BytesBuffer payloads.
BytesBuffer#
Two independent copies exist:
| Module | Class | Source |
|---|---|---|
hugegraph-server/hugegraph-core | org.apache.hugegraph.backend.serializer.BytesBuffer | BytesBuffer.java (server/core) |
hugegraph-struct | org.apache.hugegraph.serializer.BytesBuffer | BytesBuffer.java (struct) |
The struct copy was extracted from the core copy in 2025-09; both have diverged since. Key constant differences :
BLOB_LEN: 5 (core) vs. 4 (struct)ID_LEN_MAX: core declares and enforces 16384; struct declares 32768 but itswriteIdenforces 16384
Wire/Storage Encoding#
BytesBuffer extends OutputStream and wraps java.nio.ByteBuffer with auto-resize . Max buffer capacity is 128 MB .
Key encoding techniques:
- Variable-length integers:
writeVInt/readVInt(1–5 bytes) andwriteVLong/readVLong(1–10 bytes) . Negative numbers are not compressed. - Compact number IDs: Long IDs use a 2–9 byte format with a
0kkksxxxleading byte encoding sign and magnitude . Reserved prefixes:0x7f= UUID,0x7e= EdgeId. - String IDs: Length-prefixed with 1–2 byte header; 1–63 byte strings use single-byte header (
0x80 | len); up to 16384 bytes use two-byte header (0xc0high byte) . - String boundaries: Null-terminated (
0x00) in string index IDs;0x00is disallowed inside index strings . - Edge IDs: Composite encoding of owner vertex + direction + edge label + sub-label + sort values + other vertex .
- Properties:
writeProperty(PropertyKey, Object)dispatches onDataType; BOOLEAN/BYTE/INT use VInt, LONG/DATE use VLong, FLOAT/DOUBLE are fixed-width, TEXT useswriteBytes, BLOB useswriteBigBytes, UUID is two fixed longs . - Multi-value properties: The struct copy prefixes each property entry with a
(cardinality << 6) | dataTypebyte ; the server/core copy reads cardinality from the schema, not the byte stream .
Usage Layers#
Server side (BinarySerializer): All graph entities (vertices, edges, indexes, schema) are serialized/deserialized through BinarySerializer.java, which calls BytesBuffer.allocate() and BytesBuffer.wrap() throughout. This produces the binary BackendColumn.name/BackendColumn.value byte arrays stored in every backend.
Store side (hugegraph-struct copy): The store layer reads these bytes on the query/filtered-read path. SelectIterator.select() uses the struct BytesBuffer to parse server-written column values (readId, readVInt, readProperty) and re-encode filtered results. KeyUtil.idToBytes() uses the struct copy for key construction.
Pagination: PageInfo and PageState use the server/core BytesBuffer to encode cursor state (offset + backend position bytes) into Base64 page tokens passed between client and server.
Kryo Serialization#
KryoUtil provides the Kryo integration, with one thread-local Kryo instance per thread .
toKryoWithType/fromKryoWithType: Used for non-primitive property types (thedefaultbranch inwriteProperty) viawriteClassAndObject/readClassAndObject. These bytes are embedded inside awriteBytescall in theBytesBufferproperty stream .toKryo/fromKryo: Typed serialization without class embedding; used for specific known types.- UUID custom serializer: Only registered serializer; writes two raw longs .
The Kryo version is pinned transitively through TinkerPop 3.5.1 . There is no application-level version header in the Kryo byte stream and no fallback for incompatible data on read . A TODO comment in the core BytesBuffer notes intent to replace Kryo with Apache Fury .
⚠️ Upgrade risk: A Kryo or TinkerPop upgrade should not be assumed safe for stored generic property values without testing representative persisted data.
The store layer (hugegraph-struct / hstore modules) does not use Kryo; it throws IllegalArgumentException on unknown data types instead .
Compatibility Notes#
Known Divergence Between the Two BytesBuffer Copies#
The core and struct copies share the string-ID encoding logic (same 0x80/0xc0 prefix-bit scheme) and VInt/VLong encoding . However:
- Property encoding format differs: The struct copy writes a self-describing
(cardinality << 6) | dataTypebyte before each property value; the server/core copy does not . This causes an active property codec mismatch when HStore operator-sinking decodes server-written property bytes — Issue #3090 tracks the fix. - No golden test locks the struct copy's format . Per-copy characterization fixtures are the planned deliverable.
HStore Ingest Path#
Encoding is performed exclusively server-side. HstoreTable.insert() passes BackendColumn byte arrays through unchanged over gRPC (opaque bytes data field in StoreCommandRequest). The struct BytesBuffer is used only for read-side key construction and query-path property filtering, not for writing data .
External Tools#
hugegraph-loader, Hubble, and hugegraph-tools all communicate via the REST API and have no dependency on BytesBuffer or the binary row format .
Page Tokens#
Page tokens are ephemeral cursors with no cross-version compatibility guarantee. Clients should be prepared to restart pagination from the first page after a server upgrade .
Key Files#
| File | Role |
|---|---|
| BytesBuffer.java (server/core) | Primary binary codec for server/backend layer |
| BytesBuffer.java (struct) | Store-side codec for key construction and query filtering |
| BinarySerializer.java | Orchestrates BytesBuffer to serialize all graph entities |
| KryoUtil.java | Kryo thread-local setup and encode/decode helpers |
| SelectIterator.java | Store-side property filtering using struct BytesBuffer |
| KeyUtil.java | Store-side ID→bytes conversion using struct BytesBuffer |
| PageInfo.java | Pagination cursor serialization |
| PageState.java | Backend position state within a page token |