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 +4 -4
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +27 -0
- data/README.md +86 -33
- data/benchmark/README.md +47 -0
- data/benchmark/redis_namespace_comparison.rb +238 -0
- data/benchmark/results.md +90 -0
- data/lib/redis_client/namespace/command_builder.rb +18 -12
- data/lib/redis_client/namespace/middleware.rb +51 -0
- data/lib/redis_client/namespace/version.rb +1 -1
- data/lib/redis_client/namespace.rb +24 -11
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b936795d1c5a2100eccec3e4d0bda4f909dc2c7afa4c6d6585a690efe9d352b
|
4
|
+
data.tar.gz: b86791a8d99b72f3869ec42b1e3fa424b343fb63673abedf9b20d1c2685249ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
-
###
|
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
|
-
#
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
-
|
65
|
-
|
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
|
-
###
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
131
|
-
|
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
|
-
-
|
162
|
-
-
|
163
|
-
-
|
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
|
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).
|
data/benchmark/README.md
ADDED
@@ -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
|
444
|
-
command
|
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
|
-
|
453
|
-
|
451
|
+
warn("RedisClient::Namespace does not know how to handle '#{cmd_name}'.")
|
452
|
+
return command
|
454
453
|
end
|
455
454
|
|
456
|
-
|
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
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
"
|
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
|
@@ -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
|
-
#
|
11
|
-
#
|
12
|
-
#
|
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
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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
|
20
|
-
# builder = RedisClient::Namespace.new("myapp"
|
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
|
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.
|
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:
|