mudis 0.7.0 → 0.7.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/README.md +863 -695
- data/lib/mudis/version.rb +3 -3
- data/lib/mudis.rb +521 -521
- data/lib/mudis_client.rb +65 -68
- data/lib/mudis_config.rb +25 -25
- data/lib/mudis_proxy.rb +32 -0
- data/lib/mudis_server.rb +81 -79
- data/sig/mudis.rbs +56 -56
- data/sig/mudis_client.rbs +23 -0
- data/sig/mudis_config.rbs +10 -10
- data/sig/mudis_server.rbs +7 -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/mudis_client_spec.rb +137 -0
- data/spec/mudis_server_spec.rb +90 -0
- data/spec/mudis_spec.rb +183 -183
- data/spec/namespace_spec.rb +69 -69
- data/spec/reset_spec.rb +31 -31
- metadata +16 -4
- data/lib/example_mudis_server_config.md +0 -39
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,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe MudisClient do # rubocop:disable Metrics/BlockLength
|
|
6
|
+
let(:socket_path) { "/tmp/mudis.sock" }
|
|
7
|
+
let(:mock_socket) { instance_double(UNIXSocket) }
|
|
8
|
+
let(:client) { MudisClient.new }
|
|
9
|
+
|
|
10
|
+
around do |example|
|
|
11
|
+
ClimateControl.modify("SOCKET_PATH" => socket_path) do
|
|
12
|
+
example.run
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
allow(UNIXSocket).to receive(:open).and_yield(mock_socket)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "#read" do
|
|
21
|
+
it "sends a read command and returns the value" do
|
|
22
|
+
payload = { cmd: "read", key: "test_key", namespace: nil }
|
|
23
|
+
response = { ok: true, value: "test_value" }.to_json
|
|
24
|
+
|
|
25
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
26
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
27
|
+
|
|
28
|
+
expect(client.read("test_key")).to eq("test_value")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "#write" do
|
|
33
|
+
it "sends a write command and returns the value" do
|
|
34
|
+
payload = { cmd: "write", key: "test_key", value: "test_value", ttl: nil, namespace: nil }
|
|
35
|
+
response = { ok: true, value: nil }.to_json
|
|
36
|
+
|
|
37
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
38
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
39
|
+
|
|
40
|
+
expect(client.write("test_key", "test_value")).to be_nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "#delete" do
|
|
45
|
+
it "sends a delete command and returns the value" do
|
|
46
|
+
payload = { cmd: "delete", key: "test_key", namespace: nil }
|
|
47
|
+
response = { ok: true, value: nil }.to_json
|
|
48
|
+
|
|
49
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
50
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
51
|
+
|
|
52
|
+
expect(client.delete("test_key")).to be_nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "#exists?" do
|
|
57
|
+
it "sends an exists command and returns true" do
|
|
58
|
+
payload = { cmd: "exists", key: "test_key", namespace: nil }
|
|
59
|
+
response = { ok: true, value: true }.to_json
|
|
60
|
+
|
|
61
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
62
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
63
|
+
|
|
64
|
+
expect(client.exists?("test_key")).to eq(true)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "#fetch" do
|
|
69
|
+
it "fetches an existing value or writes a new one" do
|
|
70
|
+
read_response = { ok: true, value: nil }.to_json
|
|
71
|
+
write_payload = { cmd: "write", key: "test_key", value: "new_value", ttl: nil, namespace: nil }
|
|
72
|
+
write_response = { ok: true, value: nil }.to_json
|
|
73
|
+
|
|
74
|
+
expect(mock_socket).to receive(:puts).with({ cmd: "read", key: "test_key", namespace: nil }.to_json)
|
|
75
|
+
expect(mock_socket).to receive(:gets).and_return(read_response)
|
|
76
|
+
expect(mock_socket).to receive(:puts).with(write_payload.to_json)
|
|
77
|
+
expect(mock_socket).to receive(:gets).and_return(write_response)
|
|
78
|
+
|
|
79
|
+
result = client.fetch("test_key") { "new_value" } # rubocop:disable Style/RedundantFetchBlock
|
|
80
|
+
expect(result).to eq("new_value")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "#metrics" do
|
|
85
|
+
it "sends a metrics command and returns the metrics" do
|
|
86
|
+
payload = { cmd: "metrics" }
|
|
87
|
+
response = { ok: true, value: { reads: 10, writes: 5 } }.to_json
|
|
88
|
+
|
|
89
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
90
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
91
|
+
|
|
92
|
+
expect(client.metrics).to eq({ reads: 10, writes: 5 })
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "#reset_metrics!" do
|
|
97
|
+
it "sends a reset_metrics command" do
|
|
98
|
+
payload = { cmd: "reset_metrics" }
|
|
99
|
+
response = { ok: true, value: nil }.to_json
|
|
100
|
+
|
|
101
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
102
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
103
|
+
|
|
104
|
+
expect(client.reset_metrics!).to be_nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe "#reset!" do
|
|
109
|
+
it "sends a reset command" do
|
|
110
|
+
payload = { cmd: "reset" }
|
|
111
|
+
response = { ok: true, value: nil }.to_json
|
|
112
|
+
|
|
113
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
114
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
115
|
+
|
|
116
|
+
expect(client.reset!).to be_nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe "error handling" do
|
|
121
|
+
it "warns when the socket is missing" do
|
|
122
|
+
allow(UNIXSocket).to receive(:open).and_raise(Errno::ENOENT)
|
|
123
|
+
|
|
124
|
+
expect { client.read("test_key") }.to output(/Socket missing/).to_stderr
|
|
125
|
+
expect(client.read("test_key")).to be_nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "raises an error when the server returns an error" do
|
|
129
|
+
response = { ok: false, error: "Something went wrong" }.to_json
|
|
130
|
+
|
|
131
|
+
expect(mock_socket).to receive(:puts)
|
|
132
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
133
|
+
|
|
134
|
+
expect { client.read("test_key") }.to raise_error("Something went wrong")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "spec_helper"
|
|
7
|
+
|
|
8
|
+
RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
|
|
9
|
+
let(:socket_path) { MudisServer::SOCKET_PATH }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
allow(Mudis).to receive(:read).and_return("mock_value")
|
|
13
|
+
allow(Mudis).to receive(:write)
|
|
14
|
+
allow(Mudis).to receive(:delete)
|
|
15
|
+
allow(Mudis).to receive(:exists?).and_return(true)
|
|
16
|
+
allow(Mudis).to receive(:fetch).and_return("mock_fetched_value")
|
|
17
|
+
allow(Mudis).to receive(:metrics).and_return({ reads: 1, writes: 1 })
|
|
18
|
+
allow(Mudis).to receive(:reset_metrics!)
|
|
19
|
+
allow(Mudis).to receive(:reset!)
|
|
20
|
+
|
|
21
|
+
# Start the server in a separate thread
|
|
22
|
+
Thread.new { MudisServer.start! }
|
|
23
|
+
sleep 0.1 # Allow the server to start
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
after do
|
|
27
|
+
File.unlink(socket_path) if File.exist?(socket_path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def send_request(request)
|
|
31
|
+
UNIXSocket.open(socket_path) do |sock|
|
|
32
|
+
sock.puts(JSON.dump(request))
|
|
33
|
+
JSON.parse(sock.gets, symbolize_names: true)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "handles the 'read' command" do
|
|
38
|
+
response = send_request({ cmd: "read", key: "test_key", namespace: "test_ns" })
|
|
39
|
+
expect(response).to eq({ ok: true, value: "mock_value" })
|
|
40
|
+
expect(Mudis).to have_received(:read).with("test_key", namespace: "test_ns")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "handles the 'write' command" do
|
|
44
|
+
response = send_request({ cmd: "write", key: "test_key", value: "test_value", ttl: 60, namespace: "test_ns" })
|
|
45
|
+
expect(response).to eq({ ok: true })
|
|
46
|
+
expect(Mudis).to have_received(:write).with("test_key", "test_value", expires_in: 60, namespace: "test_ns")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "handles the 'delete' command" do
|
|
50
|
+
response = send_request({ cmd: "delete", key: "test_key", namespace: "test_ns" })
|
|
51
|
+
expect(response).to eq({ ok: true })
|
|
52
|
+
expect(Mudis).to have_received(:delete).with("test_key", namespace: "test_ns")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "handles the 'exists' command" do
|
|
56
|
+
response = send_request({ cmd: "exists", key: "test_key", namespace: "test_ns" })
|
|
57
|
+
expect(response).to eq({ ok: true, value: true })
|
|
58
|
+
expect(Mudis).to have_received(:exists?).with("test_key", namespace: "test_ns")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "handles the 'fetch' command" do
|
|
62
|
+
response = send_request({ cmd: "fetch", key: "test_key", ttl: 60, namespace: "test_ns",
|
|
63
|
+
fallback: "fallback_value" })
|
|
64
|
+
expect(response).to eq({ ok: true, value: "mock_fetched_value" })
|
|
65
|
+
expect(Mudis).to have_received(:fetch).with("test_key", expires_in: 60, namespace: "test_ns")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "handles the 'metrics' command" do
|
|
69
|
+
response = send_request({ cmd: "metrics" })
|
|
70
|
+
expect(response).to eq({ ok: true, value: { reads: 1, writes: 1 } })
|
|
71
|
+
expect(Mudis).to have_received(:metrics)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "handles the 'reset_metrics' command" do
|
|
75
|
+
response = send_request({ cmd: "reset_metrics" })
|
|
76
|
+
expect(response).to eq({ ok: true })
|
|
77
|
+
expect(Mudis).to have_received(:reset_metrics!)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "handles the 'reset' command" do
|
|
81
|
+
response = send_request({ cmd: "reset" })
|
|
82
|
+
expect(response).to eq({ ok: true })
|
|
83
|
+
expect(Mudis).to have_received(:reset!)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "handles unknown commands" do
|
|
87
|
+
response = send_request({ cmd: "unknown_command" })
|
|
88
|
+
expect(response).to eq({ ok: false, error: "unknown command: unknown_command" })
|
|
89
|
+
end
|
|
90
|
+
end
|