goru 0.0.0 → 0.1.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: 2052d47d66797e7a2a268ec567209e6e5a41888f3f7e805ed4883f6534c61e72
4
- data.tar.gz: c154d10da685ffc931d7e2c3e6385c18c4cd534f593aa286babd02c13296fd6f
3
+ metadata.gz: 5a78af9c418381f52acab486a5f32205f6ce73fdaf4327954fe551e6ca919b21
4
+ data.tar.gz: 1185292bd3717ea47388afaa5bd3a914c869ff8ff07132e80b0f118a03e8eff1
5
5
  SHA512:
6
- metadata.gz: b82cfe65acf08ecb2f584b6eb5817c3dd20ad59474d86525e744e0745d34f46dacfbb66ae73e16897965e43e57d4ac8b39bcc6f61141a686314a5ad9a3f71c42
7
- data.tar.gz: a2aae75a244ca2d69acd1102fc42da12519eea8221995ce49b7eb2ff00c4f19ad4a07362efc66d833b28f3335ecc12d26333fd0610278b3b04129a2a4fe4e306
6
+ metadata.gz: 5ea306fe3b3445a1eabfbfffc4f80d572f9dcecaee129cf2e28bc65e207f3b82b06d355d52431993765703c7e6e8fb959d5ea2886292160b96e781eb8fab48a3
7
+ data.tar.gz: 497417e7c3dac2ab8454878e4cb869de978942d0bc60b14a908191d84d59be27a514373543f1fe431089eb0393157ae919941dfe210c1b6256f10ea6a5f7c54f
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## [v0.1.0](https://github.com/bryanp/goru/releases/tag/v0.1.0)
2
+
3
+ *released on 2023-03-29*
4
+
5
+ * `add` [#1](https://github.com/bryanp/featuring) Initial implementation ([bryanp](https://github.com/bryanp))
6
+
7
+
data/README.md ADDED
@@ -0,0 +1,226 @@
1
+ **Concurrent routines for Ruby.**
2
+
3
+ Goru is an experimental concurrency library for Ruby.
4
+
5
+ * **Lightweight:** Goru routines are not backed by fibers or threads. Each routine creates only ~345 bytes of memory overhead.
6
+ * **Explicit:** Goru requires you to describe exactly how a routine behaves. Less magic makes for fewer bugs when writing concurrent programs.
7
+
8
+ Goru was intended for low-level programs like http servers and not for direct use in user-facing code.
9
+
10
+ ## How It Works
11
+
12
+ Routines are defined with initial state and a block that does work and (optionally) updates the state of the routine:
13
+
14
+ ```ruby
15
+ 3.times do
16
+ Goru::Scheduler.go(:running) { |routine|
17
+ case routine.state
18
+ when :running
19
+ routine.update(:sleeping)
20
+ routine.sleep(rand)
21
+ when :sleeping
22
+ puts "[#{object_id}] woke up at #{Time.now.to_f}"
23
+ routine.update(:running)
24
+ end
25
+ }
26
+ end
27
+ ```
28
+
29
+ Routines run concurrently within a reactor, each reactor running in a dedicated thread. Each eligible routine is called
30
+ once on every tick of the reactor it is scheduled to run in. In the example above, the three routines sleep for a random
31
+ interval before waking up and printing the current time. Here is some example output:
32
+
33
+ ```
34
+ [1840] woke up at 1677939216.379147
35
+ [1860] woke up at 1677939217.059535
36
+ [1920] woke up at 1677939217.190349
37
+ [1860] woke up at 1677939217.6196458
38
+ [1920] woke up at 1677939217.935916
39
+ [1840] woke up at 1677939218.033243
40
+ [1860] woke up at 1677939218.532908
41
+ [1920] woke up at 1677939218.8669178
42
+ [1840] woke up at 1677939219.379714
43
+ [1860] woke up at 1677939219.522777
44
+ [1920] woke up at 1677939220.0475688
45
+ [1840] woke up at 1677939220.253979
46
+ ```
47
+
48
+ Each reactor can only run one routine at any given point in time, but if a routine blocks (e.g. by sleeping or
49
+ performing i/o) the reactor calls another eligible routine before returning to the previously blocked routine
50
+ on the next tick.
51
+
52
+ ## Scheduler
53
+
54
+ By default Goru routines are scheduled in a global scheduler that waits at the end of the program for all routines
55
+ to finish. While this is useful for small scripts, most use-cases will involve creating your own scheduler and
56
+ registering routines directly:
57
+
58
+ ```ruby
59
+ scheduler = Goru::Scheduler.new
60
+ scheduler.go { |routine|
61
+ ...
62
+ }
63
+ scheduler.wait
64
+ ```
65
+
66
+ Routines are scheduled to run immediately after registration.
67
+
68
+ ### Tuning
69
+
70
+ Schedulers default to running a number of reactors matching the number of processors on the current system. Tune
71
+ this to your needs with the `count` option when creating a scheduler:
72
+
73
+ ```ruby
74
+ scheduler = Goru::Scheduler.new(count: 3)
75
+ ```
76
+
77
+ ## State
78
+
79
+ Routines are initialized with default state that is useful for coordination between ticks. This is perhaps the
80
+ oddest part of Goru but the explicitness can make it easier to understand exactly how your routines will behave.
81
+
82
+ Take a look at the [examples](./examples) to get some ideas.
83
+
84
+ ## Finishing
85
+
86
+ Routines will run forever until you say they are finished:
87
+
88
+ ```ruby
89
+ Goru::Scheduler.go { |routine|
90
+ routine.finished
91
+ }
92
+ ```
93
+
94
+ ### Results
95
+
96
+ When finishing a routine you can provide a final result:
97
+
98
+ ```ruby
99
+ routines = []
100
+ scheduler = Goru::Scheduler.new
101
+ routines << scheduler.go { |routine| routine.finished(true) }
102
+ routines << scheduler.go { |routine| routine.finished(false) }
103
+ routines << scheduler.go { |routine| routine.finished(true) }
104
+ scheduler.wait
105
+
106
+ pp routines.map(&:result)
107
+ # [true, false, true]
108
+ ```
109
+
110
+ ## Error Handling
111
+
112
+ Unhandled errors within a routine cause the routine to enter an `:errored` state. Calling `result` on an errored
113
+ routine causes the error to be re-raised. Routines can handle errors elegantly using the `handle` method:
114
+
115
+ ```ruby
116
+ Goru::Scheduler.go { |routine|
117
+ routine.handle(StandardError) do |event:|
118
+ # do something with `event`
119
+ end
120
+
121
+ ...
122
+ }
123
+ ```
124
+
125
+ See [`core-handler`](https://github.com/bryanp/corerb/tree/main/handler) for more about error handling.
126
+
127
+ ## Sleeping
128
+
129
+ Goru implements a non-blocking version of `sleep` that makes the routine ineligible to be called until the sleep time
130
+ has elapsed. It is important to note that Ruby's built-in sleep method will block the reactor and should not be used.
131
+
132
+ ```ruby
133
+ Goru::Scheduler.go { |routine|
134
+ routine.sleep(3)
135
+ }
136
+ ```
137
+
138
+ Unlike `Kernel#sleep` Goru's sleep method requires a duration.
139
+
140
+ ## Channels
141
+
142
+ Goru offers buffered reading and writing through channels:
143
+
144
+ ```ruby
145
+ channel = Goru::Channel.new
146
+
147
+ Goru::Scheduler.go(channel: channel, intent: :w) { |routine|
148
+ routine << SecureRandom.hex
149
+ }
150
+
151
+ # This routine is not invoked unless the channel contains data for reading.
152
+ #
153
+ Goru::Scheduler.go(channel: channel, intent: :r) { |routine|
154
+ value = routine.read
155
+ }
156
+ ```
157
+
158
+ Channels are unbounded by default, meaning they can hold an unlimited amount of data. This behavior can be changed by
159
+ initializing a channel with a specific size. Routines with the intent to write will not be invoked unless the channel
160
+ has space available for writing.
161
+
162
+ ```ruby
163
+ channel = Goru::Channel.new(size: 3)
164
+
165
+ # This routine is not invoked if the channel is full.
166
+ #
167
+ Goru::Scheduler.go(channel: channel, intent: :w) { |routine|
168
+ routine << SecureRandom.hex
169
+ }
170
+ ```
171
+
172
+ ## IO
173
+
174
+ Goru includes a pattern for non-blocking io. With it you can implement non-blocking servers, clients, etc.
175
+
176
+ Routines that involve io must be created with an io object and an intent. Possible intents include:
177
+
178
+ * `:r` for reading
179
+ * `:r` for writing
180
+ * `:rw` for reading and writing
181
+
182
+ Here is the beginning of an http server in Goru:
183
+
184
+ ```ruby
185
+ Goru::Scheduler.go(io: TCPServer.new("localhost", 4242), intent: :r) { |server_routine|
186
+ next unless client = server_routine.accept
187
+
188
+ Goru::Scheduler.go(io: client, intent: :r) { |client_routine|
189
+ next unless data = client_routine.read(16384)
190
+
191
+ # do something with `data`
192
+ }
193
+ }
194
+ ```
195
+
196
+ ### Changing Intents
197
+
198
+ Intents can be changed after a routine is created, e.g. to switch a routine from reading to writing:
199
+
200
+ ```ruby
201
+ Goru::Scheduler.go(io: io, intent: :r) { |routine|
202
+ routine.intent = :w
203
+ }
204
+ ```
205
+
206
+ ## Bridges
207
+
208
+ Goru supports coordinates buffered io using bridges:
209
+
210
+ ```ruby
211
+ writer = Goru::Channel.new
212
+
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
219
+ }
220
+ ```
221
+
222
+ This allows routines to easily write data to a buffer independently of how the data is written to io.
223
+
224
+ ## Credits
225
+
226
+ Goru was designed while writing a project in Go and imagining what Go-like concurrency might look like in Ruby.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./reactor"
4
+
5
+ module Goru
6
+ class Channel
7
+ def initialize(size: nil)
8
+ @size = size
9
+ @messages = []
10
+ @closed = false
11
+ @observers = Set.new
12
+ end
13
+
14
+ # [public]
15
+ #
16
+ attr_writer :observer
17
+
18
+ # [public]
19
+ #
20
+ def <<(message)
21
+ raise "closed" if @closed
22
+ @messages << message
23
+ @observers.each(&:channel_received)
24
+ end
25
+
26
+ # [public]
27
+ #
28
+ def read
29
+ message = @messages.shift
30
+ @observers.each(&:channel_read)
31
+ message
32
+ end
33
+
34
+ # [public]
35
+ #
36
+ def any?
37
+ @messages.any?
38
+ end
39
+
40
+ # [public]
41
+ #
42
+ def empty?
43
+ @messages.empty?
44
+ end
45
+
46
+ # [public]
47
+ #
48
+ def full?
49
+ @size && @messages.size == @size
50
+ end
51
+
52
+ # [public]
53
+ #
54
+ def closed?
55
+ @closed == true
56
+ end
57
+
58
+ # [public]
59
+ #
60
+ def close
61
+ @closed = true
62
+ @observers.each(&:channel_closed)
63
+ end
64
+
65
+ # [public]
66
+ #
67
+ def reopen
68
+ @closed = false
69
+ @observers.each(&:channel_reopened)
70
+ end
71
+
72
+ # [public]
73
+ #
74
+ def clear
75
+ @messages.clear
76
+ end
77
+
78
+ # [public]
79
+ #
80
+ def length
81
+ @messages.length
82
+ end
83
+
84
+ # [public]
85
+ #
86
+ def add_observer(observer)
87
+ @observers << observer
88
+ end
89
+
90
+ # [public]
91
+ #
92
+ def remove_observer(observer)
93
+ @observers.delete(observer)
94
+ end
95
+ end
96
+ end
data/lib/goru/reactor.rb CHANGED
@@ -5,6 +5,7 @@ require "nio"
5
5
  require "timers/group"
6
6
  require "timers/wait"
7
7
 
8
+ require_relative "routines/bridge"
8
9
  require_relative "routines/io"
9
10
 
10
11
  module Goru
@@ -14,12 +15,13 @@ module Goru
14
15
  def initialize(queue:, scheduler:)
15
16
  @queue = queue
16
17
  @scheduler = scheduler
17
- @routines = []
18
- @finished = []
18
+ @routines = Set.new
19
+ @bridges = Set.new
19
20
  @timers = Timers::Group.new
20
21
  @selector = NIO::Selector.new
21
- @status = nil
22
22
  @stopped = false
23
+ @status = nil
24
+ @mutex = Mutex.new
23
25
  end
24
26
 
25
27
  # [public]
@@ -29,58 +31,38 @@ module Goru
29
31
  # [public]
30
32
  #
31
33
  def run
32
- @status = :running
33
-
34
34
  until @stopped
35
+ set_status(:running)
36
+
35
37
  @routines.each do |routine|
36
38
  call_routine(routine)
37
39
  end
38
40
 
39
- # TODO: Remove the monitor from the selector.
40
- #
41
- cleanup_finished_routines
42
-
43
41
  begin
44
- if (routine = @queue.pop(true))
45
- adopt_routine(routine)
46
- end
42
+ wait_for_routine(block: false)
47
43
  rescue ThreadError
48
44
  interval = @timers.wait_interval
49
45
 
50
46
  if interval.nil?
51
47
  if @routines.empty?
52
48
  if @selector.empty?
53
- @status = :looking
54
- @scheduler.signal(self)
55
- if (routine = @queue.pop)
56
- adopt_routine(routine)
57
- end
49
+ become_idle
58
50
  else
59
- # TODO: The issue doing this is that this reactor won't grab new routines. Will calling `@selector.wakeup`
60
- # from the scheduler when a routine is added to the queue resolve this?
61
- #
62
- @selector.select do |monitor|
63
- monitor.value.call
51
+ wait_for_bridge do
52
+ wait_for_selector
64
53
  end
65
54
  end
66
55
  else
67
- @selector.select(0) do |monitor|
68
- monitor.value.call
56
+ wait_for_bridge do
57
+ wait_for_selector(0)
69
58
  end
70
59
  end
71
60
  elsif interval > 0
72
61
  if @selector.empty?
73
- Timers::Wait.for(interval) do |remaining|
74
- if (routine = @queue.pop_with_timeout(remaining))
75
- adopt_routine(routine)
76
- break
77
- end
78
- rescue ThreadError
79
- # nothing to do
80
- end
62
+ wait_for_interval(interval)
81
63
  else
82
- @selector.select(interval) do |monitor|
83
- monitor.value.call
64
+ wait_for_bridge do
65
+ wait_for_selector(interval)
84
66
  end
85
67
  end
86
68
  end
@@ -90,63 +72,127 @@ module Goru
90
72
  end
91
73
  ensure
92
74
  @selector.close
93
- @status = :finished
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
87
+ end
88
+ end
89
+
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
97
+
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
103
+ end
104
+ end
105
+
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))
112
+ adopt_routine(routine)
113
+ end
94
114
  end
