wires 0.6.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed1477b941ed3c64b10f1d6d7f0087fc17f07aad
4
- data.tar.gz: db91d0d10df1770328f942a8f4febe4be4c90502
3
+ metadata.gz: f6ef896589b7b29bffcde2800b2a0205c993bf70
4
+ data.tar.gz: 849f9147f51646922d9148fdae2d592dc3998fc2
5
5
  SHA512:
6
- metadata.gz: d179bf2b1d6d0642ef84483c9f8521f49f8628bd676dd11a87da1d9108da152827ed657887e43dd6458ddaa10eb7b027941644578243844c8370249584dfb20c
7
- data.tar.gz: f88af8249cceebf4774de1ba33e60bb9f203d5d0da6f13fdadb128ea0fc244cbb35bb717e50c0f573b3feea95dcf73418897bc8122236e612fa85ce0d20ccda5
6
+ metadata.gz: a27d75fe9e4020189e732e04e93cc6e22493390b7b28ebe616d31420a97d18e1a3c56a43b940a2030f75dfba27e396ea0762965dad59f47496bc0f2d8c40b6d9
7
+ data.tar.gz: ba4092b89082a0fd4f6da1af2aa7664eca682a6017e6cb5ebaa00b7d8e8804305b6f1f26107fb2eba318807456a08353f09ca5c34b7d5feef88ed316b022adec
data/lib/wires/base.rb CHANGED
@@ -48,9 +48,11 @@ loader.call 'base/util/hooks'
48
48
  loader.call 'base/util/router_table'
49
49
 
50
50
  loader.call 'base/event'
51
+ loader.call 'base/future'
51
52
  loader.call 'base/launcher'
52
53
  loader.call 'base/router'
53
54
  loader.call 'base/channel'
55
+ loader.call 'base/channel/sync_helper'
54
56
  loader.call 'base/time_scheduler_item'
55
57
  loader.call 'base/time_scheduler'
56
58
  loader.call 'base/convenience'
@@ -263,6 +263,10 @@ module Wires.current_network::Namespace
263
263
  end
264
264
  end
265
265
 
266
+ procs.map! do |pr|
267
+ Future.new &pr
268
+ end
269
+
266
270
  # Fire to selected targets
267
271
  threads = procs.uniq.map do |pr|
268
272
  Launcher.spawn \
@@ -330,103 +334,6 @@ module Wires.current_network::Namespace
330
334
  nil
331
335
  end
332
336
 
333
- # Helper class passed to user block in {Channel#sync_on} method.
334
- # Read here for how to use the helper, but never instantiate it yourself.
335
- class SyncHelper
336
-
337
- # Don't instantiate this class directly, use {Channel#sync_on}
338
- # @api private
339
- def initialize(events, channel, timeout:nil)
340
- @timeout = timeout
341
- @lock, @cond = Mutex.new, ConditionVariable.new
342
- @conditions = []
343
- @executions = []
344
- @received = []
345
- @thread = Thread.current
346
-
347
- # Create the temporary event handler to capture incoming matches
348
- proc = Proc.new do |e,c|
349
- if Thread.current==@thread
350
- snag e,c
351
- else
352
- @lock.synchronize { snag e,c }
353
- end
354
- end
355
-
356
- # Run the user block within the lock and wait afterward if they didn't
357
- @lock.synchronize {
358
- channel.register events, &proc
359
- yield self
360
- wait unless @waited
361
- channel.unregister &proc
362
- }
363
- end
364
-
365
- # Add a condition which must be fulfilled for {#wait} to find a match.
366
- #
367
- # @param block [Proc] the block specifiying the condition to be met.
368
- # It will be passed the event and channel, and the truthiness of its
369
- # return value will be evaluated to determine if the condition is met.
370
- # It will only be executed if the +[event,channel]+ pair fits the
371
- # filter and meets all of the other evaluated conditions so far.
372
- #
373
- def condition(&block)
374
- @conditions << block if block
375
- nil
376
- end
377
-
378
- # Add a execution to run on the matching event for each {#wait}.
379
- #
380
- # @param block [Proc] the block to be executed.
381
- # It will only be executed if the +[event,channel]+ pair fits the
382
- # filter and met all of the conditions to fulfill the {#wait}.
383
- # The block will not be run if the {#wait} times out.
384
- #
385
- def execute(&block)
386
- @executions << block if block
387
- nil
388
- end
389
-
390
- # Wait for exactly one matching event meeting all {#condition}s to come.
391
- #
392
- # @note This will be called once implicitly at the end of the user block
393
- # unless it gets called explicitly somewhere within the user block.
394
- # It can be called multiple times within the user block to require
395
- # one matching event each time within the block.
396
- #
397
- # @param timeout [Fixnum] The maximum time to wait for a match,
398
- # specified in seconds. By default, it will be the number used at
399
- # instantiation (passed from {Channel#sync_on}).
400
- #
401
- # @return the matching {Event} object, or nil if timed out.
402
- #
403
- def wait(timeout=@timeout)
404
- @waited = true
405
- result = nil
406
-
407
- # Loop through each result, making sure it matches the conditions,
408
- # returning nil if the wait timed out and didn't push into @received
409
- loop do
410
- @cond.wait @lock, timeout if @received.empty?
411
- result = @received.pop
412
- return nil unless result
413
- break if !@conditions.detect { |blk| !blk.call *result }
414
- end
415
-
416
- # Run all the execute blocks on the result
417
- @executions.each { |blk| blk.call *result }
418
- result.first #=> return event
419
- end
420
-
421
- private
422
-
423
- # Snag the given event and channel to try it out in the blocking thread
424
- def snag(*args)
425
- @received << args
426
- @cond.signal # Pass execution back to blocking thread and block this one
427
- end
428
- end
429
-
430
337
  # Determine if one channel matches another.
