Skip to content

Inline Redis collection TTL metadata#997

Draft
bootjp wants to merge 11 commits into
mainfrom
design/ttl-inline-value-metadata
Draft

Inline Redis collection TTL metadata#997
bootjp wants to merge 11 commits into
mainfrom
design/ttl-inline-value-metadata

Conversation

@bootjp

@bootjp bootjp commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • embed collection TTL metadata with collection values instead of requiring sidecar anchors
  • add inline TTL support for Redis HyperLogLog values
  • add migration for legacy TTL anchors and mark the design implemented

Design doc

  • docs/design/2026_04_17_implemented_ttl_inline_value.md

Validation

  • go test ./adapter ./internal/backup -run 'TestRedis.*TTL|TestRedis.*HLL|TestRedisCollection|TestTTL|TestInline|TestEncodeRedis|TestDecodeRedis' -count=1
  • golangci-lint --config=.golangci.yaml run ./adapter ./internal/backup --timeout=5m
  • git verify-commit for all branch commits

Author: bootjp

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 821dc1d3-8e19-45d8-99c1-fb1feec65b34

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request implements inline TTL for Redis collections (list, hash, set, zset, stream) and HLL payloads by embedding expiration timestamps directly in their metadata anchors, supported by a background migrator and an option to disable the legacy secondary index fallback. The code review identified several critical issues: first, applyPositiveExpire does not update collection metadata with the new TTL, leading to desynchronization; second, the background migrator batches too many keys in a single transaction, risking frequent write conflicts; and third, both hasExpired and decodeHLLSetValueAt bypass the disableLegacyTTLReadFallback gate by unconditionally calling legacyIndexTTLAt. Additionally, the migrator should be made more robust to handle legacy TTL updates on already-migrated keys.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread adapter/redis_txn.go
Comment thread adapter/redis_ttl_inline_migrator.go Outdated
Comment on lines +177 to +181
if err := c.dispatchCompaction(ctx, readTS, elems); err != nil {
c.logger.WarnContext(ctx, "ttl inline migrator: batch dispatch failed",
"type", h.typeName, "error", err)
return err
}

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.

high

migrateTTLInlineHandler accumulates migration elements for up to 512 different user keys (ttlInlineMigrationTickScanLimit) and dispatches them all in a single Raft transaction via c.dispatchCompaction. In a production environment, a single transaction containing up to 1024 elements (migration writes + TTL index syncs) across hundreds of distinct keys is highly susceptible to write conflicts. If a concurrent client write touches even one of these keys during the transaction, the entire batch of 512 keys will fail with ErrWriteConflict and abort. This leads to wasted CPU/network resources, high transaction retry overhead, and can severely slow down or stall the background migration. Recommendation: Batch the migration dispatches into much smaller groups (e.g., 16 or 32 keys per transaction), or dispatch them individually/in very small batches so that a write conflict on a single hot key does not roll back the migration progress of the entire scanned window.

Comment thread adapter/redis_compat_types.go Outdated
Comment on lines +458 to +460
if err == nil && !found {
ttl, err = r.legacyIndexTTLAt(ctx, userKey, readTS)
}

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.

high

In hasExpired, when nonStringOnly is true and collectionTTLAt misses (found == false), legacyIndexTTLAt is called unconditionally. This completely bypasses the r.disableLegacyTTLReadFallback gate. Even after the operator disables the legacy fallback to optimize hot-path reads, any non-string check (such as EXISTS on a non-existent key or a key without a TTL) will still perform an expensive, redundant point lookup on the !redis|ttl|<key> index. Recommendation: Wrap the legacyIndexTTLAt call in a check for !r.disableLegacyTTLReadFallback.

Suggested change
if err == nil && !found {
ttl, err = r.legacyIndexTTLAt(ctx, userKey, readTS)
}
if err == nil && !found && !r.disableLegacyTTLReadFallback {
ttl, err = r.legacyIndexTTLAt(ctx, userKey, readTS)
}

Comment thread adapter/redis_ttl_inline_migrator.go Outdated
Comment on lines +145 to +150
if ttlMs == 0 {
ttlMs, err = legacyTTLMillisAt(ctx, c.st, userKey, readTS)
if err != nil {
return nil, err
}
}

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.

