redisbetween 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 749198c4742de0123a0226e37853bba82f50b7effb9ccb570b421ec1a78d68c3
4
+ data.tar.gz: 1eb3ac3e7f9c40b76154bee60091b6a13d01a4a2627f157014c8614cf2764040
5
+ SHA512:
6
+ metadata.gz: 3e6cb4c970d63130e51191eb450de90078032d05e9086e888aed7da4f99597676eef5fa5a6c7bb8f7b5a0cfdf60b4d96ef06e4590d0ca572485252538408f96d
7
+ data.tar.gz: 601805fa28010afd16c3cb49859f268fcd4ce74cfb49cbec013d09f0b5990cebfe98a4cb74052d0214e5fdba87f15ea7b03e76915949566fe34101bf22594691
@@ -0,0 +1,3 @@
1
+ module Redisbetween
2
+ VERSION = "0.2.2"
3
+ end
@@ -0,0 +1,110 @@
1
+ require 'redisbetween/version'
2
+ require 'redis'
3
+ require 'uri'
4
+ require 'set'
5
+
6
+ module Redisbetween
7
+ class Error < StandardError; end
8
+
9
+ PIPELINE_START_SIGNAL = '🔜'
10
+ PIPELINE_END_SIGNAL = '🔚'
11
+
12
+ module ClientPatch
13
+ attr_reader :redisbetween_enabled
14
+
15
+ def initialize(options = {})
16
+ @redisbetween_enabled = !!options[:convert_to_redisbetween_socket]
17
+ @handle_unsupported_redisbetween_command = options[:handle_unsupported_redisbetween_command]
18
+ if @redisbetween_enabled
19
+ @handle_unsupported_redisbetween_command ||= ->(cmd) { puts "redisbetween: unsupported #{cmd}" }
20
+ end
21
+
22
+ if redisbetween_enabled
23
+ if options[:url]
24
+ u = URI(options[:url])
25
+ if u.scheme != 'unix'
26
+ path = u.path.empty? ? nil : u.path.delete_prefix('/')
27
+ u.path = Redisbetween.socket_path(options[:convert_to_redisbetween_socket], u.host, u.port, path)
28
+ u.host = nil
29
+ u.port = nil
30
+ u.scheme = 'unix'
31
+ options[:url] = u.to_s
32
+ end
33
+ elsif options[:host] && options[:port] && options[:scheme] != 'unix'
34
+ path = Redisbetween.socket_path(options[:convert_to_redisbetween_socket], options[:host], options[:port])
35
+ [:port, :host, :scheme].each { |k| options[k] = nil }
36
+ options[:url] = "unix:#{path}"
37
+ end
38
+ end
39
+ super(options)
40
+ end
41
+
42
+ UNSUPPORTED_COMMANDS = Set.new([
43
+ :auth,
44
+ :blpop,
45
+ :brpop,
46
+ :bzpopmax,
47
+ :bzpopmin,
48
+ :select,
49
+ :wait,
50
+ :xread,
51
+ :xreadgroup,
52
+ ])
53
+
54
+ def process(commands)
55
+ @handle_unsupported_redisbetween_command&.call("multi without a block") if commands == [[:multi]]
56
+
57
+ logging(commands) do
58
+ ensure_connected do
59
+ wrap = commands.size > 1 && redisbetween_enabled
60
+
61
+ _rb_wrapped_write(wrap) do
62
+ commands.each do |command|
63
+ if UNSUPPORTED_COMMANDS.member?(command.first)
64
+ @handle_unsupported_redisbetween_command&.call(command.first.to_s)
65
+ end
66
+ if command_map[command.first]
67
+ command = command.dup
68
+ command[0] = command_map[command.first]
69
+ end
70
+
71
+ write(command)
72
+ end
73
+ end
74
+
75
+ _rb_wrapped_read(wrap) do
76
+ yield if block_given?
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def _rb_wrapped_write(wrap)
83
+ write([:get, PIPELINE_START_SIGNAL]) if wrap
84
+ yield
85
+ write([:get, PIPELINE_END_SIGNAL]) if wrap
86
+ end
87
+
88
+ # the proxy sends back nil values as placeholders for the signals, so discard them
89
+ def _rb_wrapped_read(wrap)
90
+ read if wrap
91
+ res = yield
92
+ read if wrap
93
+ res
94
+ end
95
+ end
96
+
97
+ def self.socket_path(option, host, port, path = nil)
98
+ if option.respond_to?(:call)
99
+ option.call(host, port, path)
100
+ else
101
+ default_socket_path(host, port, path)
102
+ end
103
+ end
104
+
105
+ def self.default_socket_path(host, port, path = nil)
106
+ ['/var/tmp/redisbetween', host, port, path].compact.join('-') + '.sock'
107
+ end
108
+ end
109
+
110
+ Redis::Client.prepend(Redisbetween::ClientPatch)
@@ -0,0 +1,205 @@
1
+ RSpec.describe Redisbetween do
2
+ def test_logger(stream, level = Logger::DEBUG)
3
+ logger = Logger.new(stream)
4
+ logger.level = level
5
+ logger.formatter = proc { |_, _, _, msg| msg.sub("[Redis]", "").strip + "\n" }
6
+ logger
7
+ end
8
+
9
+ def redis_host
10
+ ENV['REDIS_HOST'] || '127.0.0.1'
11
+ end
12
+
13
+ def redis_url(port, db = nil)
14
+ out = "redis://#{redis_host}:#{port}"
15
+ out += "/#{db}" if db
16
+ out
17
+ end
18
+
19
+ def redis_socket(port)
20
+ "unix:/var/tmp/redisbetween-#{redis_host}-#{port}.sock"
21
+ end
22
+
23
+ def redis_client(cluster:, port:, **opts)
24
+ opts = cluster ? opts.merge(cluster: [redis_url(port)]) : opts.merge(url: redis_url(port))
25
+ Redis.new(opts)
26
+ end
27
+
28
+ it "has a version number" do
29
+ expect(Redisbetween::VERSION).not_to be nil
30
+ end
31
+
32
+ describe '#convert_to_redisbetween_socket' do
33
+ it 'should allow passing a proc' do
34
+ path = Redisbetween.socket_path(->(host, port, path) { [host, port, path].join('-') }, "h", "p", "p")
35
+ expect(path).to eq("h-p-p")
36
+ end
37
+
38
+ it 'should default to /var/tmp' do
39
+ path = Redisbetween.socket_path(true, "h", "p", "p")
40
+ expect(path).to eq("/var/tmp/redisbetween-h-p-p.sock")
41
+ end
42
+
43
+ it 'should omit the path if not given' do
44
+ path = Redisbetween.socket_path(true, "h", "p")
45
+ expect(path).to eq("/var/tmp/redisbetween-h-p.sock")
46
+ end
47
+ end
48
+
49
+ describe Redisbetween::ClientPatch do
50
+ it 'should point the client to a socket when given a url' do
51
+ client = Redis.new(url: redis_url(7006), convert_to_redisbetween_socket: true)
52
+ client.get "hi"
53
+ expect(client._client.options[:url]).to eq(redis_socket(7006))
54
+ end
55
+
56
+ it 'should point the cluster to a socket when given a url' do
57
+ client = Redis.new(cluster: [redis_url(7000)], convert_to_redisbetween_socket: true)
58
+ client._client.connection_info.map { |l| l[:location] }.each do |loc|
59
+ expect(loc).to match(/\/var\/tmp\/redisbetween-\d+\.\d+\.\d+\.\d+-\d+.sock/)
60
+ end
61
+ end
62
+
63
+ it 'should point the client to a socket when given host and port' do
64
+ client = Redis.new(host: redis_host, port: 1234, convert_to_redisbetween_socket: true)
65
+ expect(client._client.options[:url]).to eq(redis_socket(1234))
66
+ end
67
+
68
+ it 'should not mess with the url when not enabled' do
69
+ client = Redis.new(url: redis_url(7006, 10))
70
+ expect(client._client.options[:url]).to eq(redis_url(7006, 10))
71
+ end
72
+
73
+ [
74
+ [false, 7006],
75
+ [true, 7000],
76
+ ].each do |(cluster, port)|
77
+ context "with cluster #{cluster}, port #{port}" do
78
+ it 'should prepend and append the signal messages to all pipelines when enabled' do
79
+ stream = StringIO.new
80
+ client = redis_client(cluster: cluster, port: port, convert_to_redisbetween_socket: true, logger: test_logger(stream))
81
+ res = client.pipelined do
82
+ client.set("hi", 1)
83
+ client.get("hi")
84
+ client.set("yes", "maybe")
85
+ client.get("yes")
86
+ end
87
+ expect(res).to eq(%w[OK 1 OK maybe])
88
+ expect(stream.string).to include(<<~LOG
89
+ command=SET args="hi" "1"
90
+ command=GET args="hi"
91
+ command=SET args="yes" "maybe"
92
+ command=GET args="yes"
93
+ LOG
94
+ )
95
+ end
96
+
97
+ it 'should correctly process transactions with no cross slot keys' do
98
+ stream = StringIO.new
99
+ client = redis_client(cluster: cluster, port: port, convert_to_redisbetween_socket: true, logger: test_logger(stream))
100
+ res = client.multi do
101
+ client.set("{1}hi", 1)
102
+ client.get("{1}hi")
103
+ client.set("{1}yes", "maybe")
104
+ client.get("{1}yes")
105
+ end
106
+ expect(res).to eq(%w[OK 1 OK maybe])
107
+ expect(stream.string).to include(<<~LOG
108
+ command=MULTI args=
109
+ command=SET args="{1}hi" "1"
110
+ command=GET args="{1}hi"
111
+ command=SET args="{1}yes" "maybe"
112
+ command=GET args="{1}yes"
113
+ command=EXEC args=
114
+ LOG
115
+ )
116
+ end
117
+
118
+ it 'should not prepend or append the signal messages to any pipelines when not enabled' do
119
+ stream = StringIO.new
120
+ client = redis_client(cluster: cluster, port: port, logger: test_logger(stream))
121
+ res = client.pipelined do
122
+ client.set("hi", 1)
123
+ client.get("hi")
124
+ client.set("yes", "maybe")
125
+ client.get("yes")
126
+ end
127
+ expect(res).to eq(%w[OK 1 OK maybe])
128
+ expect(stream.string).to include(<<~LOG
129
+ command=SET args="hi" "1"
130
+ command=GET args="hi"
131
+ command=SET args="yes" "maybe"
132
+ command=GET args="yes"
133
+ LOG
134
+ )
135
+ end
136
+
137
+ it 'should not prepend or append the signal messages to multis when not enabled' do
138
+ stream = StringIO.new
139
+ client = redis_client(cluster: cluster, port: port, logger: test_logger(stream))
140
+ res = client.multi do
141
+ client.set("{1}hi", 1)
142
+ client.get("{1}hi")
143
+ client.set("{1}yes", "maybe")
144
+ client.get("{1}yes")
145
+ end
146
+ expect(res).to eq(%w[OK 1 OK maybe])
147
+ expect(stream.string).to include(<<~LOG
148
+ command=MULTI args=
149
+ command=SET args="{1}hi" "1"
150
+ command=GET args="{1}hi"
151
+ command=SET args="{1}yes" "maybe"
152
+ command=GET args="{1}yes"
153
+ command=EXEC args=
154
+ LOG
155
+ )
156
+ end
157
+ end
158
+ end
159
+
160
+ describe :handle_unsupported_redisbetween_command do
161
+ it 'should raise on unsupported commands when set to :raise' do
162
+ client = Redis.new(
163
+ url: redis_url(7006, 10),
164
+ handle_unsupported_redisbetween_command: ->(_cmd) { raise "hi" }
165
+ )
166
+ expect { client.select(2) }.to raise_error("hi")
167
+ end
168
+
169
+ it 'standalone: should call the given proc' do
170
+ stream = StringIO.new
171
+ logger = test_logger(stream)
172
+ client = Redis.new(
173
+ url: redis_url(7006, 10),
174
+ handle_unsupported_redisbetween_command: ->(cmd) { logger.warn("hi #{cmd}") }
175
+ )
176
+ client.select(2)
177
+ expect(stream.string).to include("hi select")
178
+ end
179
+
180
+ it 'cluster: should call the given proc' do
181
+ stream = StringIO.new
182
+ logger = test_logger(stream)
183
+ client = Redis.new(
184
+ cluster: [redis_url(7000)],
185
+ handle_unsupported_redisbetween_command: ->(cmd) { logger.warn(cmd) }
186
+ )
187
+ client.wait(1, 1)
188
+ expect(stream.string).to include("wait")
189
+ end
190
+
191
+ it 'should not mess with unsupported commands when not enabled' do
192
+ client = Redis.new(url: redis_url(7006, 10))
193
+ expect(client.select(2)).to eq("OK")
194
+ end
195
+
196
+ it 'should disallow multi without a block' do
197
+ client = Redis.new(
198
+ url: redis_url(7006, 10),
199
+ handle_unsupported_redisbetween_command: ->(cmd) { raise cmd }
200
+ )
201
+ expect { client.multi }.to raise_error("multi without a block")
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,15 @@
1
+ require "bundler/setup"
2
+ require "redisbetween"
3
+ require 'logger'
4
+
5
+ RSpec.configure do |config|
6
+ # Enable flags like --only-failures and --next-failure
7
+ config.example_status_persistence_file_path = ".rspec_status"
8
+
9
+ # Disable RSpec exposing methods globally on `Module` and `main`
10
+ config.disable_monkey_patching!
11
+
12
+ config.expect_with :rspec do |c|
13
+ c.syntax = :expect
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redisbetween
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Jordan Sitkin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-06-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Client patch for the ruby redis driver to support redisbetween
28
+ email:
29
+ - jordan.sitkin@coinbase.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/redisbetween.rb
35
+ - lib/redisbetween/version.rb
36
+ - spec/redisbetween_spec.rb
37
+ - spec/spec_helper.rb
38
+ homepage:
39
+ licenses: []
40
+ metadata: {}
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 2.3.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.2.3
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: redisbetween client gem
60
+ test_files: []