Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 14 additions & 36 deletions .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ on:
pull_request:
branches-ignore:
- actions-*

env:
BUNDLE_CLEAN: "true"
BUNDLE_PATH: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
workflow_dispatch:

jobs:
build:
Expand All @@ -34,33 +29,16 @@ jobs:
- ruby: "2.5"
appraisal: "activerecord_5"
steps:
- uses: actions/checkout@v4
- name: Set up Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
with:
ruby-version: "${{ matrix.ruby }}"
- name: Install packages
run: |
sudo apt-get update
sudo apt-get install libsqlite3-dev
- name: Setup bundler
if: matrix.bundler != ''
run: |
gem uninstall bundler --all
gem install bundler --no-document --version ${{ matrix.bundler }}
- name: Set Appraisal bundle
if: matrix.appraisal != ''
run: |
echo "using gemfile gemfiles/${{ matrix.appraisal }}.gemfile"
bundle config set gemfile "gemfiles/${{ matrix.appraisal }}.gemfile"
- name: Install gems
run: |
bundle install
- name: Run Tests
run: bundle exec rake
- name: standardrb
if: matrix.standardrb == true
run: bundle exec standardrb
- name: yard
if: matrix.yard == true
run: bundle exec yard doc --fail-on-warning
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Ruby CI
uses: bdurand/github-actions/ruby-ci@b7560883d5f8557f5d69fe8bd82721bbb5ed76df # v1.0.4
with:
ruby: ${{ matrix.ruby }}
appraisal: ${{ matrix.appraisal }}
bundler: ${{ matrix.bundler }}
standardrb: ${{ matrix.standardrb }}
yard: ${{ matrix.yard }}
frozen_strings: ${{ matrix.frozen_strings }}
34 changes: 27 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
.DS_Store

.bundle/
/vendor/bundle
/lib/bundler/man/

.rspec
.ruby-version
.yardoc/
.env
*.gem
*.rbc

/spec/reports/
/spec/examples.txt
.yardoc/
_yardoc/
/doc/
/rdoc/
/coverage/
/log/*.log
/pkg/
/tmp/

.ruby-version
.ruby-gemset
Gemfile.lock
coverage/
gemfiles/*.gemfile.lock
log/*.log
pkg/
rdoc/
doc/

.byebug_history

.claude/
.conductor/
.cursor/
.config
9 changes: 1 addition & 8 deletions .standard.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
ruby_version: 2.5
ruby_version: 2.6

format: progress

ignore:
- "**/*":
- Style/RedundantParentheses
- "spec/**/*":
- Lint/ConstantDefinitionInBlock
- Lint/UselessAssignment
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 1.1.6

### 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. A `fetch` that races with a concurrent `delete` or `clear` no longer stores its stale value back in the cache.
- The `where` clause on a `cache_by` configuration is now matched against query attributes using values cast through the attribute type, so equivalent values (e.g. `1` and `"1"`) match the same way they do when building cache keys.
- `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.

## 1.1.5

### Fixed
Expand Down
23 changes: 15 additions & 8 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ source "https://rubygems.org"

gemspec

gem "rspec", "~> 3.0"
gem "rake"
gem "irb"
gem "sqlite3"
gem "appraisal"
gem "standard", "~>1.0"
gem "pry-byebug"
gem "yard"
# Exclude development-only gems from dependabot.
unless ENV["DEPENDABOT"]
gem "sqlite3"

gem "rake"
gem "rspec", "~> 3.11"

# Exclude development-only gems from the gemfiles generated by appraisal.
unless defined?(::Appraisal::Gemfile) && is_a?(::Appraisal::Gemfile)
gem "appraisal", require: false
gem "standard", require: false
gem "simplecov", require: false
gem "yard", require: false
end
end
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ You can also set a cache per class. For instance, you can set an in-memory cache

Note that in-memory caches exist separately within each process and will not be cleared when records are changed in the database. The only way to refresh elements in an in-memory cache is to restart the process or set the `support_table_cache_ttl` value so that the entries will expire.

It is a good idea to always set `support_table_cache_ttl`, even when using a shared cache store. Cache invalidation happens when a record's changes are committed, but a process reading the old row at the same moment can still write the stale value back to the cache just after it was invalidated. A TTL puts an upper bound on how long such a stale entry can survive.

The global cache and disabled settings (`SupportTableCache.cache=` and `SupportTableCache.disable` without a block) are intended to be set during application initialization and are not synchronized for concurrent modification at runtime.

Queries made inside an open database transaction always bypass the cache. This prevents uncommitted data from being cached (which would never be cleared if the transaction were rolled back). Transactions created with `joinable: false`, such as the ones wrapping Rails transactional test fixtures, do not bypass the cache.

### Disabling Caching

You can disable the cache within a block either globally or only for a specific class. If the cache is disabled, then all queries will pass through to the database.
Expand All @@ -108,6 +114,10 @@ SupportTableCache.enable do
end
```