431
338
  #
432
339
  # In this context, a match indicates a receiver relationship.
@@ -0,0 +1,104 @@
1
+
2
+ module Wires.current_network::Namespace
3
+
4
+ class Channel
5
+
6
+ # Helper class passed to user block in {Channel#sync_on} method.
7
+ # Read here for how to use the helper, but never instantiate it yourself.
8
+ class SyncHelper
9
+
10
+ # Don't instantiate this class directly, use {Channel#sync_on}
11
+ # @api private
12
+ def initialize(events, channel, timeout:nil)
13
+ @timeout = timeout
14
+ @lock, @cond = Mutex.new, ConditionVariable.new
15
+ @conditions = []
16
+ @executions = []
17
+ @received = []
18
+ @thread = Thread.current
19
+
20
+ # Create the temporary event handler to capture incoming matches
21
+ proc = Proc.new do |e,c|
22
+ if Thread.current==@thread
23
+ snag e,c
24
+ else
25
+ @lock.synchronize { snag e,c }
26
+ end
27
+ end
28
+
29
+ # Run the user block within the lock and wait afterward if they didn't
30
+ @lock.synchronize {
31
+ channel.register events, &proc
32
+ yield self
33
+ wait unless @waited
34
+ channel.unregister &proc
35
+ }
36
+ end
37
+
38
+ # Add a condition which must be fulfilled for {#wait} to find a match.
39
+ #
40
+ # @param block [Proc] the block specifiying the condition to be met.
41
+ # It will be passed the event and channel, and the truthiness of its
42
+ # return value will be evaluated to determine if the condition is met.
43
+ # It will only be executed if the +[event,channel]+ pair fits the
44
+ # filter and meets all of the other evaluated conditions so far.
45
+ #
46
+ def condition(&block)
47
+ @conditions << block if block
48
+ nil
49
+ end
50
+
51
+ # Add a execution to run on the matching event for each {#wait}.
52
+ #
53
+ # @param block [Proc] the block to be executed.
54
+ # It will only be executed if the +[event,channel]+ pair fits the
55
+ # filter and met all of the conditions to fulfill the {#wait}.
56
+ # The block will not be run if the {#wait} times out.
57
+ #
58
+ def execute(&block)
59
+ @executions << block if block
60
+ nil
61
+ end
62
+
63
+ # Wait for exactly one matching event meeting all {#condition}s to come.
64
+ #
65
+ # @note This will be called once implicitly at the end of the user block
66
+ # unless it gets called explicitly somewhere within the user block.
67
+ # It can be called multiple times within the user block to require
68
+ # one matching event each time within the block.
69
+ #
70
+ # @param timeout [Fixnum] The maximum time to wait for a match,
71
+ # specified in seconds. By default, it will be the number used at
72
+ # instantiation (passed from {Channel#sync_on}).
73
+ #
74
+ # @return the matching {Event} object, or nil if timed out.
75
+ #
76
+ def wait(timeout=@timeout)
77
+ @waited = true
78
+ result = nil
79
+
80
+ # Loop through each result, making sure it matches the conditions,
81
+ # returning nil if the wait timed out and didn't push into @received
82
+ loop do
83
+ @cond.wait @lock, timeout if @received.empty?
84
+ result = @received.pop
85
+ return nil unless result
86
+ break if !@conditions.detect { |blk| !blk.call *result }
87
+ end
88
+
89
+ # Run all the execute blocks on the result
90
+ @executions.each { |blk| blk.call *result }
91
+ result.first #=> return event
92
+ end
93
+
94
+ private
95
+
96
+ # Snag the given event and channel to try it out in the blocking thread
97
+ def snag(*args)
98
+ @received << args
99
+ @cond.signal # Pass execution back to blocking thread and block this one
100
+ end
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,133 @@
1
+
2
+ module Wires.current_network::Namespace
3
+
4
+ # In this context, a {Future} is thread-safe container for a {#codeblock}
5
+ # and for the {#result} produced when it is {#execute}d.
6
+ #
7
+ # Call {#execute} to run the block in place, or {#start} to {#execute} in
8
+ # a new thread. After {#execute} is done, the {#result} will be available.
9
+ # Calling {#result} will block if {#execute} has not yet finished.
10
+ # The block is guaranteed to run at most one time, so any subsequent
11
+ # calls to {#execute} will merely return the existing {#result}.
12
+ # If running the block again is desired, use {#dup} to create a new {Future}
13
+ # with the same {#codeblock}.
14
+ #
15
+ # future = Future.new { |*args| expensive_operation *args }
16
+ # future.start 1,2,3 # Run expensive_operation in a thread with args=1,2,3
17
+ # # ...
18
+ # puts future.result # Block until expensive_operation is done and print result
19
+ #
20
+ # @note Primarily, {Future} is included in this library for the return value
21
+ # of {Launcher.spawn} (and, by extension, {Channel#fire}), but users should
22
+ # feel free to use it's documented API for other purposes as well.
23
+ #
24
+ class Future
25
+
26
+ # The block passed at {#initialize} to be run when {#execute} is called.
27
+ # It will be run at most one time.
28
+ #
29
+ attr_reader :codeblock
30
+
31
+ # @param codeblock [Proc] The code block to run when {#execute} is called.
32
+ # @raise [ArgumentError] If +codeblock+ is not given.
33
+ #
34
+ def initialize &codeblock
35
+ raise ArgumentError, "Future must be instantiated with a block" \
36
+ unless codeblock
37
+
38
+ @codeblock = codeblock
39
+ @state = :initial
40
+ @result = nil
41
+ @statelock = Mutex.new
42
+ @cond = ConditionVariable.new
43
+ end
44
+
45
+ # Run {#execute} in a new +Thread+. If {#execute} raises an +Exception+,
46
+ # it will be rescued at the top level of the +Thread+, but will be raised
47
+ # when {#result} is called.
48
+ #
49
+ # @param args The arguments to pass to {#codeblock} in {#execute}.
50
+ # @param block The block argument to pass to {#codeblock} in {#execute}.
51
+ # @return [Thread] The spawned +Thread+.
52
+ #
53
+ def start *args, &block
54
+ Thread.new do
55
+ begin
56
+ execute *args, &block
57
+ rescue Exception
58
+ # Exceptions get raised through when #result is called.
59
+ end
60
+ end
61
+ end
62
+
63
+ # Run the {#codeblock} passed to {#initialize} with the given arguments.
64
+ #
65
+ # If the {#codeblock} has already been {#execute}d, it won't be run again;
66
+ # instead, the result returned by the first call will be returned again.
67
+ #
68
+ # @param args The arguments to pass to {#codeblock}.
69
+ # @param block The block argument to pass to {#codeblock}.
70
+ # @return The return value of the call to {#codeblock}.
71
+ # @raise The exception raised by the call to {#codeblock}, if any.
72
+ #
73
+ def execute *args, &block
74
+ @statelock.synchronize do
75
+ return @result if @state == :complete
76
+ @state = :running
77
+
78
+ begin
79
+ @codeblock.call(*args, &block).tap do |result|
80
+ @result = result
81
+ @state = :complete
82
+ end
83
+ rescue Exception => exception
84
+ @result = exception
85
+ @state = :exception
86
+ raise exception
87
+ ensure
88
+ @cond.broadcast
89
+ end
90
+ end
91
+ end
92
+ alias call execute
93
+
94
+ # Get the return value of the call to {#codeblock}.
95
+ #
96
+ # If {#execute} has not yet been called, or if it is still {#running?},
97
+ # {#result} will block until the {#codeblock} has been run.
98
+ #
99
+ # @return The return value of the call to {#codeblock}.
100
+ # @raise The exception raised by the call to {#codeblock}, if any.
101
+ #
102
+ def result
103
+ @statelock.synchronize do
104
+ @cond.wait @statelock unless complete?
105
+ @state==:exception ? raise(@result) : @result
106
+ end
107
+ end
108
+ alias join result
109
+
110
+ # @return [Boolean]
111
+ # +true+ if {#codeblock} is currently executing, else +false+.
112
+ def running?; @state == :running; end
113
+ alias executing? running?
114
+
115
+ # @return [Boolean]
116
+ # +true+ if {#codeblock} has already executed, else +false+.
117
+ def complete?; @state == :complete || @state == :exception; end
118
+ alias ready? complete?
119
+
120
+ # Duplicate the Future, without copying its {#running?}/{#complete?} state.
121
+ # This is the preferred way to "reuse" a {Future}, because each {Future}
122
+ # is guaranteed to run at most one time. It is the same as creating a
123
+ # new {Future} with the same {#codeblock} object passed to {#initialize}.
124
+ #
125
+ # @return [Future] A new {Future} with the same {#codeblock}.
126
+ #
127
+ def dup
128
+ self.class.new &@codeblock
129
+ end
130
+
131
+ end
132
+
133
+ end
@@ -18,7 +18,6 @@ module Wires.current_network::Namespace
18
18
 
