rails-fast-cache 1.1.0 → 2.0.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: ca141bb50fad8045f8dd6c9e47a75bca1cebc3c753189bb376eb9bc3fd0ac784
4
- data.tar.gz: c6b28ef4c1bb3ee5702a4904faaad71f783d602bbda2e79bdc6d8c20391a6eff
3
+ metadata.gz: 2527d2eb60179865808c257dc84a64333d37b9c51547ff0f0546b8a4f12b35b9
4
+ data.tar.gz: 1d60656903d8d679d056436b0b1354f3da0d4666eab0a6974b02d20ac558d2af
5
5
  SHA512:
6
- metadata.gz: 3e2ffd591f426ee5ee7edb136ec3cb1fa692601a6804942a9774125ae9389113d13d23297adb3ed1b2be2b924d8f798fe1ae0152ddec193963ea467c0f53dc56
7
- data.tar.gz: 37fc88731af7baa47967f53db7cc4cccc53a64555f00f1df65348b51888a793951d3d8cd1e2d5d92e531246a2475e47a352a7358f09f033ac85567ef99214ce7
6
+ metadata.gz: 2d738b257bca50084e392871acb1235fcc77f333c628a2d37c2e57df4bb942a50baeba8df766e61820484791c643eb7d97ceda98bb846fb6450b66149374f1de
7
+ data.tar.gz: 8d116e967b4d2400e67ff18ddb0b959cf393d776936f6768a973fb9ac513861b2fe8d61c53485a1d252053289f501ee54b864631f179deff29151c876db3f078
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsFastCache
4
+ module AsyncWrites
5
+ attr_accessor :rails_fast_cache_scheduler, :rails_fast_cache_logger
6
+
7
+ def write(name, value, options = nil)
8
+ rails_fast_cache_scheduler.post { perform_async_write { super(name, value, options) } }
9
+ true
10
+ end
11
+
12
+ def write_multi(hash, options = nil)
13
+ rails_fast_cache_scheduler.post { perform_async_write { super(hash, options) } }
14
+ true
15
+ end
16
+
17
+ private
18
+
19
+ def perform_async_write
20
+ yield
21
+ rescue StandardError => e
22
+ rails_fast_cache_logger&.error("[rails-fast-cache] async write failed: #{e.class}: #{e.message}")
23
+ nil
24
+ end
25
+ end
26
+ end
@@ -1,20 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'concurrent'
4
+
3
5
  module RailsFastCache
4
6
  class Scheduler
5
7
  EXECUTOR_OPTIONS = {
6
8
  min_threads: ENV.fetch('RAILS_MAX_THREADS', 3).to_i,
7
9
  max_threads: ENV.fetch('RAILS_MAX_THREADS', 3).to_i,
8
- max_queue: 100,
10
+ max_queue: ENV.fetch('RAILS_FAST_CACHE_MAX_QUEUE', 100).to_i,
9
11
  fallback_policy: :caller_runs
10
12
  }.freeze
11
13
 
12
- def self.queue_adapter
13
- @queue_adapter ||= ActiveJob::QueueAdapters::AsyncAdapter.new(**EXECUTOR_OPTIONS)
14
+ def initialize
15
+ @executor = Concurrent::ThreadPoolExecutor.new(**EXECUTOR_OPTIONS)
16
+ @inflight = Concurrent::AtomicFixnum.new(0)
17
+ @idle = Concurrent::Event.new
18
+ @idle.set
19
+ end
20
+
21
+ def post(&block)
22
+ @inflight.increment
23
+ @idle.reset
24
+ @executor.post do
25
+ block.call
26
+ ensure
27
+ @idle.set if @inflight.decrement.zero?
28
+ end
29
+ end
30
+
31
+ def flush(timeout = nil)
32
+ @idle.wait(timeout)
14
33
  end
15
34
 
16
- def self.shutdown
17
- @queue_adapter&.shutdown(wait: true)
35
+ def shutdown(wait: true)
36
+ @executor.shutdown
37
+ @executor.wait_for_termination if wait
18
38
  end
19
39
  end
20
40
  end
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_job'
4
3
  require 'active_support'
5
4
  require 'active_support/core_ext'
6
5
 
6
+ require_relative 'async_writes'
7
7
  require_relative 'brotli_compressor'
8
8
  require_relative 'scheduler'
