bran 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|