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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b7f8dab0976c2aaf2d8045793c9c52713a7ff867
4
- data.tar.gz: 0cb9f68b9684fc6c2a5e553202a724396ae75d46
3
+ metadata.gz: 10a0bd157c17b8098e43a87046384ebc7e9ae534
4
+ data.tar.gz: 7ad179971146d74db65ffbc8a6999b1b9b8bb034
5
5
  SHA512:
6
- metadata.gz: 2e1c7b165b6cc10f382afd74618dd5a5df2a1722e8bbad9a5eb69c2742449fe6f2ca6e39ec0a4b163574b4cce8ae8aaf78a45ccbafddd18e26dc7896e072956d
7
- data.tar.gz: 2b6889a5764936fb592b0d29418aab64108c7cb53f1b22c2d2e87381a9259e2f8ab7f3f8f4e0e3366a5e554f1399d6a6218b8a28317ed5ef30f7b83ace5a964f
6
+ metadata.gz: f8fd1464e00be7ce386d4eefb1132984cbfb7632ff498eb7df6ddd97fd07127122f27b8828c1722ad22b515b606a686286fc5a5e550adc964c00409e7950aa62
7
+ data.tar.gz: 2d71bef8dfac46a9b7d4274ab755e83a3f944a05a401def43b1bda6932d69c1a2cef181a229a015faf5299d73d22361778502e9653c4e52df61d251572e304a6
data/.gitignore CHANGED
@@ -8,3 +8,6 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  log
11
+ .vagrant
12
+ .env
13
+ Gemfile.ci.lock
data/Gemfile CHANGED
@@ -1,4 +1,13 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in rdkit.gemspec
4
3
  gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry'
7
+ gem 'coco'
8
+ gem 'timecop'
9
+ gem 'guard'
10
+ gem 'guard-rspec'
11
+ end
12
+
13
+ gem 'thread', '~> 0.2.1', github: 'meh/ruby-thread'
data/Gemfile.ci ADDED
@@ -0,0 +1,13 @@
1
+ source 'http://ruby.taobao.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry'
7
+ gem 'coco'
8
+ gem 'timecop'
9
+ gem 'guard'
10
+ gem 'guard-rspec'
11
+ end
12
+
13
+ gem 'thread', '~> 0.2.1', github: 'meh/ruby-thread'
data/Guardfile ADDED
@@ -0,0 +1,40 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features)
6
+
7
+ ## Uncomment to clear the screen before every task
8
+ # clearing :on
9
+
10
+ ## Guard internally checks for changes in the Guardfile and exits.
11
+ ## If you want Guard to automatically start up again, run guard in a
12
+ ## shell loop, e.g.:
13
+ ##
14
+ ## $ while bundle exec guard; do echo "Restarting Guard..."; done
15
+ ##
16
+ ## Note: if you are using the `directories` clause above and you are not
17
+ ## watching the project directory ('.'), then you will want to move
18
+ ## the Guardfile to a watched dir and symlink it back, e.g.
19
+ #
20
+ # $ mkdir config
21
+ # $ mv Guardfile config/
22
+ # $ ln -s config/Guardfile .
23
+ #
24
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
25
+
26
+ # Note: The cmd option is now required due to the increasing number of ways
27
+ # rspec may be run, below are examples of the most common uses.
28
+ # * bundler: 'bundle exec rspec'
29
+ # * bundler binstubs: 'bin/rspec'
30
+ # * spring: 'bin/rspec' (This will use spring if running and you have
31
+ # installed the spring binstubs per the docs)
32
+ # * zeus: 'zeus rspec' (requires the server to be started separately)
33
+ # * 'just' rspec: 'rspec'
34
+
35
+ guard :rspec, cmd: "bundle exec rspec" do
36
+ watch(%r{^spec/.+_spec\.rb$})
37
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
38
+
39
+ watch('spec/spec_helper.rb') { "spec" }
40
+ end
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RDKit
2
2
 
3
- RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.
3
+ `RDKit` is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.
4
4
 
