rdkit 0.0.1 → 0.1.4

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/Gemfile +10 -1
  4. data/Gemfile.ci +13 -0
  5. data/Guardfile +40 -0
  6. data/README.md +282 -6
  7. data/Vagrantfile +13 -0
  8. data/example/blocking.rb +10 -0
  9. data/example/blocking/command_runner.rb +28 -0
  10. data/example/blocking/core.rb +24 -0
  11. data/example/blocking/server.rb +10 -0
  12. data/example/blocking/version.rb +3 -0
  13. data/example/callbacks.rb +9 -0
  14. data/example/callbacks/command_runner.rb +21 -0
  15. data/example/callbacks/core.rb +18 -0
  16. data/example/callbacks/server.rb +30 -0
  17. data/example/counter.rb +0 -2
  18. data/example/counter/command_runner.rb +4 -0
  19. data/example/counter/core.rb +4 -0
  20. data/example/http.rb +9 -0
  21. data/example/http/core.rb +18 -0
  22. data/example/http/responder.rb +7 -0
  23. data/example/http/server.rb +19 -0
  24. data/lib/rdkit.rb +20 -3
  25. data/lib/rdkit/callbacks.rb +10 -0
  26. data/lib/rdkit/client.rb +157 -0
  27. data/lib/rdkit/configuration.rb +31 -0
  28. data/lib/rdkit/core.rb +2 -4
  29. data/lib/rdkit/core_ext.rb +7 -0
  30. data/lib/rdkit/db.rb +257 -0
  31. data/lib/rdkit/db_commands.rb +182 -0
  32. data/lib/rdkit/errors.rb +32 -1
  33. data/lib/rdkit/http_parser.rb +56 -0
  34. data/lib/rdkit/http_responder.rb +74 -0
  35. data/lib/rdkit/introspection.rb +133 -21
  36. data/lib/rdkit/logger.rb +9 -4
  37. data/lib/rdkit/memory_monitoring.rb +29 -0
  38. data/lib/rdkit/notification_center.rb +21 -0
  39. data/lib/rdkit/rd_object.rb +69 -0
  40. data/lib/rdkit/resp.rb +9 -1
  41. data/lib/rdkit/{command_parser.rb → resp_parser.rb} +6 -18
  42. data/lib/rdkit/resp_responder.rb +105 -0
  43. data/lib/rdkit/server.rb +242 -86
  44. data/lib/rdkit/simple_commands.rb +17 -0
  45. data/lib/rdkit/slow_log.rb +52 -0
  46. data/lib/rdkit/subcommands.rb +157 -0
  47. data/lib/rdkit/version.rb +1 -1
  48. data/rdkit.gemspec +6 -0
  49. metadata +119 -5
  50. data/lib/rdkit/inheritable.rb +0 -15
  51. data/lib/rdkit/resp_runner.rb +0 -46
@@ -0,0 +1,69 @@
1
+ module RDKit
2
+ class RDObject
3
+ attr_accessor :type, :encoding, :value
4
+
5
+ def self.forward_to_value(*methods)
6
+ @forwarded_methods = methods
7
+ end
8
+
9
+ def method_missing(method, *args)
10
+ if forwarded_methods.include?(method)
11
+ value.__send__(method, *args)
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def forwarded_methods
20
+ self.class.instance_variable_get(:@forwarded_methods) || []
21
+ end
22
+
23
+ module ClassMethods
24
+ def string(value)
25
+ new.tap do |object|
26
+ object.type = :string
27
+ object.value = value
28
+ end
29
+ end
30
+
31
+ def list(elements)
32
+ RDList.new.tap do |object|
33
+ object.type = :list
34
+ object.value = elements
35
+ end
36
+ end
37
+
38
+ def set(elements)
39
+ require "set"
40
+
41
+ RDSet.new.tap do |set|
42
+ set.type = :set
43
+ set.value = Set.new(elements)
44
+ end
45
+ end
46
+
47
+ def create_hash(key, value)
48
+ RDHash.new.tap do |hash|
49
+ hash.type = :hash
50
+ hash.value = { key => value }
51
+ end
52
+ end
53
+ end
54
+
55
+ class << self; include ClassMethods; end
56
+ end
57
+
58
+ class RDList < RDObject
59
+ forward_to_value :unshift, :length, :shift, :empty?, :pop
60
+ end
61
+
62
+ class RDSet < RDObject
63
+ forward_to_value :add, :size, :to_a, :include?, :delete
64
+ end
65
+
66
+ class RDHash < RDObject
67
+ forward_to_value :has_key?, :[]=, :[], :size, :delete, :keys, :values
68
+ end
69
+ end
data/lib/rdkit/resp.rb CHANGED
@@ -5,6 +5,12 @@ module RDKit
5
5
  module ClassMethods
