Skip to content

Fix TTL compaction filter value parsing - #539

Merged
thweetkomputer merged 12 commits into
mainfrom
agent/fix-ttl-compaction-filter-offset
Aug 2, 2026
Merged

Fix TTL compaction filter value parsing#539
thweetkomputer merged 12 commits into
mainfrom
agent/fix-ttl-compaction-filter-offset

Conversation

@thweetkomputer

@thweetkomputer thweetkomputer commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

What

  • Read the TTL-presence marker from the encoded version timestamp at value offset 0.
  • Read the expiration timestamp from offset 8 only when that marker is set.
  • Use memcpy for word decoding while preserving the existing value-size invariants.
  • Share one value-header decoder between compaction filtering and normal record deserialization so the persisted layout cannot drift between paths.
  • Create a filter per compaction worker and capture the current time once instead of once per record.
  • Emit debug logs when a filter is created and when it removes an expired key, including the relevant timestamps.
  • Add focused regression coverage for expired, unexpired, and non-TTL values on EloqDSS RocksDB backends.
  • After the EloqStore unit suite, configure ELOQDSS_ROCKSDB_CLOUD_S3 and build/run the full suite in the same amd64/arm64 CI jobs.
  • Cancel superseded Tests workflow runs when a newer commit is pushed to the same PR or ref.

Why / root cause

EloqDSS RocksDB values are stored as:

[encoded version timestamp with TTL MSB][expiration timestamp][record]

TTLCompactionFilter incorrectly checked offset 8 for the TTL MSB. Offset 8 is the expiration timestamp, whose high bit is clear for ordinary epoch-millisecond values, so expired records were retained during both automatic and manual compaction.

Observable impact

Expired records can accumulate physically in RocksDB even though reads treat them as deleted. Scans must traverse and deserialize those records before filtering them, which can make sparse ranges progressively slower. After this fix is deployed, subsequent compactions can reclaim expired records; cleanup is still governed by normal compaction scheduling and backlog and is not immediate at expiration time.

Design notes

  • The change is limited to the EloqDSS RocksDB/RocksDB Cloud compaction filter.
  • Assertions document that every value has an encoded timestamp and TTL-marked values also have an expiration timestamp.
  • A RocksDB-owned CompactionFilterFactory captures system_clock epoch milliseconds when it creates each compaction filter; all records processed by that filter use the same timestamp.
  • This does not change SCAN batching or its behavior when it encounters long runs of expired records.

Checks

  • git diff --check origin/main...HEAD — passed.
  • .github/workflows/tests.yml YAML parse — passed.
  • Full CMake build and Catch2 test execution — not run because this host does not have CMake or the development dependency environment installed.

Risk and rollback

The main risk is deleting a record whose first-word TTL marker and second-word expiration are corrupt but happen to look valid. Correctly encoded non-TTL and unexpired values are retained by regression coverage. Rollback is a revert of these commits; already compacted expired records do not need restoration.

Reviewer focus

  • Confirm the value layout at TransformRecordToValueSlices and DeserializeValueToRecord matches offsets 0 and 8 used here.
  • Confirm the 8-byte and 16-byte assertions match the persisted value-format invariants.
  • Confirm the RocksDB-owned factory lifetime and per-compaction-worker timestamp semantics for both local and cloud RocksDB.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The TTL compaction filter now validates encoded values, safely decodes timestamps, uses millisecond system time, and removes expired TTL values. Catch2 tests cover expired, unexpired, and non-TTL values for RocksDB datastore variants.

Changes

TTL compaction filtering

Layer / File(s) Summary
Safe TTL filtering
store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp
TTLCompactionFilter::Filter validates value sizes, decodes timestamps with memcpy, detects the TTL marker, and removes expired values.
TTL filter test coverage
tx_service/tests/TTLCompactionFilter-Test.cpp, tx_service/tests/CMakeLists.txt
Catch2 tests cover expired, unexpired, and non-TTL values. The test is enabled for RocksDB, S3, and GCS datastore configurations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: liunyl

Poem

A rabbit checks each timestamp bright,
Safe bytes guide the filter’s sight.
Expired leaves fade away,
Fresh ones safely choose to stay.
Tests hop through each TTL case,
With careful steps and measured pace.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change to TTL compaction filter value parsing.
Description check ✅ Passed The description explains the problem, implementation, behavior, testing, risks, rollback, and reviewer focus with sufficient detail.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/fix-ttl-compaction-filter-offset

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thweetkomputer
thweetkomputer marked this pull request as ready for review August 2, 2026 10:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp (1)

17-44: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Fix is correct; consider extracting a shared decode helper to prevent the encode/decode logic from drifting apart again.

The logic correctly reads the TTL marker from the first word's MSB and only reads the expiration timestamp from the second word when the marker is set, with size guards that retain truncated values instead of reading past existing_value's bounds. This matches the PR's stated fix.

This decode logic duplicates the parsing already implemented in DecodeHasTTLFromTs/DeserializeValueToRecord (Lines 1374-1412) and the encoding in EncodeHasTTLIntoTs/TransformRecordToValueSlices (Lines 1329-1372). The bug this PR fixes exists because the compaction filter and the record encoder/decoder disagreed about the on-disk layout. Keeping two independent implementations of the same wire format increases the risk that a future change to one path silently breaks the other, since RocksDB compaction runs on live data with no schema check.

