mudis 0.6.0 → 0.7.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 +4 -4
- data/README.md +276 -5
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis_client.rb +65 -0
- data/lib/mudis_proxy.rb +33 -0
- data/lib/mudis_server.rb +81 -0
- data/sig/mudis_client.rbs +23 -0
- data/sig/mudis_server.rbs +7 -0
- data/spec/mudis_client_spec.rb +137 -0
- data/spec/mudis_server_spec.rb +90 -0
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8d2af60ebd6e5423bd7864ccaa02e597d90fa047bdc2f4b766c0772c16d9dd8
|
|
4
|
+
data.tar.gz: 23a3524cca4ee8b520e4984a24950962aaa3ee52543f95634016f8a7cdc45488
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abc5c28bd8c7bc0b2da2e3f9ca42cbcf458e4d2c3009654c24ee5a17c4b68b447f4f3866916bf8d8abd4b06d3790faca5ed5bc98190008277a1f4489e944d4d1
|
|
7
|
+
data.tar.gz: eaaa49a62a501f12fa5f1ebf46d2acc0c1d42fdaac0538aa015088fc4a9ae05ed18be298bffb4ffe7a7a3c5fae8369cd22b723bd16191cba1ce0cd676a751624
|
data/README.md
CHANGED
|
@@ -1,12 +1,53 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/mudis)
|
|
4
|
+
[](LICENSE)
|
|
4
5
|
|
|
5
6
|
**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.
|
|
6
7
|
|
|
7
8
|
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/desirable.
|
|
8
9
|
|
|
9
|
-
Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails app to provide a [Mudis
|
|
10
|
+
Alternatively, Mudis can be upscaled with higher sharding and resources in a dedicated Rails app to provide a [Mudis Web Cache Server](#create-a-mudis-web-cache-server).
|
|
11
|
+
|
|
12
|
+
Mudis also works naturally in Hanami because it’s a pure Ruby in-memory cache. Whether used as a singleton within a process or via IPC in cluster mode, it preserves Hanami’s lightweight and modular architecture.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Why Another Caching Gem?](#why-another-caching-gem)
|
|
19
|
+
- [Similar Gems](#similar-gems)
|
|
20
|
+
- [Feature / Function Comparison](#feature--function-comparison)
|
|
21
|
+
- [Design](#design)
|
|
22
|
+
- [Internal Structure and Behaviour](#internal-structure-and-behaviour)
|
|
23
|
+
- [Write - Read - Eviction](#write---read---eviction)
|
|
24
|
+
- [Cache Key Lifecycle](#cache-key-lifecycle)
|
|
25
|
+
- [Features](#features)
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Configuration (Rails)](#configuration-rails)
|
|
28
|
+
- [Configuration (Hanami)](#configuration-hanami)
|
|
29
|
+
- [Basic Usage](#basic-usage)
|
|
30
|
+
- [Developer Utilities](#developer-utilities)
|
|
31
|
+
- [`Mudis.reset!`](#mudisreset)
|
|
32
|
+
- [`Mudis.reset_metrics!`](#mudisreset_metrics)
|
|
33
|
+
- [`Mudis.least_touched`](#mudisleast_touched)
|
|
34
|
+
- [`Mudis.keys(namespace:)`](#mudiskeysnamespace)
|
|
35
|
+
- [`Mudis.clear_namespace(namespace:)`](#mudisclear_namespacenamespace)
|
|
36
|
+
- [Rails Service Integration](#rails-service-integration)
|
|
37
|
+
- [Metrics](#metrics)
|
|
38
|
+
- [Advanced Configuration](#advanced-configuration)
|
|
39
|
+
- [Benchmarks](#benchmarks)
|
|
40
|
+
- [Graceful Shutdown](#graceful-shutdown)
|
|
41
|
+
- [Known Limitations](#known-limitations)
|
|
42
|
+
- [Inter-Process Caching (IPC Mode)](#inter-process-caching-ipc-mode)
|
|
43
|
+
- [Overview](#overview)
|
|
44
|
+
- [Setup (Puma)](#setup-puma)
|
|
45
|
+
- [Create a Mudis Web Cache Server](#create-a-mudis-web-cache-server)
|
|
46
|
+
- [Minimal Setup](#minimal-setup)
|
|
47
|
+
- [Project Philosophy](#project-philosophy)
|
|
48
|
+
- [Roadmap](#roadmap)
|
|
49
|
+
|
|
50
|
+
---
|
|
10
51
|
|
|
11
52
|
### Why another Caching Gem?
|
|
12
53
|
|
|
@@ -41,6 +82,7 @@ There are plenty out there, in various states of maintenance and in many shapes
|
|
|
41
82
|
| **Concurrency model** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
|
42
83
|
| **Maintenance level** | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
|
43
84
|
| **Suitable for APIs or microservices** | ✅ | ⚠️ Limited | ✅ | ⚠️ Small apps | ⚠️ Small apps | ❌ |
|
|
85
|
+
| **Inter-process Caching** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
|
44
86
|
|
|
45
87
|
---
|
|
46
88
|
|
|
@@ -68,6 +110,7 @@ There are plenty out there, in various states of maintenance and in many shapes
|
|
|
68
110
|
- **Expiry Support**: Optional TTL per key with background cleanup thread.
|
|
69
111
|
- **Compression**: Optional Zlib compression for large values.
|
|
70
112
|
- **Metrics**: Tracks hits, misses, and evictions.
|
|
113
|
+
- **IPC Mode**: Shared cross-process caching for multi-process aplications.
|
|
71
114
|
|
|
72
115
|
---
|
|
73
116
|
|
|
@@ -124,6 +167,79 @@ Mudis.start_expiry_thread(interval: 60) # Cleanup every 60s
|
|
|
124
167
|
|
|
125
168
|
---
|
|
126
169
|
|
|
170
|
+
## Configuration (Hanami)
|
|
171
|
+
|
|
172
|
+
Mudis integrates seamlessly with [Hanami](https://hanamirb.org) applications. It provides the same configuration flexibility and thread-safe caching capabilities as in Rails, with minimal setup differences.
|
|
173
|
+
|
|
174
|
+
Create a boot file:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# config/boot/mudis.rb
|
|
178
|
+
require "mudis"
|
|
179
|
+
|
|
180
|
+
Mudis.configure do |c|
|
|
181
|
+
c.serializer = JSON
|
|
182
|
+
c.compress = true
|
|
183
|
+
c.max_value_bytes = 2_000_000
|
|
184
|
+
c.hard_memory_limit = true
|
|
185
|
+
c.max_bytes = 1_073_741_824
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
Mudis.start_expiry_thread(interval: 60)
|
|
189
|
+
|
|
190
|
+
at_exit { Mudis.stop_expiry_thread }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Then require it from `config/app.rb`:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# config/app.rb
|
|
197
|
+
require_relative "./boot/mudis"
|
|
198
|
+
|
|
199
|
+
module MyApp
|
|
200
|
+
class App < Hanami::App
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This ensures Mudis is initialized and available globally throughout your Hanami application in the same as it would in Rails.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### Using Mudis with Hanami’s Dependency Container
|
|
210
|
+
|
|
211
|
+
You can register Mudis as a dependency in the Hanami container to access it via dependency injection:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# config/container.rb
|
|
215
|
+
require "mudis"
|
|
216
|
+
|
|
217
|
+
MyApp::Container.register(:cache, Mudis)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Then use it inside your actions, repositories, or services:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
# apps/main/actions/users/show.rb
|
|
224
|
+
module Main
|
|
225
|
+
module Actions
|
|
226
|
+
module Users
|
|
227
|
+
class Show < Main::Action
|
|
228
|
+
include Deps[cache: "cache"]
|
|
229
|
+
|
|
230
|
+
def handle(req, res)
|
|
231
|
+
res[:user] = cache.fetch("user:#{req.params[:id]}", expires_in: 60) do
|
|
232
|
+
UserRepo.new.find(req.params[:id])
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
127
243
|
## Basic Usage
|
|
128
244
|
|
|
129
245
|
```ruby
|
|
@@ -299,6 +415,8 @@ cache.fetch(expires_in: 60) { expensive_query }
|
|
|
299
415
|
cache.inspect_meta # => { key: "users:user:42:profile", ... }
|
|
300
416
|
```
|
|
301
417
|
|
|
418
|
+
*This pattern can also be applied in Hanami services using the registered Mudis dependency*
|
|
419
|
+
|
|
302
420
|
---
|
|
303
421
|
|
|
304
422
|
## Metrics
|
|
@@ -326,7 +444,9 @@ Mudis.metrics
|
|
|
326
444
|
|
|
327
445
|
```
|
|
328
446
|
|
|
329
|
-
Optionally,
|
|
447
|
+
Optionally, expose Mudis metrics from a controller or action for remote analysis and monitoring.
|
|
448
|
+
|
|
449
|
+
**Rails:**
|
|
330
450
|
|
|
331
451
|
```ruby
|
|
332
452
|
class MudisController < ApplicationController
|
|
@@ -337,6 +457,49 @@ class MudisController < ApplicationController
|
|
|
337
457
|
end
|
|
338
458
|
```
|
|
339
459
|
|
|
460
|
+
**Hanami:**
|
|
461
|
+
|
|
462
|
+
*Mudis Registered in the container*
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
# apps/main/actions/metrics/show.rb
|
|
466
|
+
module Main::Actions::Metrics
|
|
467
|
+
class Show < Main::Action
|
|
468
|
+
include Deps[cache: "cache"]
|
|
469
|
+
|
|
470
|
+
def handle(*, res)
|
|
471
|
+
res.format = :json
|
|
472
|
+
res.body = { mudis: cache.metrics }.to_json
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
*Mudis not registered in the container*
|
|
479
|
+
|
|
480
|
+
```ruby
|
|
481
|
+
# apps/main/actions/metrics/show.rb
|
|
482
|
+
module Main::Actions::Metrics
|
|
483
|
+
class Show < Main::Action
|
|
484
|
+
def handle(*, res)
|
|
485
|
+
res.format = :json
|
|
486
|
+
res.body = { mudis: Mudis.metrics }.to_json
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
# config/routes.rb
|
|
494
|
+
module Main
|
|
495
|
+
class Routes < Hanami::Routes
|
|
496
|
+
root { "OK" }
|
|
497
|
+
|
|
498
|
+
get "/metrics", to: "metrics.show"
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
```
|
|
502
|
+
|
|
340
503
|
---
|
|
341
504
|
|
|
342
505
|
## Advanced Configuration
|
|
@@ -395,7 +558,7 @@ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression ON
|
|
|
395
558
|
|
|
396
559
|
> For context: a typical database query or HTTP call takes 10–50ms. A difference of less than 1ms per operation is negligible for most apps.
|
|
397
560
|
|
|
398
|
-
|
|
561
|
+
#### **Why this overhead exists**
|
|
399
562
|
|
|
400
563
|
Mudis includes features that MemoryStore doesn’t:
|
|
401
564
|
|
|
@@ -408,6 +571,7 @@ Mudis includes features that MemoryStore doesn’t:
|
|
|
408
571
|
| Thread safety | ✅ Bucket-level mutexes | ✅ Global mutex |
|
|
409
572
|
| Observability | ✅ | ❌ |
|
|
410
573
|
| Namespacing | ✅ | ❌ Manual scoping |
|
|
574
|
+
| IPC Aware | ✅ (if enabled) | ❌ Requires manual configuration and additional gems |
|
|
411
575
|
|
|
412
576
|
It will be down to the developer to decide if a fraction of a millisecond is worth
|
|
413
577
|
|
|
@@ -426,6 +590,22 @@ _10000 iterations of 1MB, Marshal (to match MemoryStore default), compression OF
|
|
|
426
590
|
|
|
427
591
|
With compression disabled, Mudis writes significanty faster and reads are virtually identical. Optimisation and configuration of Mudis will be determined by your individual needs.
|
|
428
592
|
|
|
593
|
+
#### Other Benchmarks
|
|
594
|
+
|
|
595
|
+
_10000 iterations of 512KB, JSON, compression OFF (to match MemoryStore default)_
|
|
596
|
+
|
|
597
|
+
| Operation | `Rails.cache` | `Mudis` | Delta |
|
|
598
|
+
| --------- | ------------- | ----------- | ------------- |
|
|
599
|
+
| Write | 1.291 ms/op | 0.32 ms/op | **−0.971 ms** |
|
|
600
|
+
| Read | 0.011 ms/op | 0.048 ms/op | +0.037 ms |
|
|
601
|
+
|
|
602
|
+
_10000 iterations of 512KB, JSON, compression ON_
|
|
603
|
+
|
|
604
|
+
| Operation | `Rails.cache` | `Mudis` | Delta |
|
|
605
|
+
| --------- | ------------- | ----------- | ------------- |
|
|
606
|
+
| Write | 1.11 ms/op | 1.16 ms/op | +0.05 ms |
|
|
607
|
+
| Read | 0.07 ms/op | 0.563 ms/op | +0.493 ms |
|
|
608
|
+
|
|
429
609
|
---
|
|
430
610
|
|
|
431
611
|
## Graceful Shutdown
|
|
@@ -445,7 +625,92 @@ at_exit { Mudis.stop_expiry_thread }
|
|
|
445
625
|
|
|
446
626
|
---
|
|
447
627
|
|
|
448
|
-
##
|
|
628
|
+
## Inter-Process Caching (IPC Mode)
|
|
629
|
+
|
|
630
|
+
While Mudis was originally designed as an in-process cache, it can also operate as a shared inter-process cache when running in environments that use concurrent processes (such as Puma in cluster mode). This is achieved through a local UNIX socket server that allows all workers to access a single, centralized Mudis instance.
|
|
631
|
+
|
|
632
|
+
### Overview
|
|
633
|
+
|
|
634
|
+
In IPC mode, Mudis runs as a singleton server within the master process.
|
|
635
|
+
|
|
636
|
+
Each worker connects to that server through a lightweight client (`MudisClient`) using a local UNIX domain socket (default: `/tmp/mudis.sock`).
|
|
637
|
+
All cache operations, e.g., read, write, delete, fetch, etc., are transparently proxied to the master process, which holds the authoritative cache state.
|
|
638
|
+
|
|
639
|
+
This design allows multiple workers to share the same cache without duplicating memory or losing synchronization, while retaining Mudis’ performance, configurability, and thread safety.
|
|
640
|
+
|
|
641
|
+
| **Benefit** | **Description** |
|
|
642
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
643
|
+
| **Shared Cache Across Processes** | All Puma workers share one Mudis instance via IPC. |
|
|
644
|
+
| **Zero External Dependencies** | No Redis, Memcached, or separate daemon required. |
|
|
645
|
+
| **Memory Efficient** | Cache data stored only once, not duplicated per worker. |
|
|
646
|
+
| **Full Feature Support** | All Mudis features (TTL, compression, metrics, etc.) work transparently. |
|
|
647
|
+
| **Safe & Local** | Communication is limited to the host system’s UNIX socket, ensuring isolation and speed. |
|
|
648
|
+
|
|
649
|
+

|
|
650
|
+
|
|
651
|
+
### Setup (Puma)
|
|
652
|
+
|
|
653
|
+
Enable IPC mode by adding the following to your Puma configuration:
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
# config/puma.rb
|
|
657
|
+
preload_app!
|
|
658
|
+
|
|
659
|
+
before_fork do
|
|
660
|
+
require "mudis"
|
|
661
|
+
require "mudis_server"
|
|
662
|
+
|
|
663
|
+
# typical Mudis configuration
|
|
664
|
+
Mudis.configure do |c|
|
|
665
|
+
c.serializer = JSON
|
|
666
|
+
c.compress = true
|
|
667
|
+
c.max_value_bytes = 2_000_000
|
|
668
|
+
c.hard_memory_limit = true
|
|
669
|
+
c.max_bytes = 1_073_741_824
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
Mudis.start_expiry_thread(interval: 60)
|
|
673
|
+
MudisServer.start!
|
|
674
|
+
|
|
675
|
+
at_exit { Mudis.stop_expiry_thread }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
on_worker_boot do
|
|
679
|
+
require "mudis_client"
|
|
680
|
+
$mudis = MudisClient.new
|
|
681
|
+
require "mudis_proxy" # optionally require the default mudis proxy to invisibly patch Mudis
|
|
682
|
+
end
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
For more granular control over Mudis, adding the Proxy manually to `initializers` (Rails) or `boot` (Hanami) allows seamless use of the API as documented.
|
|
686
|
+
|
|
687
|
+
**Do not require `mudis_proxy` if following this method**
|
|
688
|
+
|
|
689
|
+
```ruby
|
|
690
|
+
# config/<<initializers|boot>>/mudis_proxy.rb
|
|
691
|
+
unless defined?(MudisServer)
|
|
692
|
+
class Mudis
|
|
693
|
+
def self.read(*a, **k) = $mudis.read(*a, **k)
|
|
694
|
+
def self.write(*a, **k) = $mudis.write(*a, **k)
|
|
695
|
+
def self.delete(*a, **k) = $mudis.delete(*a, **k)
|
|
696
|
+
def self.fetch(*a, **k, &b) = $mudis.fetch(*a, **k, &b)
|
|
697
|
+
def self.metrics = $mudis.metrics
|
|
698
|
+
def self.reset_metrics! = $mudis.reset_metrics!
|
|
699
|
+
def self.reset! = $mudis.reset!
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
end
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Use IPC mode when:**
|
|
706
|
+
|
|
707
|
+
- Running Rails or Rack apps under Puma cluster or multi-process background job workers.
|
|
708
|
+
- You need cache consistency and memory efficiency without standing up Redis.
|
|
709
|
+
- You want to preserve Mudis’s observability, configurability, and in-process simplicity at a larger scale.
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## Create a Mudis Web Cache Server
|
|
449
714
|
|
|
450
715
|
### Minimal Setup
|
|
451
716
|
|
|
@@ -548,7 +813,7 @@ The primary use cases are:
|
|
|
548
813
|
|
|
549
814
|
Mudis is not intended to be a general-purpose, distributed caching platform. You are, however, welcome to build on top of Mudis if you want its functionality in such projects. E.g.,
|
|
550
815
|
|
|
551
|
-
- mudis-server – expose Mudis via HTTP, web sockets, hooks, etc
|
|
816
|
+
- mudis-web-cache-server – expose Mudis via HTTP, web sockets, hooks, etc
|
|
552
817
|
- mudis-broker – distributed key routing layer for coordinating multiple Mudis nodes
|
|
553
818
|
- mudis-activejob-store – adapter for using Mudis in job queues or retry buffers
|
|
554
819
|
|
|
@@ -569,6 +834,12 @@ Mudis is not intended to be a general-purpose, distributed caching platform. You
|
|
|
569
834
|
|
|
570
835
|
- [x] clear_namespace(namespace): Remove all keys in a namespace in one call
|
|
571
836
|
|
|
837
|
+
#### Refactor Mudis
|
|
838
|
+
|
|
839
|
+
- [x] Review Mudis for improved readability and reduce complexity in top-level functions
|
|
840
|
+
- [x] Enhanced guards
|
|
841
|
+
- [ ] Review for functionality gaps and enhance as needed
|
|
842
|
+
|
|
572
843
|
---
|
|
573
844
|
|
|
574
845
|
## License
|
data/lib/mudis/version.rb
CHANGED
data/lib/mudis_client.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
# thread-safe client for communicating with the MudisServer via UNIX socket.
|
|
7
|
+
class MudisClient
|
|
8
|
+
SOCKET_PATH = "/tmp/mudis.sock"
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def request(payload) # rubocop:disable Metrics/MethodLength
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
UNIXSocket.open(SOCKET_PATH) do |sock|
|
|
17
|
+
sock.puts(JSON.dump(payload))
|
|
18
|
+
response = sock.gets
|
|
19
|
+
res = JSON.parse(response, symbolize_names: true)
|
|
20
|
+
raise res[:error] unless res[:ok] # rubocop:disable Layout/EmptyLineAfterGuardClause
|
|
21
|
+
res[:value]
|
|
22
|
+
end
|
|
23
|
+
rescue Errno::ENOENT
|
|
24
|
+
warn "[MudisClient] Socket missing; master likely not running MudisServer"
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def read(key, namespace: nil)
|
|
30
|
+
request(cmd: "read", key: key, namespace: namespace)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def write(key, value, expires_in: nil, namespace: nil)
|
|
34
|
+
request(cmd: "write", key: key, value: value, ttl: expires_in, namespace: namespace)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delete(key, namespace: nil)
|
|
38
|
+
request(cmd: "delete", key: key, namespace: namespace)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def exists?(key, namespace: nil)
|
|
42
|
+
request(cmd: "exists", key: key, namespace: namespace)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fetch(key, expires_in: nil, namespace: nil)
|
|
46
|
+
val = read(key, namespace: namespace)
|
|
47
|
+
return val if val
|
|
48
|
+
|
|
49
|
+
new_val = yield
|
|
50
|
+
write(key, new_val, expires_in: expires_in, namespace: namespace)
|
|
51
|
+
new_val
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def metrics
|
|
55
|
+
request(cmd: "metrics")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reset_metrics!
|
|
59
|
+
request(cmd: "reset_metrics")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reset!
|
|
63
|
+
request(cmd: "reset")
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/mudis_proxy.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Optional Mudis proxy layer for IPC mode.
|
|
4
|
+
#
|
|
5
|
+
# To enable:
|
|
6
|
+
# require "mudis_proxy"
|
|
7
|
+
#
|
|
8
|
+
# The proxy will forward calls to `$mudis` (an instance of MudisClient)
|
|
9
|
+
# if it is defined, otherwise fallback to standard in-process behaviour.
|
|
10
|
+
|
|
11
|
+
require_relative "mudis"
|
|
12
|
+
require_relative "mudis_server"
|
|
13
|
+
require_relative "mudis_client"
|
|
14
|
+
|
|
15
|
+
if defined?(MudisServer)
|
|
16
|
+
# In the master process — no proxy needed.
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
unless defined?(MudisClient)
|
|
21
|
+
warn "[MudisProxy] MudisClient not loaded — proxy not activated"
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class << Mudis
|
|
26
|
+
def read(*a, **k) = $mudis.read(*a, **k) # rubocop:disable Naming/MethodParameterName,Style/GlobalVars
|
|
27
|
+
def write(*a, **k) = $mudis.write(*a, **k) # rubocop:disable Naming/MethodParameterName,Style/GlobalVars
|
|
28
|
+
def delete(*a, **k) = $mudis.delete(*a, **k) # rubocop:disable Naming/MethodParameterName,Style/GlobalVars
|
|
29
|
+
def fetch(*a, **k, &b) = $mudis.fetch(*a, **k, &b) # rubocop:disable Naming/MethodParameterName,Style/GlobalVars
|
|
30
|
+
def metrics = $mudis.metrics # rubocop:disable Style/GlobalVars
|
|
31
|
+
def reset_metrics! = $mudis.reset_metrics! # rubocop:disable Style/GlobalVars
|
|
32
|
+
def reset! = $mudis.reset! # rubocop:disable Style/GlobalVars
|
|
33
|
+
end
|
data/lib/mudis_server.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "mudis"
|
|
6
|
+
|
|
7
|
+
# Simple UNIX socket server for handling Mudis operations via IPC mode
|
|
8
|
+
class MudisServer
|
|
9
|
+
SOCKET_PATH = "/tmp/mudis.sock"
|
|
10
|
+
|
|
11
|
+
def self.start! # rubocop:disable Metrics/MethodLength
|
|
12
|
+
# Clean up old socket if it exists
|
|
13
|
+
File.unlink(SOCKET_PATH) if File.exist?(SOCKET_PATH)
|
|
14
|
+
|
|
15
|
+
server = UNIXServer.new(SOCKET_PATH)
|
|
16
|
+
server.listen(128)
|
|
17
|
+
puts "[MudisServer] Listening on #{SOCKET_PATH}"
|
|
18
|
+
|
|
19
|
+
Thread.new do
|
|
20
|
+
loop do
|
|
21
|
+
client = server.accept
|
|
22
|
+
Thread.new(client) do |sock|
|
|
23
|
+
handle_client(sock)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.handle_client(sock) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/MethodLength
|
|
30
|
+
request_line = sock.gets
|
|
31
|
+
return unless request_line
|
|
32
|
+
|
|
33
|
+
req = JSON.parse(request_line, symbolize_names: true)
|
|
34
|
+
cmd = req[:cmd]
|
|
35
|
+
key = req[:key]
|
|
36
|
+
ns = req[:namespace]
|
|
37
|
+
val = req[:value]
|
|
38
|
+
ttl = req[:ttl]
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
case cmd
|
|
42
|
+
when "read"
|
|
43
|
+
result = Mudis.read(key, namespace: ns)
|
|
44
|
+
sock.puts(JSON.dump({ ok: true, value: result }))
|
|
45
|
+
|
|
46
|
+
when "write"
|
|
47
|
+
Mudis.write(key, val, expires_in: ttl, namespace: ns)
|
|
48
|
+
sock.puts(JSON.dump({ ok: true }))
|
|
49
|
+
|
|
50
|
+
when "delete"
|
|
51
|
+
Mudis.delete(key, namespace: ns)
|
|
52
|
+
sock.puts(JSON.dump({ ok: true }))
|
|
53
|
+
|
|
54
|
+
when "exists"
|
|
55
|
+
sock.puts(JSON.dump({ ok: true, value: Mudis.exists?(key, namespace: ns) }))
|
|
56
|
+
|
|
57
|
+
when "fetch"
|
|
58
|
+
result = Mudis.fetch(key, expires_in: ttl, namespace: ns) { req[:fallback] }
|
|
59
|
+
sock.puts(JSON.dump({ ok: true, value: result }))
|
|
60
|
+
|
|
61
|
+
when "metrics"
|
|
62
|
+
sock.puts(JSON.dump({ ok: true, value: Mudis.metrics }))
|
|
63
|
+
|
|
64
|
+
when "reset_metrics"
|
|
65
|
+
Mudis.reset_metrics!
|
|
66
|
+
sock.puts(JSON.dump({ ok: true }))
|
|
67
|
+
|
|
68
|
+
when "reset"
|
|
69
|
+
Mudis.reset!
|
|
70
|
+
sock.puts(JSON.dump({ ok: true }))
|
|
71
|
+
|
|
72
|
+
else
|
|
73
|
+
sock.puts(JSON.dump({ ok: false, error: "unknown command: #{cmd}" }))
|
|
74
|
+
end
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
sock.puts(JSON.dump({ ok: false, error: e.message }))
|
|
77
|
+
ensure
|
|
78
|
+
sock.close
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class MudisClient
|
|
2
|
+
SOCKET_PATH: String
|
|
3
|
+
|
|
4
|
+
def initialize: () -> void
|
|
5
|
+
|
|
6
|
+
def request: (payload: { cmd: String, key?: String, value?: untyped, ttl?: Integer?, namespace?: String? }) -> untyped
|
|
7
|
+
|
|
8
|
+
def read: (key: String, namespace?: String?) -> untyped
|
|
9
|
+
|
|
10
|
+
def write: (key: String, value: untyped, expires_in?: Integer?, namespace?: String?) -> untyped
|
|
11
|
+
|
|
12
|
+
def delete: (key: String, namespace?: String?) -> untyped
|
|
13
|
+
|
|
14
|
+
def exists?: (key: String, namespace?: String?) -> bool
|
|
15
|
+
|
|
16
|
+
def fetch: (key: String, expires_in?: Integer?, namespace?: String?, &block: { () -> untyped }) -> untyped
|
|
17
|
+
|
|
18
|
+
def metrics: () -> { reads: Integer, writes: Integer, deletes: Integer, exists: Integer }
|
|
19
|
+
|
|
20
|
+
def reset_metrics!: () -> void
|
|
21
|
+
|
|
22
|
+
def reset!: () -> void
|
|
23
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe MudisClient do # rubocop:disable Metrics/BlockLength
|
|
6
|
+
let(:socket_path) { "/tmp/mudis.sock" }
|
|
7
|
+
let(:mock_socket) { instance_double(UNIXSocket) }
|
|
8
|
+
let(:client) { MudisClient.new }
|
|
9
|
+
|
|
10
|
+
around do |example|
|
|
11
|
+
ClimateControl.modify("SOCKET_PATH" => socket_path) do
|
|
12
|
+
example.run
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
allow(UNIXSocket).to receive(:open).and_yield(mock_socket)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "#read" do
|
|
21
|
+
it "sends a read command and returns the value" do
|
|
22
|
+
payload = { cmd: "read", key: "test_key", namespace: nil }
|
|
23
|
+
response = { ok: true, value: "test_value" }.to_json
|
|
24
|
+
|
|
25
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
26
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
27
|
+
|
|
28
|
+
expect(client.read("test_key")).to eq("test_value")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "#write" do
|
|
33
|
+
it "sends a write command and returns the value" do
|
|
34
|
+
payload = { cmd: "write", key: "test_key", value: "test_value", ttl: nil, namespace: nil }
|
|
35
|
+
response = { ok: true, value: nil }.to_json
|
|
36
|
+
|
|
37
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
38
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
39
|
+
|
|
40
|
+
expect(client.write("test_key", "test_value")).to be_nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "#delete" do
|
|
45
|
+
it "sends a delete command and returns the value" do
|
|
46
|
+
payload = { cmd: "delete", key: "test_key", namespace: nil }
|
|
47
|
+
response = { ok: true, value: nil }.to_json
|
|
48
|
+
|
|
49
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
50
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
51
|
+
|
|
52
|
+
expect(client.delete("test_key")).to be_nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "#exists?" do
|
|
57
|
+
it "sends an exists command and returns true" do
|
|
58
|
+
payload = { cmd: "exists", key: "test_key", namespace: nil }
|
|
59
|
+
response = { ok: true, value: true }.to_json
|
|
60
|
+
|
|
61
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
62
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
63
|
+
|
|
64
|
+
expect(client.exists?("test_key")).to eq(true)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "#fetch" do
|
|
69
|
+
it "fetches an existing value or writes a new one" do
|
|
70
|
+
read_response = { ok: true, value: nil }.to_json
|
|
71
|
+
write_payload = { cmd: "write", key: "test_key", value: "new_value", ttl: nil, namespace: nil }
|
|
72
|
+
write_response = { ok: true, value: nil }.to_json
|
|
73
|
+
|
|
74
|
+
expect(mock_socket).to receive(:puts).with({ cmd: "read", key: "test_key", namespace: nil }.to_json)
|
|
75
|
+
expect(mock_socket).to receive(:gets).and_return(read_response)
|
|
76
|
+
expect(mock_socket).to receive(:puts).with(write_payload.to_json)
|
|
77
|
+
expect(mock_socket).to receive(:gets).and_return(write_response)
|
|
78
|
+
|
|
79
|
+
result = client.fetch("test_key") { "new_value" } # rubocop:disable Style/RedundantFetchBlock
|
|
80
|
+
expect(result).to eq("new_value")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "#metrics" do
|
|
85
|
+
it "sends a metrics command and returns the metrics" do
|
|
86
|
+
payload = { cmd: "metrics" }
|
|
87
|
+
response = { ok: true, value: { reads: 10, writes: 5 } }.to_json
|
|
88
|
+
|
|
89
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
90
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
91
|
+
|
|
92
|
+
expect(client.metrics).to eq({ reads: 10, writes: 5 })
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "#reset_metrics!" do
|
|
97
|
+
it "sends a reset_metrics command" do
|
|
98
|
+
payload = { cmd: "reset_metrics" }
|
|
99
|
+
response = { ok: true, value: nil }.to_json
|
|
100
|
+
|
|
101
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
102
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
103
|
+
|
|
104
|
+
expect(client.reset_metrics!).to be_nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe "#reset!" do
|
|
109
|
+
it "sends a reset command" do
|
|
110
|
+
payload = { cmd: "reset" }
|
|
111
|
+
response = { ok: true, value: nil }.to_json
|
|
112
|
+
|
|
113
|
+
expect(mock_socket).to receive(:puts).with(payload.to_json)
|
|
114
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
115
|
+
|
|
116
|
+
expect(client.reset!).to be_nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe "error handling" do
|
|
121
|
+
it "warns when the socket is missing" do
|
|
122
|
+
allow(UNIXSocket).to receive(:open).and_raise(Errno::ENOENT)
|
|
123
|
+
|
|
124
|
+
expect { client.read("test_key") }.to output(/Socket missing/).to_stderr
|
|
125
|
+
expect(client.read("test_key")).to be_nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "raises an error when the server returns an error" do
|
|
129
|
+
response = { ok: false, error: "Something went wrong" }.to_json
|
|
130
|
+
|
|
131
|
+
expect(mock_socket).to receive(:puts)
|
|
132
|
+
expect(mock_socket).to receive(:gets).and_return(response)
|
|
133
|
+
|
|
134
|
+
expect { client.read("test_key") }.to raise_error("Something went wrong")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "spec_helper"
|
|
7
|
+
|
|
8
|
+
RSpec.describe MudisServer do # rubocop:disable Metrics/BlockLength
|
|
9
|
+
let(:socket_path) { MudisServer::SOCKET_PATH }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
allow(Mudis).to receive(:read).and_return("mock_value")
|
|
13
|
+
allow(Mudis).to receive(:write)
|
|
14
|
+
allow(Mudis).to receive(:delete)
|
|
15
|
+
allow(Mudis).to receive(:exists?).and_return(true)
|
|
16
|
+
allow(Mudis).to receive(:fetch).and_return("mock_fetched_value")
|
|
17
|
+
allow(Mudis).to receive(:metrics).and_return({ reads: 1, writes: 1 })
|
|
18
|
+
allow(Mudis).to receive(:reset_metrics!)
|
|
19
|
+
allow(Mudis).to receive(:reset!)
|
|
20
|
+
|
|
21
|
+
# Start the server in a separate thread
|
|
22
|
+
Thread.new { MudisServer.start! }
|
|
23
|
+
sleep 0.1 # Allow the server to start
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
after do
|
|
27
|
+
File.unlink(socket_path) if File.exist?(socket_path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def send_request(request)
|
|
31
|
+
UNIXSocket.open(socket_path) do |sock|
|
|
32
|
+
sock.puts(JSON.dump(request))
|
|
33
|
+
JSON.parse(sock.gets, symbolize_names: true)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "handles the 'read' command" do
|
|
38
|
+
response = send_request({ cmd: "read", key: "test_key", namespace: "test_ns" })
|
|
39
|
+
expect(response).to eq({ ok: true, value: "mock_value" })
|
|
40
|
+
expect(Mudis).to have_received(:read).with("test_key", namespace: "test_ns")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "handles the 'write' command" do
|
|
44
|
+
response = send_request({ cmd: "write", key: "test_key", value: "test_value", ttl: 60, namespace: "test_ns" })
|
|
45
|
+
expect(response).to eq({ ok: true })
|
|
46
|
+
expect(Mudis).to have_received(:write).with("test_key", "test_value", expires_in: 60, namespace: "test_ns")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "handles the 'delete' command" do
|
|
50
|
+
response = send_request({ cmd: "delete", key: "test_key", namespace: "test_ns" })
|
|
51
|
+
expect(response).to eq({ ok: true })
|
|
52
|
+
expect(Mudis).to have_received(:delete).with("test_key", namespace: "test_ns")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "handles the 'exists' command" do
|
|
56
|
+
response = send_request({ cmd: "exists", key: "test_key", namespace: "test_ns" })
|
|
57
|
+
expect(response).to eq({ ok: true, value: true })
|
|
58
|
+
expect(Mudis).to have_received(:exists?).with("test_key", namespace: "test_ns")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "handles the 'fetch' command" do
|
|
62
|
+
response = send_request({ cmd: "fetch", key: "test_key", ttl: 60, namespace: "test_ns",
|
|
63
|
+
fallback: "fallback_value" })
|
|
64
|
+
expect(response).to eq({ ok: true, value: "mock_fetched_value" })
|
|
65
|
+
expect(Mudis).to have_received(:fetch).with("test_key", expires_in: 60, namespace: "test_ns")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "handles the 'metrics' command" do
|
|
69
|
+
response = send_request({ cmd: "metrics" })
|
|
70
|
+
expect(response).to eq({ ok: true, value: { reads: 1, writes: 1 } })
|
|
71
|
+
expect(Mudis).to have_received(:metrics)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "handles the 'reset_metrics' command" do
|
|
75
|
+
response = send_request({ cmd: "reset_metrics" })
|
|
76
|
+
expect(response).to eq({ ok: true })
|
|
77
|
+
expect(Mudis).to have_received(:reset_metrics!)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "handles the 'reset' command" do
|
|
81
|
+
response = send_request({ cmd: "reset" })
|
|
82
|
+
expect(response).to eq({ ok: true })
|
|
83
|
+
expect(Mudis).to have_received(:reset!)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "handles unknown commands" do
|
|
87
|
+
response = send_request({ cmd: "unknown_command" })
|
|
88
|
+
expect(response).to eq({ ok: false, error: "unknown command: unknown_command" })
|
|
89
|
+
end
|
|
90
|
+
end
|
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.
|
|
4
|
+
version: 0.7.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-
|
|
11
|
+
date: 2025-10-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -46,18 +46,27 @@ executables: []
|
|
|
46
46
|
extensions: []
|
|
47
47
|
extra_rdoc_files:
|
|
48
48
|
- sig/mudis.rbs
|
|
49
|
+
- sig/mudis_client.rbs
|
|
49
50
|
- sig/mudis_config.rbs
|
|
51
|
+
- sig/mudis_server.rbs
|
|
50
52
|
files:
|
|
51
53
|
- README.md
|
|
52
54
|
- lib/mudis.rb
|
|
53
55
|
- lib/mudis/version.rb
|
|
56
|
+
- lib/mudis_client.rb
|
|
54
57
|
- lib/mudis_config.rb
|
|
58
|
+
- lib/mudis_proxy.rb
|
|
59
|
+
- lib/mudis_server.rb
|
|
55
60
|
- sig/mudis.rbs
|
|
61
|
+
- sig/mudis_client.rbs
|
|
56
62
|
- sig/mudis_config.rbs
|
|
63
|
+
- sig/mudis_server.rbs
|
|
57
64
|
- spec/eviction_spec.rb
|
|
58
65
|
- spec/guardrails_spec.rb
|
|
59
66
|
- spec/memory_guard_spec.rb
|
|
60
67
|
- spec/metrics_spec.rb
|
|
68
|
+
- spec/mudis_client_spec.rb
|
|
69
|
+
- spec/mudis_server_spec.rb
|
|
61
70
|
- spec/mudis_spec.rb
|
|
62
71
|
- spec/namespace_spec.rb
|
|
63
72
|
- spec/reset_spec.rb
|
|
@@ -90,6 +99,8 @@ test_files:
|
|
|
90
99
|
- spec/guardrails_spec.rb
|
|
91
100
|
- spec/memory_guard_spec.rb
|
|
92
101
|
- spec/metrics_spec.rb
|
|
102
|
+
- spec/mudis_client_spec.rb
|
|
103
|
+
- spec/mudis_server_spec.rb
|
|
93
104
|
- spec/mudis_spec.rb
|
|
94
105
|
- spec/namespace_spec.rb
|
|
95
106
|
- spec/reset_spec.rb
|