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.
- checksums.yaml +4 -4
 - data/README.md +861 -695
 - data/lib/mudis/version.rb +3 -3
 - data/lib/mudis.rb +521 -521
 - data/lib/mudis_client.rb +65 -68
 - data/lib/mudis_config.rb +25 -25
 - data/lib/mudis_proxy.rb +33 -0
 - data/lib/mudis_server.rb +81 -79
 - data/sig/mudis.rbs +56 -56
 - data/sig/mudis_client.rbs +23 -0
 - data/sig/mudis_config.rbs +10 -10
 - data/sig/mudis_server.rbs +7 -0
 - data/spec/eviction_spec.rb +29 -29
 - data/spec/guardrails_spec.rb +138 -138
 - data/spec/memory_guard_spec.rb +33 -33
 - data/spec/metrics_spec.rb +34 -34
 - data/spec/mudis_client_spec.rb +137 -0
 - data/spec/mudis_server_spec.rb +90 -0
 - data/spec/mudis_spec.rb +183 -183
 - data/spec/namespace_spec.rb +69 -69
 - data/spec/reset_spec.rb +31 -31
 - metadata +16 -4
 - data/lib/example_mudis_server_config.md +0 -39
 
    
        data/spec/guardrails_spec.rb
    CHANGED
    
    | 
         @@ -1,138 +1,138 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # frozen_string_literal: true
         
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
            require "spec_helper"
         
     | 
| 
       4 
     | 
    
         
            -
            require "climate_control"
         
     | 
| 
       5 
     | 
    
         
            -
             
     | 
| 
       6 
     | 
    
         
            -
            RSpec.describe "Mudis TTL Guardrail" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
       7 
     | 
    
         
            -
              before do
         
     | 
| 
       8 
     | 
    
         
            -
                Mudis.reset!
         
     | 
| 
       9 
     | 
    
         
            -
                Mudis.configure do |c|
         
     | 
| 
       10 
     | 
    
         
            -
                  c.max_ttl = 60 # 60 seconds max
         
     | 
| 
       11 
     | 
    
         
            -
                end
         
     | 
| 
       12 
     | 
    
         
            -
              end
         
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
              describe "default_ttl configuration" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
       15 
     | 
    
         
            -
                before do
         
     | 
| 
       16 
     | 
    
         
            -
                  Mudis.reset!
         
     | 
| 
       17 
     | 
    
         
            -
                  Mudis.configure do |c|
         
     | 
| 
       18 
     | 
    
         
            -
                    c.default_ttl = 60
         
     | 
| 
       19 
     | 
    
         
            -
                  end
         
     | 
| 
       20 
     | 
    
         
            -
                end
         
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
                it "applies default_ttl when expires_in is nil" do
         
     | 
| 
       23 
     | 
    
         
            -
                  Mudis.write("foo", "bar") # no explicit expires_in
         
     | 
| 
       24 
     | 
    
         
            -
                  meta = Mudis.inspect("foo")
         
     | 
| 
       25 
     | 
    
         
            -
                  expect(meta[:expires_at]).not_to be_nil
         
     | 
| 
       26 
     | 
    
         
            -
                  expect(meta[:expires_at]).to be_within(5).of(Time.now + 60)
         
     | 
| 
       27 
     | 
    
         
            -
                end
         
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
                it "respects expires_in if explicitly given" do
         
     | 
| 
       30 
     | 
    
         
            -
                  Mudis.write("short_lived", "bar", expires_in: 10)
         
     | 
| 
       31 
     | 
    
         
            -
                  meta = Mudis.inspect("short_lived")
         
     | 
| 
       32 
     | 
    
         
            -
                  expect(meta[:expires_at]).not_to be_nil
         
     | 
| 
       33 
     | 
    
         
            -
                  expect(meta[:expires_at]).to be_within(5).of(Time.now + 10)
         
     | 
| 
       34 
     | 
    
         
            -
                end
         
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
       36 
     | 
    
         
            -
                it "applies max_ttl over default_ttl if both are set" do
         
     | 
| 
       37 
     | 
    
         
            -
                  Mudis.configure do |c|
         
     | 
