mudis-ql 0.1.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 +7 -0
- data/README.md +596 -0
- data/lib/mudis-ql/metrics_scope.rb +175 -0
- data/lib/mudis-ql/scope.rb +184 -0
- data/lib/mudis-ql/store.rb +79 -0
- data/lib/mudis-ql/version.rb +5 -0
- data/lib/mudis-ql.rb +49 -0
- data/spec/mudis-ql/error_handling_spec.rb +330 -0
- data/spec/mudis-ql/integration_spec.rb +337 -0
- data/spec/mudis-ql/metrics_scope_spec.rb +332 -0
- data/spec/mudis-ql/performance_spec.rb +295 -0
- data/spec/mudis-ql/scope_spec.rb +169 -0
- data/spec/mudis-ql/store_spec.rb +77 -0
- data/spec/mudis-ql_spec.rb +52 -0
- metadata +118 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c8ca84afb158da1ef68705edf7990481b4248356318f76cb591ae6a640f6a3fd
|
|
4
|
+
data.tar.gz: 0c50f8055c99f60448003e67812d8b5b0ec9c630f7a4f981fddee572678354be
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aa080b7e1efdd7ab898550b1261e9af054c3f40848285b1a77a5f9f56f15cb38790eabf1be32e425e35ba5058ee49a302e714ebe677ed2824205b36d55497db2
|
|
7
|
+
data.tar.gz: '084565dea25c30c6e206238e61d5b9e184be59fef8e84135ec5c5a9eb726876c141b6308d1c09ce1e23b240fc839dd2def97eea3123a578d0bcd757fe30db63e'
|
data/README.md
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
# Mudis-QL
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/mudis-ql)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A simple query DSL for [mudis](https://github.com/kiebor81/mudis) cache. Mudis-QL extends mudis by providing a SQL-like query interface for data stored in the cache, enabling you to filter, sort, and paginate cached data without needing a full database.
|
|
7
|
+
|
|
8
|
+
## Why Mudis-QL?
|
|
9
|
+
|
|
10
|
+
**Mudis** is an excellent in-memory cache, but it only supports key-value retrieval. From the mudis documentation:
|
|
11
|
+
|
|
12
|
+
> "No SQL or equivalent query interface for cached data. Data is per Key retrieval only."
|
|
13
|
+
|
|
14
|
+
**Mudis-QL** solves this limitation by providing a chainable query DSL that allows you to:
|
|
15
|
+
- Filter cached data with `where` conditions
|
|
16
|
+
- Sort results with `order`
|
|
17
|
+
- Paginate with `limit` and `offset`
|
|
18
|
+
- Count and check existence
|
|
19
|
+
- Pluck specific fields
|
|
20
|
+
|
|
21
|
+
All while maintaining mudis's speed, thread-safety, and in-memory efficiency.
|
|
22
|
+
|
|
23
|
+
### Best Practice: Use Namespaces
|
|
24
|
+
|
|
25
|
+
**Important**: Mudis-QL is designed to query collections of related data in mudis. For optimal functionality, **always use namespaces** when storing data you intend to query:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Recommended - enables full query functionality
|
|
29
|
+
Mudis.write('user1', { name: 'Alice' }, namespace: 'users')
|
|
30
|
+
Mudis.write('user2', { name: 'Bob' }, namespace: 'users')
|
|
31
|
+
MudisQL.from('users').where(name: /^A/).all
|
|
32
|
+
|
|
33
|
+
# Limited - namespace-less keys can't be listed/queried as a collection
|
|
34
|
+
Mudis.write('user:1', { name: 'Alice' }) # individual key access only
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Namespaces provide logical separation and enable Mudis-QL to retrieve all keys in a collection for filtering, sorting, and pagination. Without namespaces, Mudis-QL can only perform individual key operations.
|
|
38
|
+
|
|
39
|
+
## Design
|
|
40
|
+
|
|
41
|
+
```mermaid
|
|
42
|
+
flowchart TD
|
|
43
|
+
Start([User Code]) --> Entry{Entry Point}
|
|
44
|
+
|
|
45
|
+
Entry -->|mudis-ql.from| CreateScope[Create Scope Instance]
|
|
46
|
+
Entry -->|mudis-ql.metrics| CreateMetrics[Create MetricsScope]
|
|
47
|
+
|
|
48
|
+
CreateScope --> InitStore[Initialize Store with namespace]
|
|
49
|
+
InitStore --> Scope[Scope Object]
|
|
50
|
+
|
|
51
|
+
CreateMetrics --> MetricsObj[MetricsScope Object]
|
|
52
|
+
|
|
53
|
+
Scope --> Chain{Chain Operations?}
|
|
54
|
+
Chain -->|where| WhereOp[Apply Conditions<br/>Hash/Proc/Regex/Range]
|
|
55
|
+
Chain -->|order| OrderOp[Apply Sorting<br/>Handle nil/mixed types]
|
|
56
|
+
Chain -->|limit| LimitOp[Apply Row Limit]
|
|
57
|
+
Chain -->|offset| OffsetOp[Apply Row Offset]
|
|
58
|
+
|
|
59
|
+
WhereOp --> Chain
|
|
60
|
+
OrderOp --> Chain
|
|
61
|
+
LimitOp --> Chain
|
|
62
|
+
OffsetOp --> Chain
|
|
63
|
+
|
|
64
|
+
Chain -->|Terminal Method| Execute{Execution Type}
|
|
65
|
+
|
|
66
|
+
Execute -->|all| FetchAll[Store.all<br/>Get all namespace keys]
|
|
67
|
+
Execute -->|first| FetchFirst[Apply filters & get first]
|
|
68
|
+
Execute -->|last| FetchLast[Apply filters & get last]
|
|
69
|
+
Execute -->|count| FetchCount[Apply filters & count]
|
|
70
|
+
Execute -->|exists?| FetchExists[Apply filters & check any?]
|
|
71
|
+
Execute -->|pluck| FetchPluck[Apply filters & extract fields]
|
|
72
|
+
|
|
73
|
+
FetchAll --> MudisRead[Mudis.keys + Mudis.read]
|
|
74
|
+
FetchFirst --> MudisRead
|
|
75
|
+
FetchLast --> MudisRead
|
|
76
|
+
FetchCount --> MudisRead
|
|
77
|
+
FetchExists --> MudisRead
|
|
78
|
+
FetchPluck --> MudisRead
|
|
79
|
+
|
|
80
|
+
MudisRead --> Transform[Transform to Hash<br/>Add _key field]
|
|
81
|
+
Transform --> Filter[Apply where conditions]
|
|
82
|
+
Filter --> Sort[Apply order]
|
|
83
|
+
Sort --> Paginate[Apply limit/offset]
|
|
84
|
+
Paginate --> Result([Return Results])
|
|
85
|
+
|
|
86
|
+
MetricsObj --> MetricsChain{Metrics Operations}
|
|
87
|
+
MetricsChain -->|summary| GetSummary[Mudis.metrics<br/>Return summary hash]
|
|
88
|
+
MetricsChain -->|hit_rate| CalcHitRate[Calculate hits/total %]
|
|
89
|
+
MetricsChain -->|efficiency| CalcEfficiency[Calculate efficiency score]
|
|
90
|
+
MetricsChain -->|least_touched| ReturnScope1[Return Scope for<br/>least accessed keys]
|
|
91
|
+
MetricsChain -->|buckets| ReturnScope2[Return Scope for<br/>bucket metrics]
|
|
92
|
+
|
|
93
|
+
GetSummary --> MetricsResult([Return Metrics])
|
|
94
|
+
CalcHitRate --> MetricsResult
|
|
95
|
+
CalcEfficiency --> MetricsResult
|
|
96
|
+
ReturnScope1 --> Scope
|
|
97
|
+
ReturnScope2 --> Scope
|
|
98
|
+
|
|
99
|
+
style Start fill:#e1f5ff
|
|
100
|
+
style Result fill:#c8e6c9
|
|
101
|
+
style MetricsResult fill:#c8e6c9
|
|
102
|
+
style MudisRead fill:#fff3e0
|
|
103
|
+
style Scope fill:#f3e5f5
|
|
104
|
+
style MetricsObj fill:#f3e5f5
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Installation
|
|
108
|
+
|
|
109
|
+
Add this line to your application's Gemfile:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
gem 'mudis-ql'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
And then execute:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
$ bundle install
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Or install it yourself as:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
$ gem install mudis-ql
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Requirements
|
|
128
|
+
|
|
129
|
+
- Ruby >= 2.7.0
|
|
130
|
+
- mudis gem
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
### Basic Setup
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
require 'mudis'
|
|
138
|
+
require 'mudis-ql'
|
|
139
|
+
|
|
140
|
+
# Configure mudis first
|
|
141
|
+
Mudis.configure do |c|
|
|
142
|
+
c.serializer = JSON
|
|
143
|
+
c.compress = true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Store some data in mudis
|
|
147
|
+
Mudis.write('user1', { name: 'Alice', age: 30, status: 'active' }, namespace: 'users')
|
|
148
|
+
Mudis.write('user2', { name: 'Bob', age: 25, status: 'active' }, namespace: 'users')
|
|
149
|
+
Mudis.write('user3', { name: 'Charlie', age: 35, status: 'inactive' }, namespace: 'users')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Query Examples
|
|
153
|
+
|
|
154
|
+
#### Simple Queries
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# Get all users from a namespace
|
|
158
|
+
users = mudis-ql.from('users').all
|
|
159
|
+
# => [{"name"=>"Alice", "age"=>30, "status"=>"active", "_key"=>"user1"}, ...]
|
|
160
|
+
|
|
161
|
+
# Filter by exact match
|
|
162
|
+
active_users = mudis-ql.from('users')
|
|
163
|
+
.where(status: 'active')
|
|
164
|
+
.all
|
|
165
|
+
|
|
166
|
+
# Chain multiple conditions
|
|
167
|
+
result = mudis-ql.from('users')
|
|
168
|
+
.where(status: 'active')
|
|
169
|
+
.where(age: ->(v) { v >= 25 })
|
|
170
|
+
.all
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### Advanced Filtering
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# Use proc for custom conditions
|
|
177
|
+
adults = mudis-ql.from('users')
|
|
178
|
+
.where(age: ->(age) { age >= 18 })
|
|
179
|
+
.all
|
|
180
|
+
|
|
181
|
+
# Use regex for pattern matching
|
|
182
|
+
a_names = mudis-ql.from('users')
|
|
183
|
+
.where(name: /^A/i)
|
|
184
|
+
.all
|
|
185
|
+
|
|
186
|
+
# Use ranges
|
|
187
|
+
young_adults = mudis-ql.from('users')
|
|
188
|
+
.where(age: 18..25)
|
|
189
|
+
.all
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### Ordering and Pagination
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
# Order by field (ascending by default)
|
|
196
|
+
sorted_users = mudis-ql.from('users')
|
|
197
|
+
.order(:age)
|
|
198
|
+
.all
|
|
199
|
+
|
|
200
|
+
# Order descending
|
|
201
|
+
sorted_desc = mudis-ql.from('users')
|
|
202
|
+
.order(:age, :desc)
|
|
203
|
+
.all
|
|
204
|
+
|
|
205
|
+
# Limit results
|
|
206
|
+
top_5 = mudis-ql.from('users')
|
|
207
|
+
.order(:age, :desc)
|
|
208
|
+
.limit(5)
|
|
209
|
+
.all
|
|
210
|
+
|
|
211
|
+
# Pagination with offset
|
|
212
|
+
page_2 = mudis-ql.from('users')
|
|
213
|
+
.order(:name)
|
|
214
|
+
.limit(10)
|
|
215
|
+
.offset(10)
|
|
216
|
+
.all
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### Utility Methods
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# Get first matching record
|
|
223
|
+
first_active = mudis-ql.from('users')
|
|
224
|
+
.where(status: 'active')
|
|
225
|
+
.first
|
|
226
|
+
|
|
227
|
+
# Get last matching record
|
|
228
|
+
last_user = mudis-ql.from('users')
|
|
229
|
+
.order(:age)
|
|
230
|
+
.last
|
|
231
|
+
|
|
232
|
+
# Count matching records
|
|
233
|
+
count = mudis-ql.from('users')
|
|
234
|
+
.where(status: 'active')
|
|
235
|
+
.count
|
|
236
|
+
|
|
237
|
+
# Check if any records match
|
|
238
|
+
has_inactive = mudis-ql.from('users')
|
|
239
|
+
.where(status: 'inactive')
|
|
240
|
+
.exists?
|
|
241
|
+
|
|
242
|
+
# Pluck specific fields
|
|
243
|
+
names = mudis-ql.from('users').pluck(:name)
|
|
244
|
+
# => ["Alice", "Bob", "Charlie"]
|
|
245
|
+
|
|
246
|
+
name_age_pairs = mudis-ql.from('users').pluck(:name, :age)
|
|
247
|
+
# => [["Alice", 30], ["Bob", 25], ["Charlie", 35]]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Complete Example
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
# Complex query combining multiple operations
|
|
254
|
+
result = mudis-ql.from('users')
|
|
255
|
+
.where(status: 'active')
|
|
256
|
+
.where(age: ->(age) { age >= 25 })
|
|
257
|
+
.order(:age, :desc)
|
|
258
|
+
.limit(10)
|
|
259
|
+
.offset(0)
|
|
260
|
+
.all
|
|
261
|
+
|
|
262
|
+
# Or using method chaining for pagination
|
|
263
|
+
def get_active_users(page: 1, per_page: 10)
|
|
264
|
+
mudis-ql.from('users')
|
|
265
|
+
.where(status: 'active')
|
|
266
|
+
.order(:name)
|
|
267
|
+
.limit(per_page)
|
|
268
|
+
.offset((page - 1) * per_page)
|
|
269
|
+
.all
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## How It Works
|
|
274
|
+
|
|
275
|
+
mudis-ql works by:
|
|
276
|
+
|
|
277
|
+
1. **Retrieving all keys** from a mudis namespace using `Mudis.keys(namespace:)`
|
|
278
|
+
2. **Loading values** for each key using `Mudis.read(key, namespace:)`
|
|
279
|
+
3. **Applying filters** in memory using Ruby's enumerable methods
|
|
280
|
+
4. **Sorting and paginating** the results
|
|
281
|
+
|
|
282
|
+
This approach is efficient for moderate-sized datasets (thousands of records) that are already cached in memory. For very large datasets, consider using a proper database.
|
|
283
|
+
|
|
284
|
+
## Integration with Rails
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# app/services/user_cache_service.rb
|
|
288
|
+
class UserCacheService
|
|
289
|
+
NAMESPACE = 'users'
|
|
290
|
+
|
|
291
|
+
def self.cache_user(user)
|
|
292
|
+
Mudis.write(
|
|
293
|
+
user.id.to_s,
|
|
294
|
+
user.attributes.slice('name', 'email', 'status', 'created_at'),
|
|
295
|
+
expires_in: 3600,
|
|
296
|
+
namespace: NAMESPACE
|
|
297
|
+
)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def self.active_users(limit: 50)
|
|
301
|
+
mudis-ql.from(NAMESPACE)
|
|
302
|
+
.where(status: 'active')
|
|
303
|
+
.order(:created_at, :desc)
|
|
304
|
+
.limit(limit)
|
|
305
|
+
.all
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.search_by_name(pattern)
|
|
309
|
+
mudis-ql.from(NAMESPACE)
|
|
310
|
+
.where(name: /#{Regexp.escape(pattern)}/i)
|
|
311
|
+
.all
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Integration with Hanami
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
# lib/my_app/repos/user_cache_repo.rb
|
|
320
|
+
module MyApp
|
|
321
|
+
module Repos
|
|
322
|
+
class UserCacheRepo
|
|
323
|
+
NAMESPACE = 'users'
|
|
324
|
+
|
|
325
|
+
def find_active(limit: 50)
|
|
326
|
+
mudis-ql.from(NAMESPACE)
|
|
327
|
+
.where(status: 'active')
|
|
328
|
+
.limit(limit)
|
|
329
|
+
.all
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def find_by_age_range(min:, max:)
|
|
333
|
+
mudis-ql.from(NAMESPACE)
|
|
334
|
+
.where(age: min..max)
|
|
335
|
+
.order(:age)
|
|
336
|
+
.all
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Querying Mudis Metrics
|
|
344
|
+
|
|
345
|
+
mudis-ql provides a powerful interface for querying mudis cache metrics:
|
|
346
|
+
|
|
347
|
+
### Basic Metrics
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
# Get a metrics scope
|
|
351
|
+
metrics = mudis-ql.metrics
|
|
352
|
+
|
|
353
|
+
# Top-level metrics summary
|
|
354
|
+
summary = metrics.summary
|
|
355
|
+
# => { hits: 150, misses: 20, evictions: 5, rejected: 0, total_memory: 45678 }
|
|
356
|
+
|
|
357
|
+
# Cache hit rate
|
|
358
|
+
hit_rate = metrics.hit_rate
|
|
359
|
+
# => 88.24 (percentage)
|
|
360
|
+
|
|
361
|
+
# Overall efficiency
|
|
362
|
+
efficiency = metrics.efficiency
|
|
363
|
+
# => { hit_rate: 88.24, miss_rate: 11.76, eviction_rate: 2.94, rejection_rate: 0.0 }
|
|
364
|
+
|
|
365
|
+
# Total keys and memory
|
|
366
|
+
metrics.total_keys # => 1000
|
|
367
|
+
metrics.total_memory # => 2048576 (bytes)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Querying Least Touched Keys
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
# Get least accessed keys (returns a Scope)
|
|
374
|
+
least_touched = metrics.least_touched
|
|
375
|
+
|
|
376
|
+
# Find never-accessed keys
|
|
377
|
+
never_used = least_touched
|
|
378
|
+
.where(access_count: 0)
|
|
379
|
+
.pluck(:key)
|
|
380
|
+
|
|
381
|
+
# Find keys accessed less than 5 times
|
|
382
|
+
rarely_used = least_touched
|
|
383
|
+
.where(access_count: ->(count) { count < 5 })
|
|
384
|
+
.order(:access_count)
|
|
385
|
+
.all
|
|
386
|
+
|
|
387
|
+
# Identify hotspots (most accessed)
|
|
388
|
+
hotspots = least_touched
|
|
389
|
+
.order(:access_count, :desc)
|
|
390
|
+
.limit(10)
|
|
391
|
+
.pluck(:key, :access_count)
|
|
392
|
+
# => [["user:123", 450], ["product:456", 380], ...]
|
|
393
|
+
|
|
394
|
+
# Quick helper for never-accessed keys
|
|
395
|
+
cold_keys = metrics.never_accessed_keys
|
|
396
|
+
# => ["temp:old_session", "cache:expired_data", ...]
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Querying Bucket Metrics
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
# Get bucket metrics (returns a Scope)
|
|
403
|
+
buckets = metrics.buckets
|
|
404
|
+
|
|
405
|
+
# Find buckets with high memory usage
|
|
406
|
+
high_memory = buckets
|
|
407
|
+
.where(memory_bytes: ->(m) { m > 1_000_000 })
|
|
408
|
+
.order(:memory_bytes, :desc)
|
|
409
|
+
.all
|
|
410
|
+
|
|
411
|
+
# Find imbalanced buckets (many keys)
|
|
412
|
+
busy_buckets = buckets
|
|
413
|
+
.where(keys: ->(k) { k > 50 })
|
|
414
|
+
.pluck(:index, :keys, :memory_bytes)
|
|
415
|
+
|
|
416
|
+
# Analyze specific bucket
|
|
417
|
+
bucket_5 = buckets.where(index: 5).first
|
|
418
|
+
|
|
419
|
+
# Distribution statistics
|
|
420
|
+
dist = metrics.bucket_distribution
|
|
421
|
+
# => {
|
|
422
|
+
# total_buckets: 32,
|
|
423
|
+
# avg_keys_per_bucket: 31.25,
|
|
424
|
+
# max_keys_per_bucket: 45,
|
|
425
|
+
# min_keys_per_bucket: 18,
|
|
426
|
+
# avg_memory_per_bucket: 65536.5,
|
|
427
|
+
# max_memory_per_bucket: 98304,
|
|
428
|
+
# min_memory_per_bucket: 32768
|
|
429
|
+
# }
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Advanced Metrics Queries
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
# Find buckets needing rebalancing
|
|
436
|
+
avg_keys = metrics.bucket_distribution[:avg_keys_per_bucket]
|
|
437
|
+
unbalanced = metrics.buckets
|
|
438
|
+
.where(keys: ->(k) { k > avg_keys * 1.5 })
|
|
439
|
+
.order(:keys, :desc)
|
|
440
|
+
.pluck(:index, :keys)
|
|
441
|
+
|
|
442
|
+
# Monitor memory hotspots
|
|
443
|
+
memory_threshold = 5_000_000
|
|
444
|
+
hot_buckets = metrics.high_memory_buckets(memory_threshold)
|
|
445
|
+
|
|
446
|
+
# Find buckets with many keys
|
|
447
|
+
key_threshold = 100
|
|
448
|
+
busy_buckets = metrics.high_key_buckets(key_threshold)
|
|
449
|
+
|
|
450
|
+
# Cache health monitoring
|
|
451
|
+
health_report = {
|
|
452
|
+
hit_rate: metrics.hit_rate,
|
|
453
|
+
total_keys: metrics.total_keys,
|
|
454
|
+
memory_usage: metrics.total_memory,
|
|
455
|
+
cold_keys_count: metrics.never_accessed_keys.size,
|
|
456
|
+
efficiency: metrics.efficiency,
|
|
457
|
+
distribution: metrics.bucket_distribution
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Real-time Monitoring
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
# Refresh metrics to get latest data
|
|
465
|
+
current_metrics = metrics.refresh
|
|
466
|
+
|
|
467
|
+
# Monitor cache performance over time
|
|
468
|
+
def cache_health_check
|
|
469
|
+
m = mudis-ql.metrics
|
|
470
|
+
|
|
471
|
+
{
|
|
472
|
+
timestamp: Time.now,
|
|
473
|
+
hit_rate: m.hit_rate,
|
|
474
|
+
total_keys: m.total_keys,
|
|
475
|
+
memory_mb: (m.total_memory / 1024.0 / 1024.0).round(2),
|
|
476
|
+
cold_keys: m.never_accessed_keys.size,
|
|
477
|
+
hottest_keys: m.least_touched.order(:access_count, :desc).limit(5).pluck(:key),
|
|
478
|
+
memory_hotspots: m.high_memory_buckets(1_000_000).size
|
|
479
|
+
}
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Create a dashboard endpoint
|
|
483
|
+
class MetricsController < ApplicationController
|
|
484
|
+
def show
|
|
485
|
+
render json: {
|
|
486
|
+
summary: mudis-ql.metrics.summary,
|
|
487
|
+
efficiency: mudis-ql.metrics.efficiency,
|
|
488
|
+
distribution: mudis-ql.metrics.bucket_distribution,
|
|
489
|
+
top_keys: mudis-ql.metrics.least_touched.order(:access_count, :desc).limit(10).all
|
|
490
|
+
}
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## API Reference
|
|
496
|
+
|
|
497
|
+
### mudis-ql.from(namespace)
|
|
498
|
+
|
|
499
|
+
Creates a new scope for the specified mudis namespace.
|
|
500
|
+
|
|
501
|
+
**Returns:** `mudis-ql::Scope`
|
|
502
|
+
|
|
503
|
+
### mudis-ql.metrics
|
|
504
|
+
|
|
505
|
+
Access mudis metrics with a queryable interface.
|
|
506
|
+
|
|
507
|
+
**Returns:** `mudis-ql::MetricsScope`
|
|
508
|
+
|
|
509
|
+
### Scope Methods
|
|
510
|
+
|
|
511
|
+
| Method | Description | Returns |
|
|
512
|
+
|--------|-------------|---------|
|
|
513
|
+
| `where(conditions)` | Filter by hash of conditions | `Scope` (chainable) |
|
|
514
|
+
| `order(field, direction)` | Sort by field (:asc or :desc) | `Scope` (chainable) |
|
|
515
|
+
| `limit(n)` | Limit results to n records | `Scope` (chainable) |
|
|
516
|
+
| `offset(n)` | Skip first n records | `Scope` (chainable) |
|
|
517
|
+
| `all` | Execute query, return all results | `Array<Hash>` |
|
|
518
|
+
| `first` | Return first matching record | `Hash` or `nil` |
|
|
519
|
+
| `last` | Return last matching record | `Hash` or `nil` |
|
|
520
|
+
| `count` | Count matching records | `Integer` |
|
|
521
|
+
| `exists?` | Check if any records match | `Boolean` |
|
|
522
|
+
| `pluck(*fields)` | Extract specific fields | `Array` |
|
|
523
|
+
|
|
524
|
+
### MetricsScope Methods
|
|
525
|
+
|
|
526
|
+
| Method | Description | Returns |
|
|
527
|
+
|--------|-------------|---------|
|
|
528
|
+
| `summary` | Top-level metrics (hits, misses, etc.) | `Hash` |
|
|
529
|
+
| `least_touched` | Query least accessed keys | `Scope` |
|
|
530
|
+
| `buckets` | Query bucket metrics | `Scope` |
|
|
531
|
+
| `total_keys` | Sum of keys across all buckets | `Integer` |
|
|
532
|
+
| `total_memory` | Total memory usage in bytes | `Integer` |
|
|
533
|
+
| `hit_rate` | Cache hit rate percentage | `Float` |
|
|
534
|
+
| `efficiency` | Hit/miss/eviction/rejection rates | `Hash` |
|
|
535
|
+
| `high_memory_buckets(threshold)` | Buckets exceeding memory threshold | `Array<Hash>` |
|
|
536
|
+
| `high_key_buckets(threshold)` | Buckets with many keys | `Array<Hash>` |
|
|
537
|
+
| `bucket_distribution` | Distribution statistics | `Hash` |
|
|
538
|
+
| `never_accessed_keys` | Keys with 0 access count | `Array<String>` |
|
|
539
|
+
| `refresh` | Reload metrics data | `MetricsScope` |
|
|
540
|
+
|
|
541
|
+
### Condition Matchers
|
|
542
|
+
|
|
543
|
+
mudis-ql supports multiple types of matchers in `where` conditions:
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
# Exact match
|
|
547
|
+
.where(status: 'active')
|
|
548
|
+
|
|
549
|
+
# Proc/Lambda for custom logic
|
|
550
|
+
.where(age: ->(v) { v >= 18 })
|
|
551
|
+
|
|
552
|
+
# Regex for pattern matching
|
|
553
|
+
.where(name: /^A/i)
|
|
554
|
+
|
|
555
|
+
# Range for inclusive matching
|
|
556
|
+
.where(age: 18..65)
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Performance Considerations
|
|
560
|
+
|
|
561
|
+
- **Best for**: Small to medium datasets (hundreds to thousands of records)
|
|
562
|
+
- **Memory**: All matching keys are loaded into memory for filtering
|
|
563
|
+
- **Speed**: Fast for cached data, but involves full table scan
|
|
564
|
+
- **Use Case**: Perfect for frequently accessed, relatively static data that benefits from caching
|
|
565
|
+
|
|
566
|
+
For very large datasets or complex queries, consider using a proper database alongside mudis for caching.
|
|
567
|
+
|
|
568
|
+
## Known Limitations
|
|
569
|
+
|
|
570
|
+
1. **Full scan required**: Unlike databases with indexes, mudis-ql must load all records from a namespace to filter them
|
|
571
|
+
2. **In-memory processing**: All filtering happens in Ruby memory, not at the storage layer
|
|
572
|
+
3. **No joins**: Cannot join data across namespaces (each query targets one namespace)
|
|
573
|
+
4. **No aggregations**: No built-in support for GROUP BY, SUM, AVG, etc.
|
|
574
|
+
5. **Namespaces required for queries**: mudis-ql requires mudis namespaces to list and query collections. Keys stored without namespaces cannot be queried as a collection (individual key access still works via mudis directly).
|
|
575
|
+
|
|
576
|
+
These limitations are by design to maintain simplicity and compatibility with mudis's key-value architecture.
|
|
577
|
+
|
|
578
|
+
## Contributing
|
|
579
|
+
|
|
580
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kiebor81/mudisql.
|
|
581
|
+
|
|
582
|
+
## Roadmap
|
|
583
|
+
|
|
584
|
+
Future enhancements under consideration:
|
|
585
|
+
|
|
586
|
+
- [ ] Index support for common query patterns
|
|
587
|
+
- [ ] Aggregation methods (sum, average, group_by)
|
|
588
|
+
- [ ] Multi-namespace queries
|
|
589
|
+
- [ ] Query result caching
|
|
590
|
+
- [ ] Bulk operations support
|
|
591
|
+
- [ ] Custom serialization per namespace
|
|
592
|
+
|
|
593
|
+
## See Also
|
|
594
|
+
|
|
595
|
+
- [Mudis](https://github.com/kiebor81/mudis)
|
|
596
|
+
|