polyphony 0.28 → 0.29
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 +4 -4
- data/.rubocop.yml +0 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/LICENSE +1 -1
- data/README.md +23 -21
- data/Rakefile +2 -0
- data/TODO.md +0 -3
- data/docs/_includes/prevnext.html +17 -0
- data/docs/_layouts/default.html +106 -0
- data/docs/_sass/custom/custom.scss +21 -0
- data/docs/faq.md +13 -10
- data/docs/getting-started/installing.md +2 -0
- data/docs/getting-started/tutorial.md +5 -3
- data/docs/index.md +4 -5
- data/docs/technical-overview/concurrency.md +21 -19
- data/docs/technical-overview/design-principles.md +12 -20
- data/docs/technical-overview/exception-handling.md +70 -1
- data/docs/technical-overview/extending.md +1 -0
- data/docs/technical-overview/fiber-scheduling.md +109 -88
- data/docs/user-guide/all-about-timers.md +126 -0
- data/docs/user-guide/web-server.md +2 -2
- data/docs/user-guide.md +1 -1
- data/examples/core/xx-deferring-an-operation.rb +2 -2
- data/examples/core/xx-sleep-forever.rb +9 -0
- data/examples/core/xx-snooze-starve.rb +16 -0
- data/examples/core/xx-spin_error_backtrace.rb +1 -1
- data/examples/core/xx-trace.rb +1 -2
- data/examples/core/xx-worker-thread.rb +30 -0
- data/examples/io/xx-happy-eyeballs.rb +37 -0
- data/ext/gyro/gyro.c +8 -3
- data/ext/gyro/gyro.h +7 -1
- data/ext/gyro/queue.c +35 -3
- data/ext/gyro/selector.c +31 -2
- data/ext/gyro/thread.c +18 -16
- data/lib/polyphony/core/global_api.rb +0 -1
- data/lib/polyphony/core/thread_pool.rb +5 -0
- data/lib/polyphony/core/throttler.rb +0 -1
- data/lib/polyphony/extensions/fiber.rb +14 -3
- data/lib/polyphony/extensions/thread.rb +16 -4
- data/lib/polyphony/irb.rb +7 -1
- data/lib/polyphony/trace.rb +44 -11
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +1 -0
- data/test/helper.rb +1 -3
- data/test/test_async.rb +1 -1
- data/test/test_cancel_scope.rb +3 -3
- data/test/test_fiber.rb +157 -54
- data/test/test_global_api.rb +51 -1
- data/test/test_gyro.rb +4 -156
- data/test/test_io.rb +1 -1
- data/test/test_supervisor.rb +2 -2
- data/test/test_thread.rb +72 -1
- data/test/test_thread_pool.rb +6 -2
- data/test/test_throttler.rb +7 -5
- data/test/test_trace.rb +6 -6
- metadata +10 -5
- data/examples/core/xx-extended_fibers.rb +0 -150
- data/examples/core/xx-mt-scheduler.rb +0 -349
@@ -4,19 +4,72 @@ title: How Fibers are Scheduled
|
|
4
4
|
nav_order: 3
|
5
5
|
parent: Technical Overview
|
6
6
|
permalink: /technical-overview/fiber-scheduling/
|
7
|
+
prev_title: Concurrency the Easy Way
|
8
|
+
next_title: Exception Handling
|
7
9
|
---
|
10
|
+
|
8
11
|
# How Fibers are Scheduled
|
9
12
|
|
13
|
+
Before we discuss how fibers are scheduled in Polyphony, let's examine how
|
14
|
+
switching between fibers works in Ruby.
|
15
|
+
|
10
16
|
Ruby provides two mechanisms for transferring control between fibers:
|
11
17
|
`Fiber#resume` /`Fiber.yield` and `Fiber#transfer`. The first is inherently
|
12
18
|
asymmetric and is famously used for implementing generators and [resumable
|
13
19
|
enumerators](https://blog.appsignal.com/2018/11/27/ruby-magic-fibers-and-enumerators-in-ruby.html).
|
14
|
-
|
15
|
-
|
20
|
+
Here's a small example:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
fib = Fiber.new do
|
24
|
+
x, y = 0, 1
|
25
|
+
loop do
|
26
|
+
Fiber.yield y
|
27
|
+
x, y = y, x + y
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
10.times { puts fib.resume }
|
32
|
+
```
|
33
|
+
|
34
|
+
Another implication of using resume / yield is that the main fiber can't yield
|
35
|
+
away, meaning we cannot pause the main fiber using `Fiber.yield`.
|
36
|
+
|
37
|
+
The other fiber control mechanism, using `Fiber#transfer`, is fully symmetric:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
require 'fiber'
|
41
|
+
|
42
|
+
ping = Fiber.new { loop { puts "ping"; pong.transfer } }
|
43
|
+
pong = Fiber.new { loop { puts "pong"; ping.transfer } }
|
44
|
+
ping.transfer
|
45
|
+
```
|
46
|
+
|
47
|
+
`Fiber#transform` also allows using the main fiber as a general purpose
|
48
|
+
resumable execution context. Polyphony uses `Fiber#transfer` exclusively for
|
49
|
+
scheduling fibers.
|
50
|
+
|
51
|
+
## The Different Fiber states
|
16
52
|
|
17
|
-
|
18
|
-
|
19
|
-
|
53
|
+
In Polyphony, each fiber has one four possible states:
|
54
|
+
|
55
|
+
A new fiber will start in a `:waiting` state. The `#spin` global method will
|
56
|
+
create the fiber and schedule it for execution, marking it as `:runnable`. When
|
57
|
+
the fiber is run, it is in a `:running` state. Finally, when the fiber has
|
58
|
+
terminated, it transitions to the `:dead` state.
|
59
|
+
|
60
|
+
Whenever a fiber performs a blocking operation - such as waiting for a timer to
|
61
|
+
elapse, or for a socket to become readable, or for a child process to terminate -
|
62
|
+
it transitions to a `:waiting` state. Once the timer has elapsed, the socket
|
63
|
+
has become readable, or the child process has terminated, the fiber is marked as
|
64
|
+
`:runnable`. It then waits its turn to run.
|
65
|
+
|
66
|
+
## Switchpoints
|
67
|
+
|
68
|
+
A switchpoint is any point in time at which control might switch from the
|
69
|
+
currently running fiber to another fiber that is `:runnable`. This usually
|
70
|
+
occurs when the currently running fiber starts a blocking operation. It also
|
71
|
+
occurs when the running fiber has yielded control using `#snooze` or `#suspend`.
|
72
|
+
A Switchpoint will also occur when the currently running fiber has terminated.
|
20
73
|
|
21
74
|
## Scheduler-less scheduling
|
22
75
|
|
@@ -27,15 +80,14 @@ reactor-based libraries and frameworks, such as `nio4r`, `EventMachine` or
|
|
27
80
|
user-supplied code *from inside the loop*. In Polyphony, however, we have chosen
|
28
81
|
a programming model that does not use a loop to schedule fibers. In fact, in
|
29
82
|
Polyphony there's no such thing as a reactor loop, and there's no *scheduler*
|
30
|
-
running on a separate execution context.
|
83
|
+
per se running on a separate execution context.
|
31
84
|
|
32
|
-
Instead, Polyphony maintains for each thread a list of
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
the
|
37
|
-
transferred to the
|
38
|
-
terminates, at which point control is transferred to the next scheduled fiber.
|
85
|
+
Instead, Polyphony maintains for each thread a run queue, a list of `:runnable`
|
86
|
+
fibers. If no fiber is `:runnable`, the libev event reactor will be ran until
|
87
|
+
one or more events have occurred. Events are handled by adding the corresponding
|
88
|
+
fibers onto the run queue. Finally, control is transferred to the first fiber on
|
89
|
+
the run queue, which will run until it blocks or terminates, at which point
|
90
|
+
control is transferred to the next runnable fiber.
|
39
91
|
|
40
92
|
This approach has numerous benefits:
|
41
93
|
|
@@ -45,89 +97,25 @@ This approach has numerous benefits:
|
|
45
97
|
scheduling code.
|
46
98
|
- Much less time is spent in reactor loop callbacks, letting the reactor loop
|
47
99
|
run more efficiently.
|
48
|
-
- Fibers are
|
100
|
+
- Fibers are switched outside of the event reactor code, making it easier to
|
49
101
|
avoid race conditions and unexpected behaviours.
|
50
102
|
|
51
|
-
## A concrete example - fiber scheduling in an echo server
|
52
|
-
|
53
|
-
Here's a barebones echo server written in Polyphony:
|
54
|
-
|
55
|
-
```ruby
|
56
|
-
require 'polyphony'
|
57
|
-
|
58
|
-
server = TCPServer.open('127.0.0.1', 8081)
|
59
|
-
while (client = server.accept)
|
60
|
-
spin do
|
61
|
-
while (data = client.gets)
|
62
|
-
client << ">>>you sent: #{data}"
|
63
|
-
break if data =~ /quit/i
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
```
|
68
|
-
|
69
|
-
Let's examine the the flow of control in our echo server program:
|
70
|
-
|
71
|
-
<p class="img-figure"><img src="../../assets/img/echo-fibers.svg"></p>
|
72
|
-
|
73
|
-
> In the above figure, the fat blue dots represents moments at which fibers can
|
74
|
-
> be switched. The light blue horizontal arrows represent switching from one
|
75
|
-
> fiber to another. The blue vertical lines represent a train of execution on a
|
76
|
-
> single fiber.
|
77
|
-
|
78
|
-
- The main fiber (fiber 1) runs a loop waiting for incoming connections.
|
79
|
-
- The call to `server.accept` blocks, and an I/O event watcher is set up. The
|
80
|
-
main fiber is suspended.
|
81
|
-
- Since there's no other runnable fiber, the associated event loop is run.
|
82
|
-
- An event is generated for the server's socket, and fiber 1 is added to the run
|
83
|
-
queue.
|
84
|
-
- Fiber 1 is pulled from the run queue and resumed. The `server.accept` call
|
85
|
-
returns a new client socket (an instance of `TCPSocket`).
|
86
|
-
- Fiber 1 continues by `spin`ning up a new fiber for handling the client. The
|
87
|
-
new fiber (fiber 2) is added to the run queue.
|
88
|
-
- Fiber 1 goes back to waiting for an incoming connection by calling
|
89
|
-
`server.accept` again. The call blocks, the main fiber suspends, and switches
|
90
|
-
to the next fiber in the run queue, fiber 2.
|
91
|
-
- Fiber 2 starts a loop and calls `client.gets`. The call blocks, an I/O event
|
92
|
-
watcher is set up, and the fiber suspends.
|
93
|
-
- Since no other fiber is runnable, the event loop is run, waiting for at least
|
94
|
-
one event to fire.
|
95
|
-
- An event fires for the acceptor socket, and fiber 1 is put on the run queue.
|
96
|
-
- An event fires for the client socket, and fiber 2 is put on the run queue.
|
97
|
-
- Fiber 1 is resumed and spins up a new client handling fiber (fiber 3), which
|
98
|
-
is put on the run queue.
|
99
|
-
- Fiber 1 calls `server.accept` again, and suspends, switching to the next
|
100
|
-
runnable fiber, fiber 2.
|
101
|
-
- Fiber 2 resumes and completes reading a line from the socket.
|
102
|
-
- Fiber 2 calls `client.<<`, blocks, sets up an I/O watcher, and suspends,
|
103
|
-
switching to the next runnable fiber, fiber 3.
|
104
|
-
- Fiber 3 resumes and calls `client.gets`. The call blocks, an I/O event watcher
|
105
|
-
is set up, and the fiber suspends.
|
106
|
-
|
107
|
-
## Fiber states
|
108
|
-
|
109
|
-
In Polyphony, each fiber has one of the following states at any given moment:
|
110
|
-
|
111
|
-
- `:running`: this is the state of the currently running fiber.
|
112
|
-
- `:dead`: the fiber has terminated.
|
113
|
-
- `:waiting`: the fiber is suspended, waiting for something to wake it up.
|
114
|
-
- `:runnable`: the fiber is on the run queue and will be run shortly.
|
115
|
-
|
116
103
|
## Fiber scheduling and fiber switching
|
117
104
|
|
118
105
|
The Polyphony scheduling model makes a clear separation between the scheduling
|
119
106
|
of fibers and the switching of fibers. The scheduling of fibers is the act of
|
120
|
-
marking the fiber to be run at the earliest opportunity, but not
|
121
|
-
The switching of fibers is the act of transferring control
|
122
|
-
|
107
|
+
marking the fiber as `:runnable`, to be run at the earliest opportunity, but not
|
108
|
+
immediately. The switching of fibers is the act of actually transferring control
|
109
|
+
to another fiber, namely the first fiber in the run queue.
|
123
110
|
|
124
111
|
The scheduling of fibers can occur at any time, either as a result of an event
|
125
112
|
occuring, an exception being raised, or using `Fiber#schedule`. The switching of
|
126
|
-
fibers will occur only when
|
127
|
-
|
128
|
-
|
113
|
+
fibers will occur only when the currently running fiber has reached a
|
114
|
+
switchpoint, e.g. when a blocking operation is started, or upon calling
|
115
|
+
`Fiber#suspend` or `Fiber#snooze`. As mentioned earlier, in order to switch to a
|
116
|
+
scheduled fiber, Polyphony uses `Fiber#transfer`.
|
129
117
|
|
130
|
-
When a fiber terminates, any other
|
118
|
+
When a fiber terminates, any other runnable fibers will be run. If no fibers
|
131
119
|
are waiting and the main fiber is done running, the Ruby process will terminate.
|
132
120
|
|
133
121
|
## Interrupting blocking operations
|
@@ -185,9 +173,42 @@ by scheduling the corresponding fiber with an exception:
|
|
185
173
|
```ruby
|
186
174
|
def timeout(duration)
|
187
175
|
fiber = Fiber.current
|
188
|
-
|
176
|
+
interrupter = spin do
|
177
|
+
Gyro::Timer.new(duration, 0).await
|
178
|
+
fiber.transfer(TimerException.new)
|
179
|
+
end
|
189
180
|
yield
|
190
181
|
ensure
|
191
|
-
|
182
|
+
interrupter.stop
|
192
183
|
end
|
193
184
|
```
|
185
|
+
|
186
|
+
## Fiber Scheduling in a Multithreaded Program
|
187
|
+
|
188
|
+
Polyphony performs fiber scheduling separately for each thread. Each thread,
|
189
|
+
therefore, will be able to run multiple fibers independently from other threads.
|
190
|
+
|
191
|
+
## The fiber scheduling algorithm in full
|
192
|
+
|
193
|
+
Here is the summary of the Polyphony scheduling algorithm:
|
194
|
+
|
195
|
+
- loop
|
196
|
+
- pull first runnable fiber from run queue
|
197
|
+
- if runnable fiber is not nil
|
198
|
+
- if the ref count greater than 0
|
199
|
+
- increment the run_no_wait counter
|
200
|
+
- if the run_no_wait counter is greater than 10 and greater than the run
|
201
|
+
queue length
|
202
|
+
- reset the run_no_wait counter
|
203
|
+
- run the event loop once without waiting for events (using
|
204
|
+
`EVRUN_NOWAIT`)
|
205
|
+
- break out of the loop
|
206
|
+
- if the ref count is 0
|
207
|
+
- break out of the loop
|
208
|
+
- run the event loop until one or more events are generated (using
|
209
|
+
`EVRUN_ONCE`)
|
210
|
+
- if next runnable fiber is nil
|
211
|
+
- return
|
212
|
+
- get scheduled resume value for next runnable fiber
|
213
|
+
- mark the next runnable fiber as not runnable
|
214
|
+
- switch to the next runnable fiber using `Fiber#transfer`
|
@@ -0,0 +1,126 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: All About Timers
|
4
|
+
nav_order: 1
|
5
|
+
parent: User Guide
|
6
|
+
permalink: /user-guide/all-about-timers/
|
7
|
+
---
|
8
|
+
# All About Timers
|
9
|
+
|
10
|
+
Timers form a major part of writing dynamic concurrent programs. They allow
|
11
|
+
programmers to create delays and to perform recurring operations with a
|
12
|
+
controllable frequency. Crucially, they also enable the implementation of
|
13
|
+
timeouts, which are an important aspect of concurrent programming.
|
14
|
+
|
15
|
+
## Sleeping
|
16
|
+
|
17
|
+
Sometimes, your code needs to wait for a certain period of time. For example,
|
18
|
+
implementing a retry mechanism for failed HTTP requests might involve waiting
|
19
|
+
for a few seconds before retrying. Polyphony patches the `Kernel#sleep` method
|
20
|
+
to be fiber-aware, that is to yield control of execution while waiting for a
|
21
|
+
timer to elapse.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# This is a naive retry implementation
|
25
|
+
def fetch(url)
|
26
|
+
fetch_url(url)
|
27
|
+
rescue
|
28
|
+
sleep 1
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## Sleeping Forever
|
34
|
+
|
35
|
+
The `#sleep` method can also be used to sleep forever, if no argument is given:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
puts "Go to sleep"
|
39
|
+
sleep
|
40
|
+
puts "Woke up" # this line will not be executed
|
41
|
+
```
|
42
|
+
|
43
|
+
The `#sleep` forever call can be used for example in the main fiber when we do
|
44
|
+
all our work in other fibers, since once the main fiber terminates the program
|
45
|
+
exits.
|
46
|
+
|
47
|
+
## Doing Work Later
|
48
|
+
|
49
|
+
While `#sleep` allows you to block execution of the current fiber, sometimes you
|
50
|
+
want to perform some work later, while not blocking the current fiber. This is done simply by spinning off another fiber:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
do_some_stuff
|
54
|
+
spin do
|
55
|
+
sleep 3
|
56
|
+
do_stuff_later
|
57
|
+
end
|
58
|
+
do_some_more_stuff
|
59
|
+
```
|
60
|
+
|
61
|
+
## Using timeouts
|
62
|
+
|
63
|
+
Polyphony provides the following global methods for using timeouts:
|
64
|
+
|
65
|
+
- `#move_on_after` - used for cancelling an operation after a certain period of time without raising an exception:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
move_on_after 1 do
|
69
|
+
sleep 60
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
This method also takes an optional return value argument:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
move_on_after 1, with_value: 'bar' do
|
77
|
+
sleep 60
|
78
|
+
'foo'
|
79
|
+
end #=> 'bar'
|
80
|
+
```
|
81
|
+
|
82
|
+
- `#cancel_after` - used for cancelling an operation after a certain period of time with a `Cancel` exception:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
cancel_after 1 do
|
86
|
+
sleep 60
|
87
|
+
end #=> raises Cancel
|
88
|
+
```
|
89
|
+
|
90
|
+
Polyphony also provides a fiber-aware version of the core Ruby `Timeout` API, which may be used directly or indirectly to interrupt blocking operations.
|
91
|
+
|
92
|
+
## Using Raw Timers
|
93
|
+
|
94
|
+
Polyphony implements timers through the `Gyro::Timer` class, which encapsulates
|
95
|
+
libev timer watchers. Using `Gyro::Timer` you can create both one-time and
|
96
|
+
recurring timers:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# Create a one-time timer
|
100
|
+
one_time = Gyro::Timer.new(1, 0)
|
101
|
+
|
102
|
+
# Create a recurring timer
|
103
|
+
recurring = Gyro::Timer.new(0.5, 1.5)
|
104
|
+
```
|
105
|
+
|
106
|
+
Once your timer is created, you can wait for it using the `#await` method:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
def delay(duration)
|
110
|
+
timer = Gyro::Timer.new(duration, 0)
|
111
|
+
timer.await
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
Waiting for the timer will *block* the current timer. This means that if you
|
116
|
+
want to do other work while waiting for the timer, you need to put it on a
|
117
|
+
different fiber:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
timer = Gyro::Timer.new(3, 0)
|
121
|
+
spin {
|
122
|
+
sleep 3
|
123
|
+
do_something_else
|
124
|
+
}
|
125
|
+
do_blocking_operation
|
126
|
+
```
|
@@ -37,7 +37,7 @@ Note that requests are handled using a callback block which takes a single
|
|
37
37
|
argument. The `request` object provides the entire API for responding to the
|
38
38
|
client.
|
39
39
|
|
40
|
-
Each client connection will be handled in a separate
|
40
|
+
Each client connection will be handled in a separate fiber, allowing
|
41
41
|
concurrent processing of incoming requests.
|
42
42
|
|
43
43
|
## HTTP 2 support
|
@@ -47,7 +47,7 @@ HTTP 2 support is baked in to the server, which supports both HTTP 2 upgrades
|
|
47
47
|
in a completely effortless manner.
|
48
48
|
|
49
49
|
Since HTTP 2 connections are multiplexed, allowing multiple concurrent requests
|
50
|
-
on a single connection, each HTTP 2 stream is handled in a separate
|
50
|
+
on a single connection, each HTTP 2 stream is handled in a separate fiber.
|
51
51
|
|
52
52
|
## TLS termination
|
53
53
|
|
data/docs/user-guide.md
CHANGED
data/examples/core/xx-trace.rb
CHANGED
@@ -5,13 +5,12 @@ require 'polyphony'
|
|
5
5
|
|
6
6
|
Exception.__disable_sanitized_backtrace__ = true
|
7
7
|
|
8
|
-
Gyro.trace(true)
|
9
8
|
|
10
9
|
sleep 0
|
11
10
|
$records = []
|
12
11
|
|
12
|
+
Gyro.trace(true)
|
13
13
|
trace = Polyphony::Trace.new { |r| $records << r }
|
14
|
-
|
15
14
|
trace.enable
|
16
15
|
|
17
16
|
f2 = spin(:f2) { 3.times { sleep 0.1 } }
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony'
|
5
|
+
|
6
|
+
def do_work(client)
|
7
|
+
result = yield
|
8
|
+
client.schedule(result)
|
9
|
+
rescue Exception => e
|
10
|
+
client.schedule(e)
|
11
|
+
end
|
12
|
+
|
13
|
+
$worker = Thread.new do
|
14
|
+
Fiber.current.tag = :worker
|
15
|
+
loop do
|
16
|
+
client, block = receive
|
17
|
+
do_work(client, &block)
|
18
|
+
end
|
19
|
+
rescue Exception => e
|
20
|
+
p e
|
21
|
+
end
|
22
|
+
|
23
|
+
def process(&block)
|
24
|
+
$worker.main_fiber << [Fiber.current, block]
|
25
|
+
receive
|
26
|
+
end
|
27
|
+
|
28
|
+
sleep 0.1
|
29
|
+
|
30
|
+
p process { 1 + 1 }
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# idea taken from the example given in trio:
|
4
|
+
# https://www.youtube.com/watch?v=oLkfnc_UMcE
|
5
|
+
|
6
|
+
require 'bundler/setup'
|
7
|
+
require 'polyphony'
|
8
|
+
|
9
|
+
def try_connect(target, supervisor)
|
10
|
+
puts "trying #{target[2]}"
|
11
|
+
socket = Polyphony::Net.tcp_connect(target[2], 80)
|
12
|
+
# connection successful
|
13
|
+
supervisor.stop([target[2], socket])
|
14
|
+
rescue IOError, SystemCallError
|
15
|
+
# ignore error
|
16
|
+
end
|
17
|
+
|
18
|
+
def happy_eyeballs(hostname, port, max_wait_time: 0.025)
|
19
|
+
targets = Socket.getaddrinfo(hostname, port, :INET, :STREAM)
|
20
|
+
t0 = Time.now
|
21
|
+
cancel_after(5) do
|
22
|
+
success = supervise do |supervisor|
|
23
|
+
targets.each_with_index do |t, idx|
|
24
|
+
sleep(max_wait_time) if idx > 0
|
25
|
+
supervisor.spin { try_connect(t, supervisor) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
if success
|
29
|
+
puts format('success: %s (%.3fs)', success[0], Time.now - t0)
|
30
|
+
else
|
31
|
+
puts "timed out (#{Time.now - t0}s)"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Let's try it out:
|
37
|
+
happy_eyeballs('debian.org', 'https')
|
data/ext/gyro/gyro.c
CHANGED
@@ -11,6 +11,7 @@ ID ID_inspect;
|
|
11
11
|
ID ID_new;
|
12
12
|
ID ID_raise;
|
13
13
|
ID ID_ivar_running;
|
14
|
+
ID ID_ivar_thread;
|
14
15
|
ID ID_runnable;
|
15
16
|
ID ID_runnable_value;
|
16
17
|
ID ID_size;
|
@@ -117,10 +118,13 @@ static VALUE Fiber_state(VALUE self) {
|
|
117
118
|
}
|
118
119
|
|
119
120
|
inline void Gyro_schedule_fiber(VALUE fiber, VALUE value) {
|
120
|
-
|
121
|
-
|
121
|
+
VALUE thread = rb_ivar_get(fiber, ID_ivar_thread);
|
122
|
+
if (thread != Qnil) {
|
123
|
+
Thread_schedule_fiber(thread, fiber, value);
|
124
|
+
}
|
125
|
+
else {
|
126
|
+
rb_warn("No thread set for fiber");
|
122
127
|
}
|
123
|
-
Thread_schedule_fiber(rb_thread_current(), fiber, value);
|
124
128
|
}
|
125
129
|
|
126
130
|
VALUE Gyro_trace(VALUE self, VALUE enabled) {
|
@@ -154,6 +158,7 @@ void Init_Gyro() {
|
|
154
158
|
ID_empty = rb_intern("empty?");
|
155
159
|
ID_inspect = rb_intern("inspect");
|
156
160
|
ID_ivar_running = rb_intern("@running");
|
161
|
+
ID_ivar_thread = rb_intern("@thread");
|
157
162
|
ID_new = rb_intern("new");
|
158
163
|
ID_pop = rb_intern("pop");
|
159
164
|
ID_push = rb_intern("push");
|
data/ext/gyro/gyro.h
CHANGED
@@ -27,7 +27,8 @@ VALUE IO_read_watcher(VALUE io);
|
|
27
27
|
VALUE IO_write_watcher(VALUE io);
|
28
28
|
VALUE Gyro_IO_await(VALUE self);
|
29
29
|
|
30
|
-
VALUE Gyro_Selector_run(VALUE self);
|
30
|
+
VALUE Gyro_Selector_run(VALUE self, VALUE current_fiber);
|
31
|
+
void Gyro_Selector_run_no_wait(VALUE self, VALUE current_fiber, long runnable_count);
|
31
32
|
|
32
33
|
VALUE Gyro_Timer_await(VALUE self);
|
33
34
|
|
@@ -36,6 +37,7 @@ void io_set_read_length(VALUE str, long n, int shrinkable);
|
|
36
37
|
VALUE io_enc_str(VALUE str, rb_io_t *fptr);
|
37
38
|
|
38
39
|
struct ev_loop *Gyro_Selector_ev_loop(VALUE selector);
|
40
|
+
ev_tstamp Gyro_Selector_now(VALUE selector);
|
39
41
|
struct ev_loop *Gyro_Selector_current_thread_ev_loop();
|
40
42
|
long Gyro_Selector_pending_count(VALUE self);
|
41
43
|
|
@@ -50,6 +52,9 @@ VALUE Thread_post_fork(VALUE thread);
|
|
50
52
|
|
51
53
|
#define OBJ_ID(obj) (NUM2LONG(rb_funcall(obj, rb_intern("object_id"), 0)))
|
52
54
|
#define INSPECT(...) (rb_funcall(rb_cObject, rb_intern("p"), __VA_ARGS__))
|
55
|
+
#define FIBER_TRACE(...) if (__tracing_enabled__) { \
|
56
|
+
rb_funcall(rb_cObject, ID_fiber_trace, __VA_ARGS__); \
|
57
|
+
}
|
53
58
|
|
54
59
|
extern VALUE mGyro;
|
55
60
|
extern VALUE cGyro_Async;
|
@@ -65,6 +70,7 @@ extern ID ID_each;
|
|
65
70
|
extern ID ID_fiber_trace;
|
66
71
|
extern ID ID_inspect;
|
67
72
|
extern ID ID_ivar_running;
|
73
|
+
extern ID ID_ivar_thread;
|
68
74
|
extern ID ID_new;
|
69
75
|
extern ID ID_raise;
|
70
76
|
extern ID ID_runnable;
|
data/ext/gyro/queue.c
CHANGED
@@ -78,22 +78,50 @@ VALUE Gyro_Queue_shift(VALUE self) {
|
|
78
78
|
return Gyro_Async_await(async);
|
79
79
|
}
|
80
80
|
|
81
|
+
VALUE Gyro_Queue_shift_no_wait(VALUE self) {
|
82
|
+
struct Gyro_Queue *queue;
|
83
|
+
GetGyro_Queue(self, queue);
|
84
|
+
|
85
|
+
return rb_ary_shift(queue->queue);
|
86
|
+
}
|
87
|
+
|
81
88
|
VALUE Gyro_Queue_shift_all(VALUE self) {
|
82
89
|
struct Gyro_Queue *queue;
|
83
90
|
GetGyro_Queue(self, queue);
|
84
91
|
|
92
|
+
VALUE old_queue = queue->queue;
|
93
|
+
queue->queue = rb_ary_new();
|
94
|
+
|
85
95
|
if (rb_block_given_p()) {
|
86
|
-
|
87
|
-
|
96
|
+
long len = RARRAY_LEN(old_queue);
|
97
|
+
for (long i = 0; i < len; i++) {
|
98
|
+
rb_yield(RARRAY_AREF(old_queue, i));
|
88
99
|
}
|
100
|
+
// while (RARRAY_LEN(old_queue) > 0) {
|
101
|
+
// rb_yield(rb_ary_shift(old_queue));
|
102
|
+
// }
|
103
|
+
return self;
|
89
104
|
}
|
90
105
|
else {
|
91
|
-
|
106
|
+
return old_queue;
|
92
107
|
}
|
108
|
+
}
|
109
|
+
|
110
|
+
VALUE Gyro_Queue_clear(VALUE self) {
|
111
|
+
struct Gyro_Queue *queue;
|
112
|
+
GetGyro_Queue(self, queue);
|
93
113
|
|
114
|
+
rb_ary_clear(queue->queue);
|
94
115
|
return self;
|
95
116
|
}
|
96
117
|
|
118
|
+
VALUE Gyro_Queue_empty_p(VALUE self) {
|
119
|
+
struct Gyro_Queue *queue;
|
120
|
+
GetGyro_Queue(self, queue);
|
121
|
+
|
122
|
+
return (RARRAY_LEN(queue->queue) == 0) ? Qtrue : Qfalse;
|
123
|
+
}
|
124
|
+
|
97
125
|
void Init_Gyro_Queue() {
|
98
126
|
cGyro_Queue = rb_define_class_under(mGyro, "Queue", rb_cData);
|
99
127
|
rb_define_alloc_func(cGyro_Queue, Gyro_Queue_allocate);
|
@@ -104,6 +132,10 @@ void Init_Gyro_Queue() {
|
|
104
132
|
|
105
133
|
rb_define_method(cGyro_Queue, "pop", Gyro_Queue_shift, 0);
|
106
134
|
rb_define_method(cGyro_Queue, "shift", Gyro_Queue_shift, 0);
|
135
|
+
|
136
|
+
rb_define_method(cGyro_Queue, "shift_no_wait", Gyro_Queue_shift_no_wait, 0);
|
107
137
|
|
108
138
|
rb_define_method(cGyro_Queue, "shift_each", Gyro_Queue_shift_all, 0);
|
139
|
+
rb_define_method(cGyro_Queue, "clear", Gyro_Queue_clear, 0);
|
140
|
+
rb_define_method(cGyro_Queue, "empty?", Gyro_Queue_empty_p, 0);
|
109
141
|
}
|