fiber_scheduler 0.0.1 → 0.9.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: fd5761664b48ce8937367c38ae5424c2aad1ffcbac56fac2c002de020f08b6fc
4
- data.tar.gz: 426a54cbbd96a1b5eb54794b2a6b295be2af98413cbfb7978780ca57ff8347f7
3
+ metadata.gz: 5b809dc12fe4aea6e8bc0bf88eab0405545abd124d7e088f86815f3ddc64ae9f
4
+ data.tar.gz: 1443f88deb315b612aefb3702bf307feefa5a32ea04cecb394bc078e27513f01
5
5
  SHA512:
6
- metadata.gz: 9cdf31791771ecd2fec6fee05a7849954e71a2d6596ea429ec99300a558bd8c129a8e402e7bcae6c1c093765ac9738b4944e70de26a423ae62c440f4c9d10e18
7
- data.tar.gz: 4cf8d6bd02b7a51ef03e902da0bb25bbd5fae16ebf82c1b5aa7cf68d84b723b458be1b9106258fff527e17704ffba3850a0b377b735bf21c0638b62c7930d20d
6
+ metadata.gz: 4fdb35be7a59a1fba021ecbce10c496e2af16828ab2d76cd22933360322f31fcf6b7f72cfb3bca88588684f51e3445ee66573a05cb480d6b6e1bed4905a49a6e
7
+ data.tar.gz: 4f933bdb78178126c8d181bb52806157c13ef1b5d736d2fc202a1a5c6667eb6ce960c48f7c2123fc053454309a2188774c246cb524f52414b7d2dbc1fae9cc5d
@@ -0,0 +1,230 @@
1
+ # Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ # The code in this class has been taken from io-event gem. It has been cleaned
22
+ # up and appropriated for use in this gem.
23
+
24
+ class FiberScheduler
25
+ class Selector
26
+ EAGAIN = Errno::EAGAIN::Errno
27
+
28
+ class Waiter
29
+ def initialize(fiber, events, tail)
30
+ @fiber = fiber
31
+ @events = events
32
+ @tail = tail
33
+ end
34
+
35
+ def alive?
36
+ @fiber&.alive?
37
+ end
38
+
39
+ def transfer(events)
40
+ if (fiber = @fiber)
41
+ @fiber = nil
42
+
43
+ fiber.transfer(events & @events) if fiber.alive?
44
+ end
45
+
46
+ @tail&.transfer(events)
47
+ end
48
+
49
+ def invalidate
50
+ @fiber = nil
51
+ end
52
+
53
+ def each(&block)
54
+ if (fiber = @fiber)
55
+ yield fiber, @events
56
+ end
57
+
58
+ @tail&.each(&block)
59
+ end
60
+ end
61
+
62
+ def initialize(fiber)
63
+ @fiber = fiber
64
+
65
+ @waiting = {}.compare_by_identity
66
+ @ready = []
67
+ end
68
+
69
+ def close
70
+ @fiber = nil
71
+ @waiting = nil
72
+ @ready = nil
73
+ end
74
+
75
+ def transfer
76
+ @fiber.transfer
77
+ end
78
+
79
+ def push(fiber)
80
+ @ready.push(fiber)
81
+ end
82
+
83
+ def io_wait(fiber, io, events)
84
+ waiter = @waiting[io] = Waiter.new(fiber, events, @waiting[io])
85
+
86
+ @fiber.transfer
87
+ ensure
88
+ waiter&.invalidate
89
+ end
90
+
91
+ def io_read(fiber, io, buffer, length)
92
+ offset = 0
93
+
94
+ loop do
95
+ maximum_size = buffer.size - offset
96
+
97
+ result = Fiber.new(blocking: true) {
98
+ io.read_nonblock(maximum_size, exception: false)
99
+ }.resume
100
+
101
+ case result
102
+ when :wait_readable
103
+ if length > 0
104
+ io_wait(fiber, io, IO::READABLE)
105
+ else
106
+ return -EAGAIN
107
+ end
108
+ when :wait_writable
109
+ if length > 0
110
+ io_wait(fiber, io, IO::WRITABLE)
111
+ else
112
+ return -EAGAIN
113
+ end
114
+ when nil
115
+ break
116
+ else
117
+ buffer.set_string(result, offset)
118
+
119
+ size = result.bytesize
120
+ offset += size
121
+ break if size >= length
122
+ length -= size
123
+ end
124
+ end
125
+
126
+ offset
127
+ end
128
+
129
+ def io_write(fiber, io, buffer, length)
130
+ offset = 0
131
+
132
+ loop do
133
+ maximum_size = buffer.size - offset
134
+
135
+ chunk = buffer.get_string(offset, maximum_size)
136
+ result = Fiber.new(blocking: true) {
137
+ io.write_nonblock(chunk, exception: false)
138
+ }.resume
139
+
140
+ case result
141
+ when :wait_readable
142
+ if length > 0
143
+ io_wait(fiber, io, IO::READABLE)
144
+ else
145
+ return -EAGAIN
146
+ end
147
+ when :wait_writable
148
+ if length > 0
149
+ io_wait(fiber, io, IO::WRITABLE)
150
+ else
151
+ return -EAGAIN
152
+ end
153
+ else
154
+ offset += result
155
+ break if result >= length
156
+ length -= result
157
+ end
158
+ end
159
+
160
+ offset
161
+ end
162
+
163
+ def process_wait(fiber, pid, flags)
164
+ reader, writer = IO.pipe
165
+
166
+ thread = Thread.new do
167
+ Process::Status.wait(pid, flags)
168
+ ensure
169
+ writer.close
170
+ end
171
+
172
+ io_wait(fiber, reader, IO::READABLE)
173
+
174
+ thread.value
175
+ ensure
176
+ reader.close
177
+ writer.close
178
+ thread&.kill
179
+ end
180
+
181
+ def select(duration = nil)
182
+ if @ready.any?
183
+ # If we have popped items from the ready list, they may influence the
184
+ # duration calculation, so we don't delay the event loop:
185
+ duration = 0
186
+
187
+ count = @ready.size
188
+ count.times do
189
+ fiber = @ready.shift
190
+ fiber.transfer if fiber.alive?
191
+ end
192
+ end
193
+
194
+ readable = []
195
+ writable = []
196
+
197
+ @waiting.each do |io, waiter|
198
+ waiter.each do |fiber, events|
199
+ if (events & IO::READABLE) > 0
200
+ readable << io
201
+ end
202
+
203
+ if (events & IO::WRITABLE) > 0
204
+ writable << io
205
+ end
206
+ end
207
+ end
208
+
209
+ duration = 0 if @ready.any?
210
+ readable, writable, _ = IO.select(readable, writable, nil, duration)
211
+
212
+ ready = Hash.new(0)
213
+
214
+ readable&.each do |io|
215
+ ready[io] |= IO::READABLE
216
+ end
217
+
218
+ writable&.each do |io|
219
+ ready[io] |= IO::WRITABLE
220
+ end
221
+
222
+ ready.each do |io, events|
223
+ waiter = @waiting.delete(io)
224
+ waiter.transfer(events)
225
+ end
226
+
227
+ ready.size
228
+ end
229
+ end
230
+ end
@@ -1,12 +1,18 @@
1
1
  class FiberScheduler