9
- require_relative 'write_job'
10
- require_relative 'write_multi_job'
11
9
 
12
10
  module RailsFastCache
13
11
  class Store < ::ActiveSupport::Cache::Store
@@ -22,47 +20,56 @@ module RailsFastCache
22
20
  :fetch,
23
21
  :fetch_multi,
24
22
  :increment,
25
- :key_matcher,
26
23
  :mute,
27
- :new,
24
+ :namespace,
25
+ :namespace=,
26
+ :options,
28
27
  :read,
28
+ :read_counter,
29
29
  :read_multi,
30
+ :silence,
30
31
  :silence!,
32
+ :silence?,
33
+ :write,
34
+ :write_counter,
35
+ :write_multi,
31
36
  to: :@cache_store
32
37
  )
33
38
  delegate_missing_to :@cache_store
34
39
 
35
- cattr_accessor :cache_store
36
-
37
40
  def self.supports_cache_versioning?
38
41
  true
39
42
  end
40
43
 
41
- def self.shutdown
42
- RailsFastCache::Scheduler.shutdown
43
- end
44
-
45
44
  def initialize(cache_store, *parameters)
46
45
  options = parameters.extract_options!
47
46
  options[:compressor] ||= RailsFastCache::BrotliCompressor if !options.key?(:coder) && cache_store != :memory_store
48
47
  options[:serializer] ||= :message_pack unless options.key?(:coder)
49
48
 
50
49
  @cache_store = ActiveSupport::Cache.lookup_store(cache_store, *parameters, **options)
51
- self.class.cache_store = @cache_store
52
- end
50
+ @scheduler = RailsFastCache::Scheduler.new
53
51
 
54
- def write(name, value, options = nil)
55
- WriteJob.perform_later(@cache_store, name, value, options)
56
- true
52
+ unless @cache_store.singleton_class.include?(RailsFastCache::AsyncWrites)
53
+ @cache_store.singleton_class.prepend(RailsFastCache::AsyncWrites)
54
+ end
55
+ @cache_store.rails_fast_cache_scheduler = @scheduler
56
+ @cache_store.rails_fast_cache_logger = @cache_store.logger
57
57
  end
58
58
 
59
- def write_multi(hash, options = nil)
60
- WriteMultiJob.perform_later(@cache_store, hash, options)
61
- true
59
+ def flush(timeout = nil)
60
+ @scheduler.flush(timeout)
62
61
  end
63
62
 
64
63
  def shutdown
65
- self.class.shutdown
64
+ @scheduler.shutdown(wait: true)
65
+ end
66
+
67
+ def write_serialized_entry(...)
68
+ @cache_store.send(:write_serialized_entry)
69
+ end
70
+
71
+ def read_serialized_entry(...)
72
+ @cache_store.send(:read_serialized_entry)
66
73
  end
67
74
  end
68
75
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsFastCache
4
- VERSION = '1.1.0'
4
+ VERSION = '2.0.0'
5
5
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RailsFastCache::AsyncWrites do
6
+ let(:logger) { instance_double('Logger', error: nil) }
7
+
8
+ def build_store(cache_store_sym = :memory_store)
9
+ RailsFastCache::Store.new(cache_store_sym)
10
+ end
11
+
12
+ describe '#write' do
13
+ it 'returns true immediately without waiting for the write to complete' do
14
+ store = build_store
15
+ result = nil
16
+ expect {
17
+ result = store.write('key', 'value')
18
+ }.not_to raise_error
19
+ expect(result).to be(true)
20
+ store.shutdown
21
+ end
22
+
23
+ it 'makes the value available after flush' do
24
+ store = build_store
25
+ store.write('async_key', 'async_value')
26
+ store.flush
27
+ expect(store.read('async_key')).to eq('async_value')
28
+ store.shutdown
29
+ end
30
+
31
+ it 'does not recurse into the async wrapper (inner write called exactly once)' do
32
+ store = build_store
33
+ inner = store.instance_variable_get(:@cache_store)
34
+ call_count = 0
35
+ original_write_entry = inner.method(:write_entry)
36
+ allow(inner).to receive(:write_entry) do |*args, **kwargs|
37
+ call_count += 1
38
+ original_write_entry.call(*args, **kwargs)
39
+ end
40
+ store.write('recurse_key', 'value')
41
+ store.flush
42
+ expect(call_count).to eq(1)
43
+ store.shutdown
44
+ end
45
+
46
+ it 'logs an error on failure but does not raise' do
47
+ store = build_store
48
+ inner = store.instance_variable_get(:@cache_store)
49
+ inner.rails_fast_cache_logger = logger
50
+ allow(inner).to receive(:write_entry).and_raise('boom')
51
+ expect { store.write('fail_key', 'value') }.not_to raise_error
52
+ store.flush
53
+ expect(logger).to have_received(:error).with(a_string_including('boom'))
54
+ store.shutdown
55
+ end
56
+ end
57
+
58
+ describe '#write_multi' do
59
+ it 'returns the hash immediately without waiting for the write to complete' do
60
+ store = build_store
61
+ hash = { 'k1' => 'v1', 'k2' => 'v2' }
62
+ result = store.write_multi(hash)
63
+ expect(result).to be_truthy
64
+ store.shutdown
65
+ end
66
+
67
+ it 'makes all values available after flush' do
68
+ store = build_store
69
+ store.write_multi('mk1' => 'mv1', 'mk2' => 'mv2')
70
+ store.flush
71
+ expect(store.read('mk1')).to eq('mv1')
72
+ expect(store.read('mk2')).to eq('mv2')
73
+ store.shutdown
74
+ end
75
+ end
76
+ end
@@ -46,9 +46,106 @@ describe RailsFastCache::Store do
46
46
  test_write(cache_store)