| 
       38 
     | 
    
         
            -
                    c.default_ttl = 120
         
     | 
| 
       39 
     | 
    
         
            -
                    c.max_ttl = 30
         
     | 
| 
       40 
     | 
    
         
            -
                  end
         
     | 
| 
       41 
     | 
    
         
            -
             
     | 
| 
       42 
     | 
    
         
            -
                  Mudis.write("capped", "baz") # no explicit expires_in
         
     | 
| 
       43 
     | 
    
         
            -
                  meta = Mudis.inspect("capped")
         
     | 
| 
       44 
     | 
    
         
            -
                  expect(meta[:expires_at]).not_to be_nil
         
     | 
| 
       45 
     | 
    
         
            -
                  expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
         
     | 
| 
       46 
     | 
    
         
            -
                end
         
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
                it "stores forever if default_ttl and expires_in are nil" do
         
     | 
| 
       49 
     | 
    
         
            -
                  Mudis.configure do |c|
         
     | 
| 
       50 
     | 
    
         
            -
                    c.default_ttl = nil
         
     | 
| 
       51 
     | 
    
         
            -
                  end
         
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
                  Mudis.write("forever", "ever")
         
     | 
| 
       54 
     | 
    
         
            -
                  meta = Mudis.inspect("forever")
         
     | 
| 
       55 
     | 
    
         
            -
                  expect(meta[:expires_at]).to be_nil
         
     | 
| 
       56 
     | 
    
         
            -
                end
         
     | 
| 
       57 
     | 
    
         
            -
              end
         
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
       59 
     | 
    
         
            -
              it "clamps expires_in to max_ttl if it exceeds max_ttl" do
         
     | 
| 
       60 
     | 
    
         
            -
                Mudis.write("foo", "bar", expires_in: 300) # user requests 5 minutes
         
     | 
| 
       61 
     | 
    
         
            -
             
     | 
| 
       62 
     | 
    
         
            -
                metadata = Mudis.inspect("foo")
         
     | 
| 
       63 
     | 
    
         
            -
                ttl = metadata[:expires_at] - metadata[:created_at]
         
     | 
| 
       64 
     | 
    
         
            -
             
     | 
| 
       65 
     | 
    
         
            -
                expect(ttl).to be <= 60
         
     | 
| 
       66 
     | 
    
         
            -
                expect(ttl).to be > 0
         
     | 
| 
       67 
     | 
    
         
            -
              end
         
     | 
| 
       68 
     | 
    
         
            -
             
     | 
| 
       69 
     | 
    
         
            -
              it "respects expires_in if below max_ttl" do
         
     | 
| 
       70 
     | 
    
         
            -
                Mudis.write("bar", "baz", expires_in: 30) # under the max_ttl
         
     | 
| 
       71 
     | 
    
         
            -
             
     | 
| 
       72 
     | 
    
         
            -
                metadata = Mudis.inspect("bar")
         
     | 
| 
       73 
     | 
    
         
            -
                ttl = metadata[:expires_at] - metadata[:created_at]
         
     | 
| 
       74 
     | 
    
         
            -
             
     | 
| 
       75 
     | 
    
         
            -
                expect(ttl).to be_within(1).of(30)
         
     | 
| 
       76 
     | 
    
         
            -
              end
         
     | 
| 
       77 
     | 
    
         
            -
             
     | 
| 
       78 
     | 
    
         
            -
              it "allows nil expires_in (no expiry) if not required" do
         
     | 
| 
       79 
     | 
    
         
            -
                Mudis.write("baz", "no expiry")
         
     | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
                metadata = Mudis.inspect("baz")
         
     | 
| 
       82 
     | 
    
         
            -
                expect(metadata[:expires_at]).to be_nil
         
     | 
| 
       83 
     | 
    
         
            -
              end
         
     | 
| 
       84 
     | 
    
         
            -
            end
         
     | 
| 
       85 
     | 
    
         
            -
             
     | 
| 
       86 
     | 
    
         
            -
            RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
       87 
     | 
    
         
            -
              after { Mudis.reset! }
         
     | 
| 
       88 
     | 
    
         
            -
             
     | 
