pitchfork 0.12.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -3
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +12 -0
  5. data/Dockerfile +1 -1
  6. data/Gemfile.lock +3 -5
  7. data/benchmark/README.md +1 -1
  8. data/benchmark/cow_benchmark.rb +1 -0
  9. data/docs/CONFIGURATION.md +39 -1
  10. data/docs/MIGRATING_FROM_UNICORN.md +34 -0
  11. data/docs/WHY_MIGRATE.md +5 -0
  12. data/examples/constant_caches.ru +1 -0
  13. data/examples/echo.ru +1 -0
  14. data/examples/hello.ru +1 -0
  15. data/examples/pitchfork.conf.minimal.rb +1 -0
  16. data/examples/pitchfork.conf.rb +1 -0
  17. data/examples/pitchfork.conf.service.rb +27 -0
  18. data/exe/pitchfork +5 -4
  19. data/ext/pitchfork_http/epollexclusive.h +2 -2
  20. data/ext/pitchfork_http/extconf.rb +3 -0
  21. data/ext/pitchfork_http/memory_page.c +223 -0
  22. data/ext/pitchfork_http/pitchfork_http.c +213 -211
  23. data/ext/pitchfork_http/pitchfork_http.rl +3 -1
  24. data/lib/pitchfork/children.rb +21 -15
  25. data/lib/pitchfork/configurator.rb +13 -0
  26. data/lib/pitchfork/const.rb +1 -0
  27. data/lib/pitchfork/flock.rb +1 -0
  28. data/lib/pitchfork/http_parser.rb +18 -72
  29. data/lib/pitchfork/http_response.rb +4 -3
  30. data/lib/pitchfork/http_server.rb +181 -62
  31. data/lib/pitchfork/launcher.rb +1 -0
  32. data/lib/pitchfork/message.rb +11 -6
  33. data/lib/pitchfork/select_waiter.rb +1 -0
  34. data/lib/pitchfork/shared_memory.rb +16 -14
  35. data/lib/pitchfork/socket_helper.rb +2 -1
  36. data/lib/pitchfork/stream_input.rb +6 -5
  37. data/lib/pitchfork/tee_input.rb +3 -2
  38. data/lib/pitchfork/tmpio.rb +1 -0
  39. data/lib/pitchfork/version.rb +1 -1
  40. data/lib/pitchfork/worker.rb +44 -15
  41. data/lib/pitchfork.rb +1 -20
  42. data/pitchfork.gemspec +0 -1
  43. metadata +7 -18
  44. data/lib/pitchfork/app/old_rails/static.rb +0 -59
@@ -16,6 +16,7 @@
16
16
  #include "child_subreaper.h"
17
17
 
18
18
  void init_pitchfork_httpdate(void);
19
+ void init_pitchfork_memory_page(VALUE);
19
20
 
20
21
  #define UH_FL_CHUNKED 0x1
21
22
  #define UH_FL_HASBODY 0x2
@@ -960,7 +961,7 @@ static VALUE HttpParser_rssget(VALUE self)
960
961
  assert(!NIL_P(var) && "missed global field"); \
961
962
  } while (0)
962
963
 
963
- void Init_pitchfork_http(void)
964
+ RUBY_FUNC_EXPORTED void Init_pitchfork_http(void)
964
965
  {
965
966
  VALUE mPitchfork;
966
967
 
@@ -1024,5 +1025,6 @@ void Init_pitchfork_http(void)
1024
1025
 
1025
1026
  init_epollexclusive(mPitchfork);
1026
1027
  init_child_subreaper(mPitchfork);
1028
+ init_pitchfork_memory_page(mPitchfork);
1027
1029
  }
1028
1030
  #undef SET_GLOBAL
@@ -1,31 +1,35 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  module Pitchfork
4
5
  # This class keep tracks of the state of all the master children.
5
6
  class Children
6
- attr_reader :mold
7
+ attr_reader :mold, :service
7
8
  attr_accessor :last_generation
8
9
 
9
10
  def initialize
10
11
  @last_generation = 0
11
- @children = {} # All children, including molds, indexed by PID.
12
+ @children = {} # All children, including molds and services, indexed by PID.
12
13
  @workers = {} # Workers indexed by their `nr`.
13
14
  @molds = {} # Molds, index by PID.
14
15
  @mold = nil # The latest mold, if any.
16
+ @service = nil
15
17
  @pending_workers = {} # Pending workers indexed by their `nr`.
16
18
  @pending_molds = {} # Worker promoted to mold, not yet acknowledged
17
19
  end
18
20
 
19
- def refresh
20
- @workers.each_value(&:refresh)
21
- @molds.each_value(&:refresh)
22
- end
23
-
24
21
  def register(child)
25
22
  # Children always start as workers, never molds, so we know they have a `#nr`.
23
+ unless child.nr
24
+ raise "[BUG] Trying to register a child without an `nr`: #{child.inspect}"
25
+ end
26
26
  @pending_workers[child.nr] = @workers[child.nr] = child
