yahns 1.8.0 → 1.9.0

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: 0bea696fff5f5d58044ab6f62df4b89d0b03a4d5
4
- data.tar.gz: 7936ab7581a763fac0e466a98f5d88e8ff1e8a1a
3
+ metadata.gz: 49368448f90a245e5b9e66d4be300840d6cecfa9
4
+ data.tar.gz: 2b97d5ff7b0ca846366ea32397ae99bd27eb4cf2
5
5
  SHA512:
6
- metadata.gz: a8ead2093e262430942ffbd4fc438d1160903b0fbb5c61f32f2339022d46488d9e7ad370714494f2232e09a1759e3c2d4b8216e1dad68f5b0abcf82cccf3abf1
7
- data.tar.gz: cd8b7d89b7341c7aa1da9681ccadff2d9d2a828107d8b1ca37354daffeee8b46039b752babd326ef5e51ecb1b97720e3a75d2c7ea55e687758b17aa87e3ee6b8
6
+ metadata.gz: 7da4504be2239c1c8387a51a88e3d5a025a7b592156e2856b190a1ce196dd24c5a726597ad13e71ba4d17fc7473140e8bdc66ae70522391cb5e1ae77dc894782
7
+ data.tar.gz: f12f41d4b2229a86cdf243706ab71c4668da13e483b462f1845d68ac67cadadd05b6e835c45a5c8d0cedba538f1ec80dbb18ab199b587182afe6725e5d8c949e
data/GIT-VERSION-FILE CHANGED
@@ -1 +1 @@
1
- VERSION = 1.8.0
1
+ VERSION = 1.9.0
data/GIT-VERSION-GEN CHANGED
@@ -4,7 +4,7 @@
4
4
  CONSTANT = "Yahns::VERSION"
5
5
  RVF = "lib/yahns/version.rb"
6
6
  GVF = "GIT-VERSION-FILE"
7
- DEF_VER = "v1.8.0"
7
+ DEF_VER = "v1.9.0"
8
8
  vn = DEF_VER
9
9
 
10
10
  # First see if there is a version file (included in release tarballs),
data/NEWS CHANGED
@@ -1,3 +1,37 @@
1
+ yahns 1.9.0 - minor updates / 2015-07-21
2
+ ----------------------------------------
3
+
4
+ This release improves socket inheritance support. TCP socket
5
+ options are now applied to inherited sockets. We also emulate
6
+ the sd_listen_fds function to allow inheriting sockets from
7
+ systemd.
8
+
9
+ HTTP status strings are now generated dynamically, allowing
10
+ applications to modify Rack::Utils::HTTP_STATUS_CODES to
11
+ apply changes in the Rack response. Unfortunately, this leads
12
+ to minor (likely unnoticeable) performance regressions.
13
+
14
+ However, our code is not optimized for Ruby 2.2+, so users on
15
+ the latest released Ruby will benefit from reduced inline cache
16
+ and constant lookups as we reduced our constant footprint.
17
+ Expect further minor performance regressions if you are running
18
+ Ruby 2.2 and earlier.
19
+
20
+ For Ruby 2.2 users, overall performance should be largely
21
+ unchanged from 1.7.0 to 1.8.0
22
+
23
+ shortlog of changes since 1.7.0:
24
+
25
+ * use opt_str_freeze for Hash#delete
26
+ * test/helper: warn atomically
27
+ * generate response status strings dynamically
28
+ * reduce constants and optimize for Ruby 2.2+
29
+ * http_response: reduce bytecode size
30
+ * apply TCP socket options on inherited sockets
31
+ * test/test_rack_hijack.rb: try to increase test reliability
32
+ * emulate sd_listen_fds for systemd support
33
+ * test/test_rack_hijack: ensure proper ordering of log messages
34
+
1
35
  yahns 1.8.0 - minor updates / 2015-06-11
2
36
  ----------------------------------------
3
37
 
@@ -11,12 +11,6 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
11
11
  include Yahns::HttpResponse
12
12
  QEV_FLAGS = Yahns::Queue::QEV_RD # used by acceptor
13
13
 
14
- # A frozen format for this is about 15% faster (note from Mongrel)
15
- REMOTE_ADDR = 'REMOTE_ADDR'.freeze
16
- RACK_INPUT = 'rack.input'.freeze
17
- RACK_HIJACK = 'rack.hijack'.freeze
18
- RACK_HIJACK_IO = "rack.hijack_io".freeze
19
-
20
14
  # called from acceptor thread
