goru 0.2.0 → 0.4.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: a1bad5e42a7c84e99e1e75651c62f0c2459497aacafea49319279d8de5251c9c
4
+ data.tar.gz: 4c6a172e82892ce4301482d42fe03c59b299e6409efbc1bd620aacaf2bc24301
5
5
  SHA512:
6
- metadata.gz: 8674505286bace79c7b462ccf6c8b779f3b8ba09b7ca16b02a657d11f803a1f45835d286385b59e50b3909eced44e7ce6601db957b20c62737df96b13a24d564
7
- data.tar.gz: 353c05c439a9a85aedd2727f6036b8b858e59dd73f5c29acc4291b9533a78d45b375a8e07e57cc3e57765c974c584bc1aa09304e101fecb38652f8f47d8e588d
6
+ metadata.gz: 9dbcc82cce20117208062ac716097982b96ad59bec1fa6a3326f5703ea47529c7afb2ddf9531d81b0b237d77a13aab253494c74da5c8312eb166bde150525707
7
+ data.tar.gz: a6259dac811fabfd7791319d09e3ddf228b8b8a7f3f73e829fcd1b29630eae52e2c503a5682c7b3ac269068464947da2254343e0f0743ee0111284f87d9a284c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,31 @@
1
- ## v0.2.0
1
+ ## [v0.4.0](https://github.com/bryanp/goru/releases/tag/v0.4.0)
2
2
 
