polyphony 0.34 → 0.41
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +11 -2
- data/.gitignore +2 -2
- data/.rubocop.yml +30 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile +0 -11
- data/Gemfile.lock +11 -10
- data/README.md +2 -1
- data/Rakefile +6 -2
- data/TODO.md +18 -95
- data/docs/_includes/head.html +40 -0
- data/docs/_includes/nav.html +5 -5
- data/docs/api-reference.md +1 -1
- data/docs/api-reference/fiber.md +18 -0
- data/docs/api-reference/gyro-async.md +57 -0
- data/docs/api-reference/gyro-child.md +29 -0
- data/docs/api-reference/gyro-queue.md +44 -0
- data/docs/api-reference/gyro-timer.md +51 -0
- data/docs/api-reference/gyro.md +25 -0
- data/docs/index.md +10 -7
- data/docs/main-concepts/design-principles.md +67 -9
- data/docs/main-concepts/extending.md +1 -1
- data/docs/main-concepts/fiber-scheduling.md +55 -72
- data/examples/core/xx-agent.rb +102 -0
- data/examples/core/xx-fork-cleanup.rb +22 -0
- data/examples/core/xx-sleeping.rb +14 -6
- data/examples/core/xx-timer-gc.rb +17 -0
- data/examples/io/tunnel.rb +48 -0
- data/examples/io/xx-irb.rb +1 -1
- data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +7 -6
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +14 -25
- data/ext/{gyro → polyphony}/extconf.rb +2 -2
- data/ext/polyphony/fiber.c +112 -0
- data/ext/{gyro → polyphony}/libev.c +0 -0
- data/ext/{gyro → polyphony}/libev.h +0 -0
- data/ext/polyphony/libev_agent.c +503 -0
- data/ext/polyphony/libev_queue.c +214 -0
- data/ext/polyphony/polyphony.c +89 -0
- data/ext/{gyro/gyro.h → polyphony/polyphony.h} +49 -59
- data/ext/polyphony/polyphony_ext.c +23 -0
- data/ext/{gyro → polyphony}/socket.c +21 -19
- data/ext/{gyro → polyphony}/thread.c +55 -119
- data/ext/{gyro → polyphony}/tracing.c +1 -1
- data/lib/polyphony.rb +37 -44
- data/lib/polyphony/adapters/fs.rb +1 -4
- data/lib/polyphony/adapters/irb.rb +2 -2
- data/lib/polyphony/adapters/postgres.rb +6 -5
- data/lib/polyphony/adapters/process.rb +27 -23
- data/lib/polyphony/adapters/trace.rb +110 -105
- data/lib/polyphony/core/channel.rb +35 -35
- data/lib/polyphony/core/exceptions.rb +29 -29
- data/lib/polyphony/core/global_api.rb +94 -91
- data/lib/polyphony/core/resource_pool.rb +83 -83
- data/lib/polyphony/core/sync.rb +16 -16
- data/lib/polyphony/core/thread_pool.rb +49 -37
- data/lib/polyphony/core/throttler.rb +30 -23
- data/lib/polyphony/event.rb +27 -0
- data/lib/polyphony/extensions/core.rb +23 -14
- data/lib/polyphony/extensions/fiber.rb +269 -267
- data/lib/polyphony/extensions/io.rb +56 -26
- data/lib/polyphony/extensions/openssl.rb +5 -9
- data/lib/polyphony/extensions/socket.rb +29 -10
- data/lib/polyphony/extensions/thread.rb +19 -12
- data/lib/polyphony/net.rb +64 -60
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +3 -6
- data/test/helper.rb +14 -1
- data/test/stress.rb +17 -12
- data/test/test_agent.rb +77 -0
- data/test/{test_async.rb → test_event.rb} +17 -9
- data/test/test_ext.rb +25 -4
- data/test/test_fiber.rb +23 -14
- data/test/test_global_api.rb +5 -5
- data/test/test_io.rb +46 -24
- data/test/test_queue.rb +74 -0
- data/test/test_signal.rb +3 -40
- data/test/test_socket.rb +33 -0
- data/test/test_thread.rb +38 -16
- data/test/test_thread_pool.rb +3 -3
- data/test/test_throttler.rb +0 -1
- data/test/test_trace.rb +6 -5
- metadata +34 -39
- data/ext/gyro/async.c +0 -158
- data/ext/gyro/child.c +0 -117
- data/ext/gyro/gyro.c +0 -203
- data/ext/gyro/gyro_ext.c +0 -31
- data/ext/gyro/io.c +0 -447
- data/ext/gyro/queue.c +0 -142
- data/ext/gyro/selector.c +0 -183
- data/ext/gyro/signal.c +0 -108
- data/ext/gyro/timer.c +0 -154
- data/test/test_timer.rb +0 -56
data/docs/api-reference.md
CHANGED
data/docs/api-reference/fiber.md
CHANGED
@@ -71,6 +71,24 @@ f << 2
|
|
71
71
|
result = receive #=> 20
|
72
72
|
```
|
73
73
|
|
74
|
+
### #auto_watcher → async
|
75
|
+
|
76
|
+
Returns a reusable `Gyro::Async` watcher instance associated with the fiber.
|
77
|
+
This method provides a way to minimize watcher allocation. Instead of allocating
|
78
|
+
a new async watcher every time one is needed, the same watcher associated with
|
79
|
+
the fiber is reused.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
def work(async)
|
83
|
+
do_something
|
84
|
+
async.signal
|
85
|
+
end
|
86
|
+
|
87
|
+
async = Fiber.current.auto_watcher
|
88
|
+
spin { work(async) }
|
89
|
+
async.await
|
90
|
+
```
|
91
|
+
|
74
92
|
### #await → object<br>#join → object
|
75
93
|
|
76
94
|
Awaits the termination of the fiber. If the fiber terminates with an uncaught
|
@@ -0,0 +1,57 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Gyro::Async
|
4
|
+
parent: API Reference
|
5
|
+
permalink: /api-reference/gyro-async/
|
6
|
+
---
|
7
|
+
# Gyro::Async
|
8
|
+
|
9
|
+
`Gyro::Async` encapsulates a libev [async
|
10
|
+
watcher](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#code_ev_async_code_how_to_wake_up_an),
|
11
|
+
allowing thread-safe synchronisation and signalling. `Gyro::Async` watchers are
|
12
|
+
used both directly and indirectly in Polyphony to implement
|
13
|
+
[queues](../gyro-queue/), await fibers and threads, and auxiliary features such
|
14
|
+
as [thread pools](../polyphony-threadpool/).
|
15
|
+
|
16
|
+
A `Gyro::Async` watcher instance is shared across two or more fibers (across one
|
17
|
+
or more threads), where one fiber waits to be signalled by calling
|
18
|
+
`Gyro::Async#await`, and one or more other fibers do the signalling by calling
|
19
|
+
`Gyro::Async#signal`:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
async = Gyro::Async.new
|
23
|
+
spin do
|
24
|
+
sleep 1
|
25
|
+
async.signal
|
26
|
+
end
|
27
|
+
|
28
|
+
async.await
|
29
|
+
```
|
30
|
+
|
31
|
+
The signalling of async watchers is compressed, which means that multiple
|
32
|
+
invocations of `Gyro::Async#signal` before the event loop can continue will
|
33
|
+
result the watcher being signalled just a single time.
|
34
|
+
|
35
|
+
In addition to signalling, the async watcher can also be used to transfer an
|
36
|
+
arbitrary value to the awaitng fiber. See `#signal` for an example.
|
37
|
+
|
38
|
+
## Instance methods
|
39
|
+
|
40
|
+
### #await → object
|
41
|
+
|
42
|
+
Blocks the current thread until the watcher is signalled.
|
43
|
+
|
44
|
+
### #initialize
|
45
|
+
|
46
|
+
Initializes the watcher instance.
|
47
|
+
|
48
|
+
### #signal(value = nil) → async
|
49
|
+
|
50
|
+
Signals the watcher, causing the fiber awaiting the watcher to become runnable
|
51
|
+
and be eventually resumed with the given value.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
async = Gyro::Async.new
|
55
|
+
spin { async.signal('foo') }
|
56
|
+
async.await #=> 'foo'
|
57
|
+
```
|
@@ -0,0 +1,29 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Gyro::Child
|
4
|
+
parent: API Reference
|
5
|
+
permalink: /api-reference/gyro-child/
|
6
|
+
---
|
7
|
+
# Gyro::Child
|
8
|
+
|
9
|
+
`Gyro::Child` encapsulates a libev [child
|
10
|
+
watcher](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#code_ev_child_code_watch_out_for_pro),
|
11
|
+
used for waiting for a child process to terminate. A `Gyro::Child` watcher
|
12
|
+
instance can be used for low-level control of child processes, instead of using
|
13
|
+
more high-level APIs such `Process.wait` etc.
|
14
|
+
|
15
|
+
## Instance methods
|
16
|
+
|
17
|
+
### #await → [pid, exitcode]
|
18
|
+
|
19
|
+
Blocks the current thread until the watcher is signalled. The return value is an
|
20
|
+
array containing the child's pid and the exit code.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
pid = Polyphony.fork { sleep 1 }
|
24
|
+
Gyro::Child.new(pid).await #=> [pid, 0]
|
25
|
+
```
|
26
|
+
|
27
|
+
### #initialize(pid)
|
28
|
+
|
29
|
+
Initializes the watcher instance with the given pid
|
@@ -0,0 +1,44 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Gyro::Queue
|
4
|
+
parent: API Reference
|
5
|
+
permalink: /api-reference/gyro-queue/
|
6
|
+
---
|
7
|
+
# Gyro::Queue
|
8
|
+
|
9
|
+
`Gyro::Queue` implements a polyphonic (fiber-aware) queue that can store 0 or
|
10
|
+
more items of any data types. Adding an item to the queue never blocks.
|
11
|
+
Retrieving an item from the queue will block if the queue is empty.
|
12
|
+
`Gyro::Queue` is both fiber-safe and thread-safe. This means multiple fibers
|
13
|
+
from multiple threads can concurrently interact with the same queue.
|
14
|
+
`Gyro::Queue` is used pervasively across the Polyphony code base for
|
15
|
+
synchronisation and fiber control.
|
16
|
+
|
17
|
+
## Instance methods
|
18
|
+
|
19
|
+
### #<<(object) → queue<br>#push(object) → queue
|
20
|
+
|
21
|
+
Adds an item to the queue.
|
22
|
+
|
23
|
+
### #clear → queue
|
24
|
+
|
25
|
+
Removes all items currently in the queue.
|
26
|
+
|
27
|
+
### #empty? → true or false
|
28
|
+
|
29
|
+
Returns true if the queue is empty. Otherwise returns false.
|
30
|
+
|
31
|
+
### #initialize
|
32
|
+
|
33
|
+
Initializes an empty queue.
|
34
|
+
|
35
|
+
### #shift → object<br>#pop → object
|
36
|
+
|
37
|
+
Retrieves an item from the queue. If the queue is empty, `#shift` blocks until
|
38
|
+
an item is added to the queue or until interrupted. Multiple fibers calling
|
39
|
+
`#shift` are served in a first-ordered first-served manner.
|
40
|
+
|
41
|
+
### #shift_each → [*object]<br>#shift_each({ block }) → queue
|
42
|
+
|
43
|
+
Removes and returns all items currently in the queue. If a block is given, it
|
44
|
+
will be invoked for each item.
|
@@ -0,0 +1,51 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Gyro::Timer
|
4
|
+
parent: API Reference
|
5
|
+
permalink: /api-reference/gyro-timer/
|
6
|
+
---
|
7
|
+
# Gyro::Timer
|
8
|
+
|
9
|
+
`Gyro::Timer` encapsulates a libev [timer
|
10
|
+
watcher](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#code_ev_timer_code_relative_and_opti),
|
11
|
+
allowing waiting a certain amount of time before proceeding with an operation.
|
12
|
+
Watchers can be either one-time timers or recurring timers. The Polyphony API
|
13
|
+
provides various APIs that use timer watchers for timeouts, throttled
|
14
|
+
operations, and sleeping.
|
15
|
+
|
16
|
+
## Instance methods
|
17
|
+
|
18
|
+
### #await → object
|
19
|
+
|
20
|
+
Blocks the current thread until the timer has elapsed. For recurrent timers,
|
21
|
+
`#await` will block until the next timer period has elapsed, as specified by the
|
22
|
+
`repeat` argument given to `#initialize`.
|
23
|
+
|
24
|
+
### #initialize(after, repeat)
|
25
|
+
|
26
|
+
Initializes the watcher instance. The `after` argument gives the time duration
|
27
|
+
in seconds before the timer has elapsed. The `repeat` argument gives the time
|
28
|
+
period for recurring timers, or `0` for non-recurring timers.
|
29
|
+
|
30
|
+
### #stop
|
31
|
+
|
32
|
+
Stops an active recurring timer. Recurring timers stay active (from the point of
|
33
|
+
view of the event loop) even after the timer period has elapsed. Calling `#stop`
|
34
|
+
marks the timer as inactive and cleans up associated resources. This should
|
35
|
+
normally be done inside an `ensure` block:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
def repeat(period)
|
39
|
+
timer = Gyro::Timer.new(period, period)
|
40
|
+
loop do
|
41
|
+
timer.await
|
42
|
+
yield
|
43
|
+
end
|
44
|
+
ensure
|
45
|
+
timer.stop
|
46
|
+
end
|
47
|
+
|
48
|
+
repeat(10) { puts Time.now }
|
49
|
+
```
|
50
|
+
|
51
|
+
There's no need to call `#stop` for non-recurring timers.
|
@@ -0,0 +1,25 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Gyro
|
4
|
+
parent: API Reference
|
5
|
+
permalink: /api-reference/gyro/
|
6
|
+
---
|
7
|
+
# Gyro
|
8
|
+
|
9
|
+
`Gyro` is the subsystem in charge of the low-level functionality in Polyphony.
|
10
|
+
It contains all of the different event watcher classes, as well as other
|
11
|
+
low-level constructs such as `Gyro::Queue`, a fiber-aware queue implementation,
|
12
|
+
used pervasively across the Polyphony code base.
|
13
|
+
|
14
|
+
While most Polyphony-based applications do not normally need to interact
|
15
|
+
directly with the `Gyro` classes, more advanced applications and libraries may
|
16
|
+
use those classes to enhance Polyphony and create custom concurrency patterns.
|
17
|
+
|
18
|
+
## Classes
|
19
|
+
|
20
|
+
- [`Gyro::Async`](../gyro-async/) - async event watcher
|
21
|
+
- [`Gyro::Child`](../gyro-child/) - child process event watcher
|
22
|
+
- [`Gyro::IO`](../gyro-io/) - IO event watcher
|
23
|
+
- [`Gyro::Queue`](../gyro-queue/) - fiber-aware queue
|
24
|
+
- [`Gyro::Signal`](../gyro-signal/) - signal event watcher
|
25
|
+
- [`Gyro::Timer`](../gyro-timer/) - timer event watcher
|
data/docs/index.md
CHANGED
@@ -14,8 +14,9 @@ implements a comprehensive
|
|
14
14
|
using [libev](https://github.com/enki/libev) as a high-performance event reactor
|
15
15
|
for I/O, timers, and other asynchronous events.
|
16
16
|
|
17
|
-
[FAQ](faq){: .btn .btn-green .text-gamma }
|
18
17
|
[Take the tutorial](getting-started/tutorial){: .btn .btn-blue .text-gamma }
|
18
|
+
[Main Concepts](main-concepts/concurrency/){: .btn .btn-green .text-gamma }
|
19
|
+
[FAQ](faq){: .btn .btn-green .text-gamma }
|
19
20
|
[Source code](https://github.com/digital-fabric/polyphony){: .btn .btn-purple .text-gamma target="_blank" }
|
20
21
|
{: .mt-6 .h-align-center }
|
21
22
|
|
@@ -64,8 +65,8 @@ adapters are being developed.
|
|
64
65
|
Polyphony draws inspiration from the following, in no particular order:
|
65
66
|
|
66
67
|
* [nio4r](https://github.com/socketry/nio4r/) and
|
67
|
-
[async](https://github.com/socketry/async) (Polyphony's C-extension code
|
68
|
-
|
68
|
+
[async](https://github.com/socketry/async) (Polyphony's C-extension code
|
69
|
+
started as a spinoff of
|
69
70
|
[nio4r's](https://github.com/socketry/nio4r/tree/master/ext))
|
70
71
|
* The [go scheduler](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html)
|
71
72
|
* [EventMachine](https://github.com/eventmachine/eventmachine)
|
@@ -73,11 +74,13 @@ Polyphony draws inspiration from the following, in no particular order:
|
|
73
74
|
* [Erlang supervisors](http://erlang.org/doc/man/supervisor.html) (and actually,
|
74
75
|
Erlang in general)
|
75
76
|
|
76
|
-
##
|
77
|
+
## Developer Resources
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
* [Tutorial](getting-started/tutorial)
|
80
|
+
* [Main Concepts](main-concepts/concurrency/)
|
81
|
+
* [User Guide](user-guide/all-about-timers/)
|
82
|
+
* [API Reference](api-reference/exception/)
|
83
|
+
* [Examples](https://github.com/digital-fabric/polyphony/tree/9e0f3b09213156bdf376ef33684ef267517f06e8/examples/README.md)
|
81
84
|
|
82
85
|
## Contributing to Polyphony
|
83
86
|
|
@@ -1,18 +1,76 @@
|
|
1
1
|
---
|
2
2
|
layout: page
|
3
|
-
title: Design
|
3
|
+
title: The Design of Polyphony
|
4
4
|
nav_order: 5
|
5
5
|
parent: Main Concepts
|
6
6
|
permalink: /main-concepts/design-principles/
|
7
7
|
prev_title: Extending Polyphony
|
8
8
|
---
|
9
|
-
# Design
|
9
|
+
# The Design of Polyphony
|
10
|
+
|
11
|
+
Polyphony is a new gem that aims to enable developing high-performance
|
12
|
+
concurrent applications in Ruby using a fluent, compact syntax and API.
|
13
|
+
Polyphony enables fine-grained concurrency - the splitting up of operations into
|
14
|
+
a large number of concurrent tasks, each concerned with small part of the whole
|
15
|
+
and advancing at its own pace. Polyphony aims to solve some of the problems
|
16
|
+
associated with concurrent Ruby programs using a novel design that sets it apart
|
17
|
+
from other approaches currently being used in Ruby.
|
18
|
+
|
19
|
+
## Origins
|
20
|
+
|
21
|
+
The Ruby core language (at least in its MRI implementation) currently provides
|
22
|
+
two main constructs for performing concurrent work: threads and fibers. While
|
23
|
+
Ruby threads are basically wrappers for OS threads, fibers are essentially
|
24
|
+
continuations, allowing pausing and resuming distinct computations. Fibers have
|
25
|
+
been traditionally used mostly for implementing enumerators and generators.
|
26
|
+
|
27
|
+
In addition to the core Ruby concurrency primitives, some Ruby gems have been
|
28
|
+
offering an alternative solution to writing concurrent Ruby apps, most notably
|
29
|
+
[EventMachine](https://github.com/eventmachine/eventmachine/), which implements
|
30
|
+
an event reactor and offers an asynchronous callback-based API for writing
|
31
|
+
concurrent code.
|
32
|
+
|
33
|
+
In the last couple of years, however, fibers have been receiving more attention
|
34
|
+
as a possible constructs for writing concurrent programs. In particular, the
|
35
|
+
[Async](https://github.com/socketry/async) framework, created by [Samuel
|
36
|
+
Williams](https://github.com/ioquatix), offering a comprehensive set of
|
37
|
+
libraries, employs fibers in conjunction with an event reactor provided by the
|
38
|
+
[nio4r](https://github.com/socketry/nio4r) gem, which wraps the C
|
39
|
+
library [libev](http://software.schmorp.de/pkg/libev.html).
|
40
|
+
|
41
|
+
In addition, recently some effort was undertaken to provide a way to
|
42
|
+
[automatically switch between fibers](https://bugs.ruby-lang.org/issues/13618)
|
43
|
+
whenever a blocking operation is performed, or to [integrate a fiber
|
44
|
+
scheduler](https://bugs.ruby-lang.org/issues/16786) into the core Ruby code.
|
45
|
+
|
46
|
+
Nevertheless, while work is being done to harness fibers for providing a better
|
47
|
+
way to do concurrency in Ruby, fibers remain a mistery for most Ruby
|
48
|
+
programmers, a perplexing unfamiliar corner right at the heart of Ruby.
|
49
|
+
|
50
|
+
## Design Principles
|
51
|
+
|
52
|
+
Polyphony started as an experiment, but over about two years of slow, jerky
|
53
|
+
evolution turned into something I'm really excited to share with the Ruby
|
54
|
+
community. Polyphony's design is both similar and different than the projects
|
55
|
+
mentioned above.
|
56
|
+
|
57
|
+
Polyphony today as nothing like the way it began. A careful examination of the
|
58
|
+
[CHANGELOG](https://github.com/digital-fabric/polyphony/blob/master/CHANGELOG.md)
|
59
|
+
would show how Polyphony explored not only different event reactor designs, but
|
60
|
+
also different API designs incorporating various concurrent paradigms such as
|
61
|
+
promises, async/await, fibers, and finally structured concurrency.
|
62
|
+
|
63
|
+
While Polyphony, like nio4r or EventMachine, uses an event reactor to turn
|
64
|
+
blocking operations into non-blocking ones, it completely embraces fibers and in
|
65
|
+
fact does not provide any callback-based APIs. Furthermore, Polyphony provides
|
66
|
+
fullblown fiber-aware implementations of blocking operations, such as
|
67
|
+
`read/write`, `sleep` or `waitpid`, instead of just event watching primitives.
|
68
|
+
|
69
|
+
Throughout the development process, it was my intention to create a programming
|
70
|
+
interface that would make highly-concurrent
|
71
|
+
|
72
|
+
|
10
73
|
|
11
|
-
Polyphony was created in order to enable developing high-performance concurrent
|
12
|
-
applications in Ruby using a fluent, compact syntax and API. Polyphony enables
|
13
|
-
fine-grained concurrency - the splitting up of operations into a large number of
|
14
|
-
concurrent tasks, each concerned with small part of the whole and advancing at
|
15
|
-
its own pace.
|
16
74
|
|
17
75
|
|
18
76
|
|
@@ -46,8 +104,8 @@ library. Polyphony's design is based on the following principles:
|
|
46
104
|
async callback-style APIs.
|
47
105
|
|
48
106
|
```ruby
|
49
|
-
# in Polyphony, I/O ops block the current fiber, but implicitly yield to
|
50
|
-
# concurrent fibers:
|
107
|
+
# in Polyphony, I/O ops might block the current fiber, but implicitly yield to
|
108
|
+
# other concurrent fibers:
|
51
109
|
clients.each { |client|
|
52
110
|
spin { client.puts 'Elvis has left the chatroom' }
|
53
111
|
}
|
@@ -15,9 +15,9 @@ switching between fibers works in Ruby.
|
|
15
15
|
|
16
16
|
Ruby provides two mechanisms for transferring control between fibers:
|
17
17
|
`Fiber#resume` /`Fiber.yield` and `Fiber#transfer`. The first is inherently
|
18
|
-
asymmetric and is
|
18
|
+
asymmetric and is mostly used for implementing generators and [resumable
|
19
19
|
enumerators](https://blog.appsignal.com/2018/11/27/ruby-magic-fibers-and-enumerators-in-ruby.html).
|
20
|
-
Here's
|
20
|
+
Here's an example:
|
21
21
|
|
22
22
|
```ruby
|
23
23
|
fib = Fiber.new do
|
@@ -31,7 +31,7 @@ end
|
|
31
31
|
10.times { puts fib.resume }
|
32
32
|
```
|
33
33
|
|
34
|
-
|
34
|
+
An implication of using resume / yield is that the main fiber can't yield
|
35
35
|
away, meaning we cannot pause the main fiber using `Fiber.yield`.
|
36
36
|
|
37
37
|
The other fiber control mechanism, using `Fiber#transfer`, is fully symmetric:
|
@@ -45,46 +45,48 @@ ping.transfer
|
|
45
45
|
```
|
46
46
|
|
47
47
|
`Fiber#transform` also allows using the main fiber as a general purpose
|
48
|
-
resumable execution context. Polyphony uses `Fiber#transfer`
|
49
|
-
scheduling fibers.
|
48
|
+
resumable execution context. For that reason, Polyphony uses `Fiber#transfer`
|
49
|
+
exclusively for scheduling fibers. Normally, however, applications based on
|
50
|
+
Polyphony will not use this API directly.
|
50
51
|
|
51
52
|
## The Different Fiber states
|
52
53
|
|
53
|
-
In Polyphony, each fiber has one four possible states:
|
54
|
+
In Polyphony, each fiber has one of four possible states:
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
the fiber is
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
`:runnable`. It then waits its turn to run.
|
56
|
+
- `:runnable` - a new fiber will start in the runnable state. This means it is
|
57
|
+
placed on the thread's run queue and is now waiting its turn to be resumed.
|
58
|
+
- `:running` - once the fiber is resumed, it transitions to the running state.
|
59
|
+
`Fiber.current.state` always returns `:running`.
|
60
|
+
- `:wait` - whenever the fiber performs a blocking operation—such as waiting for
|
61
|
+
a timer to elapse, or for a socket to become readable—the fiber transitions to
|
62
|
+
a waiting state. When the corresponding event occurs the fiber will transition
|
63
|
+
to a `:runnable` state, and will be eventually resumed (`:running`).
|
64
|
+
- `:dead` - once the fiber has terminated, it transitions to the dead state.
|
65
65
|
|
66
66
|
## Switchpoints
|
67
67
|
|
68
|
-
A switchpoint is any point in time at which control might switch from the
|
68
|
+
A switchpoint is any point in time at which control *might* switch from the
|
69
69
|
currently running fiber to another fiber that is `:runnable`. This usually
|
70
|
-
occurs when the currently running fiber starts a blocking operation
|
71
|
-
|
72
|
-
|
70
|
+
occurs when the currently running fiber starts a blocking operation, such as
|
71
|
+
reading from a socket or waiting for a timer. It also occurs when the running
|
72
|
+
fiber has explicitly yielded control using `#snooze` or `#suspend`. A
|
73
|
+
Switchpoint will also occur when the currently running fiber has terminated.
|
73
74
|
|
74
75
|
## Scheduler-less scheduling
|
75
76
|
|
76
77
|
Polyphony relies on [libev](http://software.schmorp.de/pkg/libev.html) for
|
77
78
|
handling events such as I/O readiness, timers and signals. In most event
|
78
79
|
reactor-based libraries and frameworks, such as `nio4r`, `EventMachine` or
|
79
|
-
`node.js`, the
|
80
|
-
user-supplied code *from inside the loop*.
|
81
|
-
|
82
|
-
Polyphony
|
83
|
-
|
80
|
+
`node.js`, the entire application is run inside of a reactor loop, and event
|
81
|
+
callbacks are used to schedule user-supplied code *from inside the loop*.
|
82
|
+
|
83
|
+
In Polyphony, however, we have chosen a concurrency model that does not use a
|
84
|
+
loop to schedule fibers. In fact, in Polyphony there's no outer reactor loop,
|
85
|
+
and there's no *scheduler* per se running on a separate execution context.
|
84
86
|
|
85
87
|
Instead, Polyphony maintains for each thread a run queue, a list of `:runnable`
|
86
|
-
fibers. If no fiber is `:runnable`, the libev event
|
87
|
-
|
88
|
+
fibers. If no fiber is `:runnable`, Polyphony will run the libev event loop until
|
89
|
+
at least one event has occurred. Events are handled by adding the corresponding
|
88
90
|
fibers onto the run queue. Finally, control is transferred to the first fiber on
|
89
91
|
the run queue, which will run until it blocks or terminates, at which point
|
90
92
|
control is transferred to the next runnable fiber.
|
@@ -95,8 +97,8 @@ This approach has numerous benefits:
|
|
95
97
|
leading to less context switches, and less bookkeeping.
|
96
98
|
- Clear separation between the reactor code (the `libev` code) and the fiber
|
97
99
|
scheduling code.
|
98
|
-
- Much less time is spent in
|
99
|
-
|
100
|
+
- Much less time is spent in event loop callbacks, letting the event loop run
|
101
|
+
more efficiently.
|
100
102
|
- Fibers are switched outside of the event reactor code, making it easier to
|
101
103
|
avoid race conditions and unexpected behaviours.
|
102
104
|
|
@@ -121,9 +123,9 @@ are waiting and the main fiber is done running, the Ruby process will terminate.
|
|
121
123
|
## Interrupting blocking operations
|
122
124
|
|
123
125
|
Sometimes it is desirable to be able to interrupt a blocking operation, such as
|
124
|
-
waiting for a socket to be readable, or sleeping
|
125
|
-
|
126
|
-
|
126
|
+
waiting for a socket to be readable, or sleeping. This is especially useful when
|
127
|
+
higher-level constructs are needed for controlling multiple concurrent
|
128
|
+
operations.
|
127
129
|
|
128
130
|
Polyphony provides the ability to interrupt a blocking operation by harnessing
|
129
131
|
the ability to transfer values back and forth between fibers using
|
@@ -134,52 +136,24 @@ signalling that the blocking operation has been unsuccessful and allowing
|
|
134
136
|
exception handling using the builtin mechanisms offered by Ruby, namely `rescue`
|
135
137
|
and `ensure` (see also [exception handling](exception-handling.md)).
|
136
138
|
|
137
|
-
|
138
|
-
actual code for I/O reading in Polyphony is written in C and is a bit more
|
139
|
-
involved):
|
139
|
+
This mode of operation makes implementing timeouts almost trivial:
|
140
140
|
|
141
141
|
```ruby
|
142
|
-
def
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
else
|
148
|
-
return result
|
149
|
-
end
|
142
|
+
def with_timeout(duration)
|
143
|
+
interruptible_fiber = Fiber.current
|
144
|
+
timeout_fiber = spin do
|
145
|
+
sleep duration
|
146
|
+
interruptible_fiber.raise 'timeout'
|
150
147
|
end
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
fiber = Fiber.current
|
155
|
-
watcher = Gyro::IO.new(io, :read) { fiber.transfer }
|
156
|
-
|
157
|
-
# run any scheduled fibers or run libev reactor waiting for events
|
158
|
-
result = GV.run
|
159
|
-
|
160
|
-
# waiting fiber is resumed - check transferred value
|
161
|
-
raise result if result.is_a?(Exception)
|
162
|
-
result
|
148
|
+
|
149
|
+
# do work
|
150
|
+
yield
|
163
151
|
ensure
|
164
|
-
|
165
|
-
watcher.active = false
|
152
|
+
timeout_fiber.terminate
|
166
153
|
end
|
167
|
-
```
|
168
154
|
|
169
|
-
|
170
|
-
|
171
|
-
by scheduling the corresponding fiber with an exception:
|
172
|
-
|
173
|
-
```ruby
|
174
|
-
def timeout(duration)
|
175
|
-
fiber = Fiber.current
|
176
|
-
interrupter = spin do
|
177
|
-
Gyro::Timer.new(duration, 0).await
|
178
|
-
fiber.transfer(TimerException.new)
|
179
|
-
end
|
180
|
-
yield
|
181
|
-
ensure
|
182
|
-
interrupter.stop
|
155
|
+
with_timeout(10) do
|
156
|
+
HTTParty.get 'https://acme.com/'
|
183
157
|
end
|
184
158
|
```
|
185
159
|
|
@@ -187,6 +161,15 @@ end
|
|
187
161
|
|
188
162
|
Polyphony performs fiber scheduling separately for each thread. Each thread,
|
189
163
|
therefore, will be able to run multiple fibers independently from other threads.
|
164
|
+
Multithreading in Ruby has limited benefit, due to the global virtual lock that
|
165
|
+
prevents true parallelism. But offloading work to a separate thread might be
|
166
|
+
eneficial when a Polyphonic app needs to use APIs that are not fiber-aware, such
|
167
|
+
as blocking database calls (SQLite in particular), or system calls that might
|
168
|
+
block for an extended duration.
|
169
|
+
|
170
|
+
For this, you can either spawn a new thread, or use the provided
|
171
|
+
`Polyphony::ThreadPool` class that allows you to offload work to a pool of
|
172
|
+
threads.
|
190
173
|
|
191
174
|
## The fiber scheduling algorithm in full
|
192
175
|
|