pitchfork 0.1.2 → 0.2.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.
Potentially problematic release.
This version of pitchfork might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +7 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +7 -6
- data/Rakefile +10 -2
- data/docs/CONFIGURATION.md +30 -32
- data/examples/pitchfork.conf.rb +2 -2
- data/exe/pitchfork +1 -8
- data/ext/pitchfork_http/epollexclusive.h +13 -17
- data/ext/pitchfork_http/pitchfork_http.c +196 -192
- data/ext/pitchfork_http/pitchfork_http.rl +23 -19
- data/lib/pitchfork/configurator.rb +28 -36
- data/lib/pitchfork/flock.rb +51 -0
- data/lib/pitchfork/http_server.rb +87 -70
- data/lib/pitchfork/refork_condition.rb +3 -3
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +21 -16
- data/lib/pitchfork.rb +25 -31
- metadata +4 -4
- data/lib/pitchfork/mold_selector.rb +0 -29
@@ -224,7 +224,7 @@ static int is_chunked(VALUE v)
|
|
224
224
|
return rb_funcall(cHttpParser, id_is_chunked_p, 1, v) != Qfalse;
|
225
225
|
}
|
226
226
|
|
227
|
-
static void write_value(struct http_parser *hp,
|
227
|
+
static void write_value(VALUE self, struct http_parser *hp,
|
228
228
|
const char *buffer, const char *p)
|
229
229
|
{
|
230
230
|
VALUE f = find_common_field(PTR_TO(start.field), hp->s.field_len);
|
@@ -244,7 +244,7 @@ static void write_value(struct http_parser *hp,
|
|
244
244
|
* rack env variable.
|
245
245
|
*/
|
246
246
|
if (CONST_MEM_EQ("VERSION", field, flen)) {
|
247
|
-
hp->cont
|
247
|
+
RB_OBJ_WRITE(self, &hp->cont, Qnil);
|
248
248
|
return;
|
249
249
|
}
|
250
250
|
f = uncommon_field(field, flen);
|
@@ -296,16 +296,16 @@ static void write_value(struct http_parser *hp,
|
|
296
296
|
|
297
297
|
e = rb_hash_aref(hp->env, f);
|
298
298
|
if (NIL_P(e)) {
|
299
|
-
hp->cont
|
299
|
+
RB_OBJ_WRITE(self, &hp->cont, rb_hash_aset(hp->env, f, v));
|
300
300
|
} else if (f == g_http_host) {
|
301
301
|
/*
|
302
302
|
* ignored, absolute URLs in REQUEST_URI take precedence over
|
303
303
|
* the Host: header (ref: rfc 2616, section 5.2.1)
|
304
304
|
*/
|
305
|
-
|
305
|
+
RB_OBJ_WRITE(self, &hp->cont, Qnil);
|
306
306
|
} else {
|
307
307
|
rb_str_buf_cat(e, ",", 1);
|
308
|
-
hp->cont
|
308
|
+
RB_OBJ_WRITE(self, &hp->cont, rb_str_buf_append(e, v));
|
309
309
|
}
|
310
310
|
}
|
311
311
|
|
@@ -321,7 +321,7 @@ static void write_value(struct http_parser *hp,
|
|
321
321
|
action downcase_char { downcase_char(deconst(fpc)); }
|
322
322
|
action write_field { hp->s.field_len = LEN(start.field, fpc); }
|
323
323
|
action start_value { MARK(mark, fpc); }
|
324
|
-
action write_value { write_value(hp, buffer, fpc); }
|
324
|
+
action write_value { write_value(self, hp, buffer, fpc); }
|
325
325
|
action write_cont_value { write_cont_value(hp, buffer, fpc); }
|
326
326
|
action request_method { request_method(hp, PTR_TO(mark), LEN(mark, fpc)); }
|
327
327
|
action scheme {
|
@@ -439,7 +439,7 @@ static void http_parser_init(struct http_parser *hp)
|
|
439
439
|
|
440
440
|
/** exec **/
|
441
441
|
static void
|
442
|
-
http_parser_execute(struct http_parser *hp, char *buffer, size_t len)
|
442
|
+
http_parser_execute(VALUE self, struct http_parser *hp, char *buffer, size_t len)
|
443
443
|
{
|
444
444
|
const char *p, *pe;
|
445
445
|
int cs = hp->cs;
|
@@ -485,9 +485,13 @@ static size_t hp_memsize(const void *ptr)
|
|
485
485
|
}
|
486
486
|
|
487
487
|
static const rb_data_type_t hp_type = {
|
488
|
-
|
489
|
-
|
490
|
-
|
488
|
+
.wrap_struct_name = "pitchfork_http_parser",
|
489
|
+
.function = {
|
490
|
+
.dmark = hp_mark,
|
491
|
+
.dfree = RUBY_TYPED_DEFAULT_FREE,
|
492
|
+
.dsize = hp_memsize,
|
493
|
+
},
|
494
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
491
495
|
};
|
492
496
|
|
493
497
|
static struct http_parser *data_get(VALUE self)
|
@@ -618,8 +622,8 @@ static VALUE HttpParser_init(VALUE self)
|
|
618
622
|
struct http_parser *hp = data_get(self);
|
619
623
|
|
620
624
|
http_parser_init(hp);
|
621
|
-
hp->buf
|
622
|
-
hp->env
|
625
|
+
RB_OBJ_WRITE(self, &hp->buf, rb_str_new(NULL, 0));
|
626
|
+
RB_OBJ_WRITE(self, &hp->env, rb_hash_new());
|
623
627
|
|
624
628
|
return self;
|
625
629
|
}
|
@@ -700,7 +704,7 @@ static VALUE HttpParser_parse(VALUE self)
|
|
700
704
|
if (HP_FL_TEST(hp, TO_CLEAR))
|
701
705
|
HttpParser_clear(self);
|
702
706
|
|
703
|
-
http_parser_execute(hp, RSTRING_PTR(data), RSTRING_LEN(data));
|
707
|
+
http_parser_execute(self, hp, RSTRING_PTR(data), RSTRING_LEN(data));
|
704
708
|
if (hp->offset > MAX_HEADER_LEN)
|
705
709
|
parser_raise(e413, "HTTP header is too large");
|
706
710
|
|
@@ -756,8 +760,8 @@ static VALUE HttpParser_headers(VALUE self, VALUE env, VALUE buf)
|
|
756
760
|
{
|
757
761
|
struct http_parser *hp = data_get(self);
|
758
762
|
|
759
|
-
hp->
|
760
|
-
hp->
|
763
|
+
RB_OBJ_WRITE(self, &hp->buf, buf);
|
764
|
+
RB_OBJ_WRITE(self, &hp->env, env);
|
761
765
|
|
762
766
|
return HttpParser_parse(self);
|
763
767
|
}
|
@@ -885,9 +889,9 @@ static VALUE HttpParser_filter_body(VALUE self, VALUE dst, VALUE src)
|
|
885
889
|
rb_str_resize(dst, srclen); /* we can never copy more than srclen bytes */
|
886
890
|
|
887
891
|
hp->s.dest_offset = 0;
|
888
|
-
hp->cont
|
889
|
-
hp->buf
|
890
|
-
http_parser_execute(hp, srcptr, srclen);
|
892
|
+
RB_OBJ_WRITE(self, &hp->cont, dst);
|
893
|
+
RB_OBJ_WRITE(self, &hp->buf, src);
|
894
|
+
http_parser_execute(self, hp, srcptr, srclen);
|
891
895
|
if (hp->cs == http_parser_error)
|
892
896
|
parser_raise(eHttpParserError, "Invalid HTTP format, parsing fails.");
|
893
897
|
|
@@ -917,7 +921,7 @@ static VALUE HttpParser_filter_body(VALUE self, VALUE dst, VALUE src)
|
|
917
921
|
* This causes copy-on-write behavior to be triggered anyways
|
918
922
|
* when the +src+ buffer is modified (when reading off the socket).
|
919
923
|
*/
|
920
|
-
hp->buf
|
924
|
+
RB_OBJ_WRITE(self, &hp->buf, src);
|
921
925
|
memcpy(RSTRING_PTR(dst), srcptr, nr);
|
922
926
|
hp->len.content -= nr;
|
923
927
|
if (hp->len.content == 0) {
|
@@ -31,30 +31,29 @@ module Pitchfork
|
|
31
31
|
:logger => Logger.new($stderr),
|
32
32
|
:worker_processes => 1,
|
33
33
|
:after_fork => lambda { |server, worker|
|
34
|
-
|
35
|
-
|
36
|
-
:
|
37
|
-
|
38
|
-
|
34
|
+
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
|
35
|
+
},
|
36
|
+
:after_promotion => lambda { |server, worker|
|
37
|
+
server.logger.info("gen=#{worker.generation} pid=#{$$} promoted")
|
38
|
+
},
|
39
39
|
:after_worker_exit => lambda { |server, worker, status|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
40
|
+
m = if worker.nil?
|
41
|
+
"repead unknown process (#{status.inspect})"
|
42
|
+
elsif worker.mold?
|
43
|
+
"mold pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
|
44
|
+
else
|
45
|
+
"worker=#{worker.nr rescue 'unknown'} pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
|
46
|
+
end
|
47
|
+
if status.success?
|
48
|
+
server.logger.info(m)
|
49
|
+
else
|
50
|
+
server.logger.error(m)
|
51
|
+
end
|
52
|
+
},
|
53
53
|
:after_worker_ready => lambda { |server, worker|
|
54
|
-
|
55
|
-
|
54
|
+
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} ready")
|
55
|
+
},
|
56
56
|
:early_hints => false,
|
57
|
-
:mold_selector => MoldSelector::LeastSharedMemory.new,
|
58
57
|
:refork_condition => nil,
|
59
58
|
:check_client_connection => false,
|
60
59
|
:rewindable_input => true,
|
@@ -85,9 +84,6 @@ module Pitchfork
|
|
85
84
|
|
86
85
|
RACKUP[:set_listener] and
|
87
86
|
set[:listeners] << "#{RACKUP[:host]}:#{RACKUP[:port]}"
|
88
|
-
|
89
|
-
RACKUP[:no_default_middleware] and
|
90
|
-
set[:default_middleware] = false
|
91
87
|
end
|
92
88
|
|
93
89
|
def commit!(server, options = {}) #:nodoc:
|
@@ -123,14 +119,14 @@ module Pitchfork
|
|
123
119
|
set[:logger] = obj
|
124
120
|
end
|
125
121
|
|
126
|
-
def before_fork(*args, &block)
|
127
|
-
set_hook(:before_fork, block_given? ? block : args[0])
|
128
|
-
end
|
129
|
-
|
130
122
|
def after_fork(*args, &block)
|
131
123
|
set_hook(:after_fork, block_given? ? block : args[0])
|
132
124
|
end
|
133
125
|
|
126
|
+
def after_promotion(*args, &block)
|
127
|
+
set_hook(:after_promotion, block_given? ? block : args[0])
|
128
|
+
end
|
129
|
+
|
134
130
|
def after_worker_ready(*args, &block)
|
135
131
|
set_hook(:after_worker_ready, block_given? ? block : args[0])
|
136
132
|
end
|
@@ -139,10 +135,6 @@ module Pitchfork
|
|
139
135
|
set_hook(:after_worker_exit, block_given? ? block : args[0], 3)
|
140
136
|
end
|
141
137
|
|
142
|
-
def mold_selector(*args, &block)
|
143
|
-
set_hook(:mold_selector, block_given? ? block : args[0], 3)
|
144
|
-
end
|
145
|
-
|
146
138
|
def timeout(seconds)
|
147
139
|
set_int(:timeout, seconds, 3)
|
148
140
|
# POSIX says 31 days is the smallest allowed maximum timeout for select()
|
@@ -154,10 +146,6 @@ module Pitchfork
|
|
154
146
|
set_int(:worker_processes, nr, 1)
|
155
147
|
end
|
156
148
|
|
157
|
-
def default_middleware(bool)
|
158
|
-
set_bool(:default_middleware, bool)
|
159
|
-
end
|
160
|
-
|
161
149
|
def early_hints(bool)
|
162
150
|
set_bool(:early_hints, bool)
|
163
151
|
end
|
@@ -208,8 +196,12 @@ module Pitchfork
|
|
208
196
|
# Defines the number of requests per-worker after which a new generation
|
209
197
|
# should be spawned.
|
210
198
|
#
|
199
|
+
# +false+ can be used to mark a final generation, otherwise the last request
|
200
|
+
# count is re-used indefinitely.
|
201
|
+
#
|
211
202
|
# example:
|
212
203
|
#. refork_after [50, 100, 1000]
|
204
|
+
#. refork_after [50, 100, 1000, false]
|
213
205
|
#
|
214
206
|
# Note that reforking is only available on Linux. Other Unix-like systems
|
215
207
|
# don't have this capability.
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Pitchfork
|
4
|
+
class Flock
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
@file = Tempfile.create([name, '.lock'])
|
10
|
+
@file.write("#{Process.pid}\n")
|
11
|
+
@file.flush
|
12
|
+
@owned = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def at_fork
|
16
|
+
@owned = false
|
17
|
+
@file.close
|
18
|
+
@file = File.open(@file.path, "w")
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def unlink
|
23
|
+
File.unlink(@file.path)
|
24
|
+
rescue Errno::ENOENT
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def try_lock
|
29
|
+
raise Error, "Pitchfork::Flock(#{@name}) trying to lock an already owned lock" if @owned
|
30
|
+
|
31
|
+
if @file.flock(File::LOCK_EX | File::LOCK_NB)
|
32
|
+
@owned = true
|
33
|
+
else
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def unlock
|
39
|
+
raise Error, "Pitchfork::Flock(#{@name}) trying to unlock a non-owned lock" unless @owned
|
40
|
+
|
41
|
+
begin
|
42
|
+
if @file.flock(File::LOCK_UN)
|
43
|
+
@owned = false
|
44
|
+
true
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
2
|
require 'pitchfork/pitchfork_http'
|
3
|
+
require 'pitchfork/flock'
|
3
4
|
|
4
5
|
module Pitchfork
|
5
6
|
# This is the process manager of Pitchfork. This manages worker
|
@@ -9,11 +10,11 @@ module Pitchfork
|
|
9
10
|
class HttpServer
|
10
11
|
# :stopdoc:
|
11
12
|
attr_accessor :app, :timeout, :worker_processes,
|
12
|
-
:
|
13
|
+
:after_fork, :after_promotion,
|
13
14
|
:listener_opts, :children,
|
14
15
|
:orig_app, :config, :ready_pipe,
|
15
16
|
:default_middleware, :early_hints
|
16
|
-
attr_writer :after_worker_exit, :after_worker_ready, :refork_condition
|
17
|
+
attr_writer :after_worker_exit, :after_worker_ready, :refork_condition
|
17
18
|
|
18
19
|
attr_reader :logger
|
19
20
|
include Pitchfork::SocketHelper
|
@@ -27,7 +28,6 @@ module Pitchfork
|
|
27
28
|
NOOP = '.'
|
28
29
|
|
29
30
|
REFORKING_AVAILABLE = Pitchfork::CHILD_SUBREAPER_AVAILABLE || Process.pid == 1
|
30
|
-
MAX_SLEEP = 1 # seconds
|
31
31
|
|
32
32
|
# :startdoc:
|
33
33
|
# This Hash is considered a stable interface and changing its contents
|
@@ -60,13 +60,15 @@ module Pitchfork
|
|
60
60
|
# HttpServer.run.join to join the thread that's processing
|
61
61
|
# incoming requests on the socket.
|
62
62
|
def initialize(app, options = {})
|
63
|
+
@exit_status = 0
|
63
64
|
@app = app
|
64
65
|
@respawn = false
|
65
66
|
@last_check = time_now
|
66
|
-
@
|
67
|
+
@promotion_lock = Flock.new("pitchfork-promotion")
|
68
|
+
|
67
69
|
options = options.dup
|
68
70
|
@ready_pipe = options.delete(:ready_pipe)
|
69
|
-
@init_listeners = options[:listeners]
|
71
|
+
@init_listeners = options[:listeners].dup || []
|
70
72
|
options[:use_defaults] = true
|
71
73
|
self.config = Pitchfork::Configurator.new(options)
|
72
74
|
self.listener_opts = {}
|
@@ -104,7 +106,7 @@ module Pitchfork
|
|
104
106
|
end
|
105
107
|
|
106
108
|
# Runs the thing. Returns self so you can run join on it
|
107
|
-
def start
|
109
|
+
def start(sync = true)
|
108
110
|
Pitchfork.enable_child_subreaper # noop if not supported
|
109
111
|
|
110
112
|
# This socketpair is used to wake us up from select(2) in #join when signals
|
@@ -120,7 +122,6 @@ module Pitchfork
|
|
120
122
|
@queue_sigs.each { |sig| trap(sig) { @sig_queue << sig; awaken_master } }
|
121
123
|
trap(:CHLD) { awaken_master }
|
122
124
|
|
123
|
-
bind_listeners!
|
124
125
|
if REFORKING_AVAILABLE
|
125
126
|
spawn_initial_mold
|
126
127
|
wait_for_pending_workers
|
@@ -129,13 +130,17 @@ module Pitchfork
|
|
129
130
|
end
|
130
131
|
else
|
131
132
|
build_app!
|
133
|
+
bind_listeners!
|
134
|
+
after_promotion.call(self, Worker.new(nil, pid: $$).promoted!)
|
132
135
|
end
|
133
136
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
137
|
+
if sync
|
138
|
+
spawn_missing_workers
|
139
|
+
# We could just return here as we'd register them later in #join.
|
140
|
+
# However a good part of the test suite assumes #start only return
|
141
|
+
# once all initial workers are spawned.
|
142
|
+
wait_for_pending_workers
|
143
|
+
end
|
139
144
|
|
140
145
|
self
|
141
146
|
end
|
@@ -237,10 +242,19 @@ module Pitchfork
|
|
237
242
|
end
|
238
243
|
stop # gracefully shutdown all workers on our way out
|
239
244
|
logger.info "master complete"
|
245
|
+
@exit_status
|
240
246
|
end
|
241
247
|
|
242
248
|
def monitor_loop(sleep = true)
|
243
249
|
reap_all_workers
|
250
|
+
|
251
|
+
if REFORKING_AVAILABLE && @respawn && @children.molds.empty?
|
252
|
+
logger.info("No mold alive, shutting down")
|
253
|
+
@exit_status = 1
|
254
|
+
@sig_queue << :QUIT
|
255
|
+
@respawn = false
|
256
|
+
end
|
257
|
+
|
244
258
|
case message = @sig_queue.shift
|
245
259
|
when nil
|
246
260
|
# avoid murdering workers after our master process (or the
|
@@ -253,13 +267,15 @@ module Pitchfork
|
|
253
267
|
end
|
254
268
|
if @respawn
|
255
269
|
maintain_worker_count
|
256
|
-
|
270
|
+
restart_outdated_workers if REFORKING_AVAILABLE
|
257
271
|
end
|
258
272
|
|
259
273
|
master_sleep(sleep_time) if sleep
|
260
274
|
when :QUIT # graceful shutdown
|
275
|
+
logger.info "QUIT received, starting graceful shutdown"
|
261
276
|
return StopIteration
|
262
277
|
when :TERM, :INT # immediate shutdown
|
278
|
+
logger.info "#{message} received, starting immediate shutdown"
|
263
279
|
stop(false)
|
264
280
|
return StopIteration
|
265
281
|
when :USR2 # trigger a promotion
|
@@ -290,6 +306,7 @@ module Pitchfork
|
|
290
306
|
|
291
307
|
# Terminates all workers, but does not exit master process
|
292
308
|
def stop(graceful = true)
|
309
|
+
wait_for_pending_workers
|
293
310
|
self.listeners = []
|
294
311
|
limit = time_now + timeout
|
295
312
|
until @children.workers.empty? || time_now > limit
|
@@ -302,6 +319,7 @@ module Pitchfork
|
|
302
319
|
reap_all_workers
|
303
320
|
end
|
304
321
|
kill_each_child(:KILL)
|
322
|
+
@promotion_lock.unlink
|
305
323
|
end
|
306
324
|
|
307
325
|
def rewindable_input
|
@@ -333,8 +351,6 @@ module Pitchfork
|
|
333
351
|
|
334
352
|
# wait for a signal handler to wake us up and then consume the pipe
|
335
353
|
def master_sleep(sec)
|
336
|
-
sec = MAX_SLEEP if sec > MAX_SLEEP
|
337
|
-
|
338
354
|
@control_socket[0].wait(sec) or return
|
339
355
|
case message = @control_socket[0].recvmsg_nonblock(exception: false)
|
340
356
|
when :wait_readable, NOOP
|
@@ -351,7 +367,7 @@ module Pitchfork
|
|
351
367
|
|
352
368
|
# reaps all unreaped workers
|
353
369
|
def reap_all_workers
|
354
|
-
|
370
|
+
loop do
|
355
371
|
wpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
356
372
|
wpid or return
|
357
373
|
worker = @children.reap(wpid) and worker.close rescue nil
|
@@ -362,7 +378,7 @@ module Pitchfork
|
|
362
378
|
end
|
363
379
|
rescue Errno::ECHILD
|
364
380
|
break
|
365
|
-
end
|
381
|
+
end
|
366
382
|
end
|
367
383
|
|
368
384
|
def listener_sockets
|
@@ -374,15 +390,6 @@ module Pitchfork
|
|
374
390
|
listener_fds
|
375
391
|
end
|
376
392
|
|
377
|
-
def close_sockets_on_exec(sockets)
|
378
|
-
(3..1024).each do |io|
|
379
|
-
next if sockets.include?(io)
|
380
|
-
io = IO.for_fd(io) rescue next
|
381
|
-
io.autoclose = false
|
382
|
-
io.close_on_exec = true
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
393
|
# forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File
|
387
394
|
def murder_lazy_workers
|
388
395
|
next_sleep = @timeout - 1
|
@@ -413,17 +420,17 @@ module Pitchfork
|
|
413
420
|
end
|
414
421
|
|
415
422
|
unless @children.pending_promotion?
|
416
|
-
@children.
|
417
|
-
if new_mold = @mold_selector.call(self)
|
423
|
+
if new_mold = @children.fresh_workers.first
|
418
424
|
@children.promote(new_mold)
|
419
425
|
else
|
420
|
-
logger.error("
|
426
|
+
logger.error("No children at all???")
|
421
427
|
end
|
422
428
|
else
|
423
429
|
end
|
424
430
|
end
|
425
431
|
|
426
432
|
def after_fork_internal
|
433
|
+
@promotion_lock.at_fork
|
427
434
|
@control_socket[0].close_write # this is master-only, now
|
428
435
|
@ready_pipe.close if @ready_pipe
|
429
436
|
Pitchfork::Configurator::RACKUP.clear
|
@@ -435,9 +442,9 @@ module Pitchfork
|
|
435
442
|
end
|
436
443
|
|
437
444
|
def spawn_worker(worker, detach:)
|
438
|
-
|
445
|
+
logger.info("worker=#{worker.nr} gen=#{worker.generation} spawning...")
|
439
446
|
|
440
|
-
pid =
|
447
|
+
pid = Pitchfork.clean_fork do
|
441
448
|
# We double fork so that the new worker is re-attached back
|
442
449
|
# to the master.
|
443
450
|
# This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
|
@@ -467,10 +474,11 @@ module Pitchfork
|
|
467
474
|
def spawn_initial_mold
|
468
475
|
mold = Worker.new(nil)
|
469
476
|
mold.create_socketpair!
|
470
|
-
mold.pid =
|
471
|
-
|
477
|
+
mold.pid = Pitchfork.clean_fork do
|
478
|
+
@promotion_lock.try_lock
|
472
479
|
mold.after_fork_in_child
|
473
480
|
build_app!
|
481
|
+
bind_listeners!
|
474
482
|
mold_loop(mold)
|
475
483
|
end
|
476
484
|
@children.register_mold(mold)
|
@@ -484,9 +492,11 @@ module Pitchfork
|
|
484
492
|
end
|
485
493
|
worker = Pitchfork::Worker.new(worker_nr)
|
486
494
|
|
487
|
-
if
|
488
|
-
|
489
|
-
|
495
|
+
if REFORKING_AVAILABLE
|
496
|
+
unless @children.mold&.spawn_worker(worker)
|
497
|
+
@logger.error("Failed to send a spawn_woker command")
|
498
|
+
end
|
499
|
+
else
|
490
500
|
spawn_worker(worker, detach: false)
|
491
501
|
end
|
492
502
|
# We could directly register workers when we spawn from the
|
@@ -504,7 +514,7 @@ module Pitchfork
|
|
504
514
|
while @children.pending_workers?
|
505
515
|
master_sleep(0.5)
|
506
516
|
if monitor_loop(false) == StopIteration
|
507
|
-
|
517
|
+
return StopIteration
|
508
518
|
end
|
509
519
|
end
|
510
520
|
end
|
@@ -515,34 +525,21 @@ module Pitchfork
|
|
515
525
|
@children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:QUIT) }
|
516
526
|
end
|
517
527
|
|
518
|
-
def
|
528
|
+
def restart_outdated_workers
|
519
529
|
# If we're already in the middle of forking a new generation, we just continue
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
end
|
534
|
-
end
|
535
|
-
|
536
|
-
# If all workers are alive and well, we can consider reforking a new generation
|
537
|
-
if @refork_condition
|
538
|
-
@children.refresh
|
539
|
-
if @refork_condition.met?(@children, logger)
|
540
|
-
logger.info("Refork condition met, scheduling a promotion")
|
541
|
-
unless @sig_queue.include?(:USR2)
|
542
|
-
@sig_queue << :USR2
|
543
|
-
awaken_master
|
544
|
-
end
|
545
|
-
end
|
530
|
+
return unless @children.mold
|
531
|
+
|
532
|
+
# We don't shutdown any outdated worker if any worker is already being spawned
|
533
|
+
# or a worker is exiting. Workers are only reforked one by one to minimize the
|
534
|
+
# impact on capacity.
|
535
|
+
# In the future we may want to use a dynamic limit, e.g. 10% of workers may be down at
|
536
|
+
# a time.
|
537
|
+
return if @children.pending_workers?
|
538
|
+
return if @children.workers.any?(&:exiting?)
|
539
|
+
|
540
|
+
if outdated_worker = @children.workers.find { |w| w.generation < @children.mold.generation }
|
541
|
+
logger.info("worker=#{outdated_worker.nr} pid=#{outdated_worker.pid} restarting")
|
542
|
+
outdated_worker.soft_kill(:QUIT)
|
546
543
|
end
|
547
544
|
end
|
548
545
|
|
@@ -667,6 +664,7 @@ module Pitchfork
|
|
667
664
|
|
668
665
|
def init_mold_process(worker)
|
669
666
|
proc_name "mold (gen: #{worker.generation})"
|
667
|
+
after_promotion.call(self, worker)
|
670
668
|
readers = [worker]
|
671
669
|
trap(:QUIT) { nuke_listeners!(readers) }
|
672
670
|
readers
|
@@ -702,6 +700,12 @@ module Pitchfork
|
|
702
700
|
client = false if client == :wait_readable
|
703
701
|
if client
|
704
702
|
case client
|
703
|
+
when Message::PromoteWorker
|
704
|
+
if @promotion_lock.try_lock
|
705
|
+
logger.info("Refork asked by master, promoting ourselves")
|
706
|
+
worker.tick = time_now.to_i
|
707
|
+
return worker.promoted!
|
708
|
+
end
|
705
709
|
when Message
|
706
710
|
worker.update(client)
|
707
711
|
else
|
@@ -710,11 +714,21 @@ module Pitchfork
|
|
710
714
|
end
|
711
715
|
worker.tick = time_now.to_i
|
712
716
|
end
|
713
|
-
return if worker.mold? # We've been promoted we can exit the loop
|
714
717
|
end
|
715
718
|
|
716
719
|
# timeout so we can .tick and keep parent from SIGKILL-ing us
|
717
720
|
worker.tick = time_now.to_i
|
721
|
+
if @refork_condition && !worker.outdated?
|
722
|
+
if @refork_condition.met?(worker, logger)
|
723
|
+
if @promotion_lock.try_lock
|
724
|
+
logger.info("Refork condition met, promoting ourselves")
|
725
|
+
return worker.promote! # We've been promoted we can exit the loop
|
726
|
+
else
|
727
|
+
# TODO: if we couldn't acquire the lock, we should backoff the refork_condition to avoid hammering the lock
|
728
|
+
end
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
718
732
|
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
719
733
|
rescue => e
|
720
734
|
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
@@ -724,10 +738,9 @@ module Pitchfork
|
|
724
738
|
def mold_loop(mold)
|
725
739
|
readers = init_mold_process(mold)
|
726
740
|
waiter = prep_readers(readers)
|
727
|
-
mold.
|
728
|
-
|
741
|
+
mold.declare_promotion(@control_socket[1])
|
742
|
+
@promotion_lock.unlock
|
729
743
|
ready = readers.dup
|
730
|
-
# TODO: mold ready callback?
|
731
744
|
|
732
745
|
begin
|
733
746
|
mold.tick = time_now.to_i
|
@@ -739,7 +752,11 @@ module Pitchfork
|
|
739
752
|
when false
|
740
753
|
# no message, keep looping
|
741
754
|
when Message::SpawnWorker
|
742
|
-
|
755
|
+
begin
|
756
|
+
spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
|
757
|
+
rescue => error
|
758
|
+
raise BootFailure, error.message
|
759
|
+
end
|
743
760
|
else
|
744
761
|
logger.error("Unexpected mold message #{message.inspect}")
|
745
762
|
end
|
@@ -749,7 +766,7 @@ module Pitchfork
|
|
749
766
|
mold.tick = time_now.to_i
|
750
767
|
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
751
768
|
rescue => e
|
752
|
-
Pitchfork.log_error(@logger, "
|
769
|
+
Pitchfork.log_error(@logger, "mold loop error", e) if readers[0]
|
753
770
|
end while readers[0]
|
754
771
|
end
|
755
772
|
|
@@ -7,9 +7,9 @@ module Pitchfork
|
|
7
7
|
@limits = request_counts
|
8
8
|
end
|
9
9
|
|
10
|
-
def met?(
|
11
|
-
if limit = @limits
|
12
|
-
if worker
|
10
|
+
def met?(worker, logger)
|
11
|
+
if limit = @limits.fetch(worker.generation) { @limits.last }
|
12
|
+
if worker.requests_count >= limit
|
13
13
|
logger.info("worker=#{worker.nr} pid=#{worker.pid} processed #{worker.requests_count} requests, triggering a refork")
|
14
14
|
return true
|
15
15
|
end
|