goru 0.2.0 → 0.3.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 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