oleganza-emrpc 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README +129 -0
  3. data/Rakefile +156 -0
  4. data/TODO +47 -0
  5. data/bin/emrpc +4 -0
  6. data/lib/emrpc.rb +15 -0
  7. data/lib/emrpc/archive/reference_savior.rb +48 -0
  8. data/lib/emrpc/archive/ring.rb +44 -0
  9. data/lib/emrpc/blocking_api.rb +3 -0
  10. data/lib/emrpc/blocking_api/method_proxy.rb +57 -0
  11. data/lib/emrpc/blocking_api/multithreaded_client.rb +52 -0
  12. data/lib/emrpc/blocking_api/singlethreaded_client.rb +68 -0
  13. data/lib/emrpc/client.rb +28 -0
  14. data/lib/emrpc/console.rb +32 -0
  15. data/lib/emrpc/evented_api.rb +14 -0
  16. data/lib/emrpc/evented_api/connection_mixin.rb +14 -0
  17. data/lib/emrpc/evented_api/debug_connection.rb +52 -0
  18. data/lib/emrpc/evented_api/debug_pid_callbacks.rb +39 -0
  19. data/lib/emrpc/evented_api/default_callbacks.rb +40 -0
  20. data/lib/emrpc/evented_api/evented_wrapper.rb +28 -0
  21. data/lib/emrpc/evented_api/local_connection.rb +48 -0
  22. data/lib/emrpc/evented_api/pid.rb +198 -0
  23. data/lib/emrpc/evented_api/protocol_mapper.rb +57 -0
  24. data/lib/emrpc/evented_api/reconnecting_pid.rb +105 -0
  25. data/lib/emrpc/evented_api/remote_connection.rb +73 -0
  26. data/lib/emrpc/evented_api/remote_pid.rb +38 -0
  27. data/lib/emrpc/evented_api/subscribable.rb +56 -0
  28. data/lib/emrpc/evented_api/timer.rb +23 -0
  29. data/lib/emrpc/protocols.rb +2 -0
  30. data/lib/emrpc/protocols/fast_message_protocol.rb +99 -0
  31. data/lib/emrpc/protocols/marshal_protocol.rb +33 -0
  32. data/lib/emrpc/server.rb +17 -0
  33. data/lib/emrpc/util.rb +7 -0
  34. data/lib/emrpc/util/blank_slate.rb +25 -0
  35. data/lib/emrpc/util/codec.rb +114 -0
  36. data/lib/emrpc/util/combine_modules.rb +11 -0
  37. data/lib/emrpc/util/em2rev.rb +48 -0
  38. data/lib/emrpc/util/em_start_stop_timeouts.rb +62 -0
  39. data/lib/emrpc/util/parsed_uri.rb +15 -0
  40. data/lib/emrpc/util/safe_run.rb +23 -0
  41. data/lib/emrpc/util/timers.rb +17 -0
  42. data/lib/emrpc/version.rb +3 -0
  43. data/spec/blocking_api/method_proxy_spec.rb +33 -0
  44. data/spec/blocking_api/multithreaded_client_spec.rb +52 -0
  45. data/spec/blocking_api/scenario_spec.rb +35 -0
  46. data/spec/blocking_api/singlethreaded_client_spec.rb +63 -0
  47. data/spec/blocking_api/spec_helper.rb +1 -0
  48. data/spec/blocking_api_test.rb +98 -0
  49. data/spec/evented_api/connection_mixin_spec.rb +34 -0
  50. data/spec/evented_api/default_callbacks_spec.rb +26 -0
  51. data/spec/evented_api/evented_wrapper_spec.rb +50 -0
  52. data/spec/evented_api/pid_spec.rb +194 -0
  53. data/spec/evented_api/reconnecting_pid_spec.rb +76 -0
  54. data/spec/evented_api/remote_connection_spec.rb +147 -0
  55. data/spec/evented_api/remote_pid_spec.rb +84 -0
  56. data/spec/evented_api/scenario_spec.rb +138 -0
  57. data/spec/evented_api/spec_helper.rb +10 -0
  58. data/spec/evented_api/subscribable_spec.rb +53 -0
  59. data/spec/server_spec.rb +7 -0
  60. data/spec/spec_helper.rb +96 -0
  61. data/spec/util/blank_slate_spec.rb +7 -0
  62. data/spec/util/codec_spec.rb +183 -0
  63. data/spec/util/fast_message_protocol_spec.rb +60 -0
  64. data/spec/util/marshal_protocol_spec.rb +50 -0
  65. data/spec/util/parsed_uri_spec.rb +19 -0
  66. data/spec/util/spec_helper.rb +1 -0
  67. metadata +164 -0