medium

In simpleTTLInlineMigrationHandler, the migrator only queries the legacy TTL index via legacyTTLMillisAt if ttlMs == 0 (i.e., the metadata key is in the legacy format or has no inline TTL). If a collection key has already been migrated to the inline format (so ttlMs > 0), but a subsequent EXPIRE command executed inside a MULTI/EXEC transaction or a Lua script updates the legacy TTL index without updating the metadata key, the migrator will completely skip reading the legacy TTL index. As a result, the metadata key will remain stuck with the stale TTL, and ttlAt (which prioritizes inline metadata) will return the stale TTL. While the primary fix should be ensuring that transactions and Lua scripts update the metadata key directly, the migrator could also be made more robust by always syncing the metadata TTL if the legacy TTL index has a different, newer value during the migration window.

Comment thread adapter/redis_compat_helpers.go Outdated
Comment on lines +612 to +617
if !embedded {
ttl, err = r.legacyIndexTTLAt(ctx, key, readTS)
if err != nil {
return redisSetValue{}, err
}
}

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.

medium

In decodeHLLSetValueAt, if the HLL payload is in the legacy format (embedded == false), legacyIndexTTLAt is called unconditionally. This ignores the r.disableLegacyTTLReadFallback gate, meaning we will still perform a secondary index lookup for legacy HLL keys even after the fallback has been disabled. Recommendation: Respect r.disableLegacyTTLReadFallback before calling legacyIndexTTLAt.

Suggested change
if !embedded {
ttl, err = r.legacyIndexTTLAt(ctx, key, readTS)
if err != nil {
return redisSetValue{}, err
}
}
if !embedded && !r.disableLegacyTTLReadFallback {
ttl, err = r.legacyIndexTTLAt(ctx, key, readTS)
if err != nil {
return redisSetValue{}, err
}
}

@bootjp

bootjp commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50d34a5e09

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_collection_ttl.go
Comment thread adapter/redis_txn.go
Comment thread adapter/redis_collection_ttl.go
Comment thread adapter/redis_ttl_inline_migrator.go Outdated
@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Handled the latest TTL-inline review blockers.

  • EXPIRE/PEXPIRE now falls back correctly for legacy hash/set/zset collection blobs instead of returning 0 before migration.
  • Delta-heavy list/hash metadata no longer rejects TTL updates solely because full delta folding is truncated; base TTL or the scan index still advances.
  • MULTI/EXEC staged list and zset creates now write inline TTL metadata in the same transaction.
  • Lua staged collection creates now full-commit new list/zset keys so inline TTL metadata is present.
  • The TTL-inline migrator no longer skips legitimate list user keys beginning with d|; real list meta delta rows are distinguished by value length.