| 
       89 
     | 
    
         
            -
              describe "bucket configuration" do
         
     | 
| 
       90 
     | 
    
         
            -
                it "defaults to 32 buckets if ENV is nil" do
         
     | 
| 
       91 
     | 
    
         
            -
                  Mudis.instance_variable_set(:@buckets, nil) # force recomputation
         
     | 
| 
       92 
     | 
    
         
            -
                  ClimateControl.modify(MUDIS_BUCKETS: nil) do
         
     | 
| 
       93 
     | 
    
         
            -
                    expect(Mudis.send(:buckets)).to eq(32)
         
     | 
| 
       94 
     | 
    
         
            -
                  end
         
     | 
| 
       95 
     | 
    
         
            -
                end
         
     | 
| 
       96 
     | 
    
         
            -
             
     | 
| 
       97 
     | 
    
         
            -
                it "raises if MUDIS_BUCKETS is 0 or less" do
         
     | 
| 
       98 
     | 
    
         
            -
                  expect do
         
     | 
| 
       99 
     | 
    
         
            -
                    Mudis.instance_variable_set(:@buckets, nil) # force recomputation
         
     | 
| 
       100 
     | 
    
         
            -
                    ClimateControl.modify(MUDIS_BUCKETS: "0") { Mudis.send(:buckets) }
         
     | 
| 
       101 
     | 
    
         
            -
                  end.to raise_error(ArgumentError, /bucket count must be > 0/)
         
     | 
| 
       102 
     | 
    
         
            -
             
     | 
| 
       103 
     | 
    
         
            -
                  expect do
         
     | 
| 
       104 
     | 
    
         
            -
                    Mudis.instance_variable_set(:@buckets, nil) # force recomputation
         
     | 
| 
       105 
     | 
    
         
            -
                    ClimateControl.modify(MUDIS_BUCKETS: "-5") { Mudis.send(:buckets) }
         
     | 
| 
       106 
     | 
    
         
            -
                  end.to raise_error(ArgumentError, /bucket count must be > 0/)
         
     | 
| 
       107 
     | 
    
         
            -
                end
         
     | 
| 
       108 
     | 
    
         
            -
              end
         
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
              describe "memory configuration" do
         
     | 
| 
       111 
     | 
    
         
            -
                it "raises if max_bytes is set to 0 or less" do
         
     | 
| 
       112 
     | 
    
         
            -
                  expect do
         
     | 
| 
       113 
     | 
    
         
            -
                    Mudis.max_bytes = 0
         
     | 
| 
       114 
     | 
    
         
            -
                  end.to raise_error(ArgumentError, /max_bytes must be > 0/)
         
     | 
| 
       115 
     | 
    
         
            -
             
     | 
| 
       116 
     | 
    
         
            -
                  expect do
         
     | 
| 
       117 
     | 
    
         
            -
                    Mudis.max_bytes = -1
         
     | 
| 
       118 
     | 
    
         
            -
                  end.to raise_error(ArgumentError, /max_bytes must be > 0/)
         
     | 
| 
       119 
     | 
    
         
            -
                end
         
     | 
| 
       120 
     | 
    
         
            -
             
     | 
| 
       121 
     | 
    
         
            -
                it "raises if max_value_bytes is 0 or less via config" do
         
     | 
| 
       122 
     | 
    
         
            -
                  expect do
         
     | 
| 
       123 
     | 
    
         
            -
                    Mudis.configure do |c|
         
     | 
| 
       124 
     | 
    
         
            -
                      c.max_value_bytes = 0
         
     | 
| 
       125 
     | 
    
         
            -
                    end
         
     | 
| 
       126 
     | 
    
         
            -
                  end.to raise_error(ArgumentError, /max_value_bytes must be > 0/)
         
     | 
| 
       127 
     | 
    
         
            -
                end
         
     | 
| 
       128 
     | 
    
         
            -
             
     | 
| 
       129 
     | 
    
         
            -
                it "raises if max_value_bytes exceeds max_bytes" do
         
     | 
| 
       130 
     | 
    
         
            -
                  expect do
         
     | 
| 
       131 
     | 
    
         
            -
                    Mudis.configure do |c|
         
     | 