6
6
  def compose(data)
7
7
  case data
8
+ when *%w{ OK string list set hash zset none }
9
+ "+#{data}\r\n"
10
+ when true
11
+ ":1\r\n"
12
+ when false
13
+ ":0\r\n"
8
14
  when Integer
9
15
  ":#{data}\r\n"
10
16
  when Array
@@ -12,8 +18,10 @@ module RDKit
12
18
  when NilClass
13
19
  # Null Bulk String, not Null Array of "*-1\r\n"
14
20
  "$-1\r\n"
21
+ when WrongTypeError
22
+ "-WRONGTYPE #{data.message}\r\n"
15
23
  when StandardError
16
- "-#{data.message}\r\n"
24
+ "-ERR #{data.message}\r\n"
17
25
  else
18
26
  # always Bulk String
19
27
  "$#{data.bytesize}\r\n#{data}\r\n"
@@ -3,43 +3,31 @@ require "hiredis/reader"
3
3
  # Hiredis::Reader does not handle inline commands, so
4
4
 
5
5
  module RDKit
6
- class CommandParser
6
+ class RESPParser
7
7
  def initialize
8
8
  @reader = Hiredis::Reader.new
9
9
  @buffer = []
10
10
  @regexp = Regexp.new("\\A(.+)\\r\\n\\z")
11
- @error = nil
11
+ @inline_mode = true
12
12
  end
13
13
 
14
14
  def feed(data)
15
- if data =~ @regexp
15
+ if @inline_mode && (data =~ @regexp)
16
16
  @buffer << $1.split
17
17
  else
18
- @reader.feed(data)
18
+ @inline_mode = false
19
19
 
20
- read_into_buffer!
20
+ @reader.feed(data)
21
21
  end
22
22
  end
23
23
 
24
24
  def gets
25
- raise @error unless @error.nil?
26
-
27
- if result = @buffer.shift
25
+ if @inline_mode && (result = @buffer.shift)
28
26
 
29
27
  result
30
28
  else
31
29
  @reader.gets
32
30
  end
33
31
  end
34
-
35
- private
36
-
37
- def read_into_buffer!
38
- until (reply = @reader.gets) == false
39
- @buffer << reply
40
- end
41
- rescue RuntimeError => e
42
- @error = ProtocolError.new(e) if e.message =~ /Protocol error/
43
- end
44
32
  end
45
33
  end
