solid_cache 0.1.0 → 0.3.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 +44 -14
- data/Rakefile +2 -0
- data/app/jobs/solid_cache/expiry_job.rb +2 -0
- data/app/models/solid_cache/entry.rb +27 -13
- data/app/models/solid_cache/record.rb +7 -7
- data/db/migrate/20230724121448_create_solid_cache_entries.rb +2 -0
- data/lib/active_support/cache/solid_cache_store.rb +2 -0
- data/lib/generators/solid_cache/install/install_generator.rb +2 -0
- data/lib/solid_cache/cluster/connections.rb +2 -0
- data/lib/solid_cache/cluster/execution.rb +2 -0
- data/lib/solid_cache/cluster/expiry.rb +3 -1
- data/lib/solid_cache/cluster/stats.rb +3 -1
- data/lib/solid_cache/cluster.rb +1 -0
- data/lib/solid_cache/connections/sharded.rb +2 -0
- data/lib/solid_cache/connections/single.rb +2 -0
- data/lib/solid_cache/connections/unmanaged.rb +2 -0
- data/lib/solid_cache/connections.rb +2 -0
- data/lib/solid_cache/engine.rb +2 -0
- data/lib/solid_cache/maglev_hash.rb +3 -1
- data/lib/solid_cache/store/api.rb +11 -2
- data/lib/solid_cache/store/clusters.rb +6 -8
- data/lib/solid_cache/store/entries.rb +2 -0
- data/lib/solid_cache/store/failsafe.rb +2 -0
- data/lib/solid_cache/store.rb +2 -0
- data/lib/solid_cache/version.rb +1 -1
- data/lib/solid_cache.rb +4 -2
- data/lib/tasks/solid_cache_tasks.rake +2 -0
- metadata +32 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d92ba4d7c045821d0c54facd1329613e497a46a2b21daffe3a14931bf9621f86
|
4
|
+
data.tar.gz: 46c4b4b81c48a5a5598cc2460e8c1159e69497f130116e9f855a58f8f3e52a36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f3505047c30f24d6f8928d439172f826becc79054a05f32b0c670b3cbb227a10b52889b1e64e2430fe2d279e98c6615bb92634c475fdb6016cd7d3c9c170c07
|
7
|
+
data.tar.gz: 4cb0c61cddd64a4d8093b73b06524ae5585cf5d0c70233bd31fd688b837d38c500df28479bbbb7ffa99b24e26dc3230789dad0d108f9d7fc3df4e02b0eeb0d55
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Solid Cache
|
2
2
|
|
3
|
-
Solid Cache is a database-backed Active Support cache store implementation.
|
3
|
+
Solid Cache is a database-backed Active Support cache store implementation.
|
4
4
|
|
5
5
|
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.
|
6
6
|
|
@@ -58,7 +58,7 @@ $ bin/rails db:migrate
|
|
58
58
|
There are two options that can be set on the engine:
|
59
59
|
|
60
60
|
- `executor` - the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
|
61
|
-
- `connects_to` - a custom connects to value for the abstract `SolidCache::Record` active record model.
|
61
|
+
- `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.
|
62
62
|
|
63
63
|
These can be set in your Rails configuration:
|
64
64
|
|
@@ -80,19 +80,19 @@ Solid Cache supports these options in addition to the standard `ActiveSupport::C
|
|
80
80
|
- `error_handler` - a Proc to call to handle any `ActiveRecord::ActiveRecordError`s that are raises (default: log errors as warnings)
|
81
81
|
- `expiry_batch_size` - the batch size to use when deleting old records (default: `100`)
|
82
82
|
- `expiry_method` - what expiry method to use `thread` or `job` (default: `thread`)
|
83
|
-
- `max_age` - the maximum age of entries in the cache (default: `2.weeks.to_i`)
|
84
|
-
- `max_entries` - the maximum number of entries allowed in the cache (default: `
|
83
|
+
- `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.
|
84
|
+
- `max_entries` - the maximum number of entries allowed in the cache (default: `nil`, meaning no limit)
|
85
85
|
- `cluster` - a Hash of options for the cache database cluster, e.g `{ shards: [:database1, :database2, :database3] }`
|
86
86
|
- `clusters` - and Array of Hashes for multiple cache clusters (ignored if `:cluster` is set)
|
87
87
|
- `active_record_instrumentation` - whether to instrument the cache's queries (default: `true`)
|
88
|
-
- `clear_with` - clear the cache with `:truncate` or `:delete` (default `truncate`, except for when Rails.env.test
|
88
|
+
- `clear_with` - clear the cache with `:truncate` or `:delete` (default `truncate`, except for when `Rails.env.test?` then `delete`)
|
89
89
|
- `max_key_bytesize` - the maximum size of a normalized key in bytes (default `1024`)
|
90
90
|
|
91
91
|
For more information on cache clusters see [Sharding the cache](#sharding-the-cache)
|
92
92
|
|
93
93
|
### Cache expiry
|
94
94
|
|
95
|
-
Solid Cache tracks writes to the cache. For every write it increments a counter by 1. Once the counter reaches 80% of the `expiry_batch_size` it
|
95
|
+
Solid Cache tracks writes to the cache. For every write it increments a counter by 1. Once the counter reaches 80% of the `expiry_batch_size` it adds a task to run on a background thread. That task will:
|
96
96
|
|
97
97
|
1. Check if we have exceeded the `max_entries` value (if set) by subtracting the max and min IDs from the `SolidCache::Entry` table (this is an estimate that ignores any gaps).
|
98
98
|
2. If we have it will delete `expiry_batch_size` entries
|
@@ -109,7 +109,7 @@ If you want the cache expiry to be run in a background job instead of a thread,
|
|
109
109
|
Add database configuration to database.yml, e.g.:
|
110
110
|
|
111
111
|
```
|
112
|
-
development
|
112
|
+
development:
|
113
113
|
cache:
|
114
114
|
database: cache_development
|
115
115
|
host: 127.0.0.1
|
@@ -135,7 +135,7 @@ $ mv db/migrate/*.solid_cache.rb db/cache/migrate
|
|
135
135
|
Set the engine configuration to point to the new database:
|
136
136
|
```
|
137
137
|
Rails.application.configure do
|
138
|
-
config.solid_cache.connects_to = {
|
138
|
+
config.solid_cache.connects_to = { database: { writing: :cache } }
|
139
139
|
end
|
140
140
|
```
|
141
141
|
|
@@ -155,7 +155,7 @@ To shard:
|
|
155
155
|
3. Pass the shards for the cache to use via the cluster option
|
156
156
|
|
157
157
|
For example:
|
158
|
-
```
|
158
|
+
```yml
|
159
159
|
# config/database.yml
|
160
160
|
production:
|
161
161
|
cache_shard1:
|
@@ -167,8 +167,9 @@ production:
|
|
167
167
|
cache_shard3:
|
168
168
|
database: cache3_production
|
169
169
|
host: cache3-db
|
170
|
+
```
|
170
171
|
|
171
|
-
|
172
|
+
```ruby
|
172
173
|
# config/environment/production.rb
|
173
174
|
Rails.application.configure do
|
174
175
|
config.solid_cache.connects_to = {
|
@@ -203,7 +204,7 @@ Rails.application.configure do
|
|
203
204
|
}
|
204
205
|
|
205
206
|
primary_cluster = { shards: [ :cache_primary_shard1, :cache_primary_shard2 ] }
|
206
|
-
secondary_cluster = { shards: [ :
|
207
|
+
secondary_cluster = { shards: [ :cache_secondary_shard1, :cache_secondary_shard2 ] }
|
207
208
|
config.cache_store = [ :solid_cache_store, clusters: [ primary_cluster, secondary_cluster ] ]
|
208
209
|
end
|
209
210
|
```
|
@@ -250,19 +251,48 @@ The Solid Cache migrations try to create an index with 1024 byte entries. If tha
|
|
250
251
|
|
251
252
|
## Development
|
252
253
|
|
253
|
-
Run the tests with `bin/rails test`.
|
254
|
+
Run the tests with `bin/rails test`. By default, these will run against SQLite.
|
254
255
|
|
255
|
-
You can also run the tests against MySQL and
|
256
|
+
You can also run the tests against MySQL and PostgreSQL. First start up the databases:
|
256
257
|
|
257
258
|
```shell
|
258
259
|
$ docker compose up -d
|
259
260
|
```
|
260
261
|
|
261
|
-
|
262
|
+
Next, setup the database schema:
|
263
|
+
|
264
|
+
```shell
|
265
|
+
$ TARGET_DB=mysql bin/rails db:setup
|
266
|
+
$ TARGET_DB=postgres bin/rails db:setup
|
262
267
|
```
|
268
|
+
|
269
|
+
|
270
|
+
Then run the tests for the target database:
|
271
|
+
|
272
|
+
```shell
|
263
273
|
$ TARGET_DB=mysql bin/rails test
|
264
274
|
$ TARGET_DB=postgres bin/rails test
|
265
275
|
```
|
266
276
|
|
277
|
+
### Testing with multiple Rails version
|
278
|
+
|
279
|
+
Solid Cache relies on [appraisal](https://github.com/thoughtbot/appraisal/tree/main) to test
|
280
|
+
multiple Rails version.
|
281
|
+
|
282
|
+
To run a test for a specific version run:
|
283
|
+
|
284
|
+
```shell
|
285
|
+
bundle exec appraisal rails-7-1 bin/rails test
|
286
|
+
```
|
287
|
+
|
288
|
+
After updating the dependencies in then `Gemfile` please run:
|
289
|
+
|
290
|
+
```shell
|
291
|
+
$ bundle
|
292
|
+
$ appraisal update
|
293
|
+
```
|
294
|
+
|
295
|
+
This ensures that all the Rails versions dependencies are updated.
|
296
|
+
|
267
297
|
## License
|
268
298
|
Solid Cache is licensed under MIT.
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Entry < Record
|
3
5
|
# This is all quite awkward but it achieves a couple of performance aims
|
@@ -21,10 +23,6 @@ module SolidCache
|
|
21
23
|
select_all_no_query_cache(get_all_sql(serialized_keys), serialized_keys).to_h
|
22
24
|
end
|
23
25
|
|
24
|
-
def expire(ids)
|
25
|
-
delete_no_query_cache(:id, ids) if ids.any?
|
26
|
-
end
|
27
|
-
|
28
26
|
def delete_by_key(key)
|
29
27
|
delete_no_query_cache(:key, to_binary(key))
|
30
28
|
end
|
@@ -78,8 +76,12 @@ module SolidCache
|
|
78
76
|
message = +"#{self} "
|
79
77
|
message << "Bulk " if attributes.many?
|
80
78
|
message << "Upsert"
|
81
|
-
#
|
82
|
-
connection.
|
79
|
+
# exec_query_method does not clear the query cache, exec_insert_all does
|
80
|
+
connection.send exec_query_method, sql, message
|
81
|
+
end
|
82
|
+
|
83
|
+
def exec_query_method
|
84
|
+
connection.respond_to?(:internal_exec_query) ? :internal_exec_query : :exec_query
|
83
85
|
end
|
84
86
|
|
85
87
|
def upsert_unique_by
|
@@ -113,7 +115,7 @@ module SolidCache
|
|
113
115
|
if connection.prepared_statements?
|
114
116
|
result = connection.select_all(sanitize_sql(query), "#{name} Load", Array(values), preparable: true)
|
115
117
|
else
|
116
|
-
result = connection.select_all(sanitize_sql([ query, values ]), "#{name} Load",
|
118
|
+
result = connection.select_all(sanitize_sql([ query, values ]), "#{name} Load", Array(values), preparable: false)
|
117
119
|
end
|
118
120
|
|
119
121
|
result.cast_values(SolidCache::Entry.attribute_types)
|
@@ -140,14 +142,26 @@ module SolidCache
|
|
140
142
|
|
141
143
|
def expiry_candidate_ids(count, max_age:, max_entries:)
|
142
144
|
cache_full = max_entries && max_entries < id_range
|
143
|
-
|
145
|
+
return [] unless cache_full || max_age
|
146
|
+
|
147
|
+
# In the case of multiple concurrent expiry operations, it is desirable to
|
148
|
+
# reduce the overlap of entries being addressed by each. For that reason,
|
149
|
+
# retrieve more ids than are being expired, and use random
|
150
|
+
# sampling to reduce that number to the actual intended count.
|
151
|
+
retrieve_count = count * 3
|
144
152
|
|
145
153
|
uncached do
|
146
|
-
order(:id)
|
147
|
-
|
148
|
-
|
149
|
-
.
|
150
|
-
|
154
|
+
candidates = order(:id).limit(retrieve_count)
|
155
|
+
|
156
|
+
candidate_ids = if cache_full
|
157
|
+
candidates.pluck(:id)
|
158
|
+
else
|
159
|
+
min_created_at = max_age.seconds.ago
|
160
|
+
candidates.pluck(:id, :created_at)
|
161
|
+
.filter_map { |id, created_at| id if created_at < min_created_at }
|
162
|
+
end
|
163
|
+
|
164
|
+
candidate_ids.sample(count)
|
151
165
|
end
|
152
166
|
end
|
153
167
|
end
|
@@ -1,21 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Record < ActiveRecord::Base
|
3
5
|
NULL_INSTRUMENTER = ActiveSupport::Notifications::Instrumenter.new(ActiveSupport::Notifications::Fanout.new)
|
4
6
|
|
5
7
|
self.abstract_class = true
|
6
8
|
|
7
|
-
connects_to
|
9
|
+
connects_to(**SolidCache.connects_to) if SolidCache.connects_to
|
8
10
|
|
9
11
|
class << self
|
10
|
-
def disable_instrumentation
|
11
|
-
connection.with_instrumenter(NULL_INSTRUMENTER)
|
12
|
-
yield
|
13
|
-
end
|
12
|
+
def disable_instrumentation(&block)
|
13
|
+
connection.with_instrumenter(NULL_INSTRUMENTER, &block)
|
14
14
|
end
|
15
15
|
|
16
16
|
def with_shard(shard, &block)
|
17
|
-
if shard &&
|
18
|
-
|
17
|
+
if shard && SolidCache.connects_to
|
18
|
+
connected_to(shard: shard, role: default_role, prevent_writes: false, &block)
|
19
19
|
else
|
20
20
|
block.call
|
21
21
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "concurrent/atomic/atomic_fixnum"
|
2
4
|
|
3
5
|
module SolidCache
|
@@ -34,7 +36,7 @@ module SolidCache
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def expiry_counter
|
37
|
-
@expiry_counters ||= connection_names.
|
39
|
+
@expiry_counters ||= connection_names.index_with { |connection_name| Counter.new(expire_every) }
|
38
40
|
@expiry_counters[Entry.current_shard]
|
39
41
|
end
|
40
42
|
|
data/lib/solid_cache/cluster.rb
CHANGED
data/lib/solid_cache/engine.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# See https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf
|
2
4
|
|
3
5
|
module SolidCache
|
4
6
|
class MaglevHash
|
5
7
|
attr_reader :nodes
|
6
8
|
|
7
|
-
#
|
9
|
+
# Must be prime
|
8
10
|
TABLE_SIZE = 2053
|
9
11
|
|
10
12
|
def initialize(nodes)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Store
|
3
5
|
module Api
|
@@ -74,7 +76,7 @@ module SolidCache
|
|
74
76
|
end
|
75
77
|
|
76
78
|
def read_multi_entries(names, **options)
|
77
|
-
keys_and_names = names.
|
79
|
+
keys_and_names = names.index_by { |name| normalize_key(name, options) }
|
78
80
|
serialized_entries = read_serialized_entries(keys_and_names.keys)
|
79
81
|
|
80
82
|
keys_and_names.each_with_object({}) do |(key, name), results|
|
@@ -88,7 +90,14 @@ module SolidCache
|
|
88
90
|
if entry.expired?
|
89
91
|
delete_entry(key, **options)
|
90
92
|
elsif !entry.mismatched?(version)
|
91
|
-
|
93
|
+
if defined? ActiveSupport::Cache::DeserializationError
|
94
|
+
begin
|
95
|
+
results[name] = entry.value
|
96
|
+
rescue ActiveSupport::Cache::DeserializationError
|
97
|
+
end
|
98
|
+
else
|
99
|
+
results[name] = entry.value
|
100
|
+
end
|
92
101
|
end
|
93
102
|
end
|
94
103
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Store
|
3
5
|
module Clusters
|
@@ -20,11 +22,9 @@ module SolidCache
|
|
20
22
|
end
|
21
23
|
|
22
24
|
private
|
23
|
-
def reading_key(key, failsafe:, failsafe_returning: nil)
|
25
|
+
def reading_key(key, failsafe:, failsafe_returning: nil, &block)
|
24
26
|
failsafe(failsafe, returning: failsafe_returning) do
|
25
|
-
primary_cluster.with_connection_for(key)
|
26
|
-
yield
|
27
|
-
end
|
27
|
+
primary_cluster.with_connection_for(key, &block)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -65,13 +65,11 @@ module SolidCache
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
def writing_all(failsafe:, failsafe_returning: nil)
|
68
|
+
def writing_all(failsafe:, failsafe_returning: nil, &block)
|
69
69
|
first_cluster_sync_rest_async do |cluster, async|
|
70
70
|
cluster.connection_names.each do |connection|
|
71
71
|
failsafe(failsafe, returning: failsafe_returning) do
|
72
|
-
cluster.with_connection(connection, async: async)
|
73
|
-
yield
|
74
|
-
end
|
72
|
+
cluster.with_connection(connection, async: async, &block)
|
75
73
|
end
|
76
74
|
end
|
77
75
|
end
|
data/lib/solid_cache/store.rb
CHANGED
data/lib/solid_cache/version.rb
CHANGED
data/lib/solid_cache.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "zeitwerk"
|
2
4
|
require "solid_cache/engine"
|
3
5
|
|
@@ -17,12 +19,12 @@ module SolidCache
|
|
17
19
|
connects_to && connects_to[:shards]
|
18
20
|
end
|
19
21
|
|
20
|
-
def self.each_shard
|
22
|
+
def self.each_shard(&block)
|
21
23
|
return to_enum(:each_shard) unless block_given?
|
22
24
|
|
23
25
|
if (shards = all_shards_config&.keys)
|
24
26
|
shards.each do |shard|
|
25
|
-
Record.
|
27
|
+
Record.with_shard(shard, &block)
|
26
28
|
end
|
27
29
|
else
|
28
30
|
yield
|
metadata
CHANGED
@@ -1,17 +1,45 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Donal McBreen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activejob
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
16
44
|
requirements:
|
17
45
|
- - ">="
|
@@ -109,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
137
|
- !ruby/object:Gem::Version
|
110
138
|
version: '0'
|
111
139
|
requirements: []
|
112
|
-
rubygems_version: 3.
|
140
|
+
rubygems_version: 3.5.1
|
113
141
|
signing_key:
|
114
142
|
specification_version: 4
|
115
143
|
summary: A database backed ActiveSupport::Cache::Store
|