19
19
  # Moved to a dedicated method for subclass' sake
20
20
  def class_init
21
- # @queue = Queue.new
22
21
  @max_children = nil
23
22
  @children = Array.new
24
23
  @children .extend MonitorMixin
@@ -45,10 +44,10 @@ module Wires.current_network::Namespace
45
44
  nil end
46
45
 
47
46
  def reset_neglect_procs
48
- @on_neglect = Proc.new do |args|
47
+ @on_neglect = Proc.new do |*args|
49
48
  $stderr.puts "#{self} neglected to spawn task: #{args.inspect}"
50
49
  end
51
- @on_neglect_done = Proc.new do |args|
50
+ @on_neglect_done = Proc.new do |*args|
52
51
  $stderr.puts "#{self} finally spawned neglected task: #{args.inspect}"
53
52
  end
54
53
  nil end
@@ -64,25 +63,27 @@ module Wires.current_network::Namespace
64
63
  end
65
64
 
66
65
  # Spawn a task - user code should never call this directly
67
- def spawn(*args) # :args: event, chan, proc, blocking, parallel, fire_bt
66
+ def spawn(*args) # :args: event, chan, future, blocking, parallel, fire_bt
68
67
 
69
- event, chan, proc, blocking, parallel, fire_bt = *args
68
+ event, chan, future, blocking, parallel, fire_bt = *args
70
69
  *proc_args = event, chan
