mudis 0.9.0 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 283e3b629449e6324d30dd19450d4d363a5d415ad45ace27569d4d82fb7e8066
4
- data.tar.gz: 5fe63b61e7526eb90c66c9a5f9da721c5156ec40a144398cedb89e11adc7b35e
3
+ metadata.gz: 6ed8c4f85642ad10c8fc1d4213838004b647cd5a8f9da17dc4f9e8d99f45cb8b
4
+ data.tar.gz: 4eed96ebdb4e701b9ba3b3d9b3e79d08b928215e2bd848f618775a48304160f2
5
5
  SHA512:
6
- metadata.gz: cfc3db7ea2a80a03af39738a6700bbd07b525dbd101bf7b3b8f11b1fa711fdc14a9ad248dcdeff5b469e55bca24efd15d08ac5bf3ff91c83b6614c5ddc925521
7
- data.tar.gz: 4923b6e5ccb935914e0c5811be65fbe358304db5f6e8d76097b08f25db939e441b0b72557695981ffdae7e300d36dd4a66c3d2b3d2fd163c1e3735dbddbd61e6
6
+ metadata.gz: 98fe4110f82ba005e3f5b9d93e751341b6568d15e6052ee14544508ac3258912542d5e9a25a0c2b15c69326ee23e47885c6739bb93d946ae1443df6e740cc8e6
7
+ data.tar.gz: e67673464ae604cd40e3c92d032757dd383a5f28b661fa5f630f9329d55c6f2b06096440495b91e9fcf91c55cc3b5943f7956a59b47104c9c8f4ce77fbb33bff
data/README.md CHANGED
@@ -29,8 +29,8 @@ Mudis also works naturally in Hanami because it’s a pure Ruby in-memory cache.
29
29
  - [Installation](#installation)
30
30
  - [Configuration (Ruby/Rails)](#configuration-rubyrails)
31
31
  - [Configuration (Hanami)](#configuration-hanami)
32
- - [Start and Stop Exipry Thread](#start-and-stop-exipry-thread)
33
- - [Starting Exipry Thread](#starting-exipry-thread)
32
+ - [Start and Stop Expiry Thread](#start-and-stop-expiry-thread)
33
+ - [Starting Expiry Thread](#starting-expiry-thread)
34
34
  - [Graceful Shutdown](#graceful-shutdown)
35
35
  - [Basic Usage](#basic-usage)
36
36
  - [Developer Utilities](#developer-utilities)
@@ -96,15 +96,57 @@ There are plenty out there, in various states of maintenance and in many shapes
96
96
 
97
97
  #### Internal Structure and Behaviour
98
98
 
99
- ![mudis_flow](design/mudis_obj.png "Mudis Internals")
99
+ ```mermaid
100
+ flowchart TD
101
+ A[Mudis] --> C[Config]
102
+ A --> B[Shards/Buckets]
103
+ B --> S[Store Hash]
104
+ B --> L[LRU List]
105
+ B --> M[Mutex]
106
+ A --> E[Expiry Thread]
107
+ A --> P[Persistence]
108
+ A --> R[Metrics]
109
+ A --> N[Namespace]
110
+ ```
100
111
 
101
112
  #### Write - Read - Eviction
102
113
 
103
- ![mudis_flow](design/mudis_flow.png "Write - Read - Eviction")
114
+ ```mermaid
115
+ flowchart TD
116
+ W[Write] --> S[Serialize]
117
+ S --> Z{Compress?}
118
+ Z -- Yes --> ZL[Zlib Deflate]
119
+ Z -- No --> SZ[Raw]
120
+ ZL --> G[Size/Memory Guardrails]
121
+ SZ --> G
122
+ G --> T[Set TTL]
123
+ T --> I[Insert in Bucket]
124
+ I --> L[LRU Insert]
125
+ L --> M[Update Bytes]
126
+ R[Read] --> LK[Lookup]
127
+ LK --> X{Expired?}
128
+ X -- Yes --> EV[Evict]
129
+ X -- No --> D[Deserialize/Inflate]
130
+ D --> PR[Promote LRU]
131
+ PR --> OUT[Return Value]
132
+ E[Evict] --> LRU[LRU Tail]
133
+ ```
104
134
 
105
135
  #### Cache Key Lifecycle
106
136
 
107
- ![mudis_lru](design/mudis_lru.png "Mudis Cache Key Lifecycle")
137
+ ```mermaid
138
+ sequenceDiagram
139
+ participant Client
140
+ participant Mudis
141
+ participant LRU as LRU List
142
+ Client->>Mudis: write(key, value)
143
+ Mudis->>LRU: insert(key) at head
144
+ Client->>Mudis: read(key)
145
+ Mudis->>LRU: promote(key) to head
146
+ Note over Mudis: if over threshold, evict tail
147
+ Mudis->>LRU: evict(tail)
148
+ Mudis-->>Client: value or nil
149
+ ```
108
150
 
109
151
  ---
110
152
 
@@ -245,11 +287,11 @@ end
245
287
 
246
288
  ---
247
289
 
248
- ## Start and Stop Exipry Thread
290
+ ## Start and Stop Expiry Thread
249
291
 
250
292
  The expiry thread is not triggered automatically when your application starts. You must add the start and stop in the respective process hooks.
251
293
 
252
- ### Starting Exipry Thread
294
+ ### Starting Expiry Thread
253
295
 
254
296
  To enable background expiration and removal, you must start the expiry thread at start up after configuration.
255
297
 
@@ -611,7 +653,16 @@ Mudis.load_snapshot!
611
653
 
612
654
  #### Example Flow
613
655
 
614
- ![mudis_persistence](design/mudis_persistence.png "Mudis Persistence Strategy")
656
+ ```mermaid
657
+ flowchart LR
658
+ A[App Boot] --> B[Configure Mudis]
659
+ B --> C[Load Snapshot]
660
+ C --> D[Cache Warm]
661
+ D --> E[Normal Operations]
662
+ E --> F[Exit Hook]
663
+ F --> G[Save Snapshot]
664
+ G --> H[Snapshot File]
665
+ ```
615
666
 
616
667
  1. On startup, `Mudis.load_snapshot!` repopulates the cache.
617
668
  2. Your app uses the cache as normal (`write`, `read`, `fetch`, etc.).
@@ -647,7 +698,18 @@ This design allows multiple workers to share the same cache without duplicating
647
698
  | **Full Feature Support** | All Mudis features (TTL, compression, metrics, etc.) work transparently. |
648
699
  | **Safe & Local** | Communication is limited to the host system’s UNIX socket, ensuring isolation and speed. |
649
700
 
650
- ![mudis_ipc](design/mudis_ipc.png "Mudis IPC")
701
+ ```mermaid
702
+ flowchart LR
703
+ M[Master Process] --> S[MudisServer]
704
+ W1[Worker 1] --> C1[MudisClient]
705
+ W2[Worker 2] --> C2[MudisClient]
706
+ W3[Worker 3] --> C3[MudisClient]
707
+ C1 --> U[Unix Socket/TCP]
708
+ C2 --> U
709
+ C3 --> U
710
+ U --> S
711
+ S --> Cache[Mudis Cache]
712
+ ```
651
713
 
652
714
  ### Setup (Puma)
653
715
 
@@ -799,7 +861,7 @@ _10000 iterations of 512KB, JSON, compression ON_
799
861
  ## Known Limitations
800
862
 
801
863
  - Data is **non-persistent**; only soft-persistence is optionally provided.
802
- - No SQL or equivallent query interface for cached data. Data is per Key retrieval only.
864
+ - No SQL or equivallent query interface for cached data. Data is per Key retrieval only (see [Mudis-QL](https://github.com/kiebor81/mudis-ql)).
803
865
  - Compression introduces CPU overhead.
804
866
 
805
867
  ---
@@ -934,7 +996,7 @@ Mudis is not intended to be a general-purpose, distributed caching platform. You
934
996
 
935
997
  - [x] Review Mudis for improved readability and reduce complexity in top-level functions
936
998
  - [x] Enhanced guards
937
- - [ ] Refactor main classes for enhanced readability
999
+ - [x] Refactor main classes for enhanced readability
938
1000
  - [ ] Review for functionality gaps and enhance as needed
939
1001
 
940
1002
  ---
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+
3
5
  class Mudis
4
6
  # Persistence module handles snapshot save/load operations for warm boot support
5
7
  module Persistence
@@ -80,7 +82,7 @@ class Mudis
80
82
  def safe_write_snapshot(data) # rubocop:disable Metrics/MethodLength
81
83
  path = @persistence_path
82
84
  dir = File.dirname(path)
83
- Dir.mkdir(dir) unless Dir.exist?(dir)
85
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
84
86
 
85
87
  payload =
86
88
  if (@persistence_format || :marshal).to_sym == :json
data/lib/mudis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- MUDIS_VERSION = "0.9.0"
3
+ MUDIS_VERSION = "0.9.1"
data/lib/mudis.rb CHANGED
@@ -155,8 +155,7 @@ class Mudis # rubocop:disable Metrics/ClassLength
155
155
 
156
156
  # Checks if a key exists and is not expired
157
157
  def exists?(key, namespace: nil)
158
- key = namespaced_key(key, namespace)
159
- !!read(key)
158
+ !!read(key, namespace: namespace)
160
159
  end
161
160
 
162
161
  # Reads and returns the value for a key, updating LRU and metrics
@@ -174,7 +173,10 @@ class Mudis # rubocop:disable Metrics/ClassLength
174
173
  raw_entry = nil
175
174
  end
176
175
 
177
- store[key][:touches] = (store[key][:touches] || 0) + 1 if store[key]
176
+ if store[key]
177
+ store[key][:touches] = (store[key][:touches] || 0) + 1
178
+ promote_lru(idx, key)
179
+ end
178
180
 
179
181
  metric(:hits) if raw_entry
180
182
  metric(:misses) unless raw_entry
@@ -183,7 +185,6 @@ class Mudis # rubocop:disable Metrics/ClassLength
183
185
  return nil unless raw_entry
184
186
 
185
187
  value = decompress_and_deserialize(raw_entry[:value])
186
- promote_lru(idx, key)
187
188
  value
188
189
  end
189
190
 
@@ -244,10 +245,27 @@ class Mudis # rubocop:disable Metrics/ClassLength
244
245
  new_value = yield(value)
245
246
  new_raw = serializer.dump(new_value)
246
247
  new_raw = Zlib::Deflate.deflate(new_raw) if compress
248
+ return if max_value_bytes && new_raw.bytesize > max_value_bytes
247
249
 
248
250
  mutex.synchronize do
249
- old_size = key.bytesize + raw_entry[:value].bytesize
251
+ current_entry = store[key]
252
+ return nil unless current_entry
253
+
254
+ old_size = key.bytesize + current_entry[:value].bytesize
250
255
  new_size = key.bytesize + new_raw.bytesize
256
+
257
+ if hard_memory_limit && (current_memory_bytes - old_size + new_size) > max_memory_bytes
258
+ metric(:rejected)
259
+ return
260
+ end
261
+
262
+ while (@current_bytes[idx] - old_size + new_size) > (@threshold_bytes / buckets) && @lru_tails[idx]
263
+ break if @lru_tails[idx].key == key
264
+
265
+ evict_key(idx, @lru_tails[idx].key)
266
+ metric(:evictions)
267
+ end
268
+
251
269
  store[key][:value] = new_raw
252
270
  @current_bytes[idx] += (new_size - old_size)
253
271
  promote_lru(idx, key)
@@ -270,14 +288,13 @@ class Mudis # rubocop:disable Metrics/ClassLength
270
288
  # Optionally accepts an expiration time
271
289
  # If force is true, it always fetches and writes the value
272
290
  def fetch(key, expires_in: nil, force: false, namespace: nil)
273
- key = namespaced_key(key, namespace)
274
291
  unless force
275
- cached = read(key)
292
+ cached = read(key, namespace: namespace)
276
293
  return cached if cached
277
294
  end
278
295
 
279
296
  value = yield
280
- write(key, value, expires_in: expires_in)
297
+ write(key, value, expires_in: expires_in, namespace: namespace)
281
298
  value
282
299
  end
283
300
 
data/sig/mudis.rbs CHANGED
@@ -45,8 +45,9 @@ class Mudis
45
45
  def self.clear: (String, ?namespace: String) -> void
46
46
  def self.replace: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
47
47
  def self.inspect: (String, ?namespace: String) -> Hash[Symbol, untyped]?
48
- def self.keys: (?namespace: String) -> Array[String]
49
- def self.clear_namespace: (?namespace: String) -> void
48
+ def self.keys: (namespace: String) -> Array[String]
49
+ def self.clear_namespace: (namespace: String) -> void
50
+ def self.with_namespace: (namespace: String) { () -> untyped } -> untyped
50
51
 
51
52
  # Introspection & management
52
53
  def self.metrics: () -> Hash[Symbol, untyped]
@@ -59,4 +60,9 @@ class Mudis
59
60
  # State reset
60
61
  def self.reset!: () -> void
61
62
  def self.reset_metrics!: () -> void
63
+
64
+ # Persistence
65
+ def self.save_snapshot!: () -> void
66
+ def self.load_snapshot!: () -> void
67
+ def self.install_persistence_hook!: () -> void
62
68
  end
data/sig/mudis_client.rbs CHANGED
@@ -5,7 +5,7 @@ class MudisClient
5
5
 
6
6
  def open_connection: () -> (TCPSocket | UNIXSocket)
7
7
 
8
- def request: (payload: { cmd: String, key?: String, value?: untyped, ttl?: Integer?, namespace?: String? }) -> untyped
8
+ def request: (payload: Hash[Symbol, untyped]) -> untyped
9
9
 
10
10
  def read: (key: String, namespace?: String?) -> untyped
11
11
 
@@ -17,9 +17,9 @@ class MudisClient
17
17
 
18
18
  def fetch: (key: String, expires_in?: Integer?, namespace?: String?, &block: { () -> untyped }) -> untyped
19
19
 
20
- def metrics: () -> { reads: Integer, writes: Integer, deletes: Integer, exists: Integer }
20
+ def metrics: () -> Hash[Symbol, untyped]
21
21
 
22
22
  def reset_metrics!: () -> void
23
23
 
24
24
  def reset!: () -> void
25
- end
25
+ end
data/sig/mudis_config.rbs CHANGED
@@ -7,4 +7,8 @@ class MudisConfig
7
7
  attr_accessor max_ttl: Integer?
8
8
  attr_accessor default_ttl: Integer?
9
9
  attr_accessor buckets: Integer?
10
+ attr_accessor persistence_enabled: bool
11
+ attr_accessor persistence_path: String
12
+ attr_accessor persistence_format: Symbol
13
+ attr_accessor persistence_safe_write: bool
10
14
  end
data/sig/mudis_expiry.rbs CHANGED
@@ -8,6 +8,6 @@ class Mudis
8
8
 
9
9
  private
10
10
 
11
- def effective_ttl: (?expires_in: Integer?) -> Integer?
11
+ def effective_ttl: (Integer?) -> Integer?
12
12
  end
13
13
  end
data/sig/mudis_lru.rbs CHANGED
@@ -12,7 +12,7 @@ class Mudis
12
12
 
13
13
  def evict_key: (Integer, String) -> void
14
14
 
15
- def insert_lru: (Integer, LRUNode) -> void
15
+ def insert_lru: (Integer, String) -> void
16
16
 
17
17
  def promote_lru: (Integer, String) -> void
18
18
 
@@ -8,12 +8,12 @@ class Mudis
8
8
 
9
9
  private
10
10
 
11
- def snapshot_dump: () -> Hash[String, untyped]
11
+ def snapshot_dump: () -> Array[{ key: String, value: untyped, expires_in: Integer? }]
12
12
 
13
- def snapshot_restore: (Hash[String, untyped]) -> void
13
+ def snapshot_restore: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
14
14
 
15
- def safe_write_snapshot: (Hash[String, untyped]) -> void
15
+ def safe_write_snapshot: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
16
16
 
17
- def read_snapshot: () -> Hash[String, untyped]
17
+ def read_snapshot: () -> Array[{ key: String, value: untyped, expires_in: Integer? }]
18
18
  end
19
19
  end
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)) -> void
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[String, untyped]) -> Hash[Symbol, untyped]
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
@@ -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
@@ -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).and_yield(mock_socket)
21
+ allow(UNIXSocket).to receive(:open).and_return(mock_socket)
22
22
  end
23
23
  allow(mock_socket).to receive(:close)
24
24
  end
@@ -6,6 +6,13 @@ require "json"
6
6
  require_relative "spec_helper"
7
7
 
8
8
  RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
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
15
+
9
16
  let(:socket_path) { MudisIPCConfig::SOCKET_PATH }
10
17
 
11
18
  before(:all) do
@@ -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")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mudis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kiebor81
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-02 00:00:00.000000000 Z
11
+ date: 2026-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control