mudis 0.8.1 → 0.9.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.
data/lib/mudis.rb CHANGED
@@ -5,10 +5,21 @@ require "thread" # rubocop:disable Lint/RedundantRequireStatement
5
5
  require "zlib"
6
6
 
7
7
  require_relative "mudis_config"
8
+ require_relative "mudis/lru"
9
+ require_relative "mudis/persistence"
10
+ require_relative "mudis/metrics"
11
+ require_relative "mudis/namespace"
12
+ require_relative "mudis/expiry"
8
13
 
9
14
  # Mudis is a thread-safe, in-memory, sharded, LRU cache with optional compression and expiry.
10
15
  # It is designed for high concurrency and performance within a Ruby application.
11
16
  class Mudis # rubocop:disable Metrics/ClassLength
17
+ extend LRU
18
+ extend Persistence
19
+ extend Metrics
20
+ extend Namespace
21
+ extend Expiry
22
+
12
23
  # --- Global Configuration and State ---
13
24
 
14
25
  @serializer = JSON # Default serializer (can be changed to Marshal or Oj)
@@ -20,6 +31,8 @@ class Mudis # rubocop:disable Metrics/ClassLength
20
31
  @max_ttl = nil # Optional maximum TTL for cache entries
21
32
  @default_ttl = nil # Default TTL for cache entries if not specified
22
33
 
34
+ # --- Configuration Management ---
35
+
23
36
  class << self
24
37
  attr_accessor :serializer, :compress, :hard_memory_limit, :max_ttl, :default_ttl
25
38
  attr_reader :max_bytes, :max_value_bytes
@@ -76,35 +89,6 @@ class Mudis # rubocop:disable Metrics/ClassLength
76
89
  raise ArgumentError, "default_ttl must be > 0" if config.default_ttl && config.default_ttl <= 0
77
90
  end
78
91
 
79
- # Returns a snapshot of metrics (thread-safe)
80
- def metrics # rubocop:disable Metrics/MethodLength
81
- @metrics_mutex.synchronize do
82
- {
83
- hits: @metrics[:hits],
84
- misses: @metrics[:misses],
85
- evictions: @metrics[:evictions],
86
- rejected: @metrics[:rejected],
87
- total_memory: current_memory_bytes,
88
- least_touched: least_touched(10),
89
- buckets: buckets.times.map do |idx|
90
- {
91
- index: idx,
92
- keys: @stores[idx].size,
93
- memory_bytes: @current_bytes[idx],
94
- lru_size: @lru_nodes[idx].size
95
- }
96
- end
97
- }
98
- end
99
- end
100
-
101
- # Resets metric counters (thread-safe)
102
- def reset_metrics!
103
- @metrics_mutex.synchronize do
104
- @metrics = { hits: 0, misses: 0, evictions: 0, rejected: 0 }
105
- end
106
- end
107
-
108
92
  # Fully resets all internal state (except config)
109
93
  def reset!
110
94
  stop_expiry_thread
@@ -138,17 +122,6 @@ class Mudis # rubocop:disable Metrics/ClassLength
138
122
  end
139
123
  end
140
124
 
141
- # Node structure for the LRU doubly-linked list
142
- class LRUNode
143
- attr_accessor :key, :prev, :next
144
-
145
- def initialize(key)
146
- @key = key
147
- @prev = nil
148
- @next = nil
149
- end
150
- end
151
-
152
125
  # Number of cache buckets (shards). Default: 32
153
126
  def self.buckets
154
127
  return @buckets if @buckets
@@ -172,29 +145,9 @@ class Mudis # rubocop:disable Metrics/ClassLength
172
145
  @expiry_thread = nil # Background thread for expiry cleanup
173
146
  @hard_memory_limit = false # Whether to enforce hard memory cap
174
147
 
175
- class << self
176
- # Starts a thread that periodically removes expired entries
177
- def start_expiry_thread(interval: 60)
178
- return if @expiry_thread&.alive?
179
-
180
- @stop_expiry = false
181
- @expiry_thread = Thread.new do
182
- loop do
183
- break if @stop_expiry
184
-
185
- sleep interval
186
- cleanup_expired!
187
- end
188
- end
189
- end
190
-
191
- # Signals and joins the expiry thread
192
- def stop_expiry_thread
193
- @stop_expiry = true
194
- @expiry_thread&.join
195
- @expiry_thread = nil
196
- end
148
+ # --- Core Cache Operations ---
197
149
 
