concurrent-ruby 0.3.0 → 0.3.1.pre.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 +4 -4
- data/README.md +55 -5
- data/lib/concurrent.rb +3 -0
- data/lib/concurrent/actor.rb +90 -36
- data/lib/concurrent/agent.rb +7 -21
- data/lib/concurrent/contract.rb +20 -0
- data/lib/concurrent/dereferenceable.rb +33 -0
- data/lib/concurrent/future.rb +7 -6
- data/lib/concurrent/obligation.rb +4 -3
- data/lib/concurrent/promise.rb +3 -3
- data/lib/concurrent/scheduled_task.rb +94 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/actor.md +209 -0
- data/md/agent.md +30 -11
- data/md/future.md +55 -13
- data/md/scheduled_task.md +34 -0
- data/md/supervisor.md +209 -6
- data/md/timer_task.md +1 -1
- data/spec/concurrent/actor_spec.rb +244 -48
- data/spec/concurrent/agent_spec.rb +52 -1
- data/spec/concurrent/contract_spec.rb +34 -0
- data/spec/concurrent/future_spec.rb +6 -1
- data/spec/concurrent/obligation_shared.rb +1 -1
- data/spec/concurrent/promise_spec.rb +6 -1
- data/spec/concurrent/runnable_shared.rb +1 -1
- data/spec/concurrent/scheduled_task_spec.rb +259 -0
- metadata +14 -5
@@ -1,11 +1,13 @@
|
|
1
1
|
require 'thread'
|
2
2
|
require 'timeout'
|
3
3
|
|
4
|
+
require 'concurrent/dereferenceable'
|
4
5
|
require 'concurrent/event'
|
5
6
|
|
6
7
|
module Concurrent
|
7
8
|
|
8
9
|
module Obligation
|
10
|
+
include Dereferenceable
|
9
11
|
|
10
12
|
attr_reader :state
|
11
13
|
attr_reader :reason
|
@@ -15,7 +17,7 @@ module Concurrent
|
|
15
17
|
def fulfilled?() return(@state == :fulfilled); end
|
16
18
|
alias_method :realized?, :fulfilled?
|
17
19
|
|
18
|
-
# Has the
|
20
|
+
# Has the obligation been rejected?
|
19
21
|
# @return [Boolean]
|
20
22
|
def rejected?() return(@state == :rejected); end
|
21
23
|
|
@@ -25,9 +27,8 @@ module Concurrent
|
|
25
27
|
|
26
28
|
def value(timeout = nil)
|
27
29
|
event.wait(timeout) unless timeout == 0 || @state != :pending
|
28
|
-
|
30
|
+
super()
|
29
31
|
end
|
30
|
-
alias_method :deref, :value
|
31
32
|
|
32
33
|
protected
|
33
34
|
|
data/lib/concurrent/promise.rb
CHANGED
@@ -115,13 +115,13 @@ module Concurrent
|
|
115
115
|
end
|
116
116
|
|
117
117
|
# @private
|
118
|
-
def on_fulfill(
|
118
|
+
def on_fulfill(result) # :nodoc:
|
119
119
|
@lock.synchronize do
|
120
|
-
@value = @handler.call(
|
120
|
+
@value = @handler.call(result)
|
121
121
|
@state = :fulfilled
|
122
122
|
@reason = nil
|
123
123
|
end
|
124
|
-
return
|
124
|
+
return self.value
|
125
125
|
end
|
126
126
|
|
127
127
|
# @private
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
require 'concurrent/obligation'
|
4
|
+
require 'concurrent/runnable'
|
5
|
+
|
6
|
+
module Concurrent
|
7
|
+
|
8
|
+
class ScheduledTask
|
9
|
+
include Obligation
|
10
|
+
include Observable
|
11
|
+
include Runnable
|
12
|
+
|
13
|
+
attr_reader :schedule_time
|
14
|
+
|
15
|
+
def initialize(schedule_time, opts = {}, &block)
|
16
|
+
now = Time.now
|
17
|
+
|
18
|
+
if ! block_given?
|
19
|
+
raise ArgumentError.new('no block given')
|
20
|
+
elsif schedule_time.is_a?(Time)
|
21
|
+
if schedule_time <= now
|
22
|
+
raise ArgumentError.new('schedule time must be in the future')
|
23
|
+
else
|
24
|
+
@schedule_time = schedule_time.dup
|
25
|
+
end
|
26
|
+
elsif schedule_time.to_f <= 0.0
|
27
|
+
raise ArgumentError.new('seconds must be greater than zero')
|
28
|
+
else
|
29
|
+
@schedule_time = now + schedule_time.to_f
|
30
|
+
end
|
31
|
+
|
32
|
+
@state = :pending
|
33
|
+
@task = block
|
34
|
+
@schedule_time.freeze
|
35
|
+
set_deref_options(opts)
|
36
|
+
end
|
37
|
+
|
38
|
+
def cancelled?
|
39
|
+
return @state == :cancelled
|
40
|
+
end
|
41
|
+
|
42
|
+
def in_progress?
|
43
|
+
return @state == :in_progress
|
44
|
+
end
|
45
|
+
|
46
|
+
def cancel
|
47
|
+
return false if mutex.locked?
|
48
|
+
return mutex.synchronize do
|
49
|
+
if @state == :pending
|
50
|
+
@state = :cancelled
|
51
|
+
event.set
|
52
|
+
true
|
53
|
+
else
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_observer(observer, func = :update)
|
60
|
+
return false unless @state == :pending || @state == :in_progress
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def on_task
|
67
|
+
while (diff = @schedule_time.to_f - Time.now.to_f) > 0
|
68
|
+
sleep( diff > 60 ? 60 : diff )
|
69
|
+
end
|
70
|
+
|
71
|
+
if @state == :pending
|
72
|
+
mutex.synchronize do
|
73
|
+
@state = :in_progress
|
74
|
+
begin
|
75
|
+
@value = @task.call
|
76
|
+
@state = :fulfilled
|
77
|
+
rescue => ex
|
78
|
+
@reason = ex
|
79
|
+
@state = :rejected
|
80
|
+
ensure
|
81
|
+
changed
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if self.changed?
|
87
|
+
notify_observers(Time.now, self.value, @reason)
|
88
|
+
delete_observers
|
89
|
+
end
|
90
|
+
event.set
|
91
|
+
self.stop
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/concurrent/version.rb
CHANGED
data/md/actor.md
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# All the world's a stage
|
2
|
+
|
3
|
+
Actor-based concurrency is all the rage in some circles. Originally described in
|
4
|
+
1973, the actor model is a paradigm for creating asynchronous, concurrent objects
|
5
|
+
that is becoming increasingly popular. Much has changed since actors were first
|
6
|
+
written about four decades ago, which has led to a serious fragmentation within
|
7
|
+
the actor community. There is *no* universally accepted, strict definition of
|
8
|
+
"actor" and actor implementations differ widely between languages and libraries.
|
9
|
+
|
10
|
+
A good definition of "actor" is:
|
11
|
+
|
12
|
+
> An independent, concurrent, single-purpose, computational entity that
|
13
|
+
> communicates exclusively via message passing.
|
14
|
+
|
15
|
+
The `Concurrent::Actor` class in this library is based solely on the
|
16
|
+
[Actor](http://www.scala-lang.org/api/current/index.html#scala.actors.Actor) task
|
17
|
+
defined in the Scala standard library. It does not implement all the features of
|
18
|
+
Scala's `Actor` but its behavior for what *has* been implemented is nearly identical
|
19
|
+
|
20
|
+
## Definition
|
21
|
+
|
22
|
+
Actors are defined by subclassing the `Concurrent::Actor` class and overriding the
|
23
|
+
`#act` method. The `#act` method can have any signature/arity but
|
24
|
+
|
25
|
+
> def act(*args, &block)
|
26
|
+
|
27
|
+
is the most flexible and least error-prone signature. The `#act` method is called in
|
28
|
+
response to a message being `#post` to the `Actor` instance (see *Behavior* below).
|
29
|
+
|
30
|
+
## Behavior
|
31
|
+
|
32
|
+
The `Concurrent::Actor` class includes the `Concurrent::Runnable` module. This provides
|
33
|
+
an `Actor` instance with the necessary methods for running and graceful stopping.
|
34
|
+
This also means that an `Actor` can be managed by a `Concurrent::Supervisor` for
|
35
|
+
fault tolerance.
|
36
|
+
|
37
|
+
Messages from any thread can be sent to an `Actor` using either the `#post` method. Calling
|
38
|
+
this method causes all arguments and a block (if given) to be passed to the subclass `#act`
|
39
|
+
method. Messages are processed one at a time in the order received. Each `Actor` subclass
|
40
|
+
must detemine how it will interact with the rest of the system. A common practice is for
|
41
|
+
one `Actor` to send messages to another `Actor` though this is hardly the only approach.
|
42
|
+
|
43
|
+
## Pools
|
44
|
+
|
45
|
+
Every `Actor` instance operates on its own thread. When one thread isn't enough capacity
|
46
|
+
to manage all the messages being sent to an `Actor` a *pool* can be used instead. A pool
|
47
|
+
is a collection of `Actor` instances, all of the same type, that shate a message queue.
|
48
|
+
Messages from other threads are all sent to a single queue against which all `Actor`s
|
49
|
+
load balance.
|
50
|
+
|
51
|
+
## Additional Reading
|
52
|
+
|
53
|
+
* [API documentation](http://www.scala-lang.org/api/current/index.html#scala.actors.Actor)
|
54
|
+
for the original (now deprecated) Scala Actor
|
55
|
+
* [Scala Actors: A Short Tutorial](http://www.scala-lang.org/old/node/242)
|
56
|
+
* [Scala Actors 101](http://java.dzone.com/articles/scala-threadless-concurrent)
|
57
|
+
|
58
|
+
## Examples
|
59
|
+
|
60
|
+
Two `Actor`s playing a back and forth game of Ping Pong, adapted from the Scala example
|
61
|
+
[here](http://www.somewhere.com/find/the/blog/post):
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class Ping < Concurrent::Actor
|
65
|
+
|
66
|
+
def initialize(count, pong)
|
67
|
+
super()
|
68
|
+
@pong = pong
|
69
|
+
@remaining = count
|
70
|
+
end
|
71
|
+
|
72
|
+
def act(msg)
|
73
|
+
|
74
|
+
if msg == :pong
|
75
|
+
print "Ping: pong\n" if @remaining % 1000 == 0
|
76
|
+
@pong.post(:ping)
|
77
|
+
|
78
|
+
if @remaining > 0
|
79
|
+
@pong << :ping
|
80
|
+
@remaining -= 1
|
81
|
+
else
|
82
|
+
print "Ping :stop\n"
|
83
|
+
@pong << :stop
|
84
|
+
self.stop
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Pong < Concurrent::Actor
|
91
|
+
|
92
|
+
attr_writer :ping
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
super()
|
96
|
+
@count = 0
|
97
|
+
end
|
98
|
+
|
99
|
+
def act(msg)
|
100
|
+
|
101
|
+
if msg == :ping
|
102
|
+
print "Pong: ping\n" if @count % 1000 == 0
|
103
|
+
@ping << :pong
|
104
|
+
@count += 1
|
105
|
+
|
106
|
+
elsif msg == :stop
|
107
|
+
print "Pong :stop\n"
|
108
|
+
self.stop
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
pong = Pong.new
|
114
|
+
ping = Ping.new(10000, pong)
|
115
|
+
pong.ping = ping
|
116
|
+
|
117
|
+
t1 = ping.run!
|
118
|
+
t2 = pong.run!
|
119
|
+
sleep(0.1)
|
120
|
+
|
121
|
+
ping << :pong
|
122
|
+
```
|
123
|
+
|
124
|
+
A pool of `Actor`s and a `Supervisor`
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
QUERIES = %w[YAHOO Microsoft google]
|
128
|
+
|
129
|
+
class FinanceActor < Concurrent::Actor
|
130
|
+
def act(query)
|
131
|
+
finance = Finance.new(query)
|
132
|
+
print "[#{Time.now}] RECEIVED '#{query}' to #{self} returned #{finance.update.suggested_symbols}\n\n"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
financial, pool = FinanceActor.pool(5)
|
137
|
+
|
138
|
+
overlord = Concurrent::Supervisor.new
|
139
|
+
pool.each{|actor| overlord.add_worker(actor)}
|
140
|
+
|
141
|
+
overlord.run!
|
142
|
+
|
143
|
+
pool.post('YAHOO')
|
144
|
+
|
145
|
+
#>> [2013-10-18 09:35:28 -0400] SENT 'YAHOO' from main to worker pool
|
146
|
+
#>> [2013-10-18 09:35:28 -0400] RECEIVED 'YAHOO' to #<FinanceActor:0x0000010331af70>...
|
147
|
+
```
|
148
|
+
|
149
|
+
The `#post!` method returns an `Obligation` (same API as `Future`) which can be queried
|
150
|
+
for value/reason on fulfillment/rejection.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
class EverythingActor < Concurrent::Actor
|
154
|
+
def act(message)
|
155
|
+
sleep(1)
|
156
|
+
return 42
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
life = EverythingActor.new
|
161
|
+
life.run!
|
162
|
+
universe = life.post!('What do you get when you multiply six by nine?')
|
163
|
+
universe.pending? #=> true
|
164
|
+
|
165
|
+
# wait for it...
|
166
|
+
|
167
|
+
universe.fulfilled? #=> true
|
168
|
+
universe.value #=> 42
|
169
|
+
```
|
170
|
+
|
171
|
+
The `#post?` method is a blocking call. It takes a number of seconds to wait as the
|
172
|
+
first parameter and any number of additional parameters as the message. If the message
|
173
|
+
is processed within the given number of seconds the call returns the result of the
|
174
|
+
operation. If message processing raises an exception the exception is raised again
|
175
|
+
by the `#post?` method. If the call to `#post?` times out a `Concurrent::Timeout`
|
176
|
+
exception is raised.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
# needs code examples...
|
180
|
+
```
|
181
|
+
|
182
|
+
## Copyright
|
183
|
+
|
184
|
+
*Concurrent Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
|
185
|
+
It is free software and may be redistributed under the terms specified in the LICENSE file.
|
186
|
+
|
187
|
+
## License
|
188
|
+
|
189
|
+
Released under the MIT license.
|
190
|
+
|
191
|
+
http://www.opensource.org/licenses/mit-license.php
|
192
|
+
|
193
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
194
|
+
> of this software and associated documentation files (the "Software"), to deal
|
195
|
+
> in the Software without restriction, including without limitation the rights
|
196
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
197
|
+
> copies of the Software, and to permit persons to whom the Software is
|
198
|
+
> furnished to do so, subject to the following conditions:
|
199
|
+
>
|
200
|
+
> The above copyright notice and this permission notice shall be included in
|
201
|
+
> all copies or substantial portions of the Software.
|
202
|
+
>
|
203
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
204
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
205
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
206
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
207
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
208
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
209
|
+
> THE SOFTWARE.
|
data/md/agent.md
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
# Secret Agent Man
|
2
2
|
|
3
|
-
|
4
|
-
An
|
5
|
-
of the
|
3
|
+
`Agent`s are inspired by [Clojure's](http://clojure.org/) [agent](http://clojure.org/agents) function.
|
4
|
+
An `Agent` is a single atomic value that represents an identity. The current value
|
5
|
+
of the `Agent` can be requested at any time (`deref`). Each `Agent` has a work queue and operates on
|
6
6
|
the global thread pool (see below). Consumers can `post` code blocks to the
|
7
|
-
|
8
|
-
parameter. The return value of the block will become the new value of the
|
9
|
-
two error handling modes: fail and continue. A good example of an
|
7
|
+
`Agent`. The code block (function) will receive the current value of the `Agent` as its sole
|
8
|
+
parameter. The return value of the block will become the new value of the `Agent`. `Agent`s support
|
9
|
+
two error handling modes: fail and continue. A good example of an `Agent` is a shared incrementing
|
10
10
|
counter, such as the score in a video game.
|
11
11
|
|
12
|
-
An
|
13
|
-
(or `deref`) methods. Code blocks sent to the
|
12
|
+
An `Agent` must be initialize with an initial value. This value is always accessible via the `value`
|
13
|
+
(or `deref`) methods. Code blocks sent to the `Agent` will be processed in the order received. As
|
14
14
|
each block is processed the current value is updated with the result from the block. This update
|
15
15
|
is an atomic operation so a `deref` will never block and will always return the current value.
|
16
16
|
|
17
|
-
When an
|
17
|
+
When an `Agent` is created it may be given an optional `validate` block and zero or more `rescue`
|
18
18
|
blocks. When a new value is calculated the value will be checked against the validator, if present.
|
19
19
|
If the validator returns `true` the new value will be accepted. If it returns `false` it will be
|
20
20
|
rejected. If a block raises an exception during execution the list of `rescue` blocks will be
|
@@ -22,10 +22,29 @@ seacrhed in order until one matching the current exception is found. That `rescu
|
|
22
22
|
then be called an passed the exception object. If no matching `rescue` block is found, or none
|
23
23
|
were configured, then the exception will be suppressed.
|
24
24
|
|
25
|
-
|
26
|
-
Code that observes an
|
25
|
+
`Agent`s also implement Ruby's [Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html).
|
26
|
+
Code that observes an `Agent` will receive a callback with the new value any time the value
|
27
27
|
is changed.
|
28
28
|
|
29
|
+
## Copy Options
|
30
|
+
|
31
|
+
Object references in Ruby are mutable. This can lead to serious problems when
|
32
|
+
the value of an `Agent` is a mutable reference. Which is always the case unless
|
33
|
+
the value is a `Fixnum`, `Symbol`, or similar "primative" data type. Each
|
34
|
+
`Agent` instance can be configured with a few options that can help protect the
|
35
|
+
program from potentially dangerous operations. Each of these options can be
|
36
|
+
optionally set when the `Agent` is created:
|
37
|
+
|
38
|
+
* `:dup_on_deref` when true the `Agent` will call the `#dup` method on the
|
39
|
+
`value` object every time the `#value` methid is called (default: false)
|
40
|
+
* `:freeze_on_deref` when true the `Agent` will call the `#freeze` method on the
|
41
|
+
`value` object every time the `#value` method is called (default: false)
|
42
|
+
* `:copy_on_deref` when given a `Proc` object the `Proc` will be run every time
|
43
|
+
the `#value` method is called. The `Proc` will be given the current `value` as
|
44
|
+
its only parameter and the result returned by the block will be the return
|
45
|
+
value of the `#value` call. When `nil` this option will be ignored (default:
|
46
|
+
nil)
|
47
|
+
|
29
48
|
## Examples
|
30
49
|
|
31
50
|
A simple example:
|
data/md/future.md
CHANGED
@@ -1,25 +1,33 @@
|
|
1
1
|
# We're Sending You Back to the Future!
|
2
2
|
|
3
|
-
|
4
|
-
A future represents a promise to complete an action at some time in the future.
|
5
|
-
The idea behind a future is to send an
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
`Future`s are inspired by [Clojure's](http://clojure.org/) [future](http://clojuredocs.org/clojure_core/clojure.core/future)
|
4
|
+
function. A future represents a promise to complete an action at some time in the future.
|
5
|
+
The action is atomic and permanent. The idea behind a future is to send an operation for
|
6
|
+
asynchronous completion, do other stuff, then return and retrieve the result of the async
|
7
|
+
operation at a later time. `Future`s run on the global thread pool (see below).
|
8
|
+
|
9
|
+
`Future`s have three possible states: *pending*, *rejected*, and *fulfilled*. When a `Future` is created it is set
|
10
|
+
to *pending* and will remain in that state until processing is complete. A completed `Future` is either *rejected*,
|
11
|
+
indicating that an exception was thrown during processing, or *fulfilled*, indicating succedd. If a `Future` is
|
11
12
|
*fulfilled* its `value` will be updated to reflect the result of the operation. If *rejected* the `reason` will
|
12
13
|
be updated with a reference to the thrown exception. The predicate methods `pending?`, `rejected`, and `fulfilled?`
|
13
|
-
can be called at any time to obtain the state of the
|
14
|
+
can be called at any time to obtain the state of the `Future`, as can the `state` method, which returns a symbol.
|
14
15
|
|
15
|
-
Retrieving the value of a
|
16
|
-
a
|
17
|
-
immediately. When a
|
18
|
-
When a
|
16
|
+
Retrieving the value of a `Future` is done through the `value` (alias: `deref`) method. Obtaining the value of
|
17
|
+
a `Future` is a potentially blocking operation. When a `Future` is *rejected* a call to `value` will return `nil`
|
18
|
+
immediately. When a `Future` is *fulfilled* a call to `value` will immediately return the current value.
|
19
|
+
When a `Future` is *pending* a call to `value` will block until the `Future` is either *rejected* or *fulfilled*.
|
19
20
|
A *timeout* value can be passed to `value` to limit how long the call will block. If `nil` the call will
|
20
21
|
block indefinitely. If `0` the call will not block. Any other integer or float value will indicate the
|
21
22
|
maximum number of seconds to block.
|
22
23
|
|
24
|
+
The `Future` class also includes the Ruby standard library
|
25
|
+
[Observable](http://ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html) module. On fulfillment
|
26
|
+
or rejection all observers will be notified according to the normal `Observable` behavior. The observer
|
27
|
+
callback function will be called with three parameters: the `Time` of fulfillment/rejection, the
|
28
|
+
final `value`, and the final `reason`. Observers added after fulfillment/rejection will still be
|
29
|
+
notified as normal. The notification will occur on the global thread pool.
|
30
|
+
|
23
31
|
## Examples
|
24
32
|
|
25
33
|
A fulfilled example:
|
@@ -53,6 +61,40 @@ count.rejected? #=> true
|
|
53
61
|
count.reason #=> #<StandardError: Boom!>
|
54
62
|
```
|
55
63
|
|
64
|
+
An example with observation:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class Ticker
|
68
|
+
Stock = Struct.new(:symbol, :name, :exchange)
|
69
|
+
|
70
|
+
def update(time, value, reason)
|
71
|
+
ticker = value.collect do |symbol|
|
72
|
+
Stock.new(symbol['symbol'], symbol['name'], symbol['exch'])
|
73
|
+
end
|
74
|
+
|
75
|
+
output = ticker.join("\n")
|
76
|
+
print "#{output}\n"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
yahoo = Finance.new('YAHOO')
|
81
|
+
future = Concurrent::Future.new { yahoo.update.suggested_symbols }
|
82
|
+
future.add_observer(Ticker.new)
|
83
|
+
|
84
|
+
# do important stuff...
|
85
|
+
|
86
|
+
#>> #<struct Ticker::Stock symbol="YHOO", name="Yahoo! Inc.", exchange="NMS">
|
87
|
+
#>> #<struct Ticker::Stock symbol="YHO.DE", name="Yahoo! Inc.", exchange="GER">
|
88
|
+
#>> #<struct Ticker::Stock symbol="YAHOY", name="Yahoo Japan Corporation", exchange="PNK">
|
89
|
+
#>> #<struct Ticker::Stock symbol="YAHOF", name="YAHOO JAPAN CORP", exchange="PNK">
|
90
|
+
#>> #<struct Ticker::Stock symbol="YOJ.SG", name="YAHOO JAPAN", exchange="STU">
|
91
|
+
#>> #<struct Ticker::Stock symbol="YHO.SG", name="YAHOO", exchange="STU">
|
92
|
+
#>> #<struct Ticker::Stock symbol="YHOO.BA", name="Yahoo! Inc.", exchange="BUE">
|
93
|
+
#>> #<struct Ticker::Stock symbol="YHO.DU", name="YAHOO", exchange="DUS">
|
94
|
+
#>> #<struct Ticker::Stock symbol="YHO.HM", name="YAHOO", exchange="HAM">
|
95
|
+
#>> #<struct Ticker::Stock symbol="YHO.BE", name="YAHOO", exchange="BER">
|
96
|
+
```
|
97
|
+
|
56
98
|
## Copyright
|
57
99
|
|
58
100
|
*Concurrent Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
|