Skip to content

Bug fixes#9

Open
bdurand wants to merge 4 commits into
mainfrom
detailed-review
Open

Bug fixes#9
bdurand wants to merge 4 commits into
mainfrom
detailed-review

Conversation

@bdurand

@bdurand bdurand commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Fixed

  • Queries on relations with conditions that cannot be represented in the cache key (SQL string conditions, ranges, not, or, joins, group, having, from, offset, locking, or non-hash find_by arguments) now bypass the cache instead of silently ignoring those conditions and returning or caching the wrong record.
  • create_with values are no longer included in cache keys, so queries using create_with can now be cached correctly.
  • Queries inside an open transaction now bypass the cache so that uncommitted data can never be cached. Previously a rolled back transaction could permanently poison the cache with data that was never committed. Transactions created with joinable: false (such as Rails transactional test fixtures) still use the cache.
  • Cache invalidation now runs even when caching is disabled. Previously records changed inside a disable block (or while caching was disabled globally) left stale entries behind for when caching was re-enabled.
  • Cache key values are now cast through the attribute type, so equivalent values (e.g. :one and "one", or "5" and 5) produce the same cache key. Previously such entries could be written under keys that the invalidation callbacks could never delete. This also fixes cache keys for false attribute values, which were previously indistinguishable from nil.
  • load_cache no longer raises an error when caching is disabled and now honors where conditions on cache_by configurations instead of caching records that do not match them. It also refreshes existing cache entries instead of skipping them.
  • A cache_by configuration whose where clause does not match a query no longer prevents later configurations from matching, and no longer mutates the query attributes while matching.
  • Calling cache_by in a subclass no longer mutates the superclass's cache configuration.
  • Models that include SupportTableCache without calling cache_by no longer raise an error on find_by.
  • Calling cache_belongs_to more than once for the same association no longer causes infinite recursion when reading the association.
  • SupportTableCache::MemoryCache now synchronizes all access to the underlying hash (previously reads, deletes, and clears were unsynchronized), purges expired entries, and no longer serializes values twice on a cache miss.
  • SupportTableCache::FiberLocals now stores state in the fiber's native local storage so that state cannot leak from fibers that are garbage collected while suspended inside a block.

Summary by CodeRabbit

  • Bug Fixes
    • Improved cache key correctness by type-casting key values, properly honoring where constraints, and ignoring create_with when building keys.
    • Bypassed caching for non-cacheable query shapes and inside open/joinable transactions; ensured invalidation still clears entries even when caching is disabled.
    • Hardened caching behavior for cache_by inheritance/matching, find_by edge cases, repeated cache associations, and fixed race safety in memory cache and fiber-local state isolation.
  • Documentation
    • Expanded guidance on cache TTL/stale-write races, runtime disable/enable expectations, and disabled-caching entry clearing semantics.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR is a broad correctness fix for SupportTableCache, addressing over a dozen independently reported bugs around cache poisoning, stale invalidation, and incorrect key generation. All changes are additive safety guards or targeted replacements of broken logic.

  • Cache correctness: Queries with non-equality conditions (SQL strings, ranges, joins, not, or, group, having, etc.) now bypass the cache via the new support_table_cacheable_scope? guard; queries inside a joinable transaction are also bypassed to prevent rollback-poisoning the cache. The where filter in cache_by no longer mutates query attributes or blocks later configurations from matching.
  • Key integrity: Cache keys now type-cast attribute values (so :one/"one" and 5/"5" produce the same key), false is distinguished from nil, and create_with values are excluded; invalidation callbacks are decoupled from the disabled flag so stale entries can no longer survive a disable block.
  • Infrastructure: FiberLocals is rewritten to use Thread.current[] (fiber-local in MRI Ruby) eliminating the shared mutex+hash approach; MemoryCache adds mutex guards to reads, deletes, and clears; cache_belongs_to guards its alias_method call to prevent infinite recursion on repeated calls.

Confidence Score: 4/5

The PR fixes real bugs and is safe to merge; the changes are well-tested and the logic is sound throughout.

All changes correct previously broken behavior and each fix is backed by a new test. The two main areas to watch are: (1) support_table_cacheable_scope? uses where_clause.send(:predicates) to access a private ActiveRecord method—if this API moves between Rails versions the guard silently stops working, causing cache bypasses rather than returning wrong data; (2) open_transaction? calls connection.current_transaction which is also an internal Rails method and could raise if its behavior changes. Neither is a correctness regression relative to the old code, but both create a future-compatibility dependency worth tracking.

