Skip to content
Merged
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
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ There are three 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. This will overwrite any value set in `config/solid_cache.yml`
- `size_estimate_samples` - if `max_size` is set on the cache, the number of the samples used to estimates the size
- `size_estimate_samples` - if `max_size` is set on the cache, the number of the samples used to estimate the size.
- `encrypted` - whether cache values should be encrypted (see [Enabling encryption](#enabling-encryption))
- `encryption_context_properties` - custom encryption context properties

These can be set in your Rails configuration:

Expand Down Expand Up @@ -239,14 +241,42 @@ production:

### Enabling encryption

Add this to an initializer:
To encrypt the cache values, you can add set the encrypt property.

```yaml
# config/solid_cache.yml
production:
encrypt: true
```
or
```ruby
ActiveSupport.on_load(:solid_cache_entry) do
encrypts :value
end
# application.rb
config.solid_cache.encrypt = true
```

You will need to set up your application to (use Active Record Encryption)[https://guides.rubyonrails.org/active_record_encryption.html].

Solid Cache by default uses a custom encryptor and message serializer that are optimised for it.

Firstly it disabled compression with the encryptor `ActiveRecord::Encryption::Encryptor.new(compress: false)` - the cache already compresses the data.
Secondly it uses `ActiveRecord::Encryption::MessagePackMessageSerializer.new` as the serializer. This serializer can only be used for binary columns,
but can store about 40% more data than the standard serializer.

You can choose your own context properties instead if you prefer:

```ruby
# application.rb
config.solid_cache.encryption_context_properties = {
encryptor: ActiveRecord::Encryption::Encryptor.new,
message_serializer: ActiveRecord::Encryption::MessageSerializer.new
}
```

**Note**

Encryption currently does not work for PostgreSQL, as Rails does not yet support encrypting binary columns for it.
See https://github.com/rails/rails/pull/52650.

### Index size limits
The Solid Cache migrations try to create an index with 1024 byte entries. If that is too big for your database, you should:

Expand Down
13 changes: 11 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ def run_without_aborting(*tasks)

tasks.each do |task|
Rake::Task[task].invoke
rescue Exception
rescue Exception => e
puts e.message
puts e.backtrace
errors << task
end

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

def configs
[ :default, :connects_to, :database, :no_database, :shards, :unprepared_statements ]
[ :default, :connects_to, :database, :encrypted, :encrypted_custom, :no_database, :shards, :unprepared_statements ]
end

task :test do
Expand All @@ -34,6 +36,11 @@ end
configs.each do |config|
namespace :test do
task config do
if config.to_s.start_with?("encrypted") && ENV["TARGET_DB"] == "postgres"
puts "Skipping encrypted tests on PostgreSQL as binary encrypted columns are not supported by Rails yet"
next
end

if config == :default
sh("bin/rails test")
else
Expand All @@ -42,3 +49,5 @@ configs.each do |config|
end
end
end

task default: [:test]
16 changes: 14 additions & 2 deletions app/models/solid_cache/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

module SolidCache
class Entry < Record
include Expiration, Size
include Encryption, Expiration, Size

# The estimated cost of an extra row in bytes, including fixed size columns, overhead, indexes and free space
# Based on experimentation on SQLite, MySQL and Postgresql.
# A bit high for SQLite (more like 90 bytes), but about right for MySQL/Postgresql.
ESTIMATED_ROW_OVERHEAD = 140

# Assuming MessagePack serialization
ESTIMATED_ENCRYPTION_OVERHEAD = 170

KEY_HASH_ID_RANGE = -(2**63)..(2**63 - 1)

class << self
Expand Down Expand Up @@ -99,7 +103,15 @@ def key_hashes_for(keys)
end

def byte_size_for(payload)
payload[:key].to_s.bytesize + payload[:value].to_s.bytesize + ESTIMATED_ROW_OVERHEAD
payload[:key].to_s.bytesize + payload[:value].to_s.bytesize + estimated_row_overhead
end

def estimated_row_overhead
if SolidCache.configuration.encrypt?
ESTIMATED_ROW_OVERHEAD + ESTIMATED_ENCRYPTION_OVERHEAD
else
ESTIMATED_ROW_OVERHEAD
end
end

def without_query_cache(&block)
Expand Down
15 changes: 15 additions & 0 deletions app/models/solid_cache/entry/encryption.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module SolidCache
class Entry
module Encryption
extend ActiveSupport::Concern

included do
if SolidCache.configuration.encrypt?
encrypts :value, **SolidCache.configuration.encryption_context_properties
end
end
end
end
end
35 changes: 28 additions & 7 deletions gemfiles/rails_main.gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
GIT
remote: https://github.com/rails/rails.git
revision: d7f93473682d9b64ab268a6736d5af2853e97ce2
revision: abc4538300e8a2897f350611666d7e64021e6d16
branch: main
specs:
actionpack (8.0.0.alpha)
Expand Down Expand Up @@ -95,8 +95,14 @@ GEM
ruby2_keywords (>= 0.0.5)
msgpack (1.7.2)
mysql2 (0.5.6)
nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.7-arm-linux)
racc (~> 1.4)
nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86-linux)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
Expand Down Expand Up @@ -171,9 +177,16 @@ GEM
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
sqlite3 (2.0.3-aarch64-linux-gnu)
sqlite3 (2.0.3-aarch64-linux-musl)
sqlite3 (2.0.3-arm-linux-gnu)
sqlite3 (2.0.3-arm-linux-musl)
sqlite3 (2.0.3-arm64-darwin)
sqlite3 (2.0.3-x86-linux-gnu)
sqlite3 (2.0.3-x86-linux-musl)
sqlite3 (2.0.3-x86_64-darwin)
sqlite3 (2.0.3-x86_64-linux-gnu)
sqlite3 (2.0.3-x86_64-linux-musl)
stringio (3.1.1)
strscan (3.1.0)
thor (1.3.1)
Expand All @@ -186,12 +199,20 @@ GEM
zeitwerk (2.6.17)

PLATFORMS
arm64-darwin-21
arm64-darwin-22
arm64-darwin-23
x86_64-darwin-20
x86_64-darwin-22
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
arm-linux
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86-linux
x86-linux-gnu
x86-linux-musl
x86_64-darwin
x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl

DEPENDENCIES
appraisal
Expand All @@ -212,4 +233,4 @@ DEPENDENCIES
sqlite3

BUNDLED WITH
2.5.9
2.5.17
22 changes: 20 additions & 2 deletions lib/solid_cache/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

module SolidCache
class Configuration
attr_reader :store_options, :connects_to, :executor, :size_estimate_samples
attr_reader :store_options, :connects_to, :executor, :size_estimate_samples, :encrypt, :encryption_context_properties

def initialize(store_options: {}, database: nil, databases: nil, connects_to: nil, executor: nil, size_estimate_samples: 10_000)
def initialize(store_options: {}, database: nil, databases: nil, connects_to: nil, executor: nil, encrypt: false, encryption_context_properties: nil, size_estimate_samples: 10_000)
@store_options = store_options
@size_estimate_samples = size_estimate_samples
@executor = executor
@encrypt = encrypt
@encryption_context_properties = encryption_context_properties
@encryption_context_properties ||= default_encryption_context_properties if encrypt?
set_connects_to(database: database, databases: databases, connects_to: connects_to)
end

Expand All @@ -19,6 +22,10 @@ def shard_keys
sharded? ? connects_to[:shards].keys : []
end

def encrypt?
encrypt.present?
end

private
def set_connects_to(database:, databases:, connects_to:)
if [database, databases, connects_to].compact.size > 1
Expand All @@ -37,5 +44,16 @@ def set_connects_to(database:, databases:, connects_to:)
nil
end
end

def default_encryption_context_properties
require "active_record/encryption/message_pack_message_serializer"

{
# No need to compress, the cache does that already
encryptor: ActiveRecord::Encryption::Encryptor.new(compress: false),
# Binary column only serializer that is 40% more efficient than the default MessageSerializer
message_serializer: ActiveRecord::Encryption::MessagePackMessageSerializer.new
}
end
end
end
10 changes: 10 additions & 0 deletions lib/solid_cache/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class Engine < ::Rails::Engine

options[:connects_to] = config.solid_cache.connects_to if config.solid_cache.connects_to
options[:size_estimate_samples] = config.solid_cache.size_estimate_samples if config.solid_cache.size_estimate_samples
options[:encrypt] = config.solid_cache.encrypt if config.solid_cache.encrypt
options[:encryption_context_properties] = config.solid_cache.encryption_context_properties if config.solid_cache.encryption_context_properties

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

Expand All @@ -33,5 +35,13 @@ class Engine < ::Rails::Engine
config.after_initialize do
Rails.cache.setup! if Rails.cache.is_a?(Store)
end

config.after_initialize do
if SolidCache.configuration.encrypt? && SolidCache::Record.connection.adapter_name == "PostgreSQL"
raise \
"Cannot enable encryption for Solid Cache: Active Record Encryption does not currently support " \
"encrypting binary columns on PostgreSQL"
end
end
end
end
15 changes: 8 additions & 7 deletions test/dummy/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ class Application < Rails::Application

config.cache_store = :solid_cache_store

# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
config.active_record.encryption.key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new

if ENV["SOLID_CACHE_CONFIG"] == "config/solid_cache_encrypted_custom.yml"
config.solid_cache.encryption_context_properties = {
encryptor: ActiveRecord::Encryption::Encryptor.new,
message_serializer: ActiveRecord::Encryption::MessageSerializer.new
}
end

initializer :custom_solid_cache_yml, before: :solid_cache do |app|
app.paths.add "config/solid_cache", with: ENV["SOLID_CACHE_CONFIG_PATH"]
Expand Down
1 change: 1 addition & 0 deletions test/dummy/config/credentials.yml.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KXs4HRH7tZqQZmVrP872Vy2OLbDJ3OoirCMzMrsRxDqraDWMLPB+XdNfVD9RhEDiKw05LNXPAGCIVi82dY/I4TJA2B8tz0cEX0DrfBphZ4Azo0gxjVva5xxfMcaQxa8Zv5lNtn+MXgXVOnO8FedKhrBxNFManCxaaBjZKlui9UTS/y3DeLvPjgFhSgZ6YcPQVoAU3kI08liXWUsleDFBEbaw8vJ0lKT20i5iKi/VgOyGBSju/sHW6EYUsQO6peOD8lWackcficA4I6tQ04Ce4GeYIP7KkA/tlTCwkC3oaS9Km+04J3zGfUS/9zh9apHw+C0zuoAr59+dmYNuKg3DhuTWjbI7iXjViUzGbnHRnv+qJXON4Nb3wwAjsmNgPGPqrObruzaQ10kuRCFnMgyy2GbXq/DJlZ2ZScgNItd/Z1Gcw0K0RXw6LDV+pkg8CnTYCfNJfA5NAbD/GNFv8NC9N5sJqDJoEhUfVwncbcUfX13HNqiAvO2XkxdIMzlMOVQijkT2CltmhVWYB1SRCLQv6kbd+iu/sSGcu4ropsVcxgonKkch5JsSjIXwEWCOpaDVg3Jm8RiJWP+LwjhfEC5/OSHOmFGKfIwHGSmLNvazgfx6odHnqW3ZPyTewcgYhK83ok1BS4GwX1UjDs1ZqXD3DX/W63pknqdx--lfZoS+MYEht37XUa--ICZ1nYvIPt+EI+k2Ti4pTA==
1 change: 1 addition & 0 deletions test/dummy/config/master.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
068c2c12595c52b8593721734c34dfed
13 changes: 13 additions & 0 deletions test/dummy/config/solid_cache_encrypted.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
default: &default
databases: [primary_shard_one, primary_shard_two, secondary_shard_one, secondary_shard_two]
encrypt: true

store_options:
max_age: 3600
max_size:

development:
<<: *default

test:
<<: *default
13 changes: 13 additions & 0 deletions test/dummy/config/solid_cache_encrypted_custom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
default: &default
databases: [primary_shard_one, primary_shard_two, secondary_shard_one, secondary_shard_two]
encrypt: true

store_options:
max_age: 3600
max_size:

development:
<<: *default

test:
<<: *default
Loading