21
15
  def yahns_init
22
16
  @hs = Unicorn::HttpRequest.new
@@ -206,9 +200,9 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
206
200
  # input is nil if we needed to wait for writability with
207
201
  # check_client_connection
208
202
  if input
209
- env[REMOTE_ADDR] = @kgio_addr
210
- env[RACK_HIJACK] = self
211
- env[RACK_INPUT] = input
203
+ env['REMOTE_ADDR'] = @kgio_addr
204
+ env['rack.hijack'] = self
205
+ env['rack.input'] = input
212
206
 
213
207
  if k.check_client_connection && @hs.headers?
214
208
  rv = do_ccc and return rv
@@ -262,7 +256,7 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
262
256
  # this is the env["rack.hijack"] callback exposed to the Rack app
263
257
  def call
264
258
  hijack_cleanup
265
- @hs.env[RACK_HIJACK_IO] = self
259
+ @hs.env['rack.hijack_io'] = self
266
260
  end
267
261
 
268
262
  def response_hijacked(fn)
@@ -300,7 +294,7 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
300
294
  end
301
295
 
302
296
  def app_hijacked?(env, body)
303
- return false unless env.include?(RACK_HIJACK_IO)
297
+ return false unless env.include?('rack.hijack_io'.freeze)
304
298
  body.close if body.respond_to?(:close)
305
299
  true
306
300
  end
@@ -22,14 +22,8 @@ module Yahns::HttpResponse # :nodoc:
22
22
  end
23
23
  end
24
24
 
25
- # avoid GC overhead for frequently used-strings:
26
- CONN_KA = "Connection: keep-alive\r\n\r\n"
27
- CONN_CLOSE = "Connection: close\r\n\r\n"
28
- Z = ""
25
+ # avoid GC overhead for frequently used-strings/objects:
29
26
  CCC_RESPONSE_START = [ 'HTTP', '/1.1 ' ]
30
- RESPONSE_START = CCC_RESPONSE_START.join
31
- REQUEST_METHOD = "REQUEST_METHOD"
32
- HEAD = "HEAD"
33
27
 
34
28
  # no point in using one without the other, these have been in Linux
35
29
  # for ages
@@ -46,7 +40,7 @@ module Yahns::HttpResponse # :nodoc:
46
40
  end
47
41
 
48
42
  def response_start
49
- @hs.response_start_sent ? Z : RESPONSE_START
43
+ @hs.response_start_sent ? ''.freeze : 'HTTP/1.1 '.freeze
50
44
  end
51
45
 
52
46
  def response_wait_write(rv)
@@ -59,7 +53,7 @@ module Yahns::HttpResponse # :nodoc:
59
53
  end
60
54
 
61
55
  def err_response(code)
62
- "#{response_start}#{CODES[code]}\r\n\r\n"
56
+ "#{response_start}#{code} #{Rack::Utils::HTTP_STATUS_CODES[code]}\r\n\r\n"
63
57
  end
64
58
 
65
59
  def response_header_blocked(ret, header, body, alive, offset, count)
@@ -114,23 +108,22 @@ module Yahns::HttpResponse # :nodoc:
114
108
  end
115
109
  end
116
110
 
117
- def kv_str(key, value)
118
- if value =~ /\n/
111
+ def kv_str(buf, key, value)
112
+ if value.include?("\n".freeze)
119
113
  # avoiding blank, key-only cookies with /\n+/
120
- value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
114
+ value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" }
121
115
  else
122
- "#{key}: #{value}\r\n"
116
+ buf << "#{key}: #{value}\r\n"
123
117
  end
124
118
  end
125
119
 
126
120
  def have_more?(value)
127
- value.to_i > 0 && @hs.env[REQUEST_METHOD] != HEAD
121
+ value.to_i > 0 && @hs.env['REQUEST_METHOD'] != 'HEAD'.freeze
128
122
  end
129
123
 
130
124
  # writes the rack_response to socket as an HTTP response
131
125
  # returns :wait_readable, :wait_writable, :forget, or nil
132
126
  def http_response_write(status, headers, body)
133
- status = CODES[status.to_i] || status
134
127
  offset = 0
135
128
  count = hijack = nil
136
129
  k = self.class
@@ -138,7 +131,10 @@ module Yahns::HttpResponse # :nodoc:
138
131
  flags = MSG_DONTWAIT
139
132
 
140
133
  if @hs.headers?
