arpie 0.0.6 → 0.1.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/COPYING DELETED
@@ -1,15 +0,0 @@
1
- Copyright (C) 2008 Bernhard Stoeckner <elven@swordcoast.net> and contributors
2
-
3
- This program is free software; you can redistribute it and/or modify
4
- it under the terms of the GNU General Public License as published by
5
- the Free Software Foundation; either version 2 of the License, or
6
- (at your option) any later version.
7
-
8
- This program is distributed in the hope that it will be useful,
9
- but WITHOUT ANY WARRANTY; without even the implied warranty of
10
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
- GNU General Public License for more details.
12
-
13
- You should have received a copy of the GNU General Public License along
14
- with this program; if not, write to the Free Software Foundation, Inc.,
15
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
data/README DELETED
@@ -1,167 +0,0 @@
1
- = What's this?
2
-
3
- Arpie is a end-to-end framework for sending protocol-encoded messages over arbitary
4
- IO channels, including UNIX/TCP Sockets, Pipes, and pidgeon carriers (depending on
5
- your implementation details).
6
-
7
- Arpie also provides a robust replay-protected RPC framework.
8
-
9
- The useful core of arpie is a protocol stack that can be used to read/split/assemble/write
10
- any data stream, but is tailored for packeted streaming data.
11
-
12
- The Arpie server uses one ruby-thread per client, the client runs entirely in the
13
- calling thread; though an example implementation for evented callbacks is provided.
14
-
15
- == Source Code
16
-
17
- Source code is in git[http://git.swordcoast.net/?p=lib/ruby/arpie.git;a=summary].
18
-
19
- You can contact me via email at elven@swordcoast.net.
20
-
21
- arpie is available on the rubygems gem server - just do <tt>gem1.8 install arpie</tt>
22
- to get the newest version.
23
-
24
-
25
- == Simple, contrived example: A string reverse server
26
-
27
- require 'rubygems'
28
- require 'arpie'
29
- require 'socket'
30
-
31
- server = TCPServer.new(51210)
32
-
33
- e = Arpie::Server.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
34
-
35
- e.handle do |server, ep, msg|
36
- ep.write_message msg.reverse
37
- end
38
-
39
- e.accept do
40
- server.accept
41
- end
42
-
43
- c = Arpie::Client.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
44
- c.connect do
45
- TCPSocket.new("127.0.0.1", 51210)
46
- end
47
-
48
- c.write_message "hi"
49
- puts c.read_message
50
- # => "ih"
51
-
52
- == Advanced, but still simple example: Using Proxy to access remote objects
53
-
54
- require 'rubygems'
55
- require 'arpie'
56
- require 'socket'
57
-
58
- class MyHandler
59
- def reverse str
60
- str.reverse
61
- end
62
- end
63
-
64
- server = TCPServer.new(51210)
65
-
66
- e = Arpie::ProxyServer.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
67
-
68
- e.handle MyHandler.new
69
-
70
- e.accept do
71
- server.accept
72
- end
73
-
74
- p = Arpie::ProxyClient.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
75
- p.connect do |transport|
76
- TCPSocket.new("127.0.0.1", 51210)
77
- end
78
-
79
- puts p.reverse "hi"
80
- # => "ih"
81
-
82
- == Writing custom Protocols
83
-
84
- You can use arpies Protocol layer to write your custom protocol parser/emitters.
85
- Consider the following, again very contrived, example. You have a linebased wire format,
86
- which sends regular object updates in multiple lines, each holding a property to be updated.
87
- What objects get updated is not relevant to this example.
88
-
89
- For this example, we'll be using the SeparatorProtocol already contained in protocols.rb as
90
- a base.
91
-
92
- class AssembleExample < Arpie::Protocol
93
-
94
- def from binary
95
- # The wire format is simply a collection of lines
96
- # where the first one is a number containing the
97
- # # of lines to expect.
98
- assemble! binary do |binaries, meta|
99
- binaries.size >= 1 or incomplete!
100
- binaries.size - 1 >= binaries[0].to_i or incomplete!
101
-
102
- # Here, you can wrap all collected updates in
103
- # whatever format you want it to be. We're just
104
- # "joining" them to be a single array.
105
- binaries.shift
106
- binaries
107
- end
108
- end
109
-
110
- def to object
111
- yield object.size
112
- object.each {|oo|
113
- yield oo
114
- }
115
- end
116
- end
117
-
118
- p = Arpie::ProtocolChain.new(
119
- AssembleExample.new,
120
- Arpie::SeparatorProtocol.new
121
- )
122
- r, w = IO.pipe
123
-
124
- p.write_message(w, %w{we want to be assembled})
125
-
126
- p p.read_message(r)
127
- # => ["we", "want", "to", "be", "assembled"]
128
-
129
- == Replay protection
130
-
131
- It can happen that a Client loses connection to a Server.
132
- In that case, the Transport tries transparently reconnecting by simply
133
- invoking the block again that was given to Client#connect.
134
- See the Client accessors for modifying this behaviour.
135
-
136
- It is assumed that each call, that is being placed, is atomic - eg, no
137
- connection losses in between message send and receive; lost messages
138
- will be retransmitted. Some Protocol classes provide support for replay
139
- protection through in-band UUIDs; though it is not a requirement to implement it.
140
- If a UUID is provided in the data stream, the Protocol will not call
141
- the handler again for retransmissions, but instead reply with the old,
142
- already evaluated value.
143
-
144
- Not all protocols support UUIDs; those who do not offer no replay protection,
145
- and special care has to be taken elsewhere.
146
-
147
- All object-encoding protocols support UUIDs, including YAML and Marshal.
148
- XMLRPC does not.
149
-
150
- == Benchmarks
151
-
152
- There is a benchmark script included in the git repository (and in the gem
153
- under tools/). A sample output follows; your milage may vary.
154
-
155
- user system total real
156
-
157
- native DRb
158
- 1 0.000000 0.000000 0.000000 ( 0.000172)
159
- 1000 0.110000 0.010000 0.120000 ( 0.119767)
160
-
161
- Arpie: proxied MarshalProtocol with replay protection through uuidtools
162
- 1 0.000000 0.000000 0.010000 ( 0.075373)
163
- 1000 0.530000 0.090000 0.600000 ( 0.608665)
164
-
165
- Arpie: proxied MarshalProtocol without replay protection
166
- 1 0.000000 0.000000 0.000000 ( 0.000173)
167
- 1000 0.170000 0.020000 0.190000 ( 0.194649)
@@ -1,195 +0,0 @@
1
- module Arpie
2
-
3
- # A Client is a connection manager, and acts as the
4
- # glue between a user-defined medium (for example, a TCP
5
- # socket), and a protocol, with automatic reconnecting
6
- # and fault handling.
7
- #
8
- # See README for examples.
9
- class Client
10
- # The protocol chain used.
11
- attr_reader :protocol
12
-
13
- # How often should this Client retry a connection.
14
- # 0 for never, greater than 0 for that many attempts,
15
- # nil for infinite (default).
16
- # Values other than nil will raise network exceptions
17
- # to the caller.
18
- attr_accessor :connect_retry
19
-
20
- # How long should the caller sleep after each reconnect
21
- # attempt. (default: 1.0). The default value is probably
22
- # okay. Do not set this to 0; that will produce
23
- # unnecessary load in case of network failure.
24
- attr_accessor :connect_sleep
25
-
26
- def initialize *protocols
27
- @protocol = Arpie::ProtocolChain.new(*protocols)
28
- @read_io = nil
29
- @write_io = nil
30
- @connector = lambda { raise ArgumentError, "No connector specified, cannot connect to Endpoint." }
31
- @connect_retry = nil
32
- @connect_sleep = 1.0
33
- @on_error = lambda {|client, exception|
34
- $stderr.puts "Error in Transport IO: #{exception.message.to_s}"
35
- $stderr.puts exception.backtrace.join("\n")
36
- $stderr.puts "Set Transport#on_error &block to override this."
37
- }
38
- end
39
-
40
- # Provide a connector block, which will be called
41
- # each time a connection is needed.
42
- # Expectes an IO object.
43
- # Alternatively, you can return a two-item array.
44
- # To test something without involving any networking,
45
- # simply run IO.pipe in this block.
46
- # Set +connect_immediately+ to true to connect
47
- # immediately, instead on the first message.
48
- def connect connect_immediately = false, &connector
49
- @connector = connector
50
- _connect if connect_immediately
51
- self
52
- end
53
-
54
- # Set an error handler. It will be called with two
55
- # parameters, the client, and the exception that occured.
56
- # Optional, and just for notification.
57
- def on_error &handler #:yields: client, exception
58
- @on_error = handler
59
- self
60
- end
61
-
62
- # Send a message. Returns immediately.
63
- def write_message message
64
- io_retry do
65
- @protocol.write_message(@write_io, message)
66
- end
67
- end
68
- alias_method :<<, :write_message
69
-
70
- # Receive a message. Blocks until received.
71
- def read_message
72
- io_retry do
73
- return @protocol.read_message(@read_io)
74
- end
75
- end
76
-
77
- # Execute the given block until all connection attempts
78
- # have been exceeded.
79
- # Yields self.
80
- # You do not usually want to use this.
81
- def io_retry &block
82
- try = 0
83
-
84
- begin
85
- _connect
86
- yield self
87
- rescue IOError => e
88
- try += 1
89
- @on_error.call(self, e) if @on_error
90
- p e
91
-
92
- if @connect_retry == 0 || (@connect_retry && try > @connect_retry)
93
- raise EOFError, "Cannot read from io: lost connection after #{try} attempts (#{e.message.to_s})"
94
- end
95
-
96
- sleep @connect_sleep
97
- begin; @read_io.close if @read_io; rescue; end
98
- @read_io = nil
99
- begin; @write_io.close if @write_io; rescue; end
100
- @write_io = nil
101
- retry
102
- end
103
- end
104
-
105
- private
106
-
107
- def _connect
108
- @read_io and return
109
- @read_io, @write_io = @connector.call(self)
110
- @protocol.reset
111
- @write_io ||= @read_io
112
- end
113
- end
114
-
115
- # A simple pseudo event-based client, using a thread
116
- # with a callback.
117
- class EventedClient < Client
118
- private :read_message
119
-
120
- # Set a callback for incoming messages.
121
- def handle &handler #:yields: client, message
122
- @handler = handler
123
- end
124
-
125
- private
126
-
127
- def _read_thread
128
- loop do
129
- io_retry do
130
- message = read_message
131
- @handler and @handler.call(self, message)
132
- end
133
- end
134
- end
135
-
136
- def _connect
137
- super
138
- @read_thread ||= Thread.new { _read_thread }
139
- end
140
- end
141
-
142
-
143
- # A Client extension which provides a RPC-like
144
- # interface. Used by ProxyClient.
145
- class RPCClient < Client
146
- private :read_message, :write_message
147
-
148
- def initialize *protocols
149
- super(*protocols)
150
-
151
- @on_pre_call = lambda {|client, message| }
152
- @on_post_call = lambda {|client, message, reply| }
153
- end
154
-
155
- # Callback that gets invoked before placing a call to the
156
- # Server. You can stop the call from happening by raising
157
- # an exception (which will be passed on to the caller).
158
- def pre_call &handler #:yields: client, message
159
- @on_pre_call = handler
160
- self
161
- end
162
-
163
- # Callback that gets invoked after receiving an answer.
164
- # You can raise an exception here; and it will be passed
165
- # to the caller, instead of returning the value.
166
- def post_call &handler #:yields: client, message, reply
167
- @on_post_call = handler
168
- self
169
- end
170
-
171
-
172
- # Send a message and receive a reply in a synchronous
173
- # fashion. Will block until transmitted, or until
174
- # all reconnect attempts failed.
175
- def request message
176
- reply = nil
177
-
178
- @on_pre_call.call(self, message) if @on_pre_call
179
-
180
- io_retry do
181
- write_message(message)
182
- reply = read_message
183
- end
184
-
185
- @on_post_call.call(self, message, reply) if @on_post_call
186
-
187
- case reply
188
- when Exception
189
- raise reply
190
- else
191
- reply
192
- end
193
- end
194
- end
195
- end
@@ -1,143 +0,0 @@
1
- require 'uuidtools'
2
-
3
- module Arpie
4
-
5
- # A Endpoint which supports arbitary objects as handlers,
6
- # instead of a proc.
7
- #
8
- # Note that this will only export public instance method
9
- # of the class as they are defined.
10
- class ProxyServer < Server
11
-
12
- # An array containing symbols of method names that the
13
- # handler should be allowed to call. Defaults to
14
- # all public instance methods the class defines (wysiwyg).
15
- # Set this to nil to allow calling of ALL methods, but be
16
- # warned of the security implications (instance_eval, ..).
17
- attr_accessor :interface
18
-
19
- # Set this to false to disable replay protection.
20
- attr_accessor :uuid_tracking
21
-
22
- # The maximum number of method call results to remember.
23
- # Defaults to 100, which should be enough for everyone. ;)
24
- attr_accessor :max_uuids
25
-
26
- def initialize *va
27
- super
28
- @uuids = {}
29
- @max_uuids = 100
30
- @uuid_tracking = true
31
- end
32
-
33
- # Set a class handler. All public instance methods will be
34
- # callable over RPC (with a Proxy object) (see attribute interface).
35
- def handle handler
36
- @handler = handler
37
- @interface = handler.class.public_instance_methods(false).map {|x|
38
- x.to_sym
39
- }
40
- self
41
- end
42
-
43
- private
44
-
45
- def _handle endpoint, message
46
- if !@handler.respond_to?(message.meth.to_sym) ||
47
- (@interface && !@interface.index(message.meth.to_sym))
48
- endpoint.write_message NoMethodError.new("No such method: #{message.meth.inspect}")
49
- return
50
- end
51
-
52
- begin
53
- # Prune old serials. This can probably be optimized, but works well enough for now.
54
- if @uuid_tracking && message.uuid
55
- uuid, serial = * message.uuid
56
-
57
- raise ArgumentError,
58
- "Invalid UUID given, expect [uuid/64bit, serial/numeric]." unless
59
- uuid.is_a?(Integer) && serial.is_a?(Integer)
60
-
61
- # Limit to sane values.
62
- uuid &= 0xffffffffffffffff
63
- serial &= 0xffffffffffffffff
64
-
65
- timestamps = @uuids.values.map {|v| v[0] }.sort
66
- latest_timestamp = timestamps[-@max_uuids]
67
- @uuids.reject! {|uuid, (time, value)|
68
- time < latest_timestamp
69
- } if latest_timestamp
70
-
71
- endpoint.write_message((@uuids[message.uuid] ||=
72
- [Time.now, @handler.send(message.meth, *message.argv)])[1])
73
-
74
- else
75
- endpoint.write_message @handler.send(message.meth, *message.argv)
76
- end
77
- rescue IOError
78
- raise
79
-
80
- rescue Exception => e
81
- endpoint.write_message RuntimeError.new("Internal Error")
82
- raise
83
- end
84
- end
85
- end
86
-
87
- # A Proxy is a wrapper around a Client, which transparently tunnels
88
- # method calls to the remote ProxyServer.
89
- # Note that the methods of Client cannot be proxied.
90
- class ProxyClient < RPCClient
91
- attr_accessor :namespace
92
-
93
- # Set to false to disable replay protection.
94
- # Default is true.
95
- attr_accessor :replay_protection
96
-
97
- # The current serial for this transport.
98
- attr_accessor :serial
99
-
100
- # The generated uuid for this Client.
101
- # nil if no call has been made yet.
102
- attr_accessor :uuid
103
-
104
- def initialize *protocols
105
- super
106
- @protocol, @namespace = protocol, ""
107
- @serial = 0
108
- @uuid_generator = lambda {|client, method, argv|
109
- UUIDTools::UUID.random_create.to_i
110
- }
111
- @replay_protection = true
112
- end
113
-
114
- # Set up a new UUID generator for this proxy client.
115
- # Make sure that this yields really random numbers.
116
- # The default uses the uuidtools gem and is usually okay.
117
- #
118
- # This gets called exactly once for each created ProxyClient.
119
- def uuid_generator &handler #:yields: client, method, argv
120
- @uuid_generator = handler
121
- self
122
- end
123
-
124
- def method_missing meth, *argv # :nodoc:
125
- serial = nil
126
- if @replay_protection
127
- serial = [
128
- @uuid ||= @uuid_generator.call(self, meth, argv),
129
- @serial += 1
130
- ]
131
- end
132
-
133
- call = Arpie::RPCall.new(@namespace, meth, argv, serial)
134
- ret = self.request(call)
135
- case ret
136
- when Exception
137
- raise ret
138
- else
139
- ret
140
- end
141
- end
142
- end
143
- end