| 
       132 
     | 
    
         
            -
                      c.max_bytes = 1_000_000
         
     | 
| 
       133 
     | 
    
         
            -
                      c.max_value_bytes = 2_000_000
         
     | 
| 
       134 
     | 
    
         
            -
                    end
         
     | 
| 
       135 
     | 
    
         
            -
                  end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
         
     | 
| 
       136 
     | 
    
         
            -
                end
         
     | 
| 
       137 
     | 
    
         
            -
              end
         
     | 
| 
       138 
     | 
    
         
            -
            end
         
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "spec_helper"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "climate_control"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            RSpec.describe "Mudis TTL Guardrail" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
      
 7 
     | 
    
         
            +
              before do
         
     | 
| 
      
 8 
     | 
    
         
            +
                Mudis.reset!
         
     | 
| 
      
 9 
     | 
    
         
            +
                Mudis.configure do |c|
         
     | 
| 
      
 10 
     | 
    
         
            +
                  c.max_ttl = 60 # 60 seconds max
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
              end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
              describe "default_ttl configuration" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
      
 15 
     | 
    
         
            +
                before do
         
     | 
| 
      
 16 
     | 
    
         
            +
                  Mudis.reset!
         
     | 
| 
      
 17 
     | 
    
         
            +
                  Mudis.configure do |c|
         
     | 
| 
      
 18 
     | 
    
         
            +
                    c.default_ttl = 60
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                it "applies default_ttl when expires_in is nil" do
         
     | 
| 
      
 23 
     | 
    
         
            +
                  Mudis.write("foo", "bar") # no explicit expires_in
         
     | 
| 
      
 24 
     | 
    
         
            +
                  meta = Mudis.inspect("foo")
         
     | 
| 
      
 25 
     | 
    
         
            +
                  expect(meta[:expires_at]).not_to be_nil
         
     | 
| 
      
 26 
     | 
    
         
            +
                  expect(meta[:expires_at]).to be_within(5).of(Time.now + 60)
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                it "respects expires_in if explicitly given" do
         
     | 
| 
      
 30 
     | 
    
         
            +
                  Mudis.write("short_lived", "bar", expires_in: 10)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  meta = Mudis.inspect("short_lived")
         
     | 
| 
      
 32 
     | 
    
         
            +
                  expect(meta[:expires_at]).not_to be_nil
         
     | 
| 
      
 33 
     | 
    
         
            +
                  expect(meta[:expires_at]).to be_within(5).of(Time.now + 10)
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                it "applies max_ttl over default_ttl if both are set" do
         
     | 
| 
      
 37 
     | 
    
         
            +
                  Mudis.configure do |c|
         
     | 
| 
      
 38 
     | 
    
         
            +
                    c.default_ttl = 120
         
     | 
| 
      
 39 
     | 
    
         
            +
                    c.max_ttl = 30
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  Mudis.write("capped", "baz") # no explicit expires_in
         
     | 
| 
      
 43 
     | 
    
         
            +
                  meta = Mudis.inspect("capped")
         
     | 
| 
      
 44 
     | 
    
         
            +
                  expect(meta[:expires_at]).not_to be_nil
         
     | 
| 
      
 45 
     | 
    
         
            +
                  expect(meta[:expires_at]).to be_within(5).of(Time.now + 30)
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                it "stores forever if default_ttl and expires_in are nil" do
         
     | 
| 
      
 49 
     | 
    
         
            +
                  Mudis.configure do |c|
         
     | 
| 
      
 50 
     | 
    
         
            +
                    c.default_ttl = nil
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                  Mudis.write("forever", "ever")
         
     | 
| 
      
 54 
     | 
    
         
            +
                  meta = Mudis.inspect("forever")
         
     | 
| 
      
 55 
     | 
    
         
            +
                  expect(meta[:expires_at]).to be_nil
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
              end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
              it "clamps expires_in to max_ttl if it exceeds max_ttl" do
         
     | 
| 
      
 60 
     | 
    
         
            +
                Mudis.write("foo", "bar", expires_in: 300) # user requests 5 minutes
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                metadata = Mudis.inspect("foo")
         
     | 
