polyphony 0.28 → 0.29
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
}
|