2
- class Trigger
2
+ Error = Class.new(RuntimeError)
3
+
4
+ class Timeout
3
5
  include Comparable
4
6
 
7
+ Error = Class.new(FiberScheduler::Error)
8
+
5
9
  attr_reader :time
6
10
 
7
- def initialize(duration, &block)
11
+ def initialize(duration, fiber, method, *args)
8
12
  @time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
9
- @block = block
13
+ @fiber = fiber
14
+ @method = method
15
+ @args = args
10
16
 
11
17
  @disabled = nil
12
18
  end
@@ -18,7 +24,9 @@ class FiberScheduler
18
24
  end
19
25
 
20
26
  def call
21
- @block.call
27
+ return unless @fiber.alive?
28
+
29
+ @fiber.public_send(@method, *@args)
22
30
  end
23
31
 
24
32
  def interval
@@ -0,0 +1,79 @@
1
+ require_relative "timeout"
2
+
3
+ class FiberScheduler
4
+ class Timeouts
5
+ attr_reader :timeouts
6
+
7
+ def initialize
8
+ # Array is sorted by Timeout#time
9
+ @timeouts = []
10
+ end
11
+
12
+ def call
13
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
14
+
15
+ while @timeouts.any? && @timeouts.first.time <= now
16
+ timeout = @timeouts.shift
17
+ unless timeout.disabled?
18
+ timeout.call
19
+ end
20
+ end
21
+ end
22
+
23
+ def timeout(duration, *args, method: :raise, fiber: Fiber.current, &block)
24
+ timeout = Timeout.new(duration, fiber, method, *args)
25
+
26
+ if @timeouts.empty?
27
+ @timeouts << timeout
28
+ else
29
+ # binary search
30
+ min = 0
31
+ max = @timeouts.size - 1
32
+ while min <= max
33
+ index = (min + max) / 2
34
+ t = @timeouts[index]
35
+
36
+ if t > timeout
37
+ if index.zero? || @timeouts[index - 1] <= timeout
38
+ # found it
39
+ break
40
+ else
41
+ # @timeouts[index - 1] > timeout
42
+ max = index - 1
43
+ end
44
+ else
45
+ # t <= timeout
46
+ index += 1
47
+ min = index
48
+ end
49
+ end
50
+
51
+ @timeouts.insert(index, timeout)
52
+ end
53
+
54
+ begin
55
+ block.call
56
+ ensure
57
+ # Timeout is disabled if the block finishes earlier.
58
+ timeout.disable
59
+ end
60
+ end
61
+
62
+ def interval
63
+ # Prune disabled timeouts
64
+ while @timeouts.first&.disabled?
65
+ @timeouts.shift
66
+ end
67
+
68
+ return if @timeouts.empty?
69
+
70
+ interval = @timeouts.first.interval
71
+
72
+ interval >= 0 ? interval : 0
73
+ end
74
+
75
+ def inspect
76
+ @timeouts.inspect
77
+ end
78
+ end
79
+ end
@@ -1,3 +1,3 @@
1
1
  class FiberScheduler
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "0.9.0".freeze
3
3
  end