Validation:

  • golangci-lint --config=.golangci.yaml run --fix
  • go test ./adapter -run "TestRedisCollectionExpireHandlesLegacyBlobs|TestRedisCollectionExpireAllowsDeltaHeavyCollections|TestRedisMultiExecStagedCollectionCreateWritesInlineTTL|TestRedisLuaStagedCollectionCreateWritesInlineTTL|TestRedisCollectionExpireWritesInlineMetaTTL|TestDeltaCompactor_TTLInlineMigratesLegacyString|TestDeltaCompactor_TTLInlineMigratesLegacyHLL|TestDeltaCompactor_TTLInlineMigratesLegacyHashMeta|TestDeltaCompactor_TTLInlineMigratesListUserKeyStartingWithDeltaPrefix|TestDeltaCompactor_TTLInlineRefreshesStaleInlineHashMeta|TestDeltaCompactor_TTLInlineMigrationDispatchesSmallBatches|TestRedis_MultiExec_DelThenRPushRecreatesList|TestRedis_MultiExec_ExpireSetsVisibleTTL|TestRedis_MultiExec_SetEXStoresTTL" -count=1 -timeout=300s
  • go test ./adapter -run "^$" -count=1 -timeout=120s

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 607ee9c82d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_compat_types.go
Comment thread adapter/redis_delta_compactor.go
Comment thread adapter/redis_ttl_inline_migrator.go Outdated
Comment thread adapter/redis_ttl_inline_migrator.go
@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Handled the latest TTL-inline review blockers.

  • Logical Redis backup now strips the HLL inline TTL envelope before writing hll/*.bin and deduplicates the matching hll_ttl.jsonl entry when the scan-index TTL row also appears.
  • TTL-inline migration now runs even on nodes that lead only non-default shard groups, while still filtering candidate keys through IsLeaderForKey before dispatch.
  • Empty Redis keys are treated as valid migration keys; nil remains the invalid extractor result.
  • TTL-index migration now covers base-less collection keys, including delta-only wide-column collections and legacy hash/set/zset blobs, and leaves base-meta keys to the existing base-meta migrator to avoid overlapping stale-read updates.

Validation:

  • go test ./internal/backup -count=1 -timeout=240s
  • go test ./adapter -run "TestDeltaCompactor_TTLInline|TestRedisCollectionExpire|TestRedisMultiExecStagedCollectionCreateWritesInlineTTL|TestRedisLuaStagedCollectionCreateWritesInlineTTL|TestRedis_MultiExec_DelThenRPushRecreatesList|TestRedis_MultiExec_ExpireSetsVisibleTTL|TestRedis_MultiExec_SetEXStoresTTL|TestRedisHLL" -count=1 -timeout=300s
  • golangci-lint --config=.golangci.yaml run ./adapter/... ./internal/backup/... --timeout=5m
  • git diff --check
  • git verify-commit HEAD

Note: go test ./adapter -count=1 -timeout=300s reached the package-level timeout after the focused coverage and lint above had passed.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b26fd2a95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_compat_types.go Outdated
@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Fixed the latest HLL TTL precedence finding.

Changes:

  • make ttlAt consult HLL TTL ownership before collection inline TTL
  • treat a new-format HLL anchor without embedded TTL as authoritative no-TTL, so stale collection metadata cannot expire a recreated HLL
  • keep legacy HLL payloads eligible for the legacy !redis|ttl| fallback
  • add regression coverage for stale expired collection metadata plus a fresh no-TTL HLL anchor

Caller audit:

  • hllTTLAt is only called by ttlAt
  • ttlAt callers cover Redis TTL/PTTL/Lua and the non-string guard path through hasExpired; HLL still enters the normal ttlAt path because rawKeyTypeAt classifies HLL anchors as string-family keys

Validation:

  • go test ./adapter -run 'TestRedisHLLTTLAtIgnoresStaleCollectionTTL|TestRedisHLLTTLAtReadsEmbeddedAfterScanIndexDeleted|TestRedisDispatchHLLExpireWritesInlineAnchorAndScanIndex|TestRedisLoadHLLAtTreatsExpiredInlineTTLAsEmpty|TestRedisLoadHLLAtTreatsExpiredLegacyTTLAsEmpty|TestRedisLoadHLLAtSkipsLegacyTTLWhenFallbackDisabled|TestRedisHasExpiredSkipsLegacyTTLWhenFallbackDisabled|TestRedisTTLAt_LegacyFallbackCanBeDisabled|TestDeltaCompactor_TTLInlineMigratesLegacyHLL' -count=1 -timeout=240s
  • go test ./adapter -run 'TestRedis(HLL|LoadHLL|CollectionExpire|_MultiExec_ExpireSetsVisibleTTL|_ExpiredKey_BecomesInvisible|_HGET_TTLExpired|_HEXISTS_TTLExpired|_SISMEMBER_TTLExpired)|TestLua_(HGET|HEXISTS|ZSCORE|ZRANGEBYSCORE)_TTLExpired|TestDeltaCompactor_TTLInline' -count=1 -timeout=300s
  • golangci-lint --config=.golangci.yaml run ./adapter/... --timeout=5m
  • git diff --check
  • git verify-commit HEAD

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd45d8ccfb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_txn.go
Comment thread internal/backup/redis_string.go
Comment thread adapter/redis_collection_ttl.go Outdated
Comment thread internal/backup/redis_hash.go Outdated
Comment thread adapter/redis_collection_ttl.go
@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

Handled the latest TTL-inline review blockers.

  • MULTI/EXEC SET followed by EXPIRE now treats the staged string as the TTL owner instead of trying the HLL rewrite path.
  • Current-format no-TTL collection metadata is authoritative, so stale scan-index TTL rows no longer leak into TTL/PTTL or expiry checks.
  • Logical backup now treats current-format HLL/string anchors and inline collection metadata as owning TTL, including no-TTL anchors, so stale scan-index rows are ignored.
  • Delta-only hash/set/zset EXPIRE keeps the scan-index update and signals urgent compaction when the delta scan is truncated.

Validation:

  • go test ./adapter -run 'TestRedis_MultiExec_SetThenExpireNewString|TestRedis_MultiExec_SetEXStoresTTL|TestRedis_MultiExec_ExpireSetsVisibleTTL|TestRedisCollectionExpire|TestRedisTTLAtInlineNoTTLCollectionMetaSkipsLegacyFallback|TestRedisHLLTTLAt|TestRedisHasExpiredSkipsLegacyTTLWhenFallbackDisabled|TestRedisTTLAt_LegacyFallbackCanBeDisabled|TestDeltaCompactor_TTLInline' -count=1 -timeout=300s\n- go test ./internal/backup -count=1 -timeout=240s\n- golangci-lint --config=.golangci.yaml run ./adapter/... ./internal/backup/... --timeout=5m\n- git diff --check\n- git verify-commit HEAD\n\n@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1b3251649

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +155 to +156
if legacyTTL != 0 && legacyTTL != ttlMs {
ttlMs = legacyTTL

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve authoritative inline collection TTLs

When this handler scans an already-current hash/set/zset meta value, ttlMs came from the inline metadata and is authoritative, but this fallback unconditionally re-imports any stale !redis|ttl| row. For a current 16-byte meta with ExpireAt=0 plus an old scan-index TTL, the migrator rewrites the collection with that stale TTL and the key can later expire even though the inline anchor says it is persistent. Only consult the legacy TTL when pair.Value is the legacy 8-byte meta format.

Useful? React with 👍 / 👎.

Comment on lines +461 to +462
if legacyTTL != 0 && legacyTTL != meta.ExpireAt {
meta.ExpireAt = legacyTTL

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not resurrect stale TTLs for current list meta

For a current-format list meta row, meta.ExpireAt is already the authoritative TTL, including 0 for no TTL; this unconditional legacy fallback will copy a stale !redis|ttl| value back into the inline meta. A list that was made persistent but still has an old scan-index row will be rewritten as expiring by the migrator. Gate this fallback on the legacy 24-byte list meta format; the stream migrator below repeats the same pattern and needs the same guard for 24-byte stream meta.

Useful? React with 👍 / 👎.

Comment on lines +39 to +40
prefix: []byte(redisStrPrefix),
extractUserKey: trimKnownPrefix([]byte(redisStrPrefix)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Migrate bare legacy strings before disabling fallback

This string migration pass only scans !redis|str| anchors, but the read path still supports pre-prefix bare string keys whose TTL lives in !redis|ttl|<key>. Those bare strings are never rewritten into an inline anchor, so after WithRedisLegacyTTLReadFallback(false) is enabled, ttlAt/keyTypeAt stop consulting their only TTL source and expired bare strings can look persistent to TTL/EXISTS/type checks. Either include a bare-string migration path or keep the fallback gate from being advertised as safe while bare legacy strings may exist.

Useful? React with 👍 / 👎.

Comment thread adapter/redis_txn.go
}
userKey := []byte(key)
typ := t.collectionExpireTypes[key]
built, ok, err := t.server.collectionExpireElems(ctx, userKey, t.startTS, typ, redisExpireAtMillis(*st.value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use staged collection meta for EXEC expiry

When the same EXEC recreates a list and then expires it, such as DEL k; RPUSH k v; EXPIRE k 60 against an existing list, this call rebuilds TTL metadata from t.startTS instead of the staged list state. prepareDispatch appends these collection TTL elems after buildListElems, so the stale meta Put from the pre-transaction list can overwrite the newly created inline meta and leave the list with the old head/length rather than the RPUSH result. Skip keys whose collection meta is already rewritten in the transaction, or update the staged state before emitting elems.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@bootjp

bootjp commented Jul 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

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.

1 participant