solid_cache 0.6.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -106
  3. data/Rakefile +11 -2
  4. data/app/models/solid_cache/entry/encryption.rb +15 -0
  5. data/app/models/solid_cache/entry/size/estimate.rb +1 -1
  6. data/app/models/solid_cache/entry/size/moving_average_estimate.rb +2 -2
  7. data/app/models/solid_cache/entry.rb +47 -97
  8. data/db/migrate/20240820123641_create_solid_cache_entries.rb +29 -0
  9. data/lib/generators/solid_cache/install/templates/config/solid_cache.yml.tt +1 -1
  10. data/lib/solid_cache/configuration.rb +20 -2
  11. data/lib/solid_cache/connections/sharded.rb +3 -4
  12. data/lib/solid_cache/connections.rb +1 -6
  13. data/lib/solid_cache/engine.rb +10 -0
  14. data/lib/solid_cache/store/api.rb +17 -6
  15. data/lib/solid_cache/store/connections.rb +108 -0
  16. data/lib/solid_cache/store/entries.rb +9 -7
  17. data/lib/solid_cache/{cluster → store}/execution.rb +4 -4
  18. data/lib/solid_cache/{cluster → store}/expiry.rb +1 -1
  19. data/lib/solid_cache/{cluster → store}/stats.rb +2 -2
  20. data/lib/solid_cache/store.rb +1 -5
  21. data/lib/solid_cache/version.rb +1 -1
  22. metadata +15 -19
  23. data/db/migrate/20230724121448_create_solid_cache_entries.rb +0 -11
  24. data/db/migrate/20240108155507_add_key_hash_and_byte_size_to_solid_cache_entries.rb +0 -8
  25. data/db/migrate/20240110111600_add_key_hash_and_byte_size_indexes_and_null_constraints_to_solid_cache_entries.rb +0 -11
  26. data/db/migrate/20240110111702_remove_key_index_from_solid_cache_entries.rb +0 -7
  27. data/lib/solid_cache/cluster/connections.rb +0 -55
  28. data/lib/solid_cache/cluster.rb +0 -18
  29. data/lib/solid_cache/store/clusters.rb +0 -83
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ba0486907f0e4656593843d1cca1c7ec35763772ff6d5a2a4e43b9d43b023b3
4
- data.tar.gz: a93991c212dcb6d0a1dedb20e2d8d85e0664092c2342db895489beafb9f17e23
3
+ metadata.gz: 74f6b8bdfea57b8ffbe60b8f525b3907c7278f4b73d07fad2506a41b0884a14c
4
+ data.tar.gz: b3af9a6526d97bcb9b5ac1fd313b9c9cf76551ae75e214479a10fe9b04d678d3
5
5
  SHA512:
6
- metadata.gz: 9897aef78db43aff6bceea922aa43669f76a48852d4dc94e64ce8aa198c1ca7fd04152a15033760a6fecad11245b650369342345c6f07e366c3d3d4e0a71f1da
7
- data.tar.gz: cd59a068b761fd6060005d9ef78328cd03a3420ee539c07a6afd14eed072a0e9a092d135fbadeb5a58d38aa5a146e8481286b50ef411548a15b701464c0394b0
6
+ metadata.gz: b6503fe5c3f9d3158ec64bc054465e48f1479236b05fd3b023f4c7050b8c2c0019a546cae0c494a91da0c7b3e9abf3cc3f8f700b2d6c9853a3832b8a726d750a
7
+ data.tar.gz: 38eec70ef2680b811f9f3baf42e02d5d81d72947c508f45a4ca0cd8d2c1d6d761280e5ec15261c4ca3b94fbdfe20dd1cf96a2cf871255791b41a2d11cfe8aa29
data/README.md CHANGED
@@ -4,25 +4,19 @@
4
4
 
5
5
  Solid Cache is a database-backed Active Support cache store implementation.
6
6
 