| 
      
 63 
     | 
    
         
            +
                ttl = metadata[:expires_at] - metadata[:created_at]
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                expect(ttl).to be <= 60
         
     | 
| 
      
 66 
     | 
    
         
            +
                expect(ttl).to be > 0
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
              it "respects expires_in if below max_ttl" do
         
     | 
| 
      
 70 
     | 
    
         
            +
                Mudis.write("bar", "baz", expires_in: 30) # under the max_ttl
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                metadata = Mudis.inspect("bar")
         
     | 
| 
      
 73 
     | 
    
         
            +
                ttl = metadata[:expires_at] - metadata[:created_at]
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                expect(ttl).to be_within(1).of(30)
         
     | 
| 
      
 76 
     | 
    
         
            +
              end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
              it "allows nil expires_in (no expiry) if not required" do
         
     | 
| 
      
 79 
     | 
    
         
            +
                Mudis.write("baz", "no expiry")
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                metadata = Mudis.inspect("baz")
         
     | 
| 
      
 82 
     | 
    
         
            +
                expect(metadata[:expires_at]).to be_nil
         
     | 
| 
      
 83 
     | 
    
         
            +
              end
         
     | 
| 
      
 84 
     | 
    
         
            +
            end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            RSpec.describe "Mudis Configuration Guardrails" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
      
 87 
     | 
    
         
            +
              after { Mudis.reset! }
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
              describe "bucket configuration" do
         
     | 
| 
      
 90 
     | 
    
         
            +
                it "defaults to 32 buckets if ENV is nil" do
         
     | 
| 
      
 91 
     | 
    
         
            +
                  Mudis.instance_variable_set(:@buckets, nil) # force recomputation
         
     | 
| 
      
 92 
     | 
    
         
            +
                  ClimateControl.modify(MUDIS_BUCKETS: nil) do
         
     | 
| 
      
 93 
     | 
    
         
            +
                    expect(Mudis.send(:buckets)).to eq(32)
         
     | 
| 
      
 94 
     | 
    
         
            +
                  end
         
     | 
| 
      
 95 
     | 
    
         
            +
                end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                it "raises if MUDIS_BUCKETS is 0 or less" do
         
     | 
| 
      
 98 
     | 
    
         
            +
                  expect do
         
     | 
| 
      
 99 
     | 
    
         
            +
                    Mudis.instance_variable_set(:@buckets, nil) # force recomputation
         
     | 
| 
      
 100 
     | 
    
         
            +
                    ClimateControl.modify(MUDIS_BUCKETS: "0") { Mudis.send(:buckets) }
         
     | 
| 
      
 101 
     | 
    
         
            +
                  end.to raise_error(ArgumentError, /bucket count must be > 0/)
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                  expect do
         
     | 
| 
      
 104 
     | 
    
         
            +
                    Mudis.instance_variable_set(:@buckets, nil) # force recomputation
         
     | 
| 
      
 105 
     | 
    
         
            +
                    ClimateControl.modify(MUDIS_BUCKETS: "-5") { Mudis.send(:buckets) }
         
     | 
| 
      
 106 
     | 
    
         
            +
                  end.to raise_error(ArgumentError, /bucket count must be > 0/)
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
      
 108 
     | 
    
         
            +
              end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
              describe "memory configuration" do
         
     | 
| 
      
 111 
     | 
    
         
            +
                it "raises if max_bytes is set to 0 or less" do
         
     | 
| 
      
 112 
     | 
    
         
            +
                  expect do
         
     | 
| 
      
 113 
     | 
    
         
            +
                    Mudis.max_bytes = 0
         
     | 
| 
      
 114 
     | 
    
         
            +
                  end.to raise_error(ArgumentError, /max_bytes must be > 0/)
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                  expect do
         
     | 
| 
      
 117 
     | 
    
         
            +
                    Mudis.max_bytes = -1
         
     | 
| 
      
 118 
     | 
    
         
            +
                  end.to raise_error(ArgumentError, /max_bytes must be > 0/)
         
     | 
| 
      
 119 
     | 
    
         
            +
                end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                it "raises if max_value_bytes is 0 or less via config" do
         
     | 