141
- buf = "#{response_start}#{status}\r\nDate: #{httpdate}\r\n"
134
+ code = status.to_i
135
+ msg = Rack::Utils::HTTP_STATUS_CODES[code]
136
+ buf = "#{response_start}#{msg ? %Q(#{code} #{msg}) : status}\r\n" \
137
+ "Date: #{httpdate}\r\n"
142
138
  headers.each do |key, value|
143
139
  case key
144
140
  when %r{\ADate\z}i
@@ -148,20 +144,21 @@ module Yahns::HttpResponse # :nodoc:
148
144
  offset = $1.to_i
149
145
  count = $2.to_i - offset + 1
150
146
  end
151
- buf << kv_str(key, value)
147
+ kv_str(buf, key, value)
152
148
  when %r{\AConnection\z}i
153
149
  # allow Rack apps to tell us they want to drop the client
154
150
  alive = false if value =~ /\bclose\b/i
155
151
  when %r{\AContent-Length\z}i
156
152
  flags |= MSG_MORE if have_more?(value)
157
- buf << kv_str(key, value)
153
+ kv_str(buf, key, value)
158
154
  when "rack.hijack"
159
155
  hijack = value
160
156
  else
161
- buf << kv_str(key, value)
157
+ kv_str(buf, key, value)
162
158
  end
163
159
  end
164
- buf << (alive ? CONN_KA : CONN_CLOSE)
160
+ buf << (alive ? "Connection: keep-alive\r\n\r\n".freeze
161
+ : "Connection: close\r\n\r\n".freeze)
165
162
  case rv = kgio_syssend(buf, flags)
166
163
  when nil # all done, likely
167
164
  break
@@ -255,7 +252,7 @@ module Yahns::HttpResponse # :nodoc:
255
252
  # returns nil on success
256
253
  # returns :close, :wait_writable, or :wait_readable
257
254
  def http_100_response(env)
258
- env.delete("HTTP_EXPECT") =~ /\A100-continue\z/i or return
255
+ env.delete('HTTP_EXPECT'.freeze) =~ /\A100-continue\z/i or return
259
256
  buf = @hs.response_start_sent ? "100 Continue\r\n\r\nHTTP/1.1 ".freeze
260
257
  : "HTTP/1.1 100 Continue\r\n\r\n".freeze
261
258
 
@@ -28,17 +28,13 @@ class Yahns::MaxBody # :nodoc:
28
28
  @limit = limit
29
29
  end
30
30
 
31
- RACK_INPUT = "rack.input".freeze # :nodoc:
32
- CONTENT_LENGTH = "CONTENT_LENGTH" # :nodoc:
33
- HTTP_TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING" # :nodoc:
34
-
35
31
  # our main Rack middleware endpoint
36
32
  def call(env) # :nodoc:
37
33
  catch(:yahns_EFBIG) do
38
- len = env[CONTENT_LENGTH]
34
+ len = env['CONTENT_LENGTH']
39
35
  if len && len.to_i > @limit
40
36
  return err
41
- elsif /\Achunked\z/i =~ env[HTTP_TRANSFER_ENCODING]
37
+ elsif /\Achunked\z/i =~ env['HTTP_TRANSFER_ENCODING']
42
38
  limit_input!(env)
43
39
  end
44
40
  @app.call(env)
@@ -51,9 +47,9 @@ class Yahns::MaxBody # :nodoc:
51
47
  end
52
48
 
53
49
  def limit_input!(env) # :nodoc:
54
- input = env[RACK_INPUT]
50
+ input = env['rack.input']
55
51
  klass = input.respond_to?(:rewind) ? RewindableWrapper : Wrapper
56
- env[RACK_INPUT] = klass.new(input, @limit)
52
+ env['rack.input'] = klass.new(input, @limit)
57
53
  end
58
54
  end
59
55
  require_relative 'max_body/wrapper'
@@ -38,7 +38,8 @@ module Yahns::HttpResponse # :nodoc:
38
38
  end
39
39
  # try to write something, but don't care if we fail
40
40
  Integer === code and
41
- kgio_trywrite("HTTP/1.1 #{CODES[code]}\r\n\r\n") rescue nil
41
+ kgio_trywrite("HTTP/1.1 #{code} #{
42
+ Rack::Utils::HTTP_STATUS_CODES[code]}\r\n\r\n") rescue nil
42
43
 
43
44
  shutdown rescue nil
44
45
  req_res.shutdown rescue nil
@@ -59,16 +60,16 @@ module Yahns::HttpResponse # :nodoc:
59
60
  # returns nil if completely done
