cztop-reactor 0.1.0.pre20170316155217

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.
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: []