mudis 0.5.0 → 0.7.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,68 @@
1
+ # frozen_string_literal: true
2
+ require "socket"
3
+ require "json"
4
+
5
+ class MudisClient
6
+ SOCKET_PATH = "/tmp/mudis.sock"
7
+
8
+ def initialize
9
+ @mutex = Mutex.new
10
+ end
11
+
12
+ def request(payload)
13
+ @mutex.synchronize do
14
+ UNIXSocket.open(SOCKET_PATH) do |sock|
15
+ sock.puts(JSON.dump(payload))
16
+ response = sock.gets
17
+ res = JSON.parse(response, symbolize_names: true)
18
+ raise res[:error] unless res[:ok]
19
+ res[:value]
20
+
21
+ end
22
+
23
+ rescue Errno::ENOENT
24
+ warn "[MudisClient] Socket missing; master likely not running MudisServer"
25
+ nil
26
+
27
+ end
28
+
29
+ end
30
+
31
+ def read(key, namespace: nil)
32
+ request(cmd: "read", key: key, namespace: namespace)
33
+ end
34
+
35
+ def write(key, value, expires_in: nil, namespace: nil)
36
+ request(cmd: "write", key: key, value: value, ttl: expires_in, namespace: namespace)
37
+ end
38
+
39
+ def delete(key, namespace: nil)
40
+ request(cmd: "delete", key: key, namespace: namespace)
41
+ end
42
+
43
+ def exists?(key, namespace: nil)
44
+ request(cmd: "exists", key: key, namespace: namespace)
45
+ end
46
+
47
+ def fetch(key, expires_in: nil, namespace: nil)
48
+ val = read(key, namespace: namespace)
49
+ return val if val
50
+
51
+ new_val = yield
52
+ write(key, new_val, expires_in: expires_in, namespace: namespace)
53
+ new_val
54
+ end
55
+
56
+ def metrics
57
+ request(cmd: "metrics")
58
+ end
59
+
60
+ def reset_metrics!
61
+ request(cmd: "reset_metrics")
62
+ end
63
+
64
+ def reset!
65
+ request(cmd: "reset")
66
+ end
67
+
68
+ end
data/lib/mudis_config.rb CHANGED
@@ -1,25 +1,25 @@
1
- # frozen_string_literal: true
2
-
3
- # MudisConfig holds all configuration values for Mudis,
4
- # and provides defaults that can be overridden via Mudis.configure.
5
- class MudisConfig
6
- attr_accessor :serializer,
7
- :compress,
8
- :max_value_bytes,
9
- :hard_memory_limit,
10
- :max_bytes,
11
- :buckets,
12
- :max_ttl,
13
- :default_ttl
14
-
15
- def initialize
16
- @serializer = JSON # Default serialization strategy
17
- @compress = false # Whether to compress values with Zlib
18
- @max_value_bytes = nil # Max size per value (optional)
19
- @hard_memory_limit = false # Enforce max_bytes as hard cap
20
- @max_bytes = 1_073_741_824 # 1 GB default max cache size
21
- @buckets = nil # use nil to signal fallback to ENV or default
22
- @max_ttl = nil # Max TTL for cache entries (optional)
23
- @default_ttl = nil # Default TTL for cache entries (optional)
24
- end
25
- end
1
+ # frozen_string_literal: true
2
+
3
+ # MudisConfig holds all configuration values for Mudis,
4
+ # and provides defaults that can be overridden via Mudis.configure.
5
+ class MudisConfig
6
+ attr_accessor :serializer,
7
+ :compress,
8
+ :max_value_bytes,
9
+ :hard_memory_limit,
10
+ :max_bytes,
11
+ :buckets,
12
+ :max_ttl,
13
+ :default_ttl
14
+
15
+ def initialize
16
+ @serializer = JSON # Default serialization strategy
17
+ @compress = false # Whether to compress values with Zlib
18
+ @max_value_bytes = nil # Max size per value (optional)
19
+ @hard_memory_limit = false # Enforce max_bytes as hard cap
20
+ @max_bytes = 1_073_741_824 # 1 GB default max cache size
21
+ @buckets = nil # use nil to signal fallback to ENV or default
22
+ @max_ttl = nil # Max TTL for cache entries (optional)
23
+ @default_ttl = nil # Default TTL for cache entries (optional)
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ require "socket"
3
+ require "json"
4
+ require_relative "mudis"
5
+
6
+ class MudisServer
7
+ SOCKET_PATH = "/tmp/mudis.sock"
8
+
9
+ def self.start!
10
+ # Clean up old socket if it exists
11
+ File.unlink(SOCKET_PATH) if File.exist?(SOCKET_PATH)
12
+
13
+ server = UNIXServer.new(SOCKET_PATH)
14
+ server.listen(128)
15
+ puts "[MudisServer] Listening on #{SOCKET_PATH}"
16
+
17
+ Thread.new do
18
+ loop do
19
+ client = server.accept
20
+ Thread.new(client) do |sock|
21
+ handle_client(sock)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.handle_client(sock)
28
+ request_line = sock.gets
29
+ return unless request_line
30
+
31
+ req = JSON.parse(request_line, symbolize_names: true)
32
+ cmd = req[:cmd]
33
+ key = req[:key]
34
+ ns = req[:namespace]
35
+ val = req[:value]
36
+ ttl = req[:ttl]
37
+
38
+ begin
39
+ case cmd
40
+ when "read"
41
+ result = Mudis.read(key, namespace: ns)
42
+ sock.puts(JSON.dump({ ok: true, value: result }))
43
+
44
+ when "write"
45
+ Mudis.write(key, val, expires_in: ttl, namespace: ns)
46
+ sock.puts(JSON.dump({ ok: true }))
47
+
48
+ when "delete"
49
+ Mudis.delete(key, namespace: ns)
50
+ sock.puts(JSON.dump({ ok: true }))
51
+
52
+ when "exists"
53
+ sock.puts(JSON.dump({ ok: true, value: Mudis.exists?(key, namespace: ns) }))
54
+
55
+ when "fetch"
56
+ result = Mudis.fetch(key, expires_in: ttl, namespace: ns) { req[:fallback] }
57
+ sock.puts(JSON.dump({ ok: true, value: result }))
58
+
59
+ when "metrics"
60
+ sock.puts(JSON.dump({ ok: true, value: Mudis.metrics }))
61
+
62
+ when "reset_metrics"
63
+ Mudis.reset_metrics!
64
+ sock.puts(JSON.dump({ ok: true }))
65
+
66
+ when "reset"
67
+ Mudis.reset!
68
+ sock.puts(JSON.dump({ ok: true }))
69
+
70
+ else
71
+ sock.puts(JSON.dump({ ok: false, error: "unknown command: #{cmd}" }))
72
+ end
73
+ rescue => e
74
+ sock.puts(JSON.dump({ ok: false, error: e.message }))
75
+ ensure
76
+ sock.close
77
+ end
78
+ end
79
+ end
data/sig/mudis.rbs CHANGED
@@ -1,54 +1,56 @@
1
- class Mudis
2
- # Configuration
3
- class << self
4
- attr_accessor serializer : Object
5
- attr_accessor compress : bool
6
- attr_accessor hard_memory_limit : bool
7
- attr_reader max_bytes : Integer
8
- attr_reader max_value_bytes : Integer?
9
- attr_accessor max_ttl: Integer?
10
- attr_accessor default_ttl: Integer?
11
-
12
- def configure: () { (config: MudisConfig) -> void } -> void
13
- def config: () -> MudisConfig
14
- def apply_config!: () -> void
15
- def validate_config!: () -> void
16
-
17
- def buckets: () -> Integer
18
- end
19
-
20
- # Lifecycle
21
- def self.start_expiry_thread: (?interval: Integer) -> void
22
- def self.stop_expiry_thread: () -> void
23
-
24
- # Core operations
25
- def self.write: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
26
- def self.read: (String, ?namespace: String) -> untyped?
27
- def self.update: (String, ?namespace: String) { (untyped) -> untyped } -> void
28
- def self.delete: (String, ?namespace: String) -> void
29
- def self.exists?: (String, ?namespace: String) -> bool
30
-
31
- # DSL & Helpers
32
- def self.fetch: (
33
- String,
34
- ?expires_in: Integer,
35
- ?force: bool,
36
- ?namespace: String
37
- ) { () -> untyped } -> untyped
38
-
39
- def self.clear: (String, ?namespace: String) -> void
40
- def self.replace: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
41
- def self.inspect: (String, ?namespace: String) -> Hash[Symbol, untyped]?
42
-
43
- # Introspection & management
44
- def self.metrics: () -> Hash[Symbol, untyped]
45
- def self.cleanup_expired!: () -> void
46
- def self.all_keys: () -> Array[String]
47
- def self.current_memory_bytes: () -> Integer
48
- def self.max_memory_bytes: () -> Integer
49
- def self.least_touched: (?Integer) -> Array[[String, Integer]]
50
-
51
- # State reset
52
- def self.reset!: () -> void
53
- def self.reset_metrics!: () -> void
54
- end
1
+ class Mudis
2
+ # Configuration
3
+ class << self
4
+ attr_accessor serializer : Object
5
+ attr_accessor compress : bool
6
+ attr_accessor hard_memory_limit : bool
7
+ attr_reader max_bytes : Integer
8
+ attr_reader max_value_bytes : Integer?
9
+ attr_accessor max_ttl: Integer?
10
+ attr_accessor default_ttl: Integer?
11
+
12
+ def configure: () { (config: MudisConfig) -> void } -> void
13
+ def config: () -> MudisConfig
14
+ def apply_config!: () -> void
15
+ def validate_config!: () -> void
16
+
17
+ def buckets: () -> Integer
18
+ end
19
+
20
+ # Lifecycle
21
+ def self.start_expiry_thread: (?interval: Integer) -> void
22
+ def self.stop_expiry_thread: () -> void
23
+
24
+ # Core operations
25
+ def self.write: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
26
+ def self.read: (String, ?namespace: String) -> untyped?
27
+ def self.update: (String, ?namespace: String) { (untyped) -> untyped } -> void
28
+ def self.delete: (String, ?namespace: String) -> void
29
+ def self.exists?: (String, ?namespace: String) -> bool
30
+
31
+ # DSL & Helpers
32
+ def self.fetch: (
33
+ String,
34
+ ?expires_in: Integer,
35
+ ?force: bool,
36
+ ?namespace: String
37
+ ) { () -> untyped } -> untyped
38
+
39
+ def self.clear: (String, ?namespace: String) -> void
40
+ def self.replace: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
41
+ def self.inspect: (String, ?namespace: String) -> Hash[Symbol, untyped]?
42
+ def self.keys: (?namespace: String) -> Array[String]
43
+ def self.clear_namespace: (?namespace: String) -> void
44
+
45
+ # Introspection & management
46
+ def self.metrics: () -> Hash[Symbol, untyped]
47
+ def self.cleanup_expired!: () -> void
48
+ def self.all_keys: () -> Array[String]
49
+ def self.current_memory_bytes: () -> Integer
50
+ def self.max_memory_bytes: () -> Integer
51
+ def self.least_touched: (?Integer) -> Array[[String, Integer]]
52
+
53
+ # State reset
54
+ def self.reset!: () -> void
55
+ def self.reset_metrics!: () -> void
56
+ end
data/sig/mudis_config.rbs CHANGED
@@ -1,10 +1,10 @@
1
- class MudisConfig
2
- attr_accessor serializer: Object
3
- attr_accessor compress: bool
4
- attr_accessor max_value_bytes: Integer?
5
- attr_accessor hard_memory_limit: bool
6
- attr_accessor max_bytes: Integer
7
- attr_accessor max_ttl: Integer?
8
- attr_accessor default_ttl: Integer?
9
- attr_accessor buckets: Integer?
10
- end
1
+ class MudisConfig
2
+ attr_accessor serializer: Object
3
+ attr_accessor compress: bool
4
+ attr_accessor max_value_bytes: Integer?
5
+ attr_accessor hard_memory_limit: bool
6
+ attr_accessor max_bytes: Integer
7
+ attr_accessor max_ttl: Integer?
8
+ attr_accessor default_ttl: Integer?
9
+ attr_accessor buckets: Integer?
10
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Mudis LRU Eviction" do
6
+ before do
7
+ Mudis.reset!
8
+ Mudis.stop_expiry_thread
9
+
10
+ Mudis.instance_variable_set(:@buckets, 1)
11
+ Mudis.instance_variable_set(:@stores, [{}])
12
+ Mudis.instance_variable_set(:@mutexes, [Mutex.new])
13
+ Mudis.instance_variable_set(:@lru_heads, [nil])
14
+ Mudis.instance_variable_set(:@lru_tails, [nil])
15
+ Mudis.instance_variable_set(:@lru_nodes, [{}])
16
+ Mudis.instance_variable_set(:@current_bytes, [0])
17
+ Mudis.hard_memory_limit = false
18
+ Mudis.instance_variable_set(:@threshold_bytes, 60)
19
+ Mudis.max_value_bytes = 100
20
+ end
21
+
22
+ it "evicts old entries when size limit is reached" do
23
+ Mudis.write("a", "a" * 50)
24
+ Mudis.write("b", "b" * 50)
25
+
26
+ expect(Mudis.read("a")).to be_nil
27
+ expect(Mudis.read("b")).not_to be_nil
28
+ end
29
+ end
@@ -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
@@ -0,0 +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