goru 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +226 -0
- data/lib/goru/channel.rb +96 -0
- data/lib/goru/reactor.rb +110 -64
- data/lib/goru/routine.rb +37 -15
- data/lib/goru/routines/bridge.rb +50 -0
- data/lib/goru/routines/bridges/readable.rb +37 -0
- data/lib/goru/routines/bridges/writable.rb +37 -0
- data/lib/goru/routines/channel.rb +45 -0
- data/lib/goru/routines/channels/readable.rb +37 -0
- data/lib/goru/routines/channels/writable.rb +37 -0
- data/lib/goru/routines/io.rb +87 -3
- data/lib/goru/scheduler.rb +44 -8
- data/lib/goru/version.rb +1 -1
- data/lib/goru.rb +2 -2
- metadata +30 -8
- data/lib/goru/queue.rb +0 -61
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a78af9c418381f52acab486a5f32205f6ce73fdaf4327954fe551e6ca919b21
|
4
|
+
data.tar.gz: 1185292bd3717ea47388afaa5bd3a914c869ff8ff07132e80b0f118a03e8eff1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ea306fe3b3445a1eabfbfffc4f80d572f9dcecaee129cf2e28bc65e207f3b82b06d355d52431993765703c7e6e8fb959d5ea2886292160b96e781eb8fab48a3
|
7
|
+
data.tar.gz: 497417e7c3dac2ab8454878e4cb869de978942d0bc60b14a908191d84d59be27a514373543f1fe431089eb0393157ae919941dfe210c1b6256f10ea6a5f7c54f
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
**Concurrent routines for Ruby.**
|
2
|
+
|
3
|
+
Goru is an experimental concurrency library for Ruby.
|
4
|
+
|
5
|
+
* **Lightweight:** Goru routines are not backed by fibers or threads. Each routine creates only ~345 bytes of memory overhead.
|
6
|
+
* **Explicit:** Goru requires you to describe exactly how a routine behaves. Less magic makes for fewer bugs when writing concurrent programs.
|
7
|
+
|
8
|
+
Goru was intended for low-level programs like http servers and not for direct use in user-facing code.
|
9
|
+
|
10
|
+
## How It Works
|
11
|
+
|
12
|
+
Routines are defined with initial state and a block that does work and (optionally) updates the state of the routine:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
3.times do
|
16
|
+
Goru::Scheduler.go(:running) { |routine|
|
17
|
+
case routine.state
|
18
|
+
when :running
|
19
|
+
routine.update(:sleeping)
|
20
|
+
routine.sleep(rand)
|
21
|
+
when :sleeping
|
22
|
+
puts "[#{object_id}] woke up at #{Time.now.to_f}"
|
23
|
+
routine.update(:running)
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Routines run concurrently within a reactor, each reactor running in a dedicated thread. Each eligible routine is called
|
30
|
+
once on every tick of the reactor it is scheduled to run in. In the example above, the three routines sleep for a random
|
31
|
+
interval before waking up and printing the current time. Here is some example output:
|
32
|
+
|
33
|
+
```
|
34
|
+
[1840] woke up at 1677939216.379147
|
35
|
+
[1860] woke up at 1677939217.059535
|
36
|
+
[1920] woke up at 1677939217.190349
|
37
|
+
[1860] woke up at 1677939217.6196458
|
38
|
+
[1920] woke up at 1677939217.935916
|
39
|
+
[1840] woke up at 1677939218.033243
|
40
|
+
[1860] woke up at 1677939218.532908
|
41
|
+
[1920] woke up at 1677939218.8669178
|
42
|
+
[1840] woke up at 1677939219.379714
|
43
|
+
[1860] woke up at 1677939219.522777
|
44
|
+
[1920] woke up at 1677939220.0475688
|
45
|
+
[1840] woke up at 1677939220.253979
|
46
|
+
```
|
47
|
+
|
48
|
+
Each reactor can only run one routine at any given point in time, but if a routine blocks (e.g. by sleeping or
|
49
|
+
performing i/o) the reactor calls another eligible routine before returning to the previously blocked routine
|
50
|
+
on the next tick.
|
51
|
+
|
52
|
+
## Scheduler
|
53
|
+
|
54
|
+
By default Goru routines are scheduled in a global scheduler that waits at the end of the program for all routines
|
55
|
+
to finish. While this is useful for small scripts, most use-cases will involve creating your own scheduler and
|
56
|
+
registering routines directly:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
scheduler = Goru::Scheduler.new
|
60
|
+
scheduler.go { |routine|
|
61
|
+
...
|
62
|
+
}
|
63
|
+
scheduler.wait
|
64
|
+
```
|
65
|
+
|
66
|
+
Routines are scheduled to run immediately after registration.
|
67
|
+
|
68
|
+
### Tuning
|
69
|
+
|
70
|
+
Schedulers default to running a number of reactors matching the number of processors on the current system. Tune
|
71
|
+
this to your needs with the `count` option when creating a scheduler:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
scheduler = Goru::Scheduler.new(count: 3)
|
75
|
+
```
|
76
|
+
|
77
|
+
## State
|
78
|
+
|
79
|
+
Routines are initialized with default state that is useful for coordination between ticks. This is perhaps the
|
80
|
+
oddest part of Goru but the explicitness can make it easier to understand exactly how your routines will behave.
|
81
|
+
|
82
|
+
Take a look at the [examples](./examples) to get some ideas.
|
83
|
+
|
84
|
+
## Finishing
|
85
|
+
|
86
|
+
Routines will run forever until you say they are finished:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
Goru::Scheduler.go { |routine|
|
90
|
+
routine.finished
|
91
|
+
}
|
92
|
+
```
|
93
|
+
|
94
|
+
### Results
|
95
|
+
|
96
|
+
When finishing a routine you can provide a final result:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
routines = []
|
100
|
+
scheduler = Goru::Scheduler.new
|
101
|
+
routines << scheduler.go { |routine| routine.finished(true) }
|
102
|
+
routines << scheduler.go { |routine| routine.finished(false) }
|
103
|
+
routines << scheduler.go { |routine| routine.finished(true) }
|
104
|
+
scheduler.wait
|
105
|
+
|
106
|
+
pp routines.map(&:result)
|
107
|
+
# [true, false, true]
|
108
|
+
```
|
109
|
+
|
110
|
+
## Error Handling
|
111
|
+
|
112
|
+
Unhandled errors within a routine cause the routine to enter an `:errored` state. Calling `result` on an errored
|
113
|
+
routine causes the error to be re-raised. Routines can handle errors elegantly using the `handle` method:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Goru::Scheduler.go { |routine|
|
117
|
+
routine.handle(StandardError) do |event:|
|
118
|
+
# do something with `event`
|
119
|
+
end
|
120
|
+
|
121
|
+
...
|
122
|
+
}
|
123
|
+
```
|
124
|
+
|
125
|
+
See [`core-handler`](https://github.com/bryanp/corerb/tree/main/handler) for more about error handling.
|
126
|
+
|
127
|
+
## Sleeping
|
128
|
+
|
129
|
+
Goru implements a non-blocking version of `sleep` that makes the routine ineligible to be called until the sleep time
|
130
|
+
has elapsed. It is important to note that Ruby's built-in sleep method will block the reactor and should not be used.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Goru::Scheduler.go { |routine|
|
134
|
+
routine.sleep(3)
|
135
|
+
}
|
136
|
+
```
|
137
|
+
|
138
|
+
Unlike `Kernel#sleep` Goru's sleep method requires a duration.
|
139
|
+
|
140
|
+
## Channels
|
141
|
+
|
142
|
+
Goru offers buffered reading and writing through channels:
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
channel = Goru::Channel.new
|
146
|
+
|
147
|
+
Goru::Scheduler.go(channel: channel, intent: :w) { |routine|
|
148
|
+
routine << SecureRandom.hex
|
149
|
+
}
|
150
|
+
|
151
|
+
# This routine is not invoked unless the channel contains data for reading.
|
152
|
+
#
|
153
|
+
Goru::Scheduler.go(channel: channel, intent: :r) { |routine|
|
154
|
+
value = routine.read
|
155
|
+
}
|
156
|
+
```
|
157
|
+
|
158
|
+
Channels are unbounded by default, meaning they can hold an unlimited amount of data. This behavior can be changed by
|
159
|
+
initializing a channel with a specific size. Routines with the intent to write will not be invoked unless the channel
|
160
|
+
has space available for writing.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
channel = Goru::Channel.new(size: 3)
|
164
|
+
|
165
|
+
# This routine is not invoked if the channel is full.
|
166
|
+
#
|
167
|
+
Goru::Scheduler.go(channel: channel, intent: :w) { |routine|
|
168
|
+
routine << SecureRandom.hex
|
169
|
+
}
|
170
|
+
```
|
171
|
+
|
172
|
+
## IO
|
173
|
+
|
174
|
+
Goru includes a pattern for non-blocking io. With it you can implement non-blocking servers, clients, etc.
|
175
|
+
|
176
|
+
Routines that involve io must be created with an io object and an intent. Possible intents include:
|
177
|
+
|
178
|
+
* `:r` for reading
|
179
|
+
* `:r` for writing
|
180
|
+
* `:rw` for reading and writing
|
181
|
+
|
182
|
+
Here is the beginning of an http server in Goru:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
Goru::Scheduler.go(io: TCPServer.new("localhost", 4242), intent: :r) { |server_routine|
|
186
|
+
next unless client = server_routine.accept
|
187
|
+
|
188
|
+
Goru::Scheduler.go(io: client, intent: :r) { |client_routine|
|
189
|
+
next unless data = client_routine.read(16384)
|
190
|
+
|
191
|
+
# do something with `data`
|
192
|
+
}
|
193
|
+
}
|
194
|
+
```
|
195
|
+
|
196
|
+
### Changing Intents
|
197
|
+
|
198
|
+
Intents can be changed after a routine is created, e.g. to switch a routine from reading to writing:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
Goru::Scheduler.go(io: io, intent: :r) { |routine|
|
202
|
+
routine.intent = :w
|
203
|
+
}
|
204
|
+
```
|
205
|
+
|
206
|
+
## Bridges
|
207
|
+
|
208
|
+
Goru supports coordinates buffered io using bridges:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
writer = Goru::Channel.new
|
212
|
+
|
213
|
+
Goru::Scheduler.go(io: io, intent: :w) { |routine|
|
214
|
+
routine.bridge(writer, intent: :w)
|
215
|
+
}
|
216
|
+
|
217
|
+
Goru::Scheduler.go(channel: writer) { |routine|
|
218
|
+
routine << SecureRandom.hex
|
219
|
+
}
|
220
|
+
```
|
221
|
+
|
222
|
+
This allows routines to easily write data to a buffer independently of how the data is written to io.
|
223
|
+
|
224
|
+
## Credits
|
225
|
+
|
226
|
+
Goru was designed while writing a project in Go and imagining what Go-like concurrency might look like in Ruby.
|
data/lib/goru/channel.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./reactor"
|
4
|
+
|
5
|
+
module Goru
|
6
|
+
class Channel
|
7
|
+
def initialize(size: nil)
|
8
|
+
@size = size
|
9
|
+
@messages = []
|
10
|
+
@closed = false
|
11
|
+
@observers = Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# [public]
|
15
|
+
#
|
16
|
+
attr_writer :observer
|
17
|
+
|
18
|
+
# [public]
|
19
|
+
#
|
20
|
+
def <<(message)
|
21
|
+
raise "closed" if @closed
|
22
|
+
@messages << message
|
23
|
+
@observers.each(&:channel_received)
|
24
|
+
end
|
25
|
+
|
26
|
+
# [public]
|
27
|
+
#
|
28
|
+
def read
|
29
|
+
message = @messages.shift
|
30
|
+
@observers.each(&:channel_read)
|
31
|
+
message
|
32
|
+
end
|
33
|
+
|
34
|
+
# [public]
|
35
|
+
#
|
36
|
+
def any?
|
37
|
+
@messages.any?
|
38
|
+
end
|
39
|
+
|
40
|
+
# [public]
|
41
|
+
#
|
42
|
+
def empty?
|
43
|
+
@messages.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
# [public]
|
47
|
+
#
|
48
|
+
def full?
|
49
|
+
@size && @messages.size == @size
|
50
|
+
end
|
51
|
+
|
52
|
+
# [public]
|
53
|
+
#
|
54
|
+
def closed?
|
55
|
+
@closed == true
|
56
|
+
end
|
57
|
+
|
58
|
+
# [public]
|
59
|
+
#
|
60
|
+
def close
|
61
|
+
@closed = true
|
62
|
+
@observers.each(&:channel_closed)
|
63
|
+
end
|
64
|
+
|
65
|
+
# [public]
|
66
|
+
#
|
67
|
+
def reopen
|
68
|
+
@closed = false
|
69
|
+
@observers.each(&:channel_reopened)
|
70
|
+
end
|
71
|
+
|
72
|
+
# [public]
|
73
|
+
#
|
74
|
+
def clear
|
75
|
+
@messages.clear
|
76
|
+
end
|
77
|
+
|
78
|
+
# [public]
|
79
|
+
#
|
80
|
+
def length
|
81
|
+
@messages.length
|
82
|
+
end
|
83
|
+
|
84
|
+
# [public]
|
85
|
+
#
|
86
|
+
def add_observer(observer)
|
87
|
+
@observers << observer
|
88
|
+
end
|
89
|
+
|
90
|
+
# [public]
|
91
|
+
#
|
92
|
+
def remove_observer(observer)
|
93
|
+
@observers.delete(observer)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/goru/reactor.rb
CHANGED
@@ -5,6 +5,7 @@ require "nio"
|
|
5
5
|
require "timers/group"
|
6
6
|
require "timers/wait"
|
7
7
|
|
8
|
+
require_relative "routines/bridge"
|
8
9
|
require_relative "routines/io"
|
9
10
|
|
10
11
|
module Goru
|
@@ -14,12 +15,13 @@ module Goru
|
|
14
15
|
def initialize(queue:, scheduler:)
|
15
16
|
@queue = queue
|
16
17
|
@scheduler = scheduler
|
17
|
-
@routines =
|
18
|
-
@
|
18
|
+
@routines = Set.new
|
19
|
+
@bridges = Set.new
|
19
20
|
@timers = Timers::Group.new
|
20
21
|
@selector = NIO::Selector.new
|
21
|
-
@status = nil
|
22
22
|
@stopped = false
|
23
|
+
@status = nil
|
24
|
+
@mutex = Mutex.new
|
23
25
|
end
|
24
26
|
|
25
27
|
# [public]
|
@@ -29,58 +31,38 @@ module Goru
|
|
29
31
|
# [public]
|
30
32
|
#
|
31
33
|
def run
|
32
|
-
@status = :running
|
33
|
-
|
34
34
|
until @stopped
|
35
|
+
set_status(:running)
|
36
|
+
|
35
37
|
@routines.each do |routine|
|
36
38
|
call_routine(routine)
|
37
39
|
end
|
38
40
|
|
39
|
-
# TODO: Remove the monitor from the selector.
|
40
|
-
#
|
41
|
-
cleanup_finished_routines
|
42
|
-
|
43
41
|
begin
|
44
|
-
|
45
|
-
adopt_routine(routine)
|
46
|
-
end
|
42
|
+
wait_for_routine(block: false)
|
47
43
|
rescue ThreadError
|
48
44
|
interval = @timers.wait_interval
|
49
45
|
|
50
46
|
if interval.nil?
|
51
47
|
if @routines.empty?
|
52
48
|
if @selector.empty?
|
53
|
-
|
54
|
-
@scheduler.signal(self)
|
55
|
-
if (routine = @queue.pop)
|
56
|
-
adopt_routine(routine)
|
57
|
-
end
|
49
|
+
become_idle
|
58
50
|
else
|
59
|
-
|
60
|
-
|
61
|
-
#
|
62
|
-
@selector.select do |monitor|
|
63
|
-
monitor.value.call
|
51
|
+
wait_for_bridge do
|
52
|
+
wait_for_selector
|
64
53
|
end
|
65
54
|
end
|
66
55
|
else
|
67
|
-
|
68
|
-
|
56
|
+
wait_for_bridge do
|
57
|
+
wait_for_selector(0)
|
69
58
|
end
|
70
59
|
end
|
71
60
|
elsif interval > 0
|
72
61
|
if @selector.empty?
|
73
|
-
|
74
|
-
if (routine = @queue.pop_with_timeout(remaining))
|
75
|
-
adopt_routine(routine)
|
76
|
-
break
|
77
|
-
end
|
78
|
-
rescue ThreadError
|
79
|
-
# nothing to do
|
80
|
-
end
|
62
|
+
wait_for_interval(interval)
|
81
63
|
else
|
82
|
-
|
83
|
-
|
64
|
+
wait_for_bridge do
|
65
|
+
wait_for_selector(interval)
|
84
66
|
end
|
85
67
|
end
|
86
68
|
end
|
@@ -90,63 +72,127 @@ module Goru
|
|
90
72
|
end
|
91
73
|
ensure
|
92
74
|
@selector.close
|
93
|
-
|
75
|
+
set_status(:finished)
|
76
|
+
end
|
77
|
+
|
78
|
+
private def become_idle
|
79
|
+
set_status(:idle)
|
80
|
+
@scheduler.signal(self)
|
81
|
+
wait_for_routine
|
82
|
+
end
|
83
|
+
|
84
|
+
private def wait_for_selector(timeout = nil)
|
85
|
+
@selector.select(timeout) do |monitor|
|
86
|
+
monitor.value.call
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private def wait_for_bridge
|
91
|
+
if @bridges.any?(&:applicable?) && @bridges.none?(&:ready?)
|
92
|
+
wait_for_routine
|
93
|
+
else
|
94
|
+
yield
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private def wait_for_interval(timeout)
|
99
|
+
Timers::Wait.for(timeout) do |remaining|
|
100
|
+
break if wait_for_routine(timeout: remaining)
|
101
|
+
rescue ThreadError
|
102
|
+
# nothing to do
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private def wait_for_routine(block: true, timeout: nil)
|
107
|
+
if timeout
|
108
|
+
if (routine = @queue.pop(timeout: timeout))
|
109
|
+
adopt_routine(routine)
|
110
|
+
end
|
111
|
+
elsif (routine = @queue.pop(!block))
|
112
|
+
adopt_routine(routine)
|
113
|
+
end
|
94
114
|
end
|
95
115
|
|
96
116
|
# [public]
|
97
117
|
#
|
98
|
-
def
|
99
|
-
@
|
118
|
+
def finished?
|
119
|
+
@mutex.synchronize do
|
120
|
+
@status == :idle || @status == :stopped
|
121
|
+
end
|
122
|
+
end
|
100
123
|
|
101
|
-
|
124
|
+
# [public]
|
125
|
+
#
|
126
|
+
def signal
|
127
|
+
unless @selector.empty?
|
102
128
|
@selector.wakeup
|
103
129
|
end
|
104
130
|
end
|
105
131
|
|
106
132
|
# [public]
|
107
133
|
#
|
108
|
-
def
|
134
|
+
def wakeup
|
135
|
+
signal
|
136
|
+
@queue << :wakeup
|
137
|
+
end
|
138
|
+
|
139
|
+
# [public]
|
140
|
+
#
|
141
|
+
def stop
|
142
|
+
@stopped = true
|
143
|
+
@selector.wakeup
|
144
|
+
rescue IOError
|
145
|
+
end
|
146
|
+
|
147
|
+
# [public]
|
148
|
+
#
|
149
|
+
def routine_asleep(routine, seconds)
|
109
150
|
@timers.after(seconds) {
|
110
151
|
routine.wake
|
111
152
|
}
|
112
153
|
end
|
113
154
|
|
114
|
-
|
115
|
-
|
116
|
-
|
155
|
+
# [public]
|
156
|
+
#
|
157
|
+
def adopt_routine(routine)
|
117
158
|
case routine
|
118
159
|
when Routines::IO
|
119
160
|
monitor = @selector.register(routine.io, routine.intent)
|
120
|
-
|
121
|
-
monitor
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
@finished << routine
|
129
|
-
end
|
130
|
-
}
|
131
|
-
else
|
161
|
+
monitor.value = routine
|
162
|
+
routine.monitor = monitor
|
163
|
+
routine.reactor = self
|
164
|
+
when Routines::Bridge
|
165
|
+
routine.reactor = self
|
166
|
+
@bridges << routine
|
167
|
+
when Routine
|
168
|
+
routine.reactor = self
|
132
169
|
@routines << routine
|
133
170
|
end
|
134
171
|
end
|
135
172
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
when
|
141
|
-
|
173
|
+
# [public]
|
174
|
+
#
|
175
|
+
def routine_finished(routine)
|
176
|
+
case routine
|
177
|
+
when Routines::Bridge
|
178
|
+
@bridges.delete(routine)
|
179
|
+
when Routines::IO
|
180
|
+
@selector.deregister(routine.io)
|
142
181
|
else
|
143
|
-
@
|
182
|
+
@routines.delete(routine)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
private def set_status(status)
|
187
|
+
@mutex.synchronize do
|
188
|
+
@status = status
|
144
189
|
end
|
145
190
|
end
|
146
191
|
|
147
|
-
private def
|
148
|
-
|
149
|
-
|
192
|
+
private def call_routine(routine)
|
193
|
+
case routine.status
|
194
|
+
when :ready
|
195
|
+
routine.call
|
150
196
|
end
|
151
197
|
end
|
152
198
|
end
|
data/lib/goru/routine.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "is/handler"
|
4
|
+
|
3
5
|
module Goru
|
4
6
|
# [public]
|
5
7
|
#
|
6
8
|
class Routine
|
9
|
+
include Is::Handler
|
10
|
+
|
11
|
+
handle(StandardError) do |event:|
|
12
|
+
$stderr << <<~ERROR
|
13
|
+
[goru] routine crashed: #{event}
|
14
|
+
#{event.backtrace.join("\n")}
|
15
|
+
ERROR
|
16
|
+
end
|
17
|
+
|
7
18
|
def initialize(state = nil, &block)
|
8
19
|
@state = state
|
9
20
|
@block = block
|
10
|
-
|
21
|
+
set_status(:ready)
|
11
22
|
@result, @error, @reactor = nil
|
12
23
|
end
|
13
24
|
|
@@ -17,12 +28,9 @@ module Goru
|
|
17
28
|
|
18
29
|
# [public]
|
19
30
|
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
#
|
24
|
-
def running?
|
25
|
-
@status == :running
|
31
|
+
def reactor=(reactor)
|
32
|
+
@reactor = reactor
|
33
|
+
status_changed
|
26
34
|
end
|
27
35
|
|
28
36
|
# [public]
|
@@ -30,11 +38,9 @@ module Goru
|
|
30
38
|
def call
|
31
39
|
@block.call(self)
|
32
40
|
rescue => error
|
33
|
-
puts "[routine error] #{error}"
|
34
|
-
puts error.backtrace
|
35
|
-
|
36
41
|
@error = error
|
37
|
-
|
42
|
+
set_status(:errored)
|
43
|
+
trigger(error)
|
38
44
|
end
|
39
45
|
|
40
46
|
# [public]
|
@@ -42,7 +48,7 @@ module Goru
|
|
42
48
|
def finished(result = nil)
|
43
49
|
unless @finished
|
44
50
|
@result = result
|
45
|
-
|
51
|
+
set_status(:finished)
|
46
52
|
end
|
47
53
|
end
|
48
54
|
|
@@ -66,14 +72,30 @@ module Goru
|
|
66
72
|
# [public]
|
67
73
|
#
|
68
74
|
def sleep(seconds)
|
69
|
-
|
70
|
-
@reactor.
|
75
|
+
set_status(:idle)
|
76
|
+
@reactor.routine_asleep(self, seconds)
|
71
77
|
end
|
72
78
|
|
73
79
|
# [public]
|
74
80
|
#
|
75
81
|
def wake
|
76
|
-
|
82
|
+
set_status(:ready)
|
83
|
+
end
|
84
|
+
|
85
|
+
# [public]
|
86
|
+
#
|
87
|
+
private def set_status(status)
|
88
|
+
@status = status
|
89
|
+
status_changed
|
90
|
+
end
|
91
|
+
|
92
|
+
# [public]
|
93
|
+
#
|
94
|
+
private def status_changed
|
95
|
+
case @status
|
96
|
+
when :finished
|
97
|
+
@reactor&.routine_finished(self)
|
98
|
+
end
|
77
99
|
end
|
78
100
|
end
|
79
101
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../routine"
|
4
|
+
|
5
|
+
module Goru
|
6
|
+
module Routines
|
7
|
+
# [public]
|
8
|
+
#
|
9
|
+
class Bridge < Routine
|
10
|
+
def initialize(state = nil, routine:, channel:, &block)
|
11
|
+
super(state, &block)
|
12
|
+
|
13
|
+
@routine = routine
|
14
|
+
@channel = channel
|
15
|
+
@channel.add_observer(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
# [public]
|
19
|
+
#
|
20
|
+
def ready?
|
21
|
+
@status == :ready
|
22
|
+
end
|
23
|
+
|
24
|
+
private def status_changed
|
25
|
+
case @status
|
26
|
+
when :finished
|
27
|
+
@channel.remove_observer(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def channel_received
|
34
|
+
update_status
|
35
|
+
end
|
36
|
+
|
37
|
+
def channel_read
|
38
|
+
update_status
|
39
|
+
end
|
40
|
+
|
41
|
+
def channel_closed
|
42
|
+
update_status
|
43
|
+
end
|
44
|
+
|
45
|
+
def channel_reopened
|
46
|
+
update_status
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../bridge"
|
4
|
+
|
5
|
+
module Goru
|
6
|
+
module Routines
|
7
|
+
module Bridges
|
8
|
+
class Readable < Bridge
|
9
|
+
def initialize(...)
|
10
|
+
super
|
11
|
+
|
12
|
+
update_status
|
13
|
+
end
|
14
|
+
|
15
|
+
# [public]
|
16
|
+
#
|
17
|
+
def applicable?
|
18
|
+
@routine.intent == :r
|
19
|
+
end
|
20
|
+
|
21
|
+
private def update_status
|
22
|
+
status = if @routine.status == :finished
|
23
|
+
:finished
|
24
|
+
elsif @channel.full?
|
25
|
+
:idle
|
26
|
+
elsif @channel.closed?
|
27
|
+
:idle
|
28
|
+
else
|
29
|
+
:ready
|
30
|
+
end
|
31
|
+
|
32
|
+
set_status(status)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../bridge"
|
4
|
+
|
5
|
+
module Goru
|
6
|
+
module Routines
|
7
|
+
module Bridges
|
8
|
+
class Writable < Bridge
|
9
|
+
def initialize(...)
|
10
|
+
super
|
11
|
+
|
12
|
+
update_status
|
13
|
+
end
|
14
|
+
|
15
|
+
# [public]
|
16
|
+
#
|
17
|
+
def applicable?
|
18
|
+
@routine.intent == :w
|
19
|
+
end
|
20
|
+
|
21
|
+
private def update_status
|
22
|
+
status = if @routine.status == :finished
|
23
|
+
:finished
|
24
|
+
elsif @channel.any?
|
25
|
+
:ready
|
26
|
+
elsif @channel.closed?
|
27
|
+
:finished
|
28
|
+
else
|
29
|
+
:idle
|
30
|
+
end
|
31
|
+
|
32
|
+
set_status(status)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../routine"
|
4
|
+
|
5
|
+
module Goru
|
6
|
+
module Routines
|
7
|
+
# [public]
|
8
|
+
#
|
9
|
+
class Channel < Routine
|
10
|
+
def initialize(state = nil, channel:, &block)
|
11
|
+
super(state, &block)
|
12
|
+
|
13
|
+
@channel = channel
|
14
|
+
@channel.add_observer(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
private def status_changed
|
18
|
+
case @status
|
19
|
+
when :ready
|
20
|
+
@reactor&.wakeup
|
21
|
+
when :finished
|
22
|
+
@channel.remove_observer(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def channel_received
|
29
|
+
update_status
|
30
|
+
end
|
31
|
+
|
32
|
+
def channel_read
|
33
|
+
update_status
|
34
|
+
end
|
35
|
+
|
36
|
+
def channel_closed
|
37
|
+
update_status
|
38
|
+
end
|
39
|
+
|
40
|
+
def channel_reopened
|
41
|
+
update_status
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../channel"
|
4
|
+
|
5
|
+
module Goru
|
6
|
+
module Routines
|
7
|
+
module Channels
|
8
|
+
# [public]
|
9
|
+
#
|
10
|
+
class Readable < Channel
|
11
|
+
def initialize(...)
|
12
|
+
super
|
13
|
+
|
14
|
+
update_status
|
15
|
+
end
|
16
|
+
|
17
|
+
# [public]
|
18
|
+
#
|
19
|
+
def read
|
20
|
+
@channel.read
|
21
|
+
end
|
22
|
+
|
23
|
+
private def update_status
|
24
|
+
status = if @channel.any?
|
25
|
+
:ready
|
26
|
+
elsif @channel.closed?
|
27
|
+
:finished
|
28
|
+
else
|
29
|
+
:idle
|
30
|
+
end
|
31
|
+
|
32
|
+
set_status(status)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../channel"
|
4
|
+
|
5
|
+
module Goru
|
6
|
+
module Routines
|
7
|
+
module Channels
|
8
|
+
# [public]
|
9
|
+
#
|
10
|
+
class Writable < Channel
|
11
|
+
def initialize(...)
|
12
|
+
super
|
13
|
+
|
14
|
+
update_status
|
15
|
+
end
|
16
|
+
|
17
|
+
# [public]
|
18
|
+
#
|
19
|
+
def <<(message)
|
20
|
+
@channel << message
|
21
|
+
end
|
22
|
+
|
23
|
+
private def update_status
|
24
|
+
status = if @channel.full?
|
25
|
+
:idle
|
26
|
+
elsif @channel.closed?
|
27
|
+
:idle
|
28
|
+
else
|
29
|
+
:ready
|
30
|
+
end
|
31
|
+
|
32
|
+
set_status(status)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/goru/routines/io.rb
CHANGED
@@ -1,24 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "../routine"
|
4
|
+
require_relative "bridges/readable"
|
5
|
+
require_relative "bridges/writable"
|
4
6
|
|
5
7
|
module Goru
|
6
8
|
module Routines
|
7
9
|
# [public]
|
8
10
|
#
|
9
11
|
class IO < Routine
|
10
|
-
def initialize(state = nil, io:, intent
|
12
|
+
def initialize(state = nil, io:, intent:, &block)
|
11
13
|
super(state, &block)
|
12
14
|
|
13
15
|
@io = io
|
14
|
-
@intent = intent
|
16
|
+
@intent = normalize_intent(intent)
|
15
17
|
@status = :selecting
|
18
|
+
@monitor = nil
|
19
|
+
@finishers = []
|
16
20
|
end
|
17
21
|
|
18
22
|
# [public]
|
19
23
|
#
|
20
24
|
attr_reader :io, :intent
|
21
25
|
|
26
|
+
attr_writer :monitor
|
27
|
+
|
22
28
|
# [public]
|
23
29
|
#
|
24
30
|
def accept
|
@@ -31,11 +37,89 @@ module Goru
|
|
31
37
|
result = @io.read_nonblock(bytes, exception: false)
|
32
38
|
|
33
39
|
case result
|
34
|
-
when
|
40
|
+
when nil
|
41
|
+
finished
|
42
|
+
nil
|
43
|
+
when :wait_readable
|
35
44
|
# nothing to do
|
36
45
|
else
|
37
46
|
result
|
38
47
|
end
|
48
|
+
rescue Errno::ECONNRESET
|
49
|
+
finished
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# [public]
|
54
|
+
#
|
55
|
+
def write(data)
|
56
|
+
result = @io.write_nonblock(data, exception: false)
|
57
|
+
|
58
|
+
case result
|
59
|
+
when nil
|
60
|
+
finished
|
61
|
+
nil
|
62
|
+
when :wait_writable
|
63
|
+
# nothing to do
|
64
|
+
else
|
65
|
+
result
|
66
|
+
end
|
67
|
+
rescue Errno::ECONNRESET
|
68
|
+
finished
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# [public]
|
73
|
+
#
|
74
|
+
def intent=(intent)
|
75
|
+
intent = normalize_intent(intent)
|
76
|
+
validate_intent!(intent)
|
77
|
+
|
78
|
+
@monitor.interests = intent
|
79
|
+
@intent = intent
|
80
|
+
end
|
81
|
+
|
82
|
+
# [public]
|
83
|
+
#
|
84
|
+
def bridge(channel, intent:)
|
85
|
+
intent = normalize_intent(intent)
|
86
|
+
validate_intent!(intent)
|
87
|
+
|
88
|
+
bridge = case intent
|
89
|
+
when :r
|
90
|
+
Bridges::Readable.new(routine: self, channel: channel)
|
91
|
+
when :w
|
92
|
+
Bridges::Writable.new(routine: self, channel: channel)
|
93
|
+
end
|
94
|
+
|
95
|
+
on_finished { bridge.finished }
|
96
|
+
@reactor.adopt_routine(bridge)
|
97
|
+
bridge
|
98
|
+
end
|
99
|
+
|
100
|
+
# [public]
|
101
|
+
#
|
102
|
+
def on_finished(&block)
|
103
|
+
@finishers << block
|
104
|
+
end
|
105
|
+
|
106
|
+
private def status_changed
|
107
|
+
case @status
|
108
|
+
when :finished
|
109
|
+
@finishers.each(&:call)
|
110
|
+
end
|
111
|
+
|
112
|
+
super
|
113
|
+
end
|
114
|
+
|
115
|
+
INTENTS = %i[r w].freeze
|
116
|
+
|
117
|
+
private def validate_intent!(intent)
|
118
|
+
raise ArgumentError, "unknown intent: #{intent}" unless INTENTS.include?(intent)
|
119
|
+
end
|
120
|
+
|
121
|
+
private def normalize_intent(intent)
|
122
|
+
intent.to_sym
|
39
123
|
end
|
40
124
|
end
|
41
125
|
end
|
data/lib/goru/scheduler.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "etc"
|
3
4
|
require "is/global"
|
4
5
|
|
5
|
-
require_relative "
|
6
|
+
require_relative "channel"
|
6
7
|
require_relative "reactor"
|
8
|
+
require_relative "routines/channel"
|
7
9
|
require_relative "routines/io"
|
8
10
|
|
11
|
+
require_relative "routines/channels/readable"
|
12
|
+
require_relative "routines/channels/writable"
|
13
|
+
|
9
14
|
module Goru
|
10
15
|
# [public]
|
11
16
|
#
|
@@ -13,14 +18,30 @@ module Goru
|
|
13
18
|
include Is::Global
|
14
19
|
include MonitorMixin
|
15
20
|
|
16
|
-
|
17
|
-
|
21
|
+
class << self
|
22
|
+
# Prevent issues when including `Goru` at the toplevel.
|
23
|
+
#
|
24
|
+
# [public]
|
25
|
+
#
|
26
|
+
def go(...)
|
27
|
+
global.go(...)
|
28
|
+
end
|
29
|
+
|
30
|
+
# [public]
|
31
|
+
#
|
32
|
+
def default_scheduler_count
|
33
|
+
Etc.nprocessors
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(count: self.class.default_scheduler_count)
|
38
|
+
super()
|
18
39
|
|
19
40
|
@stopping = false
|
20
|
-
@routines = Queue.new
|
41
|
+
@routines = Thread::Queue.new
|
21
42
|
@condition = new_cond
|
22
43
|
|
23
|
-
@reactors =
|
44
|
+
@reactors = count.times.map {
|
24
45
|
Reactor.new(queue: @routines, scheduler: self)
|
25
46
|
}
|
26
47
|
|
@@ -35,12 +56,26 @@ module Goru
|
|
35
56
|
|
36
57
|
# [public]
|
37
58
|
#
|
38
|
-
def go(state = nil, io: nil, intent:
|
39
|
-
|
59
|
+
def go(state = nil, io: nil, channel: nil, intent: nil, &block)
|
60
|
+
raise ArgumentError, "cannot set both `io` and `channel`" if io && channel
|
61
|
+
|
62
|
+
routine = if io
|
40
63
|
Routines::IO.new(state, io: io, intent: intent, &block)
|
64
|
+
elsif channel
|
65
|
+
case intent
|
66
|
+
when :r
|
67
|
+
Routines::Channels::Readable.new(state, channel: channel, &block)
|
68
|
+
when :w
|
69
|
+
Routines::Channels::Writable.new(state, channel: channel, &block)
|
70
|
+
end
|
41
71
|
else
|
42
72
|
Routine.new(state, &block)
|
43
73
|
end
|
74
|
+
|
75
|
+
@routines << routine
|
76
|
+
@reactors.each(&:signal)
|
77
|
+
|
78
|
+
routine
|
44
79
|
end
|
45
80
|
|
46
81
|
# [public]
|
@@ -51,6 +86,7 @@ module Goru
|
|
51
86
|
@stopping
|
52
87
|
end
|
53
88
|
end
|
89
|
+
rescue Interrupt
|
54
90
|
ensure
|
55
91
|
stop
|
56
92
|
end
|
@@ -68,7 +104,7 @@ module Goru
|
|
68
104
|
#
|
69
105
|
def signal(reactor)
|
70
106
|
synchronize do
|
71
|
-
if @reactors.all?
|
107
|
+
if @reactors.all?(&:finished?)
|
72
108
|
@stopping = true
|
73
109
|
end
|
74
110
|
|
data/lib/goru/version.rb
CHANGED
data/lib/goru.rb
CHANGED
@@ -8,8 +8,8 @@ module Goru
|
|
8
8
|
|
9
9
|
extend Is::Extension
|
10
10
|
|
11
|
-
def go(state = nil, io: nil, intent:
|
12
|
-
Scheduler.go(state, io: io, intent: intent, &block)
|
11
|
+
def go(state = nil, io: nil, channel: nil, intent: nil, &block)
|
12
|
+
Scheduler.go(state, io: io, channel: channel, intent: intent, &block)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: goru
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan Powell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: core-extension
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0.4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: core-
|
28
|
+
name: core-handler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: core-global
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.2'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: nio4r
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,20 +81,28 @@ dependencies:
|
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '4.3'
|
69
83
|
description: Concurrent routines for Ruby.
|
70
|
-
email: bryan@
|
84
|
+
email: bryan@bryanp.org
|
71
85
|
executables: []
|
72
86
|
extensions: []
|
73
87
|
extra_rdoc_files: []
|
74
88
|
files:
|
89
|
+
- CHANGELOG.md
|
75
90
|
- LICENSE
|
91
|
+
- README.md
|
76
92
|
- lib/goru.rb
|
77
|
-
- lib/goru/
|
93
|
+
- lib/goru/channel.rb
|
78
94
|
- lib/goru/reactor.rb
|
79
95
|
- lib/goru/routine.rb
|
96
|
+
- lib/goru/routines/bridge.rb
|
97
|
+
- lib/goru/routines/bridges/readable.rb
|
98
|
+
- lib/goru/routines/bridges/writable.rb
|
99
|
+
- lib/goru/routines/channel.rb
|
100
|
+
- lib/goru/routines/channels/readable.rb
|
101
|
+
- lib/goru/routines/channels/writable.rb
|
80
102
|
- lib/goru/routines/io.rb
|
81
103
|
- lib/goru/scheduler.rb
|
82
104
|
- lib/goru/version.rb
|
83
|
-
homepage: https://github.com/
|
105
|
+
homepage: https://github.com/bryanp/goru/
|
84
106
|
licenses:
|
85
107
|
- MPL-2.0
|
86
108
|
metadata: {}
|
@@ -92,14 +114,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
114
|
requirements:
|
93
115
|
- - ">="
|
94
116
|
- !ruby/object:Gem::Version
|
95
|
-
version: 2.
|
117
|
+
version: 3.2.0
|
96
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
119
|
requirements:
|
98
120
|
- - ">="
|
99
121
|
- !ruby/object:Gem::Version
|
100
122
|
version: '0'
|
101
123
|
requirements: []
|
102
|
-
rubygems_version: 3.
|
124
|
+
rubygems_version: 3.4.9
|
103
125
|
signing_key:
|
104
126
|
specification_version: 4
|
105
127
|
summary: Concurrent routines for Ruby.
|
data/lib/goru/queue.rb
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Goru
|
4
|
-
# [public] Based on: https://spin.atomicobject.com/2017/06/28/queue-pop-with-timeout-fixed/
|
5
|
-
#
|
6
|
-
class Queue
|
7
|
-
def initialize
|
8
|
-
@mutex = Mutex.new
|
9
|
-
@queue = []
|
10
|
-
@received = ConditionVariable.new
|
11
|
-
@closed = false
|
12
|
-
end
|
13
|
-
|
14
|
-
# [public]
|
15
|
-
#
|
16
|
-
def <<(x)
|
17
|
-
@mutex.synchronize do
|
18
|
-
@queue << x
|
19
|
-
@received.signal
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# [public]
|
24
|
-
#
|
25
|
-
def pop(non_block = false)
|
26
|
-
pop_with_timeout(non_block ? 0 : nil)
|
27
|
-
end
|
28
|
-
|
29
|
-
# [public]
|
30
|
-
#
|
31
|
-
def pop_with_timeout(timeout = nil)
|
32
|
-
@mutex.synchronize do
|
33
|
-
if timeout.nil?
|
34
|
-
# wait indefinitely until there is an element in the queue
|
35
|
-
while @queue.empty? && !@closed
|
36
|
-
@received.wait(@mutex)
|
37
|
-
end
|
38
|
-
elsif @queue.empty? && !@closed && timeout != 0
|
39
|
-
# wait for element or timeout
|
40
|
-
timeout_time = timeout + Time.now.to_f
|
41
|
-
while @queue.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
|
42
|
-
@received.wait(@mutex, remaining_time)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
return if @closed
|
46
|
-
# if we're still empty after the timeout, raise exception
|
47
|
-
raise ThreadError, "queue empty" if @queue.empty?
|
48
|
-
@queue.shift
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# [public]
|
53
|
-
#
|
54
|
-
def close
|
55
|
-
@mutex.synchronize do
|
56
|
-
@closed = true
|
57
|
-
@received.broadcast
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|