microsoft / microsoft/FASTER

Distinct-key iteration returns phantom keys for serialized SpanByte keys

Open Beginner friendly
#962 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
6.6k
Forks
595
PR merge metrics
No merged PRs in 30d

Description

Environment

  • FASTER 2.6.5
  • FasterKV<SpanByte, SpanByte>
  • Distinct-key iteration through session.Iterate()

Problem

session.Iterate() can return two distinct live keys for one logical key when:

  1. The key is a serialized SpanByte longer than the inline IntPtr payload field.
  2. The key has multiple log versions.
  3. An older version is processed through the non-tailmost-record path.

For example, after updating one logical key, iteration can produce:

Expected: 1 live key
Actual:   2 keys, including one corrupted/phantom key

Point reads using the original key still return the correct latest value.

Minimal reproduction

using var session =
    fht.For(new SpanByteFunctions<Empty>())
       .NewSession<SpanByteFunctions<Empty>>();

var key = MemoryMarshal.Cast<char, byte>(
    "key-longer-than-eight-bytes".AsSpan());

var initialValue = MemoryMarshal.Cast<char, byte>(
    "initial-value".AsSpan());

var latestValue = MemoryMarshal.Cast<char, byte>(
    "latest-value".AsSpan());

session.Upsert(key, initialValue);

// Make the first version immutable so the next update is appended.
fht.Log.ShiftReadOnlyAddress(fht.Log.TailAddress, wait: true);

session.Upsert(key, latestValue);

var count = 0;

using var iterator = session.Iterate();
while (iterator.GetNext(out _))
{
    ++count;
}

Assert.AreEqual(1, count);

On FASTER 2.6.5, the assertion fails because the iterator returns two keys.

Root cause

In cs/src/core/Index/FASTER/FASTERIterator.cs, non-tailmost keys are passed by value to this helper:

private void ProcessNonTailmostMainKvRecord(
    RecordInfo recordInfo,
    Key key)

Both pull and push iteration paths call it similarly:

ProcessNonTailmostMainKvRecord(recordInfo, key);

A serialized SpanByte is self-relative: its payload begins at the memory address of its payload field and may continue beyond the fixed-size struct.

Passing it by value copies only the fixed-size SpanByte representation, not the complete trailing serialized payload. The copy retains the original length and serialized flag, so AsReadOnlySpan() interprets unrelated memory after the copied inline bytes as part of the key.

The resulting corrupted key no longer matches the real key in the iterator's temporary reconciliation store and can survive as an additional distinct key.

Tailmost records are returned directly from the main iterator. Therefore, this specifically affects serialized keys processed through the non-tailmost path.

Proposed fix

Pass the iterator-owned key by reference at both call sites:

- ProcessNonTailmostMainKvRecord(recordInfo, key);
+ ProcessNonTailmostMainKvRecord(recordInfo, ref key);

Change the helper signature accordingly:

 private void ProcessNonTailmostMainKvRecord(
     RecordInfo recordInfo,
-    Key key)
+    ref Key key)

Using ref avoids copying the self-relative serialized SpanByte and preserves its original log-record address.

Validation

A local upstream regression test produced the following results:

  • Before the ref change: failed, expected 1 distinct live key but received 2.
  • After the ref change: passed, exactly 1 live key was returned.
  • Existing SpanByteLogScanTests: 2/2 passed.

Impact

Applications using distinct-key iteration with serialized SpanByte keys may observe:

  • Phantom or corrupted keys.
  • More than one result for a single logical key.
  • Incorrect unique-key counts.
  • Different behavior between point reads and iteration.

The underlying latest value remains readable through a normal point lookup; the problem occurs while reconciling historical records during distinct-key iteration.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in cs/src/core/Index/FASTER/FASTERIterator.cs and inspect both calls to ProcessNonTailmostMainKvRecord, along with the helper signature. Reproduce the issue with the supplied SpanByte update and distinct-key iteration example, then verify the ref-based change with the regression test and existing SpanByteLogScanTests; done means one live key is returned.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
databases
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
85/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.