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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 36a615f86cbc15a2b7c138816df60678ca2a0786
4
- data.tar.gz: 48570f692b614243db475256f19265f74993be70
3
+ metadata.gz: 28390755dc30a2dc1027f082f7f5340d88222bb8
4
+ data.tar.gz: 32514e834d6f931cab276f2480d45be6588e3982
5
5
  SHA512:
6
- metadata.gz: c14a66422d3310bc2619afb7c59e883c6c9ec6022b7e9139b7800b2a5aa3ca1114cab48ce76d4bce553ffbcab647de60c5604dfbc8f4f6159649ea3a18c31e6e
7
- data.tar.gz: 2ba3d09c4a6786b300e3c4c96030ed692fdc60a880c539a396efc42ba081eb66c190529e4c9bc217d12fd415399b1bacc1088b9fe5b0261f6f616fcb17ab21e8
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,11 @@
1
+
2
+ require_relative "../../bran"
3
+ require_relative "../../bran/ext"
4
+
5
+ ::Bran::Ext[:ethon] = true
6
+
7
+ require "ethon"
8
+
9
+ require_relative "ethon/curl"
10
+ require_relative "ethon/multi"
11
+ require_relative "ethon/easy"
@@ -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,15 @@
1
+
2
+ Module.new do
3
+ Ethon::Easy.prepend self
4
+
5
+ def perform
6
+ fm = Thread.current.thread_variable_get(:fiber_manager)
7
+ return super unless fm
8
+
9
+ multi = Ethon::Multi.new
10
+ multi.add self
11
+ multi.perform
12
+
13
+ return_code
14
+ end
15
+ 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
@@ -39,5 +39,9 @@ module Bran
39
39
  ::Fiber.yield
40
40
  end
41
41
 
42
+ def resume_soon(fiber)
43
+ @loop.timer_oneshot_wake(0, fiber)
44
+ end
45
+
42
46
  end
43
47
  end
@@ -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 = ptr()
107
- fd = Integer(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
- return
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 = ptr()
134
- fd = Integer(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
- return
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 = @fds_by_read_addr.fetch(poll.address)
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 = @fds_by_write_addr.fetch(poll.address)
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.1
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-03 00:00:00.000000000 Z
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