Extract a single length-checked decode helper that both Filter and DeserializeValueToRecord call, so the TTL marker and offset logic exists in one place.

As per coding guidelines, "Document non-obvious invariants and operational constraints... explain why rather than restating syntax" for **/*; consolidating the decode logic also documents the shared invariant in one place instead of two comments that can drift apart.

♻️ Proposed shared decode helper
// Decodes the version timestamp and, if present, the TTL expiration
// timestamp from a value encoded by TransformRecordToValueSlices.
// Returns false if `data`/`size` is too short to hold the fields that
// the TTL marker claims are present; callers must treat that as
// "keep the value unchanged" rather than reading past `size`.
bool TryDecodeVersionAndTtl(const char *data,
                            size_t size,
                            uint64_t &ts,
                            bool &has_ttl,
                            uint64_t &ttl,
                            size_t &record_offset)
{
    if (size < sizeof(uint64_t))
    {
        return false;
    }
    std::memcpy(&ts, data, sizeof(ts));
    has_ttl = (ts & MSB) != 0;
    ts &= MSB_MASK;
    record_offset = sizeof(uint64_t);
    if (has_ttl)
    {
        if (size < sizeof(uint64_t) * 2)
        {
            return false;
        }
        std::memcpy(&ttl, data + sizeof(uint64_t), sizeof(ttl));
        record_offset += sizeof(uint64_t);
    }
    else
    {
        ttl = 0;
    }
    return true;
}

Filter and DeserializeValueToRecord then both call this helper instead of re-implementing the offsets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp` around
lines 17 - 44, Extract a shared length-checked decode helper for the version
timestamp, TTL marker, TTL expiration, and record offset, preserving
truncated-value handling. Update both Filter and DeserializeValueToRecord to
call this helper instead of duplicating the wire-format parsing, and keep the
helper’s invariant documented in one place.

Source: Coding guidelines

tx_service/tests/TTLCompactionFilter-Test.cpp (1)

46-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for truncated/malformed values.

The PR objective states the filter retains malformed or truncated values without reading past their bounds, and rocksdb_data_store_common.cpp (Lines 17-20 and 31-34) implements two distinct bound checks for this. None of the three SECTIONs here exercise a value shorter than sizeof(uint64_t), or a value with the TTL marker set but shorter than sizeof(uint64_t) * 2. Add sections for both cases so the safety behavior this PR introduces is actually verified by the test suite.

✅ Proposed additional sections
SECTION("value shorter than the encoded timestamp is retained")
{
    const std::string value(sizeof(uint64_t) - 1, '\0');
    REQUIRE_FALSE(ShouldFilter(value));
}

SECTION("TTL-flagged value truncated before the expiration word is retained")
{
    std::string value(sizeof(uint64_t) + 1, '\0');
    uint64_t encoded_ts = EloqDS::MSB | 42;
    std::memcpy(value.data(), &encoded_ts, sizeof(encoded_ts));
    REQUIRE_FALSE(ShouldFilter(value));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tx_service/tests/TTLCompactionFilter-Test.cpp` around lines 46 - 69, Add two
sections to the TTL compaction filter test covering malformed inputs: verify a
value shorter than sizeof(uint64_t) is retained, and verify a TTL-marked value
shorter than sizeof(uint64_t) * 2 is also retained after encoding the marker
into its prefix. Place them alongside the existing sections in the test case and
assert both through ShouldFilter.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tx_service/tests/CMakeLists.txt`:
- Around line 69-74: Update the TTLCompactionFilter-Test setup in the CMake
configuration to add the store_handler/eloq_data_store_service include directory
and link the target containing rocksdb_data_store_common.cpp, while preserving
its existing Catch test registration and data-store condition.

---

Nitpick comments:
In `@store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp`:
- Around line 17-44: Extract a shared length-checked decode helper for the
version timestamp, TTL marker, TTL expiration, and record offset, preserving
truncated-value handling. Update both Filter and DeserializeValueToRecord to
call this helper instead of duplicating the wire-format parsing, and keep the
helper’s invariant documented in one place.

In `@tx_service/tests/TTLCompactionFilter-Test.cpp`:
- Around line 46-69: Add two sections to the TTL compaction filter test covering
malformed inputs: verify a value shorter than sizeof(uint64_t) is retained, and
verify a TTL-marked value shorter than sizeof(uint64_t) * 2 is also retained
after encoding the marker into its prefix. Place them alongside the existing
sections in the test case and assert both through ShouldFilter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20e31901-722a-4d06-acfe-007f7ea687cf

📥 Commits

Reviewing files that changed from the base of the PR and between fda69ce and f856fb6.

📒 Files selected for processing (3)
  • store_handler/eloq_data_store_service/rocksdb_data_store_common.cpp
  • tx_service/tests/CMakeLists.txt
  • tx_service/tests/TTLCompactionFilter-Test.cpp

Comment thread tx_service/tests/CMakeLists.txt
@thweetkomputer
thweetkomputer merged commit 63db0ec into main Aug 2, 2026
10 checks passed
@thweetkomputer
thweetkomputer deleted the agent/fix-ttl-compaction-filter-offset branch August 2, 2026 13:08
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.

2 participants