47
47
  end
48
48
 
49
+ describe '#options' do
50
+ it 'reflects options passed to the underlying cache store' do
51
+ store = RailsFastCache::Store.new(:memory_store, expires_in: 42)
52
+
53
+ expect(store.options[:expires_in]).to eq(42)
54
+ end
55
+ end
56
+
57
+ describe '#silence?' do
58
+ it 'reflects the underlying cache store mute state' do
59
+ store = RailsFastCache::Store.new(:memory_store)
60
+
61
+ store.mute { expect(store.silence?).to be true }
62
+ expect(store.silence?).to be_falsy
63
+ end
64
+ end
65
+
66
+ describe '#namespace' do
67
+ it 'reflects the namespace passed to the underlying cache store' do
68
+ store = RailsFastCache::Store.new(:memory_store, namespace: 'test_ns')
69
+
70
+ expect(store.namespace).to eq('test_ns')
71
+ end
72
+ end
73
+
74
+ describe '#read_counter and #write_counter' do
75
+ it 'delegates counter operations to the underlying cache store' do
76
+ store = RailsFastCache::Store.new(:memory_store)
77
+ store.write_counter('store_spec_counter', 1)
78
+ store.flush
79
+
80
+ expect(store.read_counter('store_spec_counter')).to eq(1)
81
+ end
82
+ end
83
+
49
84
  def initialize_store(cache_store)
50
85
  store = RailsFastCache::Store.new(*cache_store)
51
86
  store.delete_matched('store_spec_*')
52
87
  store
53
88
  end
54
89
  end