| 
      
 122 
     | 
    
         
            +
                  expect do
         
     | 
| 
      
 123 
     | 
    
         
            +
                    Mudis.configure do |c|
         
     | 
| 
      
 124 
     | 
    
         
            +
                      c.max_value_bytes = 0
         
     | 
| 
      
 125 
     | 
    
         
            +
                    end
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end.to raise_error(ArgumentError, /max_value_bytes must be > 0/)
         
     | 
| 
      
 127 
     | 
    
         
            +
                end
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
                it "raises if max_value_bytes exceeds max_bytes" do
         
     | 
| 
      
 130 
     | 
    
         
            +
                  expect do
         
     | 
| 
      
 131 
     | 
    
         
            +
                    Mudis.configure do |c|
         
     | 
| 
      
 132 
     | 
    
         
            +
                      c.max_bytes = 1_000_000
         
     | 
| 
      
 133 
     | 
    
         
            +
                      c.max_value_bytes = 2_000_000
         
     | 
| 
      
 134 
     | 
    
         
            +
                    end
         
     | 
| 
      
 135 
     | 
    
         
            +
                  end.to raise_error(ArgumentError, /max_value_bytes cannot exceed max_bytes/)
         
     | 
| 
      
 136 
     | 
    
         
            +
                end
         
     | 
| 
      
 137 
     | 
    
         
            +
              end
         
     | 
| 
      
 138 
     | 
    
         
            +
            end
         
     | 
    
        data/spec/memory_guard_spec.rb
    CHANGED
    
    | 
         @@ -1,33 +1,33 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # frozen_string_literal: true
         
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
            require "spec_helper"
         
     | 
| 
       4 
     | 
    
         
            -
             
     | 
| 
       5 
     | 
    
         
            -
            RSpec.describe "Mudis Memory Guardrails" do
         
     | 
| 
       6 
     | 
    
         
            -
              before do
         
     | 
| 
       7 
     | 
    
         
            -
                Mudis.reset!
         
     | 
| 
       8 
     | 
    
         
            -
                Mudis.stop_expiry_thread
         
     | 
| 
       9 
     | 
    
         
            -
                Mudis.instance_variable_set(:@buckets, 1)
         
     | 
| 
       10 
     | 
    
         
            -
                Mudis.instance_variable_set(:@stores, [{}])
         
     | 
| 
       11 
     | 
    
         
            -
                Mudis.instance_variable_set(:@mutexes, [Mutex.new])
         
     | 
| 
       12 
     | 
    
         
            -
                Mudis.instance_variable_set(:@lru_heads, [nil])
         
     | 
| 
       13 
     | 
    
         
            -
                Mudis.instance_variable_set(:@lru_tails, [nil])
         
     | 
| 
       14 
     | 
    
         
            -
                Mudis.instance_variable_set(:@lru_nodes, [{}])
         
     | 
| 
       15 
     | 
    
         
            -
                Mudis.instance_variable_set(:@current_bytes, [0])
         
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
                Mudis.max_value_bytes = nil
         
     | 
| 
       18 
     | 
    
         
            -
                Mudis.instance_variable_set(:@threshold_bytes, 1_000_000)
         
     | 
| 
       19 
     | 
    
         
            -
                Mudis.hard_memory_limit = true
         
     | 
| 
       20 
     | 
    
         
            -
                Mudis.instance_variable_set(:@max_bytes, 100)
         
     | 
| 
       21 
     | 
    
         
            -
              end
         
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
              it "rejects writes that exceed max memory" do
         
     | 
| 
       24 
     | 
    
         
            -
                big_value = "a" * 90
         
     | 
| 
       25 
     | 
    
         
            -
                Mudis.write("a", big_value)
         
     | 
| 
       26 
     | 
    
         
            -
                expect(Mudis.read("a")).to eq(big_value)
         
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
                big_value2 = "b" * 90
         
     | 
| 
       29 
     | 
    
         
            -
                Mudis.write("b", big_value2)
         
     | 
| 
       30 
     | 
    
         
            -
                expect(Mudis.read("b")).to be_nil
         
     | 