71
70
  *exc_args = event, chan, fire_bt
72
71
 
73
- # If not parallel, run the proc in this thread
72
+ # If not parallel, run the future in this thread
74
73
  if !parallel
75
74
  begin
76
- proc.call(*proc_args)
75
+ future.call(*proc_args)
77
76
  rescue Exception => exc
78
77
  unhandled_exception(exc, *exc_args)
79
78
  end
80
79
 
81
- return nil
80
+ return future
82
81
  end
83
82
 
84
- return neglect(*args) \
85
- if @hold_lock.instance_variable_get(:@mon_mutex).locked?
83
+ if @hold_lock.instance_variable_get(:@mon_mutex).locked?
84
+ neglect(*args)
85
+ return future
86
+ end
86
87
 
87
88
  # If not parallel, clear old threads and spawn a new thread
88
89
  Thread.exclusive do
@@ -93,7 +94,7 @@ module Wires.current_network::Namespace
93
94
  # Start the new child thread; follow with chain of neglected tasks
94
95
  @children << Thread.new do
95
96
  begin
96
- proc.call(*proc_args)
97
+ future.call(*proc_args)
97
98
  rescue Exception => exc
98
99
  unhandled_exception(exc, *exc_args)
99
100
  ensure
@@ -103,9 +104,12 @@ module Wires.current_network::Namespace
103
104
  end