150
+ class << self
198
151
  # Computes which bucket a key belongs to
199
152
  def bucket_index(key)
200
153
  key.hash % buckets
@@ -367,20 +320,6 @@ class Mudis # rubocop:disable Metrics/ClassLength
367
320
  end
368
321
  end
369
322
 
370
- # Removes expired keys across all buckets
371
- def cleanup_expired!
372
- now = Time.now
373
- buckets.times do |idx|
374
- mutex = @mutexes[idx]
375
- store = @stores[idx]
376
- mutex.synchronize do
377
- store.keys.each do |key| # rubocop:disable Style/HashEachMethods
378
- evict_key(idx, key) if store[key][:expires_at] && now > store[key][:expires_at]
379
- end
380
- end
381
- end
382
- end
383
-
384
323
  # Returns an array of all cache keys
385
324
  def all_keys
386
325
  keys = []
@@ -392,30 +331,6 @@ class Mudis # rubocop:disable Metrics/ClassLength
392
331
  keys
393
332
  end
394
333
 
395
- # Returns all keys in a specific namespace
396
- def keys(namespace:)
397
- raise ArgumentError, "namespace is required" unless namespace
398
-
399
- prefix = "#{namespace}:"
400
- all_keys.select { |key| key.start_with?(prefix) }.map { |key| key.delete_prefix(prefix) }
401
- end
402
-
403
- # Clears all keys in a specific namespace
404
- def clear_namespace(namespace:)
405
- raise ArgumentError, "namespace is required" unless namespace
406
-
407
- prefix = "#{namespace}:"
408
- buckets.times do |idx|
409
- mutex = @mutexes[idx]
410
- store = @stores[idx]
411
-
412
- mutex.synchronize do
413
- keys_to_delete = store.keys.select { |key| key.start_with?(prefix) }
414
- keys_to_delete.each { |key| evict_key(idx, key) }
415
- end
416
- end
417
- end
418
-
419
334
  # Returns the least-touched keys across all buckets
420
335
  def least_touched(n = 10) # rubocop:disable Metrics/MethodLength,Naming/MethodParameterName
421
336
  keys_with_touches = []
@@ -444,15 +359,6 @@ class Mudis # rubocop:disable Metrics/ClassLength
444
359
  @max_bytes
445
360
  end
446
361
 
447
- # Executes a block with a specific namespace, restoring the old namespace afterwards
448
- def with_namespace(namespace)
449
- old_ns = Thread.current[:mudis_namespace]
450
- Thread.current[:mudis_namespace] = namespace
451
- yield
452
- ensure
453
- Thread.current[:mudis_namespace] = old_ns
454
- end
455
-
456
362
  private
457
363
 
458
364
  # Decompresses and deserializes a raw value
@@ -460,181 +366,5 @@ class Mudis # rubocop:disable Metrics/ClassLength
460
366
  val = compress ? Zlib::Inflate.inflate(raw) : raw
461
367
  serializer.load(val)
462
368
  end