@@ -0,0 +1,105 @@
1
+ module RDKit
2
+ class RESPResponder
3
+ def run(cmd)
4
+ if cmd.respond_to?(:call)
5
+ RESP.compose(cmd.call)
6
+ else
7
+ RESP.compose(call(cmd))
8
+ end
9
+ rescue StandardError => e
10
+ RESP.compose(e)
11
+ end
12
+
13
+ include SimpleCommands
14
+ include DBCommands
15
+
16
+ # 获取服务器状态
17
+ def info(section='default')
18
+ info = Introspection.info(section)
19
+
20
+ unless info.empty?
21
+ info.map do |type, value|
22
+ "# #{type.capitalize}\r\n" + value.map { |k, v| "#{k}:#{v}" }.join("\r\n") + "\r\n"
23
+ end.join("\r\n") + "\r\n"
24
+ end
25
+ end
26
+
27
+ def gc
28
+ GC.start
29
+
30
+ 'OK'
31
+ end
32
+
33
+ def heapdump
34
+ require "objspace"
35
+
36
+ ObjectSpace.trace_object_allocations_start
37
+
38
+ GC.start
39
+
40
+ file = "tmp/heap-#{Process.pid}-#{Time.now.to_i}.json"
41
+
42
+ ObjectSpace.dump_all(output: File.open(file, "w"))
43
+
44
+ file
45
+ end
46
+
47
+ def monitor
48
+ server.monitors << server.current_client
49
+
50
+ 'OK'
51
+ end
52
+
53
+ def shutdown
54
+ server.stop
55
+ end
56
+
57
+ include Subcommands
58
+ def config(cmd, *args)
59
+ execute_subcommand('config', %w{ get set resetstat }, cmd, *args)
60
+ end
61
+
62
+ def slowlog(cmd, *args)
63
+ execute_subcommand('slowlog', %w{ get reset len }, cmd, *args)
64
+ end
65
+
66
+ def client(cmd, *args)
67
+ execute_subcommand('client', %w{ list kill getname setname }, cmd, *args)
68
+ end
69
+
70
+ def debug(cmd, *args)
71
+ execute_subcommand('debug', %w{ sleep segfault }, cmd, *args)
72
+ end
73
+
74
+ def server
75
+ Server.instance
76
+ end
77
+
78
+ private
79
+
80
+ def call(cmd)
81
+ @logger ||= Logger.new
82
+
83
+ Introspection::Stats.incr(:total_commands_processed)
84
+
85
+ @logger.debug "running command: #{cmd}"
86
+ cmd, *args = cmd
87
+
88
+ cmd.downcase!
89
+
90
+ if self.respond_to?(cmd)
91
+ self.__send__(cmd, *args)
92
+ else
93
+ raise UnknownCommandError, "unknown command '#{cmd}'"
94
+ end
95
+ rescue ArgumentError => e
96
+ raise WrongNumberOfArgumentError, "wrong number of arguments for '#{cmd}' command"
97
+ end
98
+ end
99
+
100
+ class RESPRunner < RESPResponder
101
+ def self.inherited(base)
102
+ $stderr.puts "RESPRunner is deprecated, use RESPResponder instead"
103
+ end
104
+ end
105
+ end
data/lib/rdkit/server.rb CHANGED
@@ -1,61 +1,153 @@
1
- require 'newrelic_rpm'
1
+ require "sigdump/setup"
2
+ require 'thread/pool'
2
3
 
3
4
  module RDKit
4
5
  class Server
5
6
  HZ = 10
6
- CYCLES_TIL_MEMORY_RESAMPLE = 1000
7
+
8
+ HANDLED_SIGNALS = [ :TERM, :INT, :HUP ]
7
9
 
8
10
  attr_reader :server_up_since
9
- attr_reader :runner
11
+ attr_reader :current_client
12
+ attr_reader :current_db
10
13
  attr_reader :core
11
14
  attr_reader :host, :port
15
+ attr_reader :logger
16
+ attr_reader :monitors
17
+ attr_reader :cycles
18
+ attr_accessor :parser_class
19
+
20
+ def responder
21
+ @responder ||= (( @runner && $stderr.puts("@runner is deprecated, use @responder instead") ) || @runner)
22
+ end
12
23
 
13
24
  def initialize(host, port)
14
25
  @host, @port = host, port
15
26
 
16
27
  @cycles = 0
17
- @peak_memory = 0
18
28
  @peak_connected_clients = 0
29
+ @client_id_seq = 0
19
30
 
20
- @clients, @command_parsers = Hash.new, Hash.new
31
+ @clients = Hash.new
32
+ @blocked_clients = Hash.new
33
+ @monitors = []
21
34
 
22
- @logger = Logger.new
35
+ @logger = Logger.new(ENV['RDKIT_LOG_PATH'])
36
+ @current_db = DB.new(0)
37
+ @all_dbs = [@current_db]
23
38
 
24
39
  Introspection.register(self)
25
40
 
26
41
  @server_up_since = Time.now
27
- end
28
42
 
