zmqmachine 0.3.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/.bnsignore ADDED
@@ -0,0 +1,20 @@
1
+ # The list of files that should be ignored by Mr Bones.
2
+ # Lines that start with '#' are comments.
3
+ #
4
+ # A .gitignore file can be used instead by setting it as the ignore
5
+ # file in your Rakefile:
6
+ #
7
+ # Bones {
8
+ # ignore_file '.gitignore'
9
+ # }
10
+ #
11
+ # For a project with a C extension, the following would be a good set of
12
+ # exclude patterns (uncomment them if you want to use them):
13
+ # *.[oa]
14
+ # *~
15
+ announcement.txt
16
+ coverage
17
+ doc
18
+ pkg
19
+ examples/work
20
+ .DS_Store
data/History.txt ADDED
@@ -0,0 +1,21 @@
1
+ == 0.3.0 / 2010-08-15
2
+ * Added XREQ/XREP socket types
3
+ * Moved the time functions for ZM::Timers into a pair of
4
+ class methods. Instead of accessing Time directly, it should
5
+ be accessed via ZM::Timers.now and ZM::Timers.now_converted.
6
+ Doing so allows the programmer to reopen the ZM::Timers class and
7
+ redefine how time is procured if necessary (e.g. every second of
8
+ wall clock time should translate to 1 minute of time inside the
9
+ reactor).
10
+ * Still needs specs!
11
+
12
+ == 0.2.0 / 2010-06-06
13
+ * Updated internals to conform to the 0mq api as of
14
+ release 2.0.7.
15
+
16
+ * Minor api enhancements.
17
+
18
+ == 0.1.0 / 2010-06-02
19
+
20
+ * 1 major enhancement
21
+ * Birthday!
data/README.rdoc ADDED
@@ -0,0 +1,85 @@
1
+ zmqmachine
2
+ by Chuck Remes
3
+ http://github.com/chuckremes/zmqmachine
4
+
5
+ == DESCRIPTION:
6
+
7
+ ZMQMachine is another Ruby implementation of the reactor pattern but this
8
+ time using 0mq sockets rather than POSIX sockets.
9
+
10
+ Unlike the great Eventmachine ruby project and the Python Twisted
11
+ project which work with POSIX sockets, ZMQMachine is inherently threaded. The
12
+ 0mq sockets backing the reactor use a thread pool for performing
13
+ their work so already it is different from most other reactors. Also, a
14
+ single program may create multiple reactor instances which runs in
15
+ its own thread. All activity within the reactor is single-threaded
16
+ and asynchronous.
17
+
18
+ It is possible to extend the 0mq library to "poll" normal file
19
+ descriptors. This isn't on my roadmap but patches are accepted.
20
+
21
+ == FEATURES/PROBLEMS:
22
+
23
+ * No specs yet.
24
+
25
+ * Documentation is limited. I need to write up a lot more detail on the Handler classes passed
26
+ to socket instances and how this differs from the Eventmachine way.
27
+
28
+ * Exceptions and error codes haven't really been implemented yet.
29
+
30
+ * Some classes are just skeletons.
31
+
32
+ * Recommended for JRuby since it is the only existing runtime that uses
33
+ native threads without a GIL. MRI 1.9.x is okay but may have
34
+ threading problems.
35
+
36
+ == SYNOPSIS:
37
+
38
+ Read and execute the examples in the examples directory.
39
+
40
+ == REQUIREMENTS:
41
+
42
+ Requires the 0mq library
43
+
44
+ * 0mq 2.0.7
45
+
46
+ Depends on 2 external gems.
47
+
48
+ * ffi-rzmq (>= 0.5.0)
49
+ * ffi (> 0.6.3) (built from master for threading fixes)
50
+
51
+ == INSTALL:
52
+
53
+ Make sure the 0MQ library is already installed on your system. Secondly,
54
+ make sure the FFI gem is built from its project master (see Requirements).
55
+ Lastly, verify the ffi-rzmq gem is built from its project master.
56
+
57
+ % git clone github.com/chuckremes/zmqmachine.git
58
+ % cd zmqmachine
59
+ % gem build zmqmachine.gemspec
60
+ % gem install zmqmachine-*.gem
61
+
62
+ == LICENSE:
63
+
64
+ (The MIT License)
65
+
66
+ Copyright (c) 2010 Chuck Remes
67
+
68
+ Permission is hereby granted, free of charge, to any person obtaining
69
+ a copy of this software and associated documentation files (the
70
+ 'Software'), to deal in the Software without restriction, including
71
+ without limitation the rights to use, copy, modify, merge, publish,
72
+ distribute, sublicense, and/or sell copies of the Software, and to
73
+ permit persons to whom the Software is furnished to do so, subject to
74
+ the following conditions:
75
+
76
+ The above copyright notice and this permission notice shall be
77
+ included in all copies or substantial portions of the Software.
78
+
79
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
80
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
81
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
82
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
83
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
84
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
85
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+
2
+ begin
3
+ require 'bones'
4
+ rescue LoadError
5
+ abort '### Please install the "bones" gem ###'
6
+ end
7
+
8
+ task :default => 'test:run'
9
+ task 'gem:release' => 'test:run'
10
+
11
+ Bones {
12
+ name 'zmqmachine'
13
+ authors 'Chuck Remes'
14
+ email 'cremes@mac.com'
15
+ url 'http://github.com/chuckremes/zmqmachine'
16
+ ignore_file '.gitignore'
17
+ readme_file 'README.rdoc'
18
+
19
+ depend_on 'ffi-rzmq', '>= 0.5.0'
20
+ }
21
+
@@ -0,0 +1,242 @@
1
+
2
+ require 'rubygems'
3
+ require 'ffi-rzmq'
4
+ require 'json'
5
+ require '../lib/zmqmachine'
6
+
7
+ # This example shows how to pair up REQ/REP sockets with PUB/SUB
8
+ # sockets for doing an RPC-style request with an out-of-band
9
+ # response. The RPC reply contains information about the out-of-
10
+ # band data so the servers know that the transfer was complete.
11
+ #
12
+ # This is NOT an implementation of a real FTP server; I used
13
+ # the acronym to highlight the role of each component.
14
+ #
15
+
16
+
17
+ server_address = ZM::Address.new '127.0.0.1', 6555, :tcp
18
+
19
+
20
+ class FTPDataClient
21
+ include ZM::Deferrable
22
+
23
+ def initialize context, address, topic, process
24
+ @context = context
25
+ @address = address
26
+ @topic = topic
27
+ @process = process
28
+ @received_count = 0
29
+ @expected_count = -1
30
+
31
+ callback do
32
+ puts "FTPDataClient#callback fired, received all file chunks"
33
+ @context.next_tick { @context.stop }
34
+ end
35
+ end
36
+
37
+ def on_attach socket
38
+ @socket = socket
39
+ @socket.bind @address
40
+ @socket.subscribe @topic
41
+ end
42
+
43
+ def on_readable socket, messages
44
+ obj = decode strip_topic(messages.first.copy_out_string)
45
+ puts "#{self.class.name}#on_readable: process obj #{obj.inspect}"
46
+ @received_count += @process.call(obj)
47
+ succeed if complete?
48
+ end
49
+
50
+ def on_writable(socket); puts "#{self.class.name}#on_writable, should never be called"; end
51
+
52
+ def should_expect count
53
+ puts "#{self.class.name}#should_expect, set to expect [#{count}], already received [#{@received_count}]"
54
+ @expected_count = count
55
+ succeed if complete?
56
+ end
57
+
58
+
59
+ private
60
+
61
+ def strip_topic payload
62
+ payload.split('|').last
63
+ end
64
+
65
+ def complete?
66
+ @expected_count > 0 && @expected_count == @received_count
67
+ end
68
+
69
+ def decode string
70
+ JSON.parse string
71
+ end
72
+ end
73
+
74
+ class FTPControlClient
75
+ def initialize context, address
76
+ @context = context
77
+ @address = address
78
+ @received_count = 0
79
+ end
80
+
81
+ def on_attach socket
82
+ @socket = socket
83
+ rc = socket.connect @address
84
+ end
85
+
86
+ def on_readable socket, messages
87
+ reply = decode messages.first.copy_out_string
88
+ puts "#{self.class.name}#on_readable: reply from FTPDataServer #{reply.inspect}"
89
+
90
+ @data_client.should_expect reply['total_chunks']
91
+ end
92
+
93
+ def get filename, &blk
94
+ sub_address = ZM::Address.new @address.host, 5556, @address.transport
95
+ topic = "file.#{filename}|"
96
+
97
+ @data_client = FTPDataClient.new @context, sub_address, topic, blk
98
+ @sub_socket = @context.sub_socket @data_client
99
+
100
+ req = {'filename' => filename, 'reply_to' => sub_address.to_s}
101
+
102
+ puts "#{self.class.name}#get: send request for the file chunks to be published"
103
+ @socket.send_message_string encode(req)
104
+ end
105
+
106
+
107
+ private
108
+
109
+ def decode string
110
+ JSON.parse string
111
+ end
112
+
113
+ def encode obj
114
+ JSON.generate obj
115
+ end
116
+ end
117
+
118
+ class FTPDataServer
119
+ include ZM::Deferrable
120
+
121
+ attr_reader :total_count
122
+
123
+ def initialize request
124
+ @request = request
125
+ @address_string = request['reply_to']
126
+ @topic = "file.#{request['filename']}|"
127
+ @total_count = 0
128
+ end
129
+
130
+ def on_attach socket
131
+ if open_file
132
+ @socket = socket
133
+ @socket.connect @address_string
134
+ else
135
+ puts "#{self.class.name}#on_attach: @total_count is 0"
136
+ succeed @total_count
137
+ end
138
+ end
139
+
140
+ def on_writable socket
141
+ if more_file_data?
142
+ @total_count += 1
143
+ obj = encode(next_file_chunk)
144
+ puts "#{self.class.name}#on_writable: send chunk [#{@total_count}], obj #{obj.inspect}"
145
+ socket.send_message_string obj
146
+ else
147
+ puts "#{self.class.name}#on_writable: done [#{@total_count}]"
148
+ succeed @total_count
149
+ end
150
+ end
151
+
152
+
153
+ private
154
+
155
+ def open_file
156
+ @chunks = rand(5) + 5
157
+ @chunks > 0
158
+ end
159
+
160
+ def more_file_data?
161
+ @chunks > 0
162
+ end
163
+
164
+ def next_file_chunk
165
+ @chunks -= 1
166
+ chunk = rand(999_999_999) + 1_000_000_000
167
+ {'chunk' => chunk}
168
+ end
169
+
170
+ def encode obj
171
+ "#{@topic}#{JSON.generate(obj)}"
172
+ end
173
+ end
174
+
175
+ class FTPControlServer
176
+ def initialize context, address
177
+ @context = context
178
+ @address = address
179
+ end
180
+
181
+ def on_attach socket
182
+ @socket = socket
183
+ rc = socket.bind @address
184
+ end
185
+
186
+ def on_readable socket, messages
187
+ request = decode messages.first.copy_out_string
188
+ puts "#{self.class.name}: query #{request.inspect}"
189
+ @pub_handler = FTPDataServer.new request
190
+ @pub_socket = @context.pub_socket @pub_handler
191
+
192
+ @pub_handler.callback do |total_chunks|
193
+ puts "FTPDataServer#callback fired, sent all file chunks"
194
+
195
+ reply = encode 'total_chunks' => total_chunks
196
+ socket.send_message_string reply
197
+
198
+ # clean up after ourselves
199
+ @context.close_socket @pub_socket
200
+ @pub_handler = nil
201
+ end
202
+ end
203
+
204
+ def on_writable socket
205
+ puts "#{self.class.name}#on_writable ERROR, should never be called"
206
+ end
207
+
208
+
209
+ private
210
+
211
+ def decode string
212
+ JSON.parse string
213
+ end
214
+
215
+ def encode obj
216
+ JSON.generate obj
217
+ end
218
+ end
219
+
220
+
221
+ # Run both handlers within the same reactor context
222
+ ctx1 = ZM::Reactor.new(:test).run do |context|
223
+
224
+ @request_server = FTPControlServer.new context, server_address
225
+ context.rep_socket @request_server
226
+
227
+ @request_client = FTPControlClient.new context, server_address
228
+ context.req_socket @request_client
229
+
230
+ @request_client.get('fakeo') do |file_chunk|
231
+ puts "block: file chunk #{file_chunk.inspect}"
232
+
233
+ # the number of chunks processed in this block; this return
234
+ # value is used by FTPDataClient#on_readable for tallying
235
+ # the total number of processed chunks.
236
+ 1
237
+ end
238
+
239
+ end
240
+
241
+
242
+ ctx1.join
@@ -0,0 +1,74 @@
1
+
2
+ require 'rubygems'
3
+ require 'ffi-rzmq'
4
+ require '../lib/zmqmachine'
5
+
6
+ # This example illustrates how a single handler can be used
7
+ # by multiple sockets.
8
+ #
9
+
10
+
11
+
12
+ Allowed_pongs = 100_000
13
+
14
+ class PingPongHandler
15
+ attr_reader :sent_count, :received_count
16
+
17
+ def initialize context
18
+ @context = context
19
+ @sent_count = 0
20
+ @received_count = 0
21
+ end
22
+
23
+ def on_attach socket
24
+ address = ZM::Address.new '127.0.0.1', 5555, :tcp
25
+
26
+ case socket.kind
27
+ when :reply
28
+ rc = socket.bind address
29
+ when :request
30
+ rc = socket.connect address
31
+ rc = socket.send_message_string "#{'a' * 2048}"
32
+ @sent_count += 1
33
+ end
34
+ end
35
+
36
+ def on_readable socket, messages
37
+ @received_count += 1
38
+
39
+ case socket.kind
40
+ when :reply
41
+ socket.send_message messages.first
42
+ when :request
43
+ socket.send_message messages.first
44
+ end
45
+
46
+ @sent_count += 1
47
+ @context.next_tick { @context.stop } if @sent_count == Allowed_pongs
48
+ end
49
+ end
50
+
51
+ handler = nil
52
+ # Run both handlers within the same reactor context
53
+ ctx1 = ZM::Reactor.new(:test).run do |context|
54
+ handler = PingPongHandler.new context
55
+
56
+ context.rep_socket handler
57
+
58
+ context.req_socket handler
59
+
60
+ start = Time.now
61
+ timer = context.periodical_timer(2000) do
62
+ now = Time.now
63
+ puts "[#{now - start}] seconds have elapsed; it is now [#{now}]"
64
+ end
65
+ end
66
+
67
+ ctx1.join
68
+ #puts "Started at [#{Time.now}]"
69
+ #puts "main thread will sleep [#{sleep_time}] seconds before aborting the context threads"
70
+ #sleep sleep_time
71
+ #
72
+ #ctx1.stop
73
+ #ctx2.stop
74
+ puts "received [#{handler.received_count}], sent [#{handler.sent_count}]"
@@ -0,0 +1,86 @@
1
+
2
+ require 'rubygems'
3
+ require 'ffi-rzmq'
4
+ require '../lib/zmqmachine'
5
+
6
+ # This example illustrates how to write a simple set of
7
+ # handlers for providing message ping-pong using
8
+ # a REQ/REP socket pair. All activity is asynchronous and
9
+ # relies on non-blocking I/O.
10
+
11
+
12
+ Allowed_pongs = 100_000
13
+
14
+
15
+ class PongHandler
16
+ attr_reader :sent_count, :received_count
17
+
18
+ def initialize context
19
+ @context = context
20
+ @sent_count = 0
21
+ @received_count = 0
22
+ end
23
+
24
+ def on_attach socket
25
+ address = ZM::Address.new '127.0.0.1', 5555, :tcp
26
+ rc = socket.bind address
27
+ end
28
+
29
+ def on_readable socket, messages
30
+ @received_count += 1
31
+ socket.send_message messages.first
32
+ @sent_count += 1
33
+
34
+ @context.next_tick { @context.stop } if @sent_count == Allowed_pongs
35
+ end
36
+ end
37
+
38
+ class PingHandler
39
+ attr_reader :sent_count, :received_count
40
+
41
+ def initialize context
42
+ @context = context
43
+ @sent_count = 0
44
+ @received_count = 0
45
+ end
46
+
47
+ def on_attach socket
48
+ address = ZM::Address.new '127.0.0.1', 5555, :tcp
49
+ rc = socket.connect address
50
+ rc = socket.send_message_string "#{'a' * 2048}"
51
+ @sent_count += 1
52
+ end
53
+
54
+ def on_readable socket, messages
55
+ @received_count += 1
56
+ rc = socket.send_message messages.first
57
+ @sent_count += 1
58
+ end
59
+ end
60
+
61
+ # Run both handlers within the same reactor context
62
+ ctx1 = ZM::Reactor.new(:test).run do |context|
63
+ @pong_handler = PongHandler.new context
64
+ context.rep_socket @pong_handler
65
+
66
+ @ping_handler = PingHandler.new context
67
+ context.req_socket @ping_handler
68
+
69
+ start = Time.now
70
+ timer = context.periodical_timer(2000) do
71
+ now = Time.now
72
+ puts "[#{now - start}] seconds have elapsed; it is now [#{now}]"
73
+ end
74
+ end
75
+
76
+ # Or, run each handler in separate contexts each with its
77
+ # own thread.
78
+ #ctx2 = ZM::Reactor.new(:test).run do |context|
79
+ # @ping_handler = PingHandler.new context
80
+ # context.req_socket @ping_handler
81
+ #end
82
+
83
+
84
+ ctx1.join
85
+ #ctx2.join
86
+ puts "received [#{@pong_handler.received_count}], sent [#{@pong_handler.sent_count}]"