463
-
464
- # Thread-safe metric increment
465
- def metric(name)
466
- @metrics_mutex.synchronize { @metrics[name] += 1 }
467
- end
468
-
469
- # Removes a key from storage and LRU
470
- def evict_key(idx, key)
471
- store = @stores[idx]
472
- entry = store.delete(key)
473
- return unless entry
474
-
475
- @current_bytes[idx] -= (key.bytesize + entry[:value].bytesize)
476
-
477
- node = @lru_nodes[idx].delete(key)
478
- remove_node(idx, node) if node
479
- end
480
-
481
- # Inserts a key at the head of the LRU list
482
- def insert_lru(idx, key)
483
- node = LRUNode.new(key)
484
- node.next = @lru_heads[idx]
485
- @lru_heads[idx].prev = node if @lru_heads[idx]
486
- @lru_heads[idx] = node
487
- @lru_tails[idx] ||= node
488
- @lru_nodes[idx][key] = node
489
- end
490
-
491
- # Promotes a key to the front of the LRU list
492
- def promote_lru(idx, key)
493
- node = @lru_nodes[idx][key]
494
- return unless node && @lru_heads[idx] != node
495
-
496
- remove_node(idx, node)
497
- insert_lru(idx, key)
498
- end
499
-
500
- # Removes a node from the LRU list
501
- def remove_node(idx, node)
502
- if node.prev
503
- node.prev.next = node.next
504
- else
505
- @lru_heads[idx] = node.next
506
- end
507
-
508
- if node.next
509
- node.next.prev = node.prev
510
- else
511
- @lru_tails[idx] = node.prev
512
- end
513
- end
514
-
515
- # Namespaces a key with an optional namespace
516
- def namespaced_key(key, namespace = nil)
517
- ns = namespace || Thread.current[:mudis_namespace]
518
- ns ? "#{ns}:#{key}" : key
519
- end
520
-
521
- # Calculates the effective TTL for an entry, respecting max_ttl if set
522
- def effective_ttl(expires_in)
523
- ttl = expires_in || @default_ttl
524
- return nil unless ttl
525
- return ttl unless @max_ttl
526
-
527
- [ttl, @max_ttl].min
528
- end
529
- end
530
-
531
- class << self
532
- # Saves the current cache state to disk for persistence
533
- def save_snapshot!
534
- return unless @persistence_enabled
535
-
536
- data = snapshot_dump
537
- safe_write_snapshot(data)
538
- rescue StandardError => e
539
- warn "[Mudis] Failed to save snapshot: #{e.class}: #{e.message}"
540
- end
541
-
542
- # Loads the cache state from disk for persistence
543
- def load_snapshot!
544
- return unless @persistence_enabled
545
- return unless File.exist?(@persistence_path)
546
-
547
- data = read_snapshot
548
- snapshot_restore(data)
549
- rescue StandardError => e
550
- warn "[Mudis] Failed to load snapshot: #{e.class}: #{e.message}"
551
- end
552
-
553
- # Installs an at_exit hook to save the snapshot on process exit
554
- def install_persistence_hook!
555
- return unless @persistence_enabled
556
- return if defined?(@persistence_hook_installed) && @persistence_hook_installed
557
-
558
- at_exit { save_snapshot! }
559
- @persistence_hook_installed = true
560
- end
561
- end
562
-
563
- class << self
564
- private
565
-
566
- # Collect a JSON/Marshal-safe array of { key, value, expires_in }
567
- def snapshot_dump # rubocop:disable Metrics/MethodLength
568
- entries = []
569
- now = Time.now
570
- @buckets.times do |idx|
571
- mutex = @mutexes[idx]
572
- store = @stores[idx]
573
- mutex.synchronize do
574
- store.each do |key, raw|
575
- exp_at = raw[:expires_at]
576
- next if exp_at && now > exp_at
577
-
578
- value = decompress_and_deserialize(raw[:value])
579
- expires_in = exp_at ? (exp_at - now).to_i : nil
580
- entries << { key: key, value: value, expires_in: expires_in }
581
- end
582
- end
583
- end
584
- entries
585
- end
586
-
587
- # Restore via existing write-path so LRU/limits/compression/TTL are honored
588
- def snapshot_restore(entries)
589
- return unless entries && !entries.empty?
590
-
591
- entries.each do |e|
592
- begin # rubocop:disable Style/RedundantBegin
593
- write(e[:key], e[:value], expires_in: e[:expires_in])
594
- rescue StandardError => ex
595
- warn "[Mudis] Failed to restore key #{e[:key].inspect}: #{ex.message}"
596
- end
597
- end
598
- end
599
-
600
- # Serializer for snapshot persistence
601
- # Defaults to Marshal if not JSON
602
- def serializer_for_snapshot
603
- (@persistence_format || :marshal).to_sym == :json ? JSON : :marshal
604
- end
605
-
606
- # Safely writes snapshot data to disk
607
- # Uses safe write if configured
608
- def safe_write_snapshot(data) # rubocop:disable Metrics/MethodLength
609
- path = @persistence_path
610
- dir = File.dirname(path)
611
- Dir.mkdir(dir) unless Dir.exist?(dir)
612
-
613
- payload =
614
- if (@persistence_format || :marshal).to_sym == :json
615
- serializer_for_snapshot.dump(data)
616
- else
617
- Marshal.dump(data)
618
- end
619
-
620
- if @persistence_safe_write
621
- tmp = "#{path}.tmp-#{$$}-#{Thread.current.object_id}"
622
- File.open(tmp, "wb") { |f| f.write(payload) }
623
- File.rename(tmp, path)
624
- else
625
- File.open(path, "wb") { |f| f.write(payload) }
626
- end
627
- end
628
-
629
- # Reads snapshot data from disk
630
- # Uses safe read if configured
631
- def read_snapshot
632
- if (@persistence_format || :marshal).to_sym == :json
633
- serializer_for_snapshot.load(File.binread(@persistence_path))
634
- else
635
- ## safe to use Marshal here as we control the file
636
- Marshal.load(File.binread(@persistence_path)) # rubocop:disable Security/MarshalLoad
637
- end
638
- end
639
369
  end