lib/support_table_cache/relation_override.rb (private-API predicate count) and lib/support_table_cache.rb (open_transaction? internals and where matching without type casting) are the places most likely to need revisiting on a Rails version upgrade.

Important Files Changed

Filename Overview
lib/support_table_cache.rb Core module: fixes superclass mutation in cache_by, separates invalidation cache from read cache, adds type casting in cache_key, introduces cache_key_for_query and open_transaction? helpers; where-matching in cache_key_for_query skips type casting
lib/support_table_cache/relation_override.rb Adds support_table_cacheable_scope? to gate caching on safe equality-only conditions; uses private where_clause.send(:predicates) for predicate count comparison
lib/support_table_cache/find_by_override.rb Simplified: rejects non-hash args early, defers scoped queries to RelationOverride, adds transaction guard, delegates to cache_key_for_query
lib/support_table_cache/fiber_locals.rb Rewrites fiber-local storage to use Thread.current[] (fiber-local in MRI Ruby) keyed by object_id, eliminating the mutex+hash-by-fiber-id approach and preventing state leaks from suspended GC'd fibers
lib/support_table_cache/memory_cache.rb Adds mutex synchronization to reads/deletes/clears, fixes double serialization on cache miss, write now returns the serialized value for reuse in fetch
lib/support_table_cache/associations.rb Guards alias_method with a method_defined? check to prevent infinite recursion when cache_belongs_to is called multiple times for the same association
spec/support_table_cache_spec.rb Comprehensive new tests covering transactions, complex relation conditions, create_with exclusion, equivalent-value normalization, disabled-caching invalidation, and where-condition filtering in load_cache

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant FindByOverride
    participant RelationOverride
    participant SupportTableCache
    participant Cache

    Caller->>FindByOverride: Model.find_by(name: "One")
    FindByOverride->>FindByOverride: args Hash? scoped? in transaction?
    alt scoped (default scope / scoping block)
        FindByOverride->>RelationOverride: super → find_by
        RelationOverride->>RelationOverride: "support_table_cacheable_scope?<br/>(predicates == where_values_hash size,<br/>no joins/group/having/from/offset/lock)"
        RelationOverride->>SupportTableCache: open_transaction?(klass)
        alt inside joinable transaction
            SupportTableCache-->>RelationOverride: true → bypass cache
            RelationOverride->>Caller: DB result (not cached)
        else
            RelationOverride->>SupportTableCache: cache_key_for_query(klass, attrs)
            SupportTableCache-->>RelationOverride: cache key or nil
            RelationOverride->>Cache: "fetch(key) { DB query }"
            Cache-->>Caller: record
        end
    else no scope
        FindByOverride->>SupportTableCache: open_transaction?(self)
        alt inside joinable transaction
            SupportTableCache-->>FindByOverride: true → bypass cache
            FindByOverride->>Caller: DB result (not cached)
        else
            FindByOverride->>SupportTableCache: cache_key_for_query(klass, attrs)
            SupportTableCache-->>FindByOverride: cache key or nil
            FindByOverride->>Cache: "fetch(key) { DB query }"
            Cache-->>Caller: record
        end
    end

    Note over Cache: Invalidation always runs<br/>(support_table_cache_for_invalidation<br/>ignores disabled flag)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant FindByOverride
    participant RelationOverride
    participant SupportTableCache
    participant Cache

    Caller->>FindByOverride: Model.find_by(name: "One")
    FindByOverride->>FindByOverride: args Hash? scoped? in transaction?
    alt scoped (default scope / scoping block)
        FindByOverride->>RelationOverride: super → find_by
        RelationOverride->>RelationOverride: "support_table_cacheable_scope?<br/>(predicates == where_values_hash size,<br/>no joins/group/having/from/offset/lock)"
        RelationOverride->>SupportTableCache: open_transaction?(klass)
        alt inside joinable transaction
            SupportTableCache-->>RelationOverride: true → bypass cache
            RelationOverride->>Caller: DB result (not cached)
        else
            RelationOverride->>SupportTableCache: cache_key_for_query(klass, attrs)
            SupportTableCache-->>RelationOverride: cache key or nil
            RelationOverride->>Cache: "fetch(key) { DB query }"
            Cache-->>Caller: record
        end
    else no scope
        FindByOverride->>SupportTableCache: open_transaction?(self)
        alt inside joinable transaction
            SupportTableCache-->>FindByOverride: true → bypass cache
            FindByOverride->>Caller: DB result (not cached)
        else
            FindByOverride->>SupportTableCache: cache_key_for_query(klass, attrs)
            SupportTableCache-->>FindByOverride: cache key or nil
            FindByOverride->>Cache: "fetch(key) { DB query }"
            Cache-->>Caller: record
        end
    end

    Note over Cache: Invalidation always runs<br/>(support_table_cache_for_invalidation<br/>ignores disabled flag)
