polyphony 0.43.8
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 +7 -0
- data/.gitbook.yaml +4 -0
- data/.github/workflows/test.yml +29 -0
- data/.gitignore +59 -0
- data/.rubocop.yml +175 -0
- data/CHANGELOG.md +393 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +141 -0
- data/LICENSE +21 -0
- data/README.md +51 -0
- data/Rakefile +26 -0
- data/TODO.md +201 -0
- data/bin/polyphony-debug +87 -0
- data/docs/_config.yml +64 -0
- data/docs/_includes/head.html +40 -0
- data/docs/_includes/title.html +1 -0
- data/docs/_sass/custom/custom.scss +10 -0
- data/docs/_sass/overrides.scss +0 -0
- data/docs/_user-guide/all-about-timers.md +126 -0
- data/docs/_user-guide/index.md +9 -0
- data/docs/_user-guide/web-server.md +136 -0
- data/docs/api-reference/exception.md +27 -0
- data/docs/api-reference/fiber.md +425 -0
- data/docs/api-reference/index.md +9 -0
- data/docs/api-reference/io.md +36 -0
- data/docs/api-reference/object.md +99 -0
- data/docs/api-reference/polyphony-baseexception.md +33 -0
- data/docs/api-reference/polyphony-cancel.md +26 -0
- data/docs/api-reference/polyphony-moveon.md +24 -0
- data/docs/api-reference/polyphony-net.md +20 -0
- data/docs/api-reference/polyphony-process.md +28 -0
- data/docs/api-reference/polyphony-resourcepool.md +59 -0
- data/docs/api-reference/polyphony-restart.md +18 -0
- data/docs/api-reference/polyphony-terminate.md +18 -0
- data/docs/api-reference/polyphony-threadpool.md +67 -0
- data/docs/api-reference/polyphony-throttler.md +77 -0
- data/docs/api-reference/polyphony.md +36 -0
- data/docs/api-reference/thread.md +88 -0
- data/docs/assets/img/echo-fibers.svg +1 -0
- data/docs/assets/img/sleeping-fiber.svg +1 -0
- data/docs/faq.md +195 -0
- data/docs/favicon.ico +0 -0
- data/docs/getting-started/index.md +10 -0
- data/docs/getting-started/installing.md +34 -0
- data/docs/getting-started/overview.md +486 -0
- data/docs/getting-started/tutorial.md +359 -0
- data/docs/index.md +94 -0
- data/docs/main-concepts/concurrency.md +151 -0
- data/docs/main-concepts/design-principles.md +161 -0
- data/docs/main-concepts/exception-handling.md +291 -0
- data/docs/main-concepts/extending.md +89 -0
- data/docs/main-concepts/fiber-scheduling.md +197 -0
- data/docs/main-concepts/index.md +9 -0
- data/docs/polyphony-logo.png +0 -0
- data/examples/adapters/concurrent-ruby.rb +9 -0
- data/examples/adapters/pg_client.rb +36 -0
- data/examples/adapters/pg_notify.rb +35 -0
- data/examples/adapters/pg_pool.rb +43 -0
- data/examples/adapters/pg_transaction.rb +31 -0
- data/examples/adapters/redis_blpop.rb +12 -0
- data/examples/adapters/redis_channels.rb +122 -0
- data/examples/adapters/redis_client.rb +19 -0
- data/examples/adapters/redis_pubsub.rb +26 -0
- data/examples/adapters/redis_pubsub_perf.rb +68 -0
- data/examples/core/01-spinning-up-fibers.rb +18 -0
- data/examples/core/02-awaiting-fibers.rb +20 -0
- data/examples/core/03-interrupting.rb +39 -0
- data/examples/core/04-handling-signals.rb +19 -0
- data/examples/core/xx-agent.rb +102 -0
- data/examples/core/xx-at_exit.rb +29 -0
- data/examples/core/xx-caller.rb +12 -0
- data/examples/core/xx-channels.rb +45 -0
- data/examples/core/xx-daemon.rb +14 -0
- data/examples/core/xx-deadlock.rb +8 -0
- data/examples/core/xx-deferring-an-operation.rb +14 -0
- data/examples/core/xx-erlang-style-genserver.rb +81 -0
- data/examples/core/xx-exception-backtrace.rb +40 -0
- data/examples/core/xx-fork-cleanup.rb +22 -0
- data/examples/core/xx-fork-spin.rb +42 -0
- data/examples/core/xx-fork-terminate.rb +27 -0
- data/examples/core/xx-forking.rb +24 -0
- data/examples/core/xx-move_on.rb +23 -0
- data/examples/core/xx-pingpong.rb +18 -0
- data/examples/core/xx-queue-async.rb +120 -0
- data/examples/core/xx-readpartial.rb +18 -0
- data/examples/core/xx-recurrent-timer.rb +12 -0
- data/examples/core/xx-resource_delegate.rb +31 -0
- data/examples/core/xx-signals.rb +16 -0
- data/examples/core/xx-sleep-forever.rb +9 -0
- data/examples/core/xx-sleeping.rb +25 -0
- data/examples/core/xx-snooze-starve.rb +16 -0
- data/examples/core/xx-spin-fork.rb +49 -0
- data/examples/core/xx-spin_error_backtrace.rb +33 -0
- data/examples/core/xx-state-machine.rb +51 -0
- data/examples/core/xx-stop.rb +20 -0
- data/examples/core/xx-supervise-process.rb +30 -0
- data/examples/core/xx-supervisors.rb +21 -0
- data/examples/core/xx-thread-selector-sleep.rb +51 -0
- data/examples/core/xx-thread-selector-snooze.rb +46 -0
- data/examples/core/xx-thread-sleep.rb +17 -0
- data/examples/core/xx-thread-snooze.rb +34 -0
- data/examples/core/xx-thread_pool.rb +17 -0
- data/examples/core/xx-throttling.rb +18 -0
- data/examples/core/xx-timeout.rb +10 -0
- data/examples/core/xx-timer-gc.rb +17 -0
- data/examples/core/xx-trace.rb +79 -0
- data/examples/core/xx-using-a-mutex.rb +21 -0
- data/examples/core/xx-worker-thread.rb +30 -0
- data/examples/io/tunnel.rb +48 -0
- data/examples/io/xx-backticks.rb +11 -0
- data/examples/io/xx-echo_client.rb +25 -0
- data/examples/io/xx-echo_client_from_stdin.rb +21 -0
- data/examples/io/xx-echo_pipe.rb +16 -0
- data/examples/io/xx-echo_server.rb +17 -0
- data/examples/io/xx-echo_server_with_timeout.rb +34 -0
- data/examples/io/xx-echo_stdin.rb +14 -0
- data/examples/io/xx-happy-eyeballs.rb +36 -0
- data/examples/io/xx-httparty.rb +38 -0
- data/examples/io/xx-irb.rb +17 -0
- data/examples/io/xx-net-http.rb +15 -0
- data/examples/io/xx-open.rb +16 -0
- data/examples/io/xx-switch.rb +15 -0
- data/examples/io/xx-system.rb +11 -0
- data/examples/io/xx-tcpserver.rb +15 -0
- data/examples/io/xx-tcpsocket.rb +18 -0
- data/examples/io/xx-zip.rb +19 -0
- data/examples/performance/fiber_transfer.rb +47 -0
- data/examples/performance/fs_read.rb +38 -0
- data/examples/performance/mem-usage.rb +56 -0
- data/examples/performance/messaging.rb +29 -0
- data/examples/performance/multi_snooze.rb +33 -0
- data/examples/performance/snooze.rb +39 -0
- data/examples/performance/snooze_raw.rb +39 -0
- data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +74 -0
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +45 -0
- data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +58 -0
- data/examples/performance/thread-vs-fiber/threaded_server.rb +27 -0
- data/examples/performance/thread-vs-fiber/xx-httparty_multi.rb +36 -0
- data/examples/performance/thread-vs-fiber/xx-httparty_threaded.rb +29 -0
- data/examples/performance/thread_pool_perf.rb +63 -0
- data/examples/performance/xx-array.rb +11 -0
- data/examples/performance/xx-fiber-switch.rb +9 -0
- data/examples/performance/xx-snooze.rb +15 -0
- data/examples/xx-spin.rb +32 -0
- data/ext/libev/Changes +548 -0
- data/ext/libev/LICENSE +37 -0
- data/ext/libev/README +59 -0
- data/ext/libev/README.embed +3 -0
- data/ext/libev/ev.c +5279 -0
- data/ext/libev/ev.h +856 -0
- data/ext/libev/ev_epoll.c +296 -0
- data/ext/libev/ev_kqueue.c +224 -0
- data/ext/libev/ev_linuxaio.c +642 -0
- data/ext/libev/ev_poll.c +156 -0
- data/ext/libev/ev_port.c +192 -0
- data/ext/libev/ev_select.c +316 -0
- data/ext/libev/ev_vars.h +215 -0
- data/ext/libev/ev_win32.c +162 -0
- data/ext/libev/ev_wrap.h +216 -0
- data/ext/libev/test_libev_win32.c +123 -0
- data/ext/polyphony/extconf.rb +20 -0
- data/ext/polyphony/fiber.c +109 -0
- data/ext/polyphony/libev.c +2 -0
- data/ext/polyphony/libev.h +9 -0
- data/ext/polyphony/libev_agent.c +882 -0
- data/ext/polyphony/polyphony.c +71 -0
- data/ext/polyphony/polyphony.h +97 -0
- data/ext/polyphony/polyphony_ext.c +21 -0
- data/ext/polyphony/queue.c +168 -0
- data/ext/polyphony/ring_buffer.c +96 -0
- data/ext/polyphony/ring_buffer.h +28 -0
- data/ext/polyphony/thread.c +208 -0
- data/ext/polyphony/tracing.c +11 -0
- data/lib/polyphony.rb +136 -0
- data/lib/polyphony/adapters/fs.rb +19 -0
- data/lib/polyphony/adapters/irb.rb +52 -0
- data/lib/polyphony/adapters/postgres.rb +110 -0
- data/lib/polyphony/adapters/process.rb +33 -0
- data/lib/polyphony/adapters/redis.rb +67 -0
- data/lib/polyphony/adapters/trace.rb +138 -0
- data/lib/polyphony/core/channel.rb +46 -0
- data/lib/polyphony/core/exceptions.rb +36 -0
- data/lib/polyphony/core/global_api.rb +124 -0
- data/lib/polyphony/core/resource_pool.rb +117 -0
- data/lib/polyphony/core/sync.rb +21 -0
- data/lib/polyphony/core/thread_pool.rb +64 -0
- data/lib/polyphony/core/throttler.rb +41 -0
- data/lib/polyphony/event.rb +17 -0
- data/lib/polyphony/extensions/core.rb +174 -0
- data/lib/polyphony/extensions/fiber.rb +379 -0
- data/lib/polyphony/extensions/io.rb +221 -0
- data/lib/polyphony/extensions/openssl.rb +81 -0
- data/lib/polyphony/extensions/socket.rb +150 -0
- data/lib/polyphony/extensions/thread.rb +108 -0
- data/lib/polyphony/net.rb +77 -0
- data/lib/polyphony/version.rb +5 -0
- data/polyphony.gemspec +40 -0
- data/test/coverage.rb +54 -0
- data/test/eg.rb +27 -0
- data/test/helper.rb +56 -0
- data/test/q.rb +24 -0
- data/test/run.rb +5 -0
- data/test/stress.rb +25 -0
- data/test/test_agent.rb +130 -0
- data/test/test_event.rb +59 -0
- data/test/test_ext.rb +196 -0
- data/test/test_fiber.rb +988 -0
- data/test/test_global_api.rb +352 -0
- data/test/test_io.rb +249 -0
- data/test/test_kernel.rb +57 -0
- data/test/test_process_supervision.rb +46 -0
- data/test/test_queue.rb +112 -0
- data/test/test_resource_pool.rb +138 -0
- data/test/test_signal.rb +100 -0
- data/test/test_socket.rb +34 -0
- data/test/test_supervise.rb +103 -0
- data/test/test_thread.rb +170 -0
- data/test/test_thread_pool.rb +101 -0
- data/test/test_throttler.rb +50 -0
- data/test/test_trace.rb +68 -0
- metadata +482 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: Extending Polyphony
|
4
|
+
nav_order: 4
|
5
|
+
parent: Main Concepts
|
6
|
+
permalink: /main-concepts/extending/
|
7
|
+
prev_title: Exception Handling
|
8
|
+
next_title: The Design of Polyphony
|
9
|
+
---
|
10
|
+
# Extending Polyphony
|
11
|
+
|
12
|
+
Polyphony was designed to ease the transition from blocking APIs and
|
13
|
+
callback-based API to non-blocking, fiber-based ones. It is important to
|
14
|
+
understand that not all blocking calls can be easily converted into
|
15
|
+
non-blocking calls. That might be the case with Ruby gems based on C-extensions,
|
16
|
+
such as database libraries. In that case, Polyphony's built-in
|
17
|
+
[thread pool](#threadpool) might be used for offloading such blocking calls.
|
18
|
+
|
19
|
+
### Adapting callback-based APIs
|
20
|
+
|
21
|
+
Some of the most common patterns in Ruby APIs is the callback pattern, in which
|
22
|
+
the API takes a block as a callback to be called upon completion of a task. One
|
23
|
+
such example can be found in the excellent
|
24
|
+
[http_parser.rb](https://github.com/tmm1/http_parser.rb/) gem, which is used by
|
25
|
+
Polyphony itself to provide HTTP 1 functionality. The `HTTP:Parser` provides
|
26
|
+
multiple hooks, or callbacks, for being notified when an HTTP request is
|
27
|
+
complete. The typical callback-based setup is as follows:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'http/parser'
|
31
|
+
@parser = Http::Parser.new
|
32
|
+
|
33
|
+
def on_receive(data)
|
34
|
+
@parser < data
|
35
|
+
end
|
36
|
+
|
37
|
+
@parser.on_message_complete do |env|
|
38
|
+
process_request(env)
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
A program using `http_parser.rb` in conjunction with Polyphony might do the
|
43
|
+
following:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'http/parser'
|
47
|
+
require 'polyphony'
|
48
|
+
|
49
|
+
def handle_client(client)
|
50
|
+
parser = Http::Parser.new
|
51
|
+
req = nil
|
52
|
+
parser.on_message_complete { |env| req = env }
|
53
|
+
loop do
|
54
|
+
parser << client.read
|
55
|
+
if req
|
56
|
+
handle_request(req)
|
57
|
+
req = nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
Another possibility would be to monkey-patch `Http::Parser` in order to
|
64
|
+
encapsulate the state of the request:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class Http::Parser
|
68
|
+
def setup
|
69
|
+
self.on_message_complete = proc { @request_complete = true }
|
70
|
+
end
|
71
|
+
|
72
|
+
def parser(data)
|
73
|
+
self << data
|
74
|
+
return nil unless @request_complete
|
75
|
+
|
76
|
+
@request_complete = nil
|
77
|
+
self
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle_client(client)
|
82
|
+
parser = Http::Parser.new
|
83
|
+
loop do
|
84
|
+
if req == parser.parse(client.read)
|
85
|
+
handle_request(req)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
@@ -0,0 +1,197 @@
|
|
1
|
+
---
|
2
|
+
layout: page
|
3
|
+
title: How Fibers are Scheduled
|
4
|
+
nav_order: 2
|
5
|
+
parent: Main Concepts
|
6
|
+
permalink: /main-concepts/fiber-scheduling/
|
7
|
+
prev_title: Concurrency the Easy Way
|
8
|
+
next_title: Exception Handling
|
9
|
+
---
|
10
|
+
|
11
|
+
# How Fibers are Scheduled
|
12
|
+
|
13
|
+
Before we discuss how fibers are scheduled in Polyphony, let's examine how
|
14
|
+
switching between fibers works in Ruby.
|
15
|
+
|
16
|
+
Ruby provides two mechanisms for transferring control between fibers:
|
17
|
+
`Fiber#resume` /`Fiber.yield` and `Fiber#transfer`. The first is inherently
|
18
|
+
asymmetric and is mostly used for implementing generators and [resumable
|
19
|
+
enumerators](https://blog.appsignal.com/2018/11/27/ruby-magic-fibers-and-enumerators-in-ruby.html).
|
20
|
+
Here's an 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
|
+
An 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#transfer` also allows using the main fiber as a general purpose
|
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.
|
51
|
+
|
52
|
+
## The Different Fiber states
|
53
|
+
|
54
|
+
In Polyphony, each fiber has one of four possible states:
|
55
|
+
|
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
|
+
|
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, 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.
|
74
|
+
|
75
|
+
## Scheduler-less scheduling
|
76
|
+
|
77
|
+
Polyphony relies on [libev](http://software.schmorp.de/pkg/libev.html) for
|
78
|
+
handling events such as I/O readiness, timers and signals. In most event
|
79
|
+
reactor-based libraries and frameworks, such as `nio4r`, `EventMachine` or
|
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.
|
86
|
+
|
87
|
+
Instead, Polyphony maintains for each thread a run queue, a list of `:runnable`
|
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
|
90
|
+
fibers onto the run queue. Finally, control is transferred to the first fiber on
|
91
|
+
the run queue, which will run until it blocks or terminates, at which point
|
92
|
+
control is transferred to the next runnable fiber.
|
93
|
+
|
94
|
+
This approach has numerous benefits:
|
95
|
+
|
96
|
+
- No separate reactor fiber that needs to be resumed on each blocking operation,
|
97
|
+
leading to less context switches, and less bookkeeping.
|
98
|
+
- Clear separation between the reactor code (the `libev` code) and the fiber
|
99
|
+
scheduling code.
|
100
|
+
- Much less time is spent in event loop callbacks, letting the event loop run
|
101
|
+
more efficiently.
|
102
|
+
- Fibers are switched outside of the event reactor code, making it easier to
|
103
|
+
avoid race conditions and unexpected behaviours.
|
104
|
+
|
105
|
+
## Fiber scheduling and fiber switching
|
106
|
+
|
107
|
+
The Polyphony scheduling model makes a clear separation between the scheduling
|
108
|
+
of fibers and the switching of fibers. The scheduling of fibers is the act of
|
109
|
+
marking the fiber as `:runnable`, to be run at the earliest opportunity, but not
|
110
|
+
immediately. The switching of fibers is the act of actually transferring control
|
111
|
+
to another fiber, namely the first fiber in the run queue.
|
112
|
+
|
113
|
+
The scheduling of fibers can occur at any time, either as a result of an event
|
114
|
+
occuring, an exception being raised, or using `Fiber#schedule`. The switching of
|
115
|
+
fibers will occur only when the currently running fiber has reached a
|
116
|
+
switchpoint, e.g. when a blocking operation is started, or upon calling
|
117
|
+
`Fiber#suspend` or `Fiber#snooze`. As mentioned earlier, in order to switch to a
|
118
|
+
scheduled fiber, Polyphony uses `Fiber#transfer`.
|
119
|
+
|
120
|
+
When a fiber terminates, any other runnable fibers will be run. If no fibers
|
121
|
+
are waiting and the main fiber is done running, the Ruby process will terminate.
|
122
|
+
|
123
|
+
## Interrupting blocking operations
|
124
|
+
|
125
|
+
Sometimes it is desirable to be able to interrupt a blocking operation, such as
|
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.
|
129
|
+
|
130
|
+
Polyphony provides the ability to interrupt a blocking operation by harnessing
|
131
|
+
the ability to transfer values back and forth between fibers using
|
132
|
+
`Fiber#transfer`. Whenever a waiting fiber yields control to the next scheduled
|
133
|
+
fiber, the value received upon being resumed is checked. If the value is an
|
134
|
+
exception, it will be raised in the context of the waiting fiber, effectively
|
135
|
+
signalling that the blocking operation has been unsuccessful and allowing
|
136
|
+
exception handling using the builtin mechanisms offered by Ruby, namely `rescue`
|
137
|
+
and `ensure` (see also [exception handling](exception-handling.md)).
|
138
|
+
|
139
|
+
This mode of operation makes implementing timeouts almost trivial:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
def with_timeout(duration)
|
143
|
+
interruptible_fiber = Fiber.current
|
144
|
+
timeout_fiber = spin do
|
145
|
+
sleep duration
|
146
|
+
interruptible_fiber.raise 'timeout'
|
147
|
+
end
|
148
|
+
|
149
|
+
# do work
|
150
|
+
yield
|
151
|
+
ensure
|
152
|
+
timeout_fiber.terminate
|
153
|
+
end
|
154
|
+
|
155
|
+
with_timeout(10) do
|
156
|
+
HTTParty.get 'https://acme.com/'
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
## Fiber Scheduling in a Multithreaded Program
|
161
|
+
|
162
|
+
Polyphony performs fiber scheduling separately for each thread. Each thread,
|
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.
|
173
|
+
|
174
|
+
## The fiber scheduling algorithm in full
|
175
|
+
|
176
|
+
Here is the summary of the Polyphony scheduling algorithm:
|
177
|
+
|
178
|
+
- loop
|
179
|
+
- pull first runnable fiber from run queue
|
180
|
+
- if runnable fiber is not nil
|
181
|
+
- if the ref count greater than 0
|
182
|
+
- increment the run_no_wait counter
|
183
|
+
- if the run_no_wait counter is greater than 10 and greater than the run
|
184
|
+
queue length
|
185
|
+
- reset the run_no_wait counter
|
186
|
+
- run the event loop once without waiting for events (using
|
187
|
+
`EVRUN_NOWAIT`)
|
188
|
+
- break out of the loop
|
189
|
+
- if the ref count is 0
|
190
|
+
- break out of the loop
|
191
|
+
- run the event loop until one or more events are generated (using
|
192
|
+
`EVRUN_ONCE`)
|
193
|
+
- if next runnable fiber is nil
|
194
|
+
- return
|
195
|
+
- get scheduled resume value for next runnable fiber
|
196
|
+
- mark the next runnable fiber as not runnable
|
197
|
+
- switch to the next runnable fiber using `Fiber#transfer`
|
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/adapters/postgres'
|
5
|
+
|
6
|
+
def get_records
|
7
|
+
$db.query('select 1 as test')
|
8
|
+
# puts "got #{res.ntuples} records: #{res.to_a}"
|
9
|
+
rescue StandardError => e
|
10
|
+
puts "got error: #{e.inspect}"
|
11
|
+
puts e.backtrace.join("\n")
|
12
|
+
end
|
13
|
+
|
14
|
+
time_printer = spin do
|
15
|
+
last = Time.now
|
16
|
+
throttled_loop(10) do
|
17
|
+
now = Time.now
|
18
|
+
puts now - last
|
19
|
+
last = now
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
$db = PG.connect(
|
24
|
+
host: '/tmp',
|
25
|
+
user: 'reality',
|
26
|
+
password: nil,
|
27
|
+
dbname: 'reality',
|
28
|
+
sslmode: 'require'
|
29
|
+
)
|
30
|
+
|
31
|
+
X = 10_000
|
32
|
+
t0 = Time.now
|
33
|
+
X.times { get_records }
|
34
|
+
puts "query rate: #{X / (Time.now - t0)} reqs/s"
|
35
|
+
|
36
|
+
time_printer.stop
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/adapters/postgres'
|
5
|
+
|
6
|
+
opts = {
|
7
|
+
host: '/tmp',
|
8
|
+
user: 'reality',
|
9
|
+
password: nil,
|
10
|
+
dbname: 'reality',
|
11
|
+
sslmode: 'require'
|
12
|
+
}
|
13
|
+
|
14
|
+
db1 = PG.connect(opts)
|
15
|
+
db2 = PG.connect(opts)
|
16
|
+
|
17
|
+
spin_loop {
|
18
|
+
STDOUT << '.'
|
19
|
+
sleep 0.1
|
20
|
+
}
|
21
|
+
|
22
|
+
db1.query('listen foo')
|
23
|
+
spin_loop {
|
24
|
+
db1.wait_for_notify(1) { |channel, _, msg| puts "\n#{msg}" }
|
25
|
+
STDOUT << '?'
|
26
|
+
}
|
27
|
+
|
28
|
+
spin_loop {
|
29
|
+
sql = format("notify foo, %s", db2.escape_literal(Time.now.to_s))
|
30
|
+
db2.query(sql)
|
31
|
+
STDOUT << '!'
|
32
|
+
sleep rand(1.5..3)
|
33
|
+
}
|
34
|
+
|
35
|
+
suspend
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/adapters/postgres'
|
5
|
+
|
6
|
+
PGOPTS = {
|
7
|
+
host: '/tmp',
|
8
|
+
user: 'reality',
|
9
|
+
password: nil,
|
10
|
+
dbname: 'reality',
|
11
|
+
sslmode: 'require'
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
DBPOOL = Polyphony::ResourcePool.new(limit: 16) { PG.connect(PGOPTS) }
|
15
|
+
|
16
|
+
def get_records(db)
|
17
|
+
db.query('select pg_sleep(0.001) as test')
|
18
|
+
# puts "got #{res.ntuples} records: #{res.to_a}"
|
19
|
+
rescue StandardError => e
|
20
|
+
puts "got error: #{e.inspect}"
|
21
|
+
puts e.backtrace.join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
CONCURRENCY = ARGV.first ? ARGV.first.to_i : 10
|
25
|
+
puts "concurrency: #{CONCURRENCY}"
|
26
|
+
|
27
|
+
DBPOOL.preheat!
|
28
|
+
t0 = Time.now
|
29
|
+
count = 0
|
30
|
+
|
31
|
+
fibers = CONCURRENCY.times.map do
|
32
|
+
spin do
|
33
|
+
loop do
|
34
|
+
DBPOOL.acquire do |db|
|
35
|
+
get_records(db)
|
36
|
+
count += 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
sleep 5
|
42
|
+
puts "count: #{count} query rate: #{count / (Time.now - t0)} queries/s"
|
43
|
+
fibers.each(&:interrupt)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'polyphony/adapters/postgres'
|
5
|
+
|
6
|
+
DB = PG.connect(
|
7
|
+
host: '/tmp',
|
8
|
+
user: 'reality',
|
9
|
+
password: nil,
|
10
|
+
dbname: 'reality',
|
11
|
+
sslmode: 'require'
|
12
|
+
)
|
13
|
+
|
14
|
+
def perform(error)
|
15
|
+
puts '*' * 40
|
16
|
+
DB.transaction do
|
17
|
+
res = DB.query('select 1 as test')
|
18
|
+
puts "result: #{res.to_a}"
|
19
|
+
raise 'hello' if error
|
20
|
+
|
21
|
+
DB.transaction do
|
22
|
+
res = DB.query('select 2 as test')
|
23
|
+
puts "result: #{res.to_a}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
rescue StandardError => e
|
27
|
+
puts "error: #{e.inspect}"
|
28
|
+
end
|
29
|
+
|
30
|
+
perform(true)
|
31
|
+
perform(false)
|