going 0.0.3 → 1.0.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/.travis.yml +3 -4
- data/lib/going/boolean_attr_reader.rb +11 -0
- data/lib/going/channel.rb +97 -47
- data/lib/going/nil_select_statement.rb +27 -0
- data/lib/going/operation/push.rb +15 -0
- data/lib/going/operation/shift.rb +31 -0
- data/lib/going/operation.rb +52 -0
- data/lib/going/select_helper.rb +30 -0
- data/lib/going/select_statement.rb +121 -0
- data/lib/going/version.rb +1 -1
- data/lib/going.rb +22 -0
- data/spec/going/channel_spec.rb +121 -47
- data/spec/going/select_statement_spec.rb +364 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/spy.rb +22 -0
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e654db8fb9029e5b32303e284551f9182960c418
|
4
|
+
data.tar.gz: 62605bb00f54efe7dc250850f1341f10991cecb5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed12b04c91fd266ef44da9ef91b36f62b117d7cb869d3a8af21f831e6dfee4cc2c361aaa6e768d90f264e1cc2c97f3ae256a3445c19a3313d90966384fa16918
|
7
|
+
data.tar.gz: d8a8b37ecc0f747f322ca779e08d59e8683dd62c11f873862ba80d7211de40f9667acfef1310b4206dd20ed1fda8ec5cbf096ac59e4c7ce9ea0f96cce60876c8
|
data/.travis.yml
CHANGED
data/lib/going/channel.rb
CHANGED
@@ -1,24 +1,24 @@
|
|
1
|
-
require 'going'
|
2
|
-
|
3
1
|
module Going
|
4
2
|
#
|
5
3
|
# This class represents message channels of specified capacity.
|
6
4
|
# The push operation may be blocked if the capacity is full.
|
7
|
-
# The
|
5
|
+
# The shift operation may be blocked if no messages have been sent.
|
8
6
|
#
|
9
7
|
class Channel
|
10
|
-
extend
|
8
|
+
extend BooleanAttrReader
|
11
9
|
|
12
10
|
#
|
13
11
|
# Creates a fixed-length channel with a capacity of +capacity+.
|
14
12
|
#
|
15
13
|
def initialize(capacity = 0)
|
16
|
-
fail ArgumentError, 'channel capacity must be 0 or greater'
|
14
|
+
fail ArgumentError, 'channel capacity must be 0 or greater' if capacity < 0
|
17
15
|
@capacity = capacity
|
16
|
+
|
17
|
+
@pushes = []
|
18
|
+
@shifts = []
|
19
|
+
|
18
20
|
@closed = false
|
19
21
|
@mutex = Mutex.new
|
20
|
-
@push_semaphore = ConditionVariable.new
|
21
|
-
@pop_semaphore = ConditionVariable.new
|
22
22
|
|
23
23
|
yield self if block_given?
|
24
24
|
end
|
@@ -31,9 +31,7 @@ module Going
|
|
31
31
|
#
|
32
32
|
# Returns whether or not the channel is closed.
|
33
33
|
#
|
34
|
-
|
35
|
-
@closed
|
36
|
-
end
|
34
|
+
battr_reader :closed
|
37
35
|
|
38
36
|
#
|
39
37
|
# Closes the channel. Any data in the buffer may still be retrieved.
|
@@ -41,23 +39,33 @@ module Going
|
|
41
39
|
def close
|
42
40
|
synchronize do
|
43
41
|
return false if closed?
|
44
|
-
|
45
|
-
|
42
|
+
|
43
|
+
shifts.each(&:close).clear
|
44
|
+
pushes_over_capacity!.each(&:close)
|
46
45
|
@closed = true
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
50
49
|
#
|
51
50
|
# Pushes +obj+ to the channel. If the channel is already full, waits
|
52
|
-
# until a thread
|
51
|
+
# until a thread shifts from it.
|
53
52
|
#
|
54
|
-
def push(obj)
|
53
|
+
def push(obj, &on_complete)
|
55
54
|
synchronize do
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
55
|
+
push = Push.new(message: obj, select_statement: select_statement, &on_complete)
|
56
|
+
pushes << push
|
57
|
+
|
58
|
+
pair_with_shift push
|
59
|
+
|
60
|
+
select_statement.when_complete(push, pushes, &method(:remove_operation)) if select_statement?
|
61
|
+
|
62
|
+
push.complete if under_capacity?
|
63
|
+
push.signal if select_statement?
|
64
|
+
push.close if closed?
|
65
|
+
|
66
|
+
push.wait(mutex)
|
67
|
+
|
68
|
+
fail 'cannot push to a closed channel' if closed? && !select_statement?
|
61
69
|
self
|
62
70
|
end
|
63
71
|
end
|
@@ -72,34 +80,52 @@ module Going
|
|
72
80
|
# Receives data from the channel. If the channel is already empty,
|
73
81
|
# waits until a thread pushes to it.
|
74
82
|
#
|
75
|
-
def
|
83
|
+
def shift(&on_complete)
|
76
84
|
synchronize do
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
85
|
+
shift = Shift.new(select_statement: select_statement, &on_complete)
|
86
|
+
shifts << shift
|
87
|
+
|
88
|
+
pair_with_push shift
|
89
|
+
|
90
|
+
select_statement.when_complete(shift, shifts, &method(:remove_operation)) if select_statement?
|
91
|
+
|
92
|
+
shift.signal if select_statement?
|
93
|
+
shift.close if closed?
|
94
|
+
|
95
|
+
shift.wait(mutex)
|
96
|
+
|
97
|
+
throw :close if closed? && !select_statement? && shift.incomplete?
|
98
|
+
shift.message
|
82
99
|
end
|
83
100
|
end
|
84
101
|
|
85
102
|
#
|
86
|
-
# Alias of
|
103
|
+
# Alias of shift
|
87
104
|
#
|
88
|
-
alias_method :receive, :
|
89
|
-
alias_method :next, :
|
105
|
+
alias_method :receive, :shift
|
106
|
+
alias_method :next, :shift
|
90
107
|
|
91
108
|
#
|
92
|
-
#
|
109
|
+
# Returns the number of messages in the channel
|
93
110
|
#
|
94
|
-
|
111
|
+
def size
|
112
|
+
[capacity, pushes.size].min
|
113
|
+
end
|
95
114
|
|
96
115
|
#
|
97
116
|
# Alias of size
|
98
117
|
#
|
99
118
|
alias_method :length, :size
|
100
119
|
|
120
|
+
#
|
121
|
+
# Returns whether the channel is empty.
|
122
|
+
#
|
123
|
+
def empty?
|
124
|
+
size == 0
|
125
|
+
end
|
126
|
+
|
101
127
|
def inspect
|
102
|
-
inspection = [:capacity, :
|
128
|
+
inspection = [:capacity, :size].map do |attr|
|
103
129
|
"#{attr}: #{send(attr).inspect}"
|
104
130
|
end
|
105
131
|
"#<#{self.class} #{inspection.join(', ')}>"
|
@@ -107,35 +133,59 @@ module Going
|
|
107
133
|
|
108
134
|
private
|
109
135
|
|
110
|
-
|
136
|
+
attr_reader :mutex, :pushes, :shifts
|
137
|
+
|
138
|
+
def synchronize(&blk)
|
139
|
+
mutex.synchronize(&blk)
|
140
|
+
end
|
111
141
|
|
112
|
-
def
|
113
|
-
|
142
|
+
def pair_with_push(shift)
|
143
|
+
pushes.each_with_index.any? do |push, index|
|
144
|
+
if push.select_statement != select_statement && shift.complete(push)
|
145
|
+
complete_next_push_now_that_channel_under_capacity
|
146
|
+
shifts.pop
|
147
|
+
pushes.delete_at index
|
148
|
+
true
|
149
|
+
end
|
150
|
+
end
|
114
151
|
end
|
115
152
|
|
116
|
-
def
|
117
|
-
|
153
|
+
def pair_with_shift(push)
|
154
|
+
shifts.each_with_index.any? do |shift, index|
|
155
|
+
if shift.select_statement != select_statement && shift.complete(push)
|
156
|
+
pushes.pop
|
157
|
+
shifts.delete_at index
|
158
|
+
true
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def remove_operation(operation, queue)
|
164
|
+
synchronize do
|
165
|
+
index = queue.index(operation)
|
166
|
+
queue.delete_at index if index
|
167
|
+
end
|
118
168
|
end
|
119
169
|
|
120
|
-
def
|
121
|
-
|
170
|
+
def complete_next_push_now_that_channel_under_capacity
|
171
|
+
push = pushes[capacity]
|
172
|
+
push.complete if push && push.incomplete?
|
122
173
|
end
|
123
174
|
|
124
|
-
def
|
125
|
-
|
175
|
+
def pushes_over_capacity!
|
176
|
+
pushes.slice!(capacity, pushes.size) || []
|
126
177
|
end
|
127
178
|
|
128
|
-
def
|
129
|
-
|
179
|
+
def under_capacity?
|
180
|
+
pushes.size <= capacity
|
130
181
|
end
|
131
182
|
|
132
|
-
def
|
133
|
-
|
134
|
-
@pop_semaphore.broadcast
|
183
|
+
def select_statement
|
184
|
+
SelectStatement.instance || NilSelectStatement.instance
|
135
185
|
end
|
136
186
|
|
137
|
-
def
|
138
|
-
|
187
|
+
def select_statement?
|
188
|
+
SelectStatement.instance?
|
139
189
|
end
|
140
190
|
end
|
141
191
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Going
|
2
|
+
class NilSelectStatement
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def !=(other)
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
def !
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def nil?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def once
|
18
|
+
yield
|
19
|
+
end
|
20
|
+
|
21
|
+
def complete(*args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def secondary_complete(*args)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Going
|
2
|
+
class Shift < Operation
|
3
|
+
def complete(push)
|
4
|
+
select_statement.once do
|
5
|
+
push.select_statement.once do
|
6
|
+
self.message = push.message
|
7
|
+
|
8
|
+
super()
|
9
|
+
push.complete
|
10
|
+
notify_select_statement
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def close
|
17
|
+
super
|
18
|
+
notify_select_statement
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def notify_select_statement
|
24
|
+
select_statement.complete(message, ok: ok?, &on_complete)
|
25
|
+
end
|
26
|
+
|
27
|
+
def ok?
|
28
|
+
!closed?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Going
|
2
|
+
class Operation
|
3
|
+
extend BooleanAttrReader
|
4
|
+
|
5
|
+
attr_accessor :message
|
6
|
+
attr_reader :select_statement
|
7
|
+
|
8
|
+
def initialize(opts = {}, &on_complete)
|
9
|
+
@message = opts[:message]
|
10
|
+
@select_statement = opts[:select_statement]
|
11
|
+
@on_complete = on_complete
|
12
|
+
|
13
|
+
@completed = false
|
14
|
+
@closed = false
|
15
|
+
@signaled = false
|
16
|
+
|
17
|
+
@semaphore = ConditionVariable.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def wait(mutex)
|
21
|
+
semaphore.wait(mutex) until wake?
|
22
|
+
end
|
23
|
+
|
24
|
+
def signal
|
25
|
+
@signaled = true
|
26
|
+
semaphore.signal
|
27
|
+
end
|
28
|
+
|
29
|
+
def complete
|
30
|
+
@completed = true
|
31
|
+
signal
|
32
|
+
end
|
33
|
+
|
34
|
+
def close
|
35
|
+
@closed = true
|
36
|
+
signal
|
37
|
+
end
|
38
|
+
|
39
|
+
def incomplete?
|
40
|
+
!completed?
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def wake?
|
46
|
+
signaled? || completed? || closed?
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :semaphore, :on_complete
|
50
|
+
battr_reader :signaled, :completed, :closed, :select_statement
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Going
|
2
|
+
#
|
3
|
+
# Helper methods to emulate Go's Select Cases.
|
4
|
+
#
|
5
|
+
class SelectHelper
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
#
|
9
|
+
# A case statement that will succeed immediately.
|
10
|
+
#
|
11
|
+
def default(&blk)
|
12
|
+
Channel.new(1) do |ch|
|
13
|
+
ch.push(nil, &blk)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# A case statement that will succeed after +seconds+ seconds.
|
19
|
+
#
|
20
|
+
def timeout(seconds, &blk)
|
21
|
+
Channel.new do |ch|
|
22
|
+
Going.go do
|
23
|
+
sleep seconds
|
24
|
+
ch.receive
|
25
|
+
end
|
26
|
+
ch.push(nil, &blk)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Going
|
2
|
+
|
3
|
+
class SelectStatement
|
4
|
+
extend BooleanAttrReader
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def instance
|
8
|
+
Thread.current[global_key]
|
9
|
+
end
|
10
|
+
|
11
|
+
def instance?
|
12
|
+
!instance.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def new_instance
|
16
|
+
self.instance = new
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset
|
20
|
+
self.instance = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def instance=(select_statement)
|
26
|
+
Thread.current[global_key] = select_statement
|
27
|
+
end
|
28
|
+
|
29
|
+
def global_key
|
30
|
+
@global_key ||= "Going_#{Going::SelectStatement.object_id}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@completed = false
|
36
|
+
@once_mutex = Mutex.new
|
37
|
+
@complete_mutex = Mutex.new
|
38
|
+
@semaphore = ConditionVariable.new
|
39
|
+
@when_completes = []
|
40
|
+
|
41
|
+
@args = nil
|
42
|
+
@on_complete = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def select(&blk)
|
46
|
+
select_helper = SelectHelper.instance
|
47
|
+
if blk.arity == 1
|
48
|
+
yield select_helper
|
49
|
+
else
|
50
|
+
select_helper.instance_eval(&blk)
|
51
|
+
end
|
52
|
+
|
53
|
+
wait
|
54
|
+
cleanup
|
55
|
+
call_completion_block
|
56
|
+
end
|
57
|
+
|
58
|
+
def when_complete(*args, &callback)
|
59
|
+
when_completes << proc { callback.call(*args) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def complete(*args, &on_complete)
|
63
|
+
complete_mutex.synchronize do
|
64
|
+
if !completed?
|
65
|
+
@args = args
|
66
|
+
@on_complete = on_complete
|
67
|
+
@completed = true
|
68
|
+
@secondary_completed = true
|
69
|
+
semaphore.signal
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def secondary_complete(*args, &on_complete)
|
75
|
+
complete_mutex.synchronize do
|
76
|
+
if !secondary_completed?
|
77
|
+
@args = args
|
78
|
+
@on_complete = on_complete
|
79
|
+
@secondary_completed = true
|
80
|
+
semaphore.signal
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def once(*args, &blk)
|
86
|
+
once_mutex.synchronize do
|
87
|
+
yield(*args) if block_given? && incomplete?
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
attr_reader :semaphore, :once_mutex, :complete_mutex, :when_completes
|
94
|
+
attr_reader :on_complete, :args
|
95
|
+
battr_reader :completed, :secondary_completed
|
96
|
+
|
97
|
+
def wait
|
98
|
+
complete_mutex.synchronize do
|
99
|
+
semaphore.wait(complete_mutex) until wake?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def incomplete?
|
104
|
+
complete_mutex.synchronize do
|
105
|
+
!completed?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def wake?
|
110
|
+
completed? || secondary_completed?
|
111
|
+
end
|
112
|
+
|
113
|
+
def cleanup
|
114
|
+
when_completes.each(&:call)
|
115
|
+
end
|
116
|
+
|
117
|
+
def call_completion_block
|
118
|
+
on_complete.call(*args) if on_complete
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/going/version.rb
CHANGED
data/lib/going.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
require 'going/boolean_attr_reader'
|
2
5
|
require 'going/channel'
|
6
|
+
require 'going/select_statement'
|
7
|
+
require 'going/nil_select_statement'
|
8
|
+
require 'going/select_helper'
|
9
|
+
require 'going/operation'
|
10
|
+
require 'going/operation/shift'
|
11
|
+
require 'going/operation/push'
|
3
12
|
require 'going/version'
|
4
13
|
|
5
14
|
module Going
|
@@ -9,4 +18,17 @@ module Going
|
|
9
18
|
def self.go(*args, &blk)
|
10
19
|
Thread.new(*args, &blk)
|
11
20
|
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Creates a synchronous block that will select the first
|
24
|
+
# channel operation to complete. Only one operation inside
|
25
|
+
# the block will complete and any operations that are
|
26
|
+
# incomplete will be removed afterwards.
|
27
|
+
#
|
28
|
+
def self.select(&blk)
|
29
|
+
fail 'a block must be passed' unless block_given?
|
30
|
+
select = SelectStatement.new_instance
|
31
|
+
select.select(&blk)
|
32
|
+
SelectStatement.reset
|
33
|
+
end
|
12
34
|
end
|
data/spec/going/channel_spec.rb
CHANGED
@@ -1,9 +1,22 @@
|
|
1
|
+
require 'going'
|
2
|
+
|
1
3
|
describe Going::Channel do
|
2
4
|
subject(:channel) { Going::Channel.new }
|
5
|
+
let(:buffered_channel) { Going::Channel.new 1 }
|
6
|
+
|
3
7
|
def elapsed_time(original_time)
|
4
8
|
(Time.now - original_time)
|
5
9
|
end
|
6
10
|
|
11
|
+
def sleeper(channel, queue, size)
|
12
|
+
fail "channel does not respond to #{queue}" unless channel.respond_to? queue, true
|
13
|
+
begin
|
14
|
+
Thread.pass
|
15
|
+
sleep 0.1
|
16
|
+
end until channel.send(queue).size == size
|
17
|
+
expect(channel.send(queue).size).to eq(size)
|
18
|
+
end
|
19
|
+
|
7
20
|
describe '.new' do
|
8
21
|
it 'defaults capacity to 0' do
|
9
22
|
expect(channel.capacity).to eq(0)
|
@@ -48,36 +61,35 @@ describe Going::Channel do
|
|
48
61
|
|
49
62
|
it 'will wake a blocked push' do
|
50
63
|
Going.go do
|
51
|
-
|
64
|
+
sleeper channel, :pushes, 1
|
52
65
|
channel.close
|
53
66
|
end
|
54
|
-
expect { channel.push 1 }.to
|
67
|
+
expect { channel.push 1 }.to raise_error
|
55
68
|
end
|
56
69
|
|
57
|
-
it 'will wake a blocked
|
70
|
+
it 'will wake a blocked shift' do
|
58
71
|
Going.go do
|
59
|
-
|
72
|
+
sleeper channel, :shifts, 1
|
60
73
|
channel.close
|
61
74
|
end
|
62
75
|
expect { channel.receive }.to throw_symbol(:close)
|
63
76
|
end
|
64
77
|
|
65
78
|
it 'will reject all but the first #capacity pushes' do
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
79
|
+
begin
|
80
|
+
channel = Going::Channel.new 2
|
81
|
+
Going.go do
|
82
|
+
sleeper channel, :pushes, 3
|
83
|
+
channel.close
|
84
|
+
end
|
72
85
|
3.times { |i| channel.push i }
|
86
|
+
rescue
|
87
|
+
expect(channel.size).to eq(2)
|
73
88
|
end
|
74
|
-
expect(channel.size).to eq(2)
|
75
89
|
end
|
76
90
|
end
|
77
91
|
|
78
92
|
describe '#push' do
|
79
|
-
subject(:channel) { Going::Channel.new 1 }
|
80
|
-
|
81
93
|
it 'is aliased as #<<' do
|
82
94
|
expect(channel.method(:<<)).to eq(channel.method(:push))
|
83
95
|
end
|
@@ -93,13 +105,13 @@ describe Going::Channel do
|
|
93
105
|
|
94
106
|
it 'will not block if channel is under capacity' do
|
95
107
|
now = Time.now
|
96
|
-
|
108
|
+
buffered_channel.push 1
|
97
109
|
expect(elapsed_time(now)).to be < 0.2
|
98
110
|
end
|
99
111
|
|
100
112
|
it 'will block if channel is over capacity' do
|
101
|
-
channel.push 1
|
102
113
|
Going.go do
|
114
|
+
sleeper channel, :pushes, 1
|
103
115
|
sleep 0.25
|
104
116
|
channel.receive
|
105
117
|
end
|
@@ -109,50 +121,73 @@ describe Going::Channel do
|
|
109
121
|
end
|
110
122
|
|
111
123
|
it 'will push messages in order' do
|
112
|
-
|
124
|
+
buffered_channel.push 1
|
113
125
|
Going.go do
|
114
|
-
|
126
|
+
sleeper buffered_channel, :pushes, 1
|
127
|
+
buffered_channel.push 2
|
115
128
|
end
|
116
129
|
Going.go do
|
117
|
-
|
118
|
-
|
130
|
+
sleeper buffered_channel, :pushes, 2
|
131
|
+
buffered_channel.push 3
|
119
132
|
end
|
120
|
-
|
133
|
+
|
134
|
+
sleeper buffered_channel, :pushes, 3
|
121
135
|
1.upto(3).each do |i|
|
122
|
-
expect(
|
136
|
+
expect(buffered_channel.receive).to eq(i)
|
123
137
|
end
|
124
138
|
end
|
125
139
|
|
126
140
|
it 'returns the channel' do
|
127
|
-
expect(
|
141
|
+
expect(buffered_channel.push(1)).to be(buffered_channel)
|
128
142
|
end
|
129
|
-
end
|
130
143
|
|
131
|
-
|
132
|
-
|
144
|
+
context 'when a shift is from a select_statement' do
|
145
|
+
context 'when select_statement is already completed' do
|
146
|
+
it 'attempts to complete with next shift' do
|
147
|
+
i = nil
|
148
|
+
Going.select do |s|
|
149
|
+
channel.receive
|
150
|
+
s.default
|
151
|
+
th = Going.go do
|
152
|
+
i = channel.receive
|
153
|
+
end
|
154
|
+
Going.go do
|
155
|
+
sleeper channel, :shifts, 2
|
156
|
+
channel.push 1
|
157
|
+
th.join
|
158
|
+
end.join
|
159
|
+
end
|
160
|
+
|
161
|
+
expect(i).to be(1)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
133
166
|
|
167
|
+
describe '#shift' do
|
134
168
|
it 'is aliased as #receive' do
|
135
|
-
expect(channel.method(:receive)).to eq(channel.method(:
|
169
|
+
expect(channel.method(:receive)).to eq(channel.method(:shift))
|
136
170
|
end
|
137
171
|
|
138
172
|
it 'is aliased as #next' do
|
139
|
-
expect(channel.method(:next)).to eq(channel.method(:
|
173
|
+
expect(channel.method(:next)).to eq(channel.method(:shift))
|
140
174
|
end
|
141
175
|
|
142
176
|
it 'returns the next message' do
|
143
|
-
|
144
|
-
expect(
|
177
|
+
buffered_channel.push 1
|
178
|
+
expect(buffered_channel.receive).to eq(1)
|
145
179
|
end
|
146
180
|
|
147
181
|
it 'will not block if channel is not empty' do
|
148
|
-
|
182
|
+
buffered_channel.push 1
|
149
183
|
now = Time.now
|
150
|
-
|
184
|
+
buffered_channel.receive
|
151
185
|
expect(elapsed_time(now)).to be < 0.2
|
152
186
|
end
|
153
187
|
|
154
188
|
it 'will block if channel is empty' do
|
155
189
|
Going.go do
|
190
|
+
sleeper channel, :shifts, 1
|
156
191
|
sleep 0.25
|
157
192
|
channel.push 1
|
158
193
|
end
|
@@ -161,16 +196,39 @@ describe Going::Channel do
|
|
161
196
|
expect(elapsed_time(now)).to be > 0.2
|
162
197
|
end
|
163
198
|
|
164
|
-
|
165
|
-
|
166
|
-
|
199
|
+
context 'when closed' do
|
200
|
+
it 'returns next message if any' do
|
201
|
+
buffered_channel.push 1
|
202
|
+
buffered_channel.close
|
203
|
+
expect(buffered_channel.receive).to eq(1)
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'throws :close if no messages' do
|
207
|
+
channel.close
|
208
|
+
expect { channel.receive }.to throw_symbol(:close)
|
209
|
+
end
|
167
210
|
end
|
168
211
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
212
|
+
context 'when a push is from a select_statement' do
|
213
|
+
context 'when select_statement is already completed' do
|
214
|
+
it 'attempts to complete with next push' do
|
215
|
+
i = nil
|
216
|
+
|
217
|
+
Going.select do |s|
|
218
|
+
channel.push 1
|
219
|
+
s.default
|
220
|
+
Going.go do
|
221
|
+
channel.push 2
|
222
|
+
end
|
223
|
+
Going.go do
|
224
|
+
sleeper channel, :pushes, 2
|
225
|
+
i = channel.receive
|
226
|
+
end.join
|
227
|
+
end
|
228
|
+
|
229
|
+
expect(i).to be(2)
|
230
|
+
end
|
231
|
+
end
|
174
232
|
end
|
175
233
|
end
|
176
234
|
|
@@ -191,23 +249,39 @@ describe Going::Channel do
|
|
191
249
|
channel.receive
|
192
250
|
expect(channel.size).to eq(0)
|
193
251
|
end
|
252
|
+
|
253
|
+
it 'returns 0 for unbuffered channel' do
|
254
|
+
Going.go do
|
255
|
+
channel.push 1
|
256
|
+
end
|
257
|
+
sleeper channel, :pushes, 1
|
258
|
+
expect(channel.size).to eq(0)
|
259
|
+
end
|
194
260
|
end
|
195
261
|
|
196
262
|
describe '#empty?' do
|
197
|
-
|
198
|
-
|
263
|
+
context 'when capacity is 0' do
|
264
|
+
it 'returns true when no messages in channel' do
|
265
|
+
expect(channel).to be_empty
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'returns true even if blocked pushes' do
|
269
|
+
Going.go do
|
270
|
+
channel.push 1
|
271
|
+
end
|
272
|
+
sleeper channel, :pushes, 1
|
273
|
+
expect(channel).to be_empty
|
274
|
+
end
|
199
275
|
end
|
200
276
|
|
201
277
|
context 'when capacity is greater than 0' do
|
202
|
-
|
203
|
-
|
204
|
-
it 'returns true when messages in channel' do
|
205
|
-
expect(channel).to be_empty
|
278
|
+
it 'returns true when no messages in channel' do
|
279
|
+
expect(buffered_channel).to be_empty
|
206
280
|
end
|
207
281
|
|
208
282
|
it 'returns false when messages in channel' do
|
209
|
-
|
210
|
-
expect(
|
283
|
+
buffered_channel.push 1
|
284
|
+
expect(buffered_channel).not_to be_empty
|
211
285
|
end
|
212
286
|
end
|
213
287
|
end
|
@@ -0,0 +1,364 @@
|
|
1
|
+
require 'going'
|
2
|
+
|
3
|
+
describe Going::SelectStatement do
|
4
|
+
let(:channel) { Going::Channel.new }
|
5
|
+
let(:buffered_channel) { Going::Channel.new 1 }
|
6
|
+
let(:spy) { Spy.new }
|
7
|
+
let(:dont_call) { Spy.new }
|
8
|
+
|
9
|
+
def elapsed_time(original_time)
|
10
|
+
(Time.now - original_time)
|
11
|
+
end
|
12
|
+
|
13
|
+
def sleeper(channel, queue, size)
|
14
|
+
fail "channel does not respond to #{queue}" unless channel.respond_to? queue, true
|
15
|
+
begin
|
16
|
+
Thread.pass
|
17
|
+
sleep 0.1
|
18
|
+
end until channel.send(queue).size == size
|
19
|
+
expect(channel.send(queue).size).to eq(size)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'Going.select' do
|
23
|
+
it 'blocks until a channel operation succeeds' do
|
24
|
+
now = Time.now
|
25
|
+
Going.select do |s|
|
26
|
+
channel.receive
|
27
|
+
|
28
|
+
Going.go do
|
29
|
+
sleep 0.25
|
30
|
+
channel.push 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
expect(elapsed_time(now)).to be > 0.2
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'does not block if a channel operation immediately succeeds' do
|
37
|
+
now = Time.now
|
38
|
+
Going.select do |s|
|
39
|
+
buffered_channel.push 1
|
40
|
+
end
|
41
|
+
expect(elapsed_time(now)).to be < 0.2
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'calls block on operation that succeeds' do
|
45
|
+
third_channel = Going::Channel.new 1
|
46
|
+
Going.select do |s|
|
47
|
+
channel.receive(&dont_call)
|
48
|
+
buffered_channel.push(1, &spy)
|
49
|
+
third_channel.push(2, &dont_call)
|
50
|
+
end
|
51
|
+
expect(spy).to be_called
|
52
|
+
expect(dont_call).not_to be_called
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'does not call other blocks' do
|
56
|
+
Going.select do |s|
|
57
|
+
channel.push(1, &dont_call)
|
58
|
+
buffered_channel.push 2
|
59
|
+
Going.go do
|
60
|
+
channel.receive
|
61
|
+
end
|
62
|
+
end
|
63
|
+
sleeper channel, :shifts, 1
|
64
|
+
expect(dont_call).not_to be_called
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'can not succeed from own operations' do
|
68
|
+
now = Time.now
|
69
|
+
Going.select do |s|
|
70
|
+
channel.push(1, &dont_call)
|
71
|
+
channel.receive(&spy)
|
72
|
+
channel.push(2, &dont_call)
|
73
|
+
Going.go do
|
74
|
+
sleep 0.25
|
75
|
+
channel.push(3)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
expect(elapsed_time(now)).to be > 0.2
|
79
|
+
expect(spy.args.first).to eq(3)
|
80
|
+
expect(dont_call).not_to be_called
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'does not complete other operations if already succeeded' do
|
84
|
+
Going.select do |s|
|
85
|
+
buffered_channel.receive(&dont_call)
|
86
|
+
s.default
|
87
|
+
Going.go do
|
88
|
+
buffered_channel.push 1
|
89
|
+
end.join
|
90
|
+
end
|
91
|
+
expect(dont_call).not_to be_called
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'does not preserve channel operations that are not selected' do
|
95
|
+
Going.select do |s|
|
96
|
+
buffered_channel.push 1
|
97
|
+
buffered_channel.push 2
|
98
|
+
Going.go do
|
99
|
+
buffered_channel.receive
|
100
|
+
end
|
101
|
+
end
|
102
|
+
expect(buffered_channel.size).to eq(0)
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'buffered channels' do
|
106
|
+
it 'succeeds when blocked push is now under capacity' do
|
107
|
+
buffered_channel.push 1
|
108
|
+
Going.select do |s|
|
109
|
+
buffered_channel.push(2, &spy)
|
110
|
+
Going.go do
|
111
|
+
buffered_channel.receive
|
112
|
+
end
|
113
|
+
end
|
114
|
+
expect(spy).to be_called
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe 'succeeding push' do
|
120
|
+
it 'passes nothing to the block' do
|
121
|
+
Going.select do |s|
|
122
|
+
buffered_channel.push(1, &spy)
|
123
|
+
end
|
124
|
+
|
125
|
+
expect(spy.args).to eq([])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe 'succeeding receive' do
|
130
|
+
it 'passes message as arg to the block' do
|
131
|
+
buffered_channel.push 1
|
132
|
+
Going.select do |s|
|
133
|
+
buffered_channel.receive(&spy)
|
134
|
+
end
|
135
|
+
|
136
|
+
expect(spy.args.first).to eq(1)
|
137
|
+
end
|
138
|
+
|
139
|
+
it "passes true as `ok` param to the block" do
|
140
|
+
buffered_channel.push 1
|
141
|
+
Going.select do |s|
|
142
|
+
buffered_channel.receive(&spy)
|
143
|
+
end
|
144
|
+
|
145
|
+
expect(spy.args.last).to eq({ ok: true })
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe 'closed channel' do
|
150
|
+
before(:each) { channel.close }
|
151
|
+
|
152
|
+
describe 'push' do
|
153
|
+
context 'when select fails' do
|
154
|
+
it 'raises error' do
|
155
|
+
expect do
|
156
|
+
Going.select do |s|
|
157
|
+
channel.push 1
|
158
|
+
end
|
159
|
+
end.to raise_error
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'does not call block' do
|
163
|
+
begin
|
164
|
+
Going.select do |s|
|
165
|
+
channel.push(1, &dont_call)
|
166
|
+
end
|
167
|
+
rescue
|
168
|
+
expect(dont_call).not_to be_called
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context 'when select succeeds' do
|
174
|
+
it 'does not raise error' do
|
175
|
+
expect do
|
176
|
+
Going.select do |s|
|
177
|
+
channel.push 1
|
178
|
+
s.default
|
179
|
+
end
|
180
|
+
end.not_to raise_error
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'does not call block' do
|
184
|
+
Going.select do |s|
|
185
|
+
channel.push(1, &dont_call)
|
186
|
+
s.default
|
187
|
+
end
|
188
|
+
expect(dont_call).not_to be_called
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe 'receive' do
|
194
|
+
it 'can receive buffered messages' do
|
195
|
+
buffered_channel.push 1
|
196
|
+
buffered_channel.close
|
197
|
+
|
198
|
+
Going.select do |s|
|
199
|
+
buffered_channel.receive(&spy)
|
200
|
+
end
|
201
|
+
expect(spy.args.first).to eq(1)
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'is considered a success' do
|
205
|
+
Going.select do |s|
|
206
|
+
channel.receive(&spy)
|
207
|
+
end
|
208
|
+
expect(spy).to be_called
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'passes nil as arg to the block' do
|
212
|
+
Going.select do |s|
|
213
|
+
channel.receive(&spy)
|
214
|
+
end
|
215
|
+
expect(spy.args.first).to be_nil
|
216
|
+
end
|
217
|
+
|
218
|
+
it "passes false as `ok` param to the block" do
|
219
|
+
Going.select do |s|
|
220
|
+
channel.receive(&spy)
|
221
|
+
end
|
222
|
+
expect(spy.args.last).to eq({ ok: false })
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe 'closing channel' do
|
228
|
+
describe 'push' do
|
229
|
+
context 'when select fails' do
|
230
|
+
it 'blocked push raises error' do
|
231
|
+
expect do
|
232
|
+
Going.select do |s|
|
233
|
+
channel.push 1
|
234
|
+
channel.close
|
235
|
+
end
|
236
|
+
end.to raise_error
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'does not call block' do
|
240
|
+
begin
|
241
|
+
Going.select do |s|
|
242
|
+
channel.push(1, &dont_call)
|
243
|
+
channel.close
|
244
|
+
end
|
245
|
+
rescue
|
246
|
+
expect(dont_call).not_to be_called
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context 'when select succeeds' do
|
252
|
+
it 'does not raise error if select succeeds' do
|
253
|
+
Going.select do |s|
|
254
|
+
channel.push 1
|
255
|
+
channel.receive
|
256
|
+
channel.close
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'does not call block' do
|
261
|
+
Going.select do |s|
|
262
|
+
channel.push(1, &dont_call)
|
263
|
+
channel.receive
|
264
|
+
channel.close
|
265
|
+
end
|
266
|
+
expect(dont_call).not_to be_called
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe 'receive' do
|
272
|
+
it 'is considered a success' do
|
273
|
+
Going.select do |s|
|
274
|
+
channel.receive(&spy)
|
275
|
+
channel.close
|
276
|
+
end
|
277
|
+
expect(spy).to be_called
|
278
|
+
end
|
279
|
+
|
280
|
+
it 'passes nil as arg to the block' do
|
281
|
+
Going.select do |s|
|
282
|
+
channel.receive(&spy)
|
283
|
+
channel.close
|
284
|
+
end
|
285
|
+
expect(spy.args.first).to be_nil
|
286
|
+
end
|
287
|
+
|
288
|
+
it "passes false as `ok` param to the block" do
|
289
|
+
Going.select do |s|
|
290
|
+
channel.receive(&spy)
|
291
|
+
channel.close
|
292
|
+
end
|
293
|
+
expect(spy.args.last).to eq({ ok: false })
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
describe '#default' do
|
300
|
+
it 'never blocks' do
|
301
|
+
now = Time.now
|
302
|
+
Going.select do |s|
|
303
|
+
s.default
|
304
|
+
end
|
305
|
+
expect(elapsed_time(now)).to be < 0.2
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'is not called if select statement is already succeeded' do
|
309
|
+
Going.select do |s|
|
310
|
+
buffered_channel.push 1
|
311
|
+
s.default(&dont_call)
|
312
|
+
end
|
313
|
+
expect(dont_call).not_to be_called
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'calls its block on success' do
|
317
|
+
Going.select do |s|
|
318
|
+
s.default(&spy)
|
319
|
+
end
|
320
|
+
expect(spy).to be_called
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'passes nothing to the block' do
|
324
|
+
Going.select do |s|
|
325
|
+
s.default(&spy)
|
326
|
+
end
|
327
|
+
expect(spy.args).to eq([])
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
describe '#timeout' do
|
332
|
+
it 'completes the select statement after given time' do
|
333
|
+
now = Time.now
|
334
|
+
Going.select do |s|
|
335
|
+
s.timeout(0.25)
|
336
|
+
end
|
337
|
+
expect(elapsed_time(now)).to be > 0.2
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'is not called if select statement is already completed' do
|
341
|
+
ch = nil
|
342
|
+
Going.select do |s|
|
343
|
+
ch = s.timeout(0.1, &dont_call)
|
344
|
+
s.default
|
345
|
+
end
|
346
|
+
sleeper ch, :pushes, 0
|
347
|
+
expect(dont_call).not_to be_called
|
348
|
+
end
|
349
|
+
|
350
|
+
it 'calls its block on success' do
|
351
|
+
Going.select do |s|
|
352
|
+
s.timeout(0, &spy)
|
353
|
+
end
|
354
|
+
expect(spy).to be_called
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'passes nothing to the block' do
|
358
|
+
Going.select do |s|
|
359
|
+
s.timeout(0, &spy)
|
360
|
+
end
|
361
|
+
expect(spy.args).to eq([])
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -16,6 +16,11 @@
|
|
16
16
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
17
17
|
|
18
18
|
require 'going'
|
19
|
+
require 'timeout'
|
20
|
+
|
21
|
+
Thread.abort_on_exception = true
|
22
|
+
|
23
|
+
Dir.glob(File.join(File.dirname(File.absolute_path(__FILE__)), 'support', '*.rb')) { |file| require file}
|
19
24
|
|
20
25
|
RSpec.configure do |config|
|
21
26
|
config.filter_run :focus
|
@@ -37,4 +42,10 @@ RSpec.configure do |config|
|
|
37
42
|
mocks.syntax = :expect
|
38
43
|
mocks.verify_partial_doubles = true
|
39
44
|
end
|
45
|
+
|
46
|
+
config.around(:each) do |example|
|
47
|
+
Timeout::timeout(5) do
|
48
|
+
example.run
|
49
|
+
end
|
50
|
+
end
|
40
51
|
end
|
data/spec/support/spy.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
class Spy
|
2
|
+
attr_reader :args
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@called = false
|
6
|
+
@args = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(*args)
|
10
|
+
@called = true
|
11
|
+
@args = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def called?
|
15
|
+
!!@called
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_proc
|
19
|
+
method(:call).to_proc
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
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: 0.0
|
4
|
+
version: 1.0.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-
|
11
|
+
date: 2014-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -68,13 +68,22 @@ files:
|
|
68
68
|
- Rakefile
|
69
69
|
- going.gemspec
|
70
70
|
- lib/going.rb
|
71
|
+
- lib/going/boolean_attr_reader.rb
|
71
72
|
- lib/going/channel.rb
|
72
73
|
- lib/going/kernel.rb
|
74
|
+
- lib/going/nil_select_statement.rb
|
75
|
+
- lib/going/operation.rb
|
76
|
+
- lib/going/operation/push.rb
|
77
|
+
- lib/going/operation/shift.rb
|
78
|
+
- lib/going/select_helper.rb
|
79
|
+
- lib/going/select_statement.rb
|
73
80
|
- lib/going/version.rb
|
74
81
|
- spec/going/channel_spec.rb
|
75
82
|
- spec/going/kernel_spec.rb
|
83
|
+
- spec/going/select_statement_spec.rb
|
76
84
|
- spec/going_spec.rb
|
77
85
|
- spec/spec_helper.rb
|
86
|
+
- spec/support/spy.rb
|
78
87
|
homepage: https://github.com/jridgewell/going
|
79
88
|
licenses:
|
80
89
|
- MIT
|
@@ -102,5 +111,7 @@ summary: Go for Ruby
|
|
102
111
|
test_files:
|
103
112
|
- spec/going/channel_spec.rb
|
104
113
|
- spec/going/kernel_spec.rb
|
114
|
+
- spec/going/select_statement_spec.rb
|
105
115
|
- spec/going_spec.rb
|
106
116
|
- spec/spec_helper.rb
|
117
|
+
- spec/support/spy.rb
|