104
105
 
105
106
  # Capture ThreadError from either OS or user-set limitation
106
- rescue ThreadError; return neglect(*args) end
107
+ rescue ThreadError
108
+ neglect(*args)
109
+ return future
110
+ end
107
111
 
108
- return @children.last
112
+ return future
109
113
  end
110
114
 
111
115
  end
@@ -142,13 +146,13 @@ module Wires.current_network::Namespace
142
146
  @on_neglect.call(*args)
143
147
  @neglected << args
144
148
  end
145
- false end
149
+ nil end
146
150
 
147
151
  # Run a chain of @neglected tasks in place until no more are waiting
148
152
  def spawn_neglected_task_chain
149
153
  args = @neglected.synchronize do
150
154
  return nil if @neglected.empty?
151
- ((@neglected.shift)[0...-1]<<true) # Call with blocking
155
+ @neglected.shift.tap { |a| a[3] = true } # Call with blocking
152
156
  end
153
157
  spawn(*args)
154
158
  @on_neglect_done.call(*args)
@@ -160,7 +164,7 @@ module Wires.current_network::Namespace
160
164
  until (cease||=false)
161
165
  args = @neglected.synchronize do
162
166
  break if (cease = @neglected.empty?)
163
- ((@neglected.shift)[0...-1]<<false) # Call without blocking
167
+ @neglected.shift.tap { |a| a[3] = false } # Call without blocking
164
168
  end
165
169
  break if cease
166
170
  spawn(*args)
@@ -9,7 +9,7 @@ module Wires.current_network::Namespace
9
9
  def weak?; @weak end
10
10
 
11
11
  def initialize(obj)
12
- raise ValueError "#{self.class} referent cannot be nil" if obj.nil?
12
+ raise ArgumentError, "#{self.class} referent cannot be nil" if obj.nil?
13
13
 
14
14
  # Make initial weak reference (if possible)
15
15
  begin
@@ -64,7 +64,9 @@ module Wires.current_network::Namespace
64
64
 
65
65
  def []=(key, value)
66
66
  begin; ObjectSpace.define_finalizer key, @finalizer
67
- rescue RuntimeError; end
67
+ rescue RuntimeError => e
68
+ raise unless e.message =~ /can't modify frozen/
69
+ end
68
70
 
69
71
  @lock.synchronize do
70
72
  id = key.object_id
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wires
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe McIlvain
@@ -150,6 +150,20 @@ dependencies:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: yard
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  description: A lightweight, extensible asynchronous event routing framework in Ruby.
154
168
  email: joe.eli.mac@gmail.com
155
169
  executables: []
@@ -162,8 +176,10 @@ files:
162
176
  - lib/wires/base.rb
163
177
  - lib/wires/base/actor.rb
164
178
  - lib/wires/base/channel.rb
179
+ - lib/wires/base/channel/sync_helper.rb
165
180
  - lib/wires/base/convenience.rb
166
181
  - lib/wires/base/event.rb
182
+ - lib/wires/base/future.rb
167
183
  - lib/wires/base/launcher.rb
168
184
  - lib/wires/base/router.rb
169
185
  - lib/wires/base/time_scheduler.rb
@@ -194,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
210
  version: '0'
195
211
  requirements: []
196
212
  rubyforge_project:
197
- rubygems_version: 2.2.0
213
+ rubygems_version: 2.2.2
198
214
  signing_key:
199
215
  specification_version: 4
200
216
  summary: wires