mudis 0.8.1 → 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.
@@ -0,0 +1,13 @@
1
+ class Mudis
2
+ module Expiry
3
+ def start_expiry_thread: (?interval: Integer) -> void
4
+
5
+ def stop_expiry_thread: () -> void
6
+
7
+ def cleanup_expired!: () -> void
8
+
9
+ private
10
+
11
+ def effective_ttl: (?expires_in: Integer?) -> Integer?
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # Shared configuration for IPC mode (server and client)
2
+ module MudisIPCConfig
3
+ SOCKET_PATH: String
4
+
5
+ TCP_HOST: String
6
+
7
+ TCP_PORT: Integer
8
+
9
+ def self.use_tcp?: () -> bool
10
+ end
data/sig/mudis_lru.rbs ADDED
@@ -0,0 +1,21 @@
1
+ class Mudis
2
+ class LRUNode
3
+ attr_accessor key: String
4
+ attr_accessor prev: LRUNode?
5
+ attr_accessor next: LRUNode?
6
+
7
+ def initialize: (String) -> void
8
+ end
9
+
10
+ module LRU
11
+ private
12
+
13
+ def evict_key: (Integer, String) -> void
14
+
15
+ def insert_lru: (Integer, LRUNode) -> void
16
+
17
+ def promote_lru: (Integer, String) -> void
18
+
19
+ def remove_node: (Integer, LRUNode) -> void
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ class Mudis
2
+ module Metrics
3
+ def metrics: () -> Hash[Symbol, untyped]
4
+
5
+ def reset_metrics!: () -> void
6
+
7
+ private
8
+
9
+ def metric: (Symbol) -> void
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ class Mudis
2
+ module Namespace
3
+ def keys: (namespace: String) -> Array[String]
4
+
5
+ def clear_namespace: (namespace: String) -> void
6
+
7
+ def with_namespace: (namespace: String) { () -> untyped } -> untyped
8
+
9
+ private
10
+
11
+ def namespaced_key: (String, ?namespace: String?) -> String
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ class Mudis
2
+ module Persistence
3
+ def save_snapshot!: () -> void
4
+
5
+ def load_snapshot!: () -> void
6
+
7
+ def install_persistence_hook!: () -> void
8
+
9
+ private
10
+
11
+ def snapshot_dump: () -> Hash[String, untyped]
12
+
13
+ def snapshot_restore: (Hash[String, untyped]) -> void
14
+
15
+ def safe_write_snapshot: (Hash[String, untyped]) -> void
16
+
17
+ def read_snapshot: () -> Hash[String, untyped]
18
+ end
19
+ end
data/sig/mudis_server.rbs CHANGED
@@ -1,7 +1,17 @@
1
1
  class MudisServer
2
- SOCKET_PATH: String
2
+ include MudisIPCConfig
3
3
 
4
4
  def self.start!: () -> void
5
5
 
6
- def self.handle_client: (sock: UNIXSocket) -> void
6
+ def self.start_tcp_server!: () -> void
7
+
8
+ def self.start_unix_server!: () -> void
9
+
10
+ def self.accept_connections: (server: (TCPServer | UNIXServer)) -> void
11
+
12
+ def self.handle_client: (socket: (TCPSocket | UNIXSocket)) -> void
13
+
14
+ def self.process_request: (req: Hash[String, untyped]) -> Hash[Symbol, untyped]
15
+
16
+ def self.write_response: (socket: (TCPSocket | UNIXSocket), payload: Hash[Symbol, untyped]) -> void
7
17
  end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spec_helper"