@@ -0,0 +1,48 @@
1
+ module EMRPC
2
+ class LocalConnection
3
+ include ConnectionMixin
4
+
5
+ # Helper class representing abstract connection channel.
6
+ class Channel
7
+ attr_accessor :conn12, :conn21
8
+ def initialize(pid1, pid2, conn12 = nil)
9
+ @conn21 = LocalConnection.new(pid2, pid1, self)
10
+ @conn12 = conn12 || LocalConnection.new(pid1, pid2, self)
11
+ end
12
+ def unbind
13
+ @conn12.unbind
14
+ @conn21.unbind
15
+ end
16
+ def connection
17
+ @conn12
18
+ end
19
+ end
20
+
21
+ attr_accessor :channel
22
+
23
+ def initialize(local_pid, remote_pid, channel = nil)
24
+ @channel = channel || Channel.new(local_pid, remote_pid, self)
25
+ @local_pid = local_pid
26
+ @remote_pid = local_pid.connection_established(remote_pid, self)
27
+ end
28
+
29
+ def unbind
30
+ lpid = @local_pid
31
+ rpid = @remote_pid
32
+ @local_pid = nil
33
+ @remote_pid = nil
34
+ lpid.connection_unbind(rpid, self)
35
+ end
36
+
37
+ def close_connection
38
+ @channel.unbind
39
+ end
40
+ alias close_connection_after_writing close_connection
41
+
42
+ LOCALNODE_ADDRESS = 'emrpc://localnode/'.parsed_uri.freeze
43
+ def address
44
+ LOCALNODE_ADDRESS
45
+ end
46
+
47
+ end # LocalConnection
48
+ end # EMRPC
@@ -0,0 +1,198 @@
1
+ require 'uri'
2
+ module EMRPC
3
+ # Pid is a abbreviation for "process id". Pid represents so-called lightweight process (like in Erlang OTP)
4
+ # Pids can be created, connected, disconnected, spawned, killed.
5
+ # When pid is created, it exists on its own.
6
+ # When someone connects to the pid, connection is established.
7
+ # When pid is killed, all its connections are unbinded.
8
+
9
+ module Pid
10
+ attr_accessor :uuid, :connections, :killed, :options
11
+ attr_accessor :_em_server_signature, :_protocol, :_bind_address
12
+ include DefaultCallbacks
13
+ include ProtocolMapper
14
+
15
+ # FIXME: doesn't override user-defined callbacks
16
+ include DebugPidCallbacks if $DEBUG
17
+
18
+ # shorthand for console testing
19
+ def self.new(*attributes)
20
+ # We create random global const to workaround Marshal.dump issue:
21
+ # >> Marshal.dump(Class.new.new)
22
+ # TypeError: can't dump anonymous class #<Class:0x5b5338>
23
+ #
24
+ const_set("DynamicPidClass#{rand(2**128).to_s(16).upcase}", Class.new {
25
+ include Pid
26
+ attr_accessor(*attributes)
27
+ }).new
28
+ end
29
+
30
+ def initialize(*args, &blk)
31
+ @uuid = _random_uuid
32
+ @options = {:uuid => @uuid}
33
+ _common_init
34
+ super(*args, &blk) rescue nil
35
+ end
36
+
37
+ def spawn(cls, *args, &blk)
38
+ pid = cls.new(*args, &blk)
39
+ connect(pid)
40
+ pid
41
+ end
42
+
43
+ def tcp_spawn(addr, cls, *args, &blk)
44
+ pid = spawn(cls, *args, &blk)
45
+ pid.bind(addr)
46
+ pid
47
+ end
48
+
49
+ def thread_spawn(cls, *args, &blk)
50
+ # TODO: think about thread-safe passing messages back to sender.
51
+ end
52
+
53
+ def bind(addr)
54
+ raise "Pid is already binded!" if @_em_server_signature
55
+ @_bind_address = addr.parsed_uri
56
+ this = self
57
+ @_em_server_signature = make_server_connection(@_bind_address, _protocol) do |conn|
58
+ conn.local_pid = this
59
+ conn.address = addr
60
+ end
61
+ end
62
+
63
+ # 1. Connect to the pid.
64
+ # 2. When connection is established, asks for uuid.
65
+ # 3. When uuid is received, triggers callback on the client.
66
+ # (See Protocol for details)
67
+ def connect(addr, connected_callback = nil, disconnected_callback = nil)
68
+ c = if addr.is_a?(Pid) && pid = addr
69
+ LocalConnection.new(self, pid)
70
+ else
71
+ this = self
72
+ make_client_connection(addr, _protocol) do |conn|
73
+ conn.local_pid = this
74
+ conn.address = addr
75
+ end
76
+ end
77
+ c.connected_callback = connected_callback
78
+ c.disconnected_callback = disconnected_callback
79
+ c
80
+ end
81
+
82
+ def disconnect(pid, disconnected_callback = nil)
83
+ c = @connections[pid.uuid]
84
+ c.disconnected_callback = disconnected_callback if disconnected_callback
85
+ c.close_connection_after_writing
86
+ end
87
+
88
+ def kill
89
+ return if @killed
90
+ if @_em_server_signature
91
+ EventMachine.stop_server(@_em_server_signature)
92
+ end
93
+ @connections.each do |uuid, conn|
94
+ conn.close_connection_after_writing
95
+ end
96
+ @connections.clear
97
+ @killed = true
98
+ end
99
+
100
+ # TODO:
101
+ # When connecting to a spawned pid, we should transparantly discard TCP connection
102
+ # in favor of local connection.
103
+ def connection_established(pid, conn)
104
+ @connections[pid.uuid] ||= conn
105
+ __send__(conn.connected_callback, pid)
106
+ @connections[pid.uuid].remote_pid || pid # looks like hack, but it is not.
107
+ end
108
+
109
+ def connection_unbind(pid, conn)
110
+ @connections.delete(pid.uuid)
111
+ __send__(conn.disconnected_callback, pid)
112
+ end
113
+
114
+ #
115
+ # Util
116
+ #
117
+ def options=(opts)
118
+ @options = opts
119
+ @options[:uuid] = @uuid
120
+ @options
121
+ end
122
+
123
+ def killed?
124
+ @killed
125
+ end
126
+
127
+ def find_pid(uuid)
128
+ return self if uuid == @uuid
129
+ ((conn = @connections[uuid]) and conn.remote_pid) or raise "Pid #{_uid} was not found in a #{self.inspect}"
130
+ end
131
+
132
+ def marshal_dump
133
+ @uuid
134
+ end
135
+
136
+ def marshal_load(uuid)
137
+ _common_init
138
+ @uuid = uuid
139
+ end
140
+
141
+ def connection_uuids
142
+ (@connections || {}).keys
143
+ end
144
+
145
+ def pid_class_name
146
+ "Pid"
147
+ end
148
+
149
+ def inspect
150
+ return "#<#{pid_class_name}:#{_uid} KILLED>" if @killed
151
+ "#<#{pid_class_name}:#{_uid} connected to #{connection_uuids.map{|u|_uid(u)}.inspect}>"
152
+ end
153
+
154
+ def ==(other)
155
+ other.is_a?(Pid) && other.uuid == @uuid
156
+ end
157
+
158
+ # shorter uuid for pretty output
159
+ def _uid(uuid = @uuid)
160
+ uuid && uuid[0,6]
161
+ end
162
+
163
+ #
164
+ # Private, but accessible from outside methods are prefixed with underscore.
165
+ #
166
+
167
+ def _protocol
168
+ @_protocol ||= self.__send__(:_protocol=, RemoteConnection)
169
+ end
170
+
171
+ def _protocol=(p)
172
+ @_protocol = Util.combine_modules(
173
+ p,
174
+ MarshalProtocol.new(Marshal),
175
+ FastMessageProtocol,
176
+ $DEBUG ? DebugConnection : Module.new
177
+ )
178
+ end
179
+
180
+ # TODO: remove this in favor of using codec.rb
181
+ def _send_dirty(*args)
182
+ args._initialize_pids_recursively_d4d309bd!(self)
183
+ send(*args)
184
+ end
185
+
186
+ private
187
+
188
+ def _common_init
189
+ @connections = {} # pid.uuid -> connection
190
+ end
191
+
192
+ def _random_uuid
193
+ # FIXME: insert real uuid generating here!
194
+ rand(2**128).to_s(16)
195
+ end
196
+
197
+ end # Pid
198
+ end # EMRPC
@@ -0,0 +1,57 @@
1
+ module EMRPC
2
+ module ProtocolMapper
3
+ #
4
+ # Configuration
5
+ #
6
+ MAP = Hash.new
7
+
8
+ def self.register_protocol(scheme, suffix)
9
+ MAP[scheme] = suffix
10
+ self
11
+ end
12
+
13
+ # Default mapping
14
+ register_protocol "emrpc", :emrpc_tcp
15
+ register_protocol "unix", :emrpc_unix
16
+ register_protocol "emrpc+unix", :emrpc_unix
17
+ register_protocol "http", :http_json
18
+
19
+ #
20
+ # Abstract API
21
+ #
22
+ def make_client_connection(*args, &blk)
23
+ make_some_connection(:client, *args, &blk)
24
+ end
25
+
26
+ def make_server_connection(*args, &blk)
27
+ make_some_connection(:server, *args, &blk)
28
+ end
29
+
30
+ private
31
+ def make_some_connection(sfx, addr, handler, &blk)
32
+ addr = addr.parsed_uri
33
+ pfx = MAP[addr.scheme]
34
+ __send__("#{pfx}_#{sfx}_connection", addr, handler, &blk)
35
+ end
36
+
37
+ #
38
+ # Particular protocols
39
+ #
40
+ def emrpc_tcp_client_connection(addr, handler, &blk)
41
+ EventMachine.connect(addr.host, addr.port, handler, &blk)
42
+ end
43
+
44
+ def emrpc_tcp_server_connection(addr, handler, &blk)
45
+ EventMachine.start_server(addr.host, addr.port, handler, &blk)
46
+ end
47
+
48
+ def emrpc_unix_client_connection(addr, handler, &blk)
49
+ EventMachine.connect_unix_domain(addr.path, handler, &blk)
50
+ end
51
+
52
+ def emrpc_unix_server_connection(addr, handler, &blk)
53
+ EventMachine.start_unix_domain_server(addr.path, handler, &blk)
54
+ end
55
+
56
+ end # ProtocolMapper
57
+ end # EMRPC
@@ -0,0 +1,105 @@
1
+ module EMRPC
2
+ # ReconnectingPid collects all messages in the backlog buffer and tries to reconnect.
3
+ # Calls self.on_raise() with the following exceptions:
4
+ # *
5
+ #
6
+ class ReconnectingPid
7
+ include Pid
8
+
9
+ DEFAULT_MAX_BACKLOG = 256
10
+ DEFAULT_MAX_ATTEMPTS = 5
11
+ DEFAULT_TIMEOUT = 5 # sec.
12
+ DEFAULT_TIMER = Timers::EVENTED
13
+
14
+ # Arguments:
15
+ # address Address if a pid or the pid itself to connect to.
16
+ #
17
+ # Options:
18
+ # :max_backlog Maximum backlog size. BacklogError is raised when backlog becomes larger than
19
+ # the specified size. Default is 256 messages.
20
+ #
21
+ # :max_attempts Maximum number of connection attempts. AttemptsError is raised when this number is exceeded.
22
+ # Counter is set to zero after each successful connection. Default is 5 attempts.
23
+ #
24
+ # :timeout Time interval in seconds. TimeoutError is raised when connection was not established
25
+ # in the specified amount of time. Default is 5 seconds.
26
+ #
27
+ # :timer Proc which runs a periodic timer. Default is Timers::EVENTED. See EMRPC::Timers for more info.
28
+ #
29
+ def initialize(address, options = {})
30
+ super(address, options)
31
+
32
+ @address = address
33
+
34
+ # Options
35
+
36
+ @max_backlog = options[:max_backlog] || DEFAULT_MAX_BACKLOG
37
+ @max_attempts = options[:max_attempts] || DEFAULT_MAX_ATTEMPTS
38
+ @timeout = options[:timeout] || DEFAULT_TIMEOUT
39
+ @timer = options[:timer] || DEFAULT_TIMER
40
+
41
+ # Gentlemen, start your engines!
42
+
43
+ @attempts = 1
44
+ @backlog = Array.new
45
+ @timeout_thread = @timer.call([ @timeout, 1 ].max, method(:timer_action))
46
+ connect(address)
47
+ end
48
+
49
+ def send(*args)
50
+ if rpid = @rpid
51
+ rpid.send(*args)
52
+ else
53
+ @backlog.push(args)
54
+ if @backlog.size > @max_backlog
55
+ on_raise(self, BacklogError.new("Backlog exceeded maximum size of #{@max_backlog} messages"))
56
+ end
57
+ end
58
+ end
59
+
60
+ def flush!
61
+ while args = @backlog.shift
62
+ send(*args)
63
+ end
64
+ end
65
+
66
+ def connected(rpid)
67
+ @rpid = rpid
68
+ @attempts = 1
69
+ flush!
70
+ end
71
+
72
+ def disconnected(rpid)
73
+ @rpid = nil
74
+ connect(@address) unless killed?
75
+ end
76
+
77
+ def connection_failed(conn)
78
+ a = (@attempts += 1)
79
+ if a > @max_attempts
80
+ on_raise(self, AttemptsError.new("Maximum number of #{@max_attempts} connecting attempts exceeded"))
81
+ end
82
+ connect(@address)
83
+ end
84
+
85
+ def timer_action
86
+ if @rpid
87
+ @state = nil
88
+ return
89
+ end
90
+
91
+ if @state == :timeout
92
+ @state = nil
93
+ on_raise(self, TimeoutError.new("Failed to reconnect with #{@timeout} sec. timeout"))
94
+ else
95
+ @state = :timeout
96
+ end
97
+ end
98
+
99
+ class ReconnectingError < StandardError; end
100
+ class BacklogError < ReconnectingError; end
101
+ class AttemptsError < ReconnectingError; end
102
+ class TimeoutError < ReconnectingError; end
103
+
104
+ end # ReconnectingPid
105
+ end # EMRPC
@@ -0,0 +1,73 @@
1
+ module EMRPC
2
+ module RemoteConnection
3
+ include ConnectionMixin
4
+ attr_accessor :address
5
+
6
+ #
7
+ # IMPORTANT: server doesn't trigger #connection_completed callback.
8
+ #
9
+ def post_init
10
+ # setup single-shot version of receive_marshalled_message
11
+ class <<self
12
+ alias_method :receive_marshalled_message, :receive_handshake_message
13
+ end
14
+ end
15
+
16
+ #
17
+ # Handshake protocol
18
+ #
19
+ def connection_completed
20
+ send_handshake_message(@local_pid.options)
21
+ end
22
+
23
+ def send_handshake_message(arg)
24
+ @__sent_handshake = true
25
+ send_marshalled_message([:handshake, arg])
26
+ end
27
+
28
+ def receive_handshake_message(msg)
29
+ prefix, options = msg
30
+ lpid = @local_pid
31
+ prefix == :handshake or return lpid.handshake_failed(self, msg)
32
+ rpid = RemotePid.new(self, options)
33
+ # restore receive_marshalled_message
34
+ class <<self
35
+ alias_method :receive_marshalled_message, :receive_regular_message
36
+ end
37
+ unless @__sent_handshake # server-side
38
+ send_handshake_message(@local_pid.options)
39
+ end
40
+ @remote_pid = lpid.connection_established(rpid, self)
41
+ end
42
+
43
+ #
44
+ # Regular protocol
45
+ #
46
+ def send_raw_message(args)
47
+ send_marshalled_message(args.encode_b381b571_1ab2_5889_8221_855dbbc76242(@local_pid))
48
+ end
49
+
50
+ def receive_regular_message(msg)
51
+ lpid = @local_pid
52
+ lpid.__send__(*(msg.decode_b381b571_1ab2_5889_8221_855dbbc76242(lpid)))
53
+ end
54
+
55
+ def rescue_marshal_error(e)
56
+ raise e
57
+ end
58
+
59
+ def unbind
60
+ if @remote_pid
61
+ # pid has been succesfully connected one day, but connection was lost.
62
+ # we don't put +_unregister_pid+ into +connection_lost+ callback to avoid unneccessary +super+ calls in callbacks.
63
+ rpid = @remote_pid
64
+ @remote_pid = nil
65
+ @local_pid.connection_unbind(rpid, self)
66
+ else
67
+ # there were no connection, connecting failed.
68
+ @local_pid.connection_failed(self)
69
+ end
70
+ end
71
+
72
+ end # RemoteConnection
73
+ end # EMRPC