29
- def sanity_check!
30
- unless @host && @port
31
- raise SDKRequirementNotMetError, '@host and @port are required for server to run'
32
- end
43
+ @parser_class = RESPParser
33
44
 
34
- if @core.nil?
35
- raise SDKRequirementNotMetError, '@core is required to represent your business logics'
36
- end
45
+ register_notification_observers!
37
46
 
38
- if @runner.nil?
39
- raise SDKRequirementNotMetError, '@runner is required to act as an RESP frontend'
40
- end
47
+ Server.register(self)
48
+
49
+ # Self-pipe for deferred signal-handling http://www.sitepoint.com/the-self-pipe-trick-explained/
50
+ # Borrowed from `Foreman::Engine`
51
+ reader, writer = create_pipe
52
+ @selfpipe = { :reader => reader, :writer => writer }
53
+ @signal_queue = []
41
54
  end
42
55
 
43
56
  def start
44
57
  sanity_check!
45
58
 
59
+ register_signal_handlers
60
+
46
61
  @server_socket = TCPServer.new(@host, @port)
47
62
 
48
63
  run_acceptor
49
64
  end
50
65
 
51
66
  def stop
52
- @logger.warn "signal caught, shutting down..."
67
+ @logger.warn "shutting down..."
53
68
  exit
54
69
  end
55
70
 
71
+ def create_pipe
72
+ IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
73
+ end
74
+
75
+ def register_signal_handlers
76
+ HANDLED_SIGNALS.each do |sig|
77
+ if ::Signal.list.include? sig.to_s
78
+ trap(sig) { @signal_queue << sig ; notice_signal }
79
+ end
80
+ end
81
+ end
82
+
83
+ def notice_signal
84
+ @selfpipe[:writer].write_nonblock('.')
85
+ rescue Errno::EAGAIN
86
+ # Ignore writes that would block
87
+ rescue Errno::EINT
88
+ # Retry if another signal arrived while writing
89
+ retry
90
+ end
91
+
92
+ def handle_signals
93
+ while sig = @signal_queue.shift
94
+ handle_signal(sig)
95
+ end
96
+ end
97
+
98
+ # Invoke the real handler for signal +sig+. This shouldn't be called directly
99
+ # by signal handlers, as it might invoke code which isn't re-entrant.
100
+ #
101
+ # @param [Symbol] sig the name of the signal to be handled
102
+ #
103
+ def handle_signal(sig)
104
+ case sig
105
+ when :TERM
106
+ handle_term_signal
107
+ when :INT
108
+ handle_interrupt
109
+ when :HUP
110
+ handle_hangup
111
+ else
112
+ system "unhandled signal #{sig}"
113
+ end
114
+ end
115
+
116
+ # Handle a TERM signal
117
+ #
118
+ def handle_term_signal
119
+ @logger.warn "SIGTERM received"
120
+ terminate_gracefully
121
+ end
122
+
123
+ # Handle an INT signal
124
+ #
125
+ def handle_interrupt
126
+ @logger.warn "SIGINT received"
127
+ terminate_gracefully
128
+ end
129
+
130
+ # Handle a HUP signal
131
+ #
132
+ def handle_hangup
133
+ @logger.warn "SIGHUP received"
134
+ terminate_gracefully
135
+ end
136
+
137
+ def terminate_gracefully
138
+ return if @terminating
139
+
140
+ @terminating = true
141
+
142
+ stop
143
+ end
144
+
145
+ include MemoryMonitoring
146
+
56
147
  def introspection
57
148
  {
58
149
  server: {
150
+ ruby_version: "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}",
59
151
  rdkit_version: RDKit::VERSION,
60
152
  multiplexing_api: 'select',
61
153
  process_id: Process.pid,
@@ -65,6 +157,7 @@ module RDKit
65
157
  hz: HZ,
66
158
  },
67
159
  clients: {
160
+ blocked_clients: @blocked_clients.size,
68
161
  connected_clients: @clients.size,
69
162
  connected_clients_peak: @peak_connected_clients
70
163
  },
@@ -75,118 +168,181 @@ module RDKit
75
168
  }
76
169
  end
77
170
 
