wires 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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