4
+
5
+ RSpec.describe "Mudis Public API" do
6
+ describe "ensures no breaking changes" do
7
+ it "exposes all core cache operations" do
8
+ expect(Mudis).to respond_to(:read)
9
+ expect(Mudis).to respond_to(:write)
10
+ expect(Mudis).to respond_to(:delete)
11
+ expect(Mudis).to respond_to(:exists?)
12
+ expect(Mudis).to respond_to(:update)
13
+ expect(Mudis).to respond_to(:fetch)
14
+ expect(Mudis).to respond_to(:clear)
15
+ expect(Mudis).to respond_to(:replace)
16
+ expect(Mudis).to respond_to(:inspect)
17
+ end
18
+
19
+ it "exposes all configuration methods" do
20
+ expect(Mudis).to respond_to(:configure)
21
+ expect(Mudis).to respond_to(:config)
22
+ expect(Mudis).to respond_to(:serializer)
23
+ expect(Mudis).to respond_to(:serializer=)
24
+ expect(Mudis).to respond_to(:compress)
25
+ expect(Mudis).to respond_to(:compress=)
26
+ expect(Mudis).to respond_to(:max_bytes)
27
+ expect(Mudis).to respond_to(:max_bytes=)
28
+ expect(Mudis).to respond_to(:max_value_bytes)
29
+ expect(Mudis).to respond_to(:max_value_bytes=)
30
+ expect(Mudis).to respond_to(:hard_memory_limit)
31
+ expect(Mudis).to respond_to(:hard_memory_limit=)
32
+ expect(Mudis).to respond_to(:max_ttl)
33
+ expect(Mudis).to respond_to(:max_ttl=)
34
+ expect(Mudis).to respond_to(:default_ttl)
35
+ expect(Mudis).to respond_to(:default_ttl=)
36
+ end
37
+
38
+ it "exposes all metrics methods" do
39
+ expect(Mudis).to respond_to(:metrics)
40
+ expect(Mudis).to respond_to(:reset_metrics!)
41
+ end
42
+
43
+ it "exposes all expiry methods" do
44
+ expect(Mudis).to respond_to(:start_expiry_thread)
45
+ expect(Mudis).to respond_to(:stop_expiry_thread)
46
+ expect(Mudis).to respond_to(:cleanup_expired!)
47
+ end
48
+
49
+ it "exposes all namespace methods" do
50
+ expect(Mudis).to respond_to(:keys)
51
+ expect(Mudis).to respond_to(:clear_namespace)
52
+ expect(Mudis).to respond_to(:with_namespace)
53
+ end
54
+
55
+ it "exposes all persistence methods" do
56
+ expect(Mudis).to respond_to(:save_snapshot!)
57
+ expect(Mudis).to respond_to(:load_snapshot!)
58
+ end
59
+
60
+ it "exposes all utility methods" do
61
+ expect(Mudis).to respond_to(:reset!)
62
+ expect(Mudis).to respond_to(:all_keys)
63
+ expect(Mudis).to respond_to(:current_memory_bytes)
64
+ expect(Mudis).to respond_to(:max_memory_bytes)
65
+ expect(Mudis).to respond_to(:least_touched)
66
+ expect(Mudis).to respond_to(:buckets)
67
+ end
68
+
69
+ it "maintains backward compatibility with method signatures" do
70
+ # Core operations accept namespace parameter
71
+ expect(Mudis.method(:read).parameters).to include([:key, :namespace])
72
+ expect(Mudis.method(:write).parameters).to include([:key, :namespace])
73
+ expect(Mudis.method(:delete).parameters).to include([:key, :namespace])
74
+ expect(Mudis.method(:exists?).parameters).to include([:key, :namespace])
75
+
76
+ # Fetch accepts expires_in, force, and namespace
77
+ expect(Mudis.method(:fetch).parameters).to include([:key, :expires_in])
78
+ expect(Mudis.method(:fetch).parameters).to include([:key, :force])
79
+ expect(Mudis.method(:fetch).parameters).to include([:key, :namespace])
80
+ end
81
+
82
+ it "verifies LRUNode class is still accessible" do
83
+ expect(defined?(Mudis::LRUNode)).to be_truthy
84
+ node = Mudis::LRUNode.new("test_key")
85
+ expect(node).to respond_to(:key)
86
+ expect(node).to respond_to(:prev)
87
+ expect(node).to respond_to(:next)
88
+ end
89
+
90
+ it "verifies all public methods work correctly" do
91
+ Mudis.reset!
92
+
93
+ # Write and read
94
+ Mudis.write("test", "value")
95
+ expect(Mudis.read("test")).to eq("value")
96
+
97
+ # Exists
98
+ expect(Mudis.exists?("test")).to be true
99
+
100
+ # Update
101
+ Mudis.update("test") { |v| v.upcase }
102
+ expect(Mudis.read("test")).to eq("VALUE")
103
+
104
+ # Fetch
105
+ result = Mudis.fetch("new_key") { "new_value" }
106
+ expect(result).to eq("new_value")
107
+
108
+ # Replace
109
+ Mudis.replace("test", "replaced")
110
+ expect(Mudis.read("test")).to eq("replaced")
111
+
112
+ # Inspect
113
+ meta = Mudis.inspect("test")
114
+ expect(meta).to include(:key, :bucket, :created_at, :size_bytes)
115
+
116
+ # Namespace operations
117
+ Mudis.write("k1", "v1", namespace: "ns1")
118
+ expect(Mudis.keys(namespace: "ns1")).to include("k1")
119
+
120
+ Mudis.with_namespace("ns2") do
121
+ Mudis.write("k2", "v2")
122
+ end
123
+ expect(Mudis.keys(namespace: "ns2")).to include("k2")
124
+
125
+ # Clear namespace
126
+ Mudis.clear_namespace(namespace: "ns1")
127
+ expect(Mudis.keys(namespace: "ns1")).to be_empty
128
+
129
+ # Metrics
130
+ metrics = Mudis.metrics
131
+ expect(metrics).to include(:hits, :misses, :evictions, :total_memory, :buckets)
132
+
133
+ # Least touched
134
+ touched = Mudis.least_touched(5)
135
+ expect(touched).to be_an(Array)
136
+
137
+ # All keys
138
+ keys = Mudis.all_keys
139
+ expect(keys).to be_an(Array)
140
+
141
+ # Memory tracking
142
+ expect(Mudis.current_memory_bytes).to be > 0
143
+ expect(Mudis.max_memory_bytes).to be > 0
144
+
145
+ # Delete
146
+ Mudis.delete("test")
147
+ expect(Mudis.exists?("test")).to be false
148
+
149
+ # Clear (alias for delete)
150
+ Mudis.write("to_clear", "value")
151
+ Mudis.clear("to_clear")
152
+ expect(Mudis.exists?("to_clear")).to be false
153
+ end
154
+ end
155
+ 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
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../spec_helper"
4
+
5
+ RSpec.describe Mudis::LRU do
6
+ let(:test_class) do
7
+ Class.new do
8
+ extend Mudis::LRU
9
+
10
+ @stores = Array.new(2) { {} }
11
+ @lru_heads = Array.new(2) { nil }
12
+ @lru_tails = Array.new(2) { nil }
13
+ @lru_nodes = Array.new(2) { {} }
14
+ @current_bytes = Array.new(2, 0)
15
+
16
+ class << self
17
+ attr_accessor :stores, :lru_heads, :lru_tails, :lru_nodes, :current_bytes
18
+ end
19
+ end
20
+ end
21
+
22
+ describe "LRUNode" do
23
+ it "initializes with a key" do
24
+ node = Mudis::LRUNode.new("test_key")
25
+
26
+ expect(node.key).to eq("test_key")
27
+ expect(node.prev).to be_nil
28
+ expect(node.next).to be_nil
29
+ end
30
+
31
+ it "allows setting prev and next" do
32
+ node1 = Mudis::LRUNode.new("key1")
33
+ node2 = Mudis::LRUNode.new("key2")
34
+
35
+ node1.next = node2
36
+ node2.prev = node1
37
+
38
+ expect(node1.next).to eq(node2)
39
+ expect(node2.prev).to eq(node1)
40
+ end
41
+ end
42
+
43
+ describe "#insert_lru (private)" do
44
+ it "inserts a node at the head of the LRU list" do
45
+ test_class.send(:insert_lru, 0, "key1")
46
+
47
+ expect(test_class.lru_heads[0]).to be_a(Mudis::LRUNode)
48
+ expect(test_class.lru_heads[0].key).to eq("key1")
49
+ expect(test_class.lru_tails[0]).to eq(test_class.lru_heads[0])
50
+ expect(test_class.lru_nodes[0]["key1"]).to be_a(Mudis::LRUNode)
51
+ end
52
+
53
+ it "maintains order when inserting multiple nodes" do
54
+ test_class.send(:insert_lru, 0, "key1")
55
+ test_class.send(:insert_lru, 0, "key2")
56
+ test_class.send(:insert_lru, 0, "key3")
57
+
58
+ expect(test_class.lru_heads[0].key).to eq("key3")
59
+ expect(test_class.lru_tails[0].key).to eq("key1")
60
+ end
61
+ end
62
+
63
+ describe "#promote_lru (private)" do
64
+ it "moves a node to the head of the LRU list" do
65
+ test_class.send(:insert_lru, 0, "key1")
66
+ test_class.send(:insert_lru, 0, "key2")
67
+ test_class.send(:insert_lru, 0, "key3")
68
+
69
+ test_class.send(:promote_lru, 0, "key1")
70
+
71
+ expect(test_class.lru_heads[0].key).to eq("key1")
72
+ end
73
+
74
+ it "does nothing if key is already at head" do
75
+ test_class.send(:insert_lru, 0, "key1")
76
+
77
+ head_before = test_class.lru_heads[0]
78
+ test_class.send(:promote_lru, 0, "key1")
79
+
80
+ expect(test_class.lru_heads[0].key).to eq("key1")
81
+ end
82
+
83
+ it "does nothing if node doesn't exist" do
84
+ expect { test_class.send(:promote_lru, 0, "nonexistent") }.not_to raise_error
85
+ end
86
+ end
87
+
88
+ describe "#remove_node (private)" do
89
+ it "removes a node from the middle of the list" do
90
+ test_class.send(:insert_lru, 0, "key1")
91
+ test_class.send(:insert_lru, 0, "key2")
92
+ test_class.send(:insert_lru, 0, "key3")
93
+
94
+ node = test_class.lru_nodes[0]["key2"]
95
+ test_class.send(:remove_node, 0, node)
96
+
97
+ expect(test_class.lru_heads[0].key).to eq("key3")
98
+ expect(test_class.lru_heads[0].next.key).to eq("key1")
99
+ end
100
+
101
+ it "updates head when removing head node" do
102
+ test_class.send(:insert_lru, 0, "key1")
103
+ test_class.send(:insert_lru, 0, "key2")
104
+
105
+ node = test_class.lru_heads[0]
106
+ test_class.send(:remove_node, 0, node)
107
+
108
+ expect(test_class.lru_heads[0].key).to eq("key1")
109
+ end
110
+
111
+ it "updates tail when removing tail node" do
112
+ test_class.send(:insert_lru, 0, "key1")
113
+ test_class.send(:insert_lru, 0, "key2")
114
+
115
+ node = test_class.lru_tails[0]
116
+ test_class.send(:remove_node, 0, node)
117
+
118
+ expect(test_class.lru_tails[0].key).to eq("key2")
119
+ end
120
+ end
121
+
122
+ describe "#evict_key (private)" do
123
+ it "removes key from store and LRU list" do
124
+ test_class.stores[0]["key1"] = { value: "test".b, expires_at: nil }
125
+ test_class.current_bytes[0] = 100
126
+ test_class.send(:insert_lru, 0, "key1")
127
+
128
+ test_class.send(:evict_key, 0, "key1")
129
+
130
+ expect(test_class.stores[0]).not_to have_key("key1")
131
+ expect(test_class.lru_nodes[0]).not_to have_key("key1")
132
+ expect(test_class.current_bytes[0]).to eq(92) # 100 - "key1".bytesize(4) - "test".bytesize(4)
133
+ end
134
+
135
+ it "does nothing if key doesn't exist" do
136
+ expect { test_class.send(:evict_key, 0, "nonexistent") }.not_to raise_error
137
+ end
138
+
139
+ it "updates memory counter correctly" do
140
+ test_class.stores[0]["mykey"] = { value: "myvalue".b, expires_at: nil }
141
+ test_class.current_bytes[0] = 100
142
+
143
+ test_class.send(:evict_key, 0, "mykey")
144
+
145
+ # 100 - ("mykey".bytesize + "myvalue".bytesize) = 100 - 12 = 88
146
+ expect(test_class.current_bytes[0]).to eq(88)
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../spec_helper"
4
+
5
+ RSpec.describe Mudis::Metrics do
6
+ let(:test_class) do
7
+ Class.new do
8
+ extend Mudis::Metrics
9
+
10
+ @metrics = { hits: 5, misses: 3, evictions: 2, rejected: 1 }
11
+ @metrics_mutex = Mutex.new
12
+ @buckets = 2
13
+ @stores = [{ "key1" => {} }, { "key2" => {} }]
14
+ @current_bytes = [100, 200]
15
+ @lru_nodes = [{ "key1" => nil }, { "key2" => nil }]
16
+
17
+ class << self
18
+ attr_accessor :metrics, :metrics_mutex, :buckets, :stores, :current_bytes, :lru_nodes
19
+
20
+ def current_memory_bytes
21
+ @current_bytes.sum
22
+ end
23
+
24
+ def least_touched(n)
25
+ [["key1", 5], ["key2", 10]]
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#metrics" do
32
+ it "returns a snapshot of current metrics" do
33
+ result = test_class.metrics
34
+
35
+ expect(result).to include(
36
+ hits: 5,
37
+ misses: 3,
38
+ evictions: 2,
39
+ rejected: 1
40
+ )
41
+ # total_memory and other derived stats depend on full Mudis context
42
+ end
43
+
44
+ it "includes least_touched keys via delegation" do
45
+ # This method delegates to least_touched which is defined in main class
46
+ # In integration tests this works, in isolation we just verify the structure
47
+ expect(test_class).to respond_to(:metrics)
48
+ end
49
+
50
+ it "includes per-bucket stats via delegation" do
51
+ # Bucket stats require full context from main class
52
+ # In integration tests this works, in isolation we just verify the method exists
53
+ expect(test_class).to respond_to(:metrics)
54
+ end
55
+
56
+ it "is thread-safe" do
57
+ threads = 10.times.map do
58
+ Thread.new { test_class.metrics }
59
+ end
60
+
61
+ expect { threads.each(&:join) }.not_to raise_error
62
+ end
63
+ end
64
+
65
+ describe "#reset_metrics!" do
66
+ it "resets all metric counters to zero" do
67
+ test_class.reset_metrics!
68
+
69
+ expect(test_class.metrics[:hits]).to eq(0)
70
+ expect(test_class.metrics[:misses]).to eq(0)
71
+ expect(test_class.metrics[:evictions]).to eq(0)
72
+ expect(test_class.metrics[:rejected]).to eq(0)
73
+ end
74
+
75
+ it "is thread-safe" do
76
+ threads = 10.times.map do
77
+ Thread.new { test_class.reset_metrics! }
78
+ end
79
+
80
+ expect { threads.each(&:join) }.not_to raise_error
81
+ end
82
+ end
83
+
84
+ describe "#metric (private)" do
85
+ it "increments metric counters" do
86
+ initial_hits = test_class.metrics[:hits]
87
+
88
+ test_class.send(:metric, :hits)
89
+
90
+ expect(test_class.metrics[:hits]).to eq(initial_hits + 1)
91
+ end
92
+
93
+ it "is thread-safe" do
94
+ test_class.reset_metrics!
95
+
96
+ threads = 100.times.map do
97
+ Thread.new { test_class.send(:metric, :hits) }
98
+ end
99
+
100
+ threads.each(&:join)
101
+
102
+ expect(test_class.metrics[:hits]).to eq(100)
103
+ end
104
+ end
105
+ end