@@ -1,9 +1,15 @@
1
- require "io/event"
2
1
  require "resolv"
3
- require_relative "fiber_scheduler/triggers"
2
+ require_relative "fiber_scheduler/selector"
3
+ require_relative "fiber_scheduler/timeouts"
4
+
5
+ begin
6
+ # Use io/event selector if available
7
+ require "io/event"
8
+ rescue LoadError
9
+ end
4
10
 
5
11
  module Kernel
6
- def FiberScheduler(&block)
12
+ def FiberScheduler(blocking: false, waiting: true, &block)
7
13
  if Fiber.scheduler.nil?
8
14
  scheduler = FiberScheduler.new
9
15
  Fiber.set_scheduler(scheduler)
@@ -14,20 +20,59 @@ module Kernel
14
20
  ensure
15
21
  Fiber.set_scheduler(nil)
16
22
  end
23
+
17
24
  else
18
- # Fiber.scheduler already set, just schedule a task.
19
- Fiber.schedule(&block)
25
+ scheduler = Fiber.scheduler
26
+ # Fiber.scheduler already set, just schedule a fiber.
27
+ if scheduler.is_a?(FiberScheduler)
28
+ # The default waiting is 'true' as that is the most intuitive behavior
29
+ # for a nested FiberScheduler call.
30
+ Fiber.schedule(blocking: blocking, waiting: waiting, &block)
31
+
32
+ # Unknown fiber scheduler class; can't just pass options to
33
+ # Fiber.schedule, handle each option separately.
34
+ elsif blocking
35
+ Fiber.new(blocking: true, &block).tap(&:resume)
36
+
37
+ elsif waiting
38
+ current = Fiber.current
39
+ finished = false # prevents races
40
+ Fiber.schedule do
41
+ block.call
42
+ ensure
43
+ # Resume waiting parent fiber
44
+ finished = true
45
+ scheduler.unblock(nil, current)
46
+ end
47
+
48
+ if Fiber.blocking?
49
+ # In a blocking fiber, which is potentially also a loopo fiber so
50
+ # there's nothing we can transfer to. Run other fibers (or just
51
+ # block) until waiting fiber finishes.
52
+ until finished
53
+ scheduler.run_once
54
+ end
55
+ else
56
+ scheduler.block(nil, nil) unless finished
57
+ end
58
+
59
+ else
60
+ Fiber.schedule(&block)
61
+ end
20
62
  end
21
63
  end
22
64
  end
23
65
 
24
66
  class FiberScheduler