Cache entries are still cleared when records are changed while caching is disabled, so re-enabling the cache will not expose stale data.

Note that the class-level `disable_cache` and `enable_cache` settings apply only to the class they are called on; they do not apply to subclasses in a single table inheritance hierarchy.

### Caching Belongs to Associations

You can also cache belongs to associations to cacheable models.
Expand Down
29 changes: 27 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

begin
require "bundler/setup"
rescue LoadError
Expand All @@ -13,10 +15,33 @@ task :verify_release_branch do
end
end

Rake::Task[:release].enhance([:verify_release_branch])
Rake::Task[:release].prerequisites.prepend("verify_release_branch")

require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

task default: :spec
task default: [:spec]

namespace :appraisal do
desc "Update the appraisal gemfiles"
task :update do
Dir.glob("gemfiles/*.gemfile*") do |file|
File.delete(file) if File.file?(file)
end

system "bundle exec appraisal generate" || abort("appraisal generate failed")

Dir.glob("gemfiles/*.gemfile") do |file|
puts "Locking #{file}"
Bundler.with_unbundled_env do
system(
{
"BUNDLE_GEMFILE" => file
},
"bundle", "lock", "--update"
) || abort("appraisal lock failed on #{file}")
end
end
end
end
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.5
1.1.6
8 changes: 2 additions & 6 deletions gemfiles/activerecord_5.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@

source "https://rubygems.org"

gem "rspec", "~> 3.0"
gem "rake"
gem "sqlite3", "~> 1.3.0"
gem "appraisal"
gem "standard", "~>1.0"
gem "pry-byebug"
gem "yard"
gem "rake"
gem "rspec", "~> 3.11"
gem "activerecord", "~> 5.0"

gemspec path: "../"
8 changes: 2 additions & 6 deletions gemfiles/activerecord_6.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@

source "https://rubygems.org"

gem "rspec", "~> 3.0"
gem "rake"
gem "sqlite3", "~> 1.4.0"
gem "appraisal"
gem "standard", "~>1.0"
gem "pry-byebug"
gem "yard"
gem "rake"
gem "rspec", "~> 3.11"
gem "activerecord", "~> 6.0"
gem "concurrent-ruby", "1.3.4"

Expand Down
8 changes: 2 additions & 6 deletions gemfiles/activerecord_7.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@

source "https://rubygems.org"

gem "rspec", "~> 3.0"
gem "rake"
gem "sqlite3", "~> 1.4.0"
gem "appraisal"
gem "standard", "~>1.0"
gem "pry-byebug"
gem "yard"
gem "rake"
gem "rspec", "~> 3.11"
gem "activerecord", "~> 7.0"

gemspec path: "../"
8 changes: 2 additions & 6 deletions gemfiles/activerecord_8.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@

source "https://rubygems.org"

gem "rspec", "~> 3.0"
gem "rake"
gem "sqlite3", "~> 2.5.0"
gem "appraisal"
gem "standard", "~>1.0"
gem "pry-byebug"
gem "yard"
gem "rake"
gem "rspec", "~> 3.11"
gem "activerecord", "~> 8.0"

gemspec path: "../"
Loading
Loading