Skip to content

Large-object support in AOF, Migration, and diskless Replication - #2039

Open
Ted Hart (TedHartMS) wants to merge 26 commits into
mainfrom
tedhar/aof-migrepl-chunk
Open

Large-object support in AOF, Migration, and diskless Replication#2039
Ted Hart (TedHartMS) wants to merge 26 commits into
mainfrom
tedhar/aof-migrepl-chunk

Conversation

@TedHartMS

Copy link
Copy Markdown
Contributor

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

  • Added a new ChunkedLogRecord type to MigrationRecordSpanType to represent records split across multiple send buffers.
  • Implemented TryWriteChunkedRecordSpan in GarnetClientSession to write record chunks with continuation flags, handling buffer flush and retry logic.
  • Added reusable buffers and logic in MigrateSessionCommonUtils to assemble and send records as either a whole or in chunked form, with length prefixes for overflow segments.

Migration and Replication Pipeline Updates

  • Updated migration methods to use the new chunked record logic, ensuring records are sent whole if they fit a buffer or chunked otherwise, including handling of overflow keys/values and large object values.
  • Updated replication code (AofSyncDriver, AofSyncTask, ReplicaSyncSession, and ReplicationSnapshotIterator) to support chunked record writing and to expose the new chunked write API. [1] [2] [3] [4]

Buffer Size and Overhead Handling

  • Standardized calculation of maximum send buffer content size to ensure correct chunking and avoid buffer overflows during migration. [1] [2]

Object Serialization Integration

  • Integrated chunked object serialization into the snapshot iterator for diskless replication, allowing seamless chunked transfer of large serialized object values. [1] [2]

Code Comments and Cleanup

These changes together enable robust migration and replication of very large records, improving the reliability and scalability of cluster operations.

Ted Hart (TedHartMS) and others added 18 commits July 8, 2026 16:40
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/ChunkHeader.cs Outdated
Comment thread libs/server/AOF/AofChunkHeader.cs Outdated
Comment thread libs/server/AOF/AofHeader.cs Outdated
Comment thread libs/server/Resp/Vector/VectorManager.Callbacks.cs
Comment thread libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorBase.cs
Comment thread libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorBase.cs
@vazois

Copy link
Copy Markdown
Contributor

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.
This will be testing the AofSyncDriver->AofSyncTask ->[network]-> ReplicaReplayDriver->ReplicaReplayTask path.

Comment thread libs/server/AOF/AofProcessor.cs
Ted Hart (TedHartMS) and others added 2 commits August 8, 2026 20:57
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Dispose is skipped. For a non-pinned key larger than the inline capacity, ConditionallyHoistedKey.Create rents SectorAlignedMemory, so repeated failures leak buffer-pool entries. Dispose the key in a finally block.
                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 Dispose is skipped. For a non-pinned key larger than the inline capacity, ConditionallyHoistedKey.Create rents SectorAlignedMemory, so repeated failures leak buffer-pool entries. Dispose the key in a finally block.

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();

Comment thread libs/server/AOF/GarnetLog.cs
Comment thread libs/server/AOF/AofHeader.cs
Comment thread libs/server/AOF/AofChunkedRecordReader.cs
Ted Hart (TedHartMS) and others added 2 commits August 11, 2026 12:00
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants