redis-client-namespace 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b42761a140eeca3fe1fc46feaf42636faafde11bec1aa8144f055c314686806
4
- data.tar.gz: 795aa14bf6b0616886b6231fedffd2f23c60901d452f204824a0d1523587a4b1
3
+ metadata.gz: 1b936795d1c5a2100eccec3e4d0bda4f909dc2c7afa4c6d6585a690efe9d352b
4
+ data.tar.gz: b86791a8d99b72f3869ec42b1e3fa424b343fb63673abedf9b20d1c2685249ed
5
5
  SHA512:
6
- metadata.gz: 02cc98329107c65ac11d1e851b277eea83cb520741c82e8d90f8275d4b943163fdc5a7b58806d73f7f4c68151b1b32437050b32b57f8432566a24ad339d95802
7
- data.tar.gz: a59cdff0ae554c69cd060a4bfab2062c0e088352b6ffe0afff082c2f8df1ad85ddd93be233588f9c1b61052bbdfd44e6ae0d0596e7652ad5f99a04b9e30a0821
6
+ metadata.gz: 5b18227306bb697a65ebd78583cc7ac6b1f87ca4281cc82d121f0df9299bfb10e237d56b9e70f31b6ecbcacf749b95f32049dc699c8f0be7bfe62e7b004fe1a5
7
+ data.tar.gz: 279dc732d7b2a9a692769040e0b6b8ef2de12dbab83638d550d1a4a7e5e47fd5b62cd007e2df8fa00e1f3a7fb8d2706450aa40b865944b966adb3048de1ecaa1
data/.rubocop.yml CHANGED
@@ -47,13 +47,16 @@ Naming/FileName:
47
47
  - 'lib/redis-client-namespace.rb'
48
48
 
49
49
  # Test helper method for processing Redis commands.json - complexity is justified
50
+ # Also exclude trimed_result method which handles multiple command types
50
51
  Metrics/AbcSize:
51
52
  Exclude:
52
53
  - 'spec/redis_client/namespace/command_builder_auto_spec.rb'
54
+ - 'lib/redis_client/namespace/command_builder.rb'
53
55
 
54
56
  Metrics/CyclomaticComplexity:
55
57
  Exclude:
56
58
  - 'spec/redis_client/namespace/command_builder_auto_spec.rb'
59
+ - 'lib/redis_client/namespace/command_builder.rb'
57
60
 
58
61
  Metrics/MethodLength:
59
62
  Exclude:
@@ -61,4 +64,5 @@ Metrics/MethodLength:
61
64
 
62
65
  Metrics/PerceivedComplexity:
63
66
  Exclude:
64
- - 'spec/redis_client/namespace/command_builder_auto_spec.rb'
67
+ - 'spec/redis_client/namespace/command_builder_auto_spec.rb'
68
+ - 'lib/redis_client/namespace/command_builder.rb'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-08-01
4
+
5
+ ### Added
6
+ - Added `RedisClient::Namespace::Middleware` for full namespace support including result processing
7
+ - Added automatic removal of namespace prefixes from command results (KEYS, SCAN, BLPOP, BRPOP)
8
+ - Middleware approach allows proper handling of both command transformation and result trimming
9
+ - Added comprehensive Sidekiq integration tests with Sidekiq::Launcher
10
+
11
+ ### Changed
12
+ - **BREAKING**: Middleware approach is now the recommended way to use RedisClient::Namespace
13
+ - Updated README to focus on middleware usage with comprehensive examples
14
+ - Updated Sidekiq integration examples to use middleware approach
15
+
16
+ ### Deprecated
17
+ - Using `RedisClient::Namespace` as a command_builder is now deprecated
18
+ - The command_builder approach cannot process results to remove namespace prefixes
19
+ - Users should migrate to the middleware approach for complete namespace functionality
20
+
21
+ ### Technical Changes
22
+ - Refactored `namespaced_command` method as a static method in `CommandBuilder` module
23
+ - Added `trimed_result` method for processing command results
24
+ - Enhanced middleware implementation to handle both single commands and pipelined operations
25
+
26
+ ## [0.1.1] - 2025-07-29
27
+
28
+ - Add `RedisClient::Namespace.command_builder` class method for conditional namespacing based on environment variables
29
+
3
30
  ## [0.1.0] - 2025-07-26
4
31
 
5
32
  - Initial release
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Redis namespace extension for [redis-client](https://github.com/redis-rb/redis-client) gem that automatically prefixes Redis keys with a namespace, enabling multi-tenancy and key isolation in Redis applications.
4
4
 
5
- This gem works by wrapping `RedisClient::CommandBuilder` and intercepting Redis commands to transparently add namespace prefixes to keys before they are sent to Redis.
5
+ This gem works by implementing a RedisClient middleware that intercepts Redis commands to transparently add namespace prefixes to keys before they are sent to Redis and removes them from results (with some limitations for certain commands like Pub/Sub events).
6
6
 
7
7
  ## Motivation
8
8
 
@@ -11,10 +11,7 @@ This gem was created to provide namespace support for [Sidekiq](https://github.c
11
11
  ## Features
12
12
 
13
13
  - **Transparent key namespacing**: Automatically prefixes Redis keys with a configurable namespace
14
- - **Comprehensive command support**: Supports all Redis commands with intelligent key detection
15
- - **Customizable separator**: Configure the namespace separator (default: `:`)
16
- - **Nested namespaces**: Support for nested command builders with multiple namespace levels
17
- - **Zero configuration**: Works out of the box with sensible defaults
14
+ - **Comprehensive command support**: Supports most Redis commands with intelligent key detection
18
15
  - **High performance**: Minimal overhead with efficient command transformation
19
16
  - **Thread-safe**: Safe for use in multi-threaded applications
20
17
 
@@ -40,42 +37,68 @@ gem install redis-client-namespace
40
37
 
41
38
  ## Usage
42
39
 
43
- ### Basic Usage
40
+ ### with RedisClient
41
+
42
+ RedisClient::Namespace is implemented as a [RedisClient middleware](https://github.com/redis-rb/redis-client#instrumentation-and-middlewares) that transparently handles both command transformation and result processing:
44
43
 
45
44
  ```ruby
46
45
  require 'redis-client-namespace'
47
46
 
48
- # Create a namespaced command builder
49
- namespace = RedisClient::Namespace.new("myapp")
50
-
51
- # Use with redis-client
52
- client = RedisClient.config(command_builder: namespace).new_client
47
+ # Configure RedisClient with the namespace middleware
48
+ client = RedisClient.config(
49
+ middlewares: [RedisClient::Namespace::Middleware],
50
+ custom: { namespace: "myapp" }
51
+ ).new_client
53
52
 
54
53
  # All commands will be automatically namespaced
55
54
  client.call("SET", "user:123", "john") # Actually sets "myapp:user:123"
56
55
  client.call("GET", "user:123") # Actually gets "myapp:user:123"
57
- client.call("DEL", "user:123", "user:456") # Actually deletes "myapp:user:123", "myapp:user:456"
56
+
57
+ # Commands that return keys will have the namespace automatically removed
58
+ client.call("KEYS", "*") # Returns ["user:123"] instead of ["myapp:user:123"]
59
+ client.call("SCAN", 0, "MATCH", "user:*") # Returns keys without namespace prefix
58
60
  ```
59
61
 
60
62
  ### Custom Separator
61
63
 
62
64
  ```ruby
63
65
  # Use a custom separator
64
- namespace = RedisClient::Namespace.new("myapp", separator: "-")
65
- client = RedisClient.config(command_builder: namespace).new_client
66
+ client = RedisClient.config(
67
+ middlewares: [RedisClient::Namespace::Middleware],
68
+ custom: { namespace: "myapp", separator: "-" }
69
+ ).new_client
66
70
 
67
71
  client.call("SET", "user:123", "john") # Actually sets "myapp-user:123"
68
72
  ```
69
73
 
70
- ### Nested Namespaces
74
+ ### with Redis (`redis-rb`)
75
+
76
+ This gem also works with the [redis](https://github.com/redis/redis-rb) gem through its middleware support:
71
77
 
72
78
  ```ruby
73
- # Create nested namespaces
74
- parent = RedisClient::Namespace.new("myapp")
75
- child = RedisClient::Namespace.new("jobs", parent_command_builder: parent)
79
+ require 'redis'
80
+ require 'redis-client-namespace'
81
+
82
+ # Use with redis-rb via middleware
83
+ redis = Redis.new(
84
+ host: 'localhost',
85
+ port: 6379,
86
+ middlewares: [RedisClient::Namespace::Middleware],
87
+ custom: { namespace: "myapp", separator: ":" }
88
+ )
76
89
 
77
- client = RedisClient.config(command_builder: child).new_client
78
- client.call("SET", "queue", "important") # Actually sets "jobs:myapp:queue"
90
+ # All commands will be automatically namespaced
91
+ redis.set("user:123", "john") # Actually sets "myapp:user:123"
92
+ redis.get("user:123") # Actually gets "myapp:user:123"
93
+ redis.del("user:123", "user:456") # Actually deletes "myapp:user:123", "myapp:user:456"
94
+
95
+ # Works with most Redis commands
96
+ redis.lpush("queue", ["job1", "job2"]) # Actually pushes to "myapp:queue"
97
+ redis.hset("config", "timeout", "30") # Actually sets in "myapp:config"
98
+ redis.sadd("tags", "ruby", "rails") # Actually adds to "myapp:tags"
99
+
100
+ # Pattern matching with automatic namespace removal
101
+ redis.keys("user:*") # Returns ["user:123"] instead of ["myapp:user:123"]
79
102
  ```
80
103
 
81
104
  ### Sidekiq Integration
@@ -86,19 +109,19 @@ This gem is particularly useful for Sidekiq applications that need namespace iso
86
109
  # In your Sidekiq configuration
87
110
  require 'redis-client-namespace'
88
111
 
89
- namespace = RedisClient::Namespace.new("sidekiq_production")
90
-
91
112
  Sidekiq.configure_server do |config|
92
113
  config.redis = {
93
114
  url: 'redis://redis:6379/1',
94
- command_builder: namespace,
115
+ middlewares: [RedisClient::Namespace::Middleware],
116
+ custom: { namespace: "sidekiq_production", separator: ":" }
95
117
  }
96
118
  end
97
119
 
98
120
  Sidekiq.configure_client do |config|
99
121
  config.redis = {
100
122
  url: 'redis://redis:6379/1',
101
- command_builder: namespace,
123
+ middlewares: [RedisClient::Namespace::Middleware],
124
+ custom: { namespace: "sidekiq_production", separator: ":" }
102
125
  }
103
126
  end
104
127
  ```
@@ -120,6 +143,32 @@ RedisClient::Namespace supports the vast majority of Redis commands with intelli
120
143
 
121
144
  The gem automatically detects which arguments are keys and applies the namespace prefix accordingly.
122
145
 
146
+ ### Limitations
147
+
148
+ #### Unknown Commands
149
+
150
+ Commands not explicitly supported by the gem will generate a warning and be passed through without namespace transformation. This ensures compatibility but means namespace isolation may not work for newer or less common Redis commands.
151
+
152
+ #### Pub/Sub Events
153
+
154
+ When using the Middleware approach, Pub/Sub subscribe events (via `pubsub.next_event`) are not automatically processed to remove namespace prefixes from channel names. This is because the middleware doesn't have access to intercept the `next_event` method.
155
+
156
+ If you're using Pub/Sub with the middleware approach, you'll need to manually handle namespace removal:
157
+
158
+ ```ruby
159
+ # With middleware approach
160
+ pubsub = client.pubsub
161
+ pubsub.call("SUBSCRIBE", "channel1") # Subscribes to "myapp:channel1"
162
+
163
+ # You need to manually remove the namespace prefix from received events
164
+ event = pubsub.next_event
165
+ if event && event[0] == "message"
166
+ channel = event[1].delete_prefix("myapp:") # Remove namespace manually
167
+ message = event[2]
168
+ end
169
+ ```
170
+
171
+
123
172
  ## Advanced Features
124
173
 
125
174
  ### Pattern Matching
@@ -127,11 +176,14 @@ The gem automatically detects which arguments are keys and applies the namespace
127
176
  For commands like `SCAN` and `KEYS`, the namespace is automatically applied to patterns:
128
177
 
129
178
  ```ruby
130
- namespace = RedisClient::Namespace.new("myapp")
131
- client = RedisClient.config(command_builder: namespace).new_client
179
+ client = RedisClient.config(
180
+ middlewares: [RedisClient::Namespace::Middleware],
181
+ custom: { namespace: "myapp", separator: ":" }
182
+ ).new_client
132
183
 
133
184
  # This will scan for "myapp:user:*" pattern
134
185
  client.call("SCAN", 0, "MATCH", "user:*")
186
+ # Returns keys without the namespace prefix for easier handling
135
187
  ```
136
188
 
137
189
  ### Complex Commands
@@ -150,20 +202,21 @@ client.call("EVAL", "return redis.call('get', KEYS[1])", 1, "mykey")
150
202
 
151
203
  ## Configuration Options
152
204
 
205
+ When using the middleware approach, configure via the `custom` option:
206
+
153
207
  - `namespace`: The namespace prefix to use (required)
154
208
  - `separator`: The separator between namespace and key (default: `":"`)
155
- - `parent_command_builder`: Parent command builder for nested namespaces (default: `RedisClient::CommandBuilder`)
156
209
 
157
210
  ## Thread Safety
158
211
 
159
- RedisClient::Namespace is **thread-safe** and can be used in multi-threaded applications without additional synchronization. The implementation:
212
+ RedisClient::Namespace is **thread-safe** and can be used in multi-threaded applications without additional synchronization. The middleware implementation:
160
213
 
161
- - Uses immutable instance variables (`@namespace`, `@separator`, `@parent_command_builder`) that are set once during initialization
162
- - Never modifies shared state during command processing
163
- - Creates new command arrays for each operation without mutating the original
214
+ - Reads configuration from `redis_config.custom` without maintaining any state
215
+ - Uses class methods in `CommandBuilder` that don't modify shared state
216
+ - Each call receives its own command array from RedisClient, avoiding shared mutable state
164
217
  - Uses frozen constants for strategy and command mappings
165
218
 
166
- Each `generate` call is completely independent, making it safe to use the same namespace instance across multiple threads.
219
+ Each middleware call is completely independent, making it safe to use the same middleware across multiple threads and connections.
167
220
 
168
221
  ## Performance
169
222
 
@@ -212,4 +265,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
212
265
 
213
266
  ## Code of Conduct
214
267
 
215
- Everyone interacting in the RedisClient::Namespace project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ken39arg/redis-client-namespace/blob/main/CODE_OF_CONDUCT.md).
268
+ Everyone interacting in the RedisClient::Namespace project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ken39arg/redis-client-namespace/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,47 @@
1
+ # Benchmarks
2
+
3
+ This directory contains performance benchmarks comparing `redis-client-namespace` with the traditional `redis-namespace` gem.
4
+
5
+ ## Running Benchmarks
6
+
7
+ ### Prerequisites
8
+
9
+ First, install the benchmark dependencies:
10
+
11
+ ```bash
12
+ bundle install --with benchmark
13
+ ```
14
+
15
+ ### Redis Namespace Comparison
16
+
17
+ To run the benchmark comparing `redis-namespace` vs `redis-client-namespace`:
18
+
19
+ ```bash
20
+ # Using default Redis (localhost:6379)
21
+ ruby benchmark/redis_namespace_comparison.rb
22
+
23
+ # Using custom Redis server
24
+ REDIS_HOST=localhost REDIS_PORT=16379 ruby benchmark/redis_namespace_comparison.rb
25
+ ```
26
+
27
+ This benchmark tests various Redis operations:
28
+ - Single key operations (SET, GET)
29
+ - Multiple key operations (MGET)
30
+ - List operations (LPUSH, LRANGE)
31
+ - Hash operations (HSET, HGETALL)
32
+ - Set operations (SADD, SMEMBERS)
33
+ - Pattern matching (KEYS)
34
+ - Complex operations (ZRANGE)
35
+ - Transactions (MULTI/EXEC)
36
+
37
+ ## Expected Results
38
+
39
+ The `redis-client-namespace` gem is designed to have minimal overhead compared to `redis-namespace`. The benchmarks help verify that the performance characteristics are comparable or better across different types of Redis operations.
40
+
41
+ ## Adding New Benchmarks
42
+
43
+ To add new benchmarks:
44
+ 1. Create a new Ruby file in this directory
45
+ 2. Use `benchmark-ips` for consistent measurement
46
+ 3. Include warm-up phases to ensure fair comparison
47
+ 4. Test a variety of Redis operations relevant to your use case
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "benchmark/ips"
6
+ require "redis"
7
+ require "redis-namespace"
8
+ require "redis-client-namespace"
9
+
10
+ # Setup Redis connections
11
+ REDIS_HOST = ENV.fetch("REDIS_HOST", "127.0.0.1")
12
+ REDIS_PORT = ENV.fetch("REDIS_PORT", "6379")
13
+ REDIS_DB = ENV.fetch("REDIS_DB", "0")
14
+
15
+ puts "Redis Namespace Benchmark: redis-namespace vs redis-client-namespace"
16
+ puts "=" * 70
17
+ puts "Testing with redis-rb (Redis.new)"
18
+ puts "Redis: #{REDIS_HOST}:#{REDIS_PORT}/#{REDIS_DB}"
19
+ puts "=" * 70
20
+
21
+ # Create Redis clients
22
+ plain_redis = Redis.new(host: REDIS_HOST, port: REDIS_PORT, db: REDIS_DB)
23
+
24
+ # redis-namespace (traditional approach)
25
+ redis_namespace = Redis::Namespace.new("bench_old", redis: plain_redis)
26
+
27
+ # redis-client-namespace (new approach)
28
+ redis_client_namespace = Redis.new(
29
+ host: REDIS_HOST,
30
+ port: REDIS_PORT,
31
+ db: REDIS_DB,
32
+ middlewares: [RedisClient::Namespace::Middleware],
33
+ custom: { namespace: "bench_new" }
34
+ )
35
+
36
+ # Clean up before benchmark
37
+ plain_redis.flushdb
38
+
39
+ # Warm up
40
+ redis_namespace.set("warmup", "value")
41
+ redis_client_namespace.set("warmup", "value")
42
+ redis_namespace.get("warmup")
43
+ redis_client_namespace.get("warmup")
44
+
45
+ puts "\n## Single Key Operations"
46
+ puts
47
+
48
+ Benchmark.ips do |x|
49
+ x.report("redis-namespace SET") do
50
+ redis_namespace.set("key", "value")
51
+ end
52
+
53
+ x.report("redis-client-namespace SET") do
54
+ redis_client_namespace.set("key", "value")
55
+ end
56
+
57
+ x.compare!
58
+ end
59
+
60
+ Benchmark.ips do |x|
61
+ x.report("redis-namespace GET") do
62
+ redis_namespace.get("key")
63
+ end
64
+
65
+ x.report("redis-client-namespace GET") do
66
+ redis_client_namespace.get("key")
67
+ end
68
+
69
+ x.compare!
70
+ end
71
+
72
+ puts "\n## Multiple Key Operations"
73
+ puts
74
+
75
+ # Prepare data for MGET
76
+ 10.times { |i| redis_namespace.set("mkey#{i}", "value#{i}") }
77
+ 10.times { |i| redis_client_namespace.set("mkey#{i}", "value#{i}") }
78
+
79
+ keys = (0...10).map { |i| "mkey#{i}" }
80
+
81
+ Benchmark.ips do |x|
82
+ x.report("redis-namespace MGET") do
83
+ redis_namespace.mget(*keys)
84
+ end
85
+
86
+ x.report("redis-client-namespace MGET") do
87
+ redis_client_namespace.mget(*keys)
88
+ end
89
+
90
+ x.compare!
91
+ end
92
+
93
+ puts "\n## List Operations"
94
+ puts
95
+
96
+ Benchmark.ips do |x|
97
+ x.report("redis-namespace LPUSH") do
98
+ redis_namespace.lpush("list", %w[item1 item2])
99
+ end
100
+
101
+ x.report("redis-client-namespace LPUSH") do
102
+ redis_client_namespace.lpush("list", %w[item1 item2])
103
+ end
104
+
105
+ x.compare!
106
+ end
107
+
108
+ Benchmark.ips do |x|
109
+ x.report("redis-namespace LRANGE") do
110
+ redis_namespace.lrange("list", 0, -1)
111
+ end
112
+
113
+ x.report("redis-client-namespace LRANGE") do
114
+ redis_client_namespace.lrange("list", 0, -1)
115
+ end
116
+
117
+ x.compare!
118
+ end
119
+
120
+ puts "\n## Hash Operations"
121
+ puts
122
+
123
+ Benchmark.ips do |x|
124
+ x.report("redis-namespace HSET") do
125
+ redis_namespace.hset("hash", "field1", "value1", "field2", "value2")
126
+ end
127
+
128
+ x.report("redis-client-namespace HSET") do
129
+ redis_client_namespace.hset("hash", "field1", "value1", "field2", "value2")
130
+ end
131
+
132
+ x.compare!
133
+ end
134
+
135
+ Benchmark.ips do |x|
136
+ x.report("redis-namespace HGETALL") do
137
+ redis_namespace.hgetall("hash")
138
+ end
139
+
140
+ x.report("redis-client-namespace HGETALL") do
141
+ redis_client_namespace.hgetall("hash")
142
+ end
143
+
144
+ x.compare!
145
+ end
146
+
147
+ puts "\n## Set Operations"
148
+ puts
149
+
150
+ Benchmark.ips do |x|
151
+ x.report("redis-namespace SADD") do
152
+ redis_namespace.sadd("set", %w[member1 member2])
153
+ end
154
+
155
+ x.report("redis-client-namespace SADD") do
156
+ redis_client_namespace.sadd("set", %w[member1 member2])
157
+ end
158
+
159
+ x.compare!
160
+ end
161
+
162
+ Benchmark.ips do |x|
163
+ x.report("redis-namespace SMEMBERS") do
164
+ redis_namespace.smembers("set")
165
+ end
166
+
167
+ x.report("redis-client-namespace SMEMBERS") do
168
+ redis_client_namespace.smembers("set")
169
+ end
170
+
171
+ x.compare!
172
+ end
173
+
174
+ puts "\n## Pattern Matching"
175
+ puts
176
+
177
+ # Create some keys for pattern matching
178
+ 10.times { |i| redis_namespace.set("user:#{i}", "value#{i}") }
179
+ 10.times { |i| redis_client_namespace.set("user:#{i}", "value#{i}") }
180
+
181
+ Benchmark.ips do |x|
182
+ x.report("redis-namespace KEYS") do
183
+ redis_namespace.keys("user:*")
184
+ end
185
+
186
+ x.report("redis-client-namespace KEYS") do
187
+ redis_client_namespace.keys("user:*")
188
+ end
189
+
190
+ x.compare!
191
+ end
192
+
193
+ puts "\n## Complex Operations"
194
+ puts
195
+
196
+ # Prepare sorted set data
197
+ redis_namespace.zadd("zset", [[1, "member1"], [2, "member2"], [3, "member3"]])
198
+ redis_client_namespace.zadd("zset", [[1, "member1"], [2, "member2"], [3, "member3"]])
199
+
200
+ Benchmark.ips do |x|
201
+ x.report("redis-namespace ZRANGE") do
202
+ redis_namespace.zrange("zset", 0, -1)
203
+ end
204
+
205
+ x.report("redis-client-namespace ZRANGE") do
206
+ redis_client_namespace.zrange("zset", 0, -1)
207
+ end
208
+
209
+ x.compare!
210
+ end
211
+
212
+ # Transactions
213
+ puts "\n## Transactions"
214
+ puts
215
+
216
+ Benchmark.ips do |x|
217
+ x.report("redis-namespace MULTI") do
218
+ redis_namespace.multi do |r|
219
+ r.set("tx_key1", "value1")
220
+ r.set("tx_key2", "value2")
221
+ end
222
+ end
223
+
224
+ x.report("redis-client-namespace MULTI") do
225
+ redis_client_namespace.multi do |r|
226
+ r.set("tx_key1", "value1")
227
+ r.set("tx_key2", "value2")
228
+ end
229
+ end
230
+
231
+ x.compare!
232
+ end
233
+
234
+ # Clean up
235
+ plain_redis.flushdb
236
+
237
+ puts "\n#{"=" * 70}"
238
+ puts "Benchmark completed!"
@@ -0,0 +1,90 @@
1
+ # Benchmark Results: redis-namespace vs redis-client-namespace
2
+
3
+ Date: 2025-07-31 (Updated with middleware implementation)
4
+
5
+ ## Test Environment
6
+
7
+ - Ruby: 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [arm64-darwin24]
8
+ - Redis: 127.0.0.1:16379
9
+ - Library versions:
10
+ - redis-namespace: 1.11.0
11
+ - redis-client-namespace: (current development version)
12
+
13
+ ## Summary
14
+
15
+ The benchmarks show that `redis-client-namespace` with the new middleware-based architecture performs excellently compared to `redis-namespace`. The middleware implementation has achieved performance parity or slight improvements across all tested operations, with performance differences consistently falling within the margin of error. This demonstrates that the middleware pattern successfully maintains high performance while providing cleaner architecture.
16
+
17
+ ## Detailed Results
18
+
19
+ ### Single Key Operations
20
+
21
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
22
+ |-----------|----------------|----------------------|------------|
23
+ | SET | 4,153.9 i/s | 4,206.5 i/s | Same-ish (within error) |
24
+ | GET | 4,213.9 i/s | 4,283.6 i/s | Same-ish (within error) |
25
+
26
+ ### Multiple Key Operations
27
+
28
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
29
+ |-----------|----------------|----------------------|------------|
30
+ | MGET (10 keys) | 3,993.6 i/s | 4,011.5 i/s | Same-ish (within error) |
31
+
32
+ ### List Operations
33
+
34
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
35
+ |-----------|----------------|----------------------|------------|
36
+ | LPUSH | 4,004.2 i/s | 4,042.7 i/s | Same-ish (within error) |
37
+ | LRANGE | 52.5 i/s | 53.0 i/s | Same-ish (within error) |
38
+
39
+ ### Hash Operations
40
+
41
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
42
+ |-----------|----------------|----------------------|------------|
43
+ | HSET | 4,178.2 i/s | 4,149.7 i/s | Same-ish (within error) |
44
+ | HGETALL | 4,147.0 i/s | 4,090.2 i/s | Same-ish (within error) |
45
+
46
+ ### Set Operations
47
+
48
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
49
+ |-----------|----------------|----------------------|------------|
50
+ | SADD | 3,958.2 i/s | 4,048.4 i/s | Same-ish (within error) |
51
+ | SMEMBERS | 4,129.4 i/s | 4,200.5 i/s | Same-ish (within error) |
52
+
53
+ ### Pattern Matching
54
+
55
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
56
+ |-----------|----------------|----------------------|------------|
57
+ | KEYS | 3,911.4 i/s | 4,049.3 i/s | Same-ish (within error) |
58
+
59
+ ### Complex Operations
60
+
61
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
62
+ |-----------|----------------|----------------------|------------|
63
+ | ZRANGE | 4,215.8 i/s | 4,261.9 i/s | Same-ish (within error) |
64
+
65
+ ### Transactions
66
+
67
+ | Operation | redis-namespace | redis-client-namespace | Comparison |
68
+ |-----------|----------------|----------------------|------------|
69
+ | MULTI/EXEC | 3,941.1 i/s | 4,022.1 i/s | Same-ish (within error) |
70
+
71
+ ## Key Observations
72
+
73
+ 1. **Excellent Performance**: The middleware-based `redis-client-namespace` shows consistently strong performance, often matching or slightly exceeding `redis-namespace` performance across all operations.
74
+
75
+ 2. **Middleware Architecture Success**: The new middleware pattern successfully maintains high performance while providing cleaner, more maintainable code architecture. The abstraction layer introduces virtually no performance penalty.
76
+
77
+ 3. **Significant Performance Improvements**: Compared to previous benchmarks, the middleware implementation shows substantial improvements - throughput has roughly doubled across most operations (from ~2k i/s to ~4k i/s range).
78
+
79
+ 4. **Consistent High Performance**: Both libraries now operate in the 4,000+ i/s range for most operations, demonstrating excellent performance characteristics across different Redis operation types.
80
+
81
+ 5. **Production Ready**: The benchmark results confirm that the middleware-based `redis-client-namespace` is highly suitable for production use, offering both superior architecture and excellent performance.
82
+
83
+ ## Notes
84
+
85
+ - All comparisons marked as "same-ish" indicate that the performance difference falls within the statistical margin of error
86
+ - The benchmarks used `benchmark-ips` for accurate measurements with proper warm-up periods
87
+ - Each operation was warmed up before measurement to ensure fair comparison
88
+ - LRANGE operations show lower throughput (~53 i/s) due to the large amount of data being transferred, but this is expected behavior
89
+ - The middleware implementation demonstrates that architectural improvements don't require performance sacrifices
90
+ - Performance improvements may also be attributed to Ruby 3.4.5 optimizations and updated testing environment
@@ -440,30 +440,36 @@ class RedisClient
440
440
 
441
441
  }.freeze
442
442
 
443
- def generate(args, kwargs = nil)
444
- command = @parent_command_builder.generate(args, kwargs)
445
- return command if @namespace.nil? || @namespace.empty? || command.size < 2
443
+ def self.namespaced_command(command, namespace: nil, separator: ":")
444
+ return command if namespace.nil? || namespace.empty? || command.size < 2
446
445
 
447
446
  cmd_name = command[0].to_s.upcase
448
447
  strategy = COMMANDS[cmd_name]
449
448
 
450
449
  # Raise error for unknown commands to maintain compatibility with redis-namespace
451
450
  unless strategy
452
- raise(::RedisClient::Namespace::Error,
453
- "RedisClient::Namespace does not know how to handle '#{cmd_name}'.")
451
+ warn("RedisClient::Namespace does not know how to handle '#{cmd_name}'.")
452
+ return command
454
453
  end
455
454
 
456
- STRATEGIES[strategy].call(command) { |key| rename_key(key) }
455
+ prefix = "#{namespace}#{separator}"
456
+ STRATEGIES[strategy].call(command) { |key| key.start_with?(prefix) ? key : "#{prefix}#{key}" }
457
457
 
458
458
  command
459
459
  end
460
460
 
461
- private
462
-
463
- def rename_key(key)
464
- return key if @namespace.nil? || @namespace.empty?
465
-
466
- "#{@namespace}#{@separator}#{key}"
461
+ def self.trimed_result(command, result, namespace: nil, separator: ":")
462
+ return command if namespace.nil? || namespace.empty? || command.size < 2
463
+
464
+ prefix = "#{namespace}#{separator}"
465
+ case command[0].to_s.upcase
466
+ when "SCAN"
467
+ result[1].map { |r| r.delete_prefix!(prefix) } if result.size > 1
468
+ when "KEYS"
469
+ result.map { |r| r.delete_prefix!(prefix) }
470
+ when "BLPOP", "BRPOP"
471
+ result[0].delete_prefix!(prefix) unless result.nil? || result.empty?
472
+ end
467
473
  end
468
474
  end
469
475
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command_builder"
4
+
5
+ class RedisClient
6
+ class Namespace
7
+ # Middleware for RedisClient to add namespace support
8
+ #
9
+ # This module implements the RedisClient middleware interface to intercept
10
+ # Redis commands and apply namespace transformations. It automatically prefixes
11
+ # keys with a namespace and removes the prefix from certain command results.
12
+ #
13
+ # @see https://github.com/redis-rb/redis-client/blob/master/README.md#instrumentation-and-middlewares
14
+ #
15
+ # @example Basic usage with RedisClient
16
+ # client = RedisClient.config(
17
+ # middlewares: [RedisClient::Namespace::Middleware],
18
+ # custom: { namespace: "myapp", separator: ":" }
19
+ # ).new_client
20
+ #
21
+ # client.call("SET", "key", "value") # Actually sets "myapp:key"
22
+ # client.call("GET", "key") # Gets "myapp:key" and returns "value"
23
+ #
24
+ # The middleware requires the following custom configuration:
25
+ # - namespace: The namespace prefix to apply (required)
26
+ # - separator: The separator between namespace and key (optional, default: ":")
27
+ module Middleware
28
+ def call(command, redis_config)
29
+ namespace = redis_config.custom[:namespace] or return super
30
+ separator = redis_config.custom[:separator] || ":"
31
+ command = CommandBuilder.namespaced_command(command, namespace: namespace, separator: separator)
32
+ super.tap do |result|
33
+ CommandBuilder.trimed_result(command, result, namespace: namespace, separator: separator)
34
+ end
35
+ end
36
+
37
+ def call_pipelined(commands, redis_config)
38
+ namespace = redis_config.custom[:namespace] or return super
39
+ separator = redis_config.custom[:separator] || ":"
40
+ commands = commands.map do |cmd|
41
+ CommandBuilder.namespaced_command(cmd, namespace: namespace, separator: separator)
42
+ end
43
+ super.tap do |results|
44
+ commands.each_with_index do |command, i|
45
+ CommandBuilder.trimed_result(command, results[i], namespace: namespace, separator: separator)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  class RedisClient
4
4
  class Namespace
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -3,26 +3,34 @@
3
3
  require "redis-client"
4
4
  require_relative "namespace/version"
5
5
  require_relative "namespace/command_builder"
6
+ require_relative "namespace/middleware"
6
7
 
7
8
  class RedisClient
8
9
  # RedisClient::Namespace provides transparent key namespacing for redis-client.
9
10
  #
10
- # It works by intercepting Redis commands and prefixing keys with a namespace,
11
- # allowing multiple applications or components to share a single Redis instance
12
- # without key collisions.
11
+ # **DEPRECATED**: Using this class as a command_builder is deprecated.
12
+ # Please use RedisClient::Namespace::Middleware instead for full namespace support
13
+ # including automatic removal of namespace prefixes from command results.
13
14
  #
14
- # @example Basic usage
15
- # builder = RedisClient::Namespace.new("myapp")
16
- # client = RedisClient.new(command_builder: builder)
15
+ # The command_builder approach only transforms outgoing commands but cannot
16
+ # process incoming results to remove namespace prefixes from keys returned by
17
+ # commands like KEYS, SCAN, BLPOP, etc.
18
+ #
19
+ # @deprecated Use {RedisClient::Namespace::Middleware} instead
20
+ # @example Recommended middleware approach
21
+ # client = RedisClient.config(
22
+ # middlewares: [RedisClient::Namespace::Middleware],
23
+ # custom: { namespace: "myapp", separator: ":" }
24
+ # ).new_client
17
25
  # client.call("SET", "key", "value") # Actually sets "myapp:key"
26
+ # client.call("KEYS", "*") # Returns ["key"] instead of ["myapp:key"]
18
27
  #
19
- # @example Custom separator
20
- # builder = RedisClient::Namespace.new("myapp", separator: "-")
28
+ # @example Legacy command_builder usage (not recommended)
29
+ # builder = RedisClient::Namespace.new("myapp")
21
30
  # client = RedisClient.new(command_builder: builder)
22
- # client.call("SET", "key", "value") # Actually sets "myapp-key"
31
+ # client.call("SET", "key", "value") # Actually sets "myapp:key"
32
+ # client.call("KEYS", "*") # Returns ["myapp:key"] - namespace not removed
23
33
  class Namespace
24
- include RedisClient::Namespace::CommandBuilder
25
-
26
34
  class Error < StandardError; end
27
35
 
28
36
  attr_reader :namespace, :separator, :parent_command_builder
@@ -32,5 +40,10 @@ class RedisClient
32
40
  @separator = separator
33
41
  @parent_command_builder = parent_command_builder
34
42
  end
43
+
44
+ def generate(args, kwargs = nil)
45
+ CommandBuilder.namespaced_command(@parent_command_builder.generate(args, kwargs), namespace: @namespace,
46
+ separator: @separator)
47
+ end
35
48
  end
36
49
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-client-namespace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kensaku Araga
@@ -38,10 +38,14 @@ files:
38
38
  - LICENSE.txt
39
39
  - README.md
40
40
  - Rakefile
41
+ - benchmark/README.md
42
+ - benchmark/redis_namespace_comparison.rb
43
+ - benchmark/results.md
41
44
  - compose.yml
42
45
  - lib/redis-client-namespace.rb
43
46
  - lib/redis_client/namespace.rb
44
47
  - lib/redis_client/namespace/command_builder.rb
48
+ - lib/redis_client/namespace/middleware.rb
45
49
  - lib/redis_client/namespace/version.rb
46
50
  homepage: https://github.com/ken39arg/redis-client-namespace
47
51
  licenses: