mudis 0.9.0 → 0.9.4
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 +114 -16
- data/lib/mudis/bound.rb +128 -0
- data/lib/mudis/metrics.rb +21 -2
- data/lib/mudis/persistence.rb +3 -1
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis.rb +119 -26
- data/lib/mudis_client.rb +69 -31
- data/lib/mudis_config.rb +2 -0
- data/lib/mudis_ipc_config.rb +10 -0
- data/lib/mudis_server.rb +14 -9
- data/sig/mudis.rbs +19 -4
- data/sig/mudis_bound.rbs +25 -0
- data/sig/mudis_client.rbs +15 -5
- data/sig/mudis_config.rbs +5 -0
- data/sig/mudis_expiry.rbs +1 -1
- data/sig/mudis_ipc_config.rbs +8 -0
- data/sig/mudis_lru.rbs +1 -1
- data/sig/mudis_metrics.rbs +1 -1
- data/sig/mudis_persistence.rbs +4 -4
- data/sig/mudis_server.rbs +3 -3
- data/spec/api_compatibility_spec.rb +3 -0
- data/spec/bound_spec.rb +89 -0
- data/spec/guardrails_spec.rb +14 -0
- data/spec/memory_guard_spec.rb +9 -0
- data/spec/metrics_spec.rb +21 -0
- data/spec/modules/metrics_spec.rb +4 -1
- data/spec/modules/persistence_spec.rb +24 -26
- data/spec/mudis_client_spec.rb +84 -10
- data/spec/mudis_server_spec.rb +57 -16
- data/spec/mudis_spec.rb +37 -0
- data/spec/namespace_spec.rb +23 -0
- metadata +8 -3
data/sig/mudis_server.rbs
CHANGED
|
@@ -7,11 +7,11 @@ class MudisServer
|
|
|
7
7
|
|
|
8
8
|
def self.start_unix_server!: () -> void
|
|
9
9
|
|
|
10
|
-
def self.accept_connections: (server: (TCPServer | UNIXServer)) ->
|
|
10
|
+
def self.accept_connections: (server: (TCPServer | UNIXServer)) -> Thread
|
|
11
11
|
|
|
12
12
|
def self.handle_client: (socket: (TCPSocket | UNIXSocket)) -> void
|
|
13
13
|
|
|
14
|
-
def self.process_request: (req: Hash[
|
|
14
|
+
def self.process_request: (req: Hash[Symbol, untyped]) -> untyped
|
|
15
15
|
|
|
16
16
|
def self.write_response: (socket: (TCPSocket | UNIXSocket), payload: Hash[Symbol, untyped]) -> void
|
|
17
|
-
end
|
|
17
|
+
end
|
|
@@ -29,6 +29,8 @@ RSpec.describe "Mudis Public API" do
|
|
|
29
29
|
expect(Mudis).to respond_to(:max_value_bytes=)
|
|
30
30
|
expect(Mudis).to respond_to(:hard_memory_limit)
|
|
31
31
|
expect(Mudis).to respond_to(:hard_memory_limit=)
|
|
32
|
+
expect(Mudis).to respond_to(:eviction_threshold)
|
|
33
|
+
expect(Mudis).to respond_to(:eviction_threshold=)
|
|
32
34
|
expect(Mudis).to respond_to(:max_ttl)
|
|
33
35
|
expect(Mudis).to respond_to(:max_ttl=)
|
|
34
36
|
expect(Mudis).to respond_to(:default_ttl)
|
|
@@ -77,6 +79,7 @@ RSpec.describe "Mudis Public API" do
|
|
|
77
79
|
expect(Mudis.method(:fetch).parameters).to include([:key, :expires_in])
|
|
78
80
|
expect(Mudis.method(:fetch).parameters).to include([:key, :force])
|
|
79
81
|
expect(Mudis.method(:fetch).parameters).to include([:key, :namespace])
|
|
82
|
+
expect(Mudis.method(:fetch).parameters).to include([:key, :singleflight])
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
it "verifies LRUNode class is still accessible" do
|
data/spec/bound_spec.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Mudis::Bound do
|
|
6
|
+
before do
|
|
7
|
+
Mudis.reset!
|
|
8
|
+
Mudis.serializer = JSON
|
|
9
|
+
Mudis.compress = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "scopes reads and writes to the bound namespace" do
|
|
13
|
+
bound = Mudis.bind(namespace: "caller")
|
|
14
|
+
bound.write("k", "v")
|
|
15
|
+
|
|
16
|
+
expect(Mudis.read("k")).to be_nil
|
|
17
|
+
expect(bound.read("k")).to eq("v")
|
|
18
|
+
expect(Mudis.read("k", namespace: "caller")).to eq("v")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "applies default_ttl and max_ttl within the scope" do
|
|
22
|
+
bound = Mudis.bind(namespace: "caller", default_ttl: 120, max_ttl: 30)
|
|
23
|
+
bound.write("k", "v")
|
|
24
|
+
|
|
25
|
+
meta = bound.inspect("k")
|
|
26
|
+
expect(meta[:expires_at]).not_to be_nil
|
|
27
|
+
expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "rejects values that exceed max_value_bytes" do
|
|
31
|
+
bound = Mudis.bind(namespace: "caller", max_value_bytes: 10)
|
|
32
|
+
bound.write("k", "a" * 20)
|
|
33
|
+
|
|
34
|
+
expect(bound.read("k")).to be_nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "rejects updates that exceed max_value_bytes" do
|
|
38
|
+
bound = Mudis.bind(namespace: "caller", max_value_bytes: 10)
|
|
39
|
+
bound.write("k", "ok")
|
|
40
|
+
|
|
41
|
+
bound.update("k") { "a" * 20 }
|
|
42
|
+
expect(bound.read("k")).to eq("ok")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "fetches within the bound namespace" do
|
|
46
|
+
bound = Mudis.bind(namespace: "caller")
|
|
47
|
+
value = bound.fetch("k") { "v" }
|
|
48
|
+
|
|
49
|
+
expect(value).to eq("v")
|
|
50
|
+
expect(Mudis.read("k")).to be_nil
|
|
51
|
+
expect(bound.read("k")).to eq("v")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "executes the block once with singleflight: true" do
|
|
55
|
+
bound = Mudis.bind(namespace: "caller")
|
|
56
|
+
count = 0
|
|
57
|
+
count_mutex = Mutex.new
|
|
58
|
+
results = []
|
|
59
|
+
results_mutex = Mutex.new
|
|
60
|
+
|
|
61
|
+
threads = 5.times.map do
|
|
62
|
+
Thread.new do
|
|
63
|
+
value = bound.fetch("sf", singleflight: true) do
|
|
64
|
+
count_mutex.synchronize { count += 1 }
|
|
65
|
+
sleep 0.05
|
|
66
|
+
"v"
|
|
67
|
+
end
|
|
68
|
+
results_mutex.synchronize { results << value }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
threads.each(&:join)
|
|
73
|
+
expect(count).to eq(1)
|
|
74
|
+
expect(results).to all(eq("v"))
|
|
75
|
+
expect(bound.read("sf")).to eq("v")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "exposes metrics scoped to the bound namespace" do
|
|
79
|
+
bound = Mudis.bind(namespace: "caller")
|
|
80
|
+
bound.write("k", "v")
|
|
81
|
+
bound.read("k")
|
|
82
|
+
bound.read("missing")
|
|
83
|
+
|
|
84
|
+
metrics = bound.metrics
|
|
85
|
+
expect(metrics[:namespace]).to eq("caller")
|
|
86
|
+
expect(metrics[:hits]).to eq(1)
|
|
87
|
+
expect(metrics[:misses]).to eq(1)
|
|
88
|
+
end
|
|
89
|
+
end
|
data/spec/guardrails_spec.rb
CHANGED
|
@@ -134,5 +134,19 @@ RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/Blo
|
|
|
134
134
|
end
|
|
135
135
|
end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
|
|
136
136
|
end
|
|
137
|
+
|
|
138
|
+
it "raises if eviction_threshold is <= 0 or > 1" do
|
|
139
|
+
expect do
|
|
140
|
+
Mudis.configure do |c|
|
|
141
|
+
c.eviction_threshold = 0
|
|
142
|
+
end
|
|
143
|
+
end.to raise_error(ArgumentError, /eviction_threshold must be > 0 and <= 1/)
|
|
144
|
+
|
|
145
|
+
expect do
|
|
146
|
+
Mudis.configure do |c|
|
|
147
|
+
c.eviction_threshold = 1.5
|
|
148
|
+
end
|
|
149
|
+
end.to raise_error(ArgumentError, /eviction_threshold must be > 0 and <= 1/)
|
|
150
|
+
end
|
|
137
151
|
end
|
|
138
152
|
end
|
data/spec/memory_guard_spec.rb
CHANGED
|
@@ -30,4 +30,13 @@ RSpec.describe "Mudis Memory Guardrails" do
|
|
|
30
30
|
expect(Mudis.read("b")).to be_nil
|
|
31
31
|
expect(Mudis.metrics[:rejected]).to be > 0
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
it "rejects updates that exceed max memory" do
|
|
35
|
+
Mudis.write("a", "a" * 10)
|
|
36
|
+
expect(Mudis.read("a")).to eq("a" * 10)
|
|
37
|
+
|
|
38
|
+
Mudis.update("a") { "b" * 200 }
|
|
39
|
+
expect(Mudis.read("a")).to eq("a" * 10)
|
|
40
|
+
expect(Mudis.metrics[:rejected]).to be > 0
|
|
41
|
+
end
|
|
33
42
|
end
|
data/spec/metrics_spec.rb
CHANGED
|
@@ -31,4 +31,25 @@ RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
|
|
|
31
31
|
expect(Mudis.metrics[:misses]).to eq(0)
|
|
32
32
|
expect(Mudis.read("metrics_key")).to eq("value")
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
it "tracks metrics per namespace" do
|
|
36
|
+
Mudis.write("k1", "v1", namespace: "ns1")
|
|
37
|
+
Mudis.write("k2", "v2", namespace: "ns2")
|
|
38
|
+
|
|
39
|
+
Mudis.read("k1", namespace: "ns1")
|
|
40
|
+
Mudis.read("k1", namespace: "ns1")
|
|
41
|
+
Mudis.read("missing", namespace: "ns1")
|
|
42
|
+
Mudis.read("k2", namespace: "ns2")
|
|
43
|
+
|
|
44
|
+
ns1 = Mudis.metrics(namespace: "ns1")
|
|
45
|
+
ns2 = Mudis.metrics(namespace: "ns2")
|
|
46
|
+
|
|
47
|
+
expect(ns1[:hits]).to eq(2)
|
|
48
|
+
expect(ns1[:misses]).to eq(1)
|
|
49
|
+
expect(ns1[:namespace]).to eq("ns1")
|
|
50
|
+
|
|
51
|
+
expect(ns2[:hits]).to eq(1)
|
|
52
|
+
expect(ns2[:misses]).to eq(0)
|
|
53
|
+
expect(ns2[:namespace]).to eq("ns2")
|
|
54
|
+
end
|
|
34
55
|
end
|
|
@@ -9,13 +9,16 @@ RSpec.describe Mudis::Metrics do
|
|
|
9
9
|
|
|
10
10
|
@metrics = { hits: 5, misses: 3, evictions: 2, rejected: 1 }
|
|
11
11
|
@metrics_mutex = Mutex.new
|
|
12
|
+
@metrics_by_namespace = {}
|
|
13
|
+
@metrics_by_namespace_mutex = Mutex.new
|
|
12
14
|
@buckets = 2
|
|
13
15
|
@stores = [{ "key1" => {} }, { "key2" => {} }]
|
|
14
16
|
@current_bytes = [100, 200]
|
|
15
17
|
@lru_nodes = [{ "key1" => nil }, { "key2" => nil }]
|
|
16
18
|
|
|
17
19
|
class << self
|
|
18
|
-
attr_accessor :metrics, :metrics_mutex, :
|
|
20
|
+
attr_accessor :metrics, :metrics_mutex, :metrics_by_namespace, :metrics_by_namespace_mutex,
|
|
21
|
+
:buckets, :stores, :current_bytes, :lru_nodes
|
|
19
22
|
|
|
20
23
|
def current_memory_bytes
|
|
21
24
|
@current_bytes.sum
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../spec_helper"
|
|
4
4
|
|
|
5
|
-
RSpec.describe Mudis::Persistence do
|
|
6
|
-
let(:test_class) do
|
|
5
|
+
RSpec.describe Mudis::Persistence do # rubocop:disable Metrics/BlockLength
|
|
6
|
+
let(:test_class) do # rubocop:disable Metrics/BlockLength
|
|
7
7
|
Class.new do
|
|
8
8
|
extend Mudis::Persistence
|
|
9
|
-
|
|
10
9
|
@persistence_enabled = true
|
|
11
10
|
@persistence_path = "tmp/test_persistence.json"
|
|
12
11
|
@persistence_format = :json
|
|
@@ -20,15 +19,14 @@ RSpec.describe Mudis::Persistence do
|
|
|
20
19
|
@current_bytes = Array.new(2, 0)
|
|
21
20
|
@compress = false
|
|
22
21
|
@serializer = JSON
|
|
23
|
-
|
|
24
22
|
class << self
|
|
25
|
-
attr_accessor :persistence_enabled, :persistence_path, :persistence_format,
|
|
23
|
+
attr_accessor :persistence_enabled, :persistence_path, :persistence_format,
|
|
26
24
|
:persistence_safe_write, :buckets, :mutexes, :stores, :compress, :serializer
|
|
27
|
-
|
|
25
|
+
|
|
28
26
|
def decompress_and_deserialize(raw)
|
|
29
27
|
JSON.load(raw)
|
|
30
28
|
end
|
|
31
|
-
|
|
29
|
+
|
|
32
30
|
def write(key, value, expires_in: nil)
|
|
33
31
|
# Stub write method
|
|
34
32
|
@stores[0][key] = { value: JSON.dump(value), expires_at: nil, created_at: Time.now }
|
|
@@ -43,28 +41,28 @@ RSpec.describe Mudis::Persistence do
|
|
|
43
41
|
|
|
44
42
|
describe "#save_snapshot!" do
|
|
45
43
|
it "saves cache data to disk" do
|
|
46
|
-
test_class.stores[0]["key1"] = {
|
|
47
|
-
value: JSON.dump("value1"),
|
|
48
|
-
expires_at: nil,
|
|
49
|
-
created_at: Time.now
|
|
44
|
+
test_class.stores[0]["key1"] = {
|
|
45
|
+
value: JSON.dump("value1"),
|
|
46
|
+
expires_at: nil,
|
|
47
|
+
created_at: Time.now
|
|
50
48
|
}
|
|
51
|
-
|
|
49
|
+
|
|
52
50
|
test_class.save_snapshot!
|
|
53
|
-
|
|
51
|
+
|
|
54
52
|
expect(File.exist?(test_class.persistence_path)).to be true
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
it "handles errors gracefully" do
|
|
58
56
|
allow(test_class).to receive(:snapshot_dump).and_raise("Test error")
|
|
59
|
-
|
|
57
|
+
|
|
60
58
|
expect { test_class.save_snapshot! }.to output(/Failed to save snapshot/).to_stderr
|
|
61
59
|
end
|
|
62
60
|
|
|
63
61
|
it "does nothing when persistence is disabled" do
|
|
64
62
|
test_class.persistence_enabled = false
|
|
65
|
-
|
|
63
|
+
|
|
66
64
|
test_class.save_snapshot!
|
|
67
|
-
|
|
65
|
+
|
|
68
66
|
expect(File.exist?(test_class.persistence_path)).to be false
|
|
69
67
|
end
|
|
70
68
|
end
|
|
@@ -73,9 +71,9 @@ RSpec.describe Mudis::Persistence do
|
|
|
73
71
|
it "loads cache data from disk" do
|
|
74
72
|
data = [{ key: "test_key", value: "test_value", expires_in: nil }]
|
|
75
73
|
File.write(test_class.persistence_path, JSON.dump(data))
|
|
76
|
-
|
|
74
|
+
|
|
77
75
|
expect(test_class).to receive(:write).with("test_key", "test_value", expires_in: nil)
|
|
78
|
-
|
|
76
|
+
|
|
79
77
|
test_class.load_snapshot!
|
|
80
78
|
end
|
|
81
79
|
|
|
@@ -85,16 +83,16 @@ RSpec.describe Mudis::Persistence do
|
|
|
85
83
|
|
|
86
84
|
it "handles errors gracefully" do
|
|
87
85
|
File.write(test_class.persistence_path, "invalid json")
|
|
88
|
-
|
|
86
|
+
|
|
89
87
|
expect { test_class.load_snapshot! }.to output(/Failed to load snapshot/).to_stderr
|
|
90
88
|
end
|
|
91
89
|
|
|
92
90
|
it "does nothing when persistence is disabled" do
|
|
93
91
|
test_class.persistence_enabled = false
|
|
94
92
|
File.write(test_class.persistence_path, JSON.dump([]))
|
|
95
|
-
|
|
93
|
+
|
|
96
94
|
expect(test_class).not_to receive(:write)
|
|
97
|
-
|
|
95
|
+
|
|
98
96
|
test_class.load_snapshot!
|
|
99
97
|
end
|
|
100
98
|
end
|
|
@@ -102,23 +100,23 @@ RSpec.describe Mudis::Persistence do
|
|
|
102
100
|
describe "#install_persistence_hook!" do
|
|
103
101
|
it "installs at_exit hook" do
|
|
104
102
|
expect(test_class).to receive(:at_exit)
|
|
105
|
-
|
|
103
|
+
|
|
106
104
|
test_class.install_persistence_hook!
|
|
107
105
|
end
|
|
108
106
|
|
|
109
107
|
it "only installs hook once" do
|
|
110
108
|
test_class.install_persistence_hook!
|
|
111
|
-
|
|
109
|
+
|
|
112
110
|
expect(test_class).not_to receive(:at_exit)
|
|
113
|
-
|
|
111
|
+
|
|
114
112
|
test_class.install_persistence_hook!
|
|
115
113
|
end
|
|
116
114
|
|
|
117
115
|
it "does nothing when persistence is disabled" do
|
|
118
116
|
test_class.persistence_enabled = false
|
|
119
|
-
|
|
117
|
+
|
|
120
118
|
expect(test_class).not_to receive(:at_exit)
|
|
121
|
-
|
|
119
|
+
|
|
122
120
|
test_class.install_persistence_hook!
|
|
123
121
|
end
|
|
124
122
|
end
|
data/spec/mudis_client_spec.rb
CHANGED
|
@@ -18,7 +18,7 @@ RSpec.describe MudisClient do # rubocop:disable Metrics/BlockLength
|
|
|
18
18
|
if MudisIPCConfig.use_tcp?
|
|
19
19
|
allow(TCPSocket).to receive(:new).and_return(mock_socket)
|
|
20
20
|
else
|
|
21
|
-
allow(UNIXSocket).to receive(:open).
|
|
21
|
+
allow(UNIXSocket).to receive(:open).and_return(mock_socket)
|
|
22
22
|
end
|
|
23
23
|
allow(mock_socket).to receive(:close)
|
|
24
24
|
end
|
|
@@ -99,32 +99,93 @@ RSpec.describe MudisClient do # rubocop:disable Metrics/BlockLength
|
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
describe "#
|
|
103
|
-
it "sends
|
|
104
|
-
payload = { cmd: "
|
|
105
|
-
response = { ok: true, value:
|
|
102
|
+
describe "#inspect" do
|
|
103
|
+
it "sends an inspect command and returns metadata" do
|
|
104
|
+
payload = { cmd: "inspect", key: "test_key", namespace: nil }
|
|
105
|
+
response = { ok: true, value: { key: "test_key", size_bytes: 10 } }.to_json
|
|
106
106
|
|
|
107
107
|
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
108
108
|
expect(mock_socket).to receive(:gets).and_return(response)
|
|
109
109
|
|
|
110
|
-
expect(client.
|
|
110
|
+
expect(client.inspect("test_key")).to eq({ key: "test_key", size_bytes: 10 })
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
describe "#
|
|
115
|
-
it "sends a
|
|
116
|
-
payload = { cmd: "
|
|
114
|
+
describe "#keys" do
|
|
115
|
+
it "sends a keys command and returns keys" do
|
|
116
|
+
payload = { cmd: "keys", namespace: "ns" }
|
|
117
|
+
response = { ok: true, value: ["a", "b"] }.to_json
|
|
118
|
+
|
|
119
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
120
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
121
|
+
|
|
122
|
+
expect(client.keys(namespace: "ns")).to eq(["a", "b"])
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe "#clear_namespace" do
|
|
127
|
+
it "sends a clear_namespace command" do
|
|
128
|
+
payload = { cmd: "clear_namespace", namespace: "ns" }
|
|
117
129
|
response = { ok: true, value: nil }.to_json
|
|
118
130
|
|
|
119
131
|
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
120
132
|
expect(mock_socket).to receive(:gets).and_return(response)
|
|
121
133
|
|
|
122
|
-
expect(client.
|
|
134
|
+
expect(client.clear_namespace(namespace: "ns")).to be_nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe "#least_touched" do
|
|
139
|
+
it "sends a least_touched command" do
|
|
140
|
+
payload = { cmd: "least_touched", limit: 5 }
|
|
141
|
+
response = { ok: true, value: [["a", 0], ["b", 1]] }.to_json
|
|
142
|
+
|
|
143
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
144
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
145
|
+
|
|
146
|
+
expect(client.least_touched(5)).to eq([["a", 0], ["b", 1]])
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe "#all_keys" do
|
|
151
|
+
it "sends an all_keys command" do
|
|
152
|
+
payload = { cmd: "all_keys" }
|
|
153
|
+
response = { ok: true, value: ["k1"] }.to_json
|
|
154
|
+
|
|
155
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
156
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
157
|
+
|
|
158
|
+
expect(client.all_keys).to eq(["k1"])
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe "#current_memory_bytes" do
|
|
163
|
+
it "sends a current_memory_bytes command" do
|
|
164
|
+
payload = { cmd: "current_memory_bytes" }
|
|
165
|
+
response = { ok: true, value: 123 }.to_json
|
|
166
|
+
|
|
167
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
168
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
169
|
+
|
|
170
|
+
expect(client.current_memory_bytes).to eq(123)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
describe "#max_memory_bytes" do
|
|
175
|
+
it "sends a max_memory_bytes command" do
|
|
176
|
+
payload = { cmd: "max_memory_bytes" }
|
|
177
|
+
response = { ok: true, value: 456 }.to_json
|
|
178
|
+
|
|
179
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
180
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
181
|
+
|
|
182
|
+
expect(client.max_memory_bytes).to eq(456)
|
|
123
183
|
end
|
|
124
184
|
end
|
|
125
185
|
|
|
126
186
|
describe "error handling" do
|
|
127
187
|
it "warns when the socket is missing" do
|
|
188
|
+
allow(MudisIPCConfig).to receive(:retries).and_return(1)
|
|
128
189
|
if MudisIPCConfig.use_tcp?
|
|
129
190
|
allow(TCPSocket).to receive(:new).and_raise(Errno::ECONNREFUSED)
|
|
130
191
|
else
|
|
@@ -143,5 +204,18 @@ RSpec.describe MudisClient do # rubocop:disable Metrics/BlockLength
|
|
|
143
204
|
|
|
144
205
|
expect { client.read("test_key") }.to raise_error("Something went wrong")
|
|
145
206
|
end
|
|
207
|
+
|
|
208
|
+
it "retries on timeout and then warns" do
|
|
209
|
+
allow(MudisIPCConfig).to receive(:retries).and_return(1)
|
|
210
|
+
allow(MudisIPCConfig).to receive(:timeout).and_return(0.01)
|
|
211
|
+
|
|
212
|
+
if MudisIPCConfig.use_tcp?
|
|
213
|
+
allow(TCPSocket).to receive(:new).and_raise(Timeout::Error)
|
|
214
|
+
else
|
|
215
|
+
allow(UNIXSocket).to receive(:open).and_raise(Timeout::Error)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
expect { client.read("test_key") }.to output(/Cannot connect/).to_stderr
|
|
219
|
+
end
|
|
146
220
|
end
|
|
147
221
|
end
|
data/spec/mudis_server_spec.rb
CHANGED
|
@@ -6,7 +6,12 @@ require "json"
|
|
|
6
6
|
require_relative "spec_helper"
|
|
7
7
|
|
|
8
8
|
RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
|
|
9
|
-
|
|
9
|
+
unless ENV["MUDIS_RUN_IPC"] == "true"
|
|
10
|
+
it "skips IPC socket tests unless MUDIS_RUN_IPC=true" do
|
|
11
|
+
skip "Set MUDIS_RUN_IPC=true to run IPC socket tests"
|
|
12
|
+
end
|
|
13
|
+
next
|
|
14
|
+
end
|
|
10
15
|
|
|
11
16
|
before(:all) do
|
|
12
17
|
# Start the server once for all tests
|
|
@@ -20,12 +25,18 @@ RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
|
|
|
20
25
|
allow(Mudis).to receive(:delete)
|
|
21
26
|
allow(Mudis).to receive(:exists?).and_return(true)
|
|
22
27
|
allow(Mudis).to receive(:fetch).and_return("mock_fetched_value")
|
|
28
|
+
allow(Mudis).to receive(:inspect).and_return({ key: "test_key", size_bytes: 10 })
|
|
29
|
+
allow(Mudis).to receive(:keys).and_return(["a", "b"])
|
|
30
|
+
allow(Mudis).to receive(:clear_namespace)
|
|
31
|
+
allow(Mudis).to receive(:least_touched).and_return([["a", 0]])
|
|
32
|
+
allow(Mudis).to receive(:all_keys).and_return(["k1"])
|
|
33
|
+
allow(Mudis).to receive(:current_memory_bytes).and_return(123)
|
|
34
|
+
allow(Mudis).to receive(:max_memory_bytes).and_return(456)
|
|
23
35
|
allow(Mudis).to receive(:metrics).and_return({ reads: 1, writes: 1 })
|
|
24
|
-
allow(Mudis).to receive(:reset_metrics!)
|
|
25
|
-
allow(Mudis).to receive(:reset!)
|
|
26
36
|
end
|
|
27
37
|
|
|
28
|
-
after do
|
|
38
|
+
after(:all) do
|
|
39
|
+
socket_path = MudisIPCConfig::SOCKET_PATH
|
|
29
40
|
File.unlink(socket_path) if File.exist?(socket_path) && !MudisIPCConfig.use_tcp?
|
|
30
41
|
end
|
|
31
42
|
|
|
@@ -36,7 +47,7 @@ RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
|
|
|
36
47
|
JSON.parse(sock.gets, symbolize_names: true)
|
|
37
48
|
end
|
|
38
49
|
else
|
|
39
|
-
UNIXSocket.open(
|
|
50
|
+
UNIXSocket.open(MudisIPCConfig::SOCKET_PATH) do |sock|
|
|
40
51
|
sock.puts(JSON.dump(request))
|
|
41
52
|
JSON.parse(sock.gets, symbolize_names: true)
|
|
42
53
|
end
|
|
@@ -74,22 +85,52 @@ RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
|
|
|
74
85
|
expect(Mudis).to have_received(:fetch).with("test_key", expires_in: 60, namespace: "test_ns")
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
it "handles the '
|
|
78
|
-
response = send_request({ cmd: "
|
|
79
|
-
expect(response).to eq({ ok: true, value: {
|
|
80
|
-
expect(Mudis).to have_received(:
|
|
88
|
+
it "handles the 'inspect' command" do
|
|
89
|
+
response = send_request({ cmd: "inspect", key: "test_key", namespace: "test_ns" })
|
|
90
|
+
expect(response).to eq({ ok: true, value: { key: "test_key", size_bytes: 10 } })
|
|
91
|
+
expect(Mudis).to have_received(:inspect).with("test_key", namespace: "test_ns")
|
|
81
92
|
end
|
|
82
93
|
|
|
83
|
-
it "handles the '
|
|
84
|
-
response = send_request({ cmd: "
|
|
85
|
-
expect(response).to eq({ ok: true, value:
|
|
86
|
-
expect(Mudis).to have_received(:
|
|
94
|
+
it "handles the 'keys' command" do
|
|
95
|
+
response = send_request({ cmd: "keys", namespace: "test_ns" })
|
|
96
|
+
expect(response).to eq({ ok: true, value: ["a", "b"] })
|
|
97
|
+
expect(Mudis).to have_received(:keys).with(namespace: "test_ns")
|
|
87
98
|
end
|
|
88
99
|
|
|
89
|
-
it "handles the '
|
|
90
|
-
response = send_request({ cmd: "
|
|
100
|
+
it "handles the 'clear_namespace' command" do
|
|
101
|
+
response = send_request({ cmd: "clear_namespace", namespace: "test_ns" })
|
|
91
102
|
expect(response).to eq({ ok: true, value: nil })
|
|
92
|
-
expect(Mudis).to have_received(:
|
|
103
|
+
expect(Mudis).to have_received(:clear_namespace).with(namespace: "test_ns")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "handles the 'least_touched' command" do
|
|
107
|
+
response = send_request({ cmd: "least_touched", limit: 5 })
|
|
108
|
+
expect(response).to eq({ ok: true, value: [["a", 0]] })
|
|
109
|
+
expect(Mudis).to have_received(:least_touched).with(5)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "handles the 'all_keys' command" do
|
|
113
|
+
response = send_request({ cmd: "all_keys" })
|
|
114
|
+
expect(response).to eq({ ok: true, value: ["k1"] })
|
|
115
|
+
expect(Mudis).to have_received(:all_keys)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "handles the 'current_memory_bytes' command" do
|
|
119
|
+
response = send_request({ cmd: "current_memory_bytes" })
|
|
120
|
+
expect(response).to eq({ ok: true, value: 123 })
|
|
121
|
+
expect(Mudis).to have_received(:current_memory_bytes)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "handles the 'max_memory_bytes' command" do
|
|
125
|
+
response = send_request({ cmd: "max_memory_bytes" })
|
|
126
|
+
expect(response).to eq({ ok: true, value: 456 })
|
|
127
|
+
expect(Mudis).to have_received(:max_memory_bytes)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "handles the 'metrics' command" do
|
|
131
|
+
response = send_request({ cmd: "metrics" })
|
|
132
|
+
expect(response).to eq({ ok: true, value: { reads: 1, writes: 1 } })
|
|
133
|
+
expect(Mudis).to have_received(:metrics)
|
|
93
134
|
end
|
|
94
135
|
|
|
95
136
|
it "handles unknown commands" do
|
data/spec/mudis_spec.rb
CHANGED
|
@@ -55,6 +55,19 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
|
55
55
|
Mudis.update("counter") { |v| v + 1 }
|
|
56
56
|
expect(Mudis.read("counter")).to eq(6)
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
it "refreshes TTL based on original duration" do
|
|
60
|
+
Mudis.write("ttl_key", "v", expires_in: 2)
|
|
61
|
+
meta_before = Mudis.inspect("ttl_key")
|
|
62
|
+
original_ttl = meta_before[:expires_at] - meta_before[:created_at]
|
|
63
|
+
|
|
64
|
+
sleep 1
|
|
65
|
+
Mudis.update("ttl_key") { |v| v }
|
|
66
|
+
meta_after = Mudis.inspect("ttl_key")
|
|
67
|
+
|
|
68
|
+
expect(meta_after[:created_at]).to be > meta_before[:created_at]
|
|
69
|
+
expect(meta_after[:expires_at]).to be_within(0.5).of(Time.now + original_ttl)
|
|
70
|
+
end
|
|
58
71
|
end
|
|
59
72
|
|
|
60
73
|
describe ".fetch" do
|
|
@@ -76,6 +89,30 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
|
|
|
76
89
|
result = Mudis.fetch("k", force: true) { 200 } # fix
|
|
77
90
|
expect(result).to eq(200)
|
|
78
91
|
end
|
|
92
|
+
|
|
93
|
+
it "executes the block once with singleflight: true" do
|
|
94
|
+
Mudis.delete("sf")
|
|
95
|
+
count = 0
|
|
96
|
+
count_mutex = Mutex.new
|
|
97
|
+
results = []
|
|
98
|
+
results_mutex = Mutex.new
|
|
99
|
+
|
|
100
|
+
threads = 5.times.map do
|
|
101
|
+
Thread.new do
|
|
102
|
+
value = Mudis.fetch("sf", singleflight: true) do
|
|
103
|
+
count_mutex.synchronize { count += 1 }
|
|
104
|
+
sleep 0.05
|
|
105
|
+
"v"
|
|
106
|
+
end
|
|
107
|
+
results_mutex.synchronize { results << value }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
threads.each(&:join)
|
|
112
|
+
expect(count).to eq(1)
|
|
113
|
+
expect(results).to all(eq("v"))
|
|
114
|
+
expect(Mudis.read("sf")).to eq("v")
|
|
115
|
+
end
|
|
79
116
|
end
|
|
80
117
|
|
|
81
118
|
describe ".clear" do
|
data/spec/namespace_spec.rb
CHANGED
|
@@ -23,6 +23,29 @@ RSpec.describe "Mudis Namespace Operations" do # rubocop:disable Metrics/BlockLe
|
|
|
23
23
|
expect(Mudis.read("x")).to be_nil
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
it "does not double-prefix keys in exists? under thread namespace" do
|
|
27
|
+
Mudis.with_namespace("ns") do
|
|
28
|
+
Mudis.write("k", "v")
|
|
29
|
+
expect(Mudis.exists?("k")).to be true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "does not double-prefix keys in fetch under thread namespace" do
|
|
34
|
+
Mudis.with_namespace("ns") do
|
|
35
|
+
value = Mudis.fetch("k") { "v" }
|
|
36
|
+
expect(value).to eq("v")
|
|
37
|
+
expect(Mudis.read("k")).to eq("v")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "does not double-prefix keys in replace under thread namespace" do
|
|
42
|
+
Mudis.with_namespace("ns") do
|
|
43
|
+
Mudis.write("k", "v")
|
|
44
|
+
Mudis.replace("k", "v2")
|
|
45
|
+
expect(Mudis.read("k")).to eq("v2")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
26
49
|
describe ".keys" do
|
|
27
50
|
it "returns only keys for the given namespace" do
|
|
28
51
|
Mudis.write("user:1", "Alice", namespace: "users")
|