solid_cache 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98f4ca743bf44353244b4e1bb870df48b426fcb8b082941ad0f07e150f05cffb
4
- data.tar.gz: 85a3660e7c8b63b8f838039228b887f6a4ecde5d17401707f243804efb7f98d6
3
+ metadata.gz: d92ba4d7c045821d0c54facd1329613e497a46a2b21daffe3a14931bf9621f86
4
+ data.tar.gz: 46c4b4b81c48a5a5598cc2460e8c1159e69497f130116e9f855a58f8f3e52a36
5
5
  SHA512:
6
- metadata.gz: 2251bc348573da1c8492f299cd7fadbd87450ad22252ae388b25c842b71bf6cf6525d40243587d3c2951c8af1a71fb0ac42af6eedde767465ab08a7cfcfbe9b6
7
- data.tar.gz: 1e69812734160e29b7ae9430303243f23f09f24a75440da61e7dd5d0efd0ac6e4934e0d7a6566791c60f3cf1d88e8d45364f9904bd60b742f16aa8241e0c1fbb
6
+ metadata.gz: 4f3505047c30f24d6f8928d439172f826becc79054a05f32b0c670b3cbb227a10b52889b1e64e2430fe2d279e98c6615bb92634c475fdb6016cd7d3c9c170c07
7
+ data.tar.gz: 4cb0c61cddd64a4d8093b73b06524ae5585cf5d0c70233bd31fd688b837d38c500df28479bbbb7ffa99b24e26dc3230789dad0d108f9d7fc3df4e02b0eeb0d55
data/README.md CHANGED
@@ -80,12 +80,12 @@ 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`)
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
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? then `delete`)
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)
@@ -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
- ```ruby
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: [ :cache_primary_shard1, :cache_primary_shard2 ] }
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
  ```
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/setup"
2
4
 
3
5
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  class ExpiryJob < ActiveJob::Base
3
5
  def perform(count, shard: nil, max_age:, max_entries:)
@@ -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
@@ -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", nil, preparable: false)
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
- min_created_at = max_age.seconds.ago
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
- .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)
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 **SolidCache.connects_to if SolidCache.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) do
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 && shard != Record.current_shard
18
- Record.connected_to(shard: shard, &block)
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
  class CreateSolidCacheEntries < ActiveRecord::Migration[7.0]
2
4
  def change
3
5
  create_table :solid_cache_entries do |t|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveSupport
2
4
  module Cache
3
5
  SolidCacheStore = SolidCache::Store
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class SolidCache::InstallGenerator < Rails::Generators::Base
2
4
  class_option :skip_migrations, type: :boolean, default: nil,
3
5
  desc: "Skip migrations"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  class Cluster
3
5
  module Connections
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  class Cluster
3
5
  module Execution
@@ -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.to_h { |connection_name| [ connection_name, Counter.new(expire_every) ] }
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
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  class Cluster
3
5
  module Stats
@@ -6,7 +8,7 @@ module SolidCache
6
8
  end
7
9
 
8
10
  def stats
9
- stats = {
11
+ {
10
12
  connections: connections.count,
11
13
  connection_stats: connections_stats
12
14
  }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module SolidCache
3
4
  class Cluster
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  module Connections
3
5
  class Sharded
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  module Connections
3
5
  class Single
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  module Connections
3
5
  class Unmanaged
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  module Connections
3
5
  def self.from_config(options)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support"
2
4
 
3
5
  module SolidCache
@@ -1,3 +1,5 @@
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
@@ -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.to_h { |name| [ normalize_key(name, options), name ] }
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|
@@ -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) do
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) do
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  class Store
3
5
  module Failsafe
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SolidCache
2
4
  class Store < ActiveSupport::Cache::Store
3
5
  include Api, Clusters, Entries, Failsafe
@@ -1,3 +1,3 @@
1
1
  module SolidCache
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
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.connected_to(shard: shard) { yield }
27
+ Record.with_shard(shard, &block)
26
28
  end
27
29
  else
28
30
  yield
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  desc "Copy over the migration, and set cache"
2
4
  namespace :solid_cache do
3
5
  task :install do
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.2.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: 2023-11-16 00:00:00.000000000 Z
11
+ date: 2024-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
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.4.21
140
+ rubygems_version: 3.5.1
113
141
  signing_key:
114
142
  specification_version: 4
115
143
  summary: A database backed ActiveSupport::Cache::Store