goru 0.0.0 → 0.1.0
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/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
|