rails-fast-cache 1.1.0 → 2.0.2
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/lib/rails-fast-cache/async_writes.rb +26 -0
- data/lib/rails-fast-cache/scheduler.rb +25 -5
- data/lib/rails-fast-cache/store.rb +27 -20
- data/lib/rails-fast-cache/version.rb +1 -1
- data/spec/rails-fast-cache/async_writes_spec.rb +76 -0
- data/spec/rails-fast-cache/store_spec.rb +97 -0
- metadata +9 -12
- data/lib/rails-fast-cache/non_serializing_job.rb +0 -20
- data/lib/rails-fast-cache/write_job.rb +0 -11
- data/lib/rails-fast-cache/write_multi_job.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b640d149e3e9b254fc1cf8566ab7b1885a6cd8fe3c1e878b73617326162f008
|
|
4
|
+
data.tar.gz: fa2fb11469f474771186a7bfba11786ff9df7417ddb5be3722ecb767e069d9e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08f9552be354ffb3791d626585c3b3d6dc9a6615a979019e692efc038c2cc70fccb9f5dd02e3bee7546078cbf3745a8d3ee727f6976bec84bd8089ada8ff80f4'
|
|
7
|
+
data.tar.gz: 38c368e09b77e4ed2e7493e4683a80709046be8f9f7fc9810db77bad428994d458132325c3be3a0ba709beae8e5e637f1cb6512cfb1793a9d9432af5d685d740
|
|
@@ -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
|
|
13
|
-
@
|
|
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
|
|
17
|
-
@
|
|
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
|
-
:
|
|
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
|
-
|
|
52
|
-
end
|
|
50
|
+
@scheduler = RailsFastCache::Scheduler.new
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
true
|
|
59
|
+
def flush(timeout = nil)
|
|
60
|
+
@scheduler.flush(timeout)
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
def shutdown
|
|
65
|
-
|
|
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
|
|
@@ -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:
|
|
4
|
+
version: 2.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Filippo Liverani
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: concurrent-ruby
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
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: '
|
|
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
|
-
-
|
|
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:
|
|
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
|