redis-cluster 0.0.9 → 1.0.0.pre.rc.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e2fc9fa13ac023fafd595e13fe442e70e7d83b95
4
- data.tar.gz: d577599a4bc2952ab2817fd202896caa8407bf23
3
+ metadata.gz: 1dd6f66183d627b2454e9e7ba629274ba32a1e78
4
+ data.tar.gz: 3519af542e7ed85abb0593bccbb9dba0f451762d
5
5
  SHA512:
6
- metadata.gz: cdbaa05a0d4bf1cd7b3f5bdb770cfc3349572aa96b1be80d7672e374917404ed75c7d815c4b667a0d35cce369597d5e3bd45528a161cf58c0bf827d72ad5fc51
7
- data.tar.gz: a451ee0b177399a81aabbca94c6204acf336c6d82369ed3a476b3620c1192aaf4d6f948d2a238d6701cd5dea1921b48be6aaf2a6ae790d44de57cfe9a0a93805
6
+ metadata.gz: f8c89e900077de31226af2067769576136735356388dd69d4f528697a3f67d4e2fa6685def24ba27a24a5f57992cf91eca2002a61c49e9baa4c94ae7c582c100
7
+ data.tar.gz: 74921e593e6a1338e2bcc382f6e27718b3d8a4808dd7ed8d589d539b3d3f9fce72ccac5bd0f0d002f1b142b2b04a0fe2ef3966f77fa92a68165ed1a2c01b0d6d
data/README.md CHANGED
@@ -33,6 +33,10 @@ SRE Bukalapak
33
33
  redis_opts: { timeout: 5, connect_timeout: 1 },
34
34
  cluster_opts: { force_cluster: false, read_mode: :master_slave, silent: true, logger: Logger.new }
35
35
  )
36
+ redis.middlewares.register(:commit) do |*args, &block|
37
+ puts "this is RedisCluster middlewares"
38
+ block.call
39
+ end
36
40
  ````
37
41
 
38
42
  ### Development Guide