60
61
  def proxy_response_start(res, tip, kcar, req_res)
61
62
  status, headers = res
62
- si = status.to_i
63
- status = CODES[si] || status
63
+ code = status.to_i
64
+ msg = Rack::Utils::HTTP_STATUS_CODES[code]
64
65
  env = @hs.env
65
- have_body = !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(si) &&
66
- env[REQUEST_METHOD] != HEAD
66
+ have_body = !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code) &&
67
+ env['REQUEST_METHOD'] != 'HEAD'.freeze
67
68
  flags = MSG_DONTWAIT
68
69
  alive = @hs.next? && self.class.persistent_connections
69
70
  response_headers = env['yahns.proxy_pass.response_headers']
70
71
 
71
- res = "HTTP/1.1 #{status}\r\n"
72
+ res = "HTTP/1.1 #{msg ? %Q(#{code} #{msg}) : status}\r\n"
72
73
  headers.each do |key,value| # n.b.: headers is an Array of 2-element Arrays
73
74
  case key
74
75
  when /\A(?:Connection|Keep-Alive)\z/i
@@ -90,7 +91,8 @@ module Yahns::HttpResponse # :nodoc:
90
91
 
91
92
  # For now, do not add a Date: header, assume upstream already did it
92
93
  # but do not care if they did not
93
- res << (alive ? CONN_KA : CONN_CLOSE)
94
+ res << (alive ? "Connection: keep-alive\r\n\r\n"
95
+ : "Connection: close\r\n\r\n")
94
96
 
95
97
  # send the headers
96
98
  case rv = kgio_syssend(res, flags)
data/lib/yahns/server.rb CHANGED
@@ -312,12 +312,23 @@ class Yahns::Server # :nodoc:
312
312
  # because that can completely break the non-blocking one.
313
313
  # Unfortunately, there is no one-off MSG_DONTWAIT-like flag for
314
314
  # accept4(2).
315
- inherited = ENV['YAHNS_FD'].to_s.split(',').map! do |fd|
316
- io = Socket.for_fd(fd.to_i)
315
+ inherited = ENV['YAHNS_FD'].to_s.split(',')
316
+
317
+ # emulate sd_listen_fds() for systemd
318
+ sd_pid, sd_fds = ENV.values_at('LISTEN_PID', 'LISTEN_FDS')
319
+ if sd_pid && sd_pid.to_i == $$
320
+ # 3 = SD_LISTEN_FDS_START
321
+ inherited.concat((3...(3 + sd_fds.to_i)).map { |fd| Socket.for_fd(fd) })
322
+ end
323
+ # to ease debugging, we will not unset LISTEN_PID and LISTEN_FDS
324
+
325
+ inherited.map! do |fd|
326
+ io = String === fd ? Socket.for_fd(fd.to_i) : fd
317
327
  opts = sock_opts(io)
328
+ io = server_cast(io, opts)
318
329
  set_server_sockopt(io, opts)
319
- @logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
320
- server_cast(io, opts)
330
+ @logger.info "inherited addr=#{sock_name(io)} fd=#{io.fileno}"
331
+ io
321
332
  end
322
333
 
323
334
  @listeners.replace(inherited)
data/lib/yahns/version.rb CHANGED
@@ -1 +1 @@
1
- Yahns::VERSION = '1.8.0' # :nodoc:
1
+ Yahns::VERSION = '1.9.0' # :nodoc:
data/test/helper.rb CHANGED
@@ -140,7 +140,7 @@ class DieIfUsed
140
140
  end
141
141
 
142
142
  def close
143
- warn "INFO #$$ closed DieIfUsed #{@@n += 1}"
143
+ warn "INFO #$$ closed DieIfUsed #{@@n += 1}\n"
144
144
  end
145
145
  end
146
146
 
data/test/test_bin.rb CHANGED
@@ -11,6 +11,40 @@ class TestBin < Testcase
11
11
  @cmd = %W(ruby -I lib bin/yahns)
12
12
  end
13
13
 
