fiber_scheduler 0.0.1 → 0.9.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: 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