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 +4 -4
- data/README.md +73 -11
- data/lib/mudis/persistence.rb +3 -1
- data/lib/mudis/version.rb +1 -1
- data/lib/mudis.rb +25 -8
- data/sig/mudis.rbs +8 -2
- data/sig/mudis_client.rbs +3 -3
- data/sig/mudis_config.rbs +4 -0
- data/sig/mudis_expiry.rbs +1 -1
- data/sig/mudis_lru.rbs +1 -1
- data/sig/mudis_persistence.rbs +4 -4
- data/sig/mudis_server.rbs +3 -3
- data/spec/memory_guard_spec.rb +9 -0
- data/spec/mudis_client_spec.rb +1 -1
- data/spec/mudis_server_spec.rb +7 -0
- data/spec/namespace_spec.rb +23 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ed8c4f85642ad10c8fc1d4213838004b647cd5a8f9da17dc4f9e8d99f45cb8b
|
|
4
|
+
data.tar.gz: 4eed96ebdb4e701b9ba3b3d9b3e79d08b928215e2bd848f618775a48304160f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
33
|
-
- [Starting
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
999
|
+
- [x] Refactor main classes for enhanced readability
|
|
938
1000
|
- [ ] Review for functionality gaps and enhance as needed
|
|
939
1001
|
|
|
940
1002
|
---
|
data/lib/mudis/persistence.rb
CHANGED
|
@@ -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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
49
|
-
def self.clear_namespace: (
|
|
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:
|
|
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: () ->
|
|
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
data/sig/mudis_lru.rbs
CHANGED
data/sig/mudis_persistence.rbs
CHANGED
|
@@ -8,12 +8,12 @@ class Mudis
|
|
|
8
8
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
-
def snapshot_dump: () ->
|
|
11
|
+
def snapshot_dump: () -> Array[{ key: String, value: untyped, expires_in: Integer? }]
|
|
12
12
|
|
|
13
|
-
def snapshot_restore: (
|
|
13
|
+
def snapshot_restore: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
|
|
14
14
|
|
|
15
|
-
def safe_write_snapshot: (
|
|
15
|
+
def safe_write_snapshot: (Array[{ key: String, value: untyped, expires_in: Integer? }]) -> void
|
|
16
16
|
|
|
17
|
-
def read_snapshot: () ->
|
|
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)) ->
|
|
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[
|
|
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
|
data/spec/memory_guard_spec.rb
CHANGED
|
@@ -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
|
data/spec/mudis_client_spec.rb
CHANGED
|
@@ -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).
|
|
21
|
+
allow(UNIXSocket).to receive(:open).and_return(mock_socket)
|
|
22
22
|
end
|
|
23
23
|
allow(mock_socket).to receive(:close)
|
|
24
24
|
end
|
data/spec/mudis_server_spec.rb
CHANGED
|
@@ -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
|
data/spec/namespace_spec.rb
CHANGED
|
@@ -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.
|
|
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:
|
|
11
|
+
date: 2026-02-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|