rdkit 0.0.1
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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +209 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/example/counter.rb +12 -0
- data/example/counter/command_runner.rb +16 -0
- data/example/counter/core.rb +31 -0
- data/example/counter/server.rb +17 -0
- data/example/counter/version.rb +3 -0
- data/lib/rdkit.rb +10 -0
- data/lib/rdkit/command_parser.rb +45 -0
- data/lib/rdkit/core.rb +13 -0
- data/lib/rdkit/errors.rb +12 -0
- data/lib/rdkit/inheritable.rb +15 -0
- data/lib/rdkit/introspection.rb +82 -0
- data/lib/rdkit/logger.rb +27 -0
- data/lib/rdkit/resp.rb +26 -0
- data/lib/rdkit/resp_runner.rb +46 -0
- data/lib/rdkit/server.rb +192 -0
- data/lib/rdkit/version.rb +3 -0
- data/rdkit.gemspec +26 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b7f8dab0976c2aaf2d8045793c9c52713a7ff867
|
4
|
+
data.tar.gz: 0cb9f68b9684fc6c2a5e553202a724396ae75d46
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2e1c7b165b6cc10f382afd74618dd5a5df2a1722e8bbad9a5eb69c2742449fe6f2ca6e39ec0a4b163574b4cce8ae8aaf78a45ccbafddd18e26dc7896e072956d
|
7
|
+
data.tar.gz: 2b6889a5764936fb592b0d29418aab64108c7cb53f1b22c2d2e87381a9259e2f8ab7f3f8f4e0e3366a5e554f1399d6a6218b8a28317ed5ef30f7b83ace5a964f
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# RDKit
|
2
|
+
|
3
|
+
RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.
|
4
|
+
|
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
|
+
|
7
|
+
- `redis-cli`
|
8
|
+
- `redis-benchmark`
|
9
|
+
- [Redic](https://github.com/amakawa/redic)
|
10
|
+
|
11
|
+
And a lot more.
|
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)
|
14
|
+
|
15
|
+
[](https://codeclimate.com/github/forresty/rdkit)
|
16
|
+
[](https://travis-ci.org/forresty/rdkit)
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'rdkit'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install rdkit
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
see examples under `example` folder.
|
37
|
+
|
38
|
+
### Implementing a counter server
|
39
|
+
|
40
|
+
A simple counter server source code listing:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'rdkit'
|
44
|
+
|
45
|
+
# counter/version.rb
|
46
|
+
module Counter
|
47
|
+
VERSION = '0.0.1'
|
48
|
+
end
|
49
|
+
|
50
|
+
# counter/core.rb
|
51
|
+
module Counter
|
52
|
+
class Core < RDKit::Core
|
53
|
+
attr_accessor :count
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@count = 0
|
57
|
+
@last_tick = Time.now
|
58
|
+
end
|
59
|
+
|
60
|
+
# `tick!` is called periodically by RDKit
|
61
|
+
def tick!
|
62
|
+
@last_tick = Time.now
|
63
|
+
end
|
64
|
+
|
65
|
+
def incr(n)
|
66
|
+
@count += n
|
67
|
+
end
|
68
|
+
|
69
|
+
def introspection
|
70
|
+
{
|
71
|
+
counter_version: Counter::VERSION,
|
72
|
+
count: @count,
|
73
|
+
last_tick: @last_tick
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# counter/command_runner.rb
|
80
|
+
module Counter
|
81
|
+
class CommandRunner < RDKit::RESPRunner
|
82
|
+
def initialize(counter)
|
83
|
+
@counter = counter
|
84
|
+
end
|
85
|
+
|
86
|
+
# every public method of this class will be accessible by clients
|
87
|
+
def count
|
88
|
+
@counter.count
|
89
|
+
end
|
90
|
+
|
91
|
+
def incr(n=1)
|
92
|
+
@counter.incr(n.to_i)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# counter/server.rb
|
98
|
+
module Counter
|
99
|
+
class Server < RDKit::Server
|
100
|
+
def initialize
|
101
|
+
super('0.0.0.0', 3721)
|
102
|
+
|
103
|
+
# @core is required by RDKit
|
104
|
+
@core = Core.new
|
105
|
+
|
106
|
+
# @runner is also required by RDKit
|
107
|
+
@runner = CommandRunner.new(@core)
|
108
|
+
end
|
109
|
+
|
110
|
+
def introspection
|
111
|
+
super.merge(counter: @core.introspection)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# start server
|
117
|
+
|
118
|
+
server = Counter::Server.new
|
119
|
+
|
120
|
+
trap(:INT) { server.stop }
|
121
|
+
|
122
|
+
server.start
|
123
|
+
|
124
|
+
```
|
125
|
+
|
126
|
+
### Connect using `redis-cli`
|
127
|
+
|
128
|
+
```shell
|
129
|
+
$ redis-cli -p 3721
|
130
|
+
127.0.0.1:3721> count
|
131
|
+
(integer) 0
|
132
|
+
127.0.0.1:3721> incr
|
133
|
+
(integer) 1
|
134
|
+
127.0.0.1:3721> incr 10
|
135
|
+
(integer) 11
|
136
|
+
127.0.0.1:3721> count
|
137
|
+
(integer) 11
|
138
|
+
127.0.0.1:3721> info
|
139
|
+
# Server
|
140
|
+
rdkit_version:0.0.1
|
141
|
+
multiplexing_api:select
|
142
|
+
process_id:15083
|
143
|
+
tcp_port:3721
|
144
|
+
uptime_in_seconds:268
|
145
|
+
uptime_in_days:0
|
146
|
+
hz:10
|
147
|
+
|
148
|
+
# Clients
|
149
|
+
connected_clients:1
|
150
|
+
connected_clients_peak:1
|
151
|
+
|
152
|
+
# Memory
|
153
|
+
used_memory_rss:31.89M
|
154
|
+
used_memory_peak:31.89M
|
155
|
+
|
156
|
+
# Counter
|
157
|
+
counter_version:0.0.1
|
158
|
+
count:11
|
159
|
+
last_tick:2015-05-27 20:15:38 +0800
|
160
|
+
|
161
|
+
# Stats
|
162
|
+
total_connections_received:1
|
163
|
+
total_commands_processed:6
|
164
|
+
|
165
|
+
127.0.0.1:3721> xx
|
166
|
+
(error) ERR unknown command 'xx'
|
167
|
+
```
|
168
|
+
|
169
|
+
### Benchmarking with `redis-benchmark`
|
170
|
+
|
171
|
+
```shell
|
172
|
+
$ redis-benchmark -p 3721 incr
|
173
|
+
====== count ======
|
174
|
+
10000 requests completed in 0.73 seconds
|
175
|
+
50 parallel clients
|
176
|
+
3 bytes payload
|
177
|
+
keep alive: 1
|
178
|
+
|
179
|
+
0.01% <= 1 milliseconds
|
180
|
+
2.27% <= 2 milliseconds
|
181
|
+
42.31% <= 3 milliseconds
|
182
|
+
63.99% <= 4 milliseconds
|
183
|
+
96.14% <= 5 milliseconds
|
184
|
+
...
|
185
|
+
99.97% <= 68 milliseconds
|
186
|
+
99.98% <= 71 milliseconds
|
187
|
+
99.99% <= 74 milliseconds
|
188
|
+
100.00% <= 77 milliseconds
|
189
|
+
13679.89 requests per second
|
190
|
+
```
|
191
|
+
|
192
|
+
Since it is single-threaded, the count will be correct:
|
193
|
+
|
194
|
+
```shell
|
195
|
+
127.0.0.1:3721> count
|
196
|
+
(integer) 10000
|
197
|
+
```
|
198
|
+
|
199
|
+
## Development
|
200
|
+
|
201
|
+
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.
|
202
|
+
|
203
|
+
## Contributing
|
204
|
+
|
205
|
+
1. Fork it ( https://github.com/forresty/rdkit/fork )
|
206
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
207
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
208
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
209
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rdkit"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/example/counter.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Counter
|
2
|
+
class CommandRunner < RDKit::RESPRunner
|
3
|
+
def initialize(counter)
|
4
|
+
@counter = counter
|
5
|
+
end
|
6
|
+
|
7
|
+
# every public method of this class will be accessible by clients
|
8
|
+
def count
|
9
|
+
@counter.count
|
10
|
+
end
|
11
|
+
|
12
|
+
def incr(n=1)
|
13
|
+
@counter.incr(n.to_i)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Counter
|
2
|
+
class Core < RDKit::Core
|
3
|
+
attr_accessor :count
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@count = 0
|
7
|
+
@last_tick = Time.now
|
8
|
+
end
|
9
|
+
|
10
|
+
def incr(n)
|
11
|
+
@count += n
|
12
|
+
end
|
13
|
+
|
14
|
+
###########################################
|
15
|
+
# overriding required RDKit::Core methods
|
16
|
+
###########################################
|
17
|
+
|
18
|
+
# `tick!` is called periodically by RDKit
|
19
|
+
def tick!
|
20
|
+
@last_tick = Time.now
|
21
|
+
end
|
22
|
+
|
23
|
+
def introspection
|
24
|
+
{
|
25
|
+
counter_version: Counter::VERSION,
|
26
|
+
count: @count,
|
27
|
+
last_tick: @last_tick
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Counter
|
2
|
+
class Server < RDKit::Server
|
3
|
+
def initialize
|
4
|
+
super('0.0.0.0', 3721)
|
5
|
+
|
6
|
+
# @core is required by RDKit
|
7
|
+
@core = Core.new
|
8
|
+
|
9
|
+
# @runner is also required by RDKit
|
10
|
+
@runner = CommandRunner.new(@core)
|
11
|
+
end
|
12
|
+
|
13
|
+
def introspection
|
14
|
+
super.merge(counter: @core.introspection)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/rdkit.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require_relative 'rdkit/version'
|
2
|
+
require_relative 'rdkit/errors'
|
3
|
+
require_relative 'rdkit/resp'
|
4
|
+
require_relative 'rdkit/logger'
|
5
|
+
require_relative 'rdkit/introspection'
|
6
|
+
require_relative 'rdkit/inheritable'
|
7
|
+
require_relative 'rdkit/core'
|
8
|
+
require_relative 'rdkit/command_parser'
|
9
|
+
require_relative 'rdkit/resp_runner'
|
10
|
+
require_relative 'rdkit/server'
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "hiredis/reader"
|
2
|
+
# http://redis.io/topics/protocol
|
3
|
+
# Hiredis::Reader does not handle inline commands, so
|
4
|
+
|
5
|
+
module RDKit
|
6
|
+
class CommandParser
|
7
|
+
def initialize
|
8
|
+
@reader = Hiredis::Reader.new
|
9
|
+
@buffer = []
|
10
|
+
@regexp = Regexp.new("\\A(.+)\\r\\n\\z")
|
11
|
+
@error = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def feed(data)
|
15
|
+
if data =~ @regexp
|
16
|
+
@buffer << $1.split
|
17
|
+
else
|
18
|
+
@reader.feed(data)
|
19
|
+
|
20
|
+
read_into_buffer!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def gets
|
25
|
+
raise @error unless @error.nil?
|
26
|
+
|
27
|
+
if result = @buffer.shift
|
28
|
+
|
29
|
+
result
|
30
|
+
else
|
31
|
+
@reader.gets
|
32
|
+
end
|
33
|
+
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
|
+
end
|
45
|
+
end
|
data/lib/rdkit/core.rb
ADDED
data/lib/rdkit/errors.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module RDKit
|
2
|
+
class RDKitError < StandardError; end
|
3
|
+
|
4
|
+
class ProtocolError < RDKitError; end
|
5
|
+
class UnknownCommandError < ProtocolError; end
|
6
|
+
class WrongNumberOfArgumentError < ProtocolError; end
|
7
|
+
|
8
|
+
class NotImplementedError < RDKitError; end
|
9
|
+
|
10
|
+
class SDKRequirementNotMetError < RDKitError; end
|
11
|
+
class ShouldOverrideError < SDKRequirementNotMetError; end
|
12
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# 127.0.0.1:6379> info
|
2
|
+
# # Server
|
3
|
+
# redis_version:2.8.17
|
4
|
+
# redis_git_sha1:00000000
|
5
|
+
# redis_git_dirty:0
|
6
|
+
# redis_build_id:32eb139b4f2b63
|
7
|
+
# redis_mode:standalone
|
8
|
+
# os:Darwin 13.4.0 x86_64
|
9
|
+
# arch_bits:64
|
10
|
+
# multiplexing_api:kqueue
|
11
|
+
# gcc_version:4.2.1
|
12
|
+
# process_id:471
|
13
|
+
# run_id:efa5b449cc3ba9c53f7bbb159a773e6ff20d575c
|
14
|
+
# tcp_port:6379
|
15
|
+
# uptime_in_seconds:1510124
|
16
|
+
# uptime_in_days:17
|
17
|
+
# hz:10
|
18
|
+
#
|
19
|
+
# # Clients
|
20
|
+
# connected_clients:1
|
21
|
+
# client_longest_output_list:0
|
22
|
+
# client_biggest_input_buf:0
|
23
|
+
#
|
24
|
+
# # Memory
|
25
|
+
# used_memory:124510288
|
26
|
+
# used_memory_human:118.74M
|
27
|
+
# used_memory_rss:119885824
|
28
|
+
# used_memory_peak:124510288
|
29
|
+
# used_memory_peak_human:118.74M
|
30
|
+
# used_memory_lua:33792
|
31
|
+
# mem_fragmentation_ratio:0.96
|
32
|
+
# mem_allocator:libc
|
33
|
+
#
|
34
|
+
# # Persistence
|
35
|
+
#
|
36
|
+
# # Stats
|
37
|
+
# total_connections_received:74408
|
38
|
+
# total_commands_processed:5122851
|
39
|
+
# instantaneous_ops_per_sec:0
|
40
|
+
# rejected_connections:0
|
41
|
+
# pubsub_channels:0
|
42
|
+
# pubsub_patterns:0
|
43
|
+
|
44
|
+
module RDKit
|
45
|
+
module Introspection
|
46
|
+
class Stats
|
47
|
+
attr_reader :data
|
48
|
+
|
49
|
+
def initialize
|
50
|
+
@data = {}
|
51
|
+
|
52
|
+
@data.default = 0
|
53
|
+
end
|
54
|
+
|
55
|
+
module ClassMethods
|
56
|
+
@@instance = Stats.new
|
57
|
+
|
58
|
+
def incr(key)
|
59
|
+
@@instance.data[key] += 1
|
60
|
+
end
|
61
|
+
|
62
|
+
def info
|
63
|
+
@@instance.data
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class << self; include ClassMethods; end
|
68
|
+
end
|
69
|
+
|
70
|
+
module ClassMethods
|
71
|
+
def register(server)
|
72
|
+
@@server = server
|
73
|
+
end
|
74
|
+
|
75
|
+
def info
|
76
|
+
@@server.introspection.merge({ stats: Stats.info })
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class << self; include ClassMethods; end
|
81
|
+
end
|
82
|
+
end
|
data/lib/rdkit/logger.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module RDKit
|
2
|
+
class Logger
|
3
|
+
def debug(message)
|
4
|
+
return unless $DEBUG && ENV['RACK_ENV'] != 'test'
|
5
|
+
|
6
|
+
log(message)
|
7
|
+
end
|
8
|
+
|
9
|
+
def info(message)
|
10
|
+
log(message)
|
11
|
+
end
|
12
|
+
|
13
|
+
def warn(message)
|
14
|
+
log(message)
|
15
|
+
end
|
16
|
+
|
17
|
+
def log(message)
|
18
|
+
case message
|
19
|
+
when StandardError
|
20
|
+
puts message.inspect
|
21
|
+
puts message.backtrace.join("\n")
|
22
|
+
else
|
23
|
+
puts message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/rdkit/resp.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# http://redis.io/topics/protocol
|
2
|
+
|
3
|
+
module RDKit
|
4
|
+
class RESP
|
5
|
+
module ClassMethods
|
6
|
+
def compose(data)
|
7
|
+
case data
|
8
|
+
when Integer
|
9
|
+
":#{data}\r\n"
|
10
|
+
when Array
|
11
|
+
"*#{data.size}\r\n" + data.map { |i| compose(i) }.join
|
12
|
+
when NilClass
|
13
|
+
# Null Bulk String, not Null Array of "*-1\r\n"
|
14
|
+
"$-1\r\n"
|
15
|
+
when StandardError
|
16
|
+
"-#{data.message}\r\n"
|
17
|
+
else
|
18
|
+
# always Bulk String
|
19
|
+
"$#{data.bytesize}\r\n#{data}\r\n"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self; include ClassMethods; end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module RDKit
|
2
|
+
class RESPRunner
|
3
|
+
include Inheritable
|
4
|
+
|
5
|
+
def resp(cmd)
|
6
|
+
RESP.compose(call(cmd))
|
7
|
+
rescue => e
|
8
|
+
RESP.compose(e)
|
9
|
+
end
|
10
|
+
|
11
|
+
# 获取服务器状态
|
12
|
+
def info
|
13
|
+
Introspection.info.map do |type, value|
|
14
|
+
"# #{type.capitalize}\r\n" + value.map { |k, v| "#{k}:#{v}" }.join("\r\n") + "\r\n"
|
15
|
+
end.join("\r\n") + "\r\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def call(cmd)
|
21
|
+
@logger ||= Logger.new
|
22
|
+
|
23
|
+
Introspection::Stats.incr(:total_commands_processed)
|
24
|
+
|
25
|
+
@logger.debug "running command: #{cmd}"
|
26
|
+
cmd, *args = cmd
|
27
|
+
|
28
|
+
cmd.downcase!
|
29
|
+
|
30
|
+
if self.respond_to?(cmd)
|
31
|
+
self.__send__(cmd, *args)
|
32
|
+
else
|
33
|
+
raise UnknownCommandError, "ERR unknown command '#{cmd}'"
|
34
|
+
end
|
35
|
+
rescue ArgumentError => e
|
36
|
+
raise WrongNumberOfArgumentError, "ERR wrong number of arguments for '#{cmd}' command"
|
37
|
+
end
|
38
|
+
|
39
|
+
module RedisCompatibility
|
40
|
+
def select(id); 'OK'; end
|
41
|
+
def ping; 'PONG'; end
|
42
|
+
end
|
43
|
+
|
44
|
+
include RedisCompatibility
|
45
|
+
end
|
46
|
+
end
|
data/lib/rdkit/server.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'newrelic_rpm'
|
2
|
+
|
3
|
+
module RDKit
|
4
|
+
class Server
|
5
|
+
HZ = 10
|
6
|
+
CYCLES_TIL_MEMORY_RESAMPLE = 1000
|
7
|
+
|
8
|
+
attr_reader :server_up_since
|
9
|
+
attr_reader :runner
|
10
|
+
attr_reader :core
|
11
|
+
attr_reader :host, :port
|
12
|
+
|
13
|
+
def initialize(host, port)
|
14
|
+
@host, @port = host, port
|
15
|
+
|
16
|
+
@cycles = 0
|
17
|
+
@peak_memory = 0
|
18
|
+
@peak_connected_clients = 0
|
19
|
+
|
20
|
+
@clients, @command_parsers = Hash.new, Hash.new
|
21
|
+
|
22
|
+
@logger = Logger.new
|
23
|
+
|
24
|
+
Introspection.register(self)
|
25
|
+
|
26
|
+
@server_up_since = Time.now
|
27
|
+
end
|
28
|
+
|
29
|
+
def sanity_check!
|
30
|
+
unless @host && @port
|
31
|
+
raise SDKRequirementNotMetError, '@host and @port are required for server to run'
|
32
|
+
end
|
33
|
+
|
34
|
+
if @core.nil?
|
35
|
+
raise SDKRequirementNotMetError, '@core is required to represent your business logics'
|
36
|
+
end
|
37
|
+
|
38
|
+
if @runner.nil?
|
39
|
+
raise SDKRequirementNotMetError, '@runner is required to act as an RESP frontend'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def start
|
44
|
+
sanity_check!
|
45
|
+
|
46
|
+
@server_socket = TCPServer.new(@host, @port)
|
47
|
+
|
48
|
+
run_acceptor
|
49
|
+
end
|
50
|
+
|
51
|
+
def stop
|
52
|
+
@logger.warn "signal caught, shutting down..."
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
|
56
|
+
def introspection
|
57
|
+
{
|
58
|
+
server: {
|
59
|
+
rdkit_version: RDKit::VERSION,
|
60
|
+
multiplexing_api: 'select',
|
61
|
+
process_id: Process.pid,
|
62
|
+
tcp_port: @port,
|
63
|
+
uptime_in_seconds: (Time.now - @server_up_since).to_i,
|
64
|
+
uptime_in_days: ((Time.now - @server_up_since) / (24 * 60 * 60)).to_i,
|
65
|
+
hz: HZ,
|
66
|
+
},
|
67
|
+
clients: {
|
68
|
+
connected_clients: @clients.size,
|
69
|
+
connected_clients_peak: @peak_connected_clients
|
70
|
+
},
|
71
|
+
memory: {
|
72
|
+
used_memory_rss: used_memory_rss_in_mb,
|
73
|
+
used_memory_peak: used_memory_peak_in_mb
|
74
|
+
},
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def used_memory_rss_in_mb
|
81
|
+
update_peak_memory!
|
82
|
+
|
83
|
+
'%0.2f' % used_memory_rss + 'M'
|
84
|
+
end
|
85
|
+
|
86
|
+
def used_memory_peak_in_mb
|
87
|
+
'%0.2f' % @peak_memory + 'M'
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_client
|
91
|
+
Introspection::Stats.incr(:total_connections_received)
|
92
|
+
|
93
|
+
socket = @server_socket.accept_nonblock
|
94
|
+
|
95
|
+
@command_parsers[socket] = CommandParser.new
|
96
|
+
|
97
|
+
@clients[socket] = Fiber.new do
|
98
|
+
with_error_handling(socket) do |io|
|
99
|
+
loop { process(io); Fiber.yield }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
@logger.debug "client #{socket} connected"
|
104
|
+
|
105
|
+
return @clients[socket]
|
106
|
+
end
|
107
|
+
|
108
|
+
def process(io)
|
109
|
+
feed_parser(io)
|
110
|
+
|
111
|
+
until (reply = get_parser_reply(io)) == false
|
112
|
+
send_response(io, reply)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def feed_parser(io)
|
117
|
+
cmd = io.readpartial(1024)
|
118
|
+
|
119
|
+
@command_parsers[io].feed(cmd)
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_parser_reply(io)
|
123
|
+
@command_parsers[io].gets
|
124
|
+
end
|
125
|
+
|
126
|
+
def send_response(io, cmd)
|
127
|
+
resp = runner.resp(cmd)
|
128
|
+
|
129
|
+
@logger.debug(resp)
|
130
|
+
|
131
|
+
io.write(resp)
|
132
|
+
end
|
133
|
+
|
134
|
+
def with_error_handling(socket, &block)
|
135
|
+
|
136
|
+
block.call(socket)
|
137
|
+
|
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
|
148
|
+
socket.close
|
149
|
+
@command_parsers.delete(socket)
|
150
|
+
@clients.delete(socket)
|
151
|
+
end
|
152
|
+
|
153
|
+
def run_acceptor
|
154
|
+
@logger.info "accepting on shared socket (#{@host}:#{@port})"
|
155
|
+
|
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
|
170
|
+
|
171
|
+
update_peak_memory! if @cycles % CYCLES_TIL_MEMORY_RESAMPLE == 0
|
172
|
+
update_peak_connected_clients!
|
173
|
+
|
174
|
+
@cycles += 1
|
175
|
+
|
176
|
+
core.tick!
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def update_peak_memory!
|
181
|
+
@peak_memory = [@peak_memory, used_memory_rss].max
|
182
|
+
end
|
183
|
+
|
184
|
+
def update_peak_connected_clients!
|
185
|
+
@peak_connected_clients = [@peak_connected_clients, @clients.size].max
|
186
|
+
end
|
187
|
+
|
188
|
+
def used_memory_rss
|
189
|
+
NewRelic::Agent::Samplers::MemorySampler.new.sampler.get_sample
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/rdkit.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rdkit/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rdkit"
|
8
|
+
spec.version = RDKit::VERSION
|
9
|
+
spec.authors = ["Forrest Ye"]
|
10
|
+
spec.email = ["afu@forresty.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO server.}
|
13
|
+
spec.homepage = "http://github.com/forresty/rdkit"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "exe"
|
17
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_runtime_dependency 'hiredis'
|
21
|
+
spec.add_runtime_dependency 'newrelic_rpm'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.8"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rdkit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Forrest Ye
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hiredis
|
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
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: newrelic_rpm
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- afu@forresty.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- bin/console
|
97
|
+
- bin/setup
|
98
|
+
- example/counter.rb
|
99
|
+
- example/counter/command_runner.rb
|
100
|
+
- example/counter/core.rb
|
101
|
+
- example/counter/server.rb
|
102
|
+
- example/counter/version.rb
|
103
|
+
- lib/rdkit.rb
|
104
|
+
- lib/rdkit/command_parser.rb
|
105
|
+
- lib/rdkit/core.rb
|
106
|
+
- lib/rdkit/errors.rb
|
107
|
+
- lib/rdkit/inheritable.rb
|
108
|
+
- lib/rdkit/introspection.rb
|
109
|
+
- lib/rdkit/logger.rb
|
110
|
+
- lib/rdkit/resp.rb
|
111
|
+
- lib/rdkit/resp_runner.rb
|
112
|
+
- lib/rdkit/server.rb
|
113
|
+
- lib/rdkit/version.rb
|
114
|
+
- rdkit.gemspec
|
115
|
+
homepage: http://github.com/forresty/rdkit
|
116
|
+
licenses: []
|
117
|
+
metadata: {}
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 2.4.5
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: RDKit is a simple toolkit to write Redis-like, single-threaded multiplexing-IO
|
138
|
+
server.
|
139
|
+
test_files: []
|