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.
- 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
|