goru 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de298097d409c1186dd803f5748a1af3820c23ea0a7243e1d18c42ef35229906
4
- data.tar.gz: d834815e8f7f1a0deb22ad6be3cb8c75d900f81b5ffae1e55ce8eb10ef78bb3e
3
+ metadata.gz: 65d57e96ef8a7bef1abaf43db02e461af067b60238728379b9a5e48cea91c2f9
4
+ data.tar.gz: feba54796b8120fe753014a9f904c29c3e379ef50910e6cfde0507ca85d4d695
5
5
  SHA512:
6
- metadata.gz: 8674505286bace79c7b462ccf6c8b779f3b8ba09b7ca16b02a657d11f803a1f45835d286385b59e50b3909eced44e7ce6601db957b20c62737df96b13a24d564
7
- data.tar.gz: 353c05c439a9a85aedd2727f6036b8b858e59dd73f5c29acc4291b9533a78d45b375a8e07e57cc3e57765c974c584bc1aa09304e101fecb38652f8f47d8e588d
6
+ metadata.gz: c65b2730c39ec515c1ab44ee83909c4ea93c53072cb33887aebafaedfeb0131a37c0b3d1bb323b79ce21acdc5248be3fefa3d19c007c9aa5c56217e9a7195541
7
+ data.tar.gz: f4d7f05d63ad6ce781053946fa0d0e22ec9562b7e20fed83bca8f84441b4f6ec9c30b8b4b79fa7435cc93a76425ec793b6db7ac334aba0fddac84320777226b3
data/CHANGELOG.md CHANGED
@@ -1,6 +1,21 @@
1
- ## v0.2.0
1
+ ## [v0.3.0](https://github.com/bryanp/goru/releases/tag/v0.3.0)
2
2
 