Loading

Reviews (1): Last reviewed commit: "Bug fixes" | Re-trigger Greptile

# (SQL string conditions, ranges, OR clauses, joins, etc.) is not visible in
# where_values_hash and could silently change which record the query returns.
def support_table_cacheable_scope?
return false unless where_clause.send(:predicates).size == where_values_hash.size

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 Private Rails API accessed via send

where_clause.send(:predicates) bypasses the private visibility of predicates on ActiveRecord::Relation::WhereClause. The method exists in Rails 6–8 but is not part of the documented public interface and could be renamed, moved, or removed in a future Rails release. Since this check is load-bearing for the core correctness fix (preventing wrong records from being cached), a breakage here would silently fall back to false (never caching), degrading performance rather than returning wrong data, but it is still a dependency worth tracking.

There is no clean public-API alternative today, but the dependency should be noted in a comment so a future upgrade does not catch the team off-guard.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +301 to +306
def open_transaction?(klass)
return false unless klass.connection_pool.active_connection?

connection = klass.connection
connection.transaction_open? && connection.current_transaction.joinable?
end

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 Internal Rails API: connection.current_transaction

connection.current_transaction and current_transaction.joinable? are not part of ActiveRecord's public API; they live on the internal transaction stack and have changed signatures across major Rails versions. If Rails ever returns a null-transaction sentinel (instead of a real object) from current_transaction when transaction_open? is true—or renames the method—this would raise NoMethodError on every cached lookup during a transaction. Adding a guard (.respond_to?(:joinable?)) or a comment noting the dependency would help future maintainers.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread lib/support_table_cache.rb Outdated
Comment on lines +282 to +283
where_matched = where.nil? || where.all? { |name, value| attributes.include?(name) && attributes[name] == value }
next unless where_matched

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 where matching skips type casting

The where_matched check compares query attribute values directly with the where config values using ==, without casting them through the attribute type first. cache_key (called just below) does cast, so the two paths can disagree for equivalent but differently-typed values—e.g. find_by(active: 1) with cache_by :name, where: {active: true} would fail the where_matched test (1 == true is false) while cache_key would treat them as identical. The cache would be bypassed rather than returning wrong data, but users relying on type-coerced where conditions would silently miss the cache.

@bdurand

bdurand commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SupportTableCache now restricts cache lookups to safe query shapes, normalizes typed cache keys, preserves invalidation while disabled, filters cache loading, and improves fiber-local, memory-cache, and association behavior. Tests and documentation cover the updated release 1.1.6 semantics.

Changes

Cache correctness and concurrency

Layer / File(s) Summary
Query cache selection and key construction
lib/support_table_cache.rb, lib/support_table_cache/find_by_override.rb, lib/support_table_cache/relation_override.rb, spec/spec_helper.rb, spec/support_table_cache_spec.rb
Cache lookups validate query shapes and scopes, bypass open transactions, match typed where constraints, and normalize cache-key values.
Cache loading and invalidation lifecycle
lib/support_table_cache.rb, spec/support_table_cache_spec.rb, README.md, CHANGELOG.md, VERSION
Cache loading honors configured conditions and disabled state, invalidation uses the underlying cache, and release documentation describes the updated behavior.
Runtime state and association safety
lib/support_table_cache/fiber_locals.rb, lib/support_table_cache/memory_cache.rb, lib/support_table_cache/associations.rb, spec/support_table_cache/fiber_locals_spec.rb, spec/support_table_cache/memory_cache_spec.rb, spec/support_table_cache/associations_spec.rb
Fiber-local storage uses native fiber-local state, memory-cache generations prevent stale writes after invalidation, and repeated cached association wrapping preserves aliases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant RelationOverride
  participant SupportTableCache
  participant MemoryCache
  participant Database
  Caller->>RelationOverride: find_by query
  RelationOverride->>SupportTableCache: validate query and build key
  SupportTableCache->>MemoryCache: fetch cache key
  MemoryCache-->>RelationOverride: cached record or miss
  RelationOverride->>Database: execute query on miss
  Database-->>RelationOverride: return record
  RelationOverride->>MemoryCache: cache record
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is generic and does not describe the specific cache and FiberLocals fixes in this change. Rename the PR to summarize the main change, such as cache correctness and MemoryCache/FiberLocals fixes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 84.21% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch detailed-review

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