640
370
  end
data/lib/mudis_client.rb CHANGED
@@ -2,64 +2,106 @@
2
2
 
3
3
  require "socket"
4
4
  require "json"
5
+ require_relative "mudis_ipc_config"
5
6
 
6
- # thread-safe client for communicating with the MudisServer via UNIX socket.
7
+ # Thread-safe client for communicating with the MudisServer
8
+ # Automatically uses UNIX sockets on Linux/macOS and TCP on Windows
7
9
  class MudisClient
8
- SOCKET_PATH = "/tmp/mudis.sock"
10
+ include MudisIPCConfig
9
11
 
10
12
  def initialize
11
13
  @mutex = Mutex.new
12
14
  end
13
15
 
14
- def request(payload) # rubocop:disable Metrics/MethodLength
16
+ # Open a connection to the server (TCP or UNIX)
17
+ def open_connection
18
+ if MudisIPCConfig.use_tcp?
19
+ TCPSocket.new(TCP_HOST, TCP_PORT)
20
+ else
21
+ UNIXSocket.open(SOCKET_PATH)
22
+ end
23
+ end
24
+
25
+ # Send a request to the MudisServer and return the response
26
+ # @param payload [Hash] The request payload
27
+ # @return [Object] The response value from the server
28
+ def request(payload) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
15
29
  @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"
30
+ sock = open_connection
31
+ sock.puts(JSON.dump(payload))
32
+ response = sock.gets
33
+ sock.close
34
+
35
+ return nil unless response
36
+
37
+ res = JSON.parse(response, symbolize_names: true)
38
+ raise res[:error] unless res[:ok]
39
+
40
+ res[:value]
41
+ rescue Errno::ENOENT, Errno::ECONNREFUSED
42
+ warn "[MudisClient] Cannot connect to MudisServer. Is it running?"
43
+ nil
44
+ rescue JSON::ParserError
45
+ warn "[MudisClient] Invalid JSON response from server"
46
+ nil
47
+ rescue IOError, SystemCallError => e
48
+ warn "[MudisClient] Connection error: #{e.message}"
25
49
  nil
26
50
  end
27
51
  end
28
52
 
53
+ # --- Forwarded Mudis methods ---
54
+
55
+ # Read a value from the Mudis server
29
56
  def read(key, namespace: nil)
30
- request(cmd: "read", key: key, namespace: namespace)
57
+ command("read", key:, namespace:)
31
58
  end
32
59
 
60
+ # Write a value to the Mudis server
33
61
  def write(key, value, expires_in: nil, namespace: nil)
34
- request(cmd: "write", key: key, value: value, ttl: expires_in, namespace: namespace)
62
+ command("write", key:, value:, ttl: expires_in, namespace:)
35
63
  end
36
64
 
65
+ # Delete a value from the Mudis server
37
66
  def delete(key, namespace: nil)
38
- request(cmd: "delete", key: key, namespace: namespace)
67
+ command("delete", key:, namespace:)
39
68
  end
40
69
 
70
+ # Check if a key exists in the Mudis server
41
71
  def exists?(key, namespace: nil)