3
- *unreleased*
3
+ *released on 2023-07-16*
4
+
5
+ * `add` [#22](https://github.com/bryanp/goru/pull/22) Add reactor as a reader to `Goru::Routine` ([bryanp](https://github.com/bryanp))
6
+ * `add` [#21](https://github.com/bryanp/goru/pull/21) Improve statuses ([bryanp](https://github.com/bryanp))
7
+ * `add` [#20](https://github.com/bryanp/goru/pull/20) Add observer pattern to routines ([bryanp](https://github.com/bryanp))
8
+ * `add` [#19](https://github.com/bryanp/goru/pull/19) Add ability to pause and resume a routine ([bryanp](https://github.com/bryanp))
9
+ * `chg` [#18](https://github.com/bryanp/goru/pull/18) Remove the unused `observer` writer from `Goru::Channel` ([bryanp](https://github.com/bryanp))
10
+
11
+ ## [v0.3.0](https://github.com/bryanp/goru/releases/tag/v0.3.0)
12
+
13
+ *released on 2023-07-10*
14
+
15
+ * `chg` [#16](https://github.com/bryanp/goru/pull/16) Improve control flow ([bryanp](https://github.com/bryanp))
16
+ * `fix` [#17](https://github.com/bryanp/goru/pull/17) Handle `IOError` from closed selector ([bryanp](https://github.com/bryanp))
17
+ * `chg` [#15](https://github.com/bryanp/goru/pull/15) Rename `default_scheduler_count` ([bryanp](https://github.com/bryanp))
18
+ * `chg` [#14](https://github.com/bryanp/goru/pull/14) Improve cold start of scheduler ([bryanp](https://github.com/bryanp))
19
+ * `chg` [#13](https://github.com/bryanp/goru/pull/13) Go back to selector-based reactor ([bryanp](https://github.com/bryanp))
20
+ * `chg` [#12](https://github.com/bryanp/goru/pull/12) Refactor bridges (again) ([bryanp](https://github.com/bryanp))
21
+ * `dep` [#11](https://github.com/bryanp/goru/pull/11) Change responsibilities of routine sleep behavior to be more clear ([bryanp](https://github.com/bryanp))
22
+ * `chg` [#10](https://github.com/bryanp/goru/pull/10) Optimize how reactor status is set ([bryanp](https://github.com/bryanp))
23
+ * `chg` [#9](https://github.com/bryanp/goru/pull/9) Refactor bridges ([bryanp](https://github.com/bryanp))
24
+ * `chg` [#8](https://github.com/bryanp/goru/pull/8) Cleanup finished routines on next tick ([bryanp](https://github.com/bryanp))
25
+
26
+ ## [v0.2.0](https://github.com/bryanp/goru/releases/tag/v0.2.0)
27
+
28
+ *released on 2023-05-01*
4
29
 
5
30
  * `fix` [#6](https://github.com/bryanp/goru/pull/6) Finish routines on error ([bryanp](https://github.com/bryanp))
6
31
  * `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,65 @@
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
+ STATUS_READY = :ready
17
+
18
+ # [public]
19
+ #
20
+ STATUS_FINISHED = :finished
21
+
22
+ # [public]
23
+ #
24
+ STATUS_IDLE = :idle
25
+
26
+ # [public]
27
+ #
28
+ attr_reader :status
29
+
30
+ # [public]
31
+ #
32
+ private def set_status(status)
33
+ @status = status
34
+ status_changed
35
+ end
36
+
37
+ # [public]
38
+ #
39
+ def update_status
40
+ # noop
41
+ end
42
+
43
+ private def status_changed
44
+ case @status
45
+ when :STATUS_READY
46
+ @routine.bridged
47
+ when :STATUS_FINISHED
48
+ @channel.remove_observer(self)
49
+ @routine.unbridge
50
+ end
51
+ end
52
+
53
+ def channel_received
54
+ update_status
55
+ end
56
+
57
+ def channel_read
58
+ update_status
59
+ end
60
+
61
+ def channel_closed
62
+ update_status
63
+ end
64
+ end
65
+ 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 == Routine::STATUS_FINISHED
10
+ Bridge::STATUS_FINISHED
11
+ elsif @channel.full?
12
+ Bridge::STATUS_IDLE
13
+ elsif @channel.closed?
14
+ Bridge::STATUS_FINISHED
15
+ else
16
+ Bridge::STATUS_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 == Routine::STATUS_FINISHED
10
+ Bridge::STATUS_FINISHED
11
+ elsif @channel.any?
12
+ Bridge::STATUS_READY
13
+ elsif @channel.closed?
14
+ Bridge::STATUS_FINISHED
15
+ else
16
+ Bridge::STATUS_IDLE
17
+ end
18
+
19
+ set_status(status)
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/goru/channel.rb CHANGED
@@ -11,10 +11,6 @@ module Goru
11
11
  @observers = Set.new
12
12
  end
13
13
 
14
- # [public]
15
- #
16
- attr_writer :observer
17
-
18
14
  # [public]
19
15
  #
20
16
  def <<(message)
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,14 +14,29 @@ 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
 
24
+ # [public]
25
+ #
26
+ STATUS_RUNNING = :running
27
+
28
+ # [public]
29
+ #
30
+ STATUS_FINISHED = :finished
31
+
32
+ # [public]
33
+ #
34
+ STATUS_IDLE = :idle
35
+
36
+ # [public]
37
+ #
38
+ STATUS_STOPPED = :stopped
39
+
27
40
  # [public]
28
41
  #
29
42
  attr_reader :status
@@ -31,169 +44,140 @@ module Goru
31
44
  # [public]
32
45
  #
33
46
  def run
34
- until @stopped
35
- set_status(:running)
47
+ set_status(STATUS_RUNNING)
36
48
 
37
- @routines.each do |routine|
38
- call_routine(routine)
39
- end
40
-
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
49
+ until @stopped
50
+ tick
72
51
  end
73
52
  ensure
53
+ @timers.cancel
74
54
  @selector.close
75
- set_status(:finished)
76
- end
77
-
78
- private def become_idle
79
- set_status(:idle)
80
- @scheduler.signal(self)
81
- wait_for_routine
82
- end
83
-
84
- private def wait_for_selector(timeout = nil)
85
- @selector.select(timeout) do |monitor|
86
- monitor.value.call
55
+ set_status(STATUS_FINISHED)
56
+ end
57
+
58
+ private def tick
59
+ # Apply queued commands.
60
+ #
61
+ while (command = @commands.shift)
62
+ action, routine = command
63
+
64
+ case action
65
+ when :adopt
66
+ routine.reactor = self
67
+ @routines << routine
68
+ routine.adopted
69
+ when :cleanup
70
+ @routines.delete(routine)
71
+ when :register
72
+ monitor = @selector.register(routine.io, routine.intent)
73
+ monitor.value = routine.method(:wakeup)
74
+ routine.monitor = monitor
75
+ when :deregister
76
+ routine.monitor&.close
77
+ routine.monitor = nil
78
+ end
87
79
  end
88
- end
89
80
 
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
81
+ # Call each ready routine.
82
+ #
83
+ @routines.each do |routine|
84
+ next unless routine.ready?
97
85
 
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
86
+ catch :continue do
87
+ routine.call
88
+ end
103
89
  end
104
- end
105
90
 
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))
91
+ # Adopt a new routine if available.
92
+ #
93
+ if (routine = @queue.pop(true))
112
94
  adopt_routine(routine)
113
95
  end
96
+ rescue ThreadError
97
+ interval = @timers.wait_interval
98
+
99
+ if interval.nil? && @routines.empty?
100
+ set_status(STATUS_IDLE)
101
+ @scheduler.signal
102
+ wait
103
+ set_status(STATUS_RUNNING)
104
+ elsif interval.nil?
105
+ wait unless @routines.any?(&:ready?)
106
+ elsif interval > 0
107
+ wait(timeout: interval)
108
+ end
109
+
110
+ @timers.fire
114
111
  end
115
112
 
116
- # [public]
117
- #
118
- def finished?
119
- @mutex.synchronize do
120
- @status == :idle || @status == :stopped
113
+ private def wait(timeout: nil)
114
+ @selector.select(timeout) do |monitor|
115
+ monitor.value.call
121
116
  end
122
117
  end
123
118
 
124
119
  # [public]
125
120
  #
126
- def signal
127
- unless @selector.empty?
128
- @selector.wakeup
129
- end
121
+ def finished?
122
+ @status == STATUS_IDLE || @status == STATUS_STOPPED
130
123
  end
131
124
 
132
125
  # [public]
133
126
  #
134
127
  def wakeup
135
- signal
136
- @queue << :wakeup
128
+ @selector.wakeup
129
+ rescue IOError
130
+ # nothing to do
137
131
  end
138
132
 
139
133
  # [public]
140
134
  #
141
135
  def stop
142
136
  @stopped = true
143
- @selector.wakeup
144
- rescue IOError
137
+ wakeup
138
+ rescue ClosedQueueError
139
+ # nothing to do
145
140
  end
146
141
 
147
142
  # [public]
148
143
  #
149
- def routine_asleep(routine, seconds)
144
+ def asleep_for(seconds)
150
145
  @timers.after(seconds) {
151
- routine.wake
146
+ yield
152
147
  }
153
148
  end
154
149
 
155
150
  # [public]
156
151
  #
157
152
  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
153
+ command(:adopt, routine)
171
154
  end
172
155
 
173
156
  # [public]
174
157
  #
175
158
  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
159
+ command(:cleanup, routine)
184
160
  end
185
161
 
186
- private def set_status(status)
187
- @mutex.synchronize do
188
- @status = status
189
- end
162
+ # [public]
163
+ #
164
+ def register(routine)
165
+ command(:register, routine)
190
166
  end
191
167
 
192
- private def call_routine(routine)
193
- case routine.status
194
- when :ready
195
- routine.call
196
- end
168
+ # [public]
169
+ #
170
+ def deregister(routine)
171
+ command(:deregister, routine)
172
+ end
173
+
174
+ private def command(action, routine)
175
+ @commands << [action, routine]
176
+ wakeup
177
+ end
178
+
179
+ private def set_status(status)
180
+ @status = status
197
181
  end
198
182
  end
199
183
  end
data/lib/goru/routine.rb CHANGED
@@ -20,14 +20,35 @@ module Goru
20
20
  def initialize(state = nil, &block)
21
21
  @state = state
22
22
  @block = block
23
- set_status(:ready)
23
+ @observers = Set.new
24
+ set_status(STATUS_READY)
24
25
  @result, @error, @reactor = nil
25
26
  @debug = true
26
27
  end
27
28
 
28
29
  # [public]
29
30
  #
30
- attr_reader :state, :status, :error
31
+ STATUS_READY = :ready
32
+
33
+ # [public]
34
+ #
35
+ STATUS_FINISHED = :finished
36
+
37
+ # [public]
38
+ #
39
+ STATUS_ERRORED = :errored
40
+
41
+ # [public]
42
+ #
43
+ STATUS_IDLE = :idle
44
+
45
+ # [public]
46
+ #
47
+ STATUS_PAUSED = :paused
48
+
49
+ # [public]
50
+ #
51
+ attr_reader :state, :status, :error, :reactor
31
52
 
32
53
  # [public]
33
54
  #
@@ -46,17 +67,17 @@ module Goru
46
67
  @block.call(self)
47
68
  rescue => error
48
69
  @error = error
49
- set_status(:errored)
70
+ set_status(STATUS_ERRORED)
50
71
  trigger(error)
51
72
  end
52
73
 
53
74
  # [public]
54
75
  #
55
76
  def finished(result = nil)
56
- unless @finished
57
- @result = result
58
- set_status(:finished)
59
- end
77
+ @result = result
78
+ set_status(STATUS_FINISHED)
79
+
80
+ throw :continue
60
81
  end
61
82
 
62
83
  # [public]
@@ -69,7 +90,7 @@ module Goru
69
90
  #
70
91
  def result
71
92
  case @status
72
- when :errored
93
+ when STATUS_ERRORED
73
94
  raise @error
74
95
  else
75
96
  @result
@@ -79,14 +100,36 @@ module Goru
79
100
  # [public]
80
101
  #
81
102
  def sleep(seconds)
82
- set_status(:idle)
83
- @reactor.routine_asleep(self, seconds)
103
+ set_status(STATUS_IDLE)
104
+ @reactor.asleep_for(seconds) do
105
+ set_status(STATUS_READY)
106
+ end
107
+
108
+ throw :continue
109
+ end
110
+
111
+ # [public]
112
+ #
113
+ def ready?
114
+ @status == STATUS_READY
115
+ end
116
+
117
+ # [public]
118
+ #
119
+ def finished?
120
+ @status == STATUS_ERRORED || @status == STATUS_FINISHED
84
121
  end
85
122
 
86
123
  # [public]
87
124
  #
88
- def wake
89
- set_status(:ready)
125
+ def pause
126
+ set_status(STATUS_PAUSED)
127
+ end
128
+
129
+ # [public]
130
+ #
131
+ def resume
132
+ set_status(STATUS_READY)
90
133
  end
91
134
 
92
135
  # [public]
@@ -99,10 +142,30 @@ module Goru
99
142
  # [public]
100
143
  #
101
144
  private def status_changed
145
+ @observers.each(&:call)
146
+
102
147
  case @status
103
- when :errored, :finished
148
+ when STATUS_ERRORED, STATUS_FINISHED
104
149
  @reactor&.routine_finished(self)
105
150
  end
106
151
  end
152
+
153
+ # [public]
154
+ #
155
+ def adopted
156
+ # noop
157
+ end
158
+
159
+ # [public]
160
+ #
161
+ def add_observer(observer = nil, &block)
162
+ @observers << (block || observer.method(:routine_status_changed))
163
+ end
164
+
165
+ # [public]
166
+ #
167
+ def remove_observer(observer)
168
+ @observers.delete(observer)
169
+ end
107
170
  end
108
171
  end
@@ -12,13 +12,14 @@ 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
18
19
  case @status
19
- when :ready
20
+ when Routine::STATUS_READY
20
21
  @reactor&.wakeup
21
- when :finished
22
+ when Routine::STATUS_FINISHED
22
23
  @channel.remove_observer(self)
23
24
  end
24
25
 
@@ -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
@@ -22,11 +16,11 @@ module Goru
22
16
 
23
17
  private def update_status
24
18
  status = if @channel.any?
25
- :ready
19
+ Routine::STATUS_READY
26
20
  elsif @channel.closed?
27
- :finished
21
+ Routine::STATUS_FINISHED
28
22
  else
29
- :idle
23
+ Routine::STATUS_IDLE
30
24
  end
31
25
 
32
26
  set_status(status)
@@ -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)
@@ -22,11 +16,11 @@ module Goru
22
16
 
23
17
  private def update_status
24
18
  status = if @channel.full?
25
- :idle
19
+ Routine::STATUS_IDLE
26
20
  elsif @channel.closed?
27
- :finished
21
+ Routine::STATUS_FINISHED
28
22
  else
29
- :ready
23
+ Routine::STATUS_READY
30
24
  end
31
25
 
32
26
  set_status(status)
@@ -1,51 +1,91 @@
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
+ # [public]
23
+ #
24
+ STATUS_IO_READY = :io_ready
25
+
22
26
  # [public]
23
27
  #
24
28
  attr_reader :io, :intent
25
29
 
26
- attr_writer :monitor
30
+ attr_accessor :monitor
31
+
32
+ # [public]
33
+ #
34
+ def adopted
35
+ set_status(Routine::STATUS_READY)
36
+ end
37
+
38
+ # [public]
39
+ #
40
+ def wakeup
41
+ # Keep this io from being selected again until the underlying routine is called.
42
+ # Interests are reset in `#call`.
43
+ #
44
+ @monitor&.interests = nil
45
+
46
+ set_status(STATUS_IO_READY)
47
+ end
48
+
49
+ READY_STATUSES = [STATUS_IO_READY, Routine::STATUS_READY].freeze
50
+ READY_BRIDGE_STATUSES = [nil, Bridge::STATUS_READY].freeze
51
+
52
+ # [public]
53
+ #
54
+ def ready?
55
+ READY_STATUSES.include?(@status) && READY_BRIDGE_STATUSES.include?(@bridge&.status)
56
+ end
57
+
58
+ def call
59
+ super
60
+
61
+ @monitor&.interests = @intent
62
+ end
27
63
 
28
64
  # [public]
29
65
  #
30
66
  def accept
31
67
  @io.accept_nonblock
68
+ rescue Errno::EAGAIN
69
+ wait
70
+ rescue Errno::ECONNRESET, Errno::EPIPE, EOFError
71
+ finished
72
+ nil
73
+ end
74
+
75
+ def wait
76
+ set_status(:selecting)
77
+ @reactor.register(self) unless @monitor
78
+
79
+ throw :continue
32
80
  end
33
81
 
34
82
  # [public]
35
83
  #
36
84
  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
85
+ @io.read_nonblock(bytes)
86
+ rescue Errno::EAGAIN
87
+ wait
88
+ rescue Errno::ECONNRESET, Errno::EPIPE, EOFError
49
89
  finished
50
90
  nil
51
91
  end
@@ -53,18 +93,10 @@ module Goru
53
93
  # [public]
54
94
  #
55
95
  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
96
+ @io.write_nonblock(data)
97
+ rescue Errno::EAGAIN
98
+ wait
99
+ rescue Errno::ECONNRESET, Errno::EPIPE, EOFError
68
100
  finished
69
101
  nil
70
102
  end
@@ -75,38 +107,56 @@ module Goru
75
107
  intent = normalize_intent(intent)
76
108
  validate_intent!(intent)
77
109
 
78
- @monitor.interests = intent
110
+ @monitor&.interests = intent
79
111
  @intent = intent
80
112
  end
81
113
 
82
114
  # [public]
83
115
  #
84
- def bridge(channel, intent:)
116
+ def bridge(state = nil, intent:, channel:, &block)
117
+ raise "routine is already bridged" if @bridge
118
+
85
119
  intent = normalize_intent(intent)
86
120
  validate_intent!(intent)
121
+ self.intent = intent
87
122
 
88
- bridge = case intent
123
+ @bridge = case intent
89
124
  when :r
90
125
  Bridges::Readable.new(routine: self, channel: channel)
91
126
  when :w
92
127
  Bridges::Writable.new(routine: self, channel: channel)
93
128
  end
94
129
 
95
- on_finished { bridge.finished }
96
- @reactor.adopt_routine(bridge)
97
- bridge
130
+ routine = case intent
131
+ when :r
132
+ Routines::Channels::Readable.new(state, channel: channel, &block)
133
+ when :w
134
+ Routines::Channels::Writable.new(state, channel: channel, &block)
135
+ end
136
+
137
+ @reactor.adopt_routine(routine)
138
+ @reactor.wakeup
139
+
140
+ routine
141
+ end
142
+
143
+ # [public]
144
+ #
145
+ def bridged
146
+ @reactor.wakeup
98
147
  end
99
148
 
100
149
  # [public]
101
150
  #
102
- def on_finished(&block)
103
- @finishers << block
151
+ def unbridge
152
+ @bridge = nil
153
+ @reactor.wakeup
104
154
  end
105
155
 
106
156
  private def status_changed
107
157
  case @status
108
- when :finished
109
- @finishers.each(&:call)
158
+ when Routine::STATUS_FINISHED
159
+ @reactor&.deregister(self)
110
160
  end
111
161
 
112
162
  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.4.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.4.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-16 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