| 
       31 
     | 
    
         
            -
                expect(Mudis.metrics[:rejected]).to be > 0
         
     | 
| 
       32 
     | 
    
         
            -
              end
         
     | 
| 
       33 
     | 
    
         
            -
            end
         
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "spec_helper"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            RSpec.describe "Mudis Memory Guardrails" do
         
     | 
| 
      
 6 
     | 
    
         
            +
              before do
         
     | 
| 
      
 7 
     | 
    
         
            +
                Mudis.reset!
         
     | 
| 
      
 8 
     | 
    
         
            +
                Mudis.stop_expiry_thread
         
     | 
| 
      
 9 
     | 
    
         
            +
                Mudis.instance_variable_set(:@buckets, 1)
         
     | 
| 
      
 10 
     | 
    
         
            +
                Mudis.instance_variable_set(:@stores, [{}])
         
     | 
| 
      
 11 
     | 
    
         
            +
                Mudis.instance_variable_set(:@mutexes, [Mutex.new])
         
     | 
| 
      
 12 
     | 
    
         
            +
                Mudis.instance_variable_set(:@lru_heads, [nil])
         
     | 
| 
      
 13 
     | 
    
         
            +
                Mudis.instance_variable_set(:@lru_tails, [nil])
         
     | 
| 
      
 14 
     | 
    
         
            +
                Mudis.instance_variable_set(:@lru_nodes, [{}])
         
     | 
| 
      
 15 
     | 
    
         
            +
                Mudis.instance_variable_set(:@current_bytes, [0])
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                Mudis.max_value_bytes = nil
         
     | 
| 
      
 18 
     | 
    
         
            +
                Mudis.instance_variable_set(:@threshold_bytes, 1_000_000)
         
     | 
| 
      
 19 
     | 
    
         
            +
                Mudis.hard_memory_limit = true
         
     | 
| 
      
 20 
     | 
    
         
            +
                Mudis.instance_variable_set(:@max_bytes, 100)
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              it "rejects writes that exceed max memory" do
         
     | 
| 
      
 24 
     | 
    
         
            +
                big_value = "a" * 90
         
     | 
| 
      
 25 
     | 
    
         
            +
                Mudis.write("a", big_value)
         
     | 
| 
      
 26 
     | 
    
         
            +
                expect(Mudis.read("a")).to eq(big_value)
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                big_value2 = "b" * 90
         
     | 
| 
      
 29 
     | 
    
         
            +
                Mudis.write("b", big_value2)
         
     | 
| 
      
 30 
     | 
    
         
            +
                expect(Mudis.read("b")).to be_nil
         
     | 
| 
      
 31 
     | 
    
         
            +
                expect(Mudis.metrics[:rejected]).to be > 0
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
            end
         
     | 
    
        data/spec/metrics_spec.rb
    CHANGED
    
    | 
         @@ -1,34 +1,34 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            # frozen_string_literal: true
         
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
            require_relative "spec_helper"
         
     | 
| 
       4 
     | 
    
         
            -
             
     | 
| 
       5 
     | 
    
         
            -
            RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
       6 
     | 
    
         
            -
              it "tracks hits and misses" do
         
     | 
| 
       7 
     | 
    
         
            -
                Mudis.write("hit_me", "value")
         
     | 
| 
       8 
     | 
    
         
            -
                Mudis.read("hit_me")
         
     | 
| 
       9 
     | 
    
         
            -
                Mudis.read("miss_me")
         
     | 
| 
       10 
     | 
    
         
            -
                metrics = Mudis.metrics
         
     | 
| 
       11 
     | 
    
         
            -
                expect(metrics[:hits]).to eq(1)
         
     | 
| 
       12 
     | 
    
         
            -
                expect(metrics[:misses]).to eq(1)
         
     | 
| 
       13 
     | 
    
         
            -
              end
         
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
              it "includes per-bucket stats" do
         
     | 
| 
       16 
     | 
    
         
            -
                Mudis.write("a", "x" * 50)
         
     | 
| 
       17 
     | 
    
         
            -
                metrics = Mudis.metrics
         
     | 
| 
       18 
     | 
    
         
            -
                expect(metrics).to include(:buckets)
         
     | 