5
5
  The server speaks [Redis RESP protocol](http://redis.io/topics/protocol), so you can reuse many Redis-compatible clients and tools such as:
6
6
 
@@ -10,11 +10,17 @@ The server speaks [Redis RESP protocol](http://redis.io/topics/protocol), so you
10
10
 
11
11
  And a lot more.
12
12
 
13
- `RDKit` is used to power the [520 Love Radio](http://s.weibo.com/weibo/same%2520%25E7%2594%25B5%25E5%258F%25B0) service of [same.com](http://same.com)
13
+ `RDKit` is used to power:
14
+
15
+ - [520 Love Radio](http://s.weibo.com/weibo/same%2520%25E7%2594%25B5%25E5%258F%25B0) service of [same.com](http://same.com)
16
+ - AntiSpam blacklisted photo filtering service used at [same.com](http://same.com) (BK-Tree + pHash)
17
+ - channel unread count service at [same.com](http://same.com)
14
18
 
15
19
  [![Code Climate](https://codeclimate.com/github/forresty/rdkit/badges/gpa.svg)](https://codeclimate.com/github/forresty/rdkit)
16
20
  [![Build Status](https://travis-ci.org/forresty/rdkit.svg?branch=master)](https://travis-ci.org/forresty/rdkit)
17
21
 
22
+ `RDKit` should work without problem on `MRI` 2.2+, may encounter bugs on earlier version of `MRI` or `JRuby` or `Rubinus`, in that case, please kindly open an issue on GitHub
23
+
18
24
  ## Installation
19
25
 
20
26
  Add this line to your application's Gemfile:
@@ -33,9 +39,100 @@ Or install it yourself as:
33
39
 
34
40
  ## Usage
35
41
 
36
- see examples under `example` folder.
42
+ Generally, you should implement one subclass for each of the 3 classes: `RDKit::RESPResponder`, `RDKit::Core` and `RDKit::Server`, and spawn one object for each class.
43
+
44
+ Your server object should have two instance variables `@responder` and `@core` pointed to your spawned instances.
45
+
46
+ ### RDKit::Server
47
+
48
+ ```ruby
49
+ class YourServer < RDKit::Server
50
+ def initialize
51
+ super('0.0.0.0', 3721)
52
+
53
+ @core = YourCore.new
54
+ @responder = YourResponder.new(core)
55
+ end
56
+ end
57
+
58
+ server = YourServer.new
59
+
60
+ trap(:INT) { server.stop }
61
+
62
+ server.start
63
+ ```
64
+
65
+ This will start a `TCPServer` on `0.0.0.0:3721` and stops when you `CTRL-C`.
66
+
67
+ ### RDKit::RESPResponder
68
+
69
+ `@responder` maps Redis commands to its methods and arguments, for example `info` will be sent to `RESPResponder#info`, and `info all` to `RESPResponder#info` with `"all"` as its first argument.
70
+
71
+ The return ruby object of each method will be marshaled as RESP strings, for example `'OK'` becomes `"+OK\r\n"`.
72
+
73
+ For example, with following implementation in your `RESPResponder` subclass:
74
+
75
+ ```ruby
76
+ def add(a, b)
77
+ a.to_i + b.to_i
78
+ end
79
+ ```
80
+
81
+ You implemented an adder using RDKit! See it in action:
82
+
83
+ ```shell
84
+ $ redis-cli -p 3721
85
+ 127.0.0.1:3721> add 1 2
86
+ (integer) 3
87
+ 127.0.0.1:3721> add 5
88
+ (error) ERR wrong number of arguments for 'add' command
89
+ 127.0.0.1:3721>
90
+ ```
91
+
92
+ The detailed algorithm can be found in `resp.rb`, at the time of writing it is like this:
93
+
94
+ ```ruby
95
+ def compose(data)
96
+ case data
97
+ when *%w{ OK string list set hash zset none }
98
+ "+#{data}\r\n"
99
+ when true
100
+ ":1\r\n"
101
+ when false
102
+ ":0\r\n"
103
+ when Integer
104
+ ":#{data}\r\n"
105
+ when Array
106
+ "*#{data.size}\r\n" + data.map { |i| compose(i) }.join
107
+ when NilClass
108
+ # Null Bulk String, not Null Array of "*-1\r\n"
109
+ "$-1\r\n"
110
+ when WrongTypeError
111
+ "-WRONGTYPE #{data.message}\r\n"
112
+ when StandardError
113
+ "-ERR #{data.message}\r\n"
114
+ else
115
+ # always Bulk String
116
+ "$#{data.bytesize}\r\n#{data}\r\n"
117
+ end
118
+ end
119
+ ```
120
+
121
+ ### RDKit::Core
122
+
123
+ You are required to implement a `tick!` method. `RDKit` will call it periodically (currently roughly every 0.1 sec), this gives you a chance to do some house-keeping. For example:
124
+
125
+ ```ruby
126
+ def tick!
127
+ save_non_critical_data! if server.cycles % 1000 == 0
128
+ end
129
+ ```
130
+
131
+ ### Examples
132
+
133
+ See examples under `example` folder.
37
134
 
38
- ### Implementing a counter server
135
+ #### Implementing a counter server
39
136
 
40
137
  A simple counter server source code listing:
41
138
 
@@ -123,7 +220,7 @@ server.start
123
220
 
124
221
  ```
125
222
 
126
- ### Connect using `redis-cli`
223
+ #### Connect using `redis-cli`
127
224
 
128
225
  ```shell
129
226
  $ redis-cli -p 3721
@@ -166,7 +263,9 @@ total_commands_processed:6
166
263
  (error) ERR unknown command 'xx'
167
264
  ```
168
265
 
169
- ### Benchmarking with `redis-benchmark`
266
+ Hint: if you are adventurous, try `info all`
267
+
268
+ #### Benchmarking with `redis-benchmark`
170
269
 
171
270
  ```shell
172
271
  $ redis-benchmark -p 3721 incr
@@ -196,6 +295,183 @@ Since it is single-threaded, the count will be correct:
196
295
  (integer) 10000
197
296
  ```
198
297
 
298
+ #### Implementing blocked commands
299
+
300
+ Some commands will be blocking: they may either depend on external services or need some background tasks to be run.
301
+
302
+ The clients will expect those commands to be blocking calls, they will not return until the commands are finished, but we don't want the server to be blocked as well.
303
+
304
+ Therefore we introduce `Server#blocking` methods, execution wrapped in this method call will be run in a background thread pool, and the client will be on hold until that task is finished.
305
+
306
+ Example: see `examples/blocking` folder.
307
+
308
+ ```ruby
309
+ # blocking/command_runner.rb
310
+
311
+ module Blocking
312
+ class CommandRunner < RDKit::RESPRunner
313
+ attr_reader :core
314
+
315
+ def initialize(core)
316
+ @core = core
317
+ end
318
+
319
+ def block_with_callback
320
+ core.block_with_callback
321
+
322
+ # this is ignored, instead `on_success` block of `core.block_with_callback` is evaluated and returned
323
+ 'OK'
324
+ end
325
+
326
+ def block
327
+ core.block
328
+
329
+ 'OK'
330
+ end
331
+
332
+ def nonblock
333
+ core.nonblock
334
+
335
+ 'OK'
336
+ end
337
+ end
338
+ end
339
+
340
+ # blocking/core.rb
341
+
342
+ module Blocking
343
+ class Core < RDKit::Core
344
+ def block_with_callback
345
+ on_success = lambda { 'success' }
346
+
347
+ server.blocking(on_success) { do_something }
348
+ end
349
+
350
+ def block
351
+ server.blocking { do_something }
352
+ end
353
+
354
+ def nonblock
355
+ do_something
356
+ end
357
+
358
+ def do_something
359
+ sleep 1
360
+ end
361
+
362
+ def tick!
363
+ end
364
+ end
365
+ end
366
+ ```
367
+
368
+ Running:
369
+
370
+ ```shell
371
+ $ redis-cli -p 3721
372
+ 127.0.0.1:3721> block
373
+ OK
374
+ (1.03s)
375
+ 127.0.0.1:3721> nonblock
376
+ OK
377
+ (1.01s)
378
+ 127.0.0.1:3721> block_with_callback
379
+ "success"
380
+ (1.02s)
381
+ ```
382
+
383
+ Benchmarking:
384
+
385
+ ```shell
386
+ $ redis-benchmark -p 3721 -n 10 block
387
+ ====== block ======
388
+ 10 requests completed in 1.03 seconds
389
+ 50 parallel clients
390
+ 3 bytes payload
391
+ keep alive: 1
392
+
393
+ 10.00% <= 1027 milliseconds
394
+ 100.00% <= 1027 milliseconds
395
+ 9.73 requests per second
396
+
397
+ $ redis-benchmark -p 3721 -n 10 nonblock
398
+ ====== nonblock ======
399
+ 10 requests completed in 10.04 seconds
400
+ 50 parallel clients
401
+ 3 bytes payload
402
+ keep alive: 1
403
+
404
+ 10.00% <= 1001 milliseconds
405
+ 20.00% <= 2005 milliseconds
406
+ 30.00% <= 3010 milliseconds
407
+ 40.00% <= 4013 milliseconds
408
+ 50.00% <= 5018 milliseconds
409
+ 60.00% <= 6022 milliseconds
410
+ 70.00% <= 7027 milliseconds
411
+ 80.00% <= 8030 milliseconds
412
+ 90.00% <= 9034 milliseconds
413
+ 100.00% <= 10039 milliseconds
414
+ 1.00 requests per second
415
+
416
+ ```
417
+
418
+ See the difference between blocking and non-blocking commands?
419
+
420
+ ### Implemented Redis Commands
421
+
422
+ | command | support | note |
423
+ |-------------|--------------------------------------|----------------------------------------------|
424
+ | `info` | full | additional `objspace` and `gc` commands |
425
+ | `ping` | full | |
426
+ | `echo` | full | |
427
+ | `time` | full | |
428
+ | `select` | partial/compatible | `redis-benchmark` requires `select` command |
429
+ | `config` | `get`, `set`, `resetstat` | |
430
+ | `slowlog` | full | |
431
+ | `client` | `getname`, `setname`, `list`, `kill` | `kill` filter only supports `id`, `addr` |
432
+ | `monitor` | full | |
433
+ | `debug` | `sleep`, `segfault` | |
434
+ | `shutdown` | full | |
435
+ | `get` | full | |
436
+ | `set` | without options | |
437
+ | `del` | full | |
438
+ | `keys` | without pattern (return all) | |
439
+ | `lpush` | full | |
440
+ | `lpop` | full | |
441
+ | `rpop` | full | |
442
+ | `llen` | full | |
443
+ | `lrange` | partial (not fully tested) | |
444
+ | `exists` | full | |
445
+ | `flushdb` | full | |
446
+ | `flushall` | full | |
447
+ | `mget` | full | |
448
+ | `mset` | full | |
449
+ | `strlen` | full | |
450
+ | `sadd` | full | |
451
+ | `scard` | full | |
452
+ | `smembers` | full | |
453
+ | `sismember` | full | |
454
+ | `srem` | full | |
455
+ | `hset` | full | |
456
+ | `hget` | full | |
457
+ | `hexists` | full | |
458
+ | `hlen` | full | |
459
+ | `hstrlen` | full | |
460
+ | `hdel` | full | |
461
+ | `hkeys` | full | |
462
+ | `hvals` | full | |
463
+ | `setnx` | full | |
464
+ | `getset` | full | |
465
+
466
+
467
+ ### Implemented Additional Commands
468
+
469
+ | command | description |
470
+ |-------------|----------------------------------------------------------------------------|
471
+ | `gc` | start garbage collection immediately |
472
+ | `heapdump` | `ObjectSpace.dump_all` to ./tmp |
473
+
474
+
199
475
  ## Development
200
476
 
201
477
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
data/Vagrantfile ADDED
@@ -0,0 +1,13 @@
1
+ VAGRANTFILE_API_VERSION = "2"
2
+
3
+ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
4
+ config.vm.box = "ubuntu/trusty64"
5
+
6
+ config.vm.define 'redis' do |c|
7
+ config.vm.provision "shell", inline: "sudo apt-get install redis-tools -y"
8
+
9
+ c.vm.provision "docker" do |docker|
10
+ docker.run "redis", args: "-p 6379:6379"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ require 'rdkit'
2
+
3
+ require_relative 'blocking/version'
4
+ require_relative 'blocking/core'
5
+ require_relative 'blocking/command_runner'
6
+ require_relative 'blocking/server'
7
+
8
+ server = Blocking::Server.new
9
+
10
+ server.start