14
+ def test_listen_fd3
15
+ return unless RUBY_VERSION.to_f > 2.3 # Fixed in ruby/trunk r51209, actually
16
+ @srv.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 0)
17
+ host, port = @srv.addr[3], @srv.addr[1]
18
+
19
+ ru = tmpfile(%w(test_bin_daemon .ru))
20
+ ru.write("require 'rack/lobster'; run Rack::Lobster.new\n")
21
+ cmd = %W(ruby -I lib bin/yahns-rackup
22
+ -E none -p #{port} -o #{host} #{ru.path})
23
+ pid = fork do # emulate a systemd environment
24
+ env = {
25
+ 'LISTEN_PID' => $$.to_s,
26
+ 'LISTEN_FDS' => '1',
27
+ }
28
+ exec env, *cmd, 3 => @srv, err: @err.path
29
+ end
30
+ Net::HTTP.start(host, port) do |http|
31
+ req = Net::HTTP::Get.new("/")
32
+ res = http.request(req)
33
+ assert_equal 200, res.code.to_i
34
+ assert_equal "keep-alive", res["Connection"]
35
+ end
36
+
37
+ assert_equal 1, @srv.getsockopt(:SOL_SOCKET, :SO_KEEPALIVE).int,
38
+ 'ensure the inheriting process applies TCP socket options'
39
+ ensure
40
+ if pid
41
+ Process.kill(:QUIT, pid)
42
+ _, status = Process.waitpid2(pid)
43
+ assert status.success?, status.inspect
44
+ end
45
+ ru.close! if ru
46
+ end
47
+
14
48
  def test_bin_daemon_noworker_inherit
15
49
  bin_daemon(false, true)
16
50
  end
@@ -52,16 +52,28 @@ class TestRackHijack < Testcase
52
52
  end
53
53
  pid = mkserver(cfg)
54
54
  res = Net::HTTP.start(host, port) { |h| h.get("/hijack_req") }
55
+
56
+ wait_for_msg = lambda do |n|
57
+ tries = 10000
58
+ begin
59
+ Thread.new { Thread.pass }.join # calls sched_yield() on MRI
60
+ end until File.readlines(err.path).grep(/DieIfUsed/).size >= n ||
61
+ (tries -= 1) < 0
62
+ end
55
63
  assert_equal "request.hijacked", res.body
56
64
  assert_equal 200, res.code.to_i
57
65
  assert_equal "1.0", res.http_version
58
66
 
67
+ wait_for_msg.call(1)
68
+
59
69
  res = Net::HTTP.start(host, port) { |h| h.get("/hijack_res") }
60
70
  assert_equal "response.hijacked", res.body
61
71
  assert_equal 200, res.code.to_i
62
72
  assert_equal "zzz", res["X-Test"]
63
73
  assert_equal "1.1", res.http_version
64
74
 
75
+ wait_for_msg.call(2)
76
+
65
77
  errs = File.readlines(err.path).grep(/DieIfUsed/)
66
78
  assert_equal([ "INFO #{pid} closed DieIfUsed 1\n",
67
79
  "INFO #{pid} closed DieIfUsed 2\n" ], errs)
data/test/test_server.rb CHANGED
@@ -845,4 +845,24 @@ class TestServer < Testcase
845
845
  ensure
846
846
  quit_wait(pid)
847
847
  end
848
+
849
+ def test_inherit_tcp_nodelay_set
850
+ err = @err
851
+ cfg = Yahns::Config.new
852
+ host, port = @srv.addr[3], @srv.addr[1]
853
+ @srv.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 0)
854
+ assert_equal 0, @srv.getsockopt(:IPPROTO_TCP, :TCP_NODELAY).int
855
+ cfg.instance_eval do
856
+ ru = lambda { |_| [ 200, { 'Content-Length' => '2' } , [ 'HI' ] ] }
857
+ GTL.synchronize { app(:rack, ru) { listen "#{host}:#{port}" } }
858
+ logger(Logger.new(err.path))
859
+ end
860
+ pid = mkserver(cfg, @srv) { ENV["YAHNS_FD"] = "#{@srv.fileno}" }
861
+ run_client(host, port) { |res| assert_equal "HI", res.body }
862
+
863
+ # TCP socket option is shared at file level, not FD level:
864
+ assert_equal 1, @srv.getsockopt(:IPPROTO_TCP, :TCP_NODELAY).int
865
+ ensure
866
+ quit_wait(pid)
867
+ end
848
868
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yahns
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - yahns hackers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-11 00:00:00.000000000 Z
11
+ date: 2015-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kgio
@@ -220,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
220
  version: '0'
221
221
  requirements: []
222
222
  rubyforge_project:
223
- rubygems_version: 2.4.5
223
+ rubygems_version: 2.5.0
224
224
  signing_key:
225
225
  specification_version: 4
226
226
  summary: sleepy, multi-threaded, non-blocking application server