95
115
 
96
116
  # [public]
97
117
  #
98
- def stop
99
- @stopped = true
118
+ def finished?
119
+ @mutex.synchronize do
120
+ @status == :idle || @status == :stopped
121
+ end
122
+ end
100
123
 
101
- unless @selector.closed?
124
+ # [public]
125
+ #
126
+ def signal
127
+ unless @selector.empty?
102
128
  @selector.wakeup
103
129
  end
104
130
  end
105
131
 
106
132
  # [public]
107
133
  #
108
- def sleep(routine, seconds)
134
+ def wakeup
135
+ signal
136
+ @queue << :wakeup
137
+ end
138
+
139
+ # [public]
140
+ #
141
+ def stop
142
+ @stopped = true
143
+ @selector.wakeup
144
+ rescue IOError
145
+ end
146
+
147
+ # [public]
148
+ #
149
+ def routine_asleep(routine, seconds)
109
150
  @timers.after(seconds) {
110
151
  routine.wake
111
152
  }
112
153
  end
113
154
 
114
- private def adopt_routine(routine)
115
- routine.reactor = self
116
-
155
+ # [public]
156
+ #
157
+ def adopt_routine(routine)
117
158
  case routine
118
159
  when Routines::IO
119
160
  monitor = @selector.register(routine.io, routine.intent)
