goru 0.1.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: 5a78af9c418381f52acab486a5f32205f6ce73fdaf4327954fe551e6ca919b21
4
- data.tar.gz: 1185292bd3717ea47388afaa5bd3a914c869ff8ff07132e80b0f118a03e8eff1
3
+ metadata.gz: 65d57e96ef8a7bef1abaf43db02e461af067b60238728379b9a5e48cea91c2f9
4
+ data.tar.gz: feba54796b8120fe753014a9f904c29c3e379ef50910e6cfde0507ca85d4d695
5
5
  SHA512:
6
- metadata.gz: 5ea306fe3b3445a1eabfbfffc4f80d572f9dcecaee129cf2e28bc65e207f3b82b06d355d52431993765703c7e6e8fb959d5ea2886292160b96e781eb8fab48a3
7
- data.tar.gz: 497417e7c3dac2ab8454878e4cb869de978942d0bc60b14a908191d84d59be27a514373543f1fe431089eb0393157ae919941dfe210c1b6256f10ea6a5f7c54f
6
+ metadata.gz: c65b2730c39ec515c1ab44ee83909c4ea93c53072cb33887aebafaedfeb0131a37c0b3d1bb323b79ce21acdc5248be3fefa3d19c007c9aa5c56217e9a7195541
7
+ data.tar.gz: f4d7f05d63ad6ce781053946fa0d0e22ec9562b7e20fed83bca8f84441b4f6ec9c30b8b4b79fa7435cc93a76425ec793b6db7ac334aba0fddac84320777226b3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## [v0.3.0](https://github.com/bryanp/goru/releases/tag/v0.3.0)
2
+
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*
19
+
20
+ * `fix` [#6](https://github.com/bryanp/goru/pull/6) Finish routines on error ([bryanp](https://github.com/bryanp))
21
+ * `fix` [#5](https://github.com/bryanp/goru/pull/5) Correctly set channel status to `finished` when closed ([bryanp](https://github.com/bryanp))
22
+ * `chg` [#4](https://github.com/bryanp/goru/pull/4) Only log when routines are in debug mode ([bryanp](https://github.com/bryanp))
23
+ * `fix` [#3](https://github.com/bryanp/goru/pull/3) Update `Goru::Channel#full?` to always return a boolean ([bryanp](https://github.com/bryanp))
24
+ * `dep` [#2](https://github.com/bryanp/goru/pull/2) Remove ability to reopen channels ([bryanp](https://github.com/bryanp))
25
+
1
26
  ## [v0.1.0](https://github.com/bryanp/goru/releases/tag/v0.1.0)
2
27
 
3
28
  *released on 2023-03-29*
data/README.md CHANGED
@@ -122,8 +122,6 @@ Goru::Scheduler.go { |routine|
122
122
  }
123
123
  ```
124
124
 
125
- See [`core-handler`](https://github.com/bryanp/corerb/tree/main/handler) for more about error handling.
126
-
127
125
  ## Sleeping
128
126
 
129
127
  Goru implements a non-blocking version of `sleep` that makes the routine ineligible to be called until the sleep time
@@ -176,7 +174,7 @@ Goru includes a pattern for non-blocking io. With it you can implement non-block
176
174
  Routines that involve io must be created with an io object and an intent. Possible intents include:
177
175
 
178
176
  * `:r` for reading
179
- * `:r` for writing
177
+ * `:w` for writing
180
178
  * `:rw` for reading and writing
181
179
 
182
180
  Here is the beginning of an http server in Goru:
@@ -205,21 +203,31 @@ Goru::Scheduler.go(io: io, intent: :r) { |routine|
205
203
 
206
204
  ## Bridges
207
205
 
208
- Goru supports coordinates buffered io using bridges:
206
+ Goru supports coordinated buffered io using bridges:
209
207
 
210
208
  ```ruby
211
209
  writer = Goru::Channel.new
212
210
 
213
- Goru::Scheduler.go(io: io, intent: :w) { |routine|
214
- routine.bridge(writer, intent: :w)
215
- }
216
-
217
- Goru::Scheduler.go(channel: writer) { |routine|
218
- routine << SecureRandom.hex
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
219
222
  }
220
223
  ```
221
224
 
222
- This allows routines to easily write data to a buffer independently of how the data is written to io.
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.
223
231
 
224
232
  ## Credits
225
233
 
@@ -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/channel.rb CHANGED
@@ -46,7 +46,7 @@ module Goru
46
46
  # [public]
47
47
  #
48
48
  def full?
49
- @size && @messages.size == @size
49
+ !!@size && @messages.size == @size
50
50
  end
51
51
 
52
52
  # [public]
@@ -62,13 +62,6 @@ module Goru
62
62
  @observers.each(&:channel_closed)
63
63
  end
64
64
 
65
- # [public]
66
- #
67
- def reopen
68
- @closed = false
69
- @observers.each(&:channel_reopened)
70
- end
71
-
72
65
  # [public]
73
66
  #
74
67
  def clear
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
@@ -9,10 +9,12 @@ module Goru
9
9
  include Is::Handler
10
10
 
11
11
  handle(StandardError) do |event:|
12
- $stderr << <<~ERROR
13
- [goru] routine crashed: #{event}
14
- #{event.backtrace.join("\n")}
15
- ERROR
12
+ if @debug
13
+ $stderr << <<~ERROR
14
+ [goru] routine crashed: #{event}
15
+ #{event.backtrace.join("\n")}
16
+ ERROR
17
+ end
16
18
  end
17
19
 
18
20
  def initialize(state = nil, &block)
@@ -20,11 +22,16 @@ module Goru
20
22
  @block = block
21
23
  set_status(:ready)
22
24
  @result, @error, @reactor = nil
25
+ @debug = true
23
26
  end
24
27
 
25
28
  # [public]
26
29
  #
27
- attr_reader :state, :status
30
+ attr_reader :state, :status, :error
31
+
32
+ # [public]
33
+ #
34
+ attr_writer :debug
28
35
 
29
36
  # [public]
30
37
  #
@@ -46,10 +53,10 @@ module Goru
46
53
  # [public]
47
54
  #
48
55
  def finished(result = nil)
49
- unless @finished
50
- @result = result
51
- set_status(:finished)
52
- end
56
+ @result = result
57
+ set_status(:finished)
58
+
59
+ throw :continue
53
60
  end
54
61
 
55
62
  # [public]
@@ -73,13 +80,17 @@ module Goru
73
80
  #
74
81
  def sleep(seconds)
75
82
  set_status(:idle)
76
- @reactor.routine_asleep(self, seconds)
83
+ @reactor.asleep_for(seconds) do
84
+ set_status(:ready)
85
+ end
86
+
87
+ throw :continue
77
88
  end
78
89
 
79
90
  # [public]
80
91
  #
81
- def wake
82
- set_status(:ready)
92
+ def ready?
93
+ @status == :ready
83
94
  end
84
95
 
85
96
  # [public]
@@ -93,9 +104,15 @@ module Goru
93
104
  #
94
105
  private def status_changed
95
106
  case @status
96
- when :finished
107
+ when :errored, :finished
97
108
  @reactor&.routine_finished(self)
98
109
  end
99
110
  end
111
+
112
+ # [public]
113
+ #
114
+ def adopted
115
+ # noop
116
+ end
100
117
  end
101
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
@@ -36,10 +37,6 @@ module Goru
36
37
  def channel_closed
37
38
  update_status
38
39
  end
39
-
40
- def channel_reopened
41
- update_status
42
- end
43
40
  end
44
41
  end
45
42
  end
@@ -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)
@@ -24,7 +18,7 @@ module Goru
24
18
  status = if @channel.full?
25
19
  :idle
26
20
  elsif @channel.closed?
27
- :idle
21
+ :finished
28
22
  else
29
23
  :ready
30
24
  end
@@ -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.1.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.1.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-03-30 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,50 +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
-
45
- def channel_reopened
46
- update_status
47
- end
48
- end
49
- end
50
- 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
- :idle
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