@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: 2

🤖 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 `@lib/support_table_cache/memory_cache.rb`:
- Around line 26-39: Update the cache-miss fill flow around the lookup method
and its write call so a value fetched outside `@mutex` cannot be written after a
concurrent delete or clear. Coordinate fills with the existing invalidation
methods in SupportTableCache, either by holding the cache synchronization
through the fill or by tracking per-key generations and rejecting writes from
invalidated generations; preserve expiration and nil-value behavior.

In `@lib/support_table_cache/relation_override.rb`:
- Around line 89-102: Replace the internal where_clause.send(:predicates) call
in support_table_cacheable_scope? with the stable, supported where-clause
attribute API, preserving the existing comparison that rejects non-simple
conditions. Verify the chosen API across the project’s supported Rails version
range and add a focused compatibility spec if needed so future Rails changes
fail explicitly.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 89109ee4-a762-47d8-a513-6bc1ba814363

📥 Commits

Reviewing files that changed from the base of the PR and between 60128c4 and 130deed.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.md
  • VERSION
  • lib/support_table_cache.rb
  • lib/support_table_cache/associations.rb
  • lib/support_table_cache/fiber_locals.rb
  • lib/support_table_cache/find_by_override.rb
  • lib/support_table_cache/memory_cache.rb
  • lib/support_table_cache/relation_override.rb
  • spec/spec_helper.rb
  • spec/support_table_cache/associations_spec.rb
  • spec/support_table_cache/fiber_locals_spec.rb
  • spec/support_table_cache_spec.rb

Comment thread lib/support_table_cache/memory_cache.rb
Comment on lines +89 to +102
# A relation can only be cached if all of its conditions are simple equality conditions
# on the model's own attributes that can be represented in a cache key. Anything else
# (SQL string conditions, ranges, OR clauses, joins, etc.) is not visible in
# where_values_hash and could silently change which record the query returns.
def support_table_cacheable_scope?
return false unless where_clause.send(:predicates).size == where_values_hash.size
return false if joins_values.present? || left_outer_joins_values.present?
return false if group_values.present? || !having_clause.empty?
return false if !from_clause.empty? || offset_value.present? || lock_value
return false if eager_loading?

true
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

Cacheability check depends on an internal, non-public Rails API.

where_clause.send(:predicates) reaches into ActiveRecord::Relation::WhereClause#predicates, which is protected and the class itself is documented as "for internal use only" (:nodoc:) across Rails versions. The heuristic is currently correct (verified against SQL-string/range/.not/create_with test cases), but since this isn't part of Rails' public API contract, a future Rails release could rename/restructure it, silently breaking support_table_cacheable_scope? for every relation-based cached lookup.

Consider anchoring on a more stable signal (e.g. where_clause.extract_attributes, which appears to be a public method returning the list of attributes referenced by representable predicates) or adding a CI matrix entry / explicit spec pinned to the supported Rails version range to catch breakage early.

Please confirm the target Rails version range and whether WhereClause#predicates/extract_attributes visibility is stable there.

🤖 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 `@lib/support_table_cache/relation_override.rb` around lines 89 - 102, Replace
the internal where_clause.send(:predicates) call in
support_table_cacheable_scope? with the stable, supported where-clause attribute
API, preserving the existing comparison that rejects non-simple conditions.
Verify the chosen API across the project’s supported Rails version range and add
a focused compatibility spec if needed so future Rails changes fail explicitly.

- MemoryCache#fetch no longer stores a value generated concurrently with a
  delete or clear, which could resurrect a stale record after invalidation.
  Fills are coordinated with invalidation via a generation counter.
- Cast values through the attribute type when matching cache_by where
  clauses in cache_key_for_query and load_cache so equivalent values
  (e.g. 1 and "1") match the same way cache keys are built.
- Guard the internal Rails APIs used by support_table_cacheable_scope?
  (WhereClause#predicates) and open_transaction? (current_transaction
  and joinable?) so a future Rails change degrades to bypassing the
  cache instead of raising, document the dependencies, and add specs
  pinning the APIs so an incompatible Rails upgrade fails explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/support_table_cache/relation_override.rb (1)

32-36: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not let explicit find_by args overwrite scoped predicates where(name: "Two").find_by(name: "One") must still behave like name = "Two" AND name = "One" and return nil; merging here replaces the scoped value and can return a cached record the relation excludes. Bypass the cache when relation and find_by keys overlap.

