perennial 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -118,7 +118,7 @@ module Perennial
118
118
 
119
119
  def pids_from(files)
120
120
  pids = []
121
- Dir[files].each do |file|
121
+ Dir[files.to_s].each do |file|
122
122
  pids += File.read(file).split("\n").map { |l| l.strip.to_i(10) }
123
123
  end
124
124
  pids.uniq.select { |p| alive?(p) }
@@ -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
@@ -0,0 +1,7 @@
1
+ module Perennial
2
+ module Protocols
3
+ module PureRuby
4
+ autoload :JSONTransport, 'perennial/protocols/pure_ruby/json_transport'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Perennial
2
+ module Protocols
3
+ autoload :JSONTransport, 'perennial/protocols/json_transport'
4
+ autoload :PureRuby, 'perennial/protocols/pure_ruby'
5
+ end
6
+ end
@@ -23,7 +23,7 @@ module Perennial
23
23
  end
24
24
 
25
25
  def root=(path)
26
- @@root = File.expand_path(path.to_str)
26
+ @@root = File.expand_path(path.respond_to?(:to_path) ? path.to_path : path.to_str)
27
27
  end
28
28
 
29
29
  def root
data/lib/perennial.rb CHANGED
@@ -9,12 +9,12 @@ require 'perennial/exceptions'
9
9
 
10
10
  module Perennial
11
11
 
12
- VERSION = [1, 1, 0]
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.1.0
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: 2009-11-07 00:00:00 +08:00
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