ernicorn 1.0.0
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.
- data/.gitignore +3 -0
- data/LICENSE +20 -0
- data/README.md +72 -0
- data/Rakefile +8 -0
- data/ernicorn.gemspec +30 -0
- data/examples/client.rb +13 -0
- data/examples/config.rb +36 -0
- data/examples/handler.rb +49 -0
- data/examples/start.sh +4 -0
- data/lib/ernicorn/adminrpc.rb +39 -0
- data/lib/ernicorn/server.rb +82 -0
- data/lib/ernicorn.rb +252 -0
- data/script/bootstrap +11 -0
- data/script/cibuild +23 -0
- data/script/ernicorn +51 -0
- data/script/ernicorn-ctrl +37 -0
- data/script/testsuite +7 -0
- data/test/helper.rb +4 -0
- data/test/test_ernicorn.rb +69 -0
- metadata +139 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Tom Preston-Werner, Aman Gupta
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
Ernicorn
|
2
|
+
========
|
3
|
+
|
4
|
+
A BERT-RPC server based on [Ernie](https://github.com/mojombo/ernie)'s Ruby
|
5
|
+
interface but that uses [Unicorn](http://unicorn.bogomips.org/) for worker
|
6
|
+
process management.
|
7
|
+
|
8
|
+
Ernicorn supports BERT-RPC `call` and `cast` requests. See the full BERT-RPC
|
9
|
+
specification at [bert-rpc.org](http://bert-rpc.org) for more information.
|
10
|
+
|
11
|
+
Like Ernie before it, Ernicorn was developed at GitHub and is currently in
|
12
|
+
production use serving millions of RPC requests every day.
|
13
|
+
|
14
|
+
Installation
|
15
|
+
------------
|
16
|
+
|
17
|
+
$ gem install ernicorn
|
18
|
+
|
19
|
+
Starting The Server
|
20
|
+
-------------------
|
21
|
+
|
22
|
+
Use the ernicorn command to start a new RPC server:
|
23
|
+
|
24
|
+
[rtomayko@iron:ernicorn]$ ernicorn --help
|
25
|
+
Ernicorn is an Ruby BERT-RPC Server based on Unicorn.
|
26
|
+
|
27
|
+
Basic Command Line Usage:
|
28
|
+
ernicorn [options] <handler>
|
29
|
+
|
30
|
+
-c, --config CONFIG Unicorn style config file
|
31
|
+
-p, --port PORT Port
|
32
|
+
-l, --log-level LOGLEVEL Log level (0-4)
|
33
|
+
-d, --detached Run as a daemon
|
34
|
+
-P, --pidfile PIDFILE Location to write pid file.
|
35
|
+
|
36
|
+
The handler must be given and should be a normal Ruby file that sets up the
|
37
|
+
environment and calls `Ernicorn.expose` for any server modules. See the
|
38
|
+
[examples/handler.rb][h] file for more info.
|
39
|
+
|
40
|
+
[h]: https://github.com/github/ernicorn/blob/master/examples/handler.rb
|
41
|
+
|
42
|
+
Control Commands
|
43
|
+
----------------
|
44
|
+
|
45
|
+
The `ernicorn-ctrl` command can be used to send various control and
|
46
|
+
informational commands to the running server process:
|
47
|
+
|
48
|
+
$ ernicorn-ctrl --help
|
49
|
+
Usage: ernicorn-ctrl [-p <port>] <command>
|
50
|
+
Issue a control command to an ernicorn server. The port option is used to
|
51
|
+
specify a non-default control port.
|
52
|
+
|
53
|
+
Commands:
|
54
|
+
reload-handlers Gracefully reload all handler processes.
|
55
|
+
stats Dump handler stats for the server.
|
56
|
+
halt Shut down.
|
57
|
+
|
58
|
+
Ernicorn servers also support most Unicorn signals for managing worker processes
|
59
|
+
and whatnot. See the [SIGNALS](http://unicorn.bogomips.org/SIGNALS.html) file
|
60
|
+
for more info.
|
61
|
+
|
62
|
+
Development
|
63
|
+
-----------
|
64
|
+
|
65
|
+
The `script/bootstrap` command is provided to setup a local gem environment for
|
66
|
+
development.
|
67
|
+
|
68
|
+
$ script/bootstrap
|
69
|
+
|
70
|
+
It installs all gems under `vendor/gems` and creates binstubs under a `bin`
|
71
|
+
directory. This is the best way to get setup for running tests and experimenting
|
72
|
+
in a sandbox.
|
data/Rakefile
ADDED
data/ernicorn.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
3
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
|
+
s.rubygems_version = '1.3.6'
|
5
|
+
|
6
|
+
s.name = 'ernicorn'
|
7
|
+
s.version = '1.0.0'
|
8
|
+
s.date = '2012-06-23'
|
9
|
+
|
10
|
+
s.summary = "Ernicorn is a BERT-RPC server implementation based on unicorn."
|
11
|
+
s.description = "Ernicorn is a BERT-RPC server packaged as a gem."
|
12
|
+
|
13
|
+
s.authors = ["Tom Preston-Werner", "tmm1", "rtomayko"]
|
14
|
+
s.email = 'tmm1@github.com'
|
15
|
+
s.homepage = 'https://github.com/github/ernicorn'
|
16
|
+
|
17
|
+
s.add_runtime_dependency('bert', ">= 1.1.0")
|
18
|
+
s.add_runtime_dependency('bertrpc', ">= 1.0.0")
|
19
|
+
s.add_runtime_dependency('unicorn', "~> 4.1.1")
|
20
|
+
|
21
|
+
s.add_development_dependency('shoulda', "~> 2.11.3")
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n") - %w[Gemfile Gemfile.lock]
|
24
|
+
s.test_files = `git ls-files -- test`.split("\n").select { |f| f =~ /_test.rb$/ }
|
25
|
+
|
26
|
+
s.bindir = "script"
|
27
|
+
s.executables = %w[ernicorn ernicorn-ctrl]
|
28
|
+
s.require_paths = %w[lib]
|
29
|
+
s.extra_rdoc_files = %w[LICENSE README.md]
|
30
|
+
end
|
data/examples/client.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bertrpc'
|
2
|
+
service = BERTRPC::Service.new('localhost', 9777)
|
3
|
+
|
4
|
+
loop do
|
5
|
+
begin
|
6
|
+
puts ">> add(10, 32)"
|
7
|
+
res = service.call.example.send(:add, 10, 32)
|
8
|
+
puts "=> #{res.inspect}"
|
9
|
+
rescue => boom
|
10
|
+
puts "ERROR: #{boom.class} #{boom}"
|
11
|
+
end
|
12
|
+
sleep 1
|
13
|
+
end
|
data/examples/config.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Ernicorn configuration
|
2
|
+
#
|
3
|
+
# Ernicorn config files typically live at config/ernicorn.rb and support all
|
4
|
+
# Unicorn config options:
|
5
|
+
#
|
6
|
+
# http://unicorn.bogomips.org/Unicorn/Configurator.html
|
7
|
+
#
|
8
|
+
# Example unicorn config files:
|
9
|
+
#
|
10
|
+
# http://unicorn.bogomips.org/examples/unicorn.conf.rb
|
11
|
+
# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb
|
12
|
+
warn "in unicorn config file: #{__FILE__}"
|
13
|
+
|
14
|
+
# server options
|
15
|
+
listen 9777
|
16
|
+
worker_processes 2
|
17
|
+
working_directory File.dirname(__FILE__)
|
18
|
+
|
19
|
+
# ernicorn configuration
|
20
|
+
Ernicorn.loglevel 1
|
21
|
+
|
22
|
+
# enable COW where possible
|
23
|
+
GC.respond_to?(:copy_on_write_friendly=) &&
|
24
|
+
GC.copy_on_write_friendly = true
|
25
|
+
|
26
|
+
# load server code and expose modules
|
27
|
+
require File.expand_path('../handler', __FILE__)
|
28
|
+
Ernicorn.expose(:example, Example)
|
29
|
+
|
30
|
+
# hook into new child immediately after forking
|
31
|
+
after_fork do |server, worker|
|
32
|
+
end
|
33
|
+
|
34
|
+
# hook into master immediately before forking a worker
|
35
|
+
before_fork do |server, worker|
|
36
|
+
end
|
data/examples/handler.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# An example BERTRPC server module. To start the RPC server use:
|
2
|
+
#
|
3
|
+
# $ bin/ernicorn -p 9675 examples/handler.rb
|
4
|
+
#
|
5
|
+
# Then connect and issue commands as follows:
|
6
|
+
#
|
7
|
+
# $ script/console
|
8
|
+
# >> require 'bertrpc'
|
9
|
+
# >> service = BERTRPC::Service.new('localhost', 9675)
|
10
|
+
# => #<BERTRPC::Service:0x1037e9678 @port=9765, @timeout=nil, @host="localhost">
|
11
|
+
# >> service.call.example.send(:add, 1, 2)
|
12
|
+
# => 3
|
13
|
+
# >> service.call.example.send(:fib, 10)
|
14
|
+
# => 89
|
15
|
+
|
16
|
+
module Example
|
17
|
+
# Add two numbers together
|
18
|
+
def add(a, b)
|
19
|
+
a + b
|
20
|
+
end
|
21
|
+
|
22
|
+
def fib(n)
|
23
|
+
if n == 0 || n == 1
|
24
|
+
1
|
25
|
+
else
|
26
|
+
fib(n - 1) + fib(n - 2)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def shadow(x)
|
31
|
+
"ruby"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the given number of bytes
|
35
|
+
def bytes(n)
|
36
|
+
'x' * n
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sleep for +sec+ and then return :ok
|
40
|
+
def slow(sec)
|
41
|
+
sleep(sec)
|
42
|
+
:ok
|
43
|
+
end
|
44
|
+
|
45
|
+
# Throw an error
|
46
|
+
def error
|
47
|
+
raise "abandon hope!"
|
48
|
+
end
|
49
|
+
end
|
data/examples/start.sh
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'unicorn'
|
2
|
+
|
3
|
+
module Ernicorn
|
4
|
+
module AdminRPC
|
5
|
+
def stats
|
6
|
+
queued = active = 0
|
7
|
+
|
8
|
+
Raindrops::Linux.tcp_listener_stats(Unicorn.listener_names).each do |addr,stats|
|
9
|
+
queued += stats.queued
|
10
|
+
active += stats.active
|
11
|
+
end if defined?(Raindrops::Linux.tcp_listener_stats)
|
12
|
+
|
13
|
+
return <<STATS
|
14
|
+
connections.total=#{Stats.connections_total}
|
15
|
+
connections.completed=#{Stats.connections_completed}
|
16
|
+
workers.idle=#{Stats.workers_idle}
|
17
|
+
workers.busy=#{active}
|
18
|
+
queue.high=#{queued}
|
19
|
+
queue.low=0
|
20
|
+
STATS
|
21
|
+
end
|
22
|
+
|
23
|
+
def reload_handlers
|
24
|
+
Process.kill 'USR2', master_pid
|
25
|
+
"Sent USR2 to #{master_pid}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def halt
|
29
|
+
Process.kill 'QUIT', master_pid
|
30
|
+
"Sent QUIT to #{master_pid}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def master_pid
|
34
|
+
$ernicorn.master_pid
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Ernicorn.expose(:__admin__, Ernicorn::AdminRPC)
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'unicorn'
|
2
|
+
|
3
|
+
module Ernicorn
|
4
|
+
Stats = Raindrops::Struct.new(:connections_total,
|
5
|
+
:connections_completed,
|
6
|
+
:workers_idle).new
|
7
|
+
|
8
|
+
class Server < Unicorn::HttpServer
|
9
|
+
|
10
|
+
# Private HttpServer methods we're overriding to
|
11
|
+
# implement BertRPC instead of HTTP.
|
12
|
+
|
13
|
+
def initialize(app, options={})
|
14
|
+
super app, options
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_app!
|
18
|
+
end
|
19
|
+
|
20
|
+
def worker_loop(worker)
|
21
|
+
Stats.incr_workers_idle
|
22
|
+
|
23
|
+
if worker.nr == (self.worker_processes - 1)
|
24
|
+
old_pid = "#{self.pid}.oldbin"
|
25
|
+
if File.exists?(old_pid) && self.pid != old_pid
|
26
|
+
begin
|
27
|
+
Process.kill("QUIT", File.read(old_pid).to_i)
|
28
|
+
rescue Errno::ENOENT, Errno::ESRCH
|
29
|
+
# someone else did our job for us
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
super
|
35
|
+
ensure
|
36
|
+
Stats.decr_workers_idle
|
37
|
+
end
|
38
|
+
|
39
|
+
def process_client(client)
|
40
|
+
Stats.decr_workers_idle
|
41
|
+
Stats.incr_connections_total
|
42
|
+
|
43
|
+
@client = client
|
44
|
+
# bail out if client only sent EOF
|
45
|
+
return if @client.kgio_trypeek(1).nil?
|
46
|
+
|
47
|
+
iruby, oruby = Ernicorn.process(self, self)
|
48
|
+
rescue EOFError
|
49
|
+
logger.error("EOF from #{@client.kgio_addr rescue nil}")
|
50
|
+
rescue Object => e
|
51
|
+
logger.error(e)
|
52
|
+
|
53
|
+
begin
|
54
|
+
error = t[:error, t[:server, 0, e.class.to_s, e.message, e.backtrace]]
|
55
|
+
Ernicorn.write_berp(self, error)
|
56
|
+
rescue Object => ex
|
57
|
+
logger.error(ex)
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
@client.close rescue nil
|
61
|
+
@client = nil
|
62
|
+
|
63
|
+
Stats.incr_connections_completed
|
64
|
+
Stats.incr_workers_idle
|
65
|
+
end
|
66
|
+
|
67
|
+
# We pass ourselves as both input and output to Ernicorn.process, which
|
68
|
+
# calls the following blocking read/write methods.
|
69
|
+
|
70
|
+
def read(len)
|
71
|
+
data = ''
|
72
|
+
while data.bytesize < len
|
73
|
+
data << @client.kgio_read!(len - data.bytesize)
|
74
|
+
end
|
75
|
+
data
|
76
|
+
end
|
77
|
+
|
78
|
+
def write(data)
|
79
|
+
@client.kgio_write(data)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/ernicorn.rb
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
require 'bert'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Ernicorn
|
5
|
+
VERSION = '1.0.0'
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :mods, :current_mod, :log
|
9
|
+
attr_accessor :auto_start
|
10
|
+
attr_accessor :count, :virgin_procline
|
11
|
+
end
|
12
|
+
|
13
|
+
self.count = 0
|
14
|
+
self.virgin_procline = $0
|
15
|
+
self.mods = {}
|
16
|
+
self.current_mod = nil
|
17
|
+
self.log = Logger.new(STDOUT)
|
18
|
+
self.log.level = Logger::FATAL
|
19
|
+
self.auto_start = true
|
20
|
+
|
21
|
+
# Record a module.
|
22
|
+
# +name+ is the module Symbol
|
23
|
+
# +block+ is the Block containing function definitions
|
24
|
+
#
|
25
|
+
# Returns nothing
|
26
|
+
def self.mod(name, block)
|
27
|
+
m = Mod.new(name)
|
28
|
+
self.current_mod = m
|
29
|
+
self.mods[name] = m
|
30
|
+
block.call
|
31
|
+
end
|
32
|
+
|
33
|
+
# Record a function.
|
34
|
+
# +name+ is the function Symbol
|
35
|
+
# +block+ is the Block to associate
|
36
|
+
#
|
37
|
+
# Returns nothing
|
38
|
+
def self.fun(name, block)
|
39
|
+
self.current_mod.fun(name, block)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Expose all public methods in a Ruby module:
|
43
|
+
# +name+ is the ernie module Symbol
|
44
|
+
# +mixin+ is the ruby module whose public methods are exposed
|
45
|
+
#
|
46
|
+
# Returns nothing
|
47
|
+
def self.expose(name, mixin)
|
48
|
+
context = Object.new
|
49
|
+
context.extend mixin
|
50
|
+
mod(name, lambda {
|
51
|
+
mixin.public_instance_methods.each do |meth|
|
52
|
+
fun(meth.to_sym, context.method(meth))
|
53
|
+
end
|
54
|
+
})
|
55
|
+
if defined? mixin.dispatched
|
56
|
+
self.current_mod.logger = mixin.method(:dispatched)
|
57
|
+
end
|
58
|
+
context
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set the logfile to given path.
|
62
|
+
# +file+ is the String path to the logfile
|
63
|
+
#
|
64
|
+
# Returns nothing
|
65
|
+
def self.logfile(file)
|
66
|
+
self.log = Logger.new(file)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Set the log level.
|
70
|
+
# +level+ is the Logger level (Logger::WARN, etc)
|
71
|
+
#
|
72
|
+
# Returns nothing
|
73
|
+
def self.loglevel(level)
|
74
|
+
self.log.level = level
|
75
|
+
end
|
76
|
+
|
77
|
+
# Dispatch the request to the proper mod:fun.
|
78
|
+
# +mod+ is the module Symbol
|
79
|
+
# +fun+ is the function Symbol
|
80
|
+
# +args+ is the Array of arguments
|
81
|
+
#
|
82
|
+
# Returns the Ruby object response
|
83
|
+
def self.dispatch(mod, fun, args)
|
84
|
+
self.mods[mod] || raise(ServerError.new("No such module '#{mod}'"))
|
85
|
+
self.mods[mod].funs[fun] || raise(ServerError.new("No such function '#{mod}:#{fun}'"))
|
86
|
+
|
87
|
+
start = Time.now
|
88
|
+
ret = self.mods[mod].funs[fun].call(*args)
|
89
|
+
|
90
|
+
begin
|
91
|
+
self.mods[mod].logger and
|
92
|
+
self.mods[mod].logger.call(Time.now-start, [fun, *args], ret)
|
93
|
+
rescue Object => e
|
94
|
+
self.log.fatal(e)
|
95
|
+
end
|
96
|
+
|
97
|
+
ret
|
98
|
+
end
|
99
|
+
|
100
|
+
# Read the length header from the wire.
|
101
|
+
# +input+ is the IO from which to read
|
102
|
+
#
|
103
|
+
# Returns the size Integer if one was read
|
104
|
+
# Returns nil otherwise
|
105
|
+
def self.read_4(input)
|
106
|
+
raw = input.read(4)
|
107
|
+
return nil unless raw
|
108
|
+
raw.unpack('N').first
|
109
|
+
end
|
110
|
+
|
111
|
+
# Read a BERP from the wire and decode it to a Ruby object.
|
112
|
+
# +input+ is the IO from which to read
|
113
|
+
#
|
114
|
+
# Returns a Ruby object if one could be read
|
115
|
+
# Returns nil otherwise
|
116
|
+
def self.read_berp(input)
|
117
|
+
packet_size = self.read_4(input)
|
118
|
+
return nil unless packet_size
|
119
|
+
bert = input.read(packet_size)
|
120
|
+
BERT.decode(bert)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Write the given Ruby object to the wire as a BERP.
|
124
|
+
# +output+ is the IO on which to write
|
125
|
+
# +ruby+ is the Ruby object to encode
|
126
|
+
#
|
127
|
+
# Returns nothing
|
128
|
+
def self.write_berp(output, ruby)
|
129
|
+
data = BERT.encode(ruby)
|
130
|
+
output.write([data.length].pack("N"))
|
131
|
+
output.write(data)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Start the processing loop.
|
135
|
+
#
|
136
|
+
# Loops forever
|
137
|
+
def self.start
|
138
|
+
self.procline('starting')
|
139
|
+
self.log.info("(#{Process.pid}) Starting") if self.log.level <= Logger::INFO
|
140
|
+
self.log.debug(self.mods.inspect) if self.log.level <= Logger::DEBUG
|
141
|
+
|
142
|
+
input = IO.new(3)
|
143
|
+
output = IO.new(4)
|
144
|
+
input.sync = true
|
145
|
+
output.sync = true
|
146
|
+
|
147
|
+
loop do
|
148
|
+
process(input, output)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Processes a single BertRPC command.
|
153
|
+
#
|
154
|
+
# input - The IO to #read command BERP from.
|
155
|
+
# output - The IO to #write reply BERP to.
|
156
|
+
#
|
157
|
+
# Returns a [iruby, oruby] tuple of incoming and outgoing BERPs processed.
|
158
|
+
def self.process(input, output)
|
159
|
+
self.procline('waiting')
|
160
|
+
iruby = self.read_berp(input)
|
161
|
+
self.count += 1
|
162
|
+
|
163
|
+
unless iruby
|
164
|
+
puts "Could not read BERP length header. Ernicorn server may have gone away. Exiting now."
|
165
|
+
if self.log.level <= Logger::INFO
|
166
|
+
self.log.info("(#{Process.pid}) Could not read BERP length header. Ernicorn server may have gone away. Exiting now.")
|
167
|
+
end
|
168
|
+
exit!
|
169
|
+
end
|
170
|
+
|
171
|
+
if iruby.size == 4 && iruby[0] == :call
|
172
|
+
mod, fun, args = iruby[1..3]
|
173
|
+
self.procline("#{mod}:#{fun}(#{args})")
|
174
|
+
self.log.info("-> " + iruby.inspect) if self.log.level <= Logger::INFO
|
175
|
+
begin
|
176
|
+
res = self.dispatch(mod, fun, args)
|
177
|
+
oruby = t[:reply, res]
|
178
|
+
self.log.debug("<- " + oruby.inspect) if self.log.level <= Logger::DEBUG
|
179
|
+
write_berp(output, oruby)
|
180
|
+
rescue ServerError => e
|
181
|
+
oruby = t[:error, t[:server, 0, e.class.to_s, e.message, e.backtrace]]
|
182
|
+
self.log.error("<- " + oruby.inspect) if self.log.level <= Logger::ERROR
|
183
|
+
self.log.error(e.backtrace.join("\n")) if self.log.level <= Logger::ERROR
|
184
|
+
write_berp(output, oruby)
|
185
|
+
rescue Object => e
|
186
|
+
oruby = t[:error, t[:user, 0, e.class.to_s, e.message, e.backtrace]]
|
187
|
+
self.log.error("<- " + oruby.inspect) if self.log.level <= Logger::ERROR
|
188
|
+
self.log.error(e.backtrace.join("\n")) if self.log.level <= Logger::ERROR
|
189
|
+
write_berp(output, oruby)
|
190
|
+
end
|
191
|
+
elsif iruby.size == 4 && iruby[0] == :cast
|
192
|
+
mod, fun, args = iruby[1..3]
|
193
|
+
self.procline("#{mod}:#{fun}(#{args})")
|
194
|
+
self.log.info("-> " + [:cast, mod, fun, args].inspect) if self.log.level <= Logger::INFO
|
195
|
+
oruby = t[:noreply]
|
196
|
+
write_berp(output, oruby)
|
197
|
+
|
198
|
+
begin
|
199
|
+
self.dispatch(mod, fun, args)
|
200
|
+
rescue Object => e
|
201
|
+
# ignore
|
202
|
+
end
|
203
|
+
else
|
204
|
+
self.procline("invalid request")
|
205
|
+
self.log.error("-> " + iruby.inspect) if self.log.level <= Logger::ERROR
|
206
|
+
oruby = t[:error, t[:server, 0, "Invalid request: #{iruby.inspect}"]]
|
207
|
+
self.log.error("<- " + oruby.inspect) if self.log.level <= Logger::ERROR
|
208
|
+
write_berp(output, oruby)
|
209
|
+
end
|
210
|
+
|
211
|
+
self.procline('waiting')
|
212
|
+
[iruby, oruby]
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.procline(msg)
|
216
|
+
$0 = "ernicorn handler #{VERSION} (ruby) - #{self.virgin_procline} - [#{self.count}] #{msg}"[0..159]
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.version
|
220
|
+
VERSION
|
221
|
+
end
|
222
|
+
|
223
|
+
class ServerError < StandardError; end
|
224
|
+
|
225
|
+
class Ernicorn::Mod
|
226
|
+
attr_accessor :name, :funs, :logger
|
227
|
+
|
228
|
+
def initialize(name)
|
229
|
+
self.name = name
|
230
|
+
self.funs = {}
|
231
|
+
self.logger = nil
|
232
|
+
end
|
233
|
+
|
234
|
+
def fun(name, block)
|
235
|
+
raise TypeError, "block required" if block.nil?
|
236
|
+
self.funs[name] = block
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
# Root level calls
|
241
|
+
|
242
|
+
def logfile(name)
|
243
|
+
Ernicorn.logfile(name)
|
244
|
+
end
|
245
|
+
|
246
|
+
def loglevel(level)
|
247
|
+
Ernicorn.loglevel(level)
|
248
|
+
end
|
249
|
+
|
250
|
+
at_exit do
|
251
|
+
Ernicorn.start if Ernicorn.auto_start && !defined?(Ernicorn)
|
252
|
+
end
|
data/script/bootstrap
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# Usage: script/bootstrap [--local]
|
3
|
+
# Prepare the local gem environment.
|
4
|
+
set -e
|
5
|
+
|
6
|
+
cd "$(dirname "$0")/.."
|
7
|
+
if bundle check 1>/dev/null 2>&1; then
|
8
|
+
echo "Gem environment up-to-date"
|
9
|
+
else
|
10
|
+
exec bundle install --binstubs --path vendor/gems "$@"
|
11
|
+
fi
|
data/script/cibuild
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# Usage: script/cibuild
|
3
|
+
# CI build script.
|
4
|
+
# This is tailored for the janky build machines.
|
5
|
+
set -e
|
6
|
+
|
7
|
+
# change into root dir and setup path
|
8
|
+
cd $(dirname "$0")/..
|
9
|
+
PATH="$(pwd)/bin:$(pwd)/script:/usr/share/rbenv/shims:$PATH"
|
10
|
+
|
11
|
+
# Write commit we're building at
|
12
|
+
git log -n 1 || true
|
13
|
+
echo
|
14
|
+
|
15
|
+
echo "Building under 1.8.7"
|
16
|
+
bootstrap
|
17
|
+
testsuite
|
18
|
+
echo
|
19
|
+
|
20
|
+
echo "Building under 1.9.3"
|
21
|
+
export RBENV_VERSION="${1:-1.9.3-p0}"
|
22
|
+
bootstrap
|
23
|
+
testsuite
|
data/script/ernicorn
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#/ Usage: ernicorn [options] [config file]
|
3
|
+
#/ Start a Ruby BERT-RPC Server with the given options and config file.
|
4
|
+
#/
|
5
|
+
#/ Options
|
6
|
+
#/ -h, --host=<host> Server address to listen on; default: 0.0.0.0
|
7
|
+
#/ -p, --port=<portno> Server port to listen on; default: 8000
|
8
|
+
#/ --log-level=0-4 Set the log level
|
9
|
+
#/ -d, --detached Run as a daemon
|
10
|
+
#/ -P, --pidfile=<file> Location to write pid file
|
11
|
+
#/
|
12
|
+
#/ See https://github.com/github/ernicorn for more information.
|
13
|
+
require 'optparse'
|
14
|
+
require 'ernicorn'
|
15
|
+
require 'ernicorn/server'
|
16
|
+
require 'ernicorn/adminrpc'
|
17
|
+
|
18
|
+
Ernicorn.auto_start = false
|
19
|
+
|
20
|
+
host = '0.0.0.0'
|
21
|
+
port = 8000
|
22
|
+
daemonize = false
|
23
|
+
config = 'config/ernicorn.rb'
|
24
|
+
pidfile = nil
|
25
|
+
|
26
|
+
parser = OptionParser.new do |opts|
|
27
|
+
opts.on("-c", "--config=val", String) { |config| }
|
28
|
+
opts.on("-h", "--host=val", Integer) { |host| }
|
29
|
+
opts.on("-p", "--port=val", Integer) { |port| }
|
30
|
+
opts.on( "--log-level=val", Integer) { |val| Ernicorn.loglevel(val) }
|
31
|
+
opts.on("-d", "--detached") { |daemonize| }
|
32
|
+
opts.on("-P", "--pidfile=val") { |pidfile| }
|
33
|
+
opts.on_tail("-h", "--help") { exec "grep ^#/<'#{__FILE__}'|cut -c4-" }
|
34
|
+
opts.parse!
|
35
|
+
end
|
36
|
+
|
37
|
+
config = ARGV[0] || config
|
38
|
+
|
39
|
+
options = {
|
40
|
+
:pid => pidfile,
|
41
|
+
:listeners => ["#{host}:#{port}"],
|
42
|
+
:config_file => File.expand_path(config)
|
43
|
+
}
|
44
|
+
|
45
|
+
if daemonize
|
46
|
+
require 'unicorn/launcher'
|
47
|
+
Unicorn::Launcher.daemonize!(options)
|
48
|
+
end
|
49
|
+
|
50
|
+
$ernicorn = Ernicorn::Server.new(nil, options)
|
51
|
+
$ernicorn.start.join
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#/ Usage: ernicorn-ctrl [-p <port>] <command>
|
3
|
+
#/ Issue a control command to an ernicorn server. The port option is used to
|
4
|
+
#/ specify a non-default control port.
|
5
|
+
#/
|
6
|
+
#/ Commands:
|
7
|
+
#/ reload-handlers Gracefully reload all handler processes.
|
8
|
+
#/ stats Dump handler stats for the server.
|
9
|
+
#/ halt Shut down.
|
10
|
+
require 'optparse'
|
11
|
+
require 'bertrpc'
|
12
|
+
|
13
|
+
port = 8000
|
14
|
+
|
15
|
+
def usage(status=0)
|
16
|
+
exec "grep ^#/<'#{__FILE__}'|cut -c4-; exit #{status}"
|
17
|
+
end
|
18
|
+
|
19
|
+
# parse arguments
|
20
|
+
ARGV.options do |opts|
|
21
|
+
opts.on("-p", "--port=val", Integer) { |port| }
|
22
|
+
opts.on_tail("-h", "--help") { usage }
|
23
|
+
opts.parse!
|
24
|
+
end
|
25
|
+
|
26
|
+
# the control command
|
27
|
+
command = ARGV[0]
|
28
|
+
|
29
|
+
# bail out with usage if the command wasn't specified or is invalid
|
30
|
+
if !%w[reload-handlers stats halt].include?(command)
|
31
|
+
usage(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
# connect to the adminrpc handler and send the command
|
35
|
+
require 'bertrpc'
|
36
|
+
service = BERTRPC::Service.new('localhost', port)
|
37
|
+
puts service.call.__admin__.send(command.gsub(/-/, '_'))
|
data/script/testsuite
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.expand_path('../helper', __FILE__)
|
2
|
+
|
3
|
+
class ErnicornTest < Test::Unit::TestCase
|
4
|
+
module TestExposingModule
|
5
|
+
def foo
|
6
|
+
end
|
7
|
+
|
8
|
+
def bar(a, b, c=nil)
|
9
|
+
[a, b, c]
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
def baz
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def bling
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "expose" do
|
22
|
+
setup { Ernicorn.expose :expo, TestExposingModule }
|
23
|
+
teardown { Ernicorn.mods.clear }
|
24
|
+
|
25
|
+
should "add all public methods from the module" do
|
26
|
+
assert_not_nil Ernicorn.mods[:expo].funs[:foo]
|
27
|
+
assert_not_nil Ernicorn.mods[:expo].funs[:bar]
|
28
|
+
end
|
29
|
+
|
30
|
+
should "not expose protected methods" do
|
31
|
+
assert_nil Ernicorn.mods[:expo].funs[:baz]
|
32
|
+
end
|
33
|
+
|
34
|
+
should "not expose private methods" do
|
35
|
+
assert_nil Ernicorn.mods[:expo].funs[:bling]
|
36
|
+
end
|
37
|
+
|
38
|
+
should "dispatch to module methods properly" do
|
39
|
+
res = Ernicorn.dispatch(:expo, :bar, ['a', :b, { :fiz => 42 }])
|
40
|
+
assert_equal ['a', :b, { :fiz => 42 }], res
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module TestLoggingModule
|
45
|
+
def self.logs() @logs ||= [] end
|
46
|
+
|
47
|
+
def self.dispatched(*args)
|
48
|
+
logs << args
|
49
|
+
end
|
50
|
+
|
51
|
+
def sleepy(time)
|
52
|
+
sleep(time)
|
53
|
+
time
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "logging" do
|
58
|
+
setup { Ernicorn.expose :logging, TestLoggingModule; TestLoggingModule.logs.clear }
|
59
|
+
teardown { Ernicorn.mods.clear }
|
60
|
+
|
61
|
+
should "call logger method with timing" do
|
62
|
+
ret = Ernicorn.dispatch(:logging, :sleepy, 0.1)
|
63
|
+
assert_equal 0.1, ret
|
64
|
+
|
65
|
+
assert_equal 1, TestLoggingModule.logs.size
|
66
|
+
assert_in_delta 0.1, TestLoggingModule.logs.first[0], 0.05
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ernicorn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Tom Preston-Werner
|
13
|
+
- tmm1
|
14
|
+
- rtomayko
|
15
|
+
autorequire:
|
16
|
+
bindir: script
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2012-06-23 00:00:00 -07:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: bert
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
version: 1.1.0
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: bertrpc
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 1
|
45
|
+
- 0
|
46
|
+
- 0
|
47
|
+
version: 1.0.0
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: unicorn
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 4
|
59
|
+
- 1
|
60
|
+
- 1
|
61
|
+
version: 4.1.1
|
62
|
+
type: :runtime
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: shoulda
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
segments:
|
72
|
+
- 2
|
73
|
+
- 11
|
74
|
+
- 3
|
75
|
+
version: 2.11.3
|
76
|
+
type: :development
|
77
|
+
version_requirements: *id004
|
78
|
+
description: Ernicorn is a BERT-RPC server packaged as a gem.
|
79
|
+
email: tmm1@github.com
|
80
|
+
executables:
|
81
|
+
- ernicorn
|
82
|
+
- ernicorn-ctrl
|
83
|
+
extensions: []
|
84
|
+
|
85
|
+
extra_rdoc_files:
|
86
|
+
- LICENSE
|
87
|
+
- README.md
|
88
|
+
files:
|
89
|
+
- .gitignore
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- ernicorn.gemspec
|
94
|
+
- examples/client.rb
|
95
|
+
- examples/config.rb
|
96
|
+
- examples/handler.rb
|
97
|
+
- examples/start.sh
|
98
|
+
- lib/ernicorn.rb
|
99
|
+
- lib/ernicorn/adminrpc.rb
|
100
|
+
- lib/ernicorn/server.rb
|
101
|
+
- script/bootstrap
|
102
|
+
- script/cibuild
|
103
|
+
- script/ernicorn
|
104
|
+
- script/ernicorn-ctrl
|
105
|
+
- script/testsuite
|
106
|
+
- test/helper.rb
|
107
|
+
- test/test_ernicorn.rb
|
108
|
+
has_rdoc: true
|
109
|
+
homepage: https://github.com/github/ernicorn
|
110
|
+
licenses: []
|
111
|
+
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
segments:
|
129
|
+
- 0
|
130
|
+
version: "0"
|
131
|
+
requirements: []
|
132
|
+
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 1.3.6
|
135
|
+
signing_key:
|
136
|
+
specification_version: 2
|
137
|
+
summary: Ernicorn is a BERT-RPC server implementation based on unicorn.
|
138
|
+
test_files: []
|
139
|
+
|