cztop-reactor 0.1.0.pre20170316155217

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9ff9e9366325f56b5134fb0c8b7123c90b259709
4
+ data.tar.gz: f1effb0b47c1199772305b414d2f31e34bdb7798
5
+ SHA512:
6
+ metadata.gz: 41e02a93aab2823b3dd3b6d05e2da1a15d873f99cf1efaa4068a7b632ee595b2641d0bda7a5d13b43eb931095235000edfb93a9daadb71d7517187530ffcdda6
7
+ data.tar.gz: 3e391f785107c50fa41f4a4bb761fc1ad14cef76411cb3bb52dfb61d58a952376294a0d9aa86f0c81487984e8f5d9bfb2e0b48a35d9c2b17dbd2e5a91180a006
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ README.md
3
+ ChangeLog.md
4
+
5
+ LICENSE.txt
data/.rdoc_options ADDED
@@ -0,0 +1,16 @@
1
+ --- !ruby/object:RDoc::Options
2
+ encoding: UTF-8
3
+ static_path: []
4
+ rdoc_include:
5
+ - .
6
+ charset: UTF-8
7
+ exclude:
8
+ hyperlink_all: false
9
+ line_numbers: false
10
+ main_page: README.md
11
+ markup: markdown
12
+ show_hash: false
13
+ tab_width: 8
14
+ title: CZTop-Reactor Documentation
15
+ visibility: :protected
16
+ webcvs:
data/.simplecov ADDED
@@ -0,0 +1,9 @@
1
+ # Simplecov config
2
+
3
+ SimpleCov.start do
4
+ add_filter 'spec'
5
+ add_filter 'integration'
6
+ add_group "Needing tests" do |file|
7
+ file.covered_percent < 90
8
+ end
9
+ end
data/ChangeLog ADDED
@@ -0,0 +1,30 @@
1
+ 2017-03-16 Michael Granger <ged@FaerieMUD.org>
2
+
3
+ * .hgignore:
4
+ Ignore built gems
5
+ [594168b03b71] [tip]
6
+
7
+ * README.md:
8
+ Fix link in the README
9
+ [0391133d132d] [github/master]
10
+
11
+ * .hgignore:
12
+ Ignore generated docs directory
13
+ [ad4092564786]
14
+
15
+ * README.md:
16
+ Tabs -> spaces in the README
17
+ [41ec0ff65005]
18
+
19
+ * cztop-reactor.gemspec:
20
+ Add generated gemspec
21
+ [b3fcff597575]
22
+
23
+ * .document, .editorconfig, .gems, .hgignore, .hoerc, .pryrc,
24
+ .rdoc_options, .ruby-gemset, .ruby-version, .simplecov, Gemfile,
25
+ History.md, LICENSE.txt, Manifest.txt, README.md, Rakefile,
26
+ certs/ged.pem, lib/cztop/reactor.rb, lib/cztop/reactor/event.rb,
27
+ spec/cztop/reactor/event_spec.rb, spec/cztop/reactor_spec.rb,
28
+ spec/spec_helper.rb:
29
+ Initial commit.
30
+ [5e3f2358036b]
data/History.md ADDED
@@ -0,0 +1,4 @@
1
+ ## v0.0.1 [YYYY-MM-DD] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ Initial release.
4
+
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2017 Michael Granger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,14 @@
1
+ .document
2
+ .rdoc_options
3
+ .simplecov
4
+ ChangeLog
5
+ History.md
6
+ LICENSE.txt
7
+ Manifest.txt
8
+ README.md
9
+ Rakefile
10
+ lib/cztop/reactor.rb
11
+ lib/cztop/reactor/event.rb
12
+ spec/cztop/reactor/event_spec.rb
13
+ spec/cztop/reactor_spec.rb
14
+ spec/spec_helper.rb
data/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # CZTop-Reactor
2
+
3
+ home
4
+ : http://deveiate.org/projects/CZTop-Reactor
5
+
6
+ code
7
+ : http://bitbucket.org/ged/cztop-reactor
8
+
9
+ github
10
+ : https://github.com/ged/cztop-reactor
11
+
12
+ docs
13
+ : http://deveiate.org/code/cztop-reactor
14
+
15
+
16
+ ## Description
17
+
18
+ This is an implementation of the Reactor pattern described in [Pattern-Oriented
19
+ Software Architecture (Volume 2)][POSA2]. It allows an asynchronous application
20
+ to be described as one or more "reactions" to events, in this case either I/O
21
+ conditions on a ZMQ socket or a timer expiring.
22
+
23
+ A simple example:
24
+
25
+ # Start a SERVER socket, and print out any messages sent to it
26
+ reactor = CZTop::Reactor.new
27
+ socket = CZTop::Socket::SERVER.new
28
+ socket.bind( 'tcp://0.0.0.0:8' )
29
+ reactor.register( socket, :read ) do |event|
30
+ if event.readable?
31
+ message = event.socket.receive
32
+ puts "Read: %p" % [ message.to_a ]
33
+ end
34
+ end
35
+ reactor.start_polling
36
+
37
+
38
+ ## Prerequisites
39
+
40
+ It should run under any Ruby interpreter that CZTop will, which at the time of
41
+ this writing includes:
42
+
43
+ * MRI (2.3, 2.2)
44
+ * Rubinius (HEAD)
45
+ * JRuby 9000 (HEAD)
46
+
47
+ I am also using it (and CZTop) under MRI 2.4.
48
+
49
+
50
+ ## Installation
51
+
52
+ $ gem install cztop-reactor
53
+
54
+
55
+ ## Reasons
56
+
57
+ I considered submitting this as a patch to `cztop`, but in the end elected to
58
+ distribute it as a gem for two reasons:
59
+
60
+ 1. It depends on the `timers` gem, and I didn't want to add this dependency to
61
+ `cztop`. If the [`ztimerset`][ztimerset] spec ever comes out of draft status
62
+ and `cztop` adds an implementation of it, this wouldn't be necessary.
63
+ 2. I'm not confident enough in my FFI knowledge to know if this is an
64
+ appropriate way to implement this class. I've written numerous C extensions
65
+ for Ruby, but FFI is still a bit of a mystery to me, and likely will remain
66
+ so for the foreseeable future given my misgivings about using it.
67
+
68
+
69
+ ## Contributing
70
+
71
+ You can check out the current development source with Mercurial via its
72
+ {project page}[http://bitbucket.org/ged/cztop-reactor]. Or if you prefer Git,
73
+ via {its Github mirror}[https://github.com/ged/cztop-reactor].
74
+
75
+ After checking out the source, run:
76
+
77
+ $ rake newb
78
+
79
+ This task will install any missing dependencies, run the tests/specs,
80
+ and generate the API documentation.
81
+
82
+
83
+ ## License
84
+
85
+ This library includes source from the CZTop gem by Patrik Wenger, which is
86
+ distributed under the terms of the [ISC
87
+ License](http://opensource.org/licenses/ISC):
88
+
89
+ > Copyright (c) 2016, Patrik Wenger
90
+ >
91
+ > Permission to use, copy, modify, and/or distribute this software for
92
+ > any purpose with or without fee is hereby granted, provided that
93
+ > the above copyright notice and this permission notice appear in all
94
+ > copies.
95
+ >
96
+ > THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
97
+ > WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
98
+ > WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
99
+ > AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
100
+ > DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
101
+ > OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
102
+ > TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
103
+ > PERFORMANCE OF THIS SOFTWARE.
104
+
105
+ Everything else is distributed under the same license but is:
106
+
107
+ Copyright (c) 2017, Michael Granger
108
+
109
+
110
+ [POSA2]: http://www.cs.wustl.edu/~schmidt/POSA/POSA2/
111
+ [ztimerset]: http://czmq.zeromq.org/czmq4-0:ztimerset
112
+
113
+
data/Rakefile ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env rake
2
+
3
+ begin
4
+ require 'hoe'
5
+ rescue LoadError
6
+ abort "This Rakefile requires hoe (gem install hoe)"
7
+ end
8
+
9
+ GEMSPEC = 'cztop-reactor.gemspec'
10
+
11
+
12
+ Hoe.plugin :mercurial
13
+ Hoe.plugin :signing
14
+ Hoe.plugin :deveiate
15
+
16
+ Hoe.plugins.delete :rubyforge
17
+ Hoe.plugins.delete :gemcutter # Remove for public gems
18
+
19
+ hoespec = Hoe.spec 'cztop-reactor' do |spec|
20
+ self.readme_file = 'README.md'
21
+ self.history_file = 'History.md'
22
+ self.extra_rdoc_files = FileList[ '*.rdoc', '*.md' ]
23
+ self.urls = {
24
+ home: 'http://deveiate.org/projects/cztop-reactor',
25
+ code: 'http://bitbucket.org/ged/cztop-reactor',
26
+ docs: 'http://deveiate.org/code/cztop-reactor',
27
+ github: 'http://github.com/ged/cztop-reactor',
28
+ }
29
+
30
+ spec.license 'ISC'
31
+
32
+ spec.developer 'Michael Granger', 'ged@FaerieMUD.org'
33
+
34
+ spec.dependency 'loggability', '~> 0.14'
35
+ spec.dependency 'cztop', '~> 0.11'
36
+ spec.dependency 'timers', '~> 4.1'
37
+
38
+ spec.dependency 'hoe-deveiate', '~> 0.9', :developer
39
+ spec.dependency 'simplecov', '~> 0.13', :developer
40
+ spec.dependency 'rdoc-generator-fivefish', '~> 0.3', :developer
41
+
42
+ spec.require_ruby_version( '>=2.2.4' )
43
+ spec.hg_sign_tags = true if spec.respond_to?( :hg_sign_tags= )
44
+ spec.check_history_on_release = true if spec.respond_to?( :check_history_on_release= )
45
+
46
+ self.rdoc_locations << "deveiate:/usr/local/www/public/code/#{remote_rdoc_dir}"
47
+ end
48
+
49
+
50
+ ENV['VERSION'] ||= hoespec.spec.version.to_s
51
+
52
+ # Run the tests before checking in
53
+ task 'hg:precheckin' => [ :check_history, :check_manifest, :gemspec, :spec ]
54
+
55
+ task :test => :spec
56
+
57
+ # Rebuild the ChangeLog immediately before release
58
+ task :prerelease => 'ChangeLog'
59
+ CLOBBER.include( 'ChangeLog' )
60
+
61
+ desc "Build a coverage report"
62
+ task :coverage do
63
+ ENV["COVERAGE"] = 'yes'
64
+ Rake::Task[:spec].invoke
65
+ end
66
+ CLOBBER.include( 'coverage' )
67
+
68
+
69
+ # Use the fivefish formatter for docs generated from development checkout
70
+ if File.directory?( '.hg' )
71
+ require 'rdoc/task'
72
+
73
+ Rake::Task[ 'docs' ].clear
74
+ RDoc::Task.new( 'docs' ) do |rdoc|
75
+ rdoc.markup = 'markdown'
76
+ rdoc.main = "README.md"
77
+ rdoc.rdoc_files.include( "*.md", "ChangeLog", "lib/**/*.rb" )
78
+
79
+ rdoc.generator = :fivefish
80
+ rdoc.title = 'CZTop-Reactor'
81
+ rdoc.rdoc_dir = 'doc'
82
+ end
83
+ end
84
+
85
+ task :gemspec => GEMSPEC
86
+ file GEMSPEC => __FILE__
87
+ task GEMSPEC do |task|
88
+ spec = $hoespec.spec
89
+ spec.files.delete( '.gemtest' )
90
+ spec.signing_key = nil
91
+ spec.cert_chain = ['certs/ged.pem']
92
+ spec.version = "#{spec.version.bump}.0.pre#{Time.now.strftime("%Y%m%d%H%M%S")}"
93
+ File.open( task.name, 'w' ) do |fh|
94
+ fh.write( spec.to_ruby )
95
+ end
96
+ end
97
+ CLOBBER.include( GEMSPEC.to_s )
98
+
99
+ task :default => :gemspec
100
+
@@ -0,0 +1,350 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+ require 'timers'
6
+ require 'ffi'
7
+
8
+ require 'cztop'
9
+ require 'cztop/poller'
10
+ require 'cztop/has_ffi_delegate'
11
+
12
+
13
+ # An implementation of the Reactor pattern described in
14
+ # [Pattern-Oriented Software Architecture (Volume 2)][POSA2]. It allows
15
+ # an asynchronous application to be described as one or more "reactions"
16
+ # to events, in this case either I/O conditions on a ZMQ socket or a
17
+ # timer expiring.
18
+ #
19
+ # [POSA2]: http://www.cs.wustl.edu/~schmidt/POSA/POSA2/
20
+ #
21
+ class CZTop::Reactor
22
+ extend Loggability
23
+
24
+ # The version of this library
25
+ VERSION = '0.0.1'
26
+
27
+ # The maximum number of seconds to wait for events when there are no timers
28
+ # registered.
29
+ DEFAULT_POLL_INTERVAL = 0.250
30
+
31
+ # The events that can be registered and the corresponding mask
32
+ VALID_EVENTS = {
33
+ read: CZTop::Poller::ZMQ::POLLIN,
34
+ write: CZTop::Poller::ZMQ::POLLOUT,
35
+ }.freeze
36
+
37
+
38
+ autoload :Event, 'cztop/reactor/event'
39
+
40
+
41
+ # Loggability API -- set up a logger for this class
42
+ log_as :cztop
43
+
44
+
45
+ ### Create a new CZTop::Reactor
46
+ def initialize
47
+ @sockets = Hash.new do |hsh,key|
48
+ hsh[ key ] = { events: [], handler: nil }
49
+ end
50
+ @timers = Timers::Group.new
51
+ @wakeup_timer = @timers.every( DEFAULT_POLL_INTERVAL ) do
52
+ # No-op -- just ensures that new timers that are registered are only
53
+ # delayed by (at most) the DEFAULT_POLL_INTERVAL before they start.
54
+ end
55
+
56
+ @socket_pointers = {}
57
+
58
+ @poller_ptr = CZTop::Poller::ZMQ.poller_new
59
+ ObjectSpace.define_finalizer( @poller_ptr, -> (obj_id) {
60
+ # $stderr.puts "Freeing the poller pointer %p" % [ @poller_ptr ]
61
+ ptr_ptr = ::FFI::MemoryPointer.new( :pointer )
62
+ ptr_ptr.write_pointer( @poller_ptr )
63
+ CZTop::Poller::ZMQ.poller_destroy( ptr_ptr )
64
+ })
65
+ @event_ptr = ::FFI::MemoryPointer.new( CZTop::Poller::ZMQ::PollerEvent )
66
+ end
67
+
68
+
69
+ ######
70
+ public
71
+ ######
72
+
73
+ ##
74
+ # Sockets and the handlers that handle their IO
75
+ attr_reader :sockets
76
+
77
+ ##
78
+ # Registered timers as a Timers::Group
79
+ attr_reader :timers
80
+
81
+ ##
82
+ # The handle of the default timer that is used to ensure the polling loop
83
+ # notices new sockets and timers.
84
+ attr_reader :wakeup_timer
85
+
86
+
87
+ #
88
+ # Sockets
89
+ #
90
+
91
+ ### Register the specified +socket+ with the reactor for the specified +events+.
92
+ ### The following events are supported:
93
+ ###
94
+ ### [<tt>:read</tt>]
95
+ ### Data may be read from the socket without blocking.
96
+ ### [<tt>:write</tt>]
97
+ ### Data may be written to the socket without blocking.
98
+ ###
99
+ ### Registering a handle will unregister any previously registered
100
+ ### event/handler+arguments pairs associated with the handle.
101
+ ###
102
+ def register( socket, *events, &handler )
103
+ raise LocalJumpError, "no handler given" unless handler
104
+
105
+ self.unregister( socket )
106
+
107
+ ptr = self.ptr_for_socket( socket )
108
+ rc = CZTop::Poller::ZMQ.poller_add( @poller_ptr, ptr, nil, 0 )
109
+ self.log.debug "poller_add: rc = %p" % [ rc ]
110
+ CZTop::HasFFIDelegate.raise_zmq_err if rc == -1
111
+
112
+ self.log.info "Registered: %p with handler: %p" % [ socket, handler ]
113
+ self.sockets[ socket ][ :handler ] = handler
114
+ self.enable_events( socket, *events )
115
+
116
+ @socket_pointers[ ptr.to_i ] = socket
117
+ end
118
+ alias_method :add, :register
119
+ alias_method :register_socket, :register
120
+
121
+
122
+ ### Remove the specified <tt>socket</tt> from the receiver's list of registered
123
+ ### handles, if present. Returns the handle if it was registered, or
124
+ ### <tt>nil</tt> if it was not.
125
+ def unregister( socket )
126
+ if self.sockets.delete( socket )
127
+ self.log.info "Unregistering: %p" % [ socket ]
128
+ ptr = self.ptr_for_socket( socket )
129
+ rc = CZTop::Poller::ZMQ.poller_remove( @poller_ptr, ptr )
130
+ self.log.debug "poller_remove: rc = %p" % [ rc ]
131
+ CZTop::HasFFIDelegate.raise_zmq_err if rc == -1
132
+ end
133
+
134
+ @socket_pointers.delete( ptr.to_i )
135
+ end
136
+ alias_method :remove, :unregister
137
+ alias_method :unregister_socket, :unregister
138
+
139
+
140
+ ### Returns +true+ if the given +socket+ handle is registered with the reactor.
141
+ def registered?( socket )
142
+ return self.sockets.key?( socket )
143
+ end
144
+
145
+
146
+ ### Add the specified +events+ to the list that will be polled for on the
147
+ ### given +socket+.
148
+ def enable_events( socket, *events )
149
+ invalid = events - ( events & VALID_EVENTS.keys )
150
+ if !invalid.empty?
151
+ raise ArgumentError, "invalid events: %p" % [ invalid ]
152
+ end
153
+
154
+ socket = self.socket_for_ptr( socket ) if socket.is_a?( FFI::Pointer )
155
+ raise ArgumentError, "%p is not registered yet" % [ socket ] unless
156
+ self.registered?( socket )
157
+
158
+ self.sockets[ socket ][ :events ] |= events
159
+ self.update_poller_for( socket )
160
+ end
161
+ alias_method :enable_event, :enable_events
162
+ alias_method :enable_socket_events, :enable_events
163
+ alias_method :enable_socket_event, :enable_events
164
+
165
+
166
+ ### Remove the specified +events+ from the list that will be polled for on
167
+ ### the given +socket+ handle.
168
+ def disable_events( socket, *events )
169
+ socket = self.socket_for_ptr( socket ) if socket.is_a?( FFI::Pointer )
170
+ self.sockets[ socket ][:events] -= events
171
+ self.update_poller_for( socket )
172
+ end
173
+ alias_method :disable_socket_events, :disable_events
174
+
175
+
176
+ ### Returns +true+ if the specified +event+ is enabled for the given +socket+.
177
+ def event_enabled?( socket, event )
178
+ socket = self.socket_for_ptr( socket ) if socket.is_a?( FFI::Pointer )
179
+
180
+ return false unless self.sockets.key?( socket )
181
+ return self.sockets[ socket ][ :events ].include?( event )
182
+ end
183
+ alias_method :has_event_enabled?, :event_enabled?
184
+ alias_method :socket_event_enabled?, :event_enabled?
185
+
186
+
187
+ ### Returns <tt>true</tt> if no sockets are registered.
188
+ def empty?
189
+ return self.sockets.empty? && self.timers.empty?
190
+ end
191
+
192
+
193
+ ### Clear all registered sockets and returns the sockets that were cleared.
194
+ def clear
195
+ sockets = self.sockets.keys
196
+ sockets.each {|sock| self.unregister(sock) }
197
+ return sockets
198
+ end
199
+
200
+
201
+ #
202
+ # Timers
203
+ #
204
+
205
+ ### Register a timer that will call the specified +callback+ once after +delay+
206
+ ### seconds.
207
+ def add_oneshot_timer( delay, &callback )
208
+ self.log.debug "Registering a oneshot timer: will call %p after %0.2fs" %
209
+ [ callback, delay ]
210
+ return self.timers.after( delay, &callback )
211
+ end
212
+
213
+
214
+ ### Register a timer that will call the specified +callback+ once every
215
+ ### +delay+ seconds until it is cancelled.
216
+ def add_periodic_timer( delay, &callback )
217
+ self.log.debug "Registering a periodic timer: will call %p every %0.2fs" %
218
+ [ callback, delay ]
219
+ return self.timers.every( delay, &callback )
220
+ end
221
+
222
+
223
+ ### Remove the specified +timer+ from the reactor.
224
+ def remove_timer( timer )
225
+ timer.cancel
226
+ end
227
+
228
+
229
+ #
230
+ # Monitors
231
+ #
232
+
233
+ ### Create a CZTop::Monitor for the specified +socket+ that will listen for the
234
+ ### specified +events+ (which are monitor events, not I/O events). It will be automatically
235
+ ### registered with the reactor for the `:read` event with the specified +callback+,
236
+ ### then returned.
237
+ def register_monitor( socket, *events, &callback )
238
+ events.push( 'ALL' ) if events.empty?
239
+
240
+ monitor = CZTop::Monitor.new( socket )
241
+ monitor.listen( *events )
242
+ monitor.start
243
+
244
+ self.register( monitor.actor, :read, &callback )
245
+
246
+ return monitor
247
+ end
248
+ alias_method :start_monitor, :register_monitor
249
+
250
+
251
+
252
+ #
253
+ # Polling
254
+ #
255
+
256
+ ### Poll the sockets registered to the reactor for pending events.
257
+ def start_polling
258
+ until self.empty?
259
+ self.log.debug "Polling %d sockets" % [ self.sockets.length ]
260
+
261
+ wait_interval = self.timers.wait_interval || DEFAULT_POLL_INTERVAL
262
+
263
+ # If there's a timer already due to fire, don't wait at all
264
+ event = if wait_interval > 0
265
+ self.log.debug "Waiting for IO for %fms" % [ wait_interval * 1000 ]
266
+ self.wait( wait_interval * 1000 )
267
+ else
268
+ nil
269
+ end
270
+
271
+ self.log.debug "Got event %p" % [ event ]
272
+ if event
273
+ # self.log.debug "Got event: %p" % [ event ]
274
+ handler = self.sockets[ event.socket ][ :handler ]
275
+ handler.call( event ) if handler
276
+ else
277
+ self.log.debug "Expired: firing timers."
278
+ self.timers.fire
279
+ end
280
+
281
+ self.log.debug "%d sockets after polling: %p" % [ self.sockets.length, self.sockets ]
282
+ end
283
+ end
284
+
285
+
286
+ ### Stop polling for events and prepare to shut down.
287
+ def stop_polling
288
+ self.log.debug "Stopping the poll loop."
289
+ self.clear
290
+ self.timers.cancel
291
+ end
292
+
293
+
294
+ ### Return the socket object for the given +pointer+ (an FFI::Pointer), or +nil+ if the
295
+ ### pointer is unknown.
296
+ def socket_for_ptr( pointer )
297
+ return @socket_pointers[ pointer.to_i ]
298
+ end
299
+
300
+
301
+ #########
302
+ protected
303
+ #########
304
+
305
+ ### Waits for events on registered sockets. Returns the first such event, or +nil+ if
306
+ ### no events arrived within the specified +timeout+. If +timeout+ is -1, wait
307
+ ### indefinitely.
308
+ def wait( timeout=-1 )
309
+ rc = CZTop::Poller::ZMQ.poller_wait( @poller_ptr, @event_ptr, timeout )
310
+ if rc == -1
311
+ if CZMQ::FFI::Errors.errno != Errno::ETIMEDOUT::Errno
312
+ CZTop::HasFFIDelegate.raise_zmq_err
313
+ end
314
+ return nil
315
+ end
316
+ return Event.new(self, @event_ptr)
317
+ end
318
+
319
+
320
+ ### Modify the underlying poller's event mask with the events +socket+ is
321
+ ### interested in.
322
+ def update_poller_for( socket )
323
+ event_mask = self.mask_for( socket )
324
+
325
+ ptr = self.ptr_for_socket( socket )
326
+ rc = CZTop::Poller::ZMQ.poller_modify( @poller_ptr, ptr, event_mask )
327
+ CZTop::HasFFIDelegate.raise_zmq_err if rc == -1
328
+ end
329
+
330
+
331
+ ### Return the ZMQ bitmask for the events the specified +socket+ is registered
332
+ ### for.
333
+ def mask_for( socket )
334
+ return self.sockets[ socket ][ :events ].inject( 0 ) do |mask, evt|
335
+ mask | VALID_EVENTS[ evt ]
336
+ end
337
+ end
338
+
339
+
340
+ ### Return the low-level handle for +socket+. Raises an ArgumentError if argument is
341
+ ### not a CZTop::Socket or a CZTop::Actor.
342
+ def ptr_for_socket( socket )
343
+ unless socket.is_a?( CZTop::Socket ) || socket.is_a?( CZTop::Actor )
344
+ raise ArgumentError, "expected a CZTop::Socket or a CZTop::Actor, got %p" % [ socket ]
345
+ end
346
+ return CZMQ::FFI::Zsock.resolve( socket )
347
+ end
348
+
349
+ end # class CZTop::Reactor
350
+
@@ -0,0 +1,68 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'cztop'
5
+ require 'cztop/poller'
6
+ require 'cztop/reactor' unless defined?( CZTop::Reactor )
7
+
8
+
9
+
10
+ # Represents an event returned by {CZTop::Poller#wait}.
11
+ class CZTop::Reactor::Event
12
+
13
+ # Poll events in bitwise order
14
+ POLL_EVENTS = [
15
+ :read,
16
+ :write,
17
+ :err
18
+ ]
19
+
20
+
21
+ ### Create a new event from the specified +reactor+ and +event_ptr+.
22
+ def initialize( reactor, event_ptr )
23
+ @reactor = reactor
24
+ @poller_event = CZTop::Poller::ZMQ::PollerEvent.new( event_ptr )
25
+ end
26
+
27
+
28
+ ##
29
+ # The CZTop::Reactor that generated this event
30
+ attr_reader :reactor
31
+
32
+
33
+ ### Get the Socket or Actor the event corresponds to.
34
+ def socket
35
+ return @socket ||= self.reactor.socket_for_ptr( @poller_event[:socket] )
36
+ end
37
+
38
+
39
+ ### Returns +true+ if the event indicates the socket is readable.
40
+ def readable?
41
+ return @poller_event.readable?
42
+ end
43
+
44
+
45
+ ### Returns +true+ if the event indicates the socket is writable.
46
+ def writable?
47
+ return @poller_event.writable?
48
+ end
49
+
50
+
51
+ ### Return the poll events this event represents.
52
+ def poll_events
53
+ return POLL_EVENTS.select.with_index {|ev,i| @poller_event[:events][i].nonzero? }
54
+ end
55
+
56
+
57
+ ### Return a human-readable string representation of the event suitable for
58
+ ### debugging.
59
+ def inspect
60
+ return "#<%p:%#016x %p {%s}>" % [
61
+ self.class,
62
+ self.object_id * 2,
63
+ self.socket,
64
+ self.poll_events.map( &:to_s ).join( '/' )
65
+ ]
66
+ end
67
+
68
+ end # class CZTop::Reactor::Event
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'cztop/reactor/event'
6
+
7
+
8
+ describe CZTop::Reactor::Event do
9
+
10
+ DummyPollerEvent = Struct.new( :DummyPollerEvent, :readable?, :writable?, :socket, :events )
11
+
12
+
13
+ before( :each ) do
14
+ allow( CZTop::Poller::ZMQ::PollerEvent ).to receive( :new ).and_return( poller_event )
15
+ end
16
+
17
+
18
+ let( :reactor ) { CZTop::Reactor.new }
19
+ let( :poller_event ) do
20
+ DummyPollerEvent.new( false, false, nil, 0 )
21
+ end
22
+
23
+
24
+ it "looks up the socket via the reactor it was created with" do
25
+ poller_event.socket = 0xDEADBEEF
26
+ event = described_class.new( reactor, 0xFADECAFE )
27
+
28
+ expect( reactor ).to receive( :socket_for_ptr ).with( 0xDEADBEEF ).and_return( :the_socket )
29
+ expect( event.socket ).to eq( :the_socket )
30
+ end
31
+
32
+
33
+ it "knows the poll event(s) that caused it" do
34
+ poller_event.events = CZTop::Poller::ZMQ::POLLIN|CZTop::Poller::ZMQ::POLLERR
35
+ event = described_class.new( reactor, 0xFADECAFE )
36
+
37
+ expect( event.poll_events ).to contain_exactly( :read, :err )
38
+ end
39
+
40
+ end
41
+
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env rspec -cfd
2
+ #encoding: utf-8
3
+
4
+ require_relative '../spec_helper'
5
+
6
+ require 'rspec'
7
+ require 'cztop/reactor'
8
+
9
+ describe CZTop::Reactor do
10
+
11
+ let( :reactor ) { described_class.new }
12
+
13
+
14
+ describe "socket registration" do
15
+
16
+ let( :socket ) { CZTop::Socket::REP.new }
17
+
18
+
19
+ it "allows a socket to be registered for reads" do
20
+ expect( reactor ).to_not be_registered( socket )
21
+ expect( reactor ).to_not have_event_enabled( socket, :read )
22
+ expect( reactor ).to_not have_event_enabled( socket, :write )
23
+
24
+ reactor.register( socket, :read ) {}
25
+
26
+ expect( reactor ).to be_registered( socket )
27
+ expect( reactor ).to have_event_enabled( socket, :read )
28
+ expect( reactor ).to_not have_event_enabled( socket, :write )
29
+ end
30
+
31
+
32
+ it "allows a socket to be registered for reads and writes" do
33
+ expect( reactor ).to_not be_registered( socket )
34
+ expect( reactor ).to_not have_event_enabled( socket, :read )
35
+ expect( reactor ).to_not have_event_enabled( socket, :write )
36
+
37
+ reactor.register( socket, :read, :write ) {}
38
+
39
+ expect( reactor ).to be_registered( socket )
40
+ expect( reactor ).to have_event_enabled( socket, :read )
41
+ expect( reactor ).to have_event_enabled( socket, :write )
42
+ end
43
+
44
+
45
+ it "errors if no block is given when registering a socket" do
46
+ expect {
47
+ reactor.register( socket, :read )
48
+ }.to raise_error( LocalJumpError, /no handler/i )
49
+ end
50
+
51
+
52
+ it "allows a registered socket to be unregistered" do
53
+ reactor.register( socket, :read, :write ) {}
54
+ reactor.unregister( socket )
55
+
56
+ expect( reactor ).to_not be_registered( socket )
57
+ end
58
+
59
+
60
+ it "doesn't error when unregistering an unregistered socket" do
61
+ expect {
62
+ reactor.unregister( socket )
63
+ }.to_not raise_error
64
+ end
65
+
66
+
67
+ it "allows a socket to have one or more events enabled for it after registration" do
68
+ reactor.register( socket ) {}
69
+ expect( reactor ).to_not have_event_enabled( socket, :read )
70
+ expect( reactor ).to_not have_event_enabled( socket, :write )
71
+
72
+ reactor.enable_events( socket, :read )
73
+ expect( reactor ).to have_event_enabled( socket, :read )
74
+ expect( reactor ).to_not have_event_enabled( socket, :write )
75
+
76
+ reactor.enable_events( socket, :write )
77
+ expect( reactor ).to have_event_enabled( socket, :read )
78
+ expect( reactor ).to have_event_enabled( socket, :write )
79
+ end
80
+
81
+
82
+ it "allows a socket to have one or more events disabled for it after registration" do
83
+ reactor.register( socket, :read, :write ) {}
84
+ expect( reactor ).to have_event_enabled( socket, :read )
85
+ expect( reactor ).to have_event_enabled( socket, :write )
86
+
87
+ reactor.disable_events( socket, :read )
88
+ expect( reactor ).to_not have_event_enabled( socket, :read )
89
+ expect( reactor ).to have_event_enabled( socket, :write )
90
+
91
+ reactor.disable_events( socket, :write )
92
+ expect( reactor ).to_not have_event_enabled( socket, :read )
93
+ expect( reactor ).to_not have_event_enabled( socket, :write )
94
+ end
95
+
96
+
97
+ it "can unregister all of its registered sockets" do
98
+ reactor.register( socket ) {}
99
+ reactor.register( CZTop::Socket::SUB.new ) {}
100
+ reactor.register( CZTop::Socket::REQ.new ) {}
101
+
102
+ reactor.clear
103
+
104
+ expect( reactor.sockets ).to be_empty
105
+ end
106
+
107
+ end
108
+
109
+
110
+ describe "timer registration" do
111
+
112
+ it "allows a callback to be called after a certain amount of time" do
113
+ handle = reactor.add_oneshot_timer( 5 ) {}
114
+ expect( reactor.timers ).to include( handle )
115
+ end
116
+
117
+
118
+ it "allows a callback to be called periodically on an interval" do
119
+ handle = reactor.add_periodic_timer( 5 ) {}
120
+ expect( reactor.timers ).to include( handle )
121
+ end
122
+
123
+
124
+ it "allows a timer to be cancelled" do
125
+ handle = reactor.add_periodic_timer( 5 ) {}
126
+ reactor.remove_timer( handle )
127
+ expect( reactor.timers ).to_not include( handle )
128
+ end
129
+
130
+ end
131
+
132
+
133
+ describe "monitors" do
134
+
135
+ it "can create and register a monitor for a socket" do
136
+ socket = CZTop::Socket::REP.new
137
+ begin
138
+ monitor = reactor.register_monitor( socket ) {}
139
+ expect( monitor ).to be_a( CZTop::Monitor )
140
+ expect( reactor ).to be_registered( monitor.actor )
141
+ ensure
142
+ monitor.terminate if monitor
143
+ end
144
+ end
145
+
146
+ end
147
+
148
+
149
+ describe "polling loop" do
150
+
151
+ it "processes events until it has no more sockets" do
152
+ reader = CZTop::Socket::PAIR.new( '@inproc://polling-test' )
153
+ writer = CZTop::Socket::PAIR.new( '>inproc://polling-test' )
154
+
155
+ data = nil
156
+
157
+ reactor.register( writer, :write ) do |ev|
158
+ ev.socket << "stuff"
159
+ reactor.unregister( writer )
160
+ end
161
+ reactor.register( reader, :read ) do |ev|
162
+ msg = ev.socket.receive
163
+ data = msg.frames.first.content
164
+ reactor.stop_polling
165
+ end
166
+
167
+ thr = Thread.new do
168
+ Thread.current.abort_on_exception = true
169
+ reactor.start_polling
170
+ end
171
+
172
+ thr.join( 2 )
173
+ thr.kill if thr.alive?
174
+
175
+ expect( data ).to eq( "stuff" )
176
+ end
177
+
178
+
179
+ end
180
+
181
+
182
+ end
183
+
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'simplecov' if ENV['COVERAGE']
5
+
6
+ require 'rspec'
7
+
8
+ require 'loggability/spechelpers'
9
+
10
+
11
+ ### Mock with RSpec
12
+ RSpec.configure do |config|
13
+ config.run_all_when_everything_filtered = true
14
+ config.filter_run :focus
15
+ config.order = 'random'
16
+ config.mock_with( :rspec ) do |mock|
17
+ mock.syntax = :expect
18
+ end
19
+
20
+ config.include( Loggability::SpecHelpers )
21
+ end
22
+
23
+
metadata ADDED
@@ -0,0 +1,250 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cztop-reactor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre20170316155217
5
+ platform: ruby
6
+ authors:
7
+ - Michael Granger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIEbDCCAtSgAwIBAgIBATANBgkqhkiG9w0BAQsFADA+MQwwCgYDVQQDDANnZWQx
14
+ GTAXBgoJkiaJk/IsZAEZFglGYWVyaWVNVUQxEzARBgoJkiaJk/IsZAEZFgNvcmcw
15
+ HhcNMTYwODIwMTgxNzQyWhcNMTcwODIwMTgxNzQyWjA+MQwwCgYDVQQDDANnZWQx
16
+ GTAXBgoJkiaJk/IsZAEZFglGYWVyaWVNVUQxEzARBgoJkiaJk/IsZAEZFgNvcmcw
17
+ ggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQC/JWGRHO+USzR97vXjkFgt
18
+ 83qeNf2KHkcvrRTSnR64i6um/ziin0I0oX23H7VYrDJC9A/uoUa5nGRJS5Zw/+wW
19
+ ENcvWVZS4iUzi4dsYJGY6yEOsXh2CcF46+QevV8iE+UmbkU75V7Dy1JCaUOyizEt
20
+ TH5UHsOtUU7k9TYARt/TgYZKuaoAMZZd5qyVqhF1vV+7/Qzmp89NGflXf2xYP26a
21
+ 4MAX2qqKX/FKXqmFO+AGsbwYTEds1mksBF3fGsFgsQWxftG8GfZQ9+Cyu2+l1eOw
22
+ cZ+lPcg834G9DrqW2zhqUoLr1MTly4pqxYGb7XoDhoR7dd1kFE2a067+DzWC/ADt
23
+ +QkcqWUm5oh1fN0eqr7NsZlVJDulFgdiiYPQiIN7UNsii4Wc9aZqBoGcYfBeQNPZ
24
+ soo/6za/bWajOKUmDhpqvaiRv9EDpVLzuj53uDoukMMwxCMfgb04+ckQ0t2G7wqc
25
+ /D+K9JW9DDs3Yjgv9k4h7YMhW5gftosd+NkNC/+Y2CkCAwEAAaN1MHMwCQYDVR0T
26
+ BAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFHKN/nkRusdqCJEuq3lgB3fJvyTg
27
+ MBwGA1UdEQQVMBOBEWdlZEBGYWVyaWVNVUQub3JnMBwGA1UdEgQVMBOBEWdlZEBG
28
+ YWVyaWVNVUQub3JnMA0GCSqGSIb3DQEBCwUAA4IBgQAPJzKiT0zBU7kpqe0aS2qb
29
+ FI0PJ4y5I8buU4IZGUD5NEt/N7pZNfOyBxkrZkXhS44Fp+xwBH5ebLbq/WY78Bqd
30
+ db0z6ZgW4LMYMpWFfbXsRbd9TU2f52L8oMAhxOvF7Of5qJMVWuFQ8FPagk2iHrdH
31
+ inYLQagqAF6goWTXgAJCdPd6SNeeSNqA6vlY7CV1Jh5kfNJJ6xu/CVij1GzCLu/5
32
+ DMOr26DBv+qLJRRC/2h34uX71q5QgeOyxvMg+7V3u/Q06DXyQ2VgeeqiwDFFpEH0
33
+ PFkdPO6ZqbTRcLfNH7mFgCBJjsfSjJrn0sPBlYyOXgCoByfZnZyrIMH/UY+lgQqS
34
+ 6Von1VDsfQm0eJh5zYZD64ZF86phSR7mUX3mXItwH04HrZwkWpvgd871DZVR3i1n
35
+ w8aNA5re5+Rt/Vvjxj5AcEnZnZiz5x959NaddQocX32Z1unHw44pzRNUur1GInfW
36
+ p4vpx2kUSFSAGjtCbDGTNV2AH8w9OU4xEmNz8c5lyoA=
37
+ -----END CERTIFICATE-----
38
+ date: 2017-03-16 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: loggability
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.14'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.14'
54
+ - !ruby/object:Gem::Dependency
55
+ name: cztop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.11'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.11'
68
+ - !ruby/object:Gem::Dependency
69
+ name: timers
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '4.1'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '4.1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: hoe-mercurial
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.4'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.4'
96
+ - !ruby/object:Gem::Dependency
97
+ name: hoe-deveiate
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.8'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: hoe-highline
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.2'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.2'
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.13'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.13'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rdoc-generator-fivefish
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.3'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '0.3'
152
+ - !ruby/object:Gem::Dependency
153
+ name: rdoc
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '4.0'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '4.0'
166
+ - !ruby/object:Gem::Dependency
167
+ name: hoe
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '3.15'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '3.15'
180
+ description: |-
181
+ This is an implementation of the Reactor pattern described in [Pattern-Oriented
182
+ Software Architecture (Volume 2)][POSA2]. It allows an asynchronous application
183
+ to be described as one or more "reactions" to events, in this case either I/O
184
+ conditions on a ZMQ socket or a timer expiring.
185
+
186
+ A simple example:
187
+
188
+ # Start a SERVER socket, and print out any messages sent to it
189
+ reactor = CZTop::Reactor.new
190
+ socket = CZTop::Socket::SERVER.new
191
+ socket.bind( 'tcp://0.0.0.0:8' )
192
+ reactor.register( socket, :read ) do |event|
193
+ if event.readable?
194
+ message = event.socket.receive
195
+ puts "Read: %p" % [ message.to_a ]
196
+ end
197
+ end
198
+ reactor.start_polling
199
+ email:
200
+ - ged@FaerieMUD.org
201
+ executables: []
202
+ extensions: []
203
+ extra_rdoc_files:
204
+ - History.md
205
+ - LICENSE.txt
206
+ - Manifest.txt
207
+ - README.md
208
+ files:
209
+ - ".document"
210
+ - ".rdoc_options"
211
+ - ".simplecov"
212
+ - ChangeLog
213
+ - History.md
214
+ - LICENSE.txt
215
+ - Manifest.txt
216
+ - README.md
217
+ - Rakefile
218
+ - lib/cztop/reactor.rb
219
+ - lib/cztop/reactor/event.rb
220
+ - spec/cztop/reactor/event_spec.rb
221
+ - spec/cztop/reactor_spec.rb
222
+ - spec/spec_helper.rb
223
+ homepage: http://deveiate.org/projects/cztop-reactor
224
+ licenses:
225
+ - ISC
226
+ metadata: {}
227
+ post_install_message:
228
+ rdoc_options:
229
+ - "--main"
230
+ - README.md
231
+ require_paths:
232
+ - lib
233
+ required_ruby_version: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - ">="
236
+ - !ruby/object:Gem::Version
237
+ version: 2.2.4
238
+ required_rubygems_version: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">"
241
+ - !ruby/object:Gem::Version
242
+ version: 1.3.1
243
+ requirements: []
244
+ rubyforge_project:
245
+ rubygems_version: 2.6.8
246
+ signing_key:
247
+ specification_version: 4
248
+ summary: This is an implementation of the Reactor pattern described in [Pattern-Oriented
249
+ Software Architecture (Volume 2)][POSA2]
250
+ test_files: []