🤖 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 `@lib/support_table_cache/relation_override.rb` around lines 32 - 36, Update
the relation override logic around scope_conditions and attributes so
overlapping keys between scoped predicates and explicit find_by arguments bypass
the cache instead of merging values. Preserve normal cache lookup when the key
sets do not overlap, while ensuring overlapping conditions are evaluated by the
database with both predicates so scoped exclusions cannot return a cached
record.
lib/support_table_cache.rb (1)

127-130: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make fetch_by reject queries that miss configured where clauses
RelationOverride#fetch_by still checks only attribute names, so TypedWhereModel.fetch_by(name: "typed-one") is accepted even though no cache entry can satisfy where: {value: 1} and it falls back to the database. Include the configured where scope in the safety check so fetch_by raises when the query cannot hit the cache.

🤖 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 `@lib/support_table_cache.rb` around lines 127 - 130, The cache safety check
used by RelationOverride#fetch_by must validate configured where clauses in
addition to attribute names. Update the matching logic to compare the query’s
where scope against each entry’s configured where value, preserving matches only
when both attributes and where conditions are satisfied; otherwise raise instead
of falling back to the database.
🤖 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 `@lib/support_table_cache/memory_cache.rb`:
- Around line 44-51: Update the cache write logic in the method containing
serialized_value and expire_at so replacing an expired entry without expires_in
clears the prior expiration timestamp. Ensure the stored entry uses nil or a
newly computed expiry for the replacement, rather than retaining the old
expire_at value.

---

Outside diff comments:
In `@lib/support_table_cache.rb`:
- Around line 127-130: The cache safety check used by RelationOverride#fetch_by
must validate configured where clauses in addition to attribute names. Update
the matching logic to compare the query’s where scope against each entry’s
configured where value, preserving matches only when both attributes and where
conditions are satisfied; otherwise raise instead of falling back to the
database.

In `@lib/support_table_cache/relation_override.rb`:
- Around line 32-36: Update the relation override logic around scope_conditions
and attributes so overlapping keys between scoped predicates and explicit
find_by arguments bypass the cache instead of merging values. Preserve normal
cache lookup when the key sets do not overlap, while ensuring overlapping
conditions are evaluated by the database with both predicates so scoped
exclusions cannot return a cached record.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 7abfd83a-6e41-4765-a94d-8eec46f3b864

📥 Commits

Reviewing files that changed from the base of the PR and between 130deed and 03d2513.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • lib/support_table_cache.rb
  • lib/support_table_cache/memory_cache.rb
  • lib/support_table_cache/relation_override.rb
  • spec/spec_helper.rb
  • spec/support_table_cache/memory_cache_spec.rb
  • spec/support_table_cache_spec.rb

Comment on lines 44 to +51
serialized_value = Marshal.dump(value)
expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in if expires_in

@mutex.synchronize do
# Only store the value if the cache was not invalidated while the value was being
# generated. Otherwise a record deleted by a concurrent invalidation could be
# resurrected in the cache with stale data.
@cache[key] = [serialized_value, expire_at] if @generation == generation

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset the expiry when replacing an expired entry.

If an expired entry is refetched without expires_in, expire_at still holds its old timestamp from Line 33. The replacement is therefore immediately expired and every later read becomes a miss.

Proposed fix
         serialized_value = Marshal.dump(value)
-        expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in if expires_in
+        expire_at = if expires_in
+          Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in
+        end
 
         `@mutex.synchronize` do
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
serialized_value = Marshal.dump(value)
expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in if expires_in
@mutex.synchronize do
# Only store the value if the cache was not invalidated while the value was being
# generated. Otherwise a record deleted by a concurrent invalidation could be
# resurrected in the cache with stale data.
@cache[key] = [serialized_value, expire_at] if @generation == generation
serialized_value = Marshal.dump(value)
expire_at = if expires_in
Process.clock_gettime(Process::CLOCK_MONOTONIC) + expires_in
end
`@mutex.synchronize` do
# Only store the value if the cache was not invalidated while the value was being
# generated. Otherwise a record deleted by a concurrent invalidation could be
# resurrected in the cache with stale data.
`@cache`[key] = [serialized_value, expire_at] if `@generation` == generation
🤖 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 `@lib/support_table_cache/memory_cache.rb` around lines 44 - 51, Update the
cache write logic in the method containing serialized_value and expire_at so
replacing an expired entry without expires_in clears the prior expiration
timestamp. Ensure the stored entry uses nil or a newly computed expiry for the
replacement, rather than retaining the old expire_at value.

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