42
- request(cmd: "exists", key: key, namespace: namespace)
72
+ command("exists", key:, namespace:)
43
73
  end
44
74
 
75
+ # Fetch a value, computing and storing it if not present
45
76
  def fetch(key, expires_in: nil, namespace: nil)
46
- val = read(key, namespace: namespace)
77
+ val = read(key, namespace:)
47
78
  return val if val
48
79
 
49
80
  new_val = yield
50
- write(key, new_val, expires_in: expires_in, namespace: namespace)
81
+ write(key, new_val, expires_in:, namespace:)
51
82
  new_val
52
83
  end
53
84
 
85
+ # Retrieve metrics from the Mudis server
54
86
  def metrics
55
- request(cmd: "metrics")
87
+ command("metrics")
56
88
  end
57
89
 
90
+ # Reset metrics on the Mudis server
58
91
  def reset_metrics!
59
- request(cmd: "reset_metrics")
92
+ command("reset_metrics")
60
93
  end
61
94
 
95
+ # Reset the Mudis server cache state
62
96
  def reset!
63
- request(cmd: "reset")
97
+ command("reset")
98
+ end
99
+
100
+ private
101
+
102
+ # Helper to send a command with options
103
+ def command(cmd, **opts)
104
+ request({ cmd:, **opts })
64
105
  end
106
+
65
107
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared configuration for IPC mode (server and client)
4
+ module MudisIPCConfig
5
+ SOCKET_PATH = "/tmp/mudis.sock"
6
+ TCP_HOST = "127.0.0.1"
7
+ TCP_PORT = 9876
8
+
9
+ # Check if TCP mode should be used (Windows or forced via ENV)
10
+ def self.use_tcp?
11
+ ENV["MUDIS_FORCE_TCP"] == "true" || Gem.win_platform?
12
+ end
13
+ end
data/lib/mudis_proxy.rb CHANGED
@@ -24,6 +24,8 @@ unless defined?($mudis) && $mudis # rubocop:disable Style/GlobalVars
24
24
  return
25
25
  end
26
26
 
27
+ # --- Proxy method forwarding ---
28
+
27
29
  class << Mudis
28
30
  def read(*a, **k) = $mudis.read(*a, **k) # rubocop:disable Naming/MethodParameterName,Style/GlobalVars
29
31
  def write(*a, **k) = $mudis.write(*a, **k) # rubocop:disable Naming/MethodParameterName,Style/GlobalVars
data/lib/mudis_server.rb CHANGED
@@ -3,19 +3,56 @@
3
3
  require "socket"
4
4
  require "json"
5
5
  require_relative "mudis"
6
+ require_relative "mudis_ipc_config"
6
7
 
7
- # Simple UNIX socket server for handling Mudis operations via IPC mode
8
+ # Socket server for handling Mudis operations via IPC mode
9
+ # Automatically uses UNIX sockets on Linux/macOS and TCP on Windows
8
10
  class MudisServer
9
- SOCKET_PATH = "/tmp/mudis.sock"
11
+ include MudisIPCConfig
12
+
13
+ # Define command handlers mapping
14
+ # Each command maps to a lambda that takes a request hash and performs the corresponding Mudis operation.
15
+ COMMANDS = {
16
+ "read" => ->(r) { Mudis.read(r[:key], namespace: r[:namespace]) },
17
+ "write" => ->(r) { Mudis.write(r[:key], r[:value], expires_in: r[:ttl], namespace: r[:namespace]) },
18
+ "delete" => ->(r) { Mudis.delete(r[:key], namespace: r[:namespace]) },
19
+ "exists" => ->(r) { Mudis.exists?(r[:key], namespace: r[:namespace]) },
20
+ "fetch" => ->(r) { Mudis.fetch(r[:key], expires_in: r[:ttl], namespace: r[:namespace]) { r[:fallback] } },
21
+ "metrics" => ->(_) { Mudis.metrics },
22
+ "reset_metrics" => ->(_) { Mudis.reset_metrics! },
23
+ "reset" => ->(_) { Mudis.reset! }
24
+ }.freeze
25
+
26
+ # Start the MudisServer
27
+ # Automatically selects TCP on Windows, UNIX sockets elsewhere
28
+ # This will run in a separate thread and handle incoming client connections.
29
+ def self.start!
30
+ if MudisIPCConfig.use_tcp?
31
+ start_tcp_server!
32
+ else
33
+ start_unix_server!
34
+ end
35
+ end
10
36
 
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)
37
+ # Start TCP server (for Windows or development)
38
+ def self.start_tcp_server!
39
+ warn "[MudisServer] Using TCP mode - recommended for development only"
40
+ server = TCPServer.new(TCP_HOST, TCP_PORT)
41
+ puts "[MudisServer] Listening on TCP #{TCP_HOST}:#{TCP_PORT}"
42
+ accept_connections(server)
43
+ end
14
44
 