25
- TimeoutError = Class.new(RuntimeError)
26
- IOWaitTimeout = Class.new(TimeoutError)
27
-
28
67
  def initialize
29
- @selector = IO::Event::Selector.new(Fiber.current)
30
- @triggers = Triggers.new
68
+ @fiber = Fiber.current
69
+ @selector =
70
+ if defined?(IO::Event)
71
+ IO::Event::Selector.new(@fiber)
72
+ else
73
+ Selector.new(@fiber)
74
+ end
75
+ @timeouts = Timeouts.new
31
76
 
32
77
  @count = 0
33
78
  @nested = []
@@ -35,14 +80,18 @@ class FiberScheduler
35
80
 
36
81
  def run
37
82
  while @count > 0
38
- if @nested.empty?
39
- @selector.select(@triggers.interval)
40
- @triggers.call
41
- else
42
- while @nested.any?
43
- fiber = @nested.pop
44
- fiber.transfer
45
- end
83
+ run_once
84
+ end
85
+ end
86
+
87
+ def run_once
88
+ if @nested.empty?
89
+ @selector.select(@timeouts.interval)
90
+ @timeouts.call
91
+ else
92
+ while @nested.any?
93
+ fiber = @nested.pop
94
+ fiber.transfer
46
95
  end
47
96
  end
48
97
  end
@@ -60,18 +109,11 @@ class FiberScheduler
60
109
  end
61
110
  end
62
111
 
63
- def block(blocker, timeout)
64
- return @selector.transfer unless timeout
65
-
66
- fiber = Fiber.current
67
- trigger = @triggers.add(timeout) do
68
- fiber.transfer if fiber.alive?
69
- end
112
+ def block(blocker, duration = nil)
113
+ return @selector.transfer unless duration
70
114
 
71
- begin
115
+ @timeouts.timeout(duration, method: :transfer) do
72
116
  @selector.transfer
73
- ensure
74
- trigger.disable
75
117
  end
76
118
  end
77
119
 
@@ -89,21 +131,11 @@ class FiberScheduler
89
131
  Resolv.getaddresses(hostname)
90
132
  end
91
133
 
92
- def io_wait(io, events, timeout = nil)
93
- fiber = Fiber.current
94
- return @selector.io_wait(fiber, io, events) unless timeout
95
-
96
- trigger = @triggers.raise_in(timeout, IOWaitTimeout)
97
- # trigger = @triggers.add(timeout) do
98
- # fiber.raise(IOWaitTimeout) if fiber.alive?
99
- # end
134
+ def io_wait(io, events, duration = nil)
135
+ return @selector.io_wait(Fiber.current, io, events) unless duration
100
136
 
101
- begin
102
- @selector.io_wait(fiber, io, events)
103
- rescue IOWaitTimeout
104
- false
105
- ensure
106
- trigger.disable
137
+ @timeouts.timeout(duration, method: :transfer) do
138
+ @selector.io_wait(Fiber.current, io, events)
107
139
  end
108
140
  end
109
141
 
@@ -119,32 +151,54 @@ class FiberScheduler
119
151
  @selector.process_wait(Fiber.current, pid, flags)
120
152
  end
121
153
 
122
- def timeout_after(duration, exception = TimeoutError, message = "timeout")
123
- fiber = Fiber.current
124
- trigger = @triggers.add(duration) do
125
- fiber.raise(exception, message) if fiber.alive?
126
- end
127
-
128
- begin
129
- yield duration
130
- ensure
131
- trigger.disable
132
- end
154
+ def timeout_after(duration, exception = Timeout::Error, message = "timeout", &block)
155
+ @timeouts.timeout(duration, exception, message, &block)
133
156
  end
134
157
 
135
- def fiber(&block)
136
- unless Fiber.blocking?
137
- # nested Fiber.schedule
138
- @nested << Fiber.current
139
- end
158
+ def fiber(blocking: false, waiting: false, &block)
159
+ current = Fiber.current
160
+
161
+ if blocking
162
+ # All fibers wait on a blocking fiber, so 'waiting' option is ignored.
163
+ Fiber.new(blocking: true, &block).tap(&:resume)
164
+ elsif waiting
165
+ finished = false # prevents races
166
+ fiber = Fiber.new(blocking: false) do
167
+ @count += 1
168
+ block.call
169
+ ensure
170
+ @count -= 1
171
+ finished = true
172
+ # Resume waiting parent fiber
173
+ current.transfer
174
+ end
175
+ fiber.transfer
176
+
177
+ # Current fiber is waiting until waiting fiber finishes.
178
+ if current == @fiber
179
+ # In a top-level fiber, there's nothing we can transfer to, so run
180
+ # other fibers (or just block) until waiting fiber finishes.
181
+ until finished
182
+ run_once
183
+ end
184
+ else
185
+ @selector.transfer unless finished
186
+ end
140
187
 
141
- fiber = Fiber.new(blocking: false) do
142
- @count += 1
143
- block.call
144
- ensure
145
- @count -= 1
146
- end
188
+ fiber
189
+ else
190
+ if current != @fiber
191
+ # nested Fiber.schedule
192
+ @nested << current
193
+ end
147
194
 
148
- fiber.tap(&:transfer)
195
+ fiber = Fiber.new(blocking: false) do
196
+ @count += 1
197
+ block.call
198
+ ensure
199
+ @count -= 1
200
+ end
201
+ fiber.tap(&:transfer)
202
+ end
149
203
  end
150
204
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fiber_scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Sutic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-11 00:00:00.000000000 Z
11
+ date: 2022-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: io-event
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: async
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -39,33 +25,33 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '2'
41
27
  - !ruby/object:Gem::Dependency
42
- name: rspec
28
+ name: fiber_scheduler_spec
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - "~>"
46
32
  - !ruby/object:Gem::Version
47
- version: '3.11'
33
+ version: '0.0'
48
34
  type: :development
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
38
  - - "~>"
53
39
  - !ruby/object:Gem::Version
54
- version: '3.11'
40
+ version: '0.0'
55
41
  - !ruby/object:Gem::Dependency
56
- name: rubocop-rspec
42
+ name: rspec
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: '2.8'
47
+ version: '3.11'
62
48
  type: :development
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: '2.8'
54
+ version: '3.11'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: standard
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -88,8 +74,9 @@ extra_rdoc_files: []
88
74
  files:
89
75
  - lib/fiber/scheduler.rb
90
76
  - lib/fiber_scheduler.rb
91
- - lib/fiber_scheduler/trigger.rb
92
- - lib/fiber_scheduler/triggers.rb
77
+ - lib/fiber_scheduler/selector.rb
78
+ - lib/fiber_scheduler/timeout.rb
79
+ - lib/fiber_scheduler/timeouts.rb
93
80
  - lib/fiber_scheduler/version.rb
94
81
  homepage: https://github.com/bruno-/fiber_scheduler
95
82
  licenses:
@@ -1,72 +0,0 @@
1
- require_relative "trigger"
2
-
3
- class FiberScheduler
4
- class Triggers
5
- def initialize
6
- # Array is sorted by Trigger#time
7
- @triggers = []
8
- end
9
-
10
- def call
11
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12
-
13
- while @triggers.any? && @triggers.first.time <= now
14
- trigger = @triggers.shift
15
- unless trigger.disabled?
16
- trigger.call
17
- end
18
- end
19
- end
20
-
21
- def add(duration, &block)
22
- trigger = Trigger.new(duration, &block)
23
-
24
- if @triggers.empty?
25
- @triggers << trigger
26
- return trigger
27
- end
28
-
29
- # binary search
30
- min = 0
31
- max = @triggers.size - 1
32
- while min <= max
33
- index = (min + max) / 2
34
- t = @triggers[index]
35
-
36
- if t > trigger
37
- if index.zero? || @triggers[index - 1] <= trigger
38
- # found it
39
- break
40
- else
41
- # @triggers[index - 1] > trigger
42
- max = index - 1
43
- end
44
- else
45
- # t <= trigger
46
- index += 1
47
- min = index
48
- end
49
- end
50
-
51
- @triggers.insert(index, trigger)
52
- trigger
53
- end
54
-
55
- def interval
56
- # Prune disabled triggers
57
- while @triggers.first&.disabled?
58
- @triggers.shift
59
- end
60
-
61
- return if @triggers.empty?
62
-
63
- interval = @triggers.first.interval
64
-
65
- interval >= 0 ? interval : 0
66
- end
67
-
68
- def inspect
69
- @triggers.inspect
70
- end
71
- end
72
- end