27
27
  end
28
28
 
29
+ def register_service(service)
30
+ @service = service
31
+ end
32
+
29
33
  def register_mold(mold)
30
34
  @pending_molds[mold.pid] = mold
31
35
  @children[mold.pid] = mold
@@ -43,6 +47,11 @@ module Pitchfork
43
47
  @pending_molds[mold.pid] = mold
44
48
  @children[mold.pid] = mold
45
49
  return mold
50
+ when Message::ServiceSpawned
51
+ service = @service
52
+ service.update(message)
53
+ @children[service.pid] = service
54
+ return service
46
55
  end
47
56
 
48
57
  child = @children[message.pid] || (message.nr && @workers[message.nr])
@@ -77,12 +86,17 @@ module Pitchfork
77
86
  @pending_molds.delete(child.pid)
78
87
  @molds.delete(child.pid)
79
88
  @workers.delete(child.nr)
89
+
80
90
  if @mold == child
81
91
  @pending_workers.reject! do |nr, worker|
82
92
  worker.generation == @mold.generation
83
93
  end
84
94
  @mold = nil
85
95
  end
96
+
97
+ if @service == child
98
+ @service = nil
99
+ end
86
100
  end
87
101
  child
88
102
  end
@@ -153,13 +167,5 @@ module Pitchfork
153
167
  def workers_count
154
168
  @workers.size
155
169
  end
156
-
157
- def total_pss
158
- total_pss = MemInfo.new(Process.pid).pss
159
- @children.each do |_, worker|
160
- total_pss += worker.meminfo.pss if worker.meminfo
161
- end
162
- total_pss
163
- end
164
170
  end
165
171
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
  require 'logger'
3
4
 
4
5
  module Pitchfork
@@ -51,6 +52,8 @@ module Pitchfork
51
52
  "repead unknown process (#{status.inspect})"
52
53
  elsif worker.mold?
53
54
  "mold pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
55
+ elsif worker.service?
56
+ "service pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
54
57
  else
55
58
  "worker=#{worker.nr rescue 'unknown'} pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
56
59
  end
@@ -74,6 +77,8 @@ module Pitchfork
74
77
  :check_client_connection => false,
75
78
  :rewindable_input => true,
76
79
  :client_body_buffer_size => Pitchfork::Const::MAX_BODY,
80
+ :before_service_worker_ready => nil,
81
+ :before_service_worker_exit => nil,
77
82
  }
78
83
  #:startdoc:
79
84
 
@@ -175,6 +180,14 @@ module Pitchfork
175
180
  set_hook(:after_request_complete, block_given? ? block : args[0], 3)
176
181
  end
177
182
 
183
+ def before_service_worker_ready(&block)
184
+ set_hook(:before_service_worker_ready, block, 2)
185
+ end
186
+
187
+ def before_service_worker_exit(&block)
188
+ set_hook(:before_service_worker_exit, block, 2)
189
+ end
190
+
178
191
  def timeout(seconds, cleanup: 2)
179
192
  soft_timeout = set_int(:soft_timeout, seconds, 3)
180
193
  cleanup_timeout = set_int(:cleanup_timeout, cleanup, 2)
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
 
3
4
  module Pitchfork
4
5
  module Const # :nodoc:
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'tempfile'
2
3
 
3
4
  module Pitchfork
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
  # :enddoc:
3
4
  # no stable API here
4
5
 
@@ -19,14 +20,13 @@ module Pitchfork
19
20
  "SERVER_SOFTWARE" => "Pitchfork #{Pitchfork::Const::UNICORN_VERSION}"
20
21
  }
21
22
 
22
- NULL_IO = StringIO.new("")
23
+ NULL_IO = StringIO.new.binmode
23
24
 
24
25
  # :stopdoc:
25
- HTTP_RESPONSE_START = [ 'HTTP'.freeze, '/1.1 '.freeze ]
26
+ HTTP_RESPONSE_START = [ 'HTTP', '/1.1 ' ]
26
27
  EMPTY_ARRAY = [].freeze
27
28
  @@input_class = Pitchfork::TeeInput
28
29
  @@check_client_connection = false
29
- @@tcpi_inspect_ok = Socket.const_defined?(:TCP_INFO)
30
30
 
31
31
  def self.input_class
32
32
  @@input_class
@@ -104,83 +104,29 @@ module Pitchfork
104
104
  end
105
105
 
106
106
  def hijacked?
107
- env.include?('rack.hijack_io'.freeze)
107
+ env.include?('rack.hijack_io')
108
108
  end
109
109
 
110
- if Raindrops.const_defined?(:TCP_Info)
111
- TCPI = Raindrops::TCP_Info.allocate
112
-
110
+ if Socket.const_defined?(:TCP_INFO) # Linux
113
111
  def check_client_connection(socket) # :nodoc:
114
112
  if TCPSocket === socket
