perennial 1.1.0 → 1.2.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/examples/json_echo_client.rb +14 -0
- data/examples/json_echo_server.rb +30 -0
- data/lib/perennial/daemon.rb +1 -1
- data/lib/perennial/protocols/json_transport.rb +196 -0
- data/lib/perennial/protocols/pure_ruby/json_transport.rb +171 -0
- data/lib/perennial/protocols/pure_ruby.rb +7 -0
- data/lib/perennial/protocols.rb +6 -0
- data/lib/perennial/settings.rb +1 -1
- data/lib/perennial.rb +2 -2
- metadata +8 -2
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'readline'
|
3
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "perennial")
|
4
|
+
|
5
|
+
transport = Perennial::Protocols::PureRuby::JSONTransport.new('localhost', 43241, 10.0)
|
6
|
+
|
7
|
+
input = ''
|
8
|
+
|
9
|
+
loop do
|
10
|
+
line = Readline.readline('input> ')
|
11
|
+
break if line.strip.downcase == 'exit'
|
12
|
+
transport.write_message(:echo, 'text' => line)
|
13
|
+
p transport.read_message
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "perennial")
|
4
|
+
|
5
|
+
module MyAwesomeApp
|
6
|
+
|
7
|
+
include Perennial
|
8
|
+
|
9
|
+
manifest do |m, l|
|
10
|
+
Settings.root = __FILE__.to_pathname.dirname
|
11
|
+
end
|
12
|
+
|
13
|
+
class JSONEchoServer < EventMachine::Connection
|
14
|
+
include Perennial::Protocols::JSONTransport
|
15
|
+
|
16
|
+
on_action :echo, :echo_data
|
17
|
+
|
18
|
+
def echo_data(d)
|
19
|
+
puts "Got data: #{d.inspect}"
|
20
|
+
reply :echoed_back, d
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
EM.run do
|
28
|
+
puts "Starting..."
|
29
|
+
EM.start_server "localhost", 43241, MyAwesomeApp::JSONEchoServer
|
30
|
+
end
|
data/lib/perennial/daemon.rb
CHANGED
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
require 'json' unless defined?(JSON)
|
3
|
+
|
4
|
+
module Perennial
|
5
|
+
module Protocols
|
6
|
+
module JSONTransport
|
7
|
+
|
8
|
+
SEPERATOR = "\r\n".freeze
|
9
|
+
|
10
|
+
def self.included(parent)
|
11
|
+
parent.class_eval do |parent|
|
12
|
+
is :loggable
|
13
|
+
|
14
|
+
cattr_accessor :event_handlers
|
15
|
+
|
16
|
+
include InstanceMethods
|
17
|
+
extend ClassMethods
|
18
|
+
|
19
|
+
self.event_handlers = Hash.new { |h,k| h[k] = [] }
|
20
|
+
|
21
|
+
# Simple built in Methods, applicable both ways.
|
22
|
+
on_action :exception, :handle_exception
|
23
|
+
on_action :noop, :handle_noop
|
24
|
+
on_action :enable_ssl, :handle_enable_ssl
|
25
|
+
on_action :enabled_ssl, :handle_enabled_ssl
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
|
32
|
+
def receive_data(data)
|
33
|
+
protocol_buffer.extract(data).each do |part|
|
34
|
+
receive_line(part)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive_line(line)
|
39
|
+
line.strip!
|
40
|
+
response = JSON.parse(line)
|
41
|
+
handle_response(response)
|
42
|
+
rescue Exception => e
|
43
|
+
# Typically a problem parsing JSON
|
44
|
+
handle_receiving_exception(e)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Typically you'd log a backtrace
|
48
|
+
def handle_receiving_exception(e)
|
49
|
+
end
|
50
|
+
|
51
|
+
def host_with_port
|
52
|
+
@host_with_port ||= begin
|
53
|
+
port, ip = Socket.unpack_sockaddr_in(get_peername)
|
54
|
+
"#{ip}:#{port}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def message(name, data = {}, &blk)
|
59
|
+
payload = {
|
60
|
+
"action" => name.to_s,
|
61
|
+
"payload" => data,
|
62
|
+
"sent-at" => Time.now
|
63
|
+
}
|
64
|
+
payload.merge!(options_for_callback(blk))
|
65
|
+
send_data "#{JSON.dump(payload)}#{SEPERATOR}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def reply(name, data = {}, &blk)
|
69
|
+
data = data.merge("callback-id" => @callback_id) if instance_variable_defined?(:@callback_id) && @callback_id.present?
|
70
|
+
message(name, data, &blk)
|
71
|
+
end
|
72
|
+
|
73
|
+
def use_ssl=(value)
|
74
|
+
@should_use_ssl = value
|
75
|
+
enable_ssl if connected?
|
76
|
+
end
|
77
|
+
|
78
|
+
def post_connect
|
79
|
+
end
|
80
|
+
|
81
|
+
def post_init
|
82
|
+
if !connected? && !ssl_enabled?
|
83
|
+
@connected = true
|
84
|
+
post_connect
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def ssl_handshake_complete
|
89
|
+
if !connected?
|
90
|
+
@connected = true
|
91
|
+
post_connect
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def handle_enable_ssl(data)
|
96
|
+
reply :enabled_ssl
|
97
|
+
enable_ssl
|
98
|
+
end
|
99
|
+
|
100
|
+
def handle_enabled_ssl(data)
|
101
|
+
enable_ssl
|
102
|
+
end
|
103
|
+
|
104
|
+
# Do Nothing
|
105
|
+
def handle_noop(data)
|
106
|
+
end
|
107
|
+
|
108
|
+
# A remote exception in the processing
|
109
|
+
def handle_exception(data)
|
110
|
+
logger.warn "Got exception from remote call of #{data["action"]}: #{data["message"]}"
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
def should_use_ssl?
|
116
|
+
instance_variable_defined?(:@should_use_ssl) && @should_use_ssl
|
117
|
+
end
|
118
|
+
|
119
|
+
def ssl_enabled?
|
120
|
+
instance_variable_defined?(:@ssl_enabled) && @ssl_enabled
|
121
|
+
end
|
122
|
+
|
123
|
+
def options_for_callback(blk)
|
124
|
+
return {} if blk.nil?
|
125
|
+
cb_id = "callback-#{self.object_id}-#{Time.now.to_f}"
|
126
|
+
full_id, count = nil, 0
|
127
|
+
while full_id.nil? || @callbacks.has_key?(full_id)
|
128
|
+
count += 1
|
129
|
+
full_id = callback_id(base, count)
|
130
|
+
end
|
131
|
+
self.callbacks[full_id] = blk
|
132
|
+
{"callback-id" => full_id}
|
133
|
+
end
|
134
|
+
|
135
|
+
def process_callback(data)
|
136
|
+
if data.is_a?(Hash) && data.has_key?("callback-id")
|
137
|
+
callback = @callbacks.delete(data["callback-id"])
|
138
|
+
callback.call(self, data) if callback.present?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def callback_id(base, count)
|
143
|
+
Digest::SHA256.hexdigest([base, count].compact.join("-"))
|
144
|
+
end
|
145
|
+
|
146
|
+
def protocol_buffer
|
147
|
+
@protocol_buffer ||= BufferedTokenizer.new(SEPERATOR)
|
148
|
+
end
|
149
|
+
|
150
|
+
def callbacks
|
151
|
+
@callbacks ||= {}
|
152
|
+
end
|
153
|
+
|
154
|
+
def connected?
|
155
|
+
instance_variable_defined?(:@connected) && @connected
|
156
|
+
end
|
157
|
+
|
158
|
+
def handle_response(response)
|
159
|
+
return unless response.is_a?(Hash) && response.has_key?("action")
|
160
|
+
payload = response["payload"] || {}
|
161
|
+
@callback_id = response.delete("callback-id")
|
162
|
+
process_callback(payload)
|
163
|
+
process_action(response["action"], payload)
|
164
|
+
@callback_id = nil
|
165
|
+
end
|
166
|
+
|
167
|
+
def process_action(name, data)
|
168
|
+
self.event_handlers[name.to_s].each do |handler|
|
169
|
+
if handler.respond_to?(:call)
|
170
|
+
handler.call(data, self)
|
171
|
+
elsif handler.respond_to?(:handle)
|
172
|
+
handler.handle(data)
|
173
|
+
else
|
174
|
+
self.send(handler, data)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
rescue Exception => e
|
178
|
+
reply :exception, :name => e.class.name, :message => e.message,
|
179
|
+
:action => name, :payload => data
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
module ClassMethods
|
185
|
+
|
186
|
+
def on_action(name, handler = nil, &blk)
|
187
|
+
real_name = name.to_s
|
188
|
+
self.event_handlers[real_name] << blk if blk.present?
|
189
|
+
self.event_handlers[real_name] << handler if handler.present?
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# Most of the code here, esp. the nifty code to handle protocol
|
2
|
+
# errors is from the memcache-client gem. for the appropriate license,
|
3
|
+
# see licenses/memcache-client.txt
|
4
|
+
|
5
|
+
require 'digest/sha2'
|
6
|
+
require 'socket'
|
7
|
+
require 'net/protocol'
|
8
|
+
require 'json' unless defined?(JSON)
|
9
|
+
|
10
|
+
# Nasty module declaration out of the way.
|
11
|
+
module Perennial; module Protocols; module PureRuby
|
12
|
+
end; end; end
|
13
|
+
|
14
|
+
begin
|
15
|
+
if defined?(JRUBY_VERSION) || (RUBY_VERSION >= '1.9')
|
16
|
+
require 'timeout'
|
17
|
+
Perennial::TimerImplementation = Timeout
|
18
|
+
else
|
19
|
+
require 'system_timer'
|
20
|
+
Perennial::TimerImplementation = SystemTimer
|
21
|
+
end
|
22
|
+
rescue LoadError => e
|
23
|
+
require 'timeout'
|
24
|
+
Perennial::TimerImplementation = Timeout
|
25
|
+
end
|
26
|
+
|
27
|
+
class Perennial::Protocols::PureRuby::JSONTransport
|
28
|
+
|
29
|
+
@@callbacks = {}
|
30
|
+
|
31
|
+
RETRY_DELAY = 30.0
|
32
|
+
SEPERATOR = "\r\n".freeze
|
33
|
+
|
34
|
+
attr_reader :host, :port, :retry
|
35
|
+
|
36
|
+
def initialize(host, port, timeout = nil)
|
37
|
+
@host = host
|
38
|
+
@port = port
|
39
|
+
@timeout = timeout
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_message(action, payload = {}, &callback)
|
43
|
+
# TODO: Print message.
|
44
|
+
message = JSON.dump({
|
45
|
+
"action" => action.to_s,
|
46
|
+
"payload" => payload,
|
47
|
+
"sent-at" => Time.now
|
48
|
+
}.merge(callback_options(callback))) + SEPERATOR
|
49
|
+
with_socket { |s| s.write(message) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_message
|
53
|
+
with_socket do |s|
|
54
|
+
message = JSON.parse(s.gets.strip)
|
55
|
+
return false if !message.is_a?(Hash)
|
56
|
+
action, payload = message["action"], message["payload"]
|
57
|
+
return false if !action.is_a?(String)
|
58
|
+
payload = {} unless payload.is_a?(Hash)
|
59
|
+
# We have a processed callback - huzzah!
|
60
|
+
if payload.has_key?("callback-id")
|
61
|
+
callback = @@callbacks.delete(payload["callback-id"])
|
62
|
+
callback.call(action, payload) if callback
|
63
|
+
end
|
64
|
+
return action, payload
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def alive?
|
69
|
+
!!socket
|
70
|
+
end
|
71
|
+
|
72
|
+
def close
|
73
|
+
@socket.close if @socket && !@socket.closed?
|
74
|
+
@socket = nil
|
75
|
+
@retry = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def callback_options(blk)
|
81
|
+
return {} if blk.nil?
|
82
|
+
callback_id = Digest::SHA256.hexdigest("#{self.class.name}-#{Time.now.to_f}-#{rand(1_000_000_000)}")
|
83
|
+
@@callbacks[callback_id] = blk
|
84
|
+
{"callback-id" => callback_id}
|
85
|
+
end
|
86
|
+
|
87
|
+
def with_socket(&blk)
|
88
|
+
blk.call(socket)
|
89
|
+
rescue SocketError, Errno::EAGAIN, Timeout::Error
|
90
|
+
dead!
|
91
|
+
rescue SystemCallError, IOError
|
92
|
+
retried = true
|
93
|
+
retry
|
94
|
+
end
|
95
|
+
|
96
|
+
def dead!
|
97
|
+
close
|
98
|
+
@retry = Time.now + RETRY_DELAY
|
99
|
+
end
|
100
|
+
|
101
|
+
def socket
|
102
|
+
return @socket if @socket and not @socket.closed?
|
103
|
+
@socket = nil
|
104
|
+
return if @retry and @retry > Time.now
|
105
|
+
begin
|
106
|
+
@socket = socket_for(@host, @port, @timeout)
|
107
|
+
@socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
108
|
+
@retry = nil
|
109
|
+
rescue SocketError, SystemCallError, IOError, Timeout::Error => err
|
110
|
+
# TODO: Raise a connection error here
|
111
|
+
dead!
|
112
|
+
end
|
113
|
+
@socket
|
114
|
+
end
|
115
|
+
|
116
|
+
def socket_for(host = @host, port = @port, timeout = nil)
|
117
|
+
socket = nil
|
118
|
+
if timeout
|
119
|
+
Perennial::TimerImplementation.timeout(timeout) do
|
120
|
+
socket = TCPSocket.new(host, port)
|
121
|
+
end
|
122
|
+
else
|
123
|
+
socket = TCPSocket.new(host, port)
|
124
|
+
end
|
125
|
+
io = BufferedIO.new(socket)
|
126
|
+
io.read_timeout = timeout
|
127
|
+
if timeout
|
128
|
+
secs = Integer(timeout)
|
129
|
+
if timeout
|
130
|
+
secs = Integer(timeout)
|
131
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
132
|
+
optval = [secs, usecs].pack("l_2")
|
133
|
+
begin
|
134
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
135
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
136
|
+
rescue Exception => ex
|
137
|
+
# Solaris, for one, does not like/support socket timeouts.
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
io
|
142
|
+
end
|
143
|
+
|
144
|
+
class BufferedIO < Net::BufferedIO # :nodoc:
|
145
|
+
BUFSIZE = 1024 * 16
|
146
|
+
|
147
|
+
if RUBY_VERSION < '1.9.1'
|
148
|
+
def rbuf_fill
|
149
|
+
begin
|
150
|
+
@rbuf << @io.read_nonblock(BUFSIZE)
|
151
|
+
rescue Errno::EWOULDBLOCK
|
152
|
+
retry unless @read_timeout
|
153
|
+
if IO.select([@io], nil, nil, @read_timeout)
|
154
|
+
retry
|
155
|
+
else
|
156
|
+
raise Timeout::Error, 'IO timeout'
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def setsockopt(*args)
|
163
|
+
@io.setsockopt(*args)
|
164
|
+
end
|
165
|
+
|
166
|
+
def gets
|
167
|
+
readuntil(SEPERATOR)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
data/lib/perennial/settings.rb
CHANGED
data/lib/perennial.rb
CHANGED
@@ -9,12 +9,12 @@ require 'perennial/exceptions'
|
|
9
9
|
|
10
10
|
module Perennial
|
11
11
|
|
12
|
-
VERSION = [1,
|
12
|
+
VERSION = [1, 2, 0]
|
13
13
|
|
14
14
|
has_library :dispatchable, :hookable, :loader, :logger, :nash,
|
15
15
|
:loggable, :manifest, :settings, :argument_parser,
|
16
16
|
:option_parser, :application, :generator, :daemon,
|
17
|
-
:delegateable, :reloading
|
17
|
+
:delegateable, :reloading, :protocols
|
18
18
|
|
19
19
|
def self.included(parent)
|
20
20
|
parent.extend(Manifest::Mixin)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: perennial
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Darcy Laycock
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-01-02 00:00:00 +08:00
|
13
13
|
default_executable: perennial
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -76,6 +76,10 @@ files:
|
|
76
76
|
- lib/perennial/manifest.rb
|
77
77
|
- lib/perennial/nash.rb
|
78
78
|
- lib/perennial/option_parser.rb
|
79
|
+
- lib/perennial/protocols.rb
|
80
|
+
- lib/perennial/protocols/json_transport.rb
|
81
|
+
- lib/perennial/protocols/pure_ruby.rb
|
82
|
+
- lib/perennial/protocols/pure_ruby/json_transport.rb
|
79
83
|
- lib/perennial/reloading.rb
|
80
84
|
- lib/perennial/settings.rb
|
81
85
|
- templates/application.erb
|
@@ -127,3 +131,5 @@ test_files:
|
|
127
131
|
- test/reloading_test.rb
|
128
132
|
- test/settings_test.rb
|
129
133
|
- test/test_helper.rb
|
134
|
+
- examples/json_echo_client.rb
|
135
|
+
- examples/json_echo_server.rb
|