solid_cache_mongoid 0.1.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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +189 -0
  4. data/Rakefile +48 -0
  5. data/app/jobs/solid_cache_mongoid/expiry_job.rb +9 -0
  6. data/app/models/solid_cache_mongoid/entry/expiration.rb +55 -0
  7. data/app/models/solid_cache_mongoid/entry/size/estimate.rb +133 -0
  8. data/app/models/solid_cache_mongoid/entry/size/moving_average_estimate.rb +62 -0
  9. data/app/models/solid_cache_mongoid/entry/size.rb +21 -0
  10. data/app/models/solid_cache_mongoid/entry.rb +145 -0
  11. data/app/models/solid_cache_mongoid/record.rb +77 -0
  12. data/lib/active_support/cache/solid_cache_mongoid_store.rb +9 -0
  13. data/lib/generators/solid_cache/install/USAGE +9 -0
  14. data/lib/generators/solid_cache/install/install_generator.rb +20 -0
  15. data/lib/generators/solid_cache/install/templates/config/cache.yml.tt +20 -0
  16. data/lib/generators/solid_cache/install/templates/db/cache_schema.rb +12 -0
  17. data/lib/generators/solid_cache/install/templates/db/cache_structure.mysql.sql +56 -0
  18. data/lib/generators/solid_cache/install/templates/db/cache_structure.postgresql.sql +128 -0
  19. data/lib/generators/solid_cache/install/templates/db/cache_structure.sqlite3.sql +6 -0
  20. data/lib/solid_cache_mongoid/configuration.rb +31 -0
  21. data/lib/solid_cache_mongoid/connections/unmanaged.rb +33 -0
  22. data/lib/solid_cache_mongoid/connections.rb +9 -0
  23. data/lib/solid_cache_mongoid/engine.rb +38 -0
  24. data/lib/solid_cache_mongoid/store/api.rb +178 -0
  25. data/lib/solid_cache_mongoid/store/connections.rb +88 -0
  26. data/lib/solid_cache_mongoid/store/entries.rb +79 -0
  27. data/lib/solid_cache_mongoid/store/execution.rb +52 -0
  28. data/lib/solid_cache_mongoid/store/expiry.rb +49 -0
  29. data/lib/solid_cache_mongoid/store/failsafe.rb +39 -0
  30. data/lib/solid_cache_mongoid/store/stats.rb +30 -0
  31. data/lib/solid_cache_mongoid/store.rb +20 -0
  32. data/lib/solid_cache_mongoid/version.rb +5 -0
  33. data/lib/solid_cache_mongoid.rb +16 -0
  34. data/lib/tasks/solid_cache_tasks.rake +25 -0
  35. metadata +201 -0
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ class Store
5
+ module Connections
6
+ def initialize(options = {})
7
+ super(options)
8
+ end
9
+
10
+ def with_each_connection(async: false, &block)
11
+ return enum_for(:with_each_connection) unless block_given?
12
+
13
+ connections.with_each do
14
+ execute(async, &block)
15
+ end
16
+ end
17
+
18
+ def connections
19
+ @connections ||= SolidCacheMongoid::Connections.from_config
20
+ end
21
+
22
+ private
23
+ def setup!
24
+ connections
25
+ end
26
+
27
+ def with_connection_for(key, async: false, &block)
28
+ connections.with_connection_for(key) do
29
+ execute(async, &block)
30
+ end
31
+ end
32
+
33
+ def with_connection(name, async: false, &block)
34
+ connections.with(name) do
35
+ execute(async, &block)
36
+ end
37
+ end
38
+
39
+ def group_by_connection(keys)
40
+ connections.assign(keys)
41
+ end
42
+
43
+ def connection_names
44
+ connections.names
45
+ end
46
+
47
+ def reading_key(key, failsafe:, failsafe_returning: nil, &block)
48
+ failsafe(failsafe, returning: failsafe_returning) do
49
+ with_connection_for(key, &block)
50
+ end
51
+ end
52
+
53
+ def reading_keys(keys, failsafe:, failsafe_returning: nil)
54
+ group_by_connection(keys).map do |connection, grouped_keys|
55
+ failsafe(failsafe, returning: failsafe_returning) do
56
+ with_connection(connection) do
57
+ yield grouped_keys
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def writing_key(key, failsafe:, failsafe_returning: nil, &block)
64
+ failsafe(failsafe, returning: failsafe_returning) do
65
+ with_connection_for(key, &block)
66
+ end
67
+ end
68
+
69
+ def writing_keys(entries, failsafe:, failsafe_returning: nil)
70
+ group_by_connection(entries).map do |connection, grouped_entries|
71
+ failsafe(failsafe, returning: failsafe_returning) do
72
+ with_connection(connection) do
73
+ yield grouped_entries
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def writing_all(failsafe:, failsafe_returning: nil, &block)
80
+ connection_names.map do |connection|
81
+ failsafe(failsafe, returning: failsafe_returning) do
82
+ with_connection(connection, &block)
83
+ end
84
+ end.first
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ class Store
5
+ module Entries
6
+ attr_reader :clear_with
7
+
8
+ def initialize(options = {})
9
+ super(options)
10
+
11
+ # Truncating in test mode breaks transactional tests in MySQL (not in Postgres though)
12
+ @clear_with = options.fetch(:clear_with) { Rails.env.test? ? :delete : :truncate }&.to_sym
13
+
14
+ unless [ :truncate, :delete ].include?(clear_with)
15
+ raise ArgumentError, "`clear_with` must be either ``:truncate`` or ``:delete`"
16
+ end
17
+ end
18
+
19
+ private
20
+ def entry_clear
21
+ writing_all(failsafe: :clear, failsafe_returning: nil) do
22
+ if clear_with == :truncate
23
+ Entry.clear_truncate
24
+ else
25
+ Entry.clear_delete
26
+ end
27
+ end
28
+ end
29
+
30
+ def entry_lock_and_write(key, &block)
31
+ writing_key(key, failsafe: :increment) do
32
+ Entry.lock_and_write(key) do |value|
33
+ block.call(value).tap { |result| track_writes(1) if result }
34
+ end
35
+ end
36
+ end
37
+
38
+ def entry_read(key)
39
+ reading_key(key, failsafe: :read_entry) do
40
+ Entry.read(key)
41
+ end
42
+ end
43
+
44
+ def entry_read_multi(keys)
45
+ reading_keys(keys, failsafe: :read_multi_mget, failsafe_returning: {}) do |keys|
46
+ Entry.read_multi(keys)
47
+ end
48
+ end
49
+
50
+ def entry_write(key, payload)
51
+ writing_key(key, failsafe: :write_entry, failsafe_returning: nil) do
52
+ Entry.write(key, payload)
53
+ track_writes(1)
54
+ true
55
+ end
56
+ end
57
+
58
+ def entry_write_multi(entries)
59
+ writing_keys(entries, failsafe: :write_multi_entries, failsafe_returning: false) do |entries|
60
+ Entry.write_multi(entries)
61
+ track_writes(entries.count)
62
+ true
63
+ end
64
+ end
65
+
66
+ def entry_delete(key)
67
+ writing_key(key, failsafe: :delete_entry, failsafe_returning: false) do
68
+ Entry.delete_by_key(key) > 0
69
+ end
70
+ end
71
+
72
+ def entry_delete_multi(entries)
73
+ writing_keys(entries, failsafe: :delete_multi_entries, failsafe_returning: 0) do
74
+ Entry.delete_by_key(*entries)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ class Store
5
+ module Execution
6
+ def initialize(options = {})
7
+ super(options)
8
+ @background = Concurrent::FixedThreadPool.new(1, max_queue: 100, fallback_policy: :discard)
9
+ @active_record_instrumentation = options.fetch(:active_record_instrumentation, true)
10
+ end
11
+
12
+ private
13
+ def async(&block)
14
+ @background << ->() do
15
+ wrap_in_rails_executor do
16
+ setup_instrumentation(&block)
17
+ end
18
+ rescue Exception => exception
19
+ error_handler&.call(method: :async, exception: exception, returning: nil)
20
+ end
21
+ end
22
+
23
+ def execute(async, &block)
24
+ if async
25
+ async(&block)
26
+ else
27
+ setup_instrumentation(&block)
28
+ end
29
+ end
30
+
31
+ def wrap_in_rails_executor(&block)
32
+ if SolidCacheMongoid.executor
33
+ SolidCacheMongoid.executor.wrap(&block)
34
+ else
35
+ block.call
36
+ end
37
+ end
38
+
39
+ def active_record_instrumentation?
40
+ @active_record_instrumentation
41
+ end
42
+
43
+ def setup_instrumentation(&block)
44
+ if active_record_instrumentation?
45
+ block.call
46
+ else
47
+ Entry.disable_instrumentation(&block)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ class Store
5
+ module Expiry
6
+ # For every write that we do, we attempt to delete EXPIRY_MULTIPLIER times as many records.
7
+ # This ensures there is downward pressure on the cache size while there is valid data to delete
8
+ EXPIRY_MULTIPLIER = 2
9
+
10
+ attr_reader :expiry_batch_size, :expiry_method, :expiry_queue, :expires_per_write, :max_age, :max_entries, :max_size
11
+
12
+ def initialize(options = {})
13
+ super(options)
14
+ @expiry_batch_size = options.fetch(:expiry_batch_size, 100)
15
+ @expiry_method = options.fetch(:expiry_method, :thread)
16
+ @expiry_queue = options.fetch(:expiry_queue, :default)
17
+ @expires_per_write = (1 / expiry_batch_size.to_f) * EXPIRY_MULTIPLIER
18
+ @max_age = options.fetch(:max_age, 2.weeks.to_i)
19
+ @max_entries = options.fetch(:max_entries, nil)
20
+ @max_size = options.fetch(:max_size, nil)
21
+
22
+ raise ArgumentError, "Expiry method must be one of `:thread` or `:job`" unless [ :thread, :job ].include?(expiry_method)
23
+ end
24
+
25
+ def track_writes(count)
26
+ expiry_batches(count).times { expire_later }
27
+ end
28
+
29
+ private
30
+ def expiry_batches(count)
31
+ batches = (count * expires_per_write).floor
32
+ overflow_batch_chance = count * expires_per_write - batches
33
+ batches += 1 if rand < overflow_batch_chance
34
+ batches
35
+ end
36
+
37
+ def expire_later
38
+ max_options = { max_age: max_age, max_entries: max_entries, max_size: max_size }
39
+ if expiry_method == :job
40
+ ExpiryJob
41
+ .set(queue: expiry_queue)
42
+ .perform_later(expiry_batch_size, **max_options)
43
+ else
44
+ async { Entry.expire(expiry_batch_size, **max_options) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ class Store
5
+ module Failsafe
6
+ TRANSIENT_MONGOID_ERRORS = [
7
+ ::Mongo::Error::SocketError,
8
+ ::Mongo::Error::SocketTimeoutError,
9
+ ::Mongo::Error::NoServerAvailable,
10
+ ::Mongo::Error::OperationFailure,
11
+ ::Mongo::Error::TimeoutError,
12
+ ::Mongo::Error::ServerTimeoutError
13
+ ]
14
+
15
+ DEFAULT_ERROR_HANDLER = ->(method:, returning:, exception:) do
16
+ if Store.logger
17
+ Store.logger.error { "SolidCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
18
+ end
19
+ end
20
+
21
+ def initialize(options = {})
22
+ super(options)
23
+
24
+ @error_handler = options.fetch(:error_handler, DEFAULT_ERROR_HANDLER)
25
+ end
26
+
27
+ private
28
+ attr_reader :error_handler
29
+
30
+ def failsafe(method, returning: nil)
31
+ yield
32
+ rescue *TRANSIENT_MONGOID_ERRORS => error
33
+ ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
34
+ error_handler&.call(method: method, exception: error, returning: returning)
35
+ returning
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ class Store
5
+ module Stats
6
+ def initialize(options = {})
7
+ super(options)
8
+ end
9
+
10
+ def stats
11
+ {
12
+ connections: 1,
13
+ connection_stats: connection_stats
14
+ }
15
+ end
16
+
17
+ private
18
+ def connection_stats
19
+ oldest_created_at = Entry.order_by([:id, :asc]).pick(:created_at)
20
+
21
+ {
22
+ max_age: max_age,
23
+ oldest_age: oldest_created_at ? Time.now - oldest_created_at : nil,
24
+ max_entries: max_entries,
25
+ entries: Entry.id_range
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ class Store < ActiveSupport::Cache::Store
5
+ include Api, Connections, Entries, Execution, Expiry, Failsafe, Stats
6
+ prepend ActiveSupport::Cache::Strategy::LocalCache
7
+
8
+ def initialize(options = {})
9
+ super(SolidCacheMongoid.configuration.store_options.merge(options))
10
+ end
11
+
12
+ def self.supports_cache_versioning?
13
+ true
14
+ end
15
+
16
+ def setup!
17
+ super
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidCacheMongoid
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "solid_cache_mongoid/engine"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.ignore("#{__dir__}/active_support")
8
+ loader.ignore("#{__dir__}/generators")
9
+ loader.setup
10
+
11
+ module SolidCacheMongoid
12
+ mattr_accessor :executor
13
+ mattr_accessor :configuration, default: Configuration.new
14
+ end
15
+
16
+ loader.eager_load
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Copy over the migration, and set cache"
4
+ namespace :solid_cache_mongoid do
5
+ task :install do
6
+ Rails::Command.invoke :generate, [ "solid_cache_mongoid:install" ]
7
+ end
8
+ end
9
+
10
+
11
+ require "solid_cache_mongoid/version"
12
+
13
+ desc "Pushing solid_cache_mongoid-#{SolidCacheMongoid::VERSION}.gem to rubygems"
14
+ task :release do
15
+ package = "pkg/solid_cache_mongoid-#{SolidCacheMongoid::VERSION}.gem"
16
+ ::FURY_CMD = "gem push #{package}"
17
+ ::ERROR_PACKAGE_NOT_FOUND = "Error: gem #{package} is not found"
18
+
19
+ if File.exist? package
20
+ system(FURY_CMD, exception: true)
21
+ else
22
+ STDERR.puts ERROR_PACKAGE_NOT_FOUND
23
+ exit 1
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solid_cache_mongoid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Donal McBreen
8
+ - Duvan Hernandez
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-12-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mongoid
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mongoid-locker
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mongo
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activejob
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '7.2'
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '8.1'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '7.2'
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '8.1'
75
+ - !ruby/object:Gem::Dependency
76
+ name: railties
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '7.2'
82
+ - - "<"
83
+ - !ruby/object:Gem::Version
84
+ version: '8.1'
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '7.2'
92
+ - - "<"
93
+ - !ruby/object:Gem::Version
94
+ version: '8.1'
95
+ - !ruby/object:Gem::Dependency
96
+ name: debug
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: mocha
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ - !ruby/object:Gem::Dependency
124
+ name: msgpack
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ description: A database backed ActiveSupport::Cache::Store
138
+ email:
139
+ - donal@37signals.com
140
+ - duvanherfi@gmail.com
141
+ executables: []
142
+ extensions: []
143
+ extra_rdoc_files: []
144
+ files:
145
+ - MIT-LICENSE
146
+ - README.md
147
+ - Rakefile
148
+ - app/jobs/solid_cache_mongoid/expiry_job.rb
149
+ - app/models/solid_cache_mongoid/entry.rb
150
+ - app/models/solid_cache_mongoid/entry/expiration.rb
151
+ - app/models/solid_cache_mongoid/entry/size.rb
152
+ - app/models/solid_cache_mongoid/entry/size/estimate.rb
153
+ - app/models/solid_cache_mongoid/entry/size/moving_average_estimate.rb
154
+ - app/models/solid_cache_mongoid/record.rb
155
+ - lib/active_support/cache/solid_cache_mongoid_store.rb
156
+ - lib/generators/solid_cache/install/USAGE
157
+ - lib/generators/solid_cache/install/install_generator.rb
158
+ - lib/generators/solid_cache/install/templates/config/cache.yml.tt
159
+ - lib/generators/solid_cache/install/templates/db/cache_schema.rb
160
+ - lib/generators/solid_cache/install/templates/db/cache_structure.mysql.sql
161
+ - lib/generators/solid_cache/install/templates/db/cache_structure.postgresql.sql
162
+ - lib/generators/solid_cache/install/templates/db/cache_structure.sqlite3.sql
163
+ - lib/solid_cache_mongoid.rb
164
+ - lib/solid_cache_mongoid/configuration.rb
165
+ - lib/solid_cache_mongoid/connections.rb
166
+ - lib/solid_cache_mongoid/connections/unmanaged.rb
167
+ - lib/solid_cache_mongoid/engine.rb
168
+ - lib/solid_cache_mongoid/store.rb
169
+ - lib/solid_cache_mongoid/store/api.rb
170
+ - lib/solid_cache_mongoid/store/connections.rb
171
+ - lib/solid_cache_mongoid/store/entries.rb
172
+ - lib/solid_cache_mongoid/store/execution.rb
173
+ - lib/solid_cache_mongoid/store/expiry.rb
174
+ - lib/solid_cache_mongoid/store/failsafe.rb
175
+ - lib/solid_cache_mongoid/store/stats.rb
176
+ - lib/solid_cache_mongoid/version.rb
177
+ - lib/tasks/solid_cache_tasks.rake
178
+ homepage: http://github.com/duvanherfi/solid_cache_mongoid
179
+ licenses:
180
+ - MIT
181
+ metadata:
182
+ homepage_uri: http://github.com/duvanherfi/solid_cache_mongoid
183
+ source_code_uri: http://github.com/duvanherfi/solid_cache_mongoid
184
+ rdoc_options: []
185
+ require_paths:
186
+ - lib
187
+ required_ruby_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ requirements: []
198
+ rubygems_version: 3.6.2
199
+ specification_version: 4
200
+ summary: A database backed ActiveSupport::Cache::Store
201
+ test_files: []