solid_cache 0.2.0 → 0.4.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/MIT-LICENSE +1 -1
- data/README.md +10 -6
- data/Rakefile +2 -0
- data/app/jobs/solid_cache/expiry_job.rb +2 -0
- data/app/models/solid_cache/entry/expiration.rb +49 -0
- data/app/models/solid_cache/entry.rb +88 -44
- data/app/models/solid_cache/record.rb +7 -7
- data/db/migrate/20240108155507_add_key_hash_and_byte_size_to_solid_cache_entries.rb +8 -0
- data/db/migrate/20240110111600_add_key_hash_and_byte_size_indexes_and_null_constraints_to_solid_cache_entries.rb +11 -0
- data/db/migrate/20240110111702_remove_key_index_from_solid_cache_entries.rb +7 -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 +3 -1
- data/lib/solid_cache/cluster/expiry.rb +16 -28
- 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 +8 -0
- data/lib/solid_cache/maglev_hash.rb +2 -0
- data/lib/solid_cache/store/api.rb +4 -16
- data/lib/solid_cache/store/clusters.rb +6 -8
- data/lib/solid_cache/store/entries.rb +8 -6
- data/lib/solid_cache/store/failsafe.rb +2 -0
- data/lib/solid_cache/store.rb +2 -0
- data/lib/solid_cache/version.rb +3 -1
- data/lib/solid_cache.rb +5 -2
- data/lib/tasks/solid_cache_tasks.rake +2 -0
- metadata +39 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 542cf01d73ff23edf9308fa57ef549ddfc823837bcfd01097b207d4f2db8ff41
|
4
|
+
data.tar.gz: b15fcfd3d56129511d7b19ca3758afc37927a4c4171a3dd12dcdf82fb2ced1de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb4ebf1d02ee1fce3c99176864b52225d4570e145a6226345ce9c47fb562987352fbd85ae3d90050cac41986a4d3190c27928c0713c57bd33cf1bb576e321902
|
7
|
+
data.tar.gz: c5684061ed4f60a0b9708912aa412362adb954bf44326a0151ff6e47d625afa9f6b8ed0d329dae5b2ae5129dea6dc1e02cc2850e27a06a892fac649d6495c9c3
|
data/MIT-LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Solid Cache
|
2
2
|
|
3
|
+
**Upgrading from v0.3.0 or earlier? Please see [upgrading to version 0.4.0](upgrading_to_version_0.4.x.md)**
|
4
|
+
|
3
5
|
Solid Cache is a database-backed Active Support cache store implementation.
|
4
6
|
|
5
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.
|
@@ -80,12 +82,13 @@ Solid Cache supports these options in addition to the standard `ActiveSupport::C
|
|
80
82
|
- `error_handler` - a Proc to call to handle any `ActiveRecord::ActiveRecordError`s that are raises (default: log errors as warnings)
|
81
83
|
- `expiry_batch_size` - the batch size to use when deleting old records (default: `100`)
|
82
84
|
- `expiry_method` - what expiry method to use `thread` or `job` (default: `thread`)
|
83
|
-
- `
|
85
|
+
- `expiry_queue` - which queue to add expiry jobs to (default: `default`)
|
86
|
+
- `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
87
|
- `max_entries` - the maximum number of entries allowed in the cache (default: `nil`, meaning no limit)
|
85
88
|
- `cluster` - a Hash of options for the cache database cluster, e.g `{ shards: [:database1, :database2, :database3] }`
|
86
89
|
- `clusters` - and Array of Hashes for multiple cache clusters (ignored if `:cluster` is set)
|
87
90
|
- `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
|
91
|
+
- `clear_with` - clear the cache with `:truncate` or `:delete` (default `truncate`, except for when `Rails.env.test?` then `delete`)
|
89
92
|
- `max_key_bytesize` - the maximum size of a normalized key in bytes (default `1024`)
|
90
93
|
|
91
94
|
For more information on cache clusters see [Sharding the cache](#sharding-the-cache)
|
@@ -155,7 +158,7 @@ To shard:
|
|
155
158
|
3. Pass the shards for the cache to use via the cluster option
|
156
159
|
|
157
160
|
For example:
|
158
|
-
```
|
161
|
+
```yml
|
159
162
|
# config/database.yml
|
160
163
|
production:
|
161
164
|
cache_shard1:
|
@@ -167,8 +170,9 @@ production:
|
|
167
170
|
cache_shard3:
|
168
171
|
database: cache3_production
|
169
172
|
host: cache3-db
|
173
|
+
```
|
170
174
|
|
171
|
-
|
175
|
+
```ruby
|
172
176
|
# config/environment/production.rb
|
173
177
|
Rails.application.configure do
|
174
178
|
config.solid_cache.connects_to = {
|
@@ -203,7 +207,7 @@ Rails.application.configure do
|
|
203
207
|
}
|
204
208
|
|
205
209
|
primary_cluster = { shards: [ :cache_primary_shard1, :cache_primary_shard2 ] }
|
206
|
-
secondary_cluster = { shards: [ :
|
210
|
+
secondary_cluster = { shards: [ :cache_secondary_shard1, :cache_secondary_shard2 ] }
|
207
211
|
config.cache_store = [ :solid_cache_store, clusters: [ primary_cluster, secondary_cluster ] ]
|
208
212
|
end
|
209
213
|
```
|
@@ -284,7 +288,7 @@ To run a test for a specific version run:
|
|
284
288
|
bundle exec appraisal rails-7-1 bin/rails test
|
285
289
|
```
|
286
290
|
|
287
|
-
After updating the dependencies in
|
291
|
+
After updating the dependencies in the `Gemfile` please run:
|
288
292
|
|
289
293
|
```shell
|
290
294
|
$ bundle
|
data/Rakefile
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidCache
|
4
|
+
class Entry
|
5
|
+
module Expiration
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def id_range
|
10
|
+
uncached do
|
11
|
+
pick(Arel.sql("max(id) - min(id) + 1")) || 0
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def expire(count, max_age:, max_entries:)
|
16
|
+
if (ids = expiry_candidate_ids(count, max_age: max_age, max_entries: max_entries)).any?
|
17
|
+
delete(ids)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def expiry_candidate_ids(count, max_age:, max_entries:)
|
23
|
+
cache_full = max_entries && max_entries < id_range
|
24
|
+
return [] unless cache_full || max_age
|
25
|
+
|
26
|
+
# In the case of multiple concurrent expiry operations, it is desirable to
|
27
|
+
# reduce the overlap of entries being addressed by each. For that reason,
|
28
|
+
# retrieve more ids than are being expired, and use random
|
29
|
+
# sampling to reduce that number to the actual intended count.
|
30
|
+
retrieve_count = count * 3
|
31
|
+
|
32
|
+
uncached do
|
33
|
+
candidates = order(:id).limit(retrieve_count)
|
34
|
+
|
35
|
+
candidate_ids = if cache_full
|
36
|
+
candidates.pluck(:id)
|
37
|
+
else
|
38
|
+
min_created_at = max_age.seconds.ago
|
39
|
+
candidates.pluck(:id, :created_at)
|
40
|
+
.filter_map { |id, created_at| id if created_at < min_created_at }
|
41
|
+
end
|
42
|
+
|
43
|
+
candidate_ids.sample(count)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,8 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Entry < Record
|
3
|
-
|
4
|
-
|
5
|
-
|
5
|
+
include Expiration
|
6
|
+
|
7
|
+
ID_BYTE_SIZE = 8
|
8
|
+
CREATED_AT_BYTE_SIZE = 8
|
9
|
+
KEY_HASH_BYTE_SIZE = 8
|
10
|
+
VALUE_BYTE_SIZE = 4
|
11
|
+
FIXED_SIZE_COLUMNS_BYTE_SIZE = ID_BYTE_SIZE + CREATED_AT_BYTE_SIZE + KEY_HASH_BYTE_SIZE + VALUE_BYTE_SIZE
|
12
|
+
|
13
|
+
self.ignored_columns += [ :key_hash, :byte_size] if SolidCache.key_hash_stage == :ignored
|
14
|
+
|
6
15
|
class << self
|
7
16
|
def write(key, value)
|
8
17
|
upsert_all_no_query_cache([ { key: key, value: value } ])
|
@@ -13,23 +22,23 @@ module SolidCache
|
|
13
22
|
end
|
14
23
|
|
15
24
|
def read(key)
|
16
|
-
select_all_no_query_cache(get_sql,
|
25
|
+
result = select_all_no_query_cache(get_sql, lookup_value(key)).first
|
26
|
+
result[1] if result&.first == key
|
17
27
|
end
|
18
28
|
|
19
29
|
def read_multi(keys)
|
20
|
-
|
21
|
-
select_all_no_query_cache(get_all_sql(
|
30
|
+
key_hashes = keys.map { |key| lookup_value(key) }
|
31
|
+
results = select_all_no_query_cache(get_all_sql(key_hashes), key_hashes).to_h
|
32
|
+
results.except!(results.keys - keys)
|
22
33
|
end
|
23
34
|
|
24
35
|
def delete_by_key(key)
|
25
|
-
delete_no_query_cache(
|
36
|
+
delete_no_query_cache(lookup_column, lookup_value(key))
|
26
37
|
end
|
27
38
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
delete_no_query_cache(:id, entries.map(&:id))
|
32
|
-
end
|
39
|
+
def delete_multi(keys)
|
40
|
+
serialized_keys = keys.map { |key| lookup_value(key) }
|
41
|
+
delete_no_query_cache(lookup_column, serialized_keys)
|
33
42
|
end
|
34
43
|
|
35
44
|
def clear_truncate
|
@@ -43,7 +52,8 @@ module SolidCache
|
|
43
52
|
def increment(key, amount)
|
44
53
|
transaction do
|
45
54
|
uncached do
|
46
|
-
|
55
|
+
result = lock.where(lookup_column => lookup_value(key)).pick(:key, :value)
|
56
|
+
amount += result[1].to_i if result&.first == key
|
47
57
|
write(key, amount)
|
48
58
|
amount
|
49
59
|
end
|
@@ -54,48 +64,86 @@ module SolidCache
|
|
54
64
|
increment(key, -amount)
|
55
65
|
end
|
56
66
|
|
57
|
-
def id_range
|
58
|
-
uncached do
|
59
|
-
pick(Arel.sql("max(id) - min(id) + 1")) || 0
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def expire(count, max_age:, max_entries:)
|
64
|
-
if (ids = expiry_candidate_ids(count, max_age: max_age, max_entries: max_entries)).any?
|
65
|
-
delete(ids)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
67
|
private
|
70
|
-
def upsert_all_no_query_cache(
|
71
|
-
insert_all = ActiveRecord::InsertAll.new(
|
68
|
+
def upsert_all_no_query_cache(payloads)
|
69
|
+
insert_all = ActiveRecord::InsertAll.new(
|
70
|
+
self,
|
71
|
+
add_key_hash_and_byte_size(payloads),
|
72
|
+
unique_by: upsert_unique_by,
|
73
|
+
on_duplicate: :update,
|
74
|
+
update_only: upsert_update_only
|
75
|
+
)
|
72
76
|
sql = connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(insert_all))
|
73
77
|
|
74
78
|
message = +"#{self} "
|
75
|
-
message << "Bulk " if
|
79
|
+
message << "Bulk " if payloads.many?
|
76
80
|
message << "Upsert"
|
77
81
|
# exec_query_method does not clear the query cache, exec_insert_all does
|
78
82
|
connection.send exec_query_method, sql, message
|
79
83
|
end
|
80
84
|
|
85
|
+
def add_key_hash_and_byte_size(payloads)
|
86
|
+
payloads.map do |payload|
|
87
|
+
payload.dup.tap do |payload|
|
88
|
+
if key_hash?
|
89
|
+
payload[:key_hash] = key_hash_for(payload[:key])
|
90
|
+
payload[:byte_size] = byte_size_for(payload)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def key_hash?
|
97
|
+
@key_hash ||= [ :indexed, :unindexed ].include?(SolidCache.key_hash_stage) &&
|
98
|
+
connection.column_exists?(table_name, :key_hash)
|
99
|
+
end
|
100
|
+
|
101
|
+
def key_hash_indexed?
|
102
|
+
key_hash? && SolidCache.key_hash_stage == :indexed
|
103
|
+
end
|
104
|
+
|
105
|
+
def lookup_column
|
106
|
+
key_hash_indexed? ? :key_hash : :key
|
107
|
+
end
|
108
|
+
|
109
|
+
def lookup_value(key)
|
110
|
+
key_hash_indexed? ? key_hash_for(key) : to_binary(key)
|
111
|
+
end
|
112
|
+
|
113
|
+
def lookup_placeholder
|
114
|
+
key_hash_indexed? ? 1 : "placeholder"
|
115
|
+
end
|
116
|
+
|
81
117
|
def exec_query_method
|
82
118
|
connection.respond_to?(:internal_exec_query) ? :internal_exec_query : :exec_query
|
83
119
|
end
|
84
120
|
|
85
121
|
def upsert_unique_by
|
86
|
-
connection.supports_insert_conflict_target? ?
|
122
|
+
connection.supports_insert_conflict_target? ? lookup_column : nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def upsert_update_only
|
126
|
+
if key_hash_indexed?
|
127
|
+
[ :key, :value, :byte_size ]
|
128
|
+
elsif key_hash?
|
129
|
+
[ :value, :key_hash, :byte_size ]
|
130
|
+
else
|
131
|
+
[ :value ]
|
132
|
+
end
|
87
133
|
end
|
88
134
|
|
89
135
|
def get_sql
|
90
|
-
@get_sql ||=
|
136
|
+
@get_sql ||= {}
|
137
|
+
@get_sql[lookup_column] ||= build_sql(where(lookup_column => lookup_placeholder).select(:key, :value))
|
91
138
|
end
|
92
139
|
|
93
|
-
def get_all_sql(
|
140
|
+
def get_all_sql(key_hashes)
|
94
141
|
if connection.prepared_statements?
|
95
142
|
@get_all_sql_binds ||= {}
|
96
|
-
@get_all_sql_binds[
|
143
|
+
@get_all_sql_binds[[key_hashes.count, lookup_column]] ||= build_sql(where(lookup_column => key_hashes).select(:key, :value))
|
97
144
|
else
|
98
|
-
@get_all_sql_no_binds ||=
|
145
|
+
@get_all_sql_no_binds ||= {}
|
146
|
+
@get_all_sql_no_binds[lookup_column] ||= build_sql(where(lookup_column => [ lookup_placeholder, lookup_placeholder ]).select(:key, :value)).gsub("?, ?", "?")
|
99
147
|
end
|
100
148
|
end
|
101
149
|
|
@@ -113,7 +161,7 @@ module SolidCache
|
|
113
161
|
if connection.prepared_statements?
|
114
162
|
result = connection.select_all(sanitize_sql(query), "#{name} Load", Array(values), preparable: true)
|
115
163
|
else
|
116
|
-
result = connection.select_all(sanitize_sql([ query, values ]), "#{name} Load",
|
164
|
+
result = connection.select_all(sanitize_sql([ query, values ]), "#{name} Load", Array(values), preparable: false)
|
117
165
|
end
|
118
166
|
|
119
167
|
result.cast_values(SolidCache::Entry.attribute_types)
|
@@ -138,17 +186,13 @@ module SolidCache
|
|
138
186
|
ActiveModel::Type::Binary.new.serialize(key)
|
139
187
|
end
|
140
188
|
|
141
|
-
def
|
142
|
-
|
143
|
-
|
189
|
+
def key_hash_for(key)
|
190
|
+
# Need to unpack this as a signed integer - Postgresql and SQLite don't support unsigned integers
|
191
|
+
Digest::SHA256.digest(key.to_s).unpack("q>").first
|
192
|
+
end
|
144
193
|
|
145
|
-
|
146
|
-
|
147
|
-
.limit(count * 3)
|
148
|
-
.pluck(:id, :created_at)
|
149
|
-
.filter_map { |id, created_at| id if cache_full || created_at < min_created_at }
|
150
|
-
.sample(count)
|
151
|
-
end
|
194
|
+
def byte_size_for(payload)
|
195
|
+
payload[:key].to_s.bytesize + payload[:value].to_s.bytesize + FIXED_SIZE_COLUMNS_BYTE_SIZE
|
152
196
|
end
|
153
197
|
end
|
154
198
|
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
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class AddKeyHashAndByteSizeIndexesAndNullConstraintsToSolidCacheEntries < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
change_table :solid_cache_entries, bulk: true do |t|
|
4
|
+
t.change_null :key_hash, false
|
5
|
+
t.change_null :byte_size, false
|
6
|
+
t.index :key_hash, unique: true
|
7
|
+
t.index [:key_hash, :byte_size]
|
8
|
+
t.index :byte_size
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Cluster
|
3
5
|
module Execution
|
4
6
|
def initialize(options = {})
|
5
7
|
super(options)
|
6
|
-
@background = Concurrent::
|
8
|
+
@background = Concurrent::FixedThreadPool.new(1, max_queue: 100, fallback_policy: :discard)
|
7
9
|
@active_record_instrumentation = options.fetch(:active_record_instrumentation, true)
|
8
10
|
end
|
9
11
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "concurrent/atomic/atomic_fixnum"
|
2
4
|
|
3
5
|
module SolidCache
|
@@ -7,13 +9,14 @@ module SolidCache
|
|
7
9
|
# This ensures there is downward pressure on the cache size while there is valid data to delete
|
8
10
|
EXPIRY_MULTIPLIER = 1.25
|
9
11
|
|
10
|
-
attr_reader :expiry_batch_size, :expiry_method, :
|
12
|
+
attr_reader :expiry_batch_size, :expiry_method, :expiry_queue, :expires_per_write, :max_age, :max_entries
|
11
13
|
|
12
14
|
def initialize(options = {})
|
13
15
|
super(options)
|
14
16
|
@expiry_batch_size = options.fetch(:expiry_batch_size, 100)
|
15
17
|
@expiry_method = options.fetch(:expiry_method, :thread)
|
16
|
-
@
|
18
|
+
@expiry_queue = options.fetch(:expiry_queue, :default)
|
19
|
+
@expires_per_write = (1 / expiry_batch_size.to_f) * EXPIRY_MULTIPLIER
|
17
20
|
@max_age = options.fetch(:max_age, 2.weeks.to_i)
|
18
21
|
@max_entries = options.fetch(:max_entries, nil)
|
19
22
|
|
@@ -21,41 +24,26 @@ module SolidCache
|
|
21
24
|
end
|
22
25
|
|
23
26
|
def track_writes(count)
|
24
|
-
|
27
|
+
expiry_batches(count).times { expire_later }
|
25
28
|
end
|
26
29
|
|
27
30
|
private
|
31
|
+
def expiry_batches(count)
|
32
|
+
batches = (count * expires_per_write).floor
|
33
|
+
overflow_batch_chance = count * expires_per_write - batches
|
34
|
+
batches += 1 if rand < overflow_batch_chance
|
35
|
+
batches
|
36
|
+
end
|
37
|
+
|
28
38
|
def expire_later
|
29
39
|
if expiry_method == :job
|
30
|
-
ExpiryJob
|
40
|
+
ExpiryJob
|
41
|
+
.set(queue: expiry_queue)
|
42
|
+
.perform_later(expiry_batch_size, shard: Entry.current_shard, max_age: max_age, max_entries: max_entries)
|
31
43
|
else
|
32
44
|
async { Entry.expire(expiry_batch_size, max_age: max_age, max_entries: max_entries) }
|
33
45
|
end
|
34
46
|
end
|
35
|
-
|
36
|
-
def expiry_counter
|
37
|
-
@expiry_counters ||= connection_names.to_h { |connection_name| [ connection_name, Counter.new(expire_every) ] }
|
38
|
-
@expiry_counters[Entry.current_shard]
|
39
|
-
end
|
40
|
-
|
41
|
-
class Counter
|
42
|
-
attr_reader :expire_every, :counter
|
43
|
-
|
44
|
-
def initialize(expire_every)
|
45
|
-
@expire_every = expire_every
|
46
|
-
@counter = Concurrent::AtomicFixnum.new(rand(expire_every).to_i)
|
47
|
-
end
|
48
|
-
|
49
|
-
def count(count)
|
50
|
-
value = counter.increment(count)
|
51
|
-
new_multiple_of_expire_every?(value - count, value)
|
52
|
-
end
|
53
|
-
|
54
|
-
private
|
55
|
-
def new_multiple_of_expire_every?(first_value, second_value)
|
56
|
-
first_value / expire_every != second_value / expire_every
|
57
|
-
end
|
58
|
-
end
|
59
47
|
end
|
60
48
|
end
|
61
49
|
end
|
data/lib/solid_cache/cluster.rb
CHANGED
data/lib/solid_cache/engine.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_support"
|
2
4
|
|
3
5
|
module SolidCache
|
@@ -11,6 +13,12 @@ module SolidCache
|
|
11
13
|
|
12
14
|
SolidCache.executor = config.solid_cache.executor
|
13
15
|
SolidCache.connects_to = config.solid_cache.connects_to
|
16
|
+
if config.solid_cache.key_hash_stage
|
17
|
+
unless [:ignored, :unindexed, :indexed].include?(config.solid_cache.key_hash_stage)
|
18
|
+
raise "ArgumentError, :key_hash_stage must be :ignored, :unindexed or :indexed"
|
19
|
+
end
|
20
|
+
SolidCache.key_hash_stage = config.solid_cache.key_hash_stage
|
21
|
+
end
|
14
22
|
end
|
15
23
|
|
16
24
|
config.after_initialize do
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Store
|
3
5
|
module Api
|
@@ -12,20 +14,6 @@ module SolidCache
|
|
12
14
|
@max_key_bytesize = options.fetch(:max_key_bytesize, DEFAULT_MAX_KEY_BYTESIZE)
|
13
15
|
end
|
14
16
|
|
15
|
-
def delete_matched(matcher, options = {})
|
16
|
-
instrument :delete_matched, matcher do
|
17
|
-
raise ArgumentError, "Only strings are supported: #{matcher.inspect}" unless String === matcher
|
18
|
-
raise ArgumentError, "Strings cannot start with wildcards" if SQL_WILDCARD_CHARS.include?(matcher[0])
|
19
|
-
|
20
|
-
options ||= {}
|
21
|
-
batch_size = options.fetch(:batch_size, 1000)
|
22
|
-
|
23
|
-
matcher = namespace_key(matcher, options)
|
24
|
-
|
25
|
-
entry_delete_matched(matcher, batch_size)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
17
|
def increment(name, amount = 1, options = nil)
|
30
18
|
options = merged_options(options)
|
31
19
|
key = normalize_key(name, options)
|
@@ -74,7 +62,7 @@ module SolidCache
|
|
74
62
|
end
|
75
63
|
|
76
64
|
def read_multi_entries(names, **options)
|
77
|
-
keys_and_names = names.
|
65
|
+
keys_and_names = names.index_by { |name| normalize_key(name, options) }
|
78
66
|
serialized_entries = read_serialized_entries(keys_and_names.keys)
|
79
67
|
|
80
68
|
keys_and_names.each_with_object({}) do |(key, name), results|
|
@@ -117,7 +105,7 @@ module SolidCache
|
|
117
105
|
end
|
118
106
|
|
119
107
|
def delete_multi_entries(entries, **options)
|
120
|
-
entries.
|
108
|
+
entry_delete_multi(entries).compact.sum
|
121
109
|
end
|
122
110
|
|
123
111
|
def serialize_entry(entry, raw: false, **options)
|
@@ -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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SolidCache
|
2
4
|
class Store
|
3
5
|
module Entries
|
@@ -15,12 +17,6 @@ module SolidCache
|
|
15
17
|
end
|
16
18
|
|
17
19
|
private
|
18
|
-
def entry_delete_matched(matcher, batch_size)
|
19
|
-
writing_all(failsafe: :delete_matched) do
|
20
|
-
Entry.delete_matched(matcher, batch_size: batch_size)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
20
|
def entry_clear
|
25
21
|
writing_all(failsafe: :clear) do
|
26
22
|
if clear_with == :truncate
|
@@ -76,6 +72,12 @@ module SolidCache
|
|
76
72
|
Entry.delete_by_key(key)
|
77
73
|
end
|
78
74
|
end
|
75
|
+
|
76
|
+
def entry_delete_multi(entries)
|
77
|
+
writing_keys(entries, failsafe: :delete_multi_entries, failsafe_returning: false) do
|
78
|
+
Entry.delete_multi(entries)
|
79
|
+
end
|
80
|
+
end
|
79
81
|
end
|
80
82
|
end
|
81
83
|
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
|
|
@@ -8,6 +10,7 @@ loader.setup
|
|
8
10
|
|
9
11
|
module SolidCache
|
10
12
|
mattr_accessor :executor, :connects_to
|
13
|
+
mattr_accessor :key_hash_stage, default: :indexed
|
11
14
|
|
12
15
|
def self.all_shard_keys
|
13
16
|
all_shards_config&.keys || []
|
@@ -17,12 +20,12 @@ module SolidCache
|
|
17
20
|
connects_to && connects_to[:shards]
|
18
21
|
end
|
19
22
|
|
20
|
-
def self.each_shard
|
23
|
+
def self.each_shard(&block)
|
21
24
|
return to_enum(:each_shard) unless block_given?
|
22
25
|
|
23
26
|
if (shards = all_shards_config&.keys)
|
24
27
|
shards.each do |shard|
|
25
|
-
Record.
|
28
|
+
Record.with_shard(shard, &block)
|
26
29
|
end
|
27
30
|
else
|
28
31
|
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.4.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-25 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
|
- - ">="
|
@@ -64,8 +92,12 @@ files:
|
|
64
92
|
- Rakefile
|
65
93
|
- app/jobs/solid_cache/expiry_job.rb
|
66
94
|
- app/models/solid_cache/entry.rb
|
95
|
+
- app/models/solid_cache/entry/expiration.rb
|
67
96
|
- app/models/solid_cache/record.rb
|
68
97
|
- db/migrate/20230724121448_create_solid_cache_entries.rb
|
98
|
+
- db/migrate/20240108155507_add_key_hash_and_byte_size_to_solid_cache_entries.rb
|
99
|
+
- db/migrate/20240110111600_add_key_hash_and_byte_size_indexes_and_null_constraints_to_solid_cache_entries.rb
|
100
|
+
- db/migrate/20240110111702_remove_key_index_from_solid_cache_entries.rb
|
69
101
|
- lib/active_support/cache/solid_cache_store.rb
|
70
102
|
- lib/generators/solid_cache/install/USAGE
|
71
103
|
- lib/generators/solid_cache/install/install_generator.rb
|
@@ -94,7 +126,9 @@ licenses:
|
|
94
126
|
metadata:
|
95
127
|
homepage_uri: http://github.com/rails/solid_cache
|
96
128
|
source_code_uri: http://github.com/rails/solid_cache
|
97
|
-
post_install_message:
|
129
|
+
post_install_message: |
|
130
|
+
Solid Cache v0.4 contains new database migrations.
|
131
|
+
See https://github.com/rails/solid_cache/blob/main/upgrading_to_version_0.4.x.md for upgrade instructions.
|
98
132
|
rdoc_options: []
|
99
133
|
require_paths:
|
100
134
|
- lib
|
@@ -109,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
143
|
- !ruby/object:Gem::Version
|
110
144
|
version: '0'
|
111
145
|
requirements: []
|
112
|
-
rubygems_version: 3.4
|
146
|
+
rubygems_version: 3.5.4
|
113
147
|
signing_key:
|
114
148
|
specification_version: 4
|
115
149
|
summary: A database backed ActiveSupport::Cache::Store
|