rdkit 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Code Climate](https://codeclimate.com/github/forresty/rdkit/badges/gpa.svg)](https://codeclimate.com/github/forresty/rdkit)
|
16
|
+
[![Build Status](https://travis-ci.org/forresty/rdkit.svg?branch=master)](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: []
|