115
- # Raindrops::TCP_Info#get!, #state (reads struct tcp_info#tcpi_state)
116
- raise Errno::EPIPE, "client closed connection".freeze,
117
- EMPTY_ARRAY if closed_state?(TCPI.get!(socket).state)
118
- else
119
- write_http_header(socket)
120
- end
121
- end
122
-
123
- if Raindrops.const_defined?(:TCP)
124
- # raindrops 0.18.0+ supports FreeBSD + Linux using the same names
125
- # Evaluate these hash lookups at load time so we can
126
- # generate an opt_case_dispatch instruction
127
- eval <<-EOS
128
- def closed_state?(state) # :nodoc:
129
- case state
130
- when #{Raindrops::TCP[:ESTABLISHED]}
131
- false
132
- when #{Raindrops::TCP.values_at(
133
- :CLOSE_WAIT, :TIME_WAIT, :CLOSE, :LAST_ACK, :CLOSING).join(',')}
134
- true
135
- else
136
- false
137
- end
138
- end
139
- EOS
140
- else
141
- # raindrops before 0.18 only supported TCP_INFO under Linux
142
- def closed_state?(state) # :nodoc:
143
- case state
144
- when 1 # ESTABLISHED
145
- false
146
- when 8, 6, 7, 9, 11 # CLOSE_WAIT, TIME_WAIT, CLOSE, LAST_ACK, CLOSING
147
- true
148
- else
149
- false
113
+ begin
114
+ tcp_info = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
115
+ rescue IOError, SystemCallError
116
+ return write_http_header(socket)
150
117
  end
151
- end
152
- end
153
- else
154
118
 
155
- # Ruby 2.2+ can show struct tcp_info as a string Socket::Option#inspect.
156
- # Not that efficient, but probably still better than doing unnecessary
157
- # work after a client gives up.
158
- def check_client_connection(socket) # :nodoc:
159
- if TCPSocket === socket && @@tcpi_inspect_ok
160
- opt = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).inspect
161
- if opt =~ /\bstate=(\S+)/
162
- raise Errno::EPIPE, "client closed connection".freeze,
163
- EMPTY_ARRAY if closed_state_str?($1)
164
- else
165
- @@tcpi_inspect_ok = false
166
- write_http_header(socket)
119
+ case tcp_info.data.unpack1("C")
120
+ when 6, 7, 8, 9, 11 # TIME_WAIT, CLOSE, CLOSE_WAIT, LAST_ACK, CLOSING
121
+ raise Errno::EPIPE, "client closed connection", EMPTY_ARRAY
167
122
  end
168
- opt.clear
169
123
  else
170
124
  write_http_header(socket)
171
125
  end
172
126
  end
173
-
174
- def closed_state_str?(state)
175
- case state
176
- when 'ESTABLISHED'
177
- false
178
- # not a typo, ruby maps TCP_CLOSE (no 'D') to state=CLOSED (w/ 'D')
179
- when 'CLOSE_WAIT', 'TIME_WAIT', 'CLOSED', 'LAST_ACK', 'CLOSING'
180
- true
181
- else
182
- false
183
- end
127
+ else
128
+ def check_client_connection(socket) # :nodoc:
129
+ write_http_header(socket)
184
130
  end
185
131
  end
186
132
 
@@ -199,11 +145,11 @@ module Pitchfork
199
145
  val.downcase!
200
146
  end
201
147
 
202
- if vals.pop == 'chunked'.freeze
203
- return true unless vals.include?('chunked'.freeze)
148
+ if vals.pop == 'chunked'
149
+ return true unless vals.include?('chunked')
204
150
  raise Pitchfork::HttpParserError, 'double chunked', []
205
151
  end
206
- return false unless vals.include?('chunked'.freeze)
152
+ return false unless vals.include?('chunked')
207
153
  raise Pitchfork::HttpParserError, 'chunked not last', []
208
154
  end
209
155
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
  # :enddoc:
3
4
 
4
5
  module Pitchfork
@@ -48,10 +49,10 @@ module Pitchfork
48
49
  if headers
49
50
  code = status.to_i
50
51
  msg = STATUS_CODES[code]
51
- start = req.response_start_sent ? ''.freeze : 'HTTP/1.1 '.freeze
52
+ start = req.response_start_sent ? '' : 'HTTP/1.1 '
52
53
  buf = "#{start}#{msg ? %Q(#{code} #{msg}) : status}\r\n" \
53
54
  "Date: #{httpdate}\r\n" \
54
- "Connection: close\r\n"
55
+ "Connection: close\r\n".b
55
56
  headers.each do |key, value|
56
57
  case key
57
58
  when %r{\A(?:Date|Connection)\z}i
@@ -64,7 +65,7 @@ module Pitchfork
64
65
  append_header(buf, key, value)
65
66
  end
66
67
  end
67
- socket.write(buf << "\r\n".freeze)
68
+ socket.write(buf << "\r\n")
68
69
  end
69
70
 
70
71
  if hijack