3
- *unreleased*
3
+ *released on 2023-07-10*
4
+
5
+ * `chg` [#16](https://github.com/bryanp/goru/pull/16) Improve control flow ([bryanp](https://github.com/bryanp))
6
+ * `fix` [#17](https://github.com/bryanp/goru/pull/17) Handle `IOError` from closed selector ([bryanp](https://github.com/bryanp))
7
+ * `chg` [#15](https://github.com/bryanp/goru/pull/15) Rename `default_scheduler_count` ([bryanp](https://github.com/bryanp))
8
+ * `chg` [#14](https://github.com/bryanp/goru/pull/14) Improve cold start of scheduler ([bryanp](https://github.com/bryanp))
9
+ * `chg` [#13](https://github.com/bryanp/goru/pull/13) Go back to selector-based reactor ([bryanp](https://github.com/bryanp))
10
+ * `chg` [#12](https://github.com/bryanp/goru/pull/12) Refactor bridges (again) ([bryanp](https://github.com/bryanp))
11
+ * `dep` [#11](https://github.com/bryanp/goru/pull/11) Change responsibilities of routine sleep behavior to be more clear ([bryanp](https://github.com/bryanp))
12
+ * `chg` [#10](https://github.com/bryanp/goru/pull/10) Optimize how reactor status is set ([bryanp](https://github.com/bryanp))
13
+ * `chg` [#9](https://github.com/bryanp/goru/pull/9) Refactor bridges ([bryanp](https://github.com/bryanp))
14
+ * `chg` [#8](https://github.com/bryanp/goru/pull/8) Cleanup finished routines on next tick ([bryanp](https://github.com/bryanp))
15
+
16
+ ## [v0.2.0](https://github.com/bryanp/goru/releases/tag/v0.2.0)
17
+
18
+ *released on 2023-05-01*
4
19
 
5
20
  * `fix` [#6](https://github.com/bryanp/goru/pull/6) Finish routines on error ([bryanp](https://github.com/bryanp))
6
21
  * `fix` [#5](https://github.com/bryanp/goru/pull/5) Correctly set channel status to `finished` when closed ([bryanp](https://github.com/bryanp))
data/README.md CHANGED
@@ -201,6 +201,34 @@ Goru::Scheduler.go(io: io, intent: :r) { |routine|
201
201
  }
202
202
  ```
203
203
 
204
+ ## Bridges
205
+
206
+ Goru supports coordinated buffered io using bridges:
207
+
208
+ ```ruby
209
+ writer = Goru::Channel.new
210
+
211
+ Goru::Scheduler.go(io: io, intent: :r) { |routine|
212
+ case routine.intent
213
+ when :r
214
+ routine.bridge(intent: :w, channel: writer) { |bridge|
215
+ bridge << SecureRandom.hex
216
+ }
217
+ when :w
218
+ if (data = writer.read)
219
+ routine.write(data)
220
+ end
221
+ end
222
+ }
223
+ ```
224
+
225
+ Using bridges, the io routine is only called again when two conditions are met:
226
+
227
+ 1. The io object matches the bridged intent (e.g. it is writable).
228
+ 2. The channel is in the correct state to reciprocate the intent (e.g. it has data).
229
+
230
+ See the [server example](./examples/server.rb) for a more complete use-case.
231
+
204
232
  ## Credits
205
233
 
206
234
  Goru was designed while writing a project in Go and imagining what Go-like concurrency might look like in Ruby.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Goru
4
+ # [public]
5
+ #
6
+ class Bridge
7
+ def initialize(routine:, channel:)
8
+ @routine = routine
9
+ @channel = channel
10
+ @channel.add_observer(self)
11
+ update_status
12
+ end
13
+
14
+ # [public]
15
+ #
16
+ attr_reader :status
17
+
18
+ # [public]
19
+ #
20
+ private def set_status(status)
21
+ @status = status
22
+ status_changed
23
+ end
24
+
25
+ # [public]
26
+ #
27
+ def update_status
28
+ # noop
29
+ end
30
+
31
+ private def status_changed
32
+ case @status
33
+ when :ready
34
+ @routine.bridged
35
+ when :finished
36
+ @channel.remove_observer(self)
37
+ @routine.unbridge
38
+ end
39
+ end
40
+
41
+ def channel_received
42
+ update_status
43
+ end
44
+
45
+ def channel_read
46
+ update_status
47
+ end
48
+
49
+ def channel_closed
50
+ update_status
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../bridge"
4
+
5
+ module Goru
6
+ module Bridges
7
+ class Readable < Bridge
8
+ private def update_status
9
+ status = if @routine.status == :finished
10
+ :finished
11
+ elsif @channel.full?
12
+ :idle
13
+ elsif @channel.closed?
14
+ :finished
15
+ else
16
+ :ready
17
+ end
18
+
19
+ set_status(status)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../bridge"
4
+
5
+ module Goru
6
+ module Bridges
7
+ class Writable < Bridge
8
+ private def update_status
9
+ status = if @routine.status == :finished
10
+ :finished
11
+ elsif @channel.any?
12
+ :ready
13
+ elsif @channel.closed?
14
+ :finished
15
+ else
16
+ :idle
17
+ end
18
+
19
+ set_status(status)
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/goru/reactor.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nio"
4
-
5
4
  require "timers/group"
6
5
  require "timers/wait"
7
6
 
8
- require_relative "routines/bridge"
9
7
  require_relative "routines/io"
10
8
 
11
9
  module Goru
@@ -16,12 +14,11 @@ module Goru
16
14
  @queue = queue
17
15
  @scheduler = scheduler
18
16
  @routines = Set.new
19
- @bridges = Set.new
20
17
  @timers = Timers::Group.new
21
- @selector = NIO::Selector.new
22
18
  @stopped = false
23
19
  @status = nil
24
- @mutex = Mutex.new
20
+ @selector = NIO::Selector.new
21
+ @commands = []
25
22
  end
26
23
 
27
24
  # [public]
@@ -31,169 +28,140 @@ module Goru
31
28
  # [public]
32
29
  #
33
30
  def run
34
- until @stopped
35
- set_status(:running)
36
-
37
- @routines.each do |routine|
38
- call_routine(routine)
39
- end
31
+ set_status(:running)
40
32
 
41
- begin
42
- wait_for_routine(block: false)
43
- rescue ThreadError
44
- interval = @timers.wait_interval
45
-
46
- if interval.nil?
47
- if @routines.empty?
48
- if @selector.empty?
49
- become_idle
50
- else
51
- wait_for_bridge do
52
- wait_for_selector
53
- end
54
- end
55
- else
56
- wait_for_bridge do
57
- wait_for_selector(0)
58
- end
59
- end
60
- elsif interval > 0
61
- if @selector.empty?
62
- wait_for_interval(interval)
63
- else
64
- wait_for_bridge do
65
- wait_for_selector(interval)
66
- end
67
- end
68
- end
69
-
70
- @timers.fire
71
- end
33
+ until @stopped
34
+ tick
72
35
  end
73
36
  ensure
37
+ @timers.cancel
74
38
  @selector.close
75
39
  set_status(:finished)
76
40
  end
77
41
 
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
42
+ private def tick
43
+ # Apply queued commands.
44
+ #
45
+ while (command = @commands.shift)
46
+ action, routine = command
47
+
48
+ case action
49
+ when :adopt
50
+ routine.reactor = self
51
+ @routines << routine
52
+ routine.adopted
53
+ when :cleanup
54
+ @routines.delete(routine)
55
+ when :register
56
+ monitor = @selector.register(routine.io, routine.intent)
57
+ monitor.value = routine.method(:wakeup)
58
+ routine.monitor = monitor
59
+ when :deregister
60
+ routine.monitor&.close
61
+ routine.monitor = nil
62
+ end
87
63
  end
88
- end
89
64
 
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
65
+ # Call each ready routine.
66
+ #
67
+ @routines.each do |routine|
68
+ next unless routine.ready?
97
69
 
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
70
+ catch :continue do
71
+ routine.call
72
+ end
103
73
  end
104
- end
105
74
 
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))
75
+ # Adopt a new routine if available.
76
+ #
77
+ if (routine = @queue.pop(true))
112
78
  adopt_routine(routine)
113
79
  end
80
+ rescue ThreadError
81
+ interval = @timers.wait_interval
82
+
83
+ if interval.nil? && @routines.empty?
84
+ set_status(:idle)
85
+ @scheduler.signal
86
+ wait
87
+ set_status(:running)
88
+ elsif interval.nil?
89
+ wait unless @routines.any?(&:ready?)
90
+ elsif interval > 0
91
+ wait(timeout: interval)
92
+ end
93
+
94
+ @timers.fire
114
95
  end
115
96
 
116
- # [public]
117
- #
118
- def finished?
119
- @mutex.synchronize do
120
- @status == :idle || @status == :stopped
97
+ private def wait(timeout: nil)
98
+ @selector.select(timeout) do |monitor|
99
+ monitor.value.call
121
100
  end
122
101
  end
123
102
 
124
103
  # [public]
125
104
  #
126
- def signal
127
- unless @selector.empty?
128
- @selector.wakeup
129
- end
105
+ def finished?
106
+ @status == :idle || @status == :stopped
130
107
  end
131
108
 
132
109
  # [public]
133
110
  #
134
111
  def wakeup
135
- signal
136
- @queue << :wakeup
112
+ @selector.wakeup
113
+ rescue IOError
114
+ # nothing to do
137
115
  end
138
116
 
139
117
  # [public]
140
118
  #
141
119
  def stop
142
120
  @stopped = true
143
- @selector.wakeup
144
- rescue IOError
121
+ wakeup
122
+ rescue ClosedQueueError
123
+ # nothing to do
145
124
  end
146
125
 
147
126
  # [public]
148
127
  #
149
- def routine_asleep(routine, seconds)
128
+ def asleep_for(seconds)
150
129
  @timers.after(seconds) {
151
- routine.wake
130
+ yield
152
131
  }
153
132
  end
154
133
 
155
134
  # [public]
156
135
  #
157
136
  def adopt_routine(routine)
158
- case routine
159
- when Routines::IO
160
- monitor = @selector.register(routine.io, routine.intent)
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
169
- @routines << routine
170
- end
137
+ command(:adopt, routine)
171
138
  end
172
139
 
173
140
  # [public]
174
141
  #
175
142
  def routine_finished(routine)
176
- case routine
177
- when Routines::Bridge
178
- @bridges.delete(routine)
179
- when Routines::IO
180
- @selector.deregister(routine.io)
181
- else
182
- @routines.delete(routine)
183
- end
143
+ command(:cleanup, routine)
184
144
  end
185
145
 
186
- private def set_status(status)
187
- @mutex.synchronize do
188
- @status = status
189
- end
146
+ # [public]
147
+ #
148
+ def register(routine)
149
+ command(:register, routine)
190
150
  end
191
151
 
192
- private def call_routine(routine)
193
- case routine.status
194
- when :ready
195
- routine.call
196
- end
152
+ # [public]
153
+ #
154
+ def deregister(routine)
155
+ command(:deregister, routine)
156
+ end
157
+
158
+ private def command(action, routine)
159
+ @commands << [action, routine]
160
+ wakeup
161
+ end
162
+
163
+ private def set_status(status)
164
+ @status = status
197
165
  end
198
166
  end
199
167
  end
data/lib/goru/routine.rb CHANGED
@@ -53,10 +53,10 @@ module Goru
53
53
  # [public]
54
54
  #
55
55
  def finished(result = nil)
56
- unless @finished
57
- @result = result
58
- set_status(:finished)
59
- end
56
+ @result = result
57
+ set_status(:finished)
58
+
59
+ throw :continue
60
60
  end
61
61
 
62
62
  # [public]
@@ -80,13 +80,17 @@ module Goru
80
80
  #
81
81
  def sleep(seconds)
82
82
  set_status(:idle)
83
- @reactor.routine_asleep(self, seconds)
83
+ @reactor.asleep_for(seconds) do
84
+ set_status(:ready)
85
+ end
86
+
87
+ throw :continue
84
88
  end
85
89
 
86
90
  # [public]
87
91
  #
88
- def wake
89
- set_status(:ready)
92
+ def ready?
93
+ @status == :ready
90
94
  end
91
95
 
92
96
  # [public]
@@ -104,5 +108,11 @@ module Goru
104
108
  @reactor&.routine_finished(self)
105
109
  end
106
110
  end
111
+
112
+ # [public]
113
+ #
114
+ def adopted
115
+ # noop
116
+ end
107
117
  end
108
118
  end
@@ -12,6 +12,7 @@ module Goru
12
12
 
13
13
  @channel = channel
14
14
  @channel.add_observer(self)
15
+ update_status
15
16
  end
16
17
 
17
18
  private def status_changed
@@ -8,12 +8,6 @@ module Goru
8
8
  # [public]
9
9
  #
10
10
  class Readable < Channel
11
- def initialize(...)
12
- super
13
-
14
- update_status
15
- end
16
-
17
11
  # [public]
18
12
  #
19
13
  def read
@@ -8,12 +8,6 @@ module Goru
8
8
  # [public]
9
9
  #
10
10
  class Writable < Channel
11
- def initialize(...)
12
- super
13
-
14
- update_status
15
- end
16
-
17
11
  # [public]
18
12
  #
19
13
  def <<(message)
@@ -1,51 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../routine"
4
- require_relative "bridges/readable"
5
- require_relative "bridges/writable"
4
+ require_relative "../bridges/readable"
5
+ require_relative "../bridges/writable"
6
6
 
7
7
  module Goru
8
8
  module Routines
9
9
  # [public]
10
10
  #
11
11
  class IO < Routine
12
- def initialize(state = nil, io:, intent:, &block)
12
+ def initialize(state = nil, io:, intent:, event_loop:, &block)
13
13
  super(state, &block)
14
14
 
15
15
  @io = io
16
16
  @intent = normalize_intent(intent)
17
- @status = :selecting
17
+ @event_loop = event_loop
18
+ @status = :orphaned
18
19
  @monitor = nil
19
- @finishers = []
20
20
  end
21
21
 
22
22
  # [public]
23
23
  #
24
24
  attr_reader :io, :intent
25
25
 
26
- attr_writer :monitor
26
+ attr_accessor :monitor
27
+
28
+ # [public]
29
+ #
30
+ def adopted
31
+ set_status(:ready)
32
+ end
33
+
34
+ # [public]
35
+ #
36
+ def wakeup
37
+ # Keep this io from being selected again until the underlying routine is called.
38
+ # Interests are reset in `#call`.
39
+ #
40
+ @monitor&.interests = nil
41
+
42
+ set_status(:io_ready)
43
+ end
44
+
45
+ READY_STATUSES = [:io_ready, :ready].freeze
46
+ READY_BRIDGE_STATUSES = [nil, :ready].freeze
47
+
48
+ # [public]
49
+ #
50
+ def ready?
51
+ READY_STATUSES.include?(@status) && READY_BRIDGE_STATUSES.include?(@bridge&.status)
52
+ end
53
+
54
+ def call
55
+ super
56
+
57
+ @monitor&.interests = @intent
58
+ end
27
59
 
28
60
  # [public]
29
61
  #
30
62
  def accept
31
63
  @io.accept_nonblock
64
+ rescue Errno::EAGAIN
65
+ wait
66
+ rescue Errno::ECONNRESET, Errno::EPIPE, EOFError
67
+ finished
68
+ nil
69
+ end
70
+
71
+ def wait
72
+ set_status(:selecting)
73
+ @reactor.register(self) unless @monitor
74
+
75
+ throw :continue
32
76
  end
33
77
 
34
78
  # [public]
35
79
  #
36
80
  def read(bytes)
37
- result = @io.read_nonblock(bytes, exception: false)
38
-
39
- case result
40
- when nil
41
- finished
42
- nil
43
- when :wait_readable
44
- # nothing to do
45
- else
46
- result
47
- end
48
- rescue Errno::ECONNRESET
81
+ @io.read_nonblock(bytes)
82
+ rescue Errno::EAGAIN
83
+ wait
84
+ rescue Errno::ECONNRESET, Errno::EPIPE, EOFError
49
85
  finished
50
86
  nil
51
87
  end
@@ -53,18 +89,10 @@ module Goru
53
89
  # [public]
54
90
  #
55
91
  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
92
+ @io.write_nonblock(data)
93
+ rescue Errno::EAGAIN
94
+ wait
95
+ rescue Errno::ECONNRESET, Errno::EPIPE, EOFError
68
96
  finished
69
97
  nil
70
98
  end
@@ -75,38 +103,56 @@ module Goru
75
103
  intent = normalize_intent(intent)
76
104
  validate_intent!(intent)
77
105
 
78
- @monitor.interests = intent
106
+ @monitor&.interests = intent
79
107
  @intent = intent
80
108
  end
81
109
 
82
110
  # [public]
83
111
  #
84
- def bridge(channel, intent:)
112
+ def bridge(state = nil, intent:, channel:, &block)
113
+ raise "routine is already bridged" if @bridge
114
+
85
115
  intent = normalize_intent(intent)
86
116
  validate_intent!(intent)
117
+ self.intent = intent
87
118
 
88
- bridge = case intent
119
+ @bridge = case intent
89
120
  when :r
90
121
  Bridges::Readable.new(routine: self, channel: channel)
91
122
  when :w
92
123
  Bridges::Writable.new(routine: self, channel: channel)
93
124
  end
94
125
 
95
- on_finished { bridge.finished }
96
- @reactor.adopt_routine(bridge)
97
- bridge
126
+ routine = case intent
127
+ when :r
128
+ Routines::Channels::Readable.new(state, channel: channel, &block)
129
+ when :w
130
+ Routines::Channels::Writable.new(state, channel: channel, &block)
131
+ end
132
+
133
+ @reactor.adopt_routine(routine)
134
+ @reactor.wakeup
135
+
136
+ routine
137
+ end
138
+
139
+ # [public]
140
+ #
141
+ def bridged
142
+ @reactor.wakeup
98
143
  end
99
144
 
100
145
  # [public]
101
146
  #
102
- def on_finished(&block)
103
- @finishers << block
147
+ def unbridge
148
+ @bridge = nil
149
+ @reactor.wakeup
104
150
  end
105
151
 
106
152
  private def status_changed
107
153
  case @status
108
154
  when :finished
109
- @finishers.each(&:call)
155
+ @reactor&.deregister(self)
110
156
  end
111
157
 
112
158
  super
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "etc"
4
+ require "nio"
4
5
  require "is/global"
5
6
 
6
7
  require_relative "channel"
@@ -16,7 +17,6 @@ module Goru
16
17
  #
17
18
  class Scheduler
18
19
  include Is::Global
19
- include MonitorMixin
20
20
 
21
21
  class << self
22
22
  # Prevent issues when including `Goru` at the toplevel.
@@ -29,17 +29,18 @@ module Goru
29
29
 
30
30
  # [public]
31
31
  #
32
- def default_scheduler_count
32
+ def default_reactor_count
33
33
  Etc.nprocessors
34
34
  end
35
35
  end
36
36
 
37
- def initialize(count: self.class.default_scheduler_count)
37
+ def initialize(count: self.class.default_reactor_count)
38
38
  super()
39
39
 
40
- @stopping = false
40
+ @waiting = false
41
+ @stopped = false
41
42
  @routines = Thread::Queue.new
42
- @condition = new_cond
43
+ @selector = NIO::Selector.new
43
44
 
44
45
  @reactors = count.times.map {
45
46
  Reactor.new(queue: @routines, scheduler: self)
@@ -60,7 +61,7 @@ module Goru
60
61
  raise ArgumentError, "cannot set both `io` and `channel`" if io && channel
61
62
 
62
63
  routine = if io
63
- Routines::IO.new(state, io: io, intent: intent, &block)
64
+ Routines::IO.new(state, io: io, intent: intent, event_loop: @io_event_loop, &block)
64
65
  elsif channel
65
66
  case intent
66
67
  when :r
@@ -73,7 +74,7 @@ module Goru
73
74
  end
74
75
 
75
76
  @routines << routine
76
- @reactors.each(&:signal)
77
+ @reactors.each(&:wakeup)
77
78
 
78
79
  routine
79
80
  end
@@ -81,12 +82,11 @@ module Goru
81
82
  # [public]
82
83
  #
83
84
  def wait
84
- synchronize do
85
- @condition.wait_until do
86
- @stopping
87
- end
88
- end
89
- rescue Interrupt
85
+ @waiting = true
86
+ @reactors.each(&:wakeup)
87
+ @selector.select while @waiting
88
+ rescue IOError, Interrupt
89
+ # nothing to do
90
90
  ensure
91
91
  stop
92
92
  end
@@ -94,22 +94,25 @@ module Goru
94
94
  # [public]
95
95
  #
96
96
  def stop
97
- @stopping = true
97
+ @stopped = true
98
98
  @routines.close
99
+ @selector.close
99
100
  @reactors.each(&:stop)
100
101
  @threads.each(&:join)
101
102
  end
102
103
 
103
104
  # [public]
104
105
  #
105
- def signal(reactor)
106
- synchronize do
107
- if @reactors.all?(&:finished?)
108
- @stopping = true
109
- end
106
+ def signal
107
+ return unless @waiting && @reactors.all?(&:finished?)
108
+ @waiting = false
109
+ wakeup
110
+ end
110
111
 
111
- @condition.signal
112
- end
112
+ def wakeup
113
+ @selector.wakeup
114
+ rescue IOError
115
+ # nothing to do
113
116
  end
114
117
  end
115
118
  end
data/lib/goru/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Goru
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
 
6
6
  # [public]
7
7
  #
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.2.0
4
+ version: 0.3.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: 2023-05-01 00:00:00.000000000 Z
11
+ date: 2023-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: core-extension
@@ -90,12 +90,12 @@ files:
90
90
  - LICENSE
91
91
  - README.md
92
92
  - lib/goru.rb
93
+ - lib/goru/bridge.rb
94
+ - lib/goru/bridges/readable.rb
95
+ - lib/goru/bridges/writable.rb
93
96
  - lib/goru/channel.rb
94
97
  - lib/goru/reactor.rb
95
98
  - 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
99
  - lib/goru/routines/channel.rb
100
100
  - lib/goru/routines/channels/readable.rb
101
101
  - lib/goru/routines/channels/writable.rb
@@ -121,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
121
  - !ruby/object:Gem::Version
122
122
  version: '0'
123
123
  requirements: []
124
- rubygems_version: 3.4.9
124
+ rubygems_version: 3.4.12
125
125
  signing_key:
126
126
  specification_version: 4
127
127
  summary: Concurrent routines for Ruby.
@@ -1,46 +0,0 @@
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
- end
45
- end
46
- end
@@ -1,37 +0,0 @@
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
- :finished
28
- else
29
- :ready
30
- end
31
-
32
- set_status(status)
33
- end
34
- end
35
- end
36
- end
37
- end
@@ -1,37 +0,0 @@
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