@@ -41,7 +45,7 @@ SRE Bukalapak
41
45
  See [here](https://rvm.io/rvm/install) to install `rvm`.
42
46
  And run these commands to install `bundler` and other dependencies
43
47
 
44
- ````
48
+ ````sh
45
49
  gem install bundler
46
50
  bundle install
47
51
  ````
@@ -53,20 +57,20 @@ SRE Bukalapak
53
57
 
54
58
  4. Make your change and it's test.
55
59
 
56
- ````
60
+ ````sh
57
61
  vim lib/**.rb
58
62
  vim spec/**_spec.rb
59
63
  ````
60
64
 
61
65
  5. Optionally, run the test in your local
62
66
 
63
- ````
67
+ ````sh
64
68
  rake # run all test and lint
65
69
  ````
66
70
 
67
71
  6. Commit and push your change to upstream
68
72
 
69
- ````
73
+ ````sh
70
74
  git commit -m "message"
71
75
  git push # add "--set-upstream branch_name" after "git push" if you haven't set the upstream
72
76
  ````
@@ -91,6 +95,68 @@ Option for RedisCluster.
91
95
  - `silent`: whether or not RedisCluster will raise error.
92
96
  - `logger`: if specified. RedisCluster will log all of RedisCluster errors here.
93
97
 
98
+ #### Middlewares
99
+
100
+ Middlewares are hooks that RedisCluster provide to observe RedisCluster events. To register a middlewares, provide callable object (object that respond to call)
101
+ or give block in register method. Middlewares must give block result as return value.
102
+ ````ruby
103
+ class Callable
104
+ call
105
+ start = Time.now
106
+ yield
107
+ rescue StandardError => e
108
+ raise e
109
+ ensure
110
+ Metrics.publish(elapsed_time: Time.now - start)
111
+ end
112
+ end
113
+ redis.middlewares.register(:commit, Callable.new)
114
+
115
+ redis.middlewares.register(:commit) do |*args, &block|
116
+ begin
117
+ res = block.call
118
+ rescue StandardError => e
119
+ Log.warn('failed')
120
+ raise e
121
+ end
122
+ Log.info('success')
123
+ res
124
+ end
125
+ ````
126
+
127
+ Currently there are 3 events that RedisCluster publish.
128
+ - `:commit`
129
+ RedisCluster will fire `:commit` events when RedisCluster::Client call redis server. It give queue of command as arguments.
130
+ ````ruby
131
+ redis.middlewares.register(:commit) do |queues, &block|
132
+ puts 'this is :commit events'
133
+ puts "first command: #{queues.first.first}
134
+ puts "last command: #{queues.last.first}
135
+ block.call
136
+ end
137
+ ````
138
+ - `:call`
139
+ This events is fired when command is issued in RedisCluster client before any load balancing is done. It give call arguments as arguments
140
+ ````ruby
141
+ redis.middlewares.register(:call) do |keys, command, opts = {}, &block|
142
+ puts "keys to load balance: #{keys}"
143
+ puts "redis command: #{command.first}"
144
+ block.call
145
+ end
146
+ redis.get('something')
147
+ # Output:
148
+ # keys to load balance: something
149
+ # redis command: get
150
+ ````
151
+ - `:pipelined`
152
+ This events is fired when pipelined method is called from redis client. It does not give any arguments
153
+ ````ruby
154
+ redis.middlewares.register(:pipelined) do |&block|
155
+ puts 'pipelined is called'
156
+ block.call
157
+ end
158
+ ````
159
+
94
160
  ### Limitation
95
161
 
96
162
  All multi keys operation, cluster command, multi-exec, and some commands are not supported.
@@ -1,40 +1,37 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  require_relative 'redis_cluster/cluster'
5
6
  require_relative 'redis_cluster/client'
6
7
  require_relative 'redis_cluster/future'
7
8
  require_relative 'redis_cluster/function'
9
+ require_relative 'redis_cluster/middlewares'
8
10
 
9
11
  # RedisCluster is a client for redis-cluster *huh*
10
12
  class RedisCluster
11
13
  include MonitorMixin
12
14
  include Function
13
15
 
14
- attr_reader :cluster, :options
16
+ attr_reader :cluster, :cluster_opts, :redis_opts, :middlewares
15
17
 
16
18
  def initialize(seeds, redis_opts: nil, cluster_opts: nil)
17
- @options = cluster_opts || {}
18
- cluster_options = redis_opts || {}
19
- @cluster = Cluster.new(seeds, cluster_options.merge(force_cluster: force_cluster?))
19
+ @cluster_opts = cluster_opts || {}
20
+ @redis_opts = redis_opts || {}
21
+ @middlewares = Middlewares.new
22
+
23
+ client_creater = method(:create_client)
24
+ @cluster = Cluster.new(seeds, cluster_opts, &client_creater)
20
25
 
21
26
  super()
22
27
  end
23
28
 
24
29
  def logger
25
- options[:logger]
30
+ cluster_opts[:logger]
26
31
  end
27
32
 
28
33
  def silent?
29
- options[:silent]
30
- end
31
-
32
- def read_mode
33
- options[:read_mode] || :master
34
- end
35
-
36
- def force_cluster?
37
- options[:force_cluster]
34
+ cluster_opts[:silent]
38
35
  end
39
36
 
40
37
  def connected?
@@ -49,9 +46,25 @@ class RedisCluster
49
46
  !@pipeline.nil?
50
47
  end
51
48
 
52
- def call(keys, command, opts = {})
49
+ def call(*args, &block)
50
+ middlewares.invoke(:call, *args) do
51
+ _call(*args, &block)
52
+ end
53
+ end
54
+
55
+ def pipelined(*args, &block)
56
+ middlewares.invoke(:pipelined, *args) do
57
+ _pipelined(*args, &block)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ NOOP = ->(v){ v }
64
+
65
+ def _call(keys, command, opts = {})
53
66
  opts[:transform] ||= NOOP
54
- slot = slot_for([ keys ].flatten )
67
+ slot = cluster.slot_for(keys)
55
68
 
56
69
  safely do
57
70
  if pipeline?
@@ -62,7 +75,7 @@ class RedisCluster
62
75
  end
63
76
  end
64
77
 
65
- def pipelined
78
+ def _pipelined
66
79
  safely do
67
80
  return yield if pipeline?
68
81
 
@@ -94,11 +107,6 @@ class RedisCluster
94
107
  end
95
108
  end
96
109
 
97
- private
98
-
99
- NOOP = ->(v){ v }
100
- CROSSSLOT_ERROR = Redis::CommandError.new("CROSSSLOT Keys in request don't hash to the same slot")
101
-
102
110
  def safely
103
111
  synchronize{ yield } if block_given?
104
112
  rescue StandardError => e
@@ -106,17 +114,12 @@ class RedisCluster
106
114
  raise e unless silent?
107
115
  end
108
116
 
109
- def slot_for(keys)
110
- slot = keys.map{ |k| cluster.slot_for(k) }.uniq
111
- slot.size == 1 ? slot.first : ( raise CROSSSLOT_ERROR )
112
- end
113
-
114
117
  def call_immediately(slot, command, transform:, read: false)
115
118
  try = 3
116
119
  asking = false
117
120
  reply = nil
118
- mode = read ? read_mode : :master
119
- client = cluster.public_send(mode, slot)
121
+ mode = read ? :read : :write
122
+ client = cluster.client_for(mode, slot)
120
123
 
121
124
  while try.positive?
122
125
  begin
@@ -134,7 +137,7 @@ class RedisCluster
134
137
  rescue Redis::CannotConnectError => e
135
138
  asking = false
136
139
  cluster.reset
137
- client = cluster.public_send(mode, slot)
140
+ client = cluster.client_for(mode, slot)
138
141
  reply = e
139
142
  end
140
143
  end
@@ -151,11 +154,11 @@ class RedisCluster
151
154
  def map_pipeline(pipe)
152
155
  futures = ::Hash.new{ |h, k| h[k] = [] }
153
156
  pipe.each do |future|
154
- url = future.url || cluster.master(future.slot).url
157
+ url = future.url || cluster.client_for(:write, future.slot).url
155
158
  futures[url] << future
156
159
  end
157
160
 
158
- return futures
161
+ futures
159
162
  end
160
163
 
161
164
  def do_pipelined(url, futures)
@@ -192,12 +195,12 @@ class RedisCluster
192
195
  leftover << future
193
196
  end
194
197
 
195
- return [leftover, moved]
198
+ [leftover, moved]
196
199
  rescue Redis::CannotConnectError
197
200
  # reset url and asking when connection refused
198
201
  futures.each{ |f| f.url = nil; f.asking = false }
199
202
 
200
- return [futures, true]
203
+ [futures, true]
201
204
  end
202
205
 
203
206
  def scan_reply(reply)
@@ -210,4 +213,11 @@ class RedisCluster
210
213
  raise reply
211
214
  end
212
215
  end
216
+
217
+ def create_client(url)
218
+ host, port = url.split(':', 2)
219
+ Client.new(redis_opts.merge(host: host, port: port)).tap do |c|
220
+ c.middlewares = middlewares
221
+ end
222
+ end
213
223
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  require_relative 'version'
@@ -9,6 +10,7 @@ class RedisCluster
9
10
  # useful addition
10
11
  class Client
11
12
  attr_reader :client, :queue, :url
13
+ attr_accessor :middlewares
12
14
 
13
15
  def initialize(opts)
14
16
  @client = Redis::Client.new(opts)
@@ -38,6 +40,16 @@ class RedisCluster
38
40
  end
39
41
 
40
42
  def commit
43
+ return _commit unless middlewares
44
+
45
+ middlewares.invoke(:commit, queue.dup) do
46
+ _commit
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def _commit
41
53
  return nil if queue.empty?
42
54
 
43
55
  result = Array.new(queue.size)
@@ -48,7 +60,7 @@ class RedisCluster
48
60
  end
49
61
  @queue = []
50
62
 
51
- return result
63
+ result
52
64
  end
53
65
  end
54
66
  end
@@ -1,49 +1,50 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'client'
3
4
 
4
5
  class RedisCluster
5
6
 
6
7
  # Cluster implement redis cluster logic for client.
7
8
  class Cluster
8
- attr_reader :options, :slots, :clients, :replicas
9
+ attr_reader :options, :slots, :clients, :replicas, :client_creater
9
10
 
10
11
  HASH_SLOTS = 16_384
12
+ CROSSSLOT_ERROR = Redis::CommandError.new("CROSSSLOT Keys in request don't hash to the same slot")
11
13
 
12
- def initialize(seeds, options = {})
13
- @options = options
14
+ def initialize(seeds, cluster_opts = {}, &block)
15
+ @options = cluster_opts
14
16
  @slots = []
15
17
  @clients = {}
16
18
  @replicas = nil
19
+ @client_creater = block
17
20
 
18
21
  init_client(seeds)
19
22
  end
20
23
 
21
24
  def force_cluster?
22
- options[:force_cluster]
25
+ options[:force_cluster] || false
23
26
  end
24
27
 
25
- # Return Redis::Client for a given key.
26
- # Modified from https://github.com/antirez/redis-rb-cluster/blob/master/cluster.rb#L104-L117
27
- def slot_for(key)
28
- key = key.to_s
29
- if (s = key.index('{'))
30
- if (e = key.index('}', s + 1)) && e != s+1
31
- key = key[s+1..e-1]
32
- end
33
- end
34
- crc16(key) % HASH_SLOTS
28
+ def read_mode
29
+ options[:read_mode] || :master
35
30
  end
36
31
 
37
- def master(slot)
38
- slots[slot].first
32
+ def slot_for(keys)
33
+ slot = [keys].flatten.map{ |k| _slot_for(k) }.uniq
34
+ slot.size == 1 ? slot.first : (raise CROSSSLOT_ERROR)
39
35
  end
40
36
 
41
- def slave(slot)
42
- slots[slot][1..-1].sample || slots[slot].first
43
- end
37
+ def client_for(operation, slot)
38
+ mode = operation == :read ? read_mode : :master
44
39
 
45
- def master_slave(slot)
46
- slots[slot].sample
40
+ case mode
41
+ when :master
42
+ slots[slot].first
43
+ when :slave
44
+ slots[slot][1..-1].sample || slots[slot].first
45
+ when :master_slave
46
+ slots[slot].sample
47
+ end
47
48
  end
48
49
 
49
50
  def close
@@ -66,7 +67,7 @@ class RedisCluster
66
67
  slots_and_clients(client)
67
68
  rescue StandardError => e
68
69
  clients.delete(client.url)
69
- try.positive? ? retry : ( raise e )
70
+ try.positive? ? retry : (raise e)
70
71
  end
71
72
  end
72
73
 
@@ -74,12 +75,16 @@ class RedisCluster
74
75
  clients[url] ||= create_client(url)
75
76
  end
76
77
 
78
+ def inspect
79
+ "#<RedisCluster cluster v#{RedisCluster::VERSION}>"
80
+ end
81
+
77
82
  private
78
83
 
79
84
  def slots_and_clients(client)
80
85
  replicas = ::Hash.new{ |h, k| h[k] = [] }
81
86
 
82
- result = client.call([:cluster, :slots])
87
+ result = client.call(%i[cluster slots])
83
88
  if result.is_a?(StandardError)
84
89
  if result.message.eql?('ERR This instance has cluster support disabled') &&
85
90
  !force_cluster?
@@ -124,11 +129,12 @@ class RedisCluster
124
129
  end
125
130
 
126
131
  def create_client(url)
127
- host, port = url.split(':', 2)
128
- client_options = options.clone.tap do |opts|
129
- opts.delete(:force_update)
132
+ if client_creater
133
+ client_creater.call(url)
134
+ else
135
+ host, port = url.split(':', 2)
136
+ Client.new(host: host, port: port)
130
137
  end
131
- Client.new(client_options.merge(host: host, port: port))
132
138
  end
133
139
 
134
140
  # -----------------------------------------------------------------------------
@@ -152,11 +158,23 @@ class RedisCluster
152
158
  def crc16(bytes)
153
159
  crc = 0
154
160
  bytes.each_byte do |b|
155
- crc = ((crc<<8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc>>8)^b) & 0xff]
161
+ crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
156
162
  end
157
163
  crc
158
164
  end
159
165
 
166
+ # Return Redis::Client for a given key.
167
+ # Modified from https://github.com/antirez/redis-rb-cluster/blob/master/cluster.rb#L104-L117
168
+ def _slot_for(key)
169
+ key = key.to_s
170
+ if (s = key.index('{'))
171
+ if (e = key.index('}', s + 1)) && e != s + 1
172
+ key = key[s + 1..e - 1]
173
+ end
174
+ end
175
+ crc16(key) % HASH_SLOTS
176
+ end
177
+
160
178
  XMODEM_CRC16_LOOKUP = [
161
179
  0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
162
180
  0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
@@ -190,6 +208,6 @@ class RedisCluster
190
208
  0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
191
209
  0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
192
210
  0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
193
- ]
211
+ ].freeze
194
212
  end
195
213
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  class RedisCluster
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  class RedisCluster
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  class RedisCluster
@@ -73,7 +74,7 @@ class RedisCluster
73
74
  # @param [Hash] options
74
75
  # - `:match => String`: only return keys matching the pattern
75
76
  # - `:count => Integer`: return count keys at most per iteration
76
- [:zscan, :hscan, :sscan].each do |method|
77
+ %i[zscan hscan sscan].each do |method|
77
78
  define_method "#{method}_each" do |key, options = {}, &block|
78
79
  return if block.nil?
79
80
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  class RedisCluster
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  class RedisCluster
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  class RedisCluster
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
 
4
5
  class RedisCluster
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisCluster
4
+
5
+ # Middlewares is collection for RedisCluster middleware.
6
+ class Middlewares
7
+ attr_reader :middlewares
8
+
9
+ def initialize
10
+ @middlewares = Hash.new{ |h, k| h[k] = [] }
11
+ end
12
+
13
+ def register(name, callable = nil, &block)
14
+ return if !callable && !block_given?
15
+
16
+ middlewares[name] << (callable || block)
17
+ end
18
+
19
+ def invoke(name, *args, &block)
20
+ callback = middlewares[name].reduce(block) do |acc, obs|
21
+ proc{ obs.call(*args, &acc) }
22
+ end
23
+
24
+ callback.call
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisCluster
4
- VERSION = '0.0.9'
4
+ VERSION = '1.0.0-rc.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-cluster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 1.0.0.pre.rc.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bukalapak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-02 00:00:00.000000000 Z
11
+ date: 2018-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -44,6 +44,7 @@ files:
44
44
  - lib/redis_cluster/function/sorted_set.rb
45
45
  - lib/redis_cluster/function/string.rb
46
46
  - lib/redis_cluster/future.rb
47
+ - lib/redis_cluster/middlewares.rb
47
48
  - lib/redis_cluster/version.rb
48
49
  homepage: https://github.com/bukalapak/redis-cluster
49
50
  licenses:
@@ -60,9 +61,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
61
  version: '0'
61
62
  required_rubygems_version: !ruby/object:Gem::Requirement
62
63
  requirements:
63
- - - ">="
64
+ - - ">"
64
65
  - !ruby/object:Gem::Version
65
- version: '0'
66
+ version: 1.3.1
66
67
  requirements: []
67
68
  rubyforge_project:
68
69
  rubygems_version: 2.5.1