78
- private
79
-
80
- def used_memory_rss_in_mb
81
- update_peak_memory!
171
+ def delete(socket)
172
+ @clients.delete(socket)
173
+ end
82
174
 
83
- '%0.2f' % used_memory_rss + 'M'
175
+ def clients
176
+ @clients.values
84
177
  end
85
178
 
86
- def used_memory_peak_in_mb
87
- '%0.2f' % @peak_memory + 'M'
179
+ def select_db!(index)
180
+ if db = @all_dbs.find { |db| db.index == index }
181
+ @current_db = db
182
+ else
183
+ @all_dbs << DB.new(index)
184
+
185
+ @current_db = @all_dbs.last
186
+ end
88
187
  end
89
188
 
90
- def add_client
91
- Introspection::Stats.incr(:total_connections_received)
189
+ def flushdb!
190
+ @current_db.flush!
191
+ end
92
192
 
93
- socket = @server_socket.accept_nonblock
193
+ def flushall!
194
+ flushdb!
94
195
 
95
- @command_parsers[socket] = CommandParser.new
196
+ @all_dbs = [@current_db]
197
+ end
96
198
 
97
- @clients[socket] = Fiber.new do
98
- with_error_handling(socket) do |io|
99
- loop { process(io); Fiber.yield }
100
- end
101
- end
199
+ def blocking(on_success=nil, &block)
200
+ @blocked_clients[current_client.socket] = current_client
201
+ @clients.delete(current_client.socket)
102
202
 
103
- @logger.debug "client #{socket} connected"
203
+ current_client.blocking(on_success, &block)
204
+ end
104
205
 
105
- return @clients[socket]
206
+ def pool
207
+ @pool ||= Thread.pool((ENV['RDKIT_SERVER_THREAD_POOL_SIZE'] || 10).to_i)
106
208
  end
107
209
 
108
- def process(io)
109
- feed_parser(io)
210
+ include Callbacks
211
+
212
+ private
213
+
214
+ def register_notification_observers!
215
+ if webhook = ENV['RDKIT_SLOW_LOG_BEARYCHAT_WEBHOOK']
216
+ require "httpi"
217
+ require "multi_json"
218
+ HTTPI.logger = Logger.new('/dev/null')
219
+
220
+ NotificationCenter.subscribe('slowlog', self) do |cmd, usec|
221
+ cmd, *args = cmd
110
222
 
111
- until (reply = get_parser_reply(io)) == false
112
- send_response(io, reply)
223
+ text = "host=#{@host} port=#{@port} cmd=#{cmd}(#{args.join(',') }) usec=#{usec}"
224
+
225
+ pool.process { HTTPI.post(webhook, payload: MultiJson.dump({ text: text })) }
226
+ end
113
227
  end
114
228
  end
115
229
 
116
- def feed_parser(io)
117
- cmd = io.readpartial(1024)
230
+ def sanity_check!
231
+ unless @host && @port
232
+ raise SDKRequirementNotMetError, '@host and @port are required for server to run'
233
+ end
234
+
235
+ if @core.nil?
236
+ raise SDKRequirementNotMetError, '@core is required to represent your business logics'
237
+ end
118
238
 
119
- @command_parsers[io].feed(cmd)
120
- end
239
+ if responder.nil?
240
+ raise SDKRequirementNotMetError, '@responder is required to act as an RESP frontend'
241
+ end
121
242
 
122
- def get_parser_reply(io)
123
- @command_parsers[io].gets
243
+ if responder.server.nil?
244
+ raise SDKRequirementNotMetError, '@responder should have reference to server'
245
+ end
124
246
  end
125
247
 
126
- def send_response(io, cmd)
127
- resp = runner.resp(cmd)
248
+ def add_client
249
+ Introspection::Stats.incr(:total_connections_received)
128
250
 
129
- @logger.debug(resp)
251
+ socket = @server_socket.accept_nonblock
130
252
 
131
- io.write(resp)
132
- end
253
+ client = @clients[socket] = Client.new(socket, self)
254
+ client.id = (@client_id_seq += 1)
133
255
 
