mudis 0.8.0 → 0.9.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 +4 -4
- data/README.md +958 -932
- data/lib/mudis/expiry.rb +53 -0
- data/lib/mudis/lru.rb +66 -0
- data/lib/mudis/metrics.rb +42 -0
- data/lib/mudis/namespace.rb +47 -0
- data/lib/mudis/persistence.rb +113 -0
- data/lib/mudis/version.rb +3 -3
- data/lib/mudis.rb +370 -631
- data/lib/mudis_client.rb +107 -65
- data/lib/mudis_config.rb +35 -35
- data/lib/mudis_ipc_config.rb +13 -0
- data/lib/mudis_proxy.rb +39 -37
- data/lib/mudis_server.rb +100 -81
- data/sig/mudis.rbs +62 -56
- data/sig/mudis_client.rbs +24 -22
- data/sig/mudis_config.rbs +10 -10
- data/sig/mudis_expiry.rbs +13 -0
- data/sig/mudis_ipc_config.rbs +10 -0
- data/sig/mudis_lru.rbs +21 -0
- data/sig/mudis_metrics.rbs +11 -0
- data/sig/mudis_namespace.rbs +13 -0
- data/sig/mudis_persistence.rbs +19 -0
- data/sig/mudis_server.rbs +16 -6
- data/spec/api_compatibility_spec.rb +155 -0
- data/spec/eviction_spec.rb +29 -29
- data/spec/guardrails_spec.rb +138 -138
- data/spec/memory_guard_spec.rb +33 -33
- data/spec/metrics_spec.rb +34 -34
- data/spec/modules/expiry_spec.rb +170 -0
- data/spec/modules/lru_spec.rb +149 -0
- data/spec/modules/metrics_spec.rb +105 -0
- data/spec/modules/namespace_spec.rb +157 -0
- data/spec/modules/persistence_spec.rb +125 -0
- data/spec/mudis_client_spec.rb +147 -137
- data/spec/mudis_server_spec.rb +100 -90
- data/spec/mudis_spec.rb +183 -183
- data/spec/namespace_spec.rb +69 -69
- data/spec/persistence_spec.rb +38 -37
- data/spec/reset_spec.rb +31 -31
- metadata +51 -3
data/spec/guardrails_spec.rb
CHANGED
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
require "climate_control"
|
|
5
|
-
|
|
6
|
-
RSpec.describe "Mudis TTL Guardrail" do # rubocop:disable Metrics/BlockLength
|
|
7
|
-
before do
|
|
8
|
-
Mudis.reset!
|
|
9
|
-
Mudis.configure do |c|
|
|
10
|
-
c.max_ttl = 60 # 60 seconds max
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
describe "default_ttl configuration" do # rubocop:disable Metrics/BlockLength
|
|
15
|
-
before do
|
|
16
|
-
Mudis.reset!
|
|
17
|
-
Mudis.configure do |c|
|
|
18
|
-
c.default_ttl = 60
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
it "applies default_ttl when expires_in is nil" do
|
|
23
|
-
Mudis.write("foo", "bar") # no explicit expires_in
|
|
24
|
-
meta = Mudis.inspect("foo")
|
|
25
|
-
expect(meta[:expires_at]).not_to be_nil
|
|
26
|
-
expect(meta[:expires_at]).to be_within(5).of(Time.now + 60)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
it "respects expires_in if explicitly given" do
|
|
30
|
-
Mudis.write("short_lived", "bar", expires_in: 10)
|
|
31
|
-
meta = Mudis.inspect("short_lived")
|
|
32
|
-
expect(meta[:expires_at]).not_to be_nil
|
|
33
|
-
expect(meta[:expires_at]).to be_within(5).of(Time.now + 10)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
it "applies max_ttl over default_ttl if both are set" do
|
|
37
|
-
Mudis.configure do |c|
|
|
38
|
-
c.default_ttl = 120
|
|
39
|
-
c.max_ttl = 30
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
Mudis.write("capped", "baz") # no explicit expires_in
|
|
43
|
-
meta = Mudis.inspect("capped")
|
|
44
|
-
expect(meta[:expires_at]).not_to be_nil
|
|
45
|
-
expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "stores forever if default_ttl and expires_in are nil" do
|
|
49
|
-
Mudis.configure do |c|
|
|
50
|
-
c.default_ttl = nil
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
Mudis.write("forever", "ever")
|
|
54
|
-
meta = Mudis.inspect("forever")
|
|
55
|
-
expect(meta[:expires_at]).to be_nil
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
it "clamps expires_in to max_ttl if it exceeds max_ttl" do
|
|
60
|
-
Mudis.write("foo", "bar", expires_in: 300) # user requests 5 minutes
|
|
61
|
-
|
|
62
|
-
metadata = Mudis.inspect("foo")
|
|
63
|
-
ttl = metadata[:expires_at] - metadata[:created_at]
|
|
64
|
-
|
|
65
|
-
expect(ttl).to be <= 60
|
|
66
|
-
expect(ttl).to be > 0
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
it "respects expires_in if below max_ttl" do
|
|
70
|
-
Mudis.write("bar", "baz", expires_in: 30) # under the max_ttl
|
|
71
|
-
|
|
72
|
-
metadata = Mudis.inspect("bar")
|
|
73
|
-
ttl = metadata[:expires_at] - metadata[:created_at]
|
|
74
|
-
|
|
75
|
-
expect(ttl).to be_within(1).of(30)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
it "allows nil expires_in (no expiry) if not required" do
|
|
79
|
-
Mudis.write("baz", "no expiry")
|
|
80
|
-
|
|
81
|
-
metadata = Mudis.inspect("baz")
|
|
82
|
-
expect(metadata[:expires_at]).to be_nil
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
|
|
87
|
-
after { Mudis.reset! }
|
|
88
|
-
|
|
89
|
-
describe "bucket configuration" do
|
|
90
|
-
it "defaults to 32 buckets if ENV is nil" do
|
|
91
|
-
Mudis.instance_variable_set(:@buckets, nil) # force recomputation
|
|
92
|
-
ClimateControl.modify(MUDIS_BUCKETS: nil) do
|
|
93
|
-
expect(Mudis.send(:buckets)).to eq(32)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "raises if MUDIS_BUCKETS is 0 or less" do
|
|
98
|
-
expect do
|
|
99
|
-
Mudis.instance_variable_set(:@buckets, nil) # force recomputation
|
|
100
|
-
ClimateControl.modify(MUDIS_BUCKETS: "0") { Mudis.send(:buckets) }
|
|
101
|
-
end.to raise_error(ArgumentError, /bucket count must be > 0/)
|
|
102
|
-
|
|
103
|
-
expect do
|
|
104
|
-
Mudis.instance_variable_set(:@buckets, nil) # force recomputation
|
|
105
|
-
ClimateControl.modify(MUDIS_BUCKETS: "-5") { Mudis.send(:buckets) }
|
|
106
|
-
end.to raise_error(ArgumentError, /bucket count must be > 0/)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
describe "memory configuration" do
|
|
111
|
-
it "raises if max_bytes is set to 0 or less" do
|
|
112
|
-
expect do
|
|
113
|
-
Mudis.max_bytes = 0
|
|
114
|
-
end.to raise_error(ArgumentError, /max_bytes must be > 0/)
|
|
115
|
-
|
|
116
|
-
expect do
|
|
117
|
-
Mudis.max_bytes = -1
|
|
118
|
-
end.to raise_error(ArgumentError, /max_bytes must be > 0/)
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
it "raises if max_value_bytes is 0 or less via config" do
|
|
122
|
-
expect do
|
|
123
|
-
Mudis.configure do |c|
|
|
124
|
-
c.max_value_bytes = 0
|
|
125
|
-
end
|
|
126
|
-
end.to raise_error(ArgumentError, /max_value_bytes must be > 0/)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
it "raises if max_value_bytes exceeds max_bytes" do
|
|
130
|
-
expect do
|
|
131
|
-
Mudis.configure do |c|
|
|
132
|
-
c.max_bytes = 1_000_000
|
|
133
|
-
c.max_value_bytes = 2_000_000
|
|
134
|
-
end
|
|
135
|
-
end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "climate_control"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "Mudis TTL Guardrail" do # rubocop:disable Metrics/BlockLength
|
|
7
|
+
before do
|
|
8
|
+
Mudis.reset!
|
|
9
|
+
Mudis.configure do |c|
|
|
10
|
+
c.max_ttl = 60 # 60 seconds max
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "default_ttl configuration" do # rubocop:disable Metrics/BlockLength
|
|
15
|
+
before do
|
|
16
|
+
Mudis.reset!
|
|
17
|
+
Mudis.configure do |c|
|
|
18
|
+
c.default_ttl = 60
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "applies default_ttl when expires_in is nil" do
|
|
23
|
+
Mudis.write("foo", "bar") # no explicit expires_in
|
|
24
|
+
meta = Mudis.inspect("foo")
|
|
25
|
+
expect(meta[:expires_at]).not_to be_nil
|
|
26
|
+
expect(meta[:expires_at]).to be_within(5).of(Time.now + 60)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "respects expires_in if explicitly given" do
|
|
30
|
+
Mudis.write("short_lived", "bar", expires_in: 10)
|
|
31
|
+
meta = Mudis.inspect("short_lived")
|
|
32
|
+
expect(meta[:expires_at]).not_to be_nil
|
|
33
|
+
expect(meta[:expires_at]).to be_within(5).of(Time.now + 10)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "applies max_ttl over default_ttl if both are set" do
|
|
37
|
+
Mudis.configure do |c|
|
|
38
|
+
c.default_ttl = 120
|
|
39
|
+
c.max_ttl = 30
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Mudis.write("capped", "baz") # no explicit expires_in
|
|
43
|
+
meta = Mudis.inspect("capped")
|
|
44
|
+
expect(meta[:expires_at]).not_to be_nil
|
|
45
|
+
expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "stores forever if default_ttl and expires_in are nil" do
|
|
49
|
+
Mudis.configure do |c|
|
|
50
|
+
c.default_ttl = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Mudis.write("forever", "ever")
|
|
54
|
+
meta = Mudis.inspect("forever")
|
|
55
|
+
expect(meta[:expires_at]).to be_nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "clamps expires_in to max_ttl if it exceeds max_ttl" do
|
|
60
|
+
Mudis.write("foo", "bar", expires_in: 300) # user requests 5 minutes
|
|
61
|
+
|
|
62
|
+
metadata = Mudis.inspect("foo")
|
|
63
|
+
ttl = metadata[:expires_at] - metadata[:created_at]
|
|
64
|
+
|
|
65
|
+
expect(ttl).to be <= 60
|
|
66
|
+
expect(ttl).to be > 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "respects expires_in if below max_ttl" do
|
|
70
|
+
Mudis.write("bar", "baz", expires_in: 30) # under the max_ttl
|
|
71
|
+
|
|
72
|
+
metadata = Mudis.inspect("bar")
|
|
73
|
+
ttl = metadata[:expires_at] - metadata[:created_at]
|
|
74
|
+
|
|
75
|
+
expect(ttl).to be_within(1).of(30)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "allows nil expires_in (no expiry) if not required" do
|
|
79
|
+
Mudis.write("baz", "no expiry")
|
|
80
|
+
|
|
81
|
+
metadata = Mudis.inspect("baz")
|
|
82
|
+
expect(metadata[:expires_at]).to be_nil
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
|
|
87
|
+
after { Mudis.reset! }
|
|
88
|
+
|
|
89
|
+
describe "bucket configuration" do
|
|
90
|
+
it "defaults to 32 buckets if ENV is nil" do
|
|
91
|
+
Mudis.instance_variable_set(:@buckets, nil) # force recomputation
|
|
92
|
+
ClimateControl.modify(MUDIS_BUCKETS: nil) do
|
|
93
|
+
expect(Mudis.send(:buckets)).to eq(32)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "raises if MUDIS_BUCKETS is 0 or less" do
|
|
98
|
+
expect do
|
|
99
|
+
Mudis.instance_variable_set(:@buckets, nil) # force recomputation
|
|
100
|
+
ClimateControl.modify(MUDIS_BUCKETS: "0") { Mudis.send(:buckets) }
|
|
101
|
+
end.to raise_error(ArgumentError, /bucket count must be > 0/)
|
|
102
|
+
|
|
103
|
+
expect do
|
|
104
|
+
Mudis.instance_variable_set(:@buckets, nil) # force recomputation
|
|
105
|
+
ClimateControl.modify(MUDIS_BUCKETS: "-5") { Mudis.send(:buckets) }
|
|
106
|
+
end.to raise_error(ArgumentError, /bucket count must be > 0/)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "memory configuration" do
|
|
111
|
+
it "raises if max_bytes is set to 0 or less" do
|
|
112
|
+
expect do
|
|
113
|
+
Mudis.max_bytes = 0
|
|
114
|
+
end.to raise_error(ArgumentError, /max_bytes must be > 0/)
|
|
115
|
+
|
|
116
|
+
expect do
|
|
117
|
+
Mudis.max_bytes = -1
|
|
118
|
+
end.to raise_error(ArgumentError, /max_bytes must be > 0/)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "raises if max_value_bytes is 0 or less via config" do
|
|
122
|
+
expect do
|
|
123
|
+
Mudis.configure do |c|
|
|
124
|
+
c.max_value_bytes = 0
|
|
125
|
+
end
|
|
126
|
+
end.to raise_error(ArgumentError, /max_value_bytes must be > 0/)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "raises if max_value_bytes exceeds max_bytes" do
|
|
130
|
+
expect do
|
|
131
|
+
Mudis.configure do |c|
|
|
132
|
+
c.max_bytes = 1_000_000
|
|
133
|
+
c.max_value_bytes = 2_000_000
|
|
134
|
+
end
|
|
135
|
+
end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/spec/memory_guard_spec.rb
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "spec_helper"
|
|
4
|
-
|
|
5
|
-
RSpec.describe "Mudis Memory Guardrails" do
|
|
6
|
-
before do
|
|
7
|
-
Mudis.reset!
|
|
8
|
-
Mudis.stop_expiry_thread
|
|
9
|
-
Mudis.instance_variable_set(:@buckets, 1)
|
|
10
|
-
Mudis.instance_variable_set(:@stores, [{}])
|
|
11
|
-
Mudis.instance_variable_set(:@mutexes, [Mutex.new])
|
|
12
|
-
Mudis.instance_variable_set(:@lru_heads, [nil])
|
|
13
|
-
Mudis.instance_variable_set(:@lru_tails, [nil])
|
|
14
|
-
Mudis.instance_variable_set(:@lru_nodes, [{}])
|
|
15
|
-
Mudis.instance_variable_set(:@current_bytes, [0])
|
|
16
|
-
|
|
17
|
-
Mudis.max_value_bytes = nil
|
|
18
|
-
Mudis.instance_variable_set(:@threshold_bytes, 1_000_000)
|
|
19
|
-
Mudis.hard_memory_limit = true
|
|
20
|
-
Mudis.instance_variable_set(:@max_bytes, 100)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
it "rejects writes that exceed max memory" do
|
|
24
|
-
big_value = "a" * 90
|
|
25
|
-
Mudis.write("a", big_value)
|
|
26
|
-
expect(Mudis.read("a")).to eq(big_value)
|
|
27
|
-
|
|
28
|
-
big_value2 = "b" * 90
|
|
29
|
-
Mudis.write("b", big_value2)
|
|
30
|
-
expect(Mudis.read("b")).to be_nil
|
|
31
|
-
expect(Mudis.metrics[:rejected]).to be > 0
|
|
32
|
-
end
|
|
33
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Mudis Memory Guardrails" do
|
|
6
|
+
before do
|
|
7
|
+
Mudis.reset!
|
|
8
|
+
Mudis.stop_expiry_thread
|
|
9
|
+
Mudis.instance_variable_set(:@buckets, 1)
|
|
10
|
+
Mudis.instance_variable_set(:@stores, [{}])
|
|
11
|
+
Mudis.instance_variable_set(:@mutexes, [Mutex.new])
|
|
12
|
+
Mudis.instance_variable_set(:@lru_heads, [nil])
|
|
13
|
+
Mudis.instance_variable_set(:@lru_tails, [nil])
|
|
14
|
+
Mudis.instance_variable_set(:@lru_nodes, [{}])
|
|
15
|
+
Mudis.instance_variable_set(:@current_bytes, [0])
|
|
16
|
+
|
|
17
|
+
Mudis.max_value_bytes = nil
|
|
18
|
+
Mudis.instance_variable_set(:@threshold_bytes, 1_000_000)
|
|
19
|
+
Mudis.hard_memory_limit = true
|
|
20
|
+
Mudis.instance_variable_set(:@max_bytes, 100)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "rejects writes that exceed max memory" do
|
|
24
|
+
big_value = "a" * 90
|
|
25
|
+
Mudis.write("a", big_value)
|
|
26
|
+
expect(Mudis.read("a")).to eq(big_value)
|
|
27
|
+
|
|
28
|
+
big_value2 = "b" * 90
|
|
29
|
+
Mudis.write("b", big_value2)
|
|
30
|
+
expect(Mudis.read("b")).to be_nil
|
|
31
|
+
expect(Mudis.metrics[:rejected]).to be > 0
|
|
32
|
+
end
|
|
33
|
+
end
|
data/spec/metrics_spec.rb
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "spec_helper"
|
|
4
|
-
|
|
5
|
-
RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
|
|
6
|
-
it "tracks hits and misses" do
|
|
7
|
-
Mudis.write("hit_me", "value")
|
|
8
|
-
Mudis.read("hit_me")
|
|
9
|
-
Mudis.read("miss_me")
|
|
10
|
-
metrics = Mudis.metrics
|
|
11
|
-
expect(metrics[:hits]).to eq(1)
|
|
12
|
-
expect(metrics[:misses]).to eq(1)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
it "includes per-bucket stats" do
|
|
16
|
-
Mudis.write("a", "x" * 50)
|
|
17
|
-
metrics = Mudis.metrics
|
|
18
|
-
expect(metrics).to include(:buckets)
|
|
19
|
-
expect(metrics[:buckets]).to be_an(Array)
|
|
20
|
-
expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
it "resets only the metrics without clearing cache" do
|
|
24
|
-
Mudis.write("metrics_key", "value")
|
|
25
|
-
Mudis.read("metrics_key")
|
|
26
|
-
Mudis.read("missing_key")
|
|
27
|
-
expect(Mudis.metrics[:hits]).to eq(1)
|
|
28
|
-
expect(Mudis.metrics[:misses]).to eq(1)
|
|
29
|
-
Mudis.reset_metrics!
|
|
30
|
-
expect(Mudis.metrics[:hits]).to eq(0)
|
|
31
|
-
expect(Mudis.metrics[:misses]).to eq(0)
|
|
32
|
-
expect(Mudis.read("metrics_key")).to eq("value")
|
|
33
|
-
end
|
|
34
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
|
|
6
|
+
it "tracks hits and misses" do
|
|
7
|
+
Mudis.write("hit_me", "value")
|
|
8
|
+
Mudis.read("hit_me")
|
|
9
|
+
Mudis.read("miss_me")
|
|
10
|
+
metrics = Mudis.metrics
|
|
11
|
+
expect(metrics[:hits]).to eq(1)
|
|
12
|
+
expect(metrics[:misses]).to eq(1)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "includes per-bucket stats" do
|
|
16
|
+
Mudis.write("a", "x" * 50)
|
|
17
|
+
metrics = Mudis.metrics
|
|
18
|
+
expect(metrics).to include(:buckets)
|
|
19
|
+
expect(metrics[:buckets]).to be_an(Array)
|
|
20
|
+
expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "resets only the metrics without clearing cache" do
|
|
24
|
+
Mudis.write("metrics_key", "value")
|
|
25
|
+
Mudis.read("metrics_key")
|
|
26
|
+
Mudis.read("missing_key")
|
|
27
|
+
expect(Mudis.metrics[:hits]).to eq(1)
|
|
28
|
+
expect(Mudis.metrics[:misses]).to eq(1)
|
|
29
|
+
Mudis.reset_metrics!
|
|
30
|
+
expect(Mudis.metrics[:hits]).to eq(0)
|
|
31
|
+
expect(Mudis.metrics[:misses]).to eq(0)
|
|
32
|
+
expect(Mudis.read("metrics_key")).to eq("value")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Mudis::Expiry do
|
|
6
|
+
let(:test_class) do
|
|
7
|
+
Class.new do
|
|
8
|
+
extend Mudis::Expiry
|
|
9
|
+
|
|
10
|
+
@expiry_thread = nil
|
|
11
|
+
@stop_expiry = false
|
|
12
|
+
@buckets = 2
|
|
13
|
+
@mutexes = Array.new(2) { Mutex.new }
|
|
14
|
+
@stores = Array.new(2) { {} }
|
|
15
|
+
@max_ttl = nil
|
|
16
|
+
@default_ttl = nil
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
attr_accessor :expiry_thread, :stop_expiry, :buckets, :mutexes, :stores, :max_ttl, :default_ttl
|
|
20
|
+
|
|
21
|
+
def evict_key(idx, key)
|
|
22
|
+
@stores[idx].delete(key)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
after do
|
|
29
|
+
test_class.stop_expiry_thread if test_class.expiry_thread&.alive?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "#start_expiry_thread" do
|
|
33
|
+
it "starts a background cleanup thread" do
|
|
34
|
+
test_class.start_expiry_thread(interval: 0.1)
|
|
35
|
+
|
|
36
|
+
expect(test_class.expiry_thread).to be_alive
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "does not start duplicate thread if already running" do
|
|
40
|
+
test_class.start_expiry_thread(interval: 0.1)
|
|
41
|
+
first_thread = test_class.expiry_thread
|
|
42
|
+
|
|
43
|
+
test_class.start_expiry_thread(interval: 0.1)
|
|
44
|
+
|
|
45
|
+
expect(test_class.expiry_thread).to eq(first_thread)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "periodically calls cleanup_expired!" do
|
|
49
|
+
allow(test_class).to receive(:cleanup_expired!)
|
|
50
|
+
|
|
51
|
+
test_class.start_expiry_thread(interval: 0.05)
|
|
52
|
+
sleep 0.15
|
|
53
|
+
|
|
54
|
+
expect(test_class).to have_received(:cleanup_expired!).at_least(:once)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "#stop_expiry_thread" do
|
|
59
|
+
it "stops the background thread" do
|
|
60
|
+
test_class.start_expiry_thread(interval: 0.1)
|
|
61
|
+
|
|
62
|
+
test_class.stop_expiry_thread
|
|
63
|
+
|
|
64
|
+
expect(test_class.expiry_thread).to be_nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "sets stop signal" do
|
|
68
|
+
test_class.start_expiry_thread(interval: 0.1)
|
|
69
|
+
|
|
70
|
+
test_class.stop_expiry_thread
|
|
71
|
+
|
|
72
|
+
expect(test_class.stop_expiry).to be true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "does nothing if thread is not running" do
|
|
76
|
+
expect { test_class.stop_expiry_thread }.not_to raise_error
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "#cleanup_expired!" do
|
|
81
|
+
it "removes expired keys from all buckets" do
|
|
82
|
+
now = Time.now
|
|
83
|
+
test_class.stores[0]["expired"] = { expires_at: now - 10 }
|
|
84
|
+
test_class.stores[0]["valid"] = { expires_at: now + 10 }
|
|
85
|
+
test_class.stores[1]["also_expired"] = { expires_at: now - 5 }
|
|
86
|
+
|
|
87
|
+
test_class.cleanup_expired!
|
|
88
|
+
|
|
89
|
+
expect(test_class.stores[0]).not_to have_key("expired")
|
|
90
|
+
expect(test_class.stores[0]).to have_key("valid")
|
|
91
|
+
expect(test_class.stores[1]).not_to have_key("also_expired")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "keeps keys without expiration" do
|
|
95
|
+
test_class.stores[0]["no_expiry"] = { expires_at: nil }
|
|
96
|
+
|
|
97
|
+
test_class.cleanup_expired!
|
|
98
|
+
|
|
99
|
+
expect(test_class.stores[0]).to have_key("no_expiry")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "is thread-safe" do
|
|
103
|
+
10.times do |i|
|
|
104
|
+
test_class.stores[0]["key#{i}"] = { expires_at: Time.now - 1 }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
threads = 3.times.map do
|
|
108
|
+
Thread.new { test_class.cleanup_expired! }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
expect { threads.each(&:join) }.not_to raise_error
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe "#effective_ttl (private)" do
|
|
116
|
+
it "returns provided expires_in when no constraints" do
|
|
117
|
+
result = test_class.send(:effective_ttl, 300)
|
|
118
|
+
|
|
119
|
+
expect(result).to eq(300)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "returns nil when expires_in is nil and no default" do
|
|
123
|
+
result = test_class.send(:effective_ttl, nil)
|
|
124
|
+
|
|
125
|
+
expect(result).to be_nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "uses default_ttl when expires_in is nil" do
|
|
129
|
+
test_class.default_ttl = 600
|
|
130
|
+
|
|
131
|
+
result = test_class.send(:effective_ttl, nil)
|
|
132
|
+
|
|
133
|
+
expect(result).to eq(600)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "clamps to max_ttl when expires_in exceeds it" do
|
|
137
|
+
test_class.max_ttl = 100
|
|
138
|
+
|
|
139
|
+
result = test_class.send(:effective_ttl, 500)
|
|
140
|
+
|
|
141
|
+
expect(result).to eq(100)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "allows expires_in below max_ttl" do
|
|
145
|
+
test_class.max_ttl = 1000
|
|
146
|
+
|
|
147
|
+
result = test_class.send(:effective_ttl, 500)
|
|
148
|
+
|
|
149
|
+
expect(result).to eq(500)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "applies max_ttl over default_ttl" do
|
|
153
|
+
test_class.max_ttl = 100
|
|
154
|
+
test_class.default_ttl = 500
|
|
155
|
+
|
|
156
|
+
result = test_class.send(:effective_ttl, nil)
|
|
157
|
+
|
|
158
|
+
expect(result).to eq(100)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "returns nil when both expires_in and default_ttl are nil" do
|
|
162
|
+
test_class.max_ttl = 1000
|
|
163
|
+
test_class.default_ttl = nil
|
|
164
|
+
|
|
165
|
+
result = test_class.send(:effective_ttl, nil)
|
|
166
|
+
|
|
167
|
+
expect(result).to be_nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|