Large-object support in AOF, Migration, and diskless Replication - #2039
Large-object support in AOF, Migration, and diskless Replication#2039Ted Hart (TedHartMS) wants to merge 26 commits into
Conversation
…work, coming next)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 20c40889-7d14-432d-8853-6e880b21a396
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 20c40889-7d14-432d-8853-6e880b21a396
…the objectId wire slot Migration and replication now encode out-of-line key/value/object lengths in the on-wire objectId slot instead of writing the RDH KeyLength/ValueLength fields, so a record's inline length fields are preserved unchanged (for the future hybrid-value scheme). An object value's slot carries the chunk continuation sentinel and is patched with the exact serialized length once known; overflow key/value slots carry their exact length. The receiver reads the slot to size the overflow buffer and derives the object length from the record stream. Adds SetWireOutOfLineLengths and the Get/SetWire* helpers on LogRecord, ChunkedObjectSerializer.GetWritableSpan for in-place slot backpatching, and WireLengthEncodingTests covering the >2GB continuation boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 20c40889-7d14-432d-8853-6e880b21a396
…ue; drop objectId-slot encoding and scratch copy Replaces the objectId-slot wire-length encoding with uniform 4-byte little-endian length prefixes for overflow keys and values on both the whole-record and chunked migration/replication paths. Object values remain the record tail (length derived on read), and the RDH KeyLength/ValueLength fields are left untouched for the forthcoming hybrid-value work. DiskLogRecord.SerializeChunked now emits the inline portion directly from the record's native memory instead of allocating an ArrayPool scratch copy to reset the filler length; a stale filler is harmless because the receiver locates overflow components at RoundUp(ActualSize). Removes the now-unused wire/objectId-slot helpers from LogRecord and the obsolete WireLengthEncodingTests, and reverts the chunked reassembler, replication snapshot iterator, and chunked object serializer to the length-prefix baseline. Updates the record-layout doc accordingly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ion record layout Adds a reviewer-focused section (5.4) that enumerates every buffer allocation and byte copy on the migration/replication record send and receive paths and justifies each: when the inline portion is copied (and why the replication chunked path streams it from native memory with no scratch copy), where object-value chunks are accumulated on send and receive (and why overflow key/value are not), a copies-at-a-glance table, and the one extra copy on the migration whole-record assembly path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…n record paths The wire receiver never reads the record filler: it applies each record via a semantic Upsert that sizes the destination from the source key/value field info (GetUpsertRecordSize: 'Filler information is not needed'), and overflow is located at RoundUp(ActualSize), which is filler-independent. DirectCopyInlinePortionOfRecord rewrites the filler only for its in-memory RENAME consumer, which builds a live LogRecord over the buffer; SerializeChunked streams from the live record and skips the rewrite. - Doc: make the two inline-portion bullets consistent and correct (drop the 'locate overflow' justification for the filler reset); clarify migration capture is in-epoch via ISessionFunctions.Reader -> HandleMigrate; add table footnotes for whole-record assembly and backpressure; remove prior-implementation narration per the comment style rule. - Comments: align SerializeChunked / SerializeInlinePortion / DirectCopyInlinePortionOfRecord filler comments with the above; replace the ambiguous 'RDH-encoded' note in ReadMethods with 'compacted to RoundUp(ActualSize)'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dcc36669-1128-4a5f-ba8a-3a6c8479ccd1
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end support for transferring very large records (including large object values) across AOF, cluster migration, and diskless replication by introducing chunked record framing, streaming serialization/deserialization, and replay/rehydration support so records are no longer constrained by a single send buffer.
Changes:
- Introduces chunked framing and streaming serializers/consumers to write, send, reassemble, and replay records whose serialized size exceeds buffer/page limits (including >2GB object values via
ReadOnlySequence<byte>). - Updates AOF logging + replay coordinator/processor to persist and replay chunked logical records without materializing a contiguous image.
- Extends migration + diskless sync pipelines to emit/consume chunked records, and adds tests + documentation for record layouts and edge cases.
Reviewed changes
Copilot reviewed 59 out of 59 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| website/sidebars.js | Adds a docs sidebar category for the new chunked record layout documents. |
| website/docs/dev/aof-record-layout.md | New detailed documentation for non-chunked vs chunked AOF record layout and replay flow. |
| test/standalone/Garnet.test/TestUtils.cs | Adds AOF page/sublog configuration plumbing for tests. |
| test/cluster/Garnet.test.cluster/ClusterTestContext.cs | Threads through AOF page size option for cluster test instances. |
| test/cluster/Garnet.test.cluster.replication.disklesssync/ReplicationTests/ClusterReplicationDisklessSyncTests.cs | Adds diskless-sync tests for chunked large values and explicit huge-object coverage. |
| test/cluster/Garnet.test.cluster.migrate/ClusterMigrateTests.cs | Adds migration tests for chunked large values and explicit huge-object coverage. |
| libs/storage/Tsavorite/cs/test/ChunkedByteStreamTests.cs | Adds unit tests for value-only chunked byte-stream serialization/reassembly. |
| libs/storage/Tsavorite/cs/test/AllocateBlockPartialTests.cs | Adds unit tests for page-tail partial allocation behavior. |
| libs/storage/Tsavorite/cs/src/core/TsavoriteLog/TsavoriteLogScanSingleIterator.cs | Adjusts iterator disposal to remove the correct iterator type. |
| libs/storage/Tsavorite/cs/src/core/TsavoriteLog/TsavoriteLog.cs | Refactors/enhances log internals (partial class, allocation guards, exposes buffer pool). |
| libs/storage/Tsavorite/cs/src/core/Allocator/OverflowByteArray.cs | Adds helper allocation for direct-populated overflow buffers. |
| libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/IChunkedObjectSerializerConsumer.cs | New consumer interface for chunked serialization drains (value-only and key/input variants). |
| libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/ChunkHeader.cs | Adds a chunk header type intended for chunk framing. |
| libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/ChunkedRecordConstants.cs | Adds shared continuation-flag constant for chunk prefix framing. |
| libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/ChunkedObjectSerializer.cs | Adds bounded-ring streaming serializer that drains to chunk consumers. |
| libs/storage/Tsavorite/cs/src/core/Allocator/ConditionallyHoistedKey.cs | Makes ConditionallyHoistedKey.Create public for chunked serializer usage. |
| libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorBase.cs | Adds partial page-tail allocation support and refactors retry/backoff allocation logic. |
| libs/server/UnifiedOutput.cs | Adds migration accumulator field to carry out-of-line pieces captured in-epoch. |
| libs/server/Storage/Functions/UnifiedStore/UpsertMethods.cs | Routes object upsert logging through the new chunked AOF object path. |
| libs/server/Storage/Functions/UnifiedStore/ReadMethods.cs | Updates MIGRATE read path to capture inline/overflow/object pieces into an accumulator. |
| libs/server/Storage/Functions/UnifiedStore/PrivateMethods.cs | Switches object upsert AOF logging to stream/emit chunk records (no full materialization). |
| libs/server/Storage/Functions/ObjectStore/UpsertMethods.cs | Routes object-store upsert logging through chunked AOF object path. |
| libs/server/Storage/Functions/ObjectStore/PrivateMethods.cs | Switches object-store object upsert AOF logging to chunked streaming path. |
| libs/server/Storage/Functions/FunctionsState.cs | Makes per-session GarnetObjectSerializer lazy to support streaming safely per session. |
| libs/server/Servers/GarnetServerOptions.cs | Clarifies AOF page-size constraints relative to chunking support. |
| libs/server/Resp/Vector/VectorManager.Callbacks.cs | Updates a docstring in vector key construction logic. |
| libs/server/Objects/Types/GarnetObjectSerializer.cs | Adds ReadOnlySequence<byte> streaming deserialize support via ReadOnlySequenceStream. |
| libs/server/MigrationChunkWriterAccumulator.cs | New accumulator for migration to capture overflow/object chunks in-epoch for async sending. |
| libs/server/AOF/ReplayCoordinator/TransactionGroup.cs | Adjusts transaction buffering to store typed replay operations. |
| libs/server/AOF/ReplayCoordinator/ReplayOperation.cs | New discriminated holder for raw records vs completed chunked accumulators. |
| libs/server/AOF/ReplayCoordinator/AofReplayCoordinator.cs | Buffers/replays both chunked and non-chunked ops; updates txn + fuzzy-region handling. |
| libs/server/AOF/ReplayCoordinator/AofReplayContext.cs | Adds per-sublog chunked record reader and switches fuzzy buffer to typed operations. |
| libs/server/AOF/Recover/AofRecover.cs | Minor comment update in recovery loop. |
| libs/server/AOF/GarnetLog.cs | Adds chunkability logic; writes large records as chunk records; adds object chunked enqueue. |
| libs/server/AOF/AofProcessor.cs | Adds chunk-detection path and refactors shared replay preamble; routes chunk records by keyHash. |
| libs/server/AOF/AofProcessor.ChunkReplay.cs | New chunked replay path that dispatches directly from ChunkedAccumulator. |
| libs/server/AOF/AofHeader.cs | Adds chunked header variants, mask updates, helpers, and embedded chunk header accessors. |
| libs/server/AOF/AofEntryType.cs | Adds helpers to indicate which ops carry chunked value/input and whether value is object-streamed. |
| libs/server/AOF/AofChunkHeader.cs | New per-chunk header for AOF chunk records (lengths/objectId/keyHash). |
| libs/server/AOF/AofChunkedRecordReader.cs | New chunked AOF record reassembler keyed by objectId that yields completed accumulators. |
| libs/common/ReadOnlySequenceStream.cs | New Stream wrapper to read a multi-segment ReadOnlySequence<byte> without flattening. |
| libs/common/ReadOnlySequenceBuilder.cs | New helper to build a ReadOnlySequence<byte> over chunk byte[] segments without copying. |
| libs/common/NetworkBufferSettings.cs | Standardizes send-buffer payload sizing with explicit overhead reserve and exposes max payload size. |
| libs/cluster/Session/RespClusterReplicationCommands.cs | Adds receiver-side handling of chunked incoming records during replication sync. |
| libs/cluster/Session/RespClusterMigrateCommands.cs | Adds receiver-side handling of chunked incoming records during migration. |
| libs/cluster/Session/ChunkedRecordReassembler.cs | New state machine to reassemble chunked network records into inline + overflow/object pieces. |
| libs/cluster/Server/Replication/ReplicaOps/AOFReplay/ReplicaReplaySession.cs | Fixes method name typo for background replay initialization. |
| libs/cluster/Server/Replication/ReplicaOps/AOFReplay/ReplicaReplayDriver.cs | Renames method to InitializeBackgroundReplayTask. |
| libs/cluster/Server/Replication/PrimaryOps/DisklessReplication/ReplicationSnapshotIterator.cs | Streams snapshot records as whole or chunked, with shared chunker fan-out to replicas. |
| libs/cluster/Server/Replication/PrimaryOps/DisklessReplication/ReplicaSyncSession.cs | Adds TryWriteChunkedRecordSpan for chunked streaming to replica. |
| libs/cluster/Server/Replication/PrimaryOps/AofOperations/AofSyncTask.cs | Improves comment and corrects log method name usage. |
| libs/cluster/Server/Replication/PrimaryOps/AofOperations/AofSyncDriver.cs | Exposes chunked record write API from the driver. |
| libs/cluster/Server/Migration/MigrateSessionCommonUtils.cs | Assembles/sends accumulated overflow/object pieces and supports chunked record emission. |
| libs/cluster/Server/Migration/MigrateOperation.cs | Removes outdated TODOs; uses max send buffer content size consistently. |
| libs/client/ClientSession/GarnetClientSessionIncremental.cs | Adds ChunkedLogRecord span type and a chunked write API with continuation framing. |
|
You might want to add an end-to-end test for multilog/multi-replay test. I see you are testing the recover path but not the AOF shipping when a primary is connected to a replica. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 58 out of 58 changed files in this pull request and generated 4 comments.
Suppressed comments (2)
libs/server/AOF/GarnetLog.cs:938
- If serialization or AOF allocation throws, this explicit
Disposeis skipped. For a non-pinned key larger than the inline capacity,ConditionallyHoistedKey.CreaterentsSectorAlignedMemory, so repeated failures leak buffer-pool entries. Dispose the key in afinallyblock.
var chKey = ConditionallyHoistedKey.Create(key, log.BufferPool);
log.EnqueueChunkedObject(header, AofShardedChunkHeader.ObjectIdOffset, in chKey, ref input, objectSerializer, value, ChunkBufferSize(log, value), writeInput: opType.HasChunkInput(), epochAccessor, out logicalAddress);
chKey.Dispose();
libs/server/AOF/GarnetLog.cs:914
- If serialization or AOF allocation throws, this explicit
Disposeis skipped. For a non-pinned key larger than the inline capacity,ConditionallyHoistedKey.CreaterentsSectorAlignedMemory, so repeated failures leak buffer-pool entries. Dispose the key in afinallyblock.
This issue also appears on line 936 of the same file.
var chKey = ConditionallyHoistedKey.Create(key, log.BufferPool);
log.EnqueueChunkedObject(header, AofBasicChunkHeader.ObjectIdOffset, in chKey, ref input, objectSerializer, value, ChunkBufferSize(log, value), writeInput: opType.HasChunkInput(), epochAccessor, out logicalAddress);
chKey.Dispose();
…check, drain guard - GarnetLog.EnqueueObjectChunked now applies the per-key replication backpressure wait (via a hash-based BackpressureWaitKeyHash overload) before selecting/writing the sublog, matching the span Enqueue overloads so a large-object upsert cannot advance its sublog past the shipped watermark. - Bump AofHeaderVersion 4 -> 5: the chunked format repurposed the 0b0100 flags bit into AofHeaderTypeMask (0b0111), so a down-level v4 reader would misparse a chunk header as its non-chunked base type. The bump makes down-level recovery fail safe. - AofChunkedRecordReader rejects a segment length that exceeds the entry's remaining chunk region before AppendChunk dereferences the pointer (prevents an OOB read on a truncated/corrupt AOF). - ChunkedObjectSerializer.FlushFinal adds a no-progress guard: a non-empty ring the consumer did not shrink now throws instead of spinning forever (mirrors the full-buffer guard in Write). Adds a regression test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dcc36669-1128-4a5f-ba8a-3a6c8479ccd1
Enable the chunked large-object diskless-replication tests (ClusterDisklessSyncLargeValuesChunked and the Explicit ClusterDisklessSyncHugeObjectChunked) in the multiLog diskless runner so the sharded-log (sublogCount=2) path is exercised, matching the single/sharded coverage. Add ClusterMigrateLargeObjectMultiLogAofReplay: migrates a large hash into an AOF-enabled, sublogCount=2 target, then restarts the target with recovery to verify the migrated object is written to the multiLog AOF via the sharded chunked-object path (AofShardedChunkHeader) and reconstructed on replay. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dcc36669-1128-4a5f-ba8a-3a6c8479ccd1
The v5 AOF header bump only repurposed a reserved flags bit and added chunked header types; non-chunked v4 records are byte-identical to v5. Add a test that writes real records, rewrites each entry's version byte 5 -> 4 on disk to simulate a v4 AOF, and verifies the current build recovers it across the string, object, expire, and delete replay paths. Also add a negative control asserting an unsupported newer version is rejected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: dcc36669-1128-4a5f-ba8a-3a6c8479ccd1
This pull request introduces support for chunked migration and replication of large records that do not fit into a single send buffer. It adds a new chunked record type, logic for splitting and reassembling records, and updates the migration and replication code paths to handle records of arbitrary size, including those with object values exceeding buffer limits. The implementation ensures efficient and reliable transfer of large records during cluster migration and diskless replication.
Chunked Record Support
ChunkedLogRecordtype toMigrationRecordSpanTypeto represent records split across multiple send buffers.TryWriteChunkedRecordSpaninGarnetClientSessionto write record chunks with continuation flags, handling buffer flush and retry logic.MigrateSessionCommonUtilsto assemble and send records as either a whole or in chunked form, with length prefixes for overflow segments.Migration and Replication Pipeline Updates
AofSyncDriver,AofSyncTask,ReplicaSyncSession, andReplicationSnapshotIterator) to support chunked record writing and to expose the new chunked write API. [1] [2] [3] [4]Buffer Size and Overhead Handling
Object Serialization Integration
Code Comments and Cleanup
These changes together enable robust migration and replication of very large records, improving the reliability and scalability of cluster operations.