90
+
91
+ describe '#flush' do
92
+ it 'drains pending async writes without shutting down the pool' do
93
+ store = RailsFastCache::Store.new(:memory_store)
94
+ store.write('flush_key', 'flush_value')
95
+ store.flush
96
+ expect(store.read('flush_key')).to eq('flush_value')
97
+ # pool still alive: a subsequent write works
98
+ store.write('flush_key2', 'v2')
99
+ store.flush
100
+ expect(store.read('flush_key2')).to eq('v2')
101
+ store.shutdown
102
+ end
103
+ end
104
+
105
+ describe '#fetch' do
106
+ it 'returns the block value synchronously even though the cache write is deferred' do
107
+ store = RailsFastCache::Store.new(:memory_store)
108
+ result = store.fetch('fetch_key') { 'computed' }
109
+ expect(result).to eq('computed')
110
+ store.shutdown
111
+ end
112
+
113
+ it 'populates the cache asynchronously on a miss (value present after flush)' do
114
+ store = RailsFastCache::Store.new(:memory_store)
115
+ store.fetch('async_fetch_key') { 'fetched_value' }
116
+ expect(store.read('async_fetch_key')).to be_nil.or eq('fetched_value')
117
+ store.flush
118
+ expect(store.read('async_fetch_key')).to eq('fetched_value')
119
+ store.shutdown
120
+ end
121
+
122
+ it 'does not re-execute the block on a cache hit' do
123
+ store = RailsFastCache::Store.new(:memory_store)
124
+ store.fetch('hit_key') { 'original' }
125
+ store.flush
126
+ calls = 0
127
+ store.fetch('hit_key') { calls += 1; 'new' }
128
+ expect(calls).to eq(0)
129
+ store.shutdown
130
+ end
131
+ end
132
+
133
+ describe '#supports_cache_versioning?' do
134
+ it 'returns true' do
135
+ expect(RailsFastCache::Store.supports_cache_versioning?).to be(true)
136
+ end
137
+ end
138
+
139
+ describe 'per-instance isolation' do
140
+ it 'each Store owns an independent scheduler (shutdown of one does not affect the other)' do
141
+ store_a = RailsFastCache::Store.new(:memory_store)
142
+ store_b = RailsFastCache::Store.new(:memory_store)
143
+ store_a.write('iso_key', 'iso_value')
144
+ store_a.shutdown
145
+ # store_b's pool is unaffected
146
+ store_b.write('iso_key2', 'iso_value2')
147
+ store_b.shutdown
148
+ expect(store_b.read('iso_key2')).to eq('iso_value2')
149
+ end
150
+ end
151
+
metadata CHANGED
@@ -1,29 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-fast-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Filippo Liverani
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-09-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: activejob
13
+ name: concurrent-ruby
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '7.1'
18
+ version: '1.1'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '7.1'
25
+ version: '1.1'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: activesupport
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -118,13 +117,12 @@ files:
118
117
  - LICENSE.txt
119
118
  - README.md
120
119
  - lib/rails-fast-cache.rb
120
+ - lib/rails-fast-cache/async_writes.rb
121
121
  - lib/rails-fast-cache/brotli_compressor.rb
122
- - lib/rails-fast-cache/non_serializing_job.rb
123
122
  - lib/rails-fast-cache/scheduler.rb
124
123
  - lib/rails-fast-cache/store.rb
125
124
  - lib/rails-fast-cache/version.rb
126
- - lib/rails-fast-cache/write_job.rb
127
- - lib/rails-fast-cache/write_multi_job.rb
125
+ - spec/rails-fast-cache/async_writes_spec.rb
128
126
  - spec/rails-fast-cache/brotli_compressor_spec.rb
129
127
  - spec/rails-fast-cache/store_spec.rb
130
128
  - spec/spec_helper.rb
@@ -132,7 +130,6 @@ homepage: https://github.com/filippoliverani/rails-fast-cache
132
130
  licenses:
133
131
  - MIT
134
132
  metadata: {}
135
- post_install_message:
136
133
  rdoc_options: []
137
134
  require_paths:
138
135
  - lib
@@ -147,12 +144,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
144
  - !ruby/object:Gem::Version
148
145
  version: '0'
149
146
  requirements: []
150
- rubygems_version: 3.5.18
151
- signing_key:
147
+ rubygems_version: 4.0.15
152
148
  specification_version: 4
153
149
  summary: Drop-in improvement for Rails cache, providing enhanced performance with
154
150
  asynchronous processing and better default serialization and compression
155
151
  test_files:
152
+ - spec/rails-fast-cache/async_writes_spec.rb
156
153
  - spec/rails-fast-cache/brotli_compressor_spec.rb
157
154
  - spec/rails-fast-cache/store_spec.rb
158
155
  - spec/spec_helper.rb
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_job'
4
- require_relative 'scheduler'
5
-
6
- module RailsFastCache
7
- class NonSerializingJob < ::ActiveJob::Base
8
- self.logger = nil
9
- self.log_arguments = false
10
- self.queue_adapter = RailsFastCache::Scheduler.queue_adapter
11
-
12
- def deserialize_arguments(serialized_arguments)
13
- serialized_arguments
14
- end
15
-
16
- def serialize_arguments(arguments)
17
- arguments
18
- end
19
- end
20
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'non_serializing_job'
4
-
5
- module RailsFastCache
6
- class WriteJob < NonSerializingJob
7
- def perform(cache_store, name, value, options)
8
- cache_store.write(name, value, options)
9
- end
10
- end
11
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'non_serializing_job'
4
-
5
- module RailsFastCache
6
- class WriteMultiJob < NonSerializingJob
7
- def perform(cache_store, hash, options)
8
- cache_store.write_multi(hash, options)
9
- end
10
- end
11
- end