mudis 0.7.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.
data/lib/mudis_client.rb CHANGED
@@ -1,68 +1,65 @@
1
- # frozen_string_literal: true
2
- require "socket"
3
- require "json"
4
-
5
- class MudisClient
6
- SOCKET_PATH = "/tmp/mudis.sock"
7
-
8
- def initialize
9
- @mutex = Mutex.new
10
- end
11
-
12
- def request(payload)
13
- @mutex.synchronize do
14
- UNIXSocket.open(SOCKET_PATH) do |sock|
15
- sock.puts(JSON.dump(payload))
16
- response = sock.gets
17
- res = JSON.parse(response, symbolize_names: true)
18
- raise res[:error] unless res[:ok]
19
- res[:value]
20
-
21
- end
22
-
23
- rescue Errno::ENOENT
24
- warn "[MudisClient] Socket missing; master likely not running MudisServer"
25
- nil
26
-
27
- end
28
-
29
- end
30
-
31
- def read(key, namespace: nil)
32
- request(cmd: "read", key: key, namespace: namespace)
33
- end
34
-
35
- def write(key, value, expires_in: nil, namespace: nil)
36
- request(cmd: "write", key: key, value: value, ttl: expires_in, namespace: namespace)
37
- end
38
-
39
- def delete(key, namespace: nil)
40
- request(cmd: "delete", key: key, namespace: namespace)
41
- end
42
-
43
- def exists?(key, namespace: nil)
44
- request(cmd: "exists", key: key, namespace: namespace)
45
- end
46
-
47
- def fetch(key, expires_in: nil, namespace: nil)
48
- val = read(key, namespace: namespace)
49
- return val if val
50
-
51
- new_val = yield
52
- write(key, new_val, expires_in: expires_in, namespace: namespace)
53
- new_val
54
- end
55
-
56
- def metrics
57
- request(cmd: "metrics")
58
- end
59
-
60
- def reset_metrics!
61
- request(cmd: "reset_metrics")
62
- end
63
-
64
- def reset!
65
- request(cmd: "reset")
66
- end
67
-
68
- end
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_config.rb CHANGED
@@ -1,25 +1,25 @@
1
- # frozen_string_literal: true
2
-
3
- # MudisConfig holds all configuration values for Mudis,
4
- # and provides defaults that can be overridden via Mudis.configure.
5
- class MudisConfig
6
- attr_accessor :serializer,
7
- :compress,
8
- :max_value_bytes,
9
- :hard_memory_limit,
10
- :max_bytes,
11
- :buckets,
12
- :max_ttl,
13
- :default_ttl
14
-
15
- def initialize
16
- @serializer = JSON # Default serialization strategy
17
- @compress = false # Whether to compress values with Zlib
18
- @max_value_bytes = nil # Max size per value (optional)
19
- @hard_memory_limit = false # Enforce max_bytes as hard cap
20
- @max_bytes = 1_073_741_824 # 1 GB default max cache size
21
- @buckets = nil # use nil to signal fallback to ENV or default
22
- @max_ttl = nil # Max TTL for cache entries (optional)
23
- @default_ttl = nil # Default TTL for cache entries (optional)
24
- end
25
- end
1
+ # frozen_string_literal: true
2
+
3
+ # MudisConfig holds all configuration values for Mudis,
4
+ # and provides defaults that can be overridden via Mudis.configure.
5
+ class MudisConfig
6
+ attr_accessor :serializer,
7
+ :compress,
8
+ :max_value_bytes,
9
+ :hard_memory_limit,
10
+ :max_bytes,
11
+ :buckets,
12
+ :max_ttl,
13
+ :default_ttl
14
+
15
+ def initialize
16
+ @serializer = JSON # Default serialization strategy
17
+ @compress = false # Whether to compress values with Zlib
18
+ @max_value_bytes = nil # Max size per value (optional)
19
+ @hard_memory_limit = false # Enforce max_bytes as hard cap
20
+ @max_bytes = 1_073_741_824 # 1 GB default max cache size
21
+ @buckets = nil # use nil to signal fallback to ENV or default
22
+ @max_ttl = nil # Max TTL for cache entries (optional)
23
+ @default_ttl = nil # Default TTL for cache entries (optional)
24
+ end
25
+ end
@@ -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 CHANGED
@@ -1,79 +1,81 @@
1
- # frozen_string_literal: true
2
- require "socket"
3
- require "json"
4
- require_relative "mudis"
5
-
6
- class MudisServer
7
- SOCKET_PATH = "/tmp/mudis.sock"
8
-
9
- def self.start!
10
- # Clean up old socket if it exists
11
- File.unlink(SOCKET_PATH) if File.exist?(SOCKET_PATH)
12
-
13
- server = UNIXServer.new(SOCKET_PATH)
14
- server.listen(128)
15
- puts "[MudisServer] Listening on #{SOCKET_PATH}"
16
-
17
- Thread.new do
18
- loop do
19
- client = server.accept
20
- Thread.new(client) do |sock|
21
- handle_client(sock)
22
- end
23
- end
24
- end
25
- end
26
-
27
- def self.handle_client(sock)
28
- request_line = sock.gets
29
- return unless request_line
30
-
31
- req = JSON.parse(request_line, symbolize_names: true)
32
- cmd = req[:cmd]
33
- key = req[:key]
34
- ns = req[:namespace]
35
- val = req[:value]
36
- ttl = req[:ttl]
37
-
38
- begin
39
- case cmd
40
- when "read"
41
- result = Mudis.read(key, namespace: ns)
42
- sock.puts(JSON.dump({ ok: true, value: result }))
43
-
44
- when "write"
45
- Mudis.write(key, val, expires_in: ttl, namespace: ns)
46
- sock.puts(JSON.dump({ ok: true }))
47
-
48
- when "delete"
49
- Mudis.delete(key, namespace: ns)
50
- sock.puts(JSON.dump({ ok: true }))
51
-
52
- when "exists"
53
- sock.puts(JSON.dump({ ok: true, value: Mudis.exists?(key, namespace: ns) }))
54
-
55
- when "fetch"
56
- result = Mudis.fetch(key, expires_in: ttl, namespace: ns) { req[:fallback] }
57
- sock.puts(JSON.dump({ ok: true, value: result }))
58
-
59
- when "metrics"
60
- sock.puts(JSON.dump({ ok: true, value: Mudis.metrics }))
61
-
62
- when "reset_metrics"
63
- Mudis.reset_metrics!
64
- sock.puts(JSON.dump({ ok: true }))
65
-
66
- when "reset"
67
- Mudis.reset!
68
- sock.puts(JSON.dump({ ok: true }))
69
-
70
- else
71
- sock.puts(JSON.dump({ ok: false, error: "unknown command: #{cmd}" }))
72
- end
73
- rescue => e
74
- sock.puts(JSON.dump({ ok: false, error: e.message }))
75
- ensure
76
- sock.close
77
- end
78
- end
79
- end
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
data/sig/mudis.rbs CHANGED
@@ -1,56 +1,56 @@
1
- class Mudis
2
- # Configuration
3
- class << self
4
- attr_accessor serializer : Object
5
- attr_accessor compress : bool
6
- attr_accessor hard_memory_limit : bool
7
- attr_reader max_bytes : Integer
8
- attr_reader max_value_bytes : Integer?
9
- attr_accessor max_ttl: Integer?
10
- attr_accessor default_ttl: Integer?
11
-
12
- def configure: () { (config: MudisConfig) -> void } -> void
13
- def config: () -> MudisConfig
14
- def apply_config!: () -> void
15
- def validate_config!: () -> void
16
-
17
- def buckets: () -> Integer
18
- end
19
-
20
- # Lifecycle
21
- def self.start_expiry_thread: (?interval: Integer) -> void
22
- def self.stop_expiry_thread: () -> void
23
-
24
- # Core operations
25
- def self.write: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
26
- def self.read: (String, ?namespace: String) -> untyped?
27
- def self.update: (String, ?namespace: String) { (untyped) -> untyped } -> void
28
- def self.delete: (String, ?namespace: String) -> void
29
- def self.exists?: (String, ?namespace: String) -> bool
30
-
31
- # DSL & Helpers
32
- def self.fetch: (
33
- String,
34
- ?expires_in: Integer,
35
- ?force: bool,
36
- ?namespace: String
37
- ) { () -> untyped } -> untyped
38
-
39
- def self.clear: (String, ?namespace: String) -> void
40
- def self.replace: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
41
- def self.inspect: (String, ?namespace: String) -> Hash[Symbol, untyped]?
42
- def self.keys: (?namespace: String) -> Array[String]
43
- def self.clear_namespace: (?namespace: String) -> void
44
-
45
- # Introspection & management
46
- def self.metrics: () -> Hash[Symbol, untyped]
47
- def self.cleanup_expired!: () -> void
48
- def self.all_keys: () -> Array[String]
49
- def self.current_memory_bytes: () -> Integer
50
- def self.max_memory_bytes: () -> Integer
51
- def self.least_touched: (?Integer) -> Array[[String, Integer]]
52
-
53
- # State reset
54
- def self.reset!: () -> void
55
- def self.reset_metrics!: () -> void
56
- end
1
+ class Mudis
2
+ # Configuration
3
+ class << self
4
+ attr_accessor serializer : Object
5
+ attr_accessor compress : bool
6
+ attr_accessor hard_memory_limit : bool
7
+ attr_reader max_bytes : Integer
8
+ attr_reader max_value_bytes : Integer?
9
+ attr_accessor max_ttl: Integer?
10
+ attr_accessor default_ttl: Integer?
11
+
12
+ def configure: () { (config: MudisConfig) -> void } -> void
13
+ def config: () -> MudisConfig
14
+ def apply_config!: () -> void
15
+ def validate_config!: () -> void
16
+
17
+ def buckets: () -> Integer
18
+ end
19
+
20
+ # Lifecycle
21
+ def self.start_expiry_thread: (?interval: Integer) -> void
22
+ def self.stop_expiry_thread: () -> void
23
+
24
+ # Core operations
25
+ def self.write: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
26
+ def self.read: (String, ?namespace: String) -> untyped?
27
+ def self.update: (String, ?namespace: String) { (untyped) -> untyped } -> void
28
+ def self.delete: (String, ?namespace: String) -> void
29
+ def self.exists?: (String, ?namespace: String) -> bool
30
+
31
+ # DSL & Helpers
32
+ def self.fetch: (
33
+ String,
34
+ ?expires_in: Integer,
35
+ ?force: bool,
36
+ ?namespace: String
37
+ ) { () -> untyped } -> untyped
38
+
39
+ def self.clear: (String, ?namespace: String) -> void
40
+ def self.replace: (String, untyped, ?expires_in: Integer, ?namespace: String) -> void
41
+ def self.inspect: (String, ?namespace: String) -> Hash[Symbol, untyped]?
42
+ def self.keys: (?namespace: String) -> Array[String]
43
+ def self.clear_namespace: (?namespace: String) -> void
44
+
45
+ # Introspection & management
46
+ def self.metrics: () -> Hash[Symbol, untyped]
47
+ def self.cleanup_expired!: () -> void
48
+ def self.all_keys: () -> Array[String]
49
+ def self.current_memory_bytes: () -> Integer
50
+ def self.max_memory_bytes: () -> Integer
51
+ def self.least_touched: (?Integer) -> Array[[String, Integer]]
52
+
53
+ # State reset
54
+ def self.reset!: () -> void
55
+ def self.reset_metrics!: () -> void
56
+ 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
data/sig/mudis_config.rbs CHANGED
@@ -1,10 +1,10 @@
1
- class MudisConfig
2
- attr_accessor serializer: Object
3
- attr_accessor compress: bool
4
- attr_accessor max_value_bytes: Integer?
5
- attr_accessor hard_memory_limit: bool
6
- attr_accessor max_bytes: Integer
7
- attr_accessor max_ttl: Integer?
8
- attr_accessor default_ttl: Integer?
9
- attr_accessor buckets: Integer?
10
- end
1
+ class MudisConfig
2
+ attr_accessor serializer: Object
3
+ attr_accessor compress: bool
4
+ attr_accessor max_value_bytes: Integer?
5
+ attr_accessor hard_memory_limit: bool
6
+ attr_accessor max_bytes: Integer
7
+ attr_accessor max_ttl: Integer?
8
+ attr_accessor default_ttl: Integer?
9
+ attr_accessor buckets: Integer?
10
+ end
@@ -0,0 +1,7 @@
1
+ class MudisServer
2
+ SOCKET_PATH: String
3
+
4
+ def self.start!: () -> void
5
+
6
+ def self.handle_client: (sock: UNIXSocket) -> void
7
+ end
@@ -1,29 +1,29 @@
1
- # frozen_string_literal: true
2
-
3
- require "spec_helper"
4
-
5
- RSpec.describe "Mudis LRU Eviction" do
6
- before do
7
- Mudis.reset!
8
- Mudis.stop_expiry_thread
9
-
10
- Mudis.instance_variable_set(:@buckets, 1)
11
- Mudis.instance_variable_set(:@stores, [{}])
12
- Mudis.instance_variable_set(:@mutexes, [Mutex.new])
13
- Mudis.instance_variable_set(:@lru_heads, [nil])
14
- Mudis.instance_variable_set(:@lru_tails, [nil])
15
- Mudis.instance_variable_set(:@lru_nodes, [{}])
16
- Mudis.instance_variable_set(:@current_bytes, [0])
17
- Mudis.hard_memory_limit = false
18
- Mudis.instance_variable_set(:@threshold_bytes, 60)
19
- Mudis.max_value_bytes = 100
20
- end
21
-
22
- it "evicts old entries when size limit is reached" do
23
- Mudis.write("a", "a" * 50)
24
- Mudis.write("b", "b" * 50)
25
-
26
- expect(Mudis.read("a")).to be_nil
27
- expect(Mudis.read("b")).not_to be_nil
28
- end
29
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Mudis LRU Eviction" do
6
+ before do
7
+ Mudis.reset!
8
+ Mudis.stop_expiry_thread
9
+
10
+ Mudis.instance_variable_set(:@buckets, 1)
11
+ Mudis.instance_variable_set(:@stores, [{}])
12
+ Mudis.instance_variable_set(:@mutexes, [Mutex.new])
13
+ Mudis.instance_variable_set(:@lru_heads, [nil])
14
+ Mudis.instance_variable_set(:@lru_tails, [nil])
15
+ Mudis.instance_variable_set(:@lru_nodes, [{}])
16
+ Mudis.instance_variable_set(:@current_bytes, [0])
17
+ Mudis.hard_memory_limit = false
18
+ Mudis.instance_variable_set(:@threshold_bytes, 60)
19
+ Mudis.max_value_bytes = 100
20
+ end
21
+
22
+ it "evicts old entries when size limit is reached" do
23
+ Mudis.write("a", "a" * 50)
24
+ Mudis.write("b", "b" * 50)
25
+
26
+ expect(Mudis.read("a")).to be_nil
27
+ expect(Mudis.read("b")).not_to be_nil
28
+ end
29
+ end