7
- Using SQL databases backed by SSDs we can have caches that are much larger and cheaper than traditional memory only Redis or Memcached backed caches.
7
+ Using SQL databases backed by SSDs we can have caches that are much larger and cheaper than traditional memory-only Redis or Memcached backed caches.
8
8
 
9
- ## Usage
9
+ ## Introduction
10
10
 
11
- To set Solid Cache as your Rails cache, you should add this to your environment config:
12
-
13
- ```ruby
14
- config.cache_store = :solid_cache_store
15
- ```
16
-
17
- Solid Cache is a FIFO (first in, first out) cache. While this is not as efficient as an LRU cache, this is mitigated by the longer cache lifespan.
11
+ Solid Cache is a FIFO (first in, first out) cache. While this is not as efficient as an LRU cache, it is mitigated by the longer cache lifespan.
18
12
 
19
13
  A FIFO cache is much easier to manage:
20
- 1. We don't need to track when items are read
14
+ 1. We don't need to track when items are read.
21
15
  2. We can estimate and control the cache size by comparing the maximum and minimum IDs.
22
16
  3. By deleting from one end of the table and adding at the other end we can avoid fragmentation (on MySQL at least).
23
17
 
24
- ### Installation
25
- Add this line to your application's Gemfile:
18
+ ## Installation
19
+ Add this line to your application's `Gemfile`:
26
20
 
27
21
  ```ruby
28
22
  gem "solid_cache"
@@ -38,13 +32,69 @@ Or install it yourself as:
38
32
  $ gem install solid_cache
39
33
  ```
40
34
 
41
- Add the migration to your app:
35
+ #### Cache database configuration
36
+
37
+ The default installation of Solid Cache expects a database named `cache` in `database.yml`. It should
38
+ have it's own connection pool to avoid mixing cache queries in other transactions.
39
+
40
+ You can use the primary database for your cache like this:
41
+
42
+ ```yaml
43
+ # config/database.yml
44
+ production:
45
+ primary: &production_primary
46
+ ...
47
+ cache:
48
+ <<: *production_primary
49
+ ```
50
+
51
+ Or a separate database like this:
52
+
53
+ ```yaml
54
+ production:
55
+ primary:
56
+ ...
57
+ cache:
58
+ database: cache_development
59
+ host: 127.0.0.1
60
+ migrations_paths: "db/cache/migrate"
61
+ ```
62
+
63
+ #### Install Solid Cache
64
+
65
+ Now, you need to install the necessary migrations and configure the cache store. You can do both at once using the provided generator:
66
+
67
+ ```bash
68
+ # If using the primary database
69
+ $ bin/rails generate solid_cache:install
70
+
71
+ # Or if using a dedicated database
72
+ $ DATABASE=cache bin/rails generate solid_cache:install
73
+ ```
74
+
75
+ This will set solid_cache as the cache store in production, and will copy the optional configuration file and the required migration over to your app.
76
+
77
+ Alternatively, you can add only the migration to your app:
42
78
 
43
79
  ```bash
44
- $ bin/rails solid_cache:install:migrations
80
+ # If using the primary database
81
+ $ bin/rails generate solid_cache:install:migrations
82
+
83
+ # Or if using a dedicated database
84
+ $ DATABASE=cache bin/rails generate solid_cache:install:migrations
45
85
  ```
46
86
 
47
- Then run it:
87
+ And set Solid Cache as your application's cache store backend manually, in your environment config:
88
+
89
+ ```ruby
90
+ # config/environments/production.rb
91
+ config.cache_store = :solid_cache_store
92
+ ```
93
+
94
+ #### Run migrations
95
+
96
+ Finally, you need to run the migrations:
97
+
48
98
  ```bash
49
99
  $ bin/rails db:migrate
50
100
  ```
@@ -93,9 +143,9 @@ Setting `databases` to `[cache_db, cache_db2]` is the equivalent of:
93
143
  SolidCache::Record.connects_to shards: { cache_db1: { writing: :cache_db1 }, cache_db2: { writing: :cache_db2 } }
