mudis 0.9.1 → 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 +41 -5
- data/lib/mudis/bound.rb +128 -0
- data/lib/mudis/metrics.rb +21 -2
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis.rb +99 -23
- 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 +11 -2
- data/sig/mudis_bound.rbs +25 -0
- data/sig/mudis_client.rbs +13 -3
- data/sig/mudis_config.rbs +1 -0
- data/sig/mudis_ipc_config.rbs +8 -0
- data/sig/mudis_metrics.rbs +1 -1
- data/spec/api_compatibility_spec.rb +3 -0
- data/spec/bound_spec.rb +89 -0
- data/spec/guardrails_spec.rb +14 -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 +83 -9
- data/spec/mudis_server_spec.rb +51 -17
- data/spec/mudis_spec.rb +37 -0
- metadata +7 -2
data/lib/mudis_client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
4
|
require "json"
|
|
5
|
+
require "timeout"
|
|
5
6
|
require_relative "mudis_ipc_config"
|
|
6
7
|
|
|
7
8
|
# Thread-safe client for communicating with the MudisServer
|
|
@@ -25,28 +26,41 @@ class MudisClient
|
|
|
25
26
|
# Send a request to the MudisServer and return the response
|
|
26
27
|
# @param payload [Hash] The request payload
|
|
27
28
|
# @return [Object] The response value from the server
|
|
28
|
-
def request(payload) # rubocop:disable Metrics/MethodLength
|
|
29
|
+
def request(payload) # rubocop:disable Metrics/MethodLength
|
|
29
30
|
@mutex.synchronize do
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
attempts = 0
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
attempts += 1
|
|
35
|
+
response = nil
|
|
36
|
+
|
|
37
|
+
Timeout.timeout(MudisIPCConfig.timeout) do
|
|
38
|
+
sock = open_connection
|
|
39
|
+
sock.puts(JSON.dump(payload))
|
|
40
|
+
response = sock.gets
|
|
41
|
+
sock.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return nil unless response
|
|
45
|
+
|
|
46
|
+
res = JSON.parse(response, symbolize_names: true)
|
|
47
|
+
raise res[:error] unless res[:ok]
|
|
48
|
+
|
|
49
|
+
res[:value]
|
|
50
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED, Timeout::Error
|
|
51
|
+
if attempts <= MudisIPCConfig.retries
|
|
52
|
+
retry
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
warn "[MudisClient] Cannot connect to MudisServer. Is it running?"
|
|
56
|
+
nil
|
|
57
|
+
rescue JSON::ParserError
|
|
58
|
+
warn "[MudisClient] Invalid JSON response from server"
|
|
59
|
+
nil
|
|
60
|
+
rescue IOError, SystemCallError => e
|
|
61
|
+
warn "[MudisClient] Connection error: #{e.message}"
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
50
64
|
end
|
|
51
65
|
end
|
|
52
66
|
|
|
@@ -82,19 +96,44 @@ class MudisClient
|
|
|
82
96
|
new_val
|
|
83
97
|
end
|
|
84
98
|
|
|
85
|
-
#
|
|
86
|
-
def
|
|
87
|
-
command("
|
|
99
|
+
# Inspect metadata for a key
|
|
100
|
+
def inspect(key, namespace: nil)
|
|
101
|
+
command("inspect", key:, namespace:)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Return keys for a namespace
|
|
105
|
+
def keys(namespace:)
|
|
106
|
+
command("keys", namespace:)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Clear keys in a namespace
|
|
110
|
+
def clear_namespace(namespace:)
|
|
111
|
+
command("clear_namespace", namespace:)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Return least touched keys
|
|
115
|
+
def least_touched(limit = 10)
|
|
116
|
+
command("least_touched", limit:)
|
|
88
117
|
end
|
|
89
118
|
|
|
90
|
-
#
|
|
91
|
-
def
|
|
92
|
-
command("
|
|
119
|
+
# Return all keys
|
|
120
|
+
def all_keys
|
|
121
|
+
command("all_keys")
|
|
93
122
|
end
|
|
94
123
|
|
|
95
|
-
#
|
|
96
|
-
def
|
|
97
|
-
command("
|
|
124
|
+
# Return current memory usage
|
|
125
|
+
def current_memory_bytes
|
|
126
|
+
command("current_memory_bytes")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Return max memory configured
|
|
130
|
+
def max_memory_bytes
|
|
131
|
+
command("max_memory_bytes")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Retrieve metrics from the Mudis server
|
|
135
|
+
def metrics
|
|
136
|
+
command("metrics")
|
|
98
137
|
end
|
|
99
138
|
|
|
100
139
|
private
|
|
@@ -103,5 +142,4 @@ class MudisClient
|
|
|
103
142
|
def command(cmd, **opts)
|
|
104
143
|
request({ cmd:, **opts })
|
|
105
144
|
end
|
|
106
|
-
|
|
107
145
|
end
|
data/lib/mudis_config.rb
CHANGED
|
@@ -8,6 +8,7 @@ class MudisConfig
|
|
|
8
8
|
:max_value_bytes,
|
|
9
9
|
:hard_memory_limit,
|
|
10
10
|
:max_bytes,
|
|
11
|
+
:eviction_threshold,
|
|
11
12
|
:buckets,
|
|
12
13
|
:max_ttl,
|
|
13
14
|
:default_ttl,
|
|
@@ -23,6 +24,7 @@ class MudisConfig
|
|
|
23
24
|
@max_value_bytes = nil # Max size per value (optional)
|
|
24
25
|
@hard_memory_limit = false # Enforce max_bytes as hard cap
|
|
25
26
|
@max_bytes = 1_073_741_824 # 1 GB default max cache size
|
|
27
|
+
@eviction_threshold = 0.9 # Evict when bucket exceeds threshold
|
|
26
28
|
@buckets = nil # use nil to signal fallback to ENV or default
|
|
27
29
|
@max_ttl = nil # Max TTL for cache entries (optional)
|
|
28
30
|
@default_ttl = nil # Default TTL for cache entries (optional)
|
data/lib/mudis_ipc_config.rb
CHANGED
|
@@ -5,9 +5,19 @@ module MudisIPCConfig
|
|
|
5
5
|
SOCKET_PATH = "/tmp/mudis.sock"
|
|
6
6
|
TCP_HOST = "127.0.0.1"
|
|
7
7
|
TCP_PORT = 9876
|
|
8
|
+
DEFAULT_TIMEOUT = 1
|
|
9
|
+
DEFAULT_RETRIES = 1
|
|
8
10
|
|
|
9
11
|
# Check if TCP mode should be used (Windows or forced via ENV)
|
|
10
12
|
def self.use_tcp?
|
|
11
13
|
ENV["MUDIS_FORCE_TCP"] == "true" || Gem.win_platform?
|
|
12
14
|
end
|
|
15
|
+
|
|
16
|
+
def self.timeout
|
|
17
|
+
(ENV["MUDIS_IPC_TIMEOUT"] || DEFAULT_TIMEOUT).to_f
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.retries
|
|
21
|
+
(ENV["MUDIS_IPC_RETRIES"] || DEFAULT_RETRIES).to_i
|
|
22
|
+
end
|
|
13
23
|
end
|
data/lib/mudis_server.rb
CHANGED
|
@@ -13,14 +13,19 @@ class MudisServer
|
|
|
13
13
|
# Define command handlers mapping
|
|
14
14
|
# Each command maps to a lambda that takes a request hash and performs the corresponding Mudis operation.
|
|
15
15
|
COMMANDS = {
|
|
16
|
-
"read"
|
|
17
|
-
"write"
|
|
18
|
-
"delete"
|
|
19
|
-
"exists"
|
|
20
|
-
"fetch"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
16
|
+
"read" => ->(r) { Mudis.read(r[:key], namespace: r[:namespace]) },
|
|
17
|
+
"write" => ->(r) { Mudis.write(r[:key], r[:value], expires_in: r[:ttl], namespace: r[:namespace]) },
|
|
18
|
+
"delete" => ->(r) { Mudis.delete(r[:key], namespace: r[:namespace]) },
|
|
19
|
+
"exists" => ->(r) { Mudis.exists?(r[:key], namespace: r[:namespace]) },
|
|
20
|
+
"fetch" => ->(r) { Mudis.fetch(r[:key], expires_in: r[:ttl], namespace: r[:namespace]) { r[:fallback] } },
|
|
21
|
+
"inspect" => ->(r) { Mudis.inspect(r[:key], namespace: r[:namespace]) },
|
|
22
|
+
"keys" => ->(r) { Mudis.keys(namespace: r[:namespace]) },
|
|
23
|
+
"clear_namespace" => ->(r) { Mudis.clear_namespace(namespace: r[:namespace]) },
|
|
24
|
+
"least_touched" => ->(r) { Mudis.least_touched(r[:limit]) },
|
|
25
|
+
"all_keys" => ->(_) { Mudis.all_keys },
|
|
26
|
+
"current_memory_bytes" => ->(_) { Mudis.current_memory_bytes },
|
|
27
|
+
"max_memory_bytes" => ->(_) { Mudis.max_memory_bytes },
|
|
28
|
+
"metrics" => ->(_) { Mudis.metrics }
|
|
24
29
|
}.freeze
|
|
25
30
|
|
|
26
31
|
# Start the MudisServer
|
|
@@ -43,7 +48,7 @@ class MudisServer
|
|
|
43
48
|
end
|
|
44
49
|
|
|
45
50
|
# Start UNIX socket server (production mode for Linux/macOS)
|
|
46
|
-
def self.start_unix_server!
|
|
51
|
+
def self.start_unix_server!
|
|
47
52
|
File.unlink(SOCKET_PATH) if File.exist?(SOCKET_PATH)
|
|
48
53
|
server = UNIXServer.new(SOCKET_PATH)
|
|
49
54
|
server.listen(128)
|
data/sig/mudis.rbs
CHANGED
|
@@ -14,6 +14,7 @@ class Mudis
|
|
|
14
14
|
attr_reader max_value_bytes : Integer?
|
|
15
15
|
attr_accessor max_ttl: Integer?
|
|
16
16
|
attr_accessor default_ttl: Integer?
|
|
17
|
+
attr_accessor eviction_threshold: Float?
|
|
17
18
|
|
|
18
19
|
def configure: () { (config: MudisConfig) -> void } -> void
|
|
19
20
|
def config: () -> MudisConfig
|
|
@@ -21,6 +22,13 @@ class Mudis
|
|
|
21
22
|
def validate_config!: () -> void
|
|
22
23
|
|
|
23
24
|
def buckets: () -> Integer
|
|
25
|
+
|
|
26
|
+
def bind: (
|
|
27
|
+
namespace: String,
|
|
28
|
+
?default_ttl: Integer?,
|
|
29
|
+
?max_ttl: Integer?,
|
|
30
|
+
?max_value_bytes: Integer?
|
|
31
|
+
) -> Mudis::Bound
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
# Lifecycle
|
|
@@ -39,7 +47,8 @@ class Mudis
|
|
|
39
47
|
String,
|
|
40
48
|
?expires_in: Integer,
|
|
41
49
|
?force: bool,
|
|
42
|
-
?namespace: String
|
|
50
|
+
?namespace: String,
|
|
51
|
+
?singleflight: bool
|
|
43
52
|
) { () -> untyped } -> untyped
|
|
44
53
|
|
|
45
54
|
def self.clear: (String, ?namespace: String) -> void
|
|
@@ -50,7 +59,7 @@ class Mudis
|
|
|
50
59
|
def self.with_namespace: (namespace: String) { () -> untyped } -> untyped
|
|
51
60
|
|
|
52
61
|
# Introspection & management
|
|
53
|
-
def self.metrics: () -> Hash[Symbol, untyped]
|
|
62
|
+
def self.metrics: (?namespace: String) -> Hash[Symbol, untyped]
|
|
54
63
|
def self.cleanup_expired!: () -> void
|
|
55
64
|
def self.all_keys: () -> Array[String]
|
|
56
65
|
def self.current_memory_bytes: () -> Integer
|
data/sig/mudis_bound.rbs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Mudis
|
|
2
|
+
class Bound
|
|
3
|
+
attr_reader namespace: String
|
|
4
|
+
|
|
5
|
+
def initialize: (
|
|
6
|
+
namespace: String,
|
|
7
|
+
?default_ttl: Integer?,
|
|
8
|
+
?max_ttl: Integer?,
|
|
9
|
+
?max_value_bytes: Integer?
|
|
10
|
+
) -> void
|
|
11
|
+
|
|
12
|
+
def read: (String) -> untyped?
|
|
13
|
+
def write: (String, untyped, ?expires_in: Integer?) -> void
|
|
14
|
+
def update: (String) { (untyped) -> untyped } -> void
|
|
15
|
+
def delete: (String) -> void
|
|
16
|
+
def exists?: (String) -> bool
|
|
17
|
+
def fetch: (String, ?expires_in: Integer?, ?force: bool, ?singleflight: bool) { () -> untyped } -> untyped?
|
|
18
|
+
def clear: (String) -> void
|
|
19
|
+
def replace: (String, untyped, ?expires_in: Integer?) -> void
|
|
20
|
+
def inspect: (String) -> Hash[Symbol, untyped]?
|
|
21
|
+
def keys: () -> Array[String]
|
|
22
|
+
def metrics: () -> Hash[Symbol, untyped]
|
|
23
|
+
def clear_namespace: () -> void
|
|
24
|
+
end
|
|
25
|
+
end
|
data/sig/mudis_client.rbs
CHANGED
|
@@ -17,9 +17,19 @@ class MudisClient
|
|
|
17
17
|
|
|
18
18
|
def fetch: (key: String, expires_in?: Integer?, namespace?: String?, &block: { () -> untyped }) -> untyped
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def inspect: (key: String, namespace?: String?) -> Hash[Symbol, untyped]?
|
|
21
|
+
|
|
22
|
+
def keys: (namespace: String) -> Array[String]
|
|
23
|
+
|
|
24
|
+
def clear_namespace: (namespace: String) -> void
|
|
25
|
+
|
|
26
|
+
def least_touched: (?Integer) -> Array[[String, Integer]]
|
|
21
27
|
|
|
22
|
-
def
|
|
28
|
+
def all_keys: () -> Array[String]
|
|
23
29
|
|
|
24
|
-
def
|
|
30
|
+
def current_memory_bytes: () -> Integer
|
|
31
|
+
|
|
32
|
+
def max_memory_bytes: () -> Integer
|
|
33
|
+
|
|
34
|
+
def metrics: () -> Hash[Symbol, untyped]
|
|
25
35
|
end
|
data/sig/mudis_config.rbs
CHANGED
|
@@ -4,6 +4,7 @@ class MudisConfig
|
|
|
4
4
|
attr_accessor max_value_bytes: Integer?
|
|
5
5
|
attr_accessor hard_memory_limit: bool
|
|
6
6
|
attr_accessor max_bytes: Integer
|
|
7
|
+
attr_accessor eviction_threshold: Float?
|
|
7
8
|
attr_accessor max_ttl: Integer?
|
|
8
9
|
attr_accessor default_ttl: Integer?
|
|
9
10
|
attr_accessor buckets: Integer?
|
data/sig/mudis_ipc_config.rbs
CHANGED
data/sig/mudis_metrics.rbs
CHANGED
|
@@ -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/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
|