pitchfork 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -2
- data/.ruby-version +1 -0
- data/CHANGELOG.md +6 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +2 -4
- data/docs/CONFIGURATION.md +38 -0
- data/docs/MIGRATING_FROM_UNICORN.md +34 -0
- data/docs/WHY_MIGRATE.md +5 -0
- data/examples/pitchfork.conf.service.rb +27 -0
- data/exe/pitchfork +4 -4
- data/ext/pitchfork_http/extconf.rb +2 -0
- data/ext/pitchfork_http/memory_page.c +223 -0
- data/ext/pitchfork_http/pitchfork_http.c +213 -211
- data/ext/pitchfork_http/pitchfork_http.rl +3 -1
- data/lib/pitchfork/children.rb +20 -15
- data/lib/pitchfork/configurator.rb +12 -0
- data/lib/pitchfork/http_parser.rb +11 -66
- data/lib/pitchfork/http_response.rb +1 -1
- data/lib/pitchfork/http_server.rb +175 -59
- data/lib/pitchfork/message.rb +10 -6
- data/lib/pitchfork/shared_memory.rb +16 -14
- data/lib/pitchfork/socket_helper.rb +1 -1
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +43 -15
- data/lib/pitchfork.rb +0 -20
- data/pitchfork.gemspec +0 -1
- metadata +7 -18
- data/lib/pitchfork/app/old_rails/static.rb +0 -60
@@ -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
|
data/lib/pitchfork/children.rb
CHANGED
@@ -4,29 +4,32 @@
|
|
4
4
|
module Pitchfork
|
5
5
|
# This class keep tracks of the state of all the master children.
|
6
6
|
class Children
|
7
|
-
attr_reader :mold
|
7
|
+
attr_reader :mold, :service
|
8
8
|
attr_accessor :last_generation
|
9
9
|
|
10
10
|
def initialize
|
11
11
|
@last_generation = 0
|
12
|
-
@children = {} # All children, including molds, indexed by PID.
|
12
|
+
@children = {} # All children, including molds and services, indexed by PID.
|
13
13
|
@workers = {} # Workers indexed by their `nr`.
|
14
14
|
@molds = {} # Molds, index by PID.
|
15
15
|
@mold = nil # The latest mold, if any.
|
16
|
+
@service = nil
|
16
17
|
@pending_workers = {} # Pending workers indexed by their `nr`.
|
17
18
|
@pending_molds = {} # Worker promoted to mold, not yet acknowledged
|
18
19
|
end
|
19
20
|
|
20
|
-
def refresh
|
21
|
-
@workers.each_value(&:refresh)
|
22
|
-
@molds.each_value(&:refresh)
|
23
|
-
end
|
24
|
-
|
25
21
|
def register(child)
|
26
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
|
27
26
|
@pending_workers[child.nr] = @workers[child.nr] = child
|
28
27
|
end
|
29
28
|
|
29
|
+
def register_service(service)
|
30
|
+
@service = service
|
31
|
+
end
|
32
|
+
|
30
33
|
def register_mold(mold)
|
31
34
|
@pending_molds[mold.pid] = mold
|
32
35
|
@children[mold.pid] = mold
|
@@ -44,6 +47,11 @@ module Pitchfork
|
|
44
47
|
@pending_molds[mold.pid] = mold
|
45
48
|
@children[mold.pid] = mold
|
46
49
|
return mold
|
50
|
+
when Message::ServiceSpawned
|
51
|
+
service = @service
|
52
|
+
service.update(message)
|
53
|
+
@children[service.pid] = service
|
54
|
+
return service
|
47
55
|
end
|
48
56
|
|
49
57
|
child = @children[message.pid] || (message.nr && @workers[message.nr])
|
@@ -78,12 +86,17 @@ module Pitchfork
|
|
78
86
|
@pending_molds.delete(child.pid)
|
79
87
|
@molds.delete(child.pid)
|
80
88
|
@workers.delete(child.nr)
|
89
|
+
|
81
90
|
if @mold == child
|
82
91
|
@pending_workers.reject! do |nr, worker|
|
83
92
|
worker.generation == @mold.generation
|
84
93
|
end
|
85
94
|
@mold = nil
|
86
95
|
end
|
96
|
+
|
97
|
+
if @service == child
|
98
|
+
@service = nil
|
99
|
+
end
|
87
100
|
end
|
88
101
|
child
|
89
102
|
end
|
@@ -154,13 +167,5 @@ module Pitchfork
|
|
154
167
|
def workers_count
|
155
168
|
@workers.size
|
156
169
|
end
|
157
|
-
|
158
|
-
def total_pss
|
159
|
-
total_pss = MemInfo.new(Process.pid).pss
|
160
|
-
@children.each do |_, worker|
|
161
|
-
total_pss += worker.meminfo.pss if worker.meminfo
|
162
|
-
end
|
163
|
-
total_pss
|
164
|
-
end
|
165
170
|
end
|
166
171
|
end
|
@@ -52,6 +52,8 @@ module Pitchfork
|
|
52
52
|
"repead unknown process (#{status.inspect})"
|
53
53
|
elsif worker.mold?
|
54
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})"
|
55
57
|
else
|
56
58
|
"worker=#{worker.nr rescue 'unknown'} pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
|
57
59
|
end
|
@@ -75,6 +77,8 @@ module Pitchfork
|
|
75
77
|
:check_client_connection => false,
|
76
78
|
:rewindable_input => true,
|
77
79
|
:client_body_buffer_size => Pitchfork::Const::MAX_BODY,
|
80
|
+
:before_service_worker_ready => nil,
|
81
|
+
:before_service_worker_exit => nil,
|
78
82
|
}
|
79
83
|
#:startdoc:
|
80
84
|
|
@@ -176,6 +180,14 @@ module Pitchfork
|
|
176
180
|
set_hook(:after_request_complete, block_given? ? block : args[0], 3)
|
177
181
|
end
|
178
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
|
+
|
179
191
|
def timeout(seconds, cleanup: 2)
|
180
192
|
soft_timeout = set_int(:soft_timeout, seconds, 3)
|
181
193
|
cleanup_timeout = set_int(:cleanup_timeout, cleanup, 2)
|
@@ -27,7 +27,6 @@ module Pitchfork
|
|
27
27
|
EMPTY_ARRAY = [].freeze
|
28
28
|
@@input_class = Pitchfork::TeeInput
|
29
29
|
@@check_client_connection = false
|
30
|
-
@@tcpi_inspect_ok = Socket.const_defined?(:TCP_INFO)
|
31
30
|
|
32
31
|
def self.input_class
|
33
32
|
@@input_class
|
@@ -108,80 +107,26 @@ module Pitchfork
|
|
108
107
|
env.include?('rack.hijack_io')
|
109
108
|
end
|
110
109
|
|
111
|
-
if
|
112
|
-
TCPI = Raindrops::TCP_Info.allocate
|
113
|
-
|
110
|
+
if Socket.const_defined?(:TCP_INFO) # Linux
|
114
111
|
def check_client_connection(socket) # :nodoc:
|
115
112
|
if TCPSocket === socket
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
write_http_header(socket)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
if Raindrops.const_defined?(:TCP)
|
125
|
-
# raindrops 0.18.0+ supports FreeBSD + Linux using the same names
|
126
|
-
# Evaluate these hash lookups at load time so we can
|
127
|
-
# generate an opt_case_dispatch instruction
|
128
|
-
eval <<-EOS
|
129
|
-
def closed_state?(state) # :nodoc:
|
130
|
-
case state
|
131
|
-
when #{Raindrops::TCP[:ESTABLISHED]}
|
132
|
-
false
|
133
|
-
when #{Raindrops::TCP.values_at(
|
134
|
-
:CLOSE_WAIT, :TIME_WAIT, :CLOSE, :LAST_ACK, :CLOSING).join(',')}
|
135
|
-
true
|
136
|
-
else
|
137
|
-
false
|
138
|
-
end
|
139
|
-
end
|
140
|
-
EOS
|
141
|
-
else
|
142
|
-
# raindrops before 0.18 only supported TCP_INFO under Linux
|
143
|
-
def closed_state?(state) # :nodoc:
|
144
|
-
case state
|
145
|
-
when 1 # ESTABLISHED
|
146
|
-
false
|
147
|
-
when 8, 6, 7, 9, 11 # CLOSE_WAIT, TIME_WAIT, CLOSE, LAST_ACK, CLOSING
|
148
|
-
true
|
149
|
-
else
|
150
|
-
false
|
113
|
+
begin
|
114
|
+
tcp_info = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
|
115
|
+
rescue IOError, SystemCallError
|
116
|
+
return write_http_header(socket)
|
151
117
|
end
|
152
|
-
end
|
153
|
-
end
|
154
|
-
else
|
155
118
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
def check_client_connection(socket) # :nodoc:
|
160
|
-
if TCPSocket === socket && @@tcpi_inspect_ok
|
161
|
-
opt = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).inspect
|
162
|
-
if opt =~ /\bstate=(\S+)/
|
163
|
-
raise Errno::EPIPE, "client closed connection".freeze,
|
164
|
-
EMPTY_ARRAY if closed_state_str?($1)
|
165
|
-
else
|
166
|
-
@@tcpi_inspect_ok = false
|
167
|
-
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
|
168
122
|
end
|
169
|
-
opt.clear
|
170
123
|
else
|
171
124
|
write_http_header(socket)
|
172
125
|
end
|
173
126
|
end
|
174
|
-
|
175
|
-
def
|
176
|
-
|
177
|
-
when 'ESTABLISHED'
|
178
|
-
false
|
179
|
-
# not a typo, ruby maps TCP_CLOSE (no 'D') to state=CLOSED (w/ 'D')
|
180
|
-
when 'CLOSE_WAIT', 'TIME_WAIT', 'CLOSED', 'LAST_ACK', 'CLOSING'
|
181
|
-
true
|
182
|
-
else
|
183
|
-
false
|
184
|
-
end
|
127
|
+
else
|
128
|
+
def check_client_connection(socket) # :nodoc:
|
129
|
+
write_http_header(socket)
|
185
130
|
end
|
186
131
|
end
|
187
132
|
|
@@ -77,7 +77,7 @@ module Pitchfork
|
|
77
77
|
|
78
78
|
# :stopdoc:
|
79
79
|
attr_accessor :app, :timeout, :timeout_signal, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
|
80
|
-
:before_fork, :after_worker_fork, :after_mold_fork,
|
80
|
+
:before_fork, :after_worker_fork, :after_mold_fork, :before_service_worker_ready, :before_service_worker_exit,
|
81
81
|
:listener_opts, :children,
|
82
82
|
:orig_app, :config, :ready_pipe,
|
83
83
|
:default_middleware, :early_hints
|
@@ -95,32 +95,6 @@ module Pitchfork
|
|
95
95
|
|
96
96
|
NOOP = '.'
|
97
97
|
|
98
|
-
# :startdoc:
|
99
|
-
# This Hash is considered a stable interface and changing its contents
|
100
|
-
# will allow you to switch between different installations of Pitchfork
|
101
|
-
# or even different installations of the same applications without
|
102
|
-
# downtime. Keys of this constant Hash are described as follows:
|
103
|
-
#
|
104
|
-
# * 0 - the path to the pitchfork executable
|
105
|
-
# * :argv - a deep copy of the ARGV array the executable originally saw
|
106
|
-
# * :cwd - the working directory of the application, this is where
|
107
|
-
# you originally started Pitchfork.
|
108
|
-
# TODO: Can we get rid of this?
|
109
|
-
START_CTX = {
|
110
|
-
:argv => ARGV.map(&:dup),
|
111
|
-
0 => $0.dup,
|
112
|
-
}
|
113
|
-
# We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
|
114
|
-
# and like systems
|
115
|
-
START_CTX[:cwd] = begin
|
116
|
-
a = File.stat(pwd = ENV['PWD'])
|
117
|
-
b = File.stat(Dir.pwd)
|
118
|
-
a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
|
119
|
-
rescue
|
120
|
-
Dir.pwd
|
121
|
-
end
|
122
|
-
# :stopdoc:
|
123
|
-
|
124
98
|
# Creates a working server on host:port (strange things happen if
|
125
99
|
# port isn't a Number). Use HttpServer::run to start the server and
|
126
100
|
# HttpServer.run.join to join the thread that's processing
|
@@ -140,7 +114,7 @@ module Pitchfork
|
|
140
114
|
self.config = Pitchfork::Configurator.new(options)
|
141
115
|
self.listener_opts = {}
|
142
116
|
|
143
|
-
proc_name role: 'monitor', status:
|
117
|
+
proc_name role: 'monitor', status: ARGV.join(' ')
|
144
118
|
|
145
119
|
# We use @control_socket differently in the master and worker processes:
|
146
120
|
#
|
@@ -173,7 +147,7 @@ module Pitchfork
|
|
173
147
|
:QUIT, :INT, :TERM, :USR2, :TTIN, :TTOU ]
|
174
148
|
|
175
149
|
Info.workers_count = worker_processes
|
176
|
-
SharedMemory.
|
150
|
+
SharedMemory.preallocate_pages(worker_processes)
|
177
151
|
end
|
178
152
|
|
179
153
|
# Runs the thing. Returns self so you can run join on it
|
@@ -296,7 +270,7 @@ module Pitchfork
|
|
296
270
|
def join
|
297
271
|
@respawn = true
|
298
272
|
|
299
|
-
proc_name role: 'monitor', status:
|
273
|
+
proc_name role: 'monitor', status: ARGV.join(' ')
|
300
274
|
|
301
275
|
logger.info "master process ready" # test_exec.rb relies on this message
|
302
276
|
if @ready_pipe
|
@@ -374,6 +348,9 @@ module Pitchfork
|
|
374
348
|
when Message::MoldSpawned
|
375
349
|
new_mold = @children.update(message)
|
376
350
|
logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} spawned")
|
351
|
+
when Message::ServiceSpawned
|
352
|
+
new_service = @children.update(message)
|
353
|
+
logger.info("service pid=#{new_service.pid} gen=#{new_service.generation} spawned")
|
377
354
|
when Message::MoldReady
|
378
355
|
old_molds = @children.molds
|
379
356
|
new_mold = @children.update(message)
|
@@ -429,6 +406,20 @@ module Pitchfork
|
|
429
406
|
Process.exit
|
430
407
|
end
|
431
408
|
|
409
|
+
def service_exit(service)
|
410
|
+
logger.info "service pid=#{service.pid} gen=#{service.generation} exiting"
|
411
|
+
proc_name status: "exiting"
|
412
|
+
|
413
|
+
if @before_service_worker_exit
|
414
|
+
begin
|
415
|
+
@before_service_worker_exit.call(self, service)
|
416
|
+
rescue => error
|
417
|
+
Pitchfork.log_error(logger, "before_service_worker_exit error", error)
|
418
|
+
end
|
419
|
+
end
|
420
|
+
Process.exit
|
421
|
+
end
|
422
|
+
|
432
423
|
def rewindable_input
|
433
424
|
Pitchfork::HttpParser.input_class.method_defined?(:rewind)
|
434
425
|
end
|
@@ -590,6 +581,69 @@ module Pitchfork
|
|
590
581
|
worker
|
591
582
|
end
|
592
583
|
|
584
|
+
def service_loop(service)
|
585
|
+
readers = init_service_process(service)
|
586
|
+
waiter = prep_readers(readers)
|
587
|
+
|
588
|
+
ready = readers.dup
|
589
|
+
|
590
|
+
if @before_service_worker_ready
|
591
|
+
begin
|
592
|
+
@before_service_worker_ready.call(self, service)
|
593
|
+
rescue => error
|
594
|
+
Pitchfork.log_error(logger, "before_service_worker_ready", error)
|
595
|
+
Process.exit(1)
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
proc_name status: "ready"
|
600
|
+
|
601
|
+
while readers[0]
|
602
|
+
begin
|
603
|
+
service.update_deadline(@timeout)
|
604
|
+
while sock = ready.shift
|
605
|
+
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
606
|
+
# but that will return false
|
607
|
+
client = sock.accept_nonblock(exception: false)
|
608
|
+
|
609
|
+
case client
|
610
|
+
when false, :wait_readable
|
611
|
+
# no message, keep looping
|
612
|
+
end
|
613
|
+
|
614
|
+
service.update_deadline(@timeout)
|
615
|
+
end
|
616
|
+
|
617
|
+
# timeout so we can update .deadline and keep parent from SIGKILL-ing us
|
618
|
+
service.update_deadline(@timeout)
|
619
|
+
|
620
|
+
|
621
|
+
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
622
|
+
rescue => e
|
623
|
+
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
624
|
+
end
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
def spawn_service(service, detach:)
|
629
|
+
logger.info("service gen=#{service.generation} spawning...")
|
630
|
+
|
631
|
+
# We set the deadline before spawning the child so that if for some
|
632
|
+
# reason it gets stuck before reaching the worker loop,
|
633
|
+
# the monitor process will kill it.
|
634
|
+
service.update_deadline(@spawn_timeout)
|
635
|
+
@before_fork&.call(self)
|
636
|
+
fork_sibling("spawn_service") do
|
637
|
+
service.pid = Process.pid
|
638
|
+
|
639
|
+
after_fork_internal
|
640
|
+
service_loop(service)
|
641
|
+
service_exit(service)
|
642
|
+
end
|
643
|
+
|
644
|
+
service
|
645
|
+
end
|
646
|
+
|
593
647
|
def spawn_initial_mold
|
594
648
|
mold = Worker.new(nil)
|
595
649
|
mold.create_socketpair!
|
@@ -606,6 +660,21 @@ module Pitchfork
|
|
606
660
|
end
|
607
661
|
|
608
662
|
def spawn_missing_workers
|
663
|
+
if @before_service_worker_ready && !@children.service
|
664
|
+
service = Pitchfork::Service.new
|
665
|
+
if REFORKING_AVAILABLE
|
666
|
+
service.generation = @children.mold&.generation || 0
|
667
|
+
|
668
|
+
unless @children.mold&.spawn_service(service)
|
669
|
+
@logger.error("Failed to send a spawn_service command")
|
670
|
+
end
|
671
|
+
else
|
672
|
+
spawn_service(service, detach: false)
|
673
|
+
end
|
674
|
+
|
675
|
+
@children.register_service(service)
|
676
|
+
end
|
677
|
+
|
609
678
|
worker_nr = -1
|
610
679
|
until (worker_nr += 1) == @worker_processes
|
611
680
|
if @children.nr_alive?(worker_nr)
|
@@ -643,9 +712,18 @@ module Pitchfork
|
|
643
712
|
end
|
644
713
|
|
645
714
|
def maintain_worker_count
|
646
|
-
|
647
|
-
off
|
648
|
-
|
715
|
+
off = @children.workers_count - worker_processes
|
716
|
+
off -= 1 if @before_service_worker_ready && !@children.service
|
717
|
+
|
718
|
+
if off < 0
|
719
|
+
spawn_missing_workers
|
720
|
+
elsif off > 0
|
721
|
+
@children.each_worker do |worker|
|
722
|
+
if worker.nr >= worker_processes
|
723
|
+
worker.soft_kill(:TERM)
|
724
|
+
end
|
725
|
+
end
|
726
|
+
end
|
649
727
|
end
|
650
728
|
|
651
729
|
def restart_outdated_workers
|
@@ -658,6 +736,16 @@ module Pitchfork
|
|
658
736
|
max_pending_workers = (worker_processes * 0.1).ceil
|
659
737
|
workers_to_restart = max_pending_workers - @children.restarting_workers_count
|
660
738
|
|
739
|
+
if service = @children.service
|
740
|
+
if service.outdated?
|
741
|
+
if service.soft_kill(:TERM)
|
742
|
+
logger.info("Sent SIGTERM to service pid=#{service.pid} gen=#{service.generation}")
|
743
|
+
else
|
744
|
+
logger.info("Failed to send SIGTERM to service pid=#{service.pid} gen=#{service.generation}")
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|
748
|
+
|
661
749
|
if workers_to_restart > 0
|
662
750
|
outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
|
663
751
|
outdated_workers.each do |worker|
|
@@ -701,18 +789,14 @@ module Pitchfork
|
|
701
789
|
rss = @request.response_start_sent
|
702
790
|
buf = (rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n").b
|
703
791
|
headers.each { |key, value| append_header(buf, key, value) }
|
704
|
-
buf << (rss ? "\r\nHTTP/1.1 "
|
792
|
+
buf << (rss ? "\r\nHTTP/1.1 " : "\r\n")
|
705
793
|
client.write(buf)
|
706
794
|
end
|
707
795
|
|
708
796
|
def e100_response_write(client, env)
|
709
|
-
# We use String#freeze to avoid allocations under Ruby 2.1+
|
710
|
-
# Not many users hit this code path, so it's better to reduce the
|
711
|
-
# constant table sizes even for Ruby 2.0 users who'll hit extra
|
712
|
-
# allocations here.
|
713
797
|
client.write(@request.response_start_sent ?
|
714
|
-
"100 Continue\r\n\r\nHTTP/1.1 "
|
715
|
-
"HTTP/1.1 100 Continue\r\n\r\n"
|
798
|
+
"100 Continue\r\n\r\nHTTP/1.1 " :
|
799
|
+
"HTTP/1.1 100 Continue\r\n\r\n")
|
716
800
|
env.delete('HTTP_EXPECT')
|
717
801
|
end
|
718
802
|
|
@@ -723,7 +807,11 @@ module Pitchfork
|
|
723
807
|
@request = Pitchfork::HttpParser.new
|
724
808
|
env = @request.read(client)
|
725
809
|
|
726
|
-
|
810
|
+
status = "requests: #{worker.requests_count}, processing: #{env["PATH_INFO"]}"
|
811
|
+
if request_id = env["HTTP_X_REQUEST_ID"]
|
812
|
+
status += ", request_id: #{request_id}"
|
813
|
+
end
|
814
|
+
proc_name status: status
|
727
815
|
|
728
816
|
env["pitchfork.worker"] = worker
|
729
817
|
timeout_handler.rack_env = env
|
@@ -817,6 +905,18 @@ module Pitchfork
|
|
817
905
|
readers
|
818
906
|
end
|
819
907
|
|
908
|
+
def init_service_process(service)
|
909
|
+
proc_name role: "(gen:#{service.generation}) mold", status: "init"
|
910
|
+
LISTENERS.each(&:close) # Don't appear as listening to incoming requests
|
911
|
+
service.register_to_master(@control_socket[1])
|
912
|
+
readers = [service]
|
913
|
+
trap(:QUIT) { nuke_listeners!(readers) }
|
914
|
+
trap(:TERM) { nuke_listeners!(readers) }
|
915
|
+
trap(:INT) { nuke_listeners!(readers); exit!(0) }
|
916
|
+
proc_name role: "(gen:#{service.generation}) service", status: "ready"
|
917
|
+
readers
|
918
|
+
end
|
919
|
+
|
820
920
|
def init_mold_process(mold)
|
821
921
|
proc_name role: "(gen:#{mold.generation}) mold", status: "init"
|
822
922
|
after_mold_fork.call(self, mold)
|
@@ -830,7 +930,7 @@ module Pitchfork
|
|
830
930
|
|
831
931
|
if Pitchfork.const_defined?(:Waiter)
|
832
932
|
def prep_readers(readers)
|
833
|
-
Pitchfork::Waiter.prep_readers(readers)
|
933
|
+
Pitchfork::Info.keep_io(Pitchfork::Waiter.prep_readers(readers))
|
834
934
|
end
|
835
935
|
else
|
836
936
|
require_relative 'select_waiter'
|
@@ -858,24 +958,25 @@ module Pitchfork
|
|
858
958
|
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
859
959
|
# but that will return false
|
860
960
|
client = sock.accept_nonblock(exception: false)
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
|
869
|
-
end
|
870
|
-
when Message
|
871
|
-
worker.update(client)
|
961
|
+
|
962
|
+
case client
|
963
|
+
when false, :wait_readable
|
964
|
+
# no message, keep looping
|
965
|
+
when Message::PromoteWorker
|
966
|
+
if Info.fork_safe?
|
967
|
+
spawn_mold(worker)
|
872
968
|
else
|
873
|
-
|
874
|
-
@after_request_complete&.call(self, worker, request_env)
|
875
|
-
worker.increment_requests_count
|
969
|
+
logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
|
876
970
|
end
|
877
|
-
|
971
|
+
when Message
|
972
|
+
worker.update(client)
|
973
|
+
else
|
974
|
+
request_env = process_client(client, worker, prepare_timeout(worker))
|
975
|
+
worker.increment_requests_count
|
976
|
+
@after_request_complete&.call(self, worker, request_env)
|
878
977
|
end
|
978
|
+
|
979
|
+
worker.update_deadline(@timeout)
|
879
980
|
end
|
880
981
|
|
881
982
|
# timeout so we can update .deadline and keep parent from SIGKILL-ing us
|
@@ -958,6 +1059,22 @@ module Pitchfork
|
|
958
1059
|
rescue => error
|
959
1060
|
raise BootFailure, error.message
|
960
1061
|
end
|
1062
|
+
when Message::SpawnService
|
1063
|
+
retries = 1
|
1064
|
+
begin
|
1065
|
+
spawn_service(Service.new(generation: mold.generation), detach: true)
|
1066
|
+
rescue ForkFailure
|
1067
|
+
if retries > 0
|
1068
|
+
@logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a service. Retrying.")
|
1069
|
+
retries -= 1
|
1070
|
+
retry
|
1071
|
+
else
|
1072
|
+
@logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a service twice in a row. Corrupted mold process?")
|
1073
|
+
Process.exit(1)
|
1074
|
+
end
|
1075
|
+
rescue => error
|
1076
|
+
raise BootFailure, error.message
|
1077
|
+
end
|
961
1078
|
else
|
962
1079
|
logger.error("Unexpected mold message #{message.inspect}")
|
963
1080
|
end
|
@@ -1004,7 +1121,7 @@ module Pitchfork
|
|
1004
1121
|
@proctitle_role = role if role
|
1005
1122
|
@proctitle_status = status if status
|
1006
1123
|
|
1007
|
-
Process.setproctitle("#{File.basename(
|
1124
|
+
Process.setproctitle("#{File.basename($PROGRAM_NAME)} #{@proctitle_role} - #{@proctitle_status}")
|
1008
1125
|
end
|
1009
1126
|
|
1010
1127
|
def bind_listeners!
|
@@ -1012,7 +1129,6 @@ module Pitchfork
|
|
1012
1129
|
if listeners.empty?
|
1013
1130
|
listeners << Pitchfork::Const::DEFAULT_LISTEN
|
1014
1131
|
@init_listeners << Pitchfork::Const::DEFAULT_LISTEN
|
1015
|
-
START_CTX[:argv] << "-l#{Pitchfork::Const::DEFAULT_LISTEN}"
|
1016
1132
|
end
|
1017
1133
|
listeners.each { |addr| listen(addr) }
|
1018
1134
|
raise ArgumentError, "no listeners" if LISTENERS.empty?
|
data/lib/pitchfork/message.rb
CHANGED
@@ -122,12 +122,16 @@ module Pitchfork
|
|
122
122
|
|
123
123
|
Message = Class.new(Struct)
|
124
124
|
class Message
|
125
|
-
SpawnWorker =
|
126
|
-
WorkerSpawned =
|
127
|
-
PromoteWorker =
|
128
|
-
MoldSpawned = Message.new(:nr, :pid, :generation, :pipe)
|
129
|
-
MoldReady = Message.new(:nr, :pid, :generation)
|
125
|
+
SpawnWorker = new(:nr)
|
126
|
+
WorkerSpawned = new(:nr, :pid, :generation, :pipe)
|
127
|
+
PromoteWorker = new(:generation)
|
130
128
|
|
131
|
-
|
129
|
+
MoldSpawned = new(:nr, :pid, :generation, :pipe)
|
130
|
+
MoldReady = new(:nr, :pid, :generation)
|
131
|
+
|
132
|
+
SpawnService = new(:_) # Struct.new requires at least 1 member on Ruby < 3.3
|
133
|
+
ServiceSpawned = new(:pid, :generation, :pipe)
|
134
|
+
|
135
|
+
SoftKill = new(:signum)
|
132
136
|
end
|
133
137
|
end
|