120
-
121
- monitor.value = proc {
122
- # TODO: Try to combine this with `call_routine` below.
123
- #
124
- case routine.status
125
- when :selecting
126
- routine.call
127
- else
128
- @finished << routine
129
- end
130
- }
131
- else
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
132
169
  @routines << routine
133
170
  end
134
171
  end
135
172
 
136
- private def call_routine(routine)
137
- case routine.status
138
- when :running
139
- routine.call
140
- when :sleeping, :selecting
141
- # ignore these
173
+ # [public]
174
+ #
175
+ def routine_finished(routine)
176
+ case routine
177
+ when Routines::Bridge
178
+ @bridges.delete(routine)
179
+ when Routines::IO
180
+ @selector.deregister(routine.io)
142
181
  else
143
- @finished << routine
182
+ @routines.delete(routine)
183
+ end
184
+ end
185
+
186
+ private def set_status(status)
187
+ @mutex.synchronize do
188
+ @status = status
144
189
  end
145
190
  end
146
191
 
147
- private def cleanup_finished_routines
148
- until @finished.empty?
149
- @routines.delete(@finished.pop)
192
+ private def call_routine(routine)
193
+ case routine.status
194
+ when :ready
195
+ routine.call
150
196
  end
151
197
  end
152
198
  end
data/lib/goru/routine.rb CHANGED
@@ -1,13 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "is/handler"
4
+
3
5
  module Goru
4
6
  # [public]
5
7
  #
6
8
  class Routine
9
+ include Is::Handler
10
+
11
+ handle(StandardError) do |event:|
12
+ $stderr << <<~ERROR
13
+ [goru] routine crashed: #{event}
14
+ #{event.backtrace.join("\n")}
15
+ ERROR
16
+ end
17
+
7
18
  def initialize(state = nil, &block)
8
19
  @state = state
9
20
  @block = block
10
- @status = :running
21
+ set_status(:ready)
11
22
  @result, @error, @reactor = nil
12
23
  end
13
24
 
@@ -17,12 +28,9 @@ module Goru
17
28
 
18
29
  # [public]
19
30
  #
20
- attr_writer :reactor
21
-
22
- # [public]
23
- #
24
- def running?
25
- @status == :running
31
+ def reactor=(reactor)
32
+ @reactor = reactor
33
+ status_changed
26
34
  end
27
35
 
28
36
  # [public]
@@ -30,11 +38,9 @@ module Goru
30
38
  def call
31
39
  @block.call(self)
32
40
  rescue => error
33
- puts "[routine error] #{error}"
34
- puts error.backtrace
35
-
36
41
  @error = error
37
- @status = :errored
42
+ set_status(:errored)
43
+ trigger(error)
38
44
  end
39
45
 
40
46
  # [public]
@@ -42,7 +48,7 @@ module Goru
42
48
  def finished(result = nil)
43
49
  unless @finished
44
50
  @result = result
45
- @status = :finished
51
+ set_status(:finished)
46
52
  end
47
53
  end
48
54
 
@@ -66,14 +72,30 @@ module Goru
66
72
  # [public]
67
73
  #
68
74
  def sleep(seconds)
69
- @status = :sleeping
70
- @reactor.sleep(self, seconds)
75
+ set_status(:idle)
76
+ @reactor.routine_asleep(self, seconds)
71
77
  end
72
78
 
73
79
  # [public]
74
80
  #
75
81
  def wake
76
- @status = :running
82
+ set_status(:ready)
83
+ end
84
+
85
+ # [public]
86
+ #
87
+ private def set_status(status)
88
+ @status = status
89
+ status_changed
90
+ end
91
+
92
+ # [public]
93
+ #
94
+ private def status_changed
95
+ case @status
96
+ when :finished
97
+ @reactor&.routine_finished(self)
98
+ end
77
99
  end
78
100
  end
79
101
  end
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../routine"
4
+
5
+ module Goru
6
+ module Routines
7
+ # [public]
8
+ #
9
+ class Channel < Routine
10
+ def initialize(state = nil, channel:, &block)
11
+ super(state, &block)
12
+
13
+ @channel = channel
14
+ @channel.add_observer(self)
15
+ end
16
+
17
+ private def status_changed
18
+ case @status
19
+ when :ready
20
+ @reactor&.wakeup
21
+ when :finished
22
+ @channel.remove_observer(self)
23
+ end
24
+
25
+ super
26
+ end
27
+
28
+ def channel_received
29
+ update_status
30
+ end
31
+
32
+ def channel_read
33
+ update_status
34
+ end
35
+
36
+ def channel_closed
37
+ update_status
38
+ end
39
+
40
+ def channel_reopened
41
+ update_status
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../channel"
4
+
5
+ module Goru
6
+ module Routines
7
+ module Channels
8
+ # [public]
9
+ #
10
+ class Readable < Channel
11
+ def initialize(...)
12
+ super
13
+
14
+ update_status
15
+ end
16
+
17
+ # [public]
18
+ #
19
+ def read
20
+ @channel.read
21
+ end
22
+
23
+ private def update_status
24
+ status = if @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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../channel"
4
+
5
+ module Goru
6
+ module Routines
7
+ module Channels
8
+ # [public]
9
+ #
10
+ class Writable < Channel
11
+ def initialize(...)
12
+ super
13
+
14
+ update_status
15
+ end
16
+
17
+ # [public]
18
+ #
19
+ def <<(message)
20
+ @channel << message
21
+ end
22
+
23
+ private def update_status
24
+ status = if @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,24 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../routine"
4
+ require_relative "bridges/readable"
5
+ require_relative "bridges/writable"
4
6
 
5
7
  module Goru
6
8
  module Routines
7
9
  # [public]
8
10
  #
9
11
  class IO < Routine
10
- def initialize(state = nil, io:, intent: :rw, &block)
12
+ def initialize(state = nil, io:, intent:, &block)
11
13
  super(state, &block)
12
14
 
13
15
  @io = io
14
- @intent = intent
16
+ @intent = normalize_intent(intent)
15
17
  @status = :selecting
18
+ @monitor = nil
19
+ @finishers = []
16
20
  end
17
21
 
18
22
  # [public]
19
23
  #
20
24
  attr_reader :io, :intent
21
25
 
26
+ attr_writer :monitor
27
+
22
28
  # [public]
23
29
  #
24
30
  def accept
@@ -31,11 +37,89 @@ module Goru
31
37
  result = @io.read_nonblock(bytes, exception: false)
32
38
 
33
39
  case result
34
- when :wait_readable, nil
40
+ when nil
41
+ finished
42
+ nil
43
+ when :wait_readable
35
44
  # nothing to do
36
45
  else
37
46
  result
38
47
  end
48
+ rescue Errno::ECONNRESET
49
+ finished
50
+ nil
51
+ end
52
+
53
+ # [public]
54
+ #
55
+ 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
68
+ finished
69
+ nil
70
+ end
71
+
72
+ # [public]
73
+ #
74
+ def intent=(intent)
75
+ intent = normalize_intent(intent)
76
+ validate_intent!(intent)
77
+
78
+ @monitor.interests = intent
79
+ @intent = intent
80
+ end
81
+
82
+ # [public]
83
+ #
84
+ def bridge(channel, intent:)
85
+ intent = normalize_intent(intent)
86
+ validate_intent!(intent)
87
+
88
+ bridge = case intent
89
+ when :r
90
+ Bridges::Readable.new(routine: self, channel: channel)
91
+ when :w
92
+ Bridges::Writable.new(routine: self, channel: channel)
93
+ end
94
+
95
+ on_finished { bridge.finished }
96
+ @reactor.adopt_routine(bridge)
97
+ bridge
98
+ end
99
+
100
+ # [public]
101
+ #
102
+ def on_finished(&block)
103
+ @finishers << block
104
+ end
105
+
106
+ private def status_changed
107
+ case @status
108
+ when :finished
109
+ @finishers.each(&:call)
110
+ end
111
+
112
+ super
113
+ end
114
+
115
+ INTENTS = %i[r w].freeze
116
+
117
+ private def validate_intent!(intent)
118
+ raise ArgumentError, "unknown intent: #{intent}" unless INTENTS.include?(intent)
119
+ end
120
+
121
+ private def normalize_intent(intent)
122
+ intent.to_sym
39
123
  end
40
124
  end
41
125
  end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "etc"
3
4
  require "is/global"
4
5
 
5
- require_relative "queue"
6
+ require_relative "channel"
6
7
  require_relative "reactor"
8
+ require_relative "routines/channel"
7
9
  require_relative "routines/io"
8
10
 
11
+ require_relative "routines/channels/readable"
12
+ require_relative "routines/channels/writable"
13
+
9
14
  module Goru
10
15
  # [public]
11
16
  #
@@ -13,14 +18,30 @@ module Goru
13
18
  include Is::Global
14
19
  include MonitorMixin
15
20
 
16
- def initialize(...)
17
- super
21
+ class << self
22
+ # Prevent issues when including `Goru` at the toplevel.
23
+ #
24
+ # [public]
25
+ #
26
+ def go(...)
27
+ global.go(...)
28
+ end
29
+
30
+ # [public]
31
+ #
32
+ def default_scheduler_count
33
+ Etc.nprocessors
34
+ end
35
+ end
36
+
37
+ def initialize(count: self.class.default_scheduler_count)
38
+ super()
18
39
 
19
40
  @stopping = false
20
- @routines = Queue.new
41
+ @routines = Thread::Queue.new
21
42
  @condition = new_cond
22
43
 
23
- @reactors = 10.times.map {
44
+ @reactors = count.times.map {
24
45
  Reactor.new(queue: @routines, scheduler: self)
25
46
  }
26
47
 
@@ -35,12 +56,26 @@ module Goru
35
56
 
36
57
  # [public]
37
58
  #
38
- def go(state = nil, io: nil, intent: :rw, &block)
39
- @routines << if io
59
+ def go(state = nil, io: nil, channel: nil, intent: nil, &block)
60
+ raise ArgumentError, "cannot set both `io` and `channel`" if io && channel
61
+
62
+ routine = if io
40
63
  Routines::IO.new(state, io: io, intent: intent, &block)
64
+ elsif channel
65
+ case intent
66
+ when :r
67
+ Routines::Channels::Readable.new(state, channel: channel, &block)
68
+ when :w
69
+ Routines::Channels::Writable.new(state, channel: channel, &block)
70
+ end
41
71
  else
42
72
  Routine.new(state, &block)
43
73
  end
74
+
75
+ @routines << routine
76
+ @reactors.each(&:signal)
77
+
78
+ routine
44
79
  end
45
80
 
46
81
  # [public]
@@ -51,6 +86,7 @@ module Goru
51
86
  @stopping
52
87
  end
53
88
  end
89
+ rescue Interrupt
54
90
  ensure
55
91
  stop
56
92
  end
@@ -68,7 +104,7 @@ module Goru
68
104
  #
69
105
  def signal(reactor)
70
106
  synchronize do
71
- if @reactors.all? { |reactor| reactor.status == :looking || reactor.status == :stopped }
107
+ if @reactors.all?(&:finished?)
72
108
  @stopping = true
73
109
  end
74
110
 
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.0.0"
4
+ VERSION = "0.1.0"
5
5
 
6
6
  # [public]
7
7
  #
data/lib/goru.rb CHANGED
@@ -8,8 +8,8 @@ module Goru
8
8
 
9
9
  extend Is::Extension
10
10
 
11
- def go(state = nil, io: nil, intent: :rw, &block)
12
- Scheduler.go(state, io: io, intent: intent, &block)
11
+ def go(state = nil, io: nil, channel: nil, intent: nil, &block)
12
+ Scheduler.go(state, io: io, channel: channel, intent: intent, &block)
13
13
  end
14
14
  end
15
15
 
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.0.0
4
+ version: 0.1.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: 2022-01-06 00:00:00.000000000 Z
11
+ date: 2023-03-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: core-extension
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.4'
27
27
  - !ruby/object:Gem::Dependency
28
- name: core-global
28
+ name: core-handler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: core-global
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.2'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: nio4r
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -67,20 +81,28 @@ dependencies:
67
81
  - !ruby/object:Gem::Version
68
82
  version: '4.3'
69
83
  description: Concurrent routines for Ruby.
70
- email: bryan@metabahn.com
84
+ email: bryan@bryanp.org
71
85
  executables: []
72
86
  extensions: []
73
87
  extra_rdoc_files: []
74
88
  files:
89
+ - CHANGELOG.md
75
90
  - LICENSE
91
+ - README.md
76
92
  - lib/goru.rb
77
- - lib/goru/queue.rb
93
+ - lib/goru/channel.rb
78
94
  - lib/goru/reactor.rb
79
95
  - 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
+ - lib/goru/routines/channel.rb
100
+ - lib/goru/routines/channels/readable.rb
101
+ - lib/goru/routines/channels/writable.rb
80
102
  - lib/goru/routines/io.rb
81
103
  - lib/goru/scheduler.rb
82
104
  - lib/goru/version.rb
83
- homepage: https://github.com/metabahn/goru/
105
+ homepage: https://github.com/bryanp/goru/
84
106
  licenses:
85
107
  - MPL-2.0
86
108
  metadata: {}
@@ -92,14 +114,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
114
  requirements:
93
115
  - - ">="
94
116
  - !ruby/object:Gem::Version
95
- version: 2.6.7
117
+ version: 3.2.0
96
118
  required_rubygems_version: !ruby/object:Gem::Requirement
97
119
  requirements:
98
120
  - - ">="
99
121
  - !ruby/object:Gem::Version
100
122
  version: '0'
101
123
  requirements: []
102
- rubygems_version: 3.2.22
124
+ rubygems_version: 3.4.9
103
125
  signing_key:
104
126
  specification_version: 4
105
127
  summary: Concurrent routines for Ruby.
data/lib/goru/queue.rb DELETED
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Goru
4
- # [public] Based on: https://spin.atomicobject.com/2017/06/28/queue-pop-with-timeout-fixed/
5
- #
6
- class Queue
7
- def initialize
8
- @mutex = Mutex.new
9
- @queue = []
10
- @received = ConditionVariable.new
11
- @closed = false
12
- end
13
-
14
- # [public]
15
- #
16
- def <<(x)
17
- @mutex.synchronize do
18
- @queue << x
19
- @received.signal
20
- end
21
- end
22
-
23
- # [public]
24
- #
25
- def pop(non_block = false)
26
- pop_with_timeout(non_block ? 0 : nil)
27
- end
28
-
29
- # [public]
30
- #
31
- def pop_with_timeout(timeout = nil)
32
- @mutex.synchronize do
33
- if timeout.nil?
34
- # wait indefinitely until there is an element in the queue
35
- while @queue.empty? && !@closed
36
- @received.wait(@mutex)
37
- end
38
- elsif @queue.empty? && !@closed && timeout != 0
39
- # wait for element or timeout
40
- timeout_time = timeout + Time.now.to_f
41
- while @queue.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
42
- @received.wait(@mutex, remaining_time)
43
- end
44
- end
45
- return if @closed
46
- # if we're still empty after the timeout, raise exception
47
- raise ThreadError, "queue empty" if @queue.empty?
48
- @queue.shift
49
- end
50
- end
51
-
52
- # [public]
53
- #
54
- def close
55
- @mutex.synchronize do
56
- @closed = true
57
- @received.broadcast
58
- end
59
- end
60
- end
61
- end