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.
@@ -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