pitchfork 0.12.0 → 0.14.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.
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