94
144
  ```
95
145
 
96
- If `connects_to` is set it will be passed directly.
146
+ If `connects_to` is set, it will be passed directly.
97
147
 
98
- If none of these are set, then Solid Cache will use the `ActiveRecord::Base` connection pool. This means that cache reads and writes will be part of any wrapping
148
+ If none of these are set, Solid Cache will use the `ActiveRecord::Base` connection pool. This means that cache reads and writes will be part of any wrapping
99
149
  database transaction.
100
150
 
101
151
  #### Engine configuration
@@ -104,7 +154,9 @@ There are three options that can be set on the engine:
104
154
 
105
155
  - `executor` - the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
106
156
  - `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`
107
- - `size_estimate_samples` - if `max_size` is set on the cache, the number of the samples used to estimates the size.
157
+ - `size_estimate_samples` - if `max_size` is set on the cache, the number of the samples used to estimate the size.
158
+ - `encrypted` - whether cache values should be encrypted (see [Enabling encryption](#enabling-encryption))
159
+ - `encryption_context_properties` - custom encryption context properties
108
160
 
109
161
  These can be set in your Rails configuration:
110
162
 
@@ -116,7 +168,7 @@ end
116
168
 
117
169
  #### Cache configuration
118
170
 
119
- Solid Cache supports these options in addition to the standard `ActiveSupport::Cache::Store` options.
171
+ Solid Cache supports these options in addition to the standard `ActiveSupport::Cache::Store` options:
120
172
 
121
173
  - `error_handler` - a Proc to call to handle any `ActiveRecord::ActiveRecordError`s that are raises (default: log errors as warnings)
122
174
  - `expiry_batch_size` - the batch size to use when deleting old records (default: `100`)
@@ -125,79 +177,40 @@ Solid Cache supports these options in addition to the standard `ActiveSupport::C
125
177
  - `max_age` - the maximum age of entries in the cache (default: `2.weeks.to_i`). Can be set to `nil`, but this is not recommended unless using `max_entries` to limit the size of the cache.
126
178
  - `max_entries` - the maximum number of entries allowed in the cache (default: `nil`, meaning no limit)
127
179
  - `max_size` - the maximum size of the cache entries (default `nil`, meaning no limit)
128
- - `cluster` - a Hash of options for the cache database cluster, e.g `{ shards: [:database1, :database2, :database3] }`
129
- - `clusters` - and Array of Hashes for multiple cache clusters (ignored if `:cluster` is set)
180
+ - `cluster` - (deprecated) a Hash of options for the cache database cluster, e.g `{ shards: [:database1, :database2, :database3] }`
181
+ - `clusters` - (deprecated) an Array of Hashes for multiple cache clusters (ignored if `:cluster` is set)
182
+ - `shards` - an Array of databases
130
183
  - `active_record_instrumentation` - whether to instrument the cache's queries (default: `true`)
131
184
  - `clear_with` - clear the cache with `:truncate` or `:delete` (default `truncate`, except for when `Rails.env.test?` then `delete`)
132
185
  - `max_key_bytesize` - the maximum size of a normalized key in bytes (default `1024`)
133
186
 
