{"record":{"id":"3e9ee602ed1863ec","repo":"microsoft/garnet","slug":"readbuffers-are-required-to-readrecordobjects","errorCode":null,"errorMessage":"ReadBuffers are required to ReadRecordObjects","messagePattern":"ReadBuffers are required to ReadRecordObjects","errorType":"exception","errorClass":"TsavoriteException","httpStatus":null,"severity":"error","filePath":"libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/ObjectLogReader.cs","lineNumber":98,"sourceCode":"        ///     If we don't have <paramref name=\"requestedKey\"/>, this is either ReadAtAddress (which is an implicit match) or Scan or Restore.</item>\n        /// <item>If we have an Overflow or Object value, read and store it in the transient <see cref=\"ObjectIdMap\"/> in <paramref name=\"logRecord\"/>.</item>\n        /// </list>\n        /// </summary>\n        /// <param name=\"logRecord\">The initial record read from disk from Pending IO, so it is of size <see cref=\"IStreamBuffer.DefaultInitialIORecordSize\"/> or less.</param>\n        /// <param name=\"requestedKey\">The requested key, if not ReadAtAddress; we will compare to see if it matches the record.</param>\n        /// <param name=\"segmentSizeBits\">Number of bits in segment size</param>\n        /// <returns>False if requestedKey is set and we read an Overflow key and it did not match; otherwise true</returns>\n        [MethodImpl(MethodImplOptions.NoInlining)]\n        public bool ReadRecordObjects<TKey>(ref LogRecord logRecord, TKey requestedKey, int segmentSizeBits)\n            where TKey : IKey\n#if NET9_0_OR_GREATER\n                , allows ref struct\n#endif\n        {\n            Debug.Assert(logRecord.DataHeader.RecordHasObjects, \"Inline records should have been checked by the caller\");\n            Debug.Assert(logRecord.HasReuseObjectIdForSize, \"ReadRecordObjects requires the ReuseObjectIdForSize flag to be set on the ObjectLogPosition\");\n            if (readBuffers is null)\n                throw new TsavoriteException(\"ReadBuffers are required to ReadRecordObjects\");\n\n            // R11 encoding: lengths returned by GetObjectLogRecordStartPositionAndLengths combine the RDH low bits with the next 32 bits\n            // from the int* slot at keyAddress/valueAddress. No length prefix is in the object stream.\n            var positionWord = logRecord.GetObjectLogRecordStartPositionAndLengths(out var keyLength, out var valueLength);\n            if (!readBuffers.OnBeginRecord(new ObjectLogFilePositionInfo(positionWord, segmentSizeBits)))\n                throw new TsavoriteException(\"ReadRecordObjects found no data available in ReadBuffers\");\n\n            // TODO: Optimize the reading of large internal sector-aligned parts of Overflow Keys and Values to read directly into the overflow, similar to how ObjectLogWriter writes\n            //       directly from overflow. This requires changing the read-ahead in CircularDiskReadBuffer.OnBeginReadRecords and the \"backfill\" in CircularDiskReadBuffer.MoveToNextBuffer.\n\n            // Note: Similar logic to this is in DiskLogRecord.Deserialize.\n            var keyWasSet = false;\n            try\n            {\n                if (logRecord.DataHeader.KeyIsOverflow)\n                {\n                    // This assignment also allocates the slot in ObjectIdMap, overwriting the int* slot at keyAddress\n                    // (which held the high 32 bits of the on-disk key length per R11). The raw RDH KeyLength is restored","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/microsoft/garnet/blob/951b0fc6838721f89d102c2bbe1b914e8d39d700/libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/ObjectLogReader.cs#L80-L116","documentation":"Thrown by ObjectLogReader.ReadRecordObjects when readBuffers is null. ReadRecordObjects reads overflow keys/values and object values from the object log, which requires a populated read-buffer set (CircularDiskReadBuffer). A null readBuffers means the reader was constructed/entered for a code path that doesn't supply object-log read buffers, yet the record claims to have objects.","triggerScenarios":"Invoking ReadRecordObjects on an ObjectLogReader/DiskStreamReadBuffer whose readBuffers field was never set — e.g. a single-record disk IO path or a configuration where NumberOfDeserializationBuffers-related buffers weren't allocated, but the record has RecordHasObjects.","commonSituations":"A read path that was set up for inline-only records but received a record with objects; a refactor that dropped buffer initialization; misconfigured object store where the object-log read buffer is absent.","solutions":["Ensure the object-log read path always allocates readBuffers (configure NumberOfDeserializationBuffers correctly for the object allocator).","Only call ReadRecordObjects on readers initialized with a read buffer; for inline-only paths, skip object reading.","Check logRecord.DataHeader.RecordHasObjects before invoking the object-read path."],"exampleFix":"// before\nreader.ReadRecordObjects(ref logRecord, requestedKey, segmentSizeBits); // readBuffers null\n// after\nif (logRecord.DataHeader.RecordHasObjects && reader.HasReadBuffers)\n    reader.ReadRecordObjects(ref logRecord, requestedKey, segmentSizeBits);","handlingStrategy":"validation","validationCode":"if (logRecord.DataHeader.RecordHasObjects && !reader.HasReadBuffers)\n    throw new InvalidOperationException(\"Record has objects but no read buffers are configured\");","typeGuard":"static bool CanReadObjects(ObjectLogReader r, LogRecord rec) => rec.DataHeader.RecordHasObjects && r.HasReadBuffers;","tryCatchPattern":null,"preventionTips":["Always configure NumberOfDeserializationBuffers for object stores.","Check RecordHasObjects before entering the object-read path."],"tags":["object-log","read-path","buffer","configuration"],"backgroundTag":null,"analyzedSha":"951b0fc6838721f89d102c2bbe1b914e8d39d700","analyzedAt":"2026-08-13T19:01:32.939Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}