solid_cache 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +110 -106
- data/Rakefile +11 -2
- data/app/models/solid_cache/entry/encryption.rb +15 -0
- data/app/models/solid_cache/entry/size/estimate.rb +1 -1
- data/app/models/solid_cache/entry/size/moving_average_estimate.rb +2 -2
- data/app/models/solid_cache/entry.rb +47 -97
- data/db/migrate/20240820123641_create_solid_cache_entries.rb +29 -0
- data/lib/generators/solid_cache/install/templates/config/solid_cache.yml.tt +1 -1
- data/lib/solid_cache/configuration.rb +20 -2
- data/lib/solid_cache/connections/sharded.rb +3 -4
- data/lib/solid_cache/connections.rb +1 -6
- data/lib/solid_cache/engine.rb +10 -0
- data/lib/solid_cache/store/api.rb +17 -6
- data/lib/solid_cache/store/connections.rb +108 -0
- data/lib/solid_cache/store/entries.rb +9 -7
- data/lib/solid_cache/{cluster → store}/execution.rb +4 -4
- data/lib/solid_cache/{cluster → store}/expiry.rb +1 -1
- data/lib/solid_cache/{cluster → store}/stats.rb +2 -2
- data/lib/solid_cache/store.rb +1 -5
- data/lib/solid_cache/version.rb +1 -1
- metadata +15 -19
- data/db/migrate/20230724121448_create_solid_cache_entries.rb +0 -11
- data/db/migrate/20240108155507_add_key_hash_and_byte_size_to_solid_cache_entries.rb +0 -8
- data/db/migrate/20240110111600_add_key_hash_and_byte_size_indexes_and_null_constraints_to_solid_cache_entries.rb +0 -11
- data/db/migrate/20240110111702_remove_key_index_from_solid_cache_entries.rb +0 -7
- data/lib/solid_cache/cluster/connections.rb +0 -55
- data/lib/solid_cache/cluster.rb +0 -18
- data/lib/solid_cache/store/clusters.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 74f6b8bdfea57b8ffbe60b8f525b3907c7278f4b73d07fad2506a41b0884a14c
|
4
|
+
data.tar.gz: b3af9a6526d97bcb9b5ac1fd313b9c9cf76551ae75e214479a10fe9b04d678d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
##
|
9
|
+
## Introduction
|
10
10
|
|
11
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
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` -
|
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
|
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
|
-
###
|
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
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
243
|
+
encrypt: true
|
244
|
+
```
|
245
|
+
or
|
246
|
+
```ruby
|
247
|
+
# application.rb
|
248
|
+
config.solid_cache.encrypt = true
|
239
249
|
```
|
240
250
|
|
241
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
259
|
+
You can choose your own context properties instead if you prefer:
|
263
260
|
|
264
261
|
```ruby
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
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
|
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, :
|
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
|
-
#
|
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
|
6
|
+
# Moving average cache size estimation
|
7
7
|
#
|
8
|
-
# To reduce
|
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
|
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
|
-
|
19
|
+
write_multi([ { key: key, value: value } ])
|
16
20
|
end
|
17
21
|
|
18
22
|
def write_multi(payloads)
|
19
|
-
|
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
|
-
|
24
|
-
result[1] if result&.first == key
|
31
|
+
read_multi([key])[key]
|
25
32
|
end
|
26
33
|
|
27
34
|
def read_multi(keys)
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
107
|
-
|
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
|
111
|
-
|
112
|
-
|
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
|
124
|
-
|
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
|
133
|
-
|
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
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
159
|
-
|
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
|