going 1.0.0 → 1.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/Gemfile +2 -0
- data/README.md +255 -2
- data/lib/going.rb +5 -0
- data/lib/going/channel.rb +16 -0
- data/lib/going/select_helper.rb +1 -1
- data/lib/going/select_statement.rb +4 -5
- data/lib/going/version.rb +1 -1
- data/spec/going/channel_spec.rb +22 -0
- data/spec/going/select_statement_spec.rb +8 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d24f3e696f0f5be0f6cd9fb1267f32f9f5ed2df
|
4
|
+
data.tar.gz: 2504de315ea4e0db31e5772152a495a870fadb34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11e3bd89f64c0bbf2312bb1cbc80ee31145f2619b50f934e0fbcee30147dcdbd8b70d70e163bcf4e906e5a70bda53bc504795b43d86def996e54effc1cbd5c3c
|
7
|
+
data.tar.gz: 6c1f571f54a90a484be36d8d03936de988b836a4b9d18f3aca081dbb9f40a38e33784173f3e00ef9f234bd82ca4b08c561e7893ef7cddec34e49f1357567d460
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Going [](https://travis-ci.org/jridgewell/Going)
|
2
2
|
|
3
|
-
Go
|
3
|
+
A Ruby implementation of Go Channels.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -20,7 +20,260 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
|
23
|
+
Wording stolen from the [Go Language
|
24
|
+
Specification](https://golang.org/ref/spec) and [Effective Go
|
25
|
+
Document](https://golang.org/doc/effective_go.html), and converted over
|
26
|
+
into the equivalent Ruby code.
|
27
|
+
|
28
|
+
### Channels
|
29
|
+
|
30
|
+
Unbuffered channels combine communication — the exchange of a value —
|
31
|
+
with synchronization — guaranteeing that two calculations ("goroutines",
|
32
|
+
or threads) are in a known state.
|
33
|
+
|
34
|
+
There are lots of nice idioms using channels. Here's one to get us
|
35
|
+
started. A channel can allow the launching goroutine to wait for the
|
36
|
+
sort to complete.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
list = [3, 2, 1]
|
40
|
+
c = Going::Channel.new # Allocate a channel.
|
41
|
+
|
42
|
+
# Start the sort in a goroutine; when it completes, signal on the channel.
|
43
|
+
Going.go do
|
44
|
+
list.sort!
|
45
|
+
c.push 1 # Send a signal; value does not matter.
|
46
|
+
end
|
47
|
+
|
48
|
+
# doSomethingForAWhile
|
49
|
+
c.receive # Wait for sort to finish; discard sent value.
|
50
|
+
```
|
51
|
+
|
52
|
+
Receivers always block until there is data to receive. If the channel is
|
53
|
+
unbuffered, the sender blocks until the receiver has received the value.
|
54
|
+
If the channel has a buffer, the sender blocks only until the value has
|
55
|
+
been copied to the buffer; if the buffer is full, this means waiting
|
56
|
+
until some receiver has retrieved a value.
|
57
|
+
|
58
|
+
A buffered channel can be used like a semaphore, for instance to limit
|
59
|
+
throughput. In this example, incoming requests are passed to `handle`,
|
60
|
+
which sends a value into the channel, processes the request, and then
|
61
|
+
receives a value from the channel to ready the "semaphore" for the next
|
62
|
+
consumer. The capacity of the channel buffer limits the number of
|
63
|
+
simultaneous calls to process.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
sem = Going::Channel.new(MaxOutstanding)
|
67
|
+
|
68
|
+
def handle(request)
|
69
|
+
sem.push 1 # Wait for active queue to drain.
|
70
|
+
process r # May take a long time.
|
71
|
+
sem.receive # Done; enable next request to run.
|
72
|
+
end
|
73
|
+
|
74
|
+
def serve(request_queue)
|
75
|
+
request_queue.each do |req|
|
76
|
+
Going.go do
|
77
|
+
handle req # Don't wait for handle to finish.
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Once `MaxOutstanding` handlers are executing `process`, any more will
|
84
|
+
block trying to send into the filled channel buffer, until one of the
|
85
|
+
existing handlers finishes and receives from the buffer.
|
86
|
+
|
87
|
+
This design has a problem, though: `serve` creates a new goroutine for
|
88
|
+
every incoming request, even though only `MaxOutstanding` of them can
|
89
|
+
run at any moment. As a result, the program can consume unlimited
|
90
|
+
resources if the requests come in too fast. We can address that
|
91
|
+
deficiency by changing `serve` to gate the creation of the goroutines.
|
92
|
+
Here's an obvious solution.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
def serve(request_queue) {
|
96
|
+
request_queue.each do |req|
|
97
|
+
sem.push 1
|
98
|
+
Going.go do
|
99
|
+
process req
|
100
|
+
sem.receive
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
Going back to the general problem of writing the server, another
|
107
|
+
approach that manages resources well is to start a fixed number of
|
108
|
+
`handle` goroutines all reading from the request channel. The number of
|
109
|
+
goroutines limits the number of simultaneous calls to process. This
|
110
|
+
`serve` function also accepts a channel on which it will be told to
|
111
|
+
exit; after launching the goroutines it blocks receiving from that
|
112
|
+
channel.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
def handle(request_queue)
|
116
|
+
request_queue.each do |req|
|
117
|
+
process req
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def serve(request_queue, quit) {
|
122
|
+
# Start handlers
|
123
|
+
MaxOutstanding.times do
|
124
|
+
Going.go do
|
125
|
+
handle request_queue
|
126
|
+
end
|
127
|
+
end
|
128
|
+
quit.receive # Wait to be told to exit.
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
### Close
|
133
|
+
|
134
|
+
For a channel `ch`, the method `ch.close` records that no more values
|
135
|
+
will be sent on the channel. Sending to a closed channel causes an
|
136
|
+
exception to be thrown. After calling `#close`, and after any previously
|
137
|
+
sent values have been received, receive operations will throw the
|
138
|
+
`:close` symbol.
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
ch = Going::Channel.new 2
|
142
|
+
|
143
|
+
# Push an initial value into the buffered channel
|
144
|
+
ch.push 1
|
145
|
+
|
146
|
+
# Close the channel, preventing any futher message
|
147
|
+
ch.close
|
148
|
+
|
149
|
+
begin
|
150
|
+
ch.push 2
|
151
|
+
rescue
|
152
|
+
# Sending to a closed channel causes an exception
|
153
|
+
end
|
154
|
+
|
155
|
+
# You may receive already sent values
|
156
|
+
ch.receive # => 1
|
157
|
+
|
158
|
+
# Closed channels throw when there are no more messages
|
159
|
+
catch :close do
|
160
|
+
ch.receive
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
### Size
|
165
|
+
|
166
|
+
For a channel `ch`, the method `ch.size` returns the number of completed
|
167
|
+
send operations on the channel. For an unbuffered channel, that number
|
168
|
+
is always 0.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
unbuffered_channel = Going::Channel.new
|
172
|
+
unbuffered_channel.size # => 0
|
173
|
+
|
174
|
+
Going.go do
|
175
|
+
unbuffered_channel.push 'message'
|
176
|
+
end
|
177
|
+
# after the goroutine has blocked on send
|
178
|
+
unbuffered_channel.size # => 0
|
179
|
+
|
180
|
+
|
181
|
+
buffered_channel = Going::Channel.new 2
|
182
|
+
buffered_channel.size # => 0
|
183
|
+
|
184
|
+
buffered_channel.push 'message'
|
185
|
+
buffered_channel.size # => 1
|
186
|
+
|
187
|
+
buffered_channel.push 'message'
|
188
|
+
buffered_channel.size # => 2
|
189
|
+
|
190
|
+
buffered_channel.receive
|
191
|
+
buffered_channel.size # => 1
|
192
|
+
```
|
193
|
+
|
194
|
+
### Capacity
|
195
|
+
|
196
|
+
For a channel `ch`, the method `ch.capacity` returns the buffer size of
|
197
|
+
the channel. For an unbuffered channel, that number is 0.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
unbuffered_channel = Going::Channel.new
|
201
|
+
unbuffered_channel.capacity # => 0
|
202
|
+
|
203
|
+
|
204
|
+
buffered_channel = Going::Channel.new 2
|
205
|
+
buffered_channel.capacity # => 2
|
206
|
+
|
207
|
+
buffered_channel.push 'message'
|
208
|
+
buffered_channel.capacity # => 2
|
209
|
+
```
|
210
|
+
|
211
|
+
### Select Statements
|
212
|
+
|
213
|
+
A "select" statement chooses which of a set of possible send or receive
|
214
|
+
operations will proceed. It acts similar to a "case" statement but
|
215
|
+
with the cases all referring to communication operations.
|
216
|
+
|
217
|
+
Execution of a "select" statement proceeds in several steps:
|
218
|
+
|
219
|
+
1. For all the cases in the statement, the channel operands of receive
|
220
|
+
operations and the channel and right-hand-side expressions of send
|
221
|
+
statements are evaluated exactly once, in source order, upon entering
|
222
|
+
the "select" statement. The result is a set of channels to receive
|
223
|
+
from or send to, and the corresponding values to send. Any side
|
224
|
+
effects in that evaluation will occur irrespective of which (if any)
|
225
|
+
communication operation is selected to proceed. Expressions on the
|
226
|
+
left-hand side of a receive statement with a variable assignment are
|
227
|
+
not evaluated.
|
228
|
+
|
229
|
+
2. If one or more of the communications can proceed, a single one that
|
230
|
+
can proceed is chosen in source order. Otherwise, if there is a
|
231
|
+
default case, that case is chosen. If there is no default case, the
|
232
|
+
"select" statement blocks until at least one of the communications
|
233
|
+
can proceed.
|
234
|
+
|
235
|
+
3. Unless the selected case is the default case, the respective
|
236
|
+
communication operation is executed.
|
237
|
+
|
238
|
+
4. If the selected case is a receive statement with a variable
|
239
|
+
assignment, the corresponding block is executed with the received
|
240
|
+
message as the first parameter. A second, optional, hash is
|
241
|
+
also passed, with a single key `ok`. `ok` will equal `true` if the
|
242
|
+
channel is not closed, or `false` if the channel is closed.
|
243
|
+
|
244
|
+
5. If the selected case is a send statement, the corresponding block is
|
245
|
+
executed.
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
Going.select do
|
249
|
+
channel.receive { |msg|
|
250
|
+
# do something with `msg`.
|
251
|
+
}
|
252
|
+
|
253
|
+
channel2.push(1) {
|
254
|
+
# do something after pushing
|
255
|
+
}
|
256
|
+
|
257
|
+
channel3.receive { |msg, ok: true|
|
258
|
+
if ok
|
259
|
+
# do something with msg
|
260
|
+
else
|
261
|
+
# channel3 was closed, msg is `nil`
|
262
|
+
end
|
263
|
+
}
|
264
|
+
|
265
|
+
timeout(5) {
|
266
|
+
# 5 second passed and no channel operations succeeded.
|
267
|
+
}
|
268
|
+
|
269
|
+
default {
|
270
|
+
# An immediately executing block, if nothing has succeeded yet
|
271
|
+
}
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
|
276
|
+
## Obligatory Sieve of Eratosthenes Example
|
24
277
|
|
25
278
|
```ruby
|
26
279
|
require 'going'
|
data/lib/going.rb
CHANGED
data/lib/going/channel.rb
CHANGED
@@ -124,6 +124,22 @@ module Going
|
|
124
124
|
size == 0
|
125
125
|
end
|
126
126
|
|
127
|
+
#
|
128
|
+
# Calls the given block once for each message until the channel is closed,
|
129
|
+
# passing that message as a parameter.
|
130
|
+
#
|
131
|
+
# Note that this is a destructive action, since each message is `shift`ed.
|
132
|
+
#
|
133
|
+
def each
|
134
|
+
return enum_for(:each) unless block_given?
|
135
|
+
|
136
|
+
catch :close do
|
137
|
+
loop do
|
138
|
+
yield self.shift
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
127
143
|
def inspect
|
128
144
|
inspection = [:capacity, :size].map do |attr|
|
129
145
|
"#{attr}: #{send(attr).inspect}"
|
data/lib/going/select_helper.rb
CHANGED
@@ -52,7 +52,6 @@ module Going
|
|
52
52
|
|
53
53
|
wait
|
54
54
|
cleanup
|
55
|
-
call_completion_block
|
56
55
|
end
|
57
56
|
|
58
57
|
def when_complete(*args, &callback)
|
@@ -88,6 +87,10 @@ module Going
|
|
88
87
|
end
|
89
88
|
end
|
90
89
|
|
90
|
+
def call_completion_block
|
91
|
+
on_complete.call(*args) if on_complete
|
92
|
+
end
|
93
|
+
|
91
94
|
private
|
92
95
|
|
93
96
|
attr_reader :semaphore, :once_mutex, :complete_mutex, :when_completes
|
@@ -113,9 +116,5 @@ module Going
|
|
113
116
|
def cleanup
|
114
117
|
when_completes.each(&:call)
|
115
118
|
end
|
116
|
-
|
117
|
-
def call_completion_block
|
118
|
-
on_complete.call(*args) if on_complete
|
119
|
-
end
|
120
119
|
end
|
121
120
|
end
|
data/lib/going/version.rb
CHANGED
data/spec/going/channel_spec.rb
CHANGED
@@ -285,4 +285,26 @@ describe Going::Channel do
|
|
285
285
|
end
|
286
286
|
end
|
287
287
|
end
|
288
|
+
|
289
|
+
describe '#each' do
|
290
|
+
it 'yields for each message until channel is closed' do
|
291
|
+
Going.go do
|
292
|
+
10.times do |i|
|
293
|
+
channel.push i
|
294
|
+
end
|
295
|
+
channel.close
|
296
|
+
end
|
297
|
+
|
298
|
+
i = 0
|
299
|
+
channel.each do |message|
|
300
|
+
expect(message).to eq(i)
|
301
|
+
i += 1
|
302
|
+
end
|
303
|
+
expect(i).to eq(10)
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'returns an enumerator if no block is given' do
|
307
|
+
expect(channel.each).to be_an Enumerator
|
308
|
+
end
|
309
|
+
end
|
288
310
|
end
|
@@ -102,6 +102,14 @@ describe Going::SelectStatement do
|
|
102
102
|
expect(buffered_channel.size).to eq(0)
|
103
103
|
end
|
104
104
|
|
105
|
+
it 'calls succeeding block after the select_statement has been evaluated' do
|
106
|
+
Going.select do |s|
|
107
|
+
s.default do
|
108
|
+
expect(Going::SelectStatement.instance).to be_nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
105
113
|
context 'buffered channels' do
|
106
114
|
it 'succeeds when blocked push is now under capacity' do
|
107
115
|
buffered_channel.push 1
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: going
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Ridgewell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|