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