134
- For more information on cache clusters see [Sharding the cache](#sharding-the-cache)
187
+ For more information on cache clusters, see [Sharding the cache](#sharding-the-cache)
135
188
 
136
189
  ### Cache expiry
137
190
 
138
191
  Solid Cache tracks writes to the cache. For every write it increments a counter by 1. Once the counter reaches 50% of the `expiry_batch_size` it adds a task to run on a background thread. That task will:
139
192
 
140
- 1. Check if we have exceeded the `max_entries` or `max_size` values (if set)
193
+ 1. Check if we have exceeded the `max_entries` or `max_size` values (if set).
141
194
  The current entries are estimated by subtracting the max and min IDs from the `SolidCache::Entry` table.
142
195
  The current size is estimated by sampling the entry `byte_size` columns.
143
- 2. If we have it will delete `expiry_batch_size` entries
144
- 3. If not it will delete up to `expiry_batch_size` entries, provided they are all older than `max_age`.
196
+ 2. If we have, it will delete `expiry_batch_size` entries.
197
+ 3. If not, it will delete up to `expiry_batch_size` entries, provided they are all older than `max_age`.
145
198
 
146
199
  Expiring when we reach 50% of the batch size allows us to expire records from the cache faster than we write to it when we need to reduce the cache size.
147
200
 
148
- Only triggering expiry when we write means that the if the cache is idle, the background thread is also idle.
201
+ Only triggering expiry when we write means that if the cache is idle, the background thread is also idle.
149
202
 
150
203
  If you want the cache expiry to be run in a background job instead of a thread, you can set `expiry_method` to `:job`. This will enqueue a `SolidCache::ExpiryJob`.
151
204
 
152
- ### Using a dedicated cache database
153
-
154
- Add database configuration to database.yml, e.g.:
155
-
156
- ```
157
- development:
158
- cache:
159
- database: cache_development
160
- host: 127.0.0.1
161
- migrations_paths: "db/cache/migrate"
162
- ```
163
-
164
- Create database:
165
- ```
166
- $ bin/rails db:create
167
- ```
168
-
169
- Install migrations:
170
- ```
171
- $ bin/rails solid_cache:install:migrations
172
- ```
173
-
174
- Move migrations to custom migrations folder:
175
- ```
176
- $ mkdir -p db/cache/migrate
177
- $ mv db/migrate/*.solid_cache.rb db/cache/migrate
178
- ```
179
-
180
- Set the engine configuration to point to the new database:
181
- ```yaml
182
- # config/solid_cache.yml
183
- production:
184
- database: cache
185
- ```
186
-
187
- Run migrations:
188
- ```
189
- $ bin/rails db:migrate
190
- ```
191
-
192
205
  ### Sharding the cache
193
206
 
194
207
  Solid Cache uses the [Maglev](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf) consistent hashing scheme to shard the cache across multiple databases.
195
208
 
196
209
  To shard:
197
210
 
198
- 1. Add the configuration for the database shards to database.yml
199
- 2. Configure the shards via `config.solid_cache.connects_to`
200
- 3. Pass the shards for the cache to use via the cluster option
211
+ 1. Add the configuration for the database shards to database.yml.
212
+ 2. Configure the shards via `config.solid_cache.connects_to`.
213
+ 3. Pass the shards for the cache to use via the cluster option.
201
214
 
202
215
  For example:
203
216
  ```yml
@@ -220,58 +233,49 @@ production:
220
233
  databases: [cache_shard1, cache_shard2, cache_shard3]
221
234
  ```
222
235
 
223
- ### Secondary cache clusters
224
-
225
- You can add secondary cache clusters. Reads will only be sent to the primary cluster (i.e. the first one listed).
226
-
227
- Writes will go to all clusters. The writes to the primary cluster are synchronous, but asynchronous to the secondary clusters.
236
+ ### Enabling encryption
228
237
 
229
- To specific multiple clusters you can do:
238
+ To encrypt the cache values, you can add set the encrypt property.
230
239
 
231
240
  ```yaml
232
241
  # config/solid_cache.yml
233
242
  production:
234
- databases: [cache_primary_shard1, cache_primary_shard2, cache_secondary_shard1, cache_secondary_shard2]
235
- store_options:
236
- clusters:
237
- - shards: [cache_primary_shard1, cache_primary_shard2]
238
- - shards: [cache_secondary_shard1, cache_secondary_shard2]
243
+ encrypt: true
244
+ ```
245
+ or
246
+ ```ruby
247
+ # application.rb
248
+ config.solid_cache.encrypt = true
239
249
  ```
240
250
 
241
- ### Named shard destinations
242
-
243
- By default, the node key used for sharding is the name of the database in `database.yml`.
244
-
245
- It is possible to add names for the shards in the cluster config. This will allow you to shuffle or remove shards without breaking consistent hashing.
251
+ You will need to set up your application to (use Active Record Encryption)[https://guides.rubyonrails.org/active_record_encryption.html].
246
252
 
247
- ```yaml
248
- production:
249
- databases: [cache_primary_shard1, cache_primary_shard2, cache_secondary_shard1, cache_secondary_shard2]
250
- store_options:
251
- clusters:
252
- - shards:
253
- cache_primary_shard1: node1
254
- cache_primary_shard2: node2
255
- - shards:
256
- cache_secondary_shard1: node3
257
- cache_secondary_shard2: node4
258
- ```
253
+ Solid Cache by default uses a custom encryptor and message serializer that are optimised for it.
259
254
 
260
- ### Enabling encryption
255
+ Firstly it disabled compression with the encryptor `ActiveRecord::Encryption::Encryptor.new(compress: false)` - the cache already compresses the data.
256
+ Secondly it uses `ActiveRecord::Encryption::MessagePackMessageSerializer.new` as the serializer. This serializer can only be used for binary columns,
257
+ but can store about 40% more data than the standard serializer.
261
258
 
262
- Add this to an initializer:
259
+ You can choose your own context properties instead if you prefer:
263
260
 
264
261
  ```ruby
265
- ActiveSupport.on_load(:solid_cache_entry) do
266
- encrypts :value
267
- end
262
+ # application.rb
263
+ config.solid_cache.encryption_context_properties = {
264
+ encryptor: ActiveRecord::Encryption::Encryptor.new,
265
+ message_serializer: ActiveRecord::Encryption::MessageSerializer.new
266
+ }
268
267
  ```
269
268
 
269
+ **Note**
270
+
271
+ Encryption currently does not work for PostgreSQL, as Rails does not yet support encrypting binary columns for it.
272
+ See https://github.com/rails/rails/pull/52650.
273
+
270
274
  ### Index size limits
271
275
  The Solid Cache migrations try to create an index with 1024 byte entries. If that is too big for your database, you should:
272
276
 
273
- 1. Edit the index size in the migration
274
- 2. Set `max_key_bytesize` on your cache to the new value
277
+ 1. Edit the index size in the migration.
278
+ 2. Set `max_key_bytesize` on your cache to the new value.
275
279
 
276
280
  ## Development
277
281
 
@@ -298,10 +302,10 @@ $ TARGET_DB=mysql bin/rake test
298
302
  $ TARGET_DB=postgres bin/rake test
299
303
  ```
300
304
 
301
- ### Testing with multiple Rails version
305
+ ### Testing with multiple Rails versions
302
306
 
303
307
  Solid Cache relies on [appraisal](https://github.com/thoughtbot/appraisal/tree/main) to test
304
- multiple Rails version.
308
+ multiple Rails versions.
305
309
 
306
310
  To run a test for a specific version run:
307
311
 
data/Rakefile CHANGED
@@ -15,7 +15,9 @@ def run_without_aborting(*tasks)
15
15
 
16
16
  tasks.each do |task|
17
17
  Rake::Task[task].invoke
18
- rescue Exception
18
+ rescue Exception => e
19
+ puts e.message
20
+ puts e.backtrace
19
21
  errors << task
20
22
  end
21
23
 
@@ -23,7 +25,7 @@ def run_without_aborting(*tasks)
23
25
  end
24
26
 
25
27
  def configs
26
- [ :default, :cluster, :cluster_inferred, :clusters, :clusters_named, :database, :no_database ]
28
+ [ :default, :connects_to, :database, :encrypted, :encrypted_custom, :no_database, :shards, :unprepared_statements ]
27
29
  end
28
30
 
29
31
  task :test do
@@ -34,6 +36,11 @@ end
34
36
  configs.each do |config|
35
37
  namespace :test do
36
38
  task config do
39
+ if config.to_s.start_with?("encrypted") && ENV["TARGET_DB"] == "postgres"
40
+ puts "Skipping encrypted tests on PostgreSQL as binary encrypted columns are not supported by Rails yet"
41
+ next
42
+ end
43
+
37
44
  if config == :default
38
45
  sh("bin/rails test")
39
46
  else
@@ -42,3 +49,5 @@ configs.each do |config|
42
49
  end
43
50
  end
44
51
  end
52
+
53
+ task default: [:test]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCache
4
+ class Entry
5
+ module Encryption
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ if SolidCache.configuration.encrypt?
10
+ encrypts :value, **SolidCache.configuration.encryption_context_properties
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -27,7 +27,7 @@ module SolidCache
27
27
  # We then calculate the fraction of the rows we want to sample by dividing the sample size by the estimated number
28
28
  # of rows.
29
29
  #
30
- # The we grab the byte_size sum of the rows in the range of key_hash values excluding any rows that are larger than
30
+ # Then we grab the byte_size sum of the rows in the range of key_hash values excluding any rows that are larger than
31
31
  # our minimum outlier cutoff. We then divide this by the sampling fraction to get an estimate of the size of the
32
32
  # non outlier rows
33
33
  #
@@ -3,9 +3,9 @@
3
3
  module SolidCache
4
4
  class Entry
5
5
  module Size
6
- # Moving averate cache size estimation
6
+ # Moving average cache size estimation
7
7
  #
8
- # To reduce variablitity in the cache size estimate, we'll use a moving average of the previous 20 estimates.
8
+ # To reduce variability in the cache size estimate, we'll use a moving average of the previous 20 estimates.
9
9
  # The estimates are stored directly in the cache, under the "__solid_cache_entry_size_moving_average_estimates" key.
10
10
  #
11
11
  # We'll remove the largest and smallest estimates, and then average remaining ones.
@@ -2,41 +2,45 @@
2
2
 
3
3
  module SolidCache
4
4
  class Entry < Record
5
- include Expiration, Size
5
+ include Encryption, Expiration, Size
6
6
 
7
7
  # The estimated cost of an extra row in bytes, including fixed size columns, overhead, indexes and free space
8
- # Based on expirimentation on SQLite, MySQL and Postgresql.
8
+ # Based on experimentation on SQLite, MySQL and Postgresql.
9
9
  # A bit high for SQLite (more like 90 bytes), but about right for MySQL/Postgresql.
10
10
  ESTIMATED_ROW_OVERHEAD = 140
11
+
12
+ # Assuming MessagePack serialization
13
+ ESTIMATED_ENCRYPTION_OVERHEAD = 170
14
+
11
15
  KEY_HASH_ID_RANGE = -(2**63)..(2**63 - 1)
12
16
 
13
17
  class << self
14
18
  def write(key, value)
15
- upsert_all_no_query_cache([ { key: key, value: value } ])
19
+ write_multi([ { key: key, value: value } ])
16
20
  end
17
21
 
18
22
  def write_multi(payloads)
19
- upsert_all_no_query_cache(payloads)
23
+ without_query_cache do
24
+ upsert_all \
25
+ add_key_hash_and_byte_size(payloads),
26
+ unique_by: upsert_unique_by, on_duplicate: :update, update_only: [ :key, :value, :byte_size ]
27
+ end
20
28
  end
21
29
 
22
30
  def read(key)
23
- result = select_all_no_query_cache(get_sql, key_hash_for(key)).first
24
- result[1] if result&.first == key
31
+ read_multi([key])[key]
25
32
  end
26
33
 
27
34
  def read_multi(keys)
28
- key_hashes = keys.map { |key| key_hash_for(key) }
29
- results = select_all_no_query_cache(get_all_sql(key_hashes), key_hashes).to_h
30
- results.except!(results.keys - keys)
31
- end
32
-
33
- def delete_by_key(key)
34
- delete_no_query_cache(:key_hash, key_hash_for(key)) > 0
35
+ without_query_cache do
36
+ find_by_sql([select_sql(keys), *key_hashes_for(keys)]).pluck(:key, :value).to_h
37
+ end
35
38
  end
36
39
 
37
- def delete_multi(keys)
38
- serialized_keys = keys.map { |key| key_hash_for(key) }
39
- delete_no_query_cache(:key_hash, serialized_keys)
40
+ def delete_by_key(*keys)
41
+ without_query_cache do
42
+ where(key_hash: key_hashes_for(keys)).delete_all
43
+ end
40
44
  end
41
45
 
42
46
  def clear_truncate
@@ -44,48 +48,29 @@ module SolidCache
44
48
  end
45
49
 
46
50
  def clear_delete
47
- in_batches.delete_all
51
+ without_query_cache do
52
+ in_batches.delete_all
53
+ end
48
54
  end
49
55
 
50
56
  def lock_and_write(key, &block)
51
57
  transaction do
52
- uncached do
58
+ without_query_cache do
53
59
  result = lock.where(key_hash: key_hash_for(key)).pick(:key, :value)
54
60
  new_value = block.call(result&.first == key ? result[1] : nil)
55
- write(key, new_value)
61
+ write(key, new_value) if new_value
56
62
  new_value
57
63
  end
58
64
  end
59
65
  end
60
66
 
61
67
  def id_range
62
- uncached do
68
+ without_query_cache do
63
69
  pick(Arel.sql("max(id) - min(id) + 1")) || 0
64
70
  end
65
71
  end
66
72
 
67
73
  private
68
- def upsert_all_no_query_cache(payloads)
69
- args = [ self,
70
- connection_for_insert_all,
71
- add_key_hash_and_byte_size(payloads) ].compact
72
- options = { unique_by: upsert_unique_by,
73
- on_duplicate: :update,
74
- update_only: upsert_update_only }
75
- insert_all = ActiveRecord::InsertAll.new(*args, **options)
76
- sql = connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(insert_all))
77
-
78
- message = +"#{self} "
79
- message << "Bulk " if payloads.many?
80
- message << "Upsert"
81
- # exec_query_method does not clear the query cache, exec_insert_all does
82
- connection.send exec_query_method, sql, message
83
- end
84
-
85
- def connection_for_insert_all
86
- Rails.version >= "7.2" ? connection : nil
87
- end
88
-
89
74
  def add_key_hash_and_byte_size(payloads)
90
75
  payloads.map do |payload|
91
76
  payload.dup.tap do |payload|
@@ -95,77 +80,42 @@ module SolidCache
95
80
  end
96
81
  end
97
82
 
98
- def exec_query_method
99
- connection.respond_to?(:internal_exec_query) ? :internal_exec_query : :exec_query
100
- end
101
-
102
83
  def upsert_unique_by
103
84
  connection.supports_insert_conflict_target? ? :key_hash : nil
104
85
  end
105
86
 
106
- def upsert_update_only
107
- [ :key, :value, :byte_size ]
87
+ def select_sql(keys)
88
+ @get_sql ||= {}
89
+ @get_sql[keys.count] ||= \
90
+ where(key_hash: [ "1111", "2222" ])
91
+ .select(:key, :value)
92
+ .to_sql
93
+ .gsub("1111, 2222", (["?"] * keys.count).join(", "))
108
94
  end
109
95
 
110
- def get_sql
111
- @get_sql ||= build_sql(where(key_hash: 1).select(:key, :value))
112
- end
113
-
114
- def get_all_sql(key_hashes)
115
- if connection.prepared_statements?
116
- @get_all_sql_binds ||= {}
117
- @get_all_sql_binds[key_hashes.count] ||= build_sql(where(key_hash: key_hashes).select(:key, :value))
118
- else
119
- @get_all_sql_no_binds ||= build_sql(where(key_hash: [ 1, 2 ]).select(:key, :value)).gsub("?, ?", "?")
120
- end
96
+ def key_hash_for(key)
97
+ # Need to unpack this as a signed integer - Postgresql and SQLite don't support unsigned integers
98
+ Digest::SHA256.digest(key.to_s).unpack("q>").first
121
99
  end
122
100
 
123
- def build_sql(relation)
124
- collector = Arel::Collectors::Composite.new(
125
- Arel::Collectors::SQLString.new,
126
- Arel::Collectors::Bind.new,
127
- )
128
-
129
- connection.visitor.compile(relation.arel.ast, collector)[0]
101
+ def key_hashes_for(keys)
102
+ keys.map { |key| key_hash_for(key) }
130
103
  end
131
104
 
132
- def select_all_no_query_cache(query, values)
133
- uncached do
134
- if connection.prepared_statements?
135
- result = connection.select_all(sanitize_sql(query), "#{name} Load", Array(values), preparable: true)
136
- else
137
- result = connection.select_all(sanitize_sql([ query, values ]), "#{name} Load", Array(values), preparable: false)
138
- end
139
-
140
- result.cast_values(SolidCache::Entry.attribute_types)
141
- end
105
+ def byte_size_for(payload)
106
+ payload[:key].to_s.bytesize + payload[:value].to_s.bytesize + estimated_row_overhead
142
107
  end
143
108
 
144
- def delete_no_query_cache(attribute, values)
145
- uncached do
146
- relation = where(attribute => values)
147
- sql = connection.to_sql(relation.arel.compile_delete(relation.table[primary_key]))
148
-
149
- # exec_delete does not clear the query cache
150
- if connection.prepared_statements?
151
- connection.exec_delete(sql, "#{name} Delete All", Array(values))
152
- else
153
- connection.exec_delete(sql, "#{name} Delete All")
154
- end
109
+ def estimated_row_overhead
110
+ if SolidCache.configuration.encrypt?
111
+ ESTIMATED_ROW_OVERHEAD + ESTIMATED_ENCRYPTION_OVERHEAD
112
+ else
113
+ ESTIMATED_ROW_OVERHEAD
155
114
  end
156
115
  end
157
116
 
158
- def to_binary(key)
159
- ActiveModel::Type::Binary.new.serialize(key)
160
- end
161
-
162
- def key_hash_for(key)
163
- # Need to unpack this as a signed integer - Postgresql and SQLite don't support unsigned integers
164
- Digest::SHA256.digest(key.to_s).unpack("q>").first
165
- end
166
-
167
- def byte_size_for(payload)
168
- payload[:key].to_s.bytesize + payload[:value].to_s.bytesize + ESTIMATED_ROW_OVERHEAD
117
+ def without_query_cache(&block)
118
+ uncached(dirties: false, &block)
169
119
  end
170
120
  end
171
121
  end
@@ -0,0 +1,29 @@
1
+ class CreateSolidCacheEntries < ActiveRecord::Migration[7.2]
2
+ def up
3
+ create_table :solid_cache_entries, if_not_exists: true do |t|
4
+ t.binary :key, null: false, limit: 1024
5
+ t.binary :value, null: false, limit: 512.megabytes
6
+ t.datetime :created_at, null: false
7
+ t.integer :key_hash, null: false, limit: 8
8
+ t.integer :byte_size, null: false, limit: 4
9
+
10
+ t.index :key_hash, unique: true
11
+ t.index [:key_hash, :byte_size]
12
+ t.index :byte_size
13
+ end
14
+
15
+ raise "column \"key_hash\" does not exist" unless column_exists? :solid_cache_entries, :key_hash
16
+ rescue => e
17
+ if e.message =~ /(column "key_hash" does not exist|no such column: key_hash)/
18
+ raise \
19
+ "Could not find key_hash column on solid_cache_entries, if upgrading from v0.3 or earlier, have you followed " \
20
+ "the steps in https://github.com/rails/solid_cache/blob/main/upgrading_to_version_0.4.x.md?"
21
+ else
22
+ raise
23
+ end
24
+ end
25
+
26
+ def down
27
+ drop_table :solid_cache_entries
28
+ end
29
+ end
@@ -1,5 +1,5 @@
1
1
  default: &default
2
- database: <%%= Rails.env %>
2
+ database: <%= ENV.fetch("DATABASE", "cache") %>
3
3
  store_options:
4
4
  max_age: <%%= 1.week.to_i %>
5
5
  max_size: <%%= 256.megabytes %>