45
+ # Start UNIX socket server (production mode for Linux/macOS)
46
+ def self.start_unix_server! # rubocop:disable Metrics/MethodLength
47
+ File.unlink(SOCKET_PATH) if File.exist?(SOCKET_PATH)
15
48
  server = UNIXServer.new(SOCKET_PATH)
16
49
  server.listen(128)
17
- puts "[MudisServer] Listening on #{SOCKET_PATH}"
50
+ puts "[MudisServer] Listening on UNIX socket #{SOCKET_PATH}"
51
+ accept_connections(server)
52
+ end
18
53
 
54
+ # Accept connections in a loop (works for both TCP and UNIX)
55
+ def self.accept_connections(server)
19
56
  Thread.new do
20
57
  loop do
21
58
  client = server.accept
@@ -26,56 +63,38 @@ class MudisServer
26
63
  end
27
64
  end
28
65
 
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 }))
66
+ # Handle a single client connection
67
+ # Reads the request, processes it, and sends back the response
68
+ # @param socket [Socket] The client socket (TCP or UNIX)
69
+ # @return [void]
70
+ def self.handle_client(socket)
71
+ request = JSON.parse(socket.gets, symbolize_names: true)
72
+ return unless request
73
+
74
+ response = process_request(request)
75
+ write_response(socket, ok: true, value: response)
76
+ rescue StandardError => e
77
+ write_response(socket, ok: false, error: e.message)
78
+ ensure
79
+ socket.close
80
+ end
63
81
 
64
- when "reset_metrics"
65
- Mudis.reset_metrics!
66
- sock.puts(JSON.dump({ ok: true }))
82
+ # Process a request hash and return the result
83
+ # Raises an error if the command is unknown
84
+ # @param req [Hash] The request hash containing :cmd and other parameters
85
+ # @return [Object] The result of the command execution
86
+ def self.process_request(req)
87
+ handler = COMMANDS[req[:cmd]]
88
+ raise "Unknown command: #{req[:cmd]}" unless handler
67
89
 
68
- when "reset"
69
- Mudis.reset!
70
- sock.puts(JSON.dump({ ok: true }))
90
+ handler.call(req)
91
+ end
71
92
 
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
93
+ # Write a response to the client socket
94
+ # @param socket [Socket] The client socket
95
+ # @param payload [Hash] The response payload
96
+ # @return [void]
97
+ def self.write_response(socket, payload)
98
+ socket.puts(JSON.dump(payload))
80
99
  end
81
100
  end
data/sig/mudis.rbs CHANGED
@@ -1,4 +1,10 @@
1
1
  class Mudis
2
+ extend LRU
3
+ extend Persistence
4
+ extend Metrics
5
+ extend Namespace
6
+ extend Expiry
7
+
2
8
  # Configuration
3
9
  class << self
4
10
  attr_accessor serializer : Object
data/sig/mudis_client.rbs CHANGED
@@ -1,8 +1,10 @@
1
1
  class MudisClient
2
- SOCKET_PATH: String
2
+ include MudisIPCConfig
3
3
 
4
4
  def initialize: () -> void
5
5
 
6
+ def open_connection: () -> (TCPSocket | UNIXSocket)
7
+
6
8
  def request: (payload: { cmd: String, key?: String, value?: untyped, ttl?: Integer?, namespace?: String? }) -> untyped
7
9
 
8
10
  def read: (key: String, namespace?: String?) -> untyped