| 
       19 
     | 
    
         
            -
                expect(metrics[:buckets]).to be_an(Array)
         
     | 
| 
       20 
     | 
    
         
            -
                expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
         
     | 
| 
       21 
     | 
    
         
            -
              end
         
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
              it "resets only the metrics without clearing cache" do
         
     | 
| 
       24 
     | 
    
         
            -
                Mudis.write("metrics_key", "value")
         
     | 
| 
       25 
     | 
    
         
            -
                Mudis.read("metrics_key")
         
     | 
| 
       26 
     | 
    
         
            -
                Mudis.read("missing_key")
         
     | 
| 
       27 
     | 
    
         
            -
                expect(Mudis.metrics[:hits]).to eq(1)
         
     | 
| 
       28 
     | 
    
         
            -
                expect(Mudis.metrics[:misses]).to eq(1)
         
     | 
| 
       29 
     | 
    
         
            -
                Mudis.reset_metrics!
         
     | 
| 
       30 
     | 
    
         
            -
                expect(Mudis.metrics[:hits]).to eq(0)
         
     | 
| 
       31 
     | 
    
         
            -
                expect(Mudis.metrics[:misses]).to eq(0)
         
     | 
| 
       32 
     | 
    
         
            -
                expect(Mudis.read("metrics_key")).to eq("value")
         
     | 
| 
       33 
     | 
    
         
            -
              end
         
     | 
| 
       34 
     | 
    
         
            -
            end
         
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "spec_helper"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            RSpec.describe "Mudis Metrics" do # rubocop:disable Metrics/BlockLength
         
     | 
| 
      
 6 
     | 
    
         
            +
              it "tracks hits and misses" do
         
     | 
| 
      
 7 
     | 
    
         
            +
                Mudis.write("hit_me", "value")
         
     | 
| 
      
 8 
     | 
    
         
            +
                Mudis.read("hit_me")
         
     | 
| 
      
 9 
     | 
    
         
            +
                Mudis.read("miss_me")
         
     | 
| 
      
 10 
     | 
    
         
            +
                metrics = Mudis.metrics
         
     | 
| 
      
 11 
     | 
    
         
            +
                expect(metrics[:hits]).to eq(1)
         
     | 
| 
      
 12 
     | 
    
         
            +
                expect(metrics[:misses]).to eq(1)
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              it "includes per-bucket stats" do
         
     | 
| 
      
 16 
     | 
    
         
            +
                Mudis.write("a", "x" * 50)
         
     | 
| 
      
 17 
     | 
    
         
            +
                metrics = Mudis.metrics
         
     | 
| 
      
 18 
     | 
    
         
            +
                expect(metrics).to include(:buckets)
         
     | 
| 
      
 19 
     | 
    
         
            +
                expect(metrics[:buckets]).to be_an(Array)
         
     | 
| 
      
 20 
     | 
    
         
            +
                expect(metrics[:buckets].first).to include(:index, :keys, :memory_bytes, :lru_size)
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              it "resets only the metrics without clearing cache" do
         
     | 
| 
      
 24 
     | 
    
         
            +
                Mudis.write("metrics_key", "value")
         
     | 
| 
      
 25 
     | 
    
         
            +
                Mudis.read("metrics_key")
         
     | 
| 
      
 26 
     | 
    
         
            +
                Mudis.read("missing_key")
         
     | 
| 
      
 27 
     | 
    
         
            +
                expect(Mudis.metrics[:hits]).to eq(1)
         
     | 
| 
      
 28 
     | 
    
         
            +
                expect(Mudis.metrics[:misses]).to eq(1)
         
     | 
| 
      
 29 
     | 
    
         
            +
                Mudis.reset_metrics!
         
     | 
| 
      
 30 
     | 
    
         
            +
                expect(Mudis.metrics[:hits]).to eq(0)
         
     | 
| 
      
 31 
     | 
    
         
            +
                expect(Mudis.metrics[:misses]).to eq(0)
         
     | 
| 
      
 32 
     | 
    
         
            +
                expect(Mudis.read("metrics_key")).to eq("value")
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
            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
         
     |