134
- def with_error_handling(socket, &block)
256
+ @logger.debug "client #{socket} connected"
257
+ client_connected(client)
135
258
 
136
- block.call(socket)
259
+ update_peak_connected_clients!
137
260
 
138
- rescue Errno::ECONNRESET, EOFError => e
139
- # client disconnected
140
- @logger.debug "client #{socket.inspect} has disconnected"
141
- @logger.debug e
142
- @command_parsers.delete(socket)
143
- @clients.delete(socket)
144
- rescue ProtocolError => e
145
- # client protocol error, force disconnect
146
- @logger.debug "client protocol error"
147
- @logger.debug e
261
+ return @clients[socket]
262
+ end
263
+
264
+ def process(socket)
265
+ client = @clients[socket]
266
+ @current_client = client
267
+ client.resume
268
+ client_command_processed(client)
269
+ rescue ClientDisconnectedError => e
148
270
  socket.close
149
- @command_parsers.delete(socket)
150
- @clients.delete(socket)
271
+ @monitors.delete(client)
272
+ delete(socket)
273
+ client_disconnected(client)
151
274
  end
152
275
 
153
276
  def run_acceptor
154
- @logger.info "accepting on shared socket (#{@host}:#{@port})"
277
+ @logger.info "accepting on shared socket (#{@host}:#{@port}), PID #{Process.pid}"
155
278
 
156
- loop do
157
- readable, _ = IO.select([@server_socket, @clients.keys].flatten, nil, nil, 1.0 / HZ)
158
-
159
- if readable
160
- readable.each do |socket|
161
- if socket == @server_socket
162
- add_client
163
- else
164
- # client is a Fiber
165
- client = @clients[socket]
166
- client.resume
167
- end
168
- end
169
- end
279
+ server_started
170
280
 
171
- update_peak_memory! if @cycles % CYCLES_TIL_MEMORY_RESAMPLE == 0
172
- update_peak_connected_clients!
281
+ loop do
282
+ process_blocked_clients
283
+ process_clients
173
284
 
174
285
  @cycles += 1
175
286
 
176
287
  core.tick!
288
+
289
+ gc_pool.process if @cycles % 1000 == 0
177
290
  end
291
+ rescue Exception => e
292
+ @logger.warn e unless e.class == SystemExit
293
+ raise e
178
294
  end
179
295
 
180
- def update_peak_memory!
181
- @peak_memory = [@peak_memory, used_memory_rss].max
296
+ def gc_pool
297
+ @gc_pool ||= Thread.pool(1) do
298
+ _, usec = SlowLog.monitor('bg_gc') { GC.start }
299
+
300
+ Introspection::Commandstats.record('bg_gc', usec)
301
+ end
302
+ end
303
+
304
+ def process_blocked_clients
305
+ @blocked_clients.each do |socket, client|
306
+ if client.finished?
307
+ @clients[socket] = client
308
+ @blocked_clients.delete(socket)
309
+
310
+ client.unblock!
311
+ end
312
+ end
313
+ end
314
+
315
+ def process_clients
316
+ readable, _ = IO.select([@server_socket, @selfpipe[:reader], @clients.keys].flatten, nil, nil, 1.0 / HZ)
317
+
318
+ if readable
319
+ readable.each do |socket|
320
+ if socket == @server_socket
321
+ add_client
322
+ elsif socket == @selfpipe[:reader]
323
+ handle_signals
324
+ else
325
+ process(socket)
326
+ end
327
+
328
+ process_blocked_clients
329
+ end
330
+ end
182
331
  end
183
332
 
184
333
  def update_peak_connected_clients!
185
334
  @peak_connected_clients = [@peak_connected_clients, @clients.size].max
186
335
  end
187
336
 
188
- def used_memory_rss
189
- NewRelic::Agent::Samplers::MemorySampler.new.sampler.get_sample
337
+ module ClassMethods
338
+ def register(instance)
339
+ @@instance = instance
340
+ end
341
+
342
+ def instance
343
+ @@instance
344
+ end
190
345
  end
346
+ class << self; include ClassMethods; end
191
347
  end
192
348
  end