Skip to content

Commit

Permalink
Merge pull request #144 from rails/solid-cache-yml-config
Browse files Browse the repository at this point in the history
Solid cache yml config
  • Loading branch information
djmb authored Feb 2, 2024
2 parents beea92e + c34e3f3 commit ecda9ea
Show file tree
Hide file tree
Showing 32 changed files with 669 additions and 377 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,4 @@ jobs:
sleep 2
bin/rails db:setup
- name: Run tests
run: bin/rails test
- name: Run tests (no connects-to)
run: NO_CONNECTS_TO=true bin/rails test
run: bundle exec rake test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/tmp/
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-*
/test/dummy/log/*.log
/test/dummy/log/
/test/dummy/storage/
/test/dummy/tmp/
/test/fixtures/tmp
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,57 @@ $ bin/rails db:migrate

### Configuration

#### solid_cache.yml

Configuration will be read from solid_cache.yml. You can change the location of the config file by setting the `SOLID_CACHE_CONFIG` env variable.

The format of the file is:

```yml
default:
store_options: &default_store_options
max_age: <%= 60.days.to_i %>
namespace: <%= Rails.env %>

development: &production
database: development_cache
store_options:
<<: *default_store_options
max_entries: 1_000_000

production: &production
databases: [production_cache1, production_cache2]
store_options:
<<: *default_store_options
max_entries: 10_000_000
```
For the full list of store_options see [Cache configuration](#cache_configuration). Any options passed to the cache lookup will overwrite those specified here.
#### Connection configuration
You can set one of `database`, `databases` and `connects_to` in the config file. They will be used to configure the cache databases in `SolidCache::Record#connects_to`.

Setting `database` to `cache_db` will configure with:

```ruby
SolidCache::Record.connects_to database: { writing: :cache_db }
```

Setting `databases` to `[cache_db, cache_db2]` is the equivalent of:

```ruby
SolidCache::Record.connects_to shards: { cache_db1: { writing: :cache_db1 }, cache_db2: { writing: :cache_db2 } }
```

If `connects_to` is set it will be passed directly.

#### Engine configuration

There are two options that can be set on the engine:

- `executor` - the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
- `connects_to` - a custom connects to value for the abstract `SolidCache::Record` active record model. Required for sharding and/or using a separate cache database to the main app.
- `connects_to` - a custom connects to value for the abstract `SolidCache::Record` active record model. Required for sharding and/or using a separate cache database to the main app. This will overwrite any value set in `config/solid_cache.yml`

These can be set in your Rails configuration:

Expand Down
34 changes: 34 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,37 @@ load "rails/tasks/engine.rake"
load "rails/tasks/statistics.rake"

require "bundler/gem_tasks"
require "rake/testtask"

def run_without_aborting(*tasks)
errors = []

tasks.each do |task|
Rake::Task[task].invoke
rescue Exception
errors << task
end

abort "Errors running #{errors.join(', ')}" if errors.any?
end

def configs
[ :default, :cluster, :clusters, :clusters_named, :database, :no_database ]
end

task :test do
tasks = configs.map { |config| "test:#{config}" }
run_without_aborting(*tasks)
end

configs.each do |config|
namespace :test do
task config do
if config == :default
sh("bin/rails test")
else
sh("SOLID_CACHE_CONFIG=config/solid_cache_#{config}.yml bin/rails test")
end
end
end
end
6 changes: 3 additions & 3 deletions app/models/solid_cache/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Entry < Record
VALUE_BYTE_SIZE = 4
FIXED_SIZE_COLUMNS_BYTE_SIZE = ID_BYTE_SIZE + CREATED_AT_BYTE_SIZE + KEY_HASH_BYTE_SIZE + VALUE_BYTE_SIZE

self.ignored_columns += [ :key_hash, :byte_size] if SolidCache.key_hash_stage == :ignored
self.ignored_columns += [ :key_hash, :byte_size] if SolidCache.configuration.key_hash_stage == :ignored

class << self
def write(key, value)
Expand Down Expand Up @@ -94,12 +94,12 @@ def add_key_hash_and_byte_size(payloads)
end

def key_hash?
@key_hash ||= [ :indexed, :unindexed ].include?(SolidCache.key_hash_stage) &&
@key_hash ||= [ :indexed, :unindexed ].include?(SolidCache.configuration.key_hash_stage) &&
connection.column_exists?(table_name, :key_hash)
end

def key_hash_indexed?
key_hash? && SolidCache.key_hash_stage == :indexed
key_hash? && SolidCache.configuration.key_hash_stage == :indexed
end

def lookup_column
Expand Down
16 changes: 14 additions & 2 deletions app/models/solid_cache/record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,32 @@ class Record < ActiveRecord::Base

self.abstract_class = true

connects_to(**SolidCache.connects_to) if SolidCache.connects_to
connects_to(**SolidCache.configuration.connects_to) if SolidCache.configuration.connects_to

class << self
def disable_instrumentation(&block)
connection.with_instrumenter(NULL_INSTRUMENTER, &block)
end

def with_shard(shard, &block)
if shard && SolidCache.connects_to
if shard && SolidCache.configuration.sharded?
connected_to(shard: shard, role: default_role, prevent_writes: false, &block)
else
block.call
end
end

def each_shard(&block)
return to_enum(:each_shard) unless block_given?

if SolidCache.configuration.sharded?
SolidCache.configuration.shard_keys.each do |shard|
Record.with_shard(shard, &block)
end
else
yield
end
end
end
end
end
Expand Down
24 changes: 2 additions & 22 deletions lib/solid_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,8 @@
loader.setup

module SolidCache
mattr_accessor :executor, :connects_to
mattr_accessor :key_hash_stage, default: :indexed

def self.all_shard_keys
all_shards_config&.keys || []
end

def self.all_shards_config
connects_to && connects_to[:shards]
end

def self.each_shard(&block)
return to_enum(:each_shard) unless block_given?

if (shards = all_shards_config&.keys)
shards.each do |shard|
Record.with_shard(shard, &block)
end
else
yield
end
end
mattr_accessor :executor
mattr_accessor :configuration, default: Configuration.new
end

loader.eager_load
3 changes: 3 additions & 0 deletions lib/solid_cache/cluster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ module SolidCache
class Cluster
include Connections, Execution, Expiry, Stats

attr_reader :error_handler

def initialize(options = {})
@error_handler = options[:error_handler]
super(options)
end

Expand Down
10 changes: 6 additions & 4 deletions lib/solid_cache/cluster/connections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module SolidCache
class Cluster
module Connections
attr_reader :shard_options

def initialize(options = {})
super(options)
@shard_options = options.fetch(:shards, nil)
Expand Down Expand Up @@ -40,14 +42,14 @@ def connection_names
connections.names
end

def connections
@connections ||= SolidCache::Connections.from_config(@shard_options)
end

private
def setup!
connections
end

def connections
@connections ||= SolidCache::Connections.from_config(@shard_options)
end
end
end
end
2 changes: 2 additions & 0 deletions lib/solid_cache/cluster/execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ def async(&block)
instrument(&block)
end
end
rescue Exception => exception
error_handler&.call(method: :async, exception: exception, returning: nil)
end
end

Expand Down
41 changes: 41 additions & 0 deletions lib/solid_cache/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module SolidCache
class Configuration
attr_reader :store_options, :connects_to, :key_hash_stage, :executor

def initialize(store_options: {}, database: nil, databases: nil, connects_to: nil, key_hash_stage: :indexed, executor: nil)
@store_options = store_options
@key_hash_stage = key_hash_stage
@executor = executor
set_connects_to(database: database, databases: databases, connects_to: connects_to)
end

def sharded?
connects_to && connects_to[:shards]
end

def shard_keys
sharded? ? connects_to[:shards].keys : []
end

private
def set_connects_to(database:, databases:, connects_to:)
if [database, databases, connects_to].compact.size > 1
raise ArgumentError, "You can only specify one of :database, :databases, or :connects_to"
end

@connects_to =
case
when database
{ database: { writing: database.to_sym } }
when databases
{ shards: databases.map(&:to_sym).index_with { |database| { writing: database } } }
when connects_to
connects_to
else
nil
end
end
end
end
12 changes: 6 additions & 6 deletions lib/solid_cache/connections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
module SolidCache
module Connections
def self.from_config(options)
if options.present? || SolidCache.all_shards_config.present?
if options.present? || SolidCache.configuration.sharded?
case options
when NilClass
names = SolidCache.all_shard_keys
names = SolidCache.configuration.shard_keys
nodes = names.to_h { |name| [ name, name ] }
when Array
names = options
names = options.map(&:to_sym)
nodes = names.to_h { |name| [ name, name ] }
when Hash
names = options.keys
nodes = options.invert
names = options.keys.map(&:to_sym)
nodes = options.to_h { |names, nodes| [ nodes.to_sym, names.to_sym ] }
end

if (unknown_shards = names - SolidCache.all_shard_keys).any?
if (unknown_shards = names - SolidCache.configuration.shard_keys).any?
raise ArgumentError, "Unknown #{"shard".pluralize(unknown_shards)}: #{unknown_shards.join(", ")}"
end

Expand Down
22 changes: 12 additions & 10 deletions lib/solid_cache/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ class Engine < ::Rails::Engine

config.solid_cache = ActiveSupport::OrderedOptions.new

initializer "solid_cache", before: :run_prepare_callbacks do |app|
config.solid_cache.executor ||= app.executor

SolidCache.executor = config.solid_cache.executor
SolidCache.connects_to = config.solid_cache.connects_to
if config.solid_cache.key_hash_stage
unless [:ignored, :unindexed, :indexed].include?(config.solid_cache.key_hash_stage)
raise "ArgumentError, :key_hash_stage must be :ignored, :unindexed or :indexed"
end
SolidCache.key_hash_stage = config.solid_cache.key_hash_stage
initializer "solid_cache.config" do |app|
app.paths.add "config/solid_cache", with: ENV["SOLID_CACHE_CONFIG"] || "config/solid_cache.yml"

if (config_path = Pathname.new(app.config.paths["config/solid_cache"].first)).exist?
options = app.config_for(config_path).to_h.deep_symbolize_keys
options[:connects_to] = config.solid_cache.connects_to if config.solid_cache.connects_to

SolidCache.configuration = SolidCache::Configuration.new(**options)
end
end

initializer "solid_cache.app_executor", before: :run_prepare_callbacks do |app|
SolidCache.executor = config.solid_cache.executor || app.executor
end

config.after_initialize do
Rails.cache.setup! if Rails.cache.is_a?(Store)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/solid_cache/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class Store < ActiveSupport::Cache::Store
include Api, Clusters, Entries, Failsafe
prepend ActiveSupport::Cache::Strategy::LocalCache

def initialize(options = {})
super(SolidCache.configuration.store_options.merge(options))
end

def self.supports_cache_versioning?
true
end
Expand Down
2 changes: 1 addition & 1 deletion lib/solid_cache/store/clusters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def initialize(options = {})
clusters_options = options.fetch(:clusters) { [ options.fetch(:cluster, {}) ] }

@clusters = clusters_options.map.with_index do |cluster_options, index|
Cluster.new(options.merge(cluster_options).merge(async_writes: index != 0))
Cluster.new(options.merge(cluster_options).merge(async_writes: index != 0, error_handler: error_handler))
end

@primary_cluster = clusters.first
Expand Down
12 changes: 2 additions & 10 deletions test/dummy/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,8 @@ class Application < Rails::Application
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")

unless ENV["NO_CONNECTS_TO"]
config.solid_cache.connects_to = {
shards: {
default: { writing: :primary, reading: :primary_replica },
primary_shard_one: { writing: :primary_shard_one },
primary_shard_two: { writing: :primary_shard_two },
secondary_shard_one: { writing: :secondary_shard_one },
secondary_shard_two: { writing: :secondary_shard_two }
}
}
initializer :custom_solid_cache_yml, before: :solid_cache do |app|
app.paths.add "config/solid_cache", with: ENV["SOLID_CACHE_CONFIG_PATH"]
end
end
end
Loading

0 comments on commit ecda9ea

Please sign in to comment.