bran 0.0.1 → 0.0.2
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/README.md +32 -0
- data/lib/bran/ext/ethon.rb +11 -0
- data/lib/bran/ext/ethon/curl.rb +21 -0
- data/lib/bran/ext/ethon/easy.rb +15 -0
- data/lib/bran/ext/ethon/multi.rb +24 -0
- data/lib/bran/ext/ethon/multi/bran.rb +142 -0
- data/lib/bran/fiber_manager.rb +4 -0
- data/lib/bran/libuv/reactor.rb +70 -35
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28390755dc30a2dc1027f082f7f5340d88222bb8
|
4
|
+
data.tar.gz: 32514e834d6f931cab276f2480d45be6588e3982
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52c90e1f8c60ccc4607da7af594e30366732c1a97778b320749f09f5feced266b4169bb5e1320621fc7a8a72eef7aee3f1edc6ddac2bd85b392e3e33024e7aa6
|
7
|
+
data.tar.gz: 86e450e1c22aacd177e02e4331f71100de0761912361304d63eb9bd579327a05a4874202c3caccc86d085384959d7512c59b30ad52bb783f9d6c41e62852e1e1
|
data/README.md
CHANGED
@@ -18,6 +18,35 @@ Bran::Ext.check_assumptions = true
|
|
18
18
|
|
19
19
|
The description of each Bran integration will also be labeled "Tested with" for known good combinations of dependent gems.
|
20
20
|
|
21
|
+
### IO, TCPServer
|
22
|
+
|
23
|
+
Use Bran with any pure-Ruby library or application that uses plain IO (such as TCPSocket and TCPServer). All blocking `read` operations on IO, `IO.select`, and `TCPServer#accept` will be patched to yield their fiber instead of blocking.
|
24
|
+
|
25
|
+
Without any other Bran integrations, this requires manually running your own concurrent fibers of execution, scoped with a thread-local fiber manager. If no fiber manager is present, the methods will function as normal (without Bran). It's easier to use with another application-wide integration (like the Rainbows integration) that manages concurrent fibers for you.
|
26
|
+
|
27
|
+
To activate the Bran integrations, simply load the corresponding files:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require "bran/ext/io"
|
31
|
+
require "bran/ext/tcp_server"
|
32
|
+
```
|
33
|
+
|
34
|
+
### Ethon (Typhoeus)
|
35
|
+
|
36
|
+
Use Bran with any library or application that uses [Ethon][ethon] to perform HTTP requests (for example, [Typhoeus][typhoeus]). Both `Ethon::Easy#perform` and `Ethon::Multi#perform` will be patched to yield their fiber instead of blocking while waiting for the HTTP response.
|
37
|
+
|
38
|
+
Without any other Bran integrations, this requires manually running your own concurrent fibers of execution, scoped with a thread-local fiber manager. If no fiber manager is present, the methods will function as normal (without Bran). It's easier to use with another application-wide integration (like the Rainbows integration) that manages concurrent fibers for you.
|
39
|
+
|
40
|
+
To activate the Bran integration, simply load the corresponding file:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require "bran/ext/ethon"
|
44
|
+
```
|
45
|
+
|
46
|
+
Tested with:
|
47
|
+
|
48
|
+
- `ethon 0.8.1`
|
49
|
+
|
21
50
|
### Rainbows
|
22
51
|
|
23
52
|
Use Bran with [Rainbows][rainbows], a [Unicorn][unicorn]-based and [Unicorn][unicorn]-compatible webserver from the creators of [Unicorn][unicorn].
|
@@ -36,8 +65,11 @@ end
|
|
36
65
|
```
|
37
66
|
|
38
67
|
Tested with:
|
68
|
+
|
39
69
|
- `rainbows 5.0.0` (`unicorn 5.0.0`, `kgio 2.10.0`)
|
40
70
|
- `rainbows 5.0.0` (`unicorn 5.0.1`, `kgio 2.10.0`)
|
41
71
|
|
72
|
+
[ethon]: https://github.com/typhoeus/ethon
|
73
|
+
[typhoeus]: https://github.com/typhoeus/typhoeus
|
42
74
|
[rainbows]: http://rainbows.bogomips.org/
|
43
75
|
[unicorn]: http://unicorn.bogomips.org/
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
Ethon::Curl.ffi_lib ["libcurl", "libcurl.so.4"]
|
3
|
+
|
4
|
+
Ethon::Curl.attach_function :multi_socket_action, :curl_multi_socket_action,
|
5
|
+
[:pointer, :int, :int, :pointer], :multi_code, blocking: true
|
6
|
+
|
7
|
+
module Ethon
|
8
|
+
module Curl
|
9
|
+
POLL_NONE = 0
|
10
|
+
POLL_IN = 1
|
11
|
+
POLL_OUT = 2
|
12
|
+
POLL_INOUT = 3
|
13
|
+
POLL_REMOVE = 4
|
14
|
+
|
15
|
+
CSELECT_IN = 0x01
|
16
|
+
CSELECT_OUT = 0x02
|
17
|
+
CSELECT_ERR = 0x04
|
18
|
+
|
19
|
+
SOCKET_TIMEOUT = -1
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
require_relative "multi/bran"
|
3
|
+
|
4
|
+
Module.new do
|
5
|
+
Ethon::Multi.prepend self
|
6
|
+
|
7
|
+
def initialize(*)
|
8
|
+
super
|
9
|
+
|
10
|
+
# Create a bran performer that can steal work from this multi.
|
11
|
+
# Fall back to normal path (no bran) if not under fiber management.
|
12
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
13
|
+
@bran = Ethon::Multi::Bran.new(fm, self) if fm
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
# Use the bran performer instead of the normal path to perform this multi.
|
18
|
+
# Fall back to normal path (no bran) if not under fiber management,
|
19
|
+
# or if it fails because we are not under the same fiber manager as before.
|
20
|
+
(@bran && @bran.perform) || super
|
21
|
+
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
|
2
|
+
::Bran::Ext.assume do
|
3
|
+
check ::Ethon::Multi < ::Ethon::Multi::Operations
|
4
|
+
check ::Ethon::Multi::Operations.instance_method(:handle).arity == 0
|
5
|
+
check ::Ethon::Multi::Operations.instance_method(:check).arity == 0
|
6
|
+
|
7
|
+
check ::Ethon::Multi < ::Ethon::Multi::Stack
|
8
|
+
check ::Ethon::Multi::Stack.instance_method(:easy_handles).arity == 0
|
9
|
+
|
10
|
+
check ::Ethon::Multi < ::Ethon::Multi::Options
|
11
|
+
check ::Ethon::Multi::Options.instance_method(:socketfunction=).arity == 1
|
12
|
+
check ::Ethon::Multi::Options.instance_method(:timerfunction=).arity == 1
|
13
|
+
|
14
|
+
check ::Ethon::Errors::MultiTimeout.instance_method(:initialize).arity == 1
|
15
|
+
|
16
|
+
check ::Ethon::Curl.method(:multi_socket_action)
|
17
|
+
check ::Ethon::Curl::POLL_IN
|
18
|
+
check ::Ethon::Curl::POLL_OUT
|
19
|
+
check ::Ethon::Curl::POLL_INOUT
|
20
|
+
check ::Ethon::Curl::POLL_REMOVE
|
21
|
+
check ::Ethon::Curl::SOCKET_TIMEOUT
|
22
|
+
check ::Ethon::Curl::CSELECT_IN
|
23
|
+
check ::Ethon::Curl::CSELECT_OUT
|
24
|
+
end
|
25
|
+
|
26
|
+
module Ethon
|
27
|
+
|
28
|
+
class Multi
|
29
|
+
class Bran
|
30
|
+
def initialize(fm, multi)
|
31
|
+
@fm = fm
|
32
|
+
@multi = multi
|
33
|
+
|
34
|
+
@multi.socketfunction = method(:_socketfunction_callback).to_proc
|
35
|
+
@multi.timerfunction = method(:_timerfunction_callback).to_proc
|
36
|
+
end
|
37
|
+
|
38
|
+
def perform
|
39
|
+
fm = Thread.current.thread_variable_get(:fiber_manager)
|
40
|
+
return unless fm == @fm # abort if not run within the same context.
|
41
|
+
|
42
|
+
@curl_readables = {}
|
43
|
+
@curl_writables = {}
|
44
|
+
@fds_remaining_ptr = ::FFI::MemoryPointer.new(:int)
|
45
|
+
|
46
|
+
raise @callback_error if @callback_error
|
47
|
+
|
48
|
+
@perform_fiber = ::Fiber.current
|
49
|
+
::Fiber.yield
|
50
|
+
@perform_fiber = nil
|
51
|
+
|
52
|
+
raise @callback_error if @callback_error
|
53
|
+
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def _socketfunction_callback(easy_handle, fd, action, *)
|
60
|
+
case action
|
61
|
+
when ::Ethon::Curl::POLL_IN then poll_in(fd)
|
62
|
+
when ::Ethon::Curl::POLL_OUT then poll_out(fd)
|
63
|
+
when ::Ethon::Curl::POLL_INOUT then poll_in(fd); poll_out(fd)
|
64
|
+
when ::Ethon::Curl::POLL_REMOVE then poll_remove(fd)
|
65
|
+
end
|
66
|
+
|
67
|
+
0
|
68
|
+
rescue Exception => e
|
69
|
+
@callback_error = e
|
70
|
+
@fm.resume_soon(@perform_fiber) if @perform_fiber
|
71
|
+
|
72
|
+
0
|
73
|
+
end
|
74
|
+
|
75
|
+
def _timerfunction_callback(_, timeout_ms, *)
|
76
|
+
timer_cancel
|
77
|
+
|
78
|
+
return if timeout_ms.is_a?(::Bignum) # corresponds to -1
|
79
|
+
|
80
|
+
@timeout_timer = @fm.loop.timer_oneshot timeout_ms / 1000.0 do
|
81
|
+
@timeout_timer = nil
|
82
|
+
|
83
|
+
socket_action!(::Ethon::Curl::SOCKET_TIMEOUT, 0) if @perform_fiber
|
84
|
+
end
|
85
|
+
|
86
|
+
0
|
87
|
+
rescue Exception => e
|
88
|
+
@callback_error = e
|
89
|
+
@fm.resume_soon(@perform_fiber) if @perform_fiber
|
90
|
+
|
91
|
+
0
|
92
|
+
end
|
93
|
+
|
94
|
+
def poll_in(fd)
|
95
|
+
return if @curl_readables[fd]
|
96
|
+
@curl_readables[fd] = true
|
97
|
+
|
98
|
+
@fm.loop.push_readable fd,
|
99
|
+
Proc.new { socket_action!(fd, ::Ethon::Curl::CSELECT_IN) }
|
100
|
+
end
|
101
|
+
|
102
|
+
def poll_out(fd)
|
103
|
+
return if @curl_writables[fd]
|
104
|
+
@curl_writables[fd] = true
|
105
|
+
|
106
|
+
@fm.loop.push_writable fd,
|
107
|
+
Proc.new { socket_action!(fd, ::Ethon::Curl::CSELECT_OUT) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def poll_remove(fd)
|
111
|
+
@fm.loop.pop_readable(fd) if @curl_readables.delete(fd)
|
112
|
+
@fm.loop.pop_writable(fd) if @curl_writables.delete(fd)
|
113
|
+
|
114
|
+
maybe_finish!
|
115
|
+
end
|
116
|
+
|
117
|
+
def timer_cancel
|
118
|
+
@fm.loop.timer_cancel @timeout_timer if @timeout_timer
|
119
|
+
end
|
120
|
+
|
121
|
+
def socket_action!(fd, flags)
|
122
|
+
code = ::Ethon::Curl.multi_socket_action(@multi.handle, fd, flags,
|
123
|
+
@fds_remaining_ptr)
|
124
|
+
raise ::Ethon::Errors::MultiTimeout.new(code) unless code == :ok
|
125
|
+
|
126
|
+
if @fds_remaining_ptr.read_int == 0
|
127
|
+
@multi.__send__(:check)
|
128
|
+
maybe_finish!
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def maybe_finish!
|
133
|
+
return unless @curl_readables.empty? && @curl_writables.empty? \
|
134
|
+
&& @multi.easy_handles.empty?
|
135
|
+
timer_cancel
|
136
|
+
|
137
|
+
@perform_fiber.resume
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/bran/fiber_manager.rb
CHANGED
data/lib/bran/libuv/reactor.rb
CHANGED
@@ -30,6 +30,7 @@ module Bran
|
|
30
30
|
|
31
31
|
@poll_read_callback = FFI.uv_poll_cb(&method(:_poll_read_callback))
|
32
32
|
@poll_write_callback = FFI.uv_poll_cb(&method(:_poll_write_callback))
|
33
|
+
@poll_rw_callback = FFI.uv_poll_cb(&method(:_poll_rw_callback))
|
33
34
|
|
34
35
|
# TODO: add more Ruby-compatible signal handlers by default?
|
35
36
|
signal_start(:INT) { @abort_signal = :INT; stop! }
|
@@ -103,54 +104,64 @@ module Bran
|
|
103
104
|
# Push the given handler for the given fd, adding if necessary.
|
104
105
|
# If persistent is false, the handler will be popped after one trigger.
|
105
106
|
def push_readable(fd, handler, persistent = true)
|
106
|
-
ptr
|
107
|
-
fd
|
107
|
+
ptr = ptr()
|
108
|
+
fd = Integer(fd)
|
109
|
+
poll = nil
|
108
110
|
|
109
|
-
# TODO: worry about readable vs writable mixing/overlap
|
110
111
|
if (readables = @on_readables[fd])
|
111
112
|
readables << [handler, persistent]
|
112
|
-
|
113
|
+
elsif (poll = @running_writes[fd])
|
114
|
+
@running_reads[fd] = poll
|
115
|
+
@fds_by_read_addr[poll.address] = fd
|
116
|
+
@on_readables[fd] = [[handler, persistent]]
|
117
|
+
|
118
|
+
Util.error_check "starting the poll readable + writable entry",
|
119
|
+
FFI.uv_poll_start(poll, FFI::UV_READABLE | FFI::UV_WRITABLE, @poll_rw_callback)
|
120
|
+
else
|
121
|
+
poll = @available_polls.pop || FFI.uv_poll_alloc
|
122
|
+
@running_reads[fd] = poll
|
123
|
+
@fds_by_read_addr[poll.address] = fd
|
124
|
+
@on_readables[fd] = [[handler, persistent]]
|
125
|
+
|
126
|
+
Util.error_check "creating the poll readable entry",
|
127
|
+
FFI.uv_poll_init(ptr, poll, fd)
|
128
|
+
|
129
|
+
Util.error_check "starting the poll readable entry",
|
130
|
+
FFI.uv_poll_start(poll, FFI::UV_READABLE, @poll_read_callback)
|
113
131
|
end
|
114
132
|
|
115
|
-
poll = @available_polls.pop || FFI.uv_poll_alloc
|
116
|
-
@running_reads[fd] = poll
|
117
|
-
@fds_by_read_addr[poll.address] = fd
|
118
|
-
@on_readables[fd] = [[handler, persistent]]
|
119
|
-
|
120
|
-
# TODO: investigate if need not init existing available_polls
|
121
|
-
Util.error_check "creating the poll readable entry",
|
122
|
-
FFI.uv_poll_init(ptr, poll, fd)
|
123
|
-
|
124
|
-
Util.error_check "starting the poll readable entry",
|
125
|
-
FFI.uv_poll_start(poll, FFI::UV_READABLE, @poll_read_callback)
|
126
|
-
|
127
133
|
fd
|
128
134
|
end
|
129
135
|
|
130
136
|
# Push the given handler for the given fd, adding if necessary.
|
131
137
|
# If persistent is false, the handler will be popped after one trigger.
|
132
138
|
def push_writable(fd, handler, persistent = true)
|
133
|
-
ptr
|
134
|
-
fd
|
139
|
+
ptr = ptr()
|
140
|
+
fd = Integer(fd)
|
141
|
+
poll = nil
|
135
142
|
|
136
|
-
# TODO: worry about writable vs writable mixing/overlap
|
137
143
|
if (writables = @on_writables[fd])
|
138
144
|
writables << [handler, persistent]
|
139
|
-
|
145
|
+
elsif (poll = @running_reads[fd])
|
146
|
+
@running_writes[fd] = poll
|
147
|
+
@fds_by_write_addr[poll.address] = fd
|
148
|
+
@on_writables[fd] = [[handler, persistent]]
|
149
|
+
|
150
|
+
Util.error_check "starting the poll readable + writable entry",
|
151
|
+
FFI.uv_poll_start(poll, FFI::UV_READABLE | FFI::UV_WRITABLE, @poll_rw_callback)
|
152
|
+
else
|
153
|
+
poll = @available_polls.pop || FFI.uv_poll_alloc
|
154
|
+
@running_writes[fd] = poll
|
155
|
+
@fds_by_write_addr[poll.address] = fd
|
156
|
+
@on_writables[fd] = [[handler, persistent]]
|
157
|
+
|
158
|
+
Util.error_check "creating the poll writeable entry",
|
159
|
+
FFI.uv_poll_init(ptr, poll, fd)
|
160
|
+
|
161
|
+
Util.error_check "starting the poll writeable entry",
|
162
|
+
FFI.uv_poll_start(poll, FFI::UV_WRITABLE, @poll_write_callback)
|
140
163
|
end
|
141
164
|
|
142
|
-
poll = @available_polls.pop || FFI.uv_poll_alloc
|
143
|
-
@running_writes[fd] = poll
|
144
|
-
@fds_by_write_addr[poll.address] = fd
|
145
|
-
@on_writables[fd] = [[handler, persistent]]
|
146
|
-
|
147
|
-
# TODO: investigate if need not init existing available_polls
|
148
|
-
Util.error_check "creating the poll writable entry",
|
149
|
-
FFI.uv_poll_init(ptr, poll, fd)
|
150
|
-
|
151
|
-
Util.error_check "starting the poll writable entry",
|
152
|
-
FFI.uv_poll_start(poll, FFI::UV_WRITABLE, @poll_write_callback)
|
153
|
-
|
154
165
|
fd
|
155
166
|
end
|
156
167
|
|
@@ -160,7 +171,7 @@ module Bran
|
|
160
171
|
|
161
172
|
readables = @on_readables[fd]
|
162
173
|
return unless readables
|
163
|
-
|
174
|
+
|
164
175
|
readables.pop
|
165
176
|
return unless readables.empty?
|
166
177
|
|
@@ -171,6 +182,13 @@ module Bran
|
|
171
182
|
Util.error_check "stopping the poll readable entry",
|
172
183
|
FFI.uv_poll_stop(poll)
|
173
184
|
|
185
|
+
if poll == @running_writes[fd]
|
186
|
+
Util.error_check "restarting the poll writable entry",
|
187
|
+
FFI.uv_poll_start(poll, FFI::UV_WRITABLE, @poll_write_callback)
|
188
|
+
|
189
|
+
return
|
190
|
+
end
|
191
|
+
|
174
192
|
@available_polls << poll
|
175
193
|
|
176
194
|
nil
|
@@ -193,6 +211,13 @@ module Bran
|
|
193
211
|
Util.error_check "stopping the poll writable entry",
|
194
212
|
FFI.uv_poll_stop(poll)
|
195
213
|
|
214
|
+
if poll == @running_reads[fd]
|
215
|
+
Util.error_check "restarting the poll readable entry",
|
216
|
+
FFI.uv_poll_start(poll, FFI::UV_READABLE, @poll_read_callback)
|
217
|
+
|
218
|
+
return
|
219
|
+
end
|
220
|
+
|
196
221
|
@available_polls << poll
|
197
222
|
|
198
223
|
nil
|
@@ -314,7 +339,9 @@ module Bran
|
|
314
339
|
# Callback method called directly from FFI when an event is readable.
|
315
340
|
def _poll_read_callback(poll, rc, events)
|
316
341
|
rescue_abort do
|
317
|
-
fd
|
342
|
+
fd = @fds_by_read_addr[poll.address]
|
343
|
+
return unless fd
|
344
|
+
|
318
345
|
readables = @on_readables.fetch(fd)
|
319
346
|
|
320
347
|
handler, persistent = readables.last
|
@@ -327,7 +354,9 @@ module Bran
|
|
327
354
|
# Callback method called directly from FFI when an event is writable.
|
328
355
|
def _poll_write_callback(poll, rc, events)
|
329
356
|
rescue_abort do
|
330
|
-
fd
|
357
|
+
fd = @fds_by_write_addr[poll.address]
|
358
|
+
return unless fd
|
359
|
+
|
331
360
|
writables = @on_writables.fetch(fd)
|
332
361
|
|
333
362
|
handler, persistent = writables.last
|
@@ -337,6 +366,12 @@ module Bran
|
|
337
366
|
end
|
338
367
|
end
|
339
368
|
|
369
|
+
# Callback method called directly from FFI when an event is readable or writable.
|
370
|
+
def _poll_rw_callback(poll, rc, events)
|
371
|
+
_poll_read_callback(poll, rc, events) if events & FFI::UV_READABLE != 0
|
372
|
+
_poll_write_callback(poll, rc, events) if events & FFI::UV_WRITABLE != 0
|
373
|
+
end
|
374
|
+
|
340
375
|
# Invoke the given handler, possibly converting the given rc to an error.
|
341
376
|
def invoke_handler(handler, rc)
|
342
377
|
case handler
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bran
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe McIlvain
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -125,6 +125,11 @@ files:
|
|
125
125
|
- ext/libuv/Rakefile
|
126
126
|
- lib/bran.rb
|
127
127
|
- lib/bran/ext.rb
|
128
|
+
- lib/bran/ext/ethon.rb
|
129
|
+
- lib/bran/ext/ethon/curl.rb
|
130
|
+
- lib/bran/ext/ethon/easy.rb
|
131
|
+
- lib/bran/ext/ethon/multi.rb
|
132
|
+
- lib/bran/ext/ethon/multi/bran.rb
|
128
133
|
- lib/bran/ext/io.rb
|
129
134
|
- lib/bran/ext/rainbows.rb
|
130
135
|
- lib/bran/ext/rainbows/bran.rb
|