mudis 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1586fbcdac3ae237f88954090971beca8be02f3892f36c25363403d2ac80f34f
4
- data.tar.gz: b12179ea11d6f51a3049d3eaa4428648fc088ecb6ade607bbe832227ceb64678
3
+ metadata.gz: 05a589eca123a045badb19d25119bbc9e51c48e6afec22c490615f656c73f65f
4
+ data.tar.gz: 478553d02b5c62f08d9194355136b0e284c257d47184ba74d79c2d9a26553460
5
5
  SHA512:
6
- metadata.gz: bff8cda9a3f627f6c94b3f40b737365603ab51fd6edd8e845f058bed517ccfa86a4ad61465fb4b807540525b779586cfbbe77eccd8a16ab59afc69aa2f7cd43a
7
- data.tar.gz: 544c25fee1a7346e8c0725ee5e83d5764f3028d76a261666ecdc0d3291c2bccf5a52cf07713d480750cb149e12c52201a2da2a6de0faa8f191ea45b7ba81d47b
6
+ metadata.gz: bbae5238c153bfc4d29e0efd795c01e6c473fd17a69b28e145dc4fe8ada0faa286fce117cb53a0eaba0df8c67560c08814eb15305fe5bf01e405751b56a6f218
7
+ data.tar.gz: 1807351fac67cc35c66dc898264807b9542871c2e708baaa2e3fc91bd28b9cda1913257a9083e3115da341b836bd7ce3ca15ef23aae8cf998da4fbc65e6f0e6d
data/README.md CHANGED
@@ -1,9 +1,13 @@
1
1
  ![mudis_signet](design/mudis.png "Mudis")
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/mudis.svg)](https://rubygems.org/gems/mudis)
4
+
3
5
  **Mudis** is a fast, thread-safe, in-memory, sharded LRU (Least Recently Used) cache for Ruby applications. Inspired by Redis, it provides value serialization, optional compression, per-key expiry, and metric tracking in a lightweight, dependency-free package that lives inside your Ruby process.
4
6
 
5
7
  It’s ideal for scenarios where performance and process-local caching are critical, and where a full Redis setup is overkill or otherwise not possible.
6
8
 
9
+ Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated rails app to provide a Mudis server.
10
+
7
11
  ---
8
12
 
9
13
  ## Design
@@ -91,7 +95,7 @@ Mudis.delete('user:123')
91
95
 
92
96
  ## Rails Service Integration
93
97
 
94
- For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class:
98
+ For simplified or transient use in a controller, you can wrap your cache logic in a reusable thin class (TODO: add more useful abstraction once DSL and namespacing is introduced):
95
99
 
96
100
  ```ruby
97
101
  class MudisService
@@ -126,7 +130,7 @@ end
126
130
  Use it like:
127
131
 
128
132
  ```ruby
129
- cache = MudisCacheService.new("user:#{current_user.id}")
133
+ cache = MudisService.new("user:#{current_user.id}")
130
134
  cache.write({ preferences: "dark" }, expires_in: 3600)
131
135
  cache.read # => { "preferences" => "dark" }
132
136
  ```
@@ -188,9 +192,8 @@ at_exit { Mudis.stop_expiry_thread }
188
192
 
189
193
  ## Known Limitations
190
194
 
191
- - Data is **process-local** and **non-persistent**.
192
- - Not suitable for cross-process or cross-language use.
193
- - Keys are globally scoped (no namespacing by default).
195
+ - Data is **non-persistent**.
196
+ - Keys are globally scoped (no namespacing by default). Namespaving must be handled by the caller/consumer using scoped keys.
194
197
  - Compression introduces CPU overhead.
195
198
 
196
199
  ---
@@ -200,7 +203,6 @@ at_exit { Mudis.stop_expiry_thread }
200
203
  - [ ] Namespaced cache keys
201
204
  - [ ] Stats per bucket
202
205
  - [ ] Optional max memory cap per bucket
203
- - [ ] Built-in fetch/read-or-write DSL
204
206
 
205
207
  ---
206
208
 
data/lib/mudis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- MUDIS_VERSION = "0.1.0"
3
+ MUDIS_VERSION = "0.2.0"
data/lib/mudis.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # lib/mudis.rb
2
- require 'json'
3
- require 'thread'
4
- require 'zlib'
2
+
3
+ require "json"
4
+ require "thread" # rubocop:disable Lint/RedundantRequireStatement
5
+ require "zlib"
5
6
 
6
7
  # Mudis is a thread-safe, in-memory, sharded, LRU cache with optional compression and expiry.
7
8
  # It is designed for high concurrency and performance within a Ruby application.
8
- class Mudis
9
+ class Mudis # rubocop:disable Metrics/ClassLength
9
10
  # --- Global Configuration and State ---
10
11
 
11
12
  @serializer = JSON # Default serializer (can be changed to Marshal or Oj)
@@ -27,6 +28,7 @@ class Mudis
27
28
  # Node structure for the LRU doubly-linked list
28
29
  class LRUNode
29
30
  attr_accessor :key, :prev, :next
31
+
30
32
  def initialize(key)
31
33
  @key = key
32
34
  @prev = nil
@@ -36,7 +38,7 @@ class Mudis
36
38
 
37
39
  # Number of cache buckets (shards). Default: 32
38
40
  def self.buckets
39
- @buckets ||= (ENV['MUDIS_BUCKETS']&.to_i || 32)
41
+ @buckets ||= (ENV["MUDIS_BUCKETS"]&.to_i || 32) # rubocop:disable Style/RedundantParentheses
40
42
  end
41
43
 
42
44
  # --- Internal Structures ---
@@ -48,8 +50,8 @@ class Mudis
48
50
  @lru_nodes = Array.new(buckets) { {} } # Map of key => LRU node
49
51
  @current_bytes = Array.new(buckets, 0) # Memory usage per bucket
50
52
  @max_bytes = 1_073_741_824 # 1 GB global max cache size
51
- @threshold_bytes = (@max_bytes * 0.9).to_i # Eviction threshold at 90%
52
- @expiry_thread = nil # Background thread for expiry cleanup
53
+ @threshold_bytes = (@max_bytes * 0.9).to_i # Eviction threshold at 90%
54
+ @expiry_thread = nil # Background thread for expiry cleanup
53
55
 
54
56
  class << self
55
57
  # Starts a thread that periodically removes expired entries
@@ -60,6 +62,7 @@ class Mudis
60
62
  @expiry_thread = Thread.new do
61
63
  loop do
62
64
  break if @stop_expiry
65
+
63
66
  sleep interval
64
67
  cleanup_expired!
65
68
  end
@@ -84,7 +87,7 @@ class Mudis
84
87
  end
85
88
 
86
89
  # Reads and returns the value for a key, updating LRU and metrics
87
- def read(key)
90
+ def read(key) # rubocop:disable Metrics/MethodLength
88
91
  raw_entry = nil
89
92
  idx = bucket_index(key)
90
93
  mutex = @mutexes[idx]
@@ -108,7 +111,7 @@ class Mudis
108
111
  end
109
112
 
110
113
  # Writes a value to the cache with optional expiry and LRU tracking
111
- def write(key, value, expires_in: nil)
114
+ def write(key, value, expires_in: nil) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize
112
115
  raw = serializer.dump(value)
113
116
  raw = Zlib::Deflate.deflate(raw) if compress
114
117
  size = key.bytesize + raw.bytesize
@@ -138,7 +141,7 @@ class Mudis
138
141
  end
139
142
 
140
143
  # Atomically updates the value for a key using a block
141
- def update(key)
144
+ def update(key) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
142
145
  idx = bucket_index(key)
143
146
  mutex = @mutexes[idx]
144
147
  store = @stores[idx]
@@ -173,6 +176,59 @@ class Mudis
173
176
  end
174
177
  end
175
178
 
179
+ # Fetches a value for a key, writing it if not present or expired
180
+ # The block is executed to generate the value if it doesn't exist
181
+ # Optionally accepts an expiration time
182
+ # If force is true, it always fetches and writes the value
183
+ def fetch(key, expires_in: nil, force: false)
184
+ unless force
185
+ cached = read(key)
186
+ return cached if cached
187
+ end
188
+
189
+ value = yield
190
+ write(key, value, expires_in: expires_in)
191
+ value
192
+ end
193
+
194
+ # Clears a specific key from the cache, a semantic synonym for delete
195
+ # This method is provided for clarity in usage
196
+ # It behaves the same as delete
197
+ def clear(key)
198
+ delete(key)
199
+ end
200
+
201
+ # Replaces the value for a key if it exists, otherwise does nothing
202
+ # This is useful for updating values without needing to check existence first
203
+ # It will write the new value and update the expiration if provided
204
+ # If the key does not exist, it will not create a new entry
205
+ def replace(key, value, expires_in: nil)
206
+ return unless exists?(key)
207
+
208
+ write(key, value, expires_in: expires_in)
209
+ end
210
+
211
+ # Inspects a key and returns all meta data for it
212
+ def inspect(key) # rubocop:disable Metrics/MethodLength
213
+ idx = bucket_index(key)
214
+ store = @stores[idx]
215
+ mutex = @mutexes[idx]
216
+
217
+ mutex.synchronize do
218
+ entry = store[key]
219
+ return nil unless entry
220
+
221
+ {
222
+ key: key,
223
+ bucket: idx,
224
+ expires_at: entry[:expires_at],
225
+ created_at: entry[:created_at],
226
+ size_bytes: key.bytesize + entry[:value].bytesize,
227
+ compressed: compress
228
+ }
229
+ end
230
+ end
231
+
176
232
  # Removes expired keys across all buckets
177
233
  def cleanup_expired!
178
234
  now = Time.now
@@ -180,10 +236,8 @@ class Mudis
180
236
  mutex = @mutexes[idx]
181
237
  store = @stores[idx]
182
238
  mutex.synchronize do
183
- store.keys.each do |key|
184
- if store[key][:expires_at] && now > store[key][:expires_at]
185
- evict_key(idx, key)
186
- end
239
+ store.keys.each do |key| # rubocop:disable Style/HashEachMethods
240
+ evict_key(idx, key) if store[key][:expires_at] && now > store[key][:expires_at]
187
241
  end
188
242
  end
189
243
  end
@@ -270,4 +324,3 @@ class Mudis
270
324
  end
271
325
  end
272
326
  end
273
-
data/sig/mudis.rbs CHANGED
@@ -19,6 +19,17 @@ class Mudis
19
19
  def self.delete: (String) -> void
20
20
  def self.exists?: (String) -> bool
21
21
 
22
+ # DSL & Helpers
23
+ def self.fetch: (
24
+ String,
25
+ ?expires_in: Integer,
26
+ ?force: bool
27
+ ) { () -> untyped } -> untyped
28
+
29
+ def self.clear: (String) -> void
30
+ def self.replace: (String, untyped, ?expires_in: Integer) -> void
31
+ def self.inspect: (String) -> Hash[Symbol, untyped]?
32
+
22
33
  # Introspection & management
23
34
  def self.metrics: () -> Hash[Symbol, Integer]
24
35
  def self.cleanup_expired!: () -> void
data/spec/mudis_spec.rb CHANGED
@@ -1,4 +1,5 @@
1
- # spec/mudis_spec.rb
1
+ # frozen_string_literal: true
2
+
2
3
  require_relative "spec_helper"
3
4
 
4
5
  RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
@@ -56,6 +57,63 @@ RSpec.describe Mudis do # rubocop:disable Metrics/BlockLength
56
57
  end
57
58
  end
58
59
 
60
+ describe ".fetch" do
61
+ it "returns cached value if exists" do
62
+ Mudis.write("k", 123)
63
+ result = Mudis.fetch("k", expires_in: 60) { 999 } # fix: use keyword arg
64
+ expect(result).to eq(123)
65
+ end
66
+
67
+ it "writes and returns block result if missing" do
68
+ Mudis.delete("k")
69
+ result = Mudis.fetch("k", expires_in: 60) { 999 } # fix
70
+ expect(result).to eq(999)
71
+ expect(Mudis.read("k")).to eq(999)
72
+ end
73
+
74
+ it "forces overwrite if force: true" do
75
+ Mudis.write("k", 100)
76
+ result = Mudis.fetch("k", force: true) { 200 } # fix
77
+ expect(result).to eq(200)
78
+ end
79
+ end
80
+
81
+ describe ".clear" do
82
+ it "removes a key from the cache" do
83
+ Mudis.write("to_clear", 123)
84
+ expect(Mudis.read("to_clear")).to eq(123)
85
+ Mudis.clear("to_clear")
86
+ expect(Mudis.read("to_clear")).to be_nil
87
+ end
88
+ end
89
+
90
+ describe ".replace" do
91
+ it "replaces value only if key exists" do
92
+ Mudis.write("to_replace", 100)
93
+ Mudis.replace("to_replace", 200)
94
+ expect(Mudis.read("to_replace")).to eq(200)
95
+
96
+ Mudis.delete("to_replace")
97
+ Mudis.replace("to_replace", 300)
98
+ expect(Mudis.read("to_replace")).to be_nil
99
+ end
100
+ end
101
+
102
+ describe ".inspect" do
103
+ it "returns metadata for a cached key" do
104
+ Mudis.write("key1", "abc", expires_in: 60)
105
+ meta = Mudis.inspect("key1")
106
+
107
+ expect(meta).to include(:key, :bucket, :expires_at, :created_at, :size_bytes, :compressed)
108
+ expect(meta[:key]).to eq("key1")
109
+ expect(meta[:compressed]).to eq(false)
110
+ end
111
+
112
+ it "returns nil for missing key" do
113
+ expect(Mudis.inspect("unknown")).to be_nil
114
+ end
115
+ end
116
+
59
117
  describe "expiry handling" do
60
118
  it "expires values after specified time" do
61
119
  Mudis.write("short_lived", "gone soon", expires_in: 1)
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kiebor81
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-15 00:00:00.000000000 Z
11
+ date: 2025-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec