goru 0.0.0 → 0.1.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: 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