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.

@@ -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 = Qnil;
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 = rb_hash_aset(hp->env, f, v);
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
- hp->cont = Qnil;
305
+ RB_OBJ_WRITE(self, &hp->cont, Qnil);
306
306
  } else {
307
307
  rb_str_buf_cat(e, ",", 1);
308
- hp->cont = rb_str_buf_append(e, v);
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
- "pitchfork_http",
489
- { hp_mark, RUBY_TYPED_DEFAULT_FREE, hp_memsize, /* reserved */ },
490
- /* parent, data, [ flags ] */
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 = rb_str_new(NULL, 0);
622
- hp->env = rb_hash_new();
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->env = env;
760
- hp->buf = buf;
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 = dst;
889
- hp->buf = src;
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 = src;
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
- server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
35
- },
36
- :before_fork => lambda { |server, worker|
37
- server.logger.info("worker=#{worker.nr} gen=#{worker.generation} spawning...")
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
- 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
- },
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
- server.logger.info("worker=#{worker.nr} ready")
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
- :before_fork, :after_fork,
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, :mold_selector
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
- @default_middleware = true
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] ? options[:listeners].dup : []
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
- spawn_missing_workers
135
- # We could just return here as we'd register them later in #join.
136
- # However a good part of the test suite assumes #start only return
137
- # once all initial workers are spawned.
138
- wait_for_pending_workers
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
- automatically_refork_workers if REFORKING_AVAILABLE
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
- begin
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 while true
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.refresh
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("The mold select didn't return a candidate")
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
- before_fork.call(self, worker)
445
+ logger.info("worker=#{worker.nr} gen=#{worker.generation} spawning...")
439
446
 
440
- pid = fork do
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 = fork do
471
- after_fork_internal
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 !@children.mold || !@children.mold.spawn_worker(worker)
488
- # If there's no mold, or the mold was somehow unreachable
489
- # we fallback to spawning the missing workers ourselves.
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
- break
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 automatically_refork_workers
528
+ def restart_outdated_workers
519
529
  # If we're already in the middle of forking a new generation, we just continue
520
- if @children.mold
521
- # We don't shutdown any outdated worker if any worker is already being spawned
522
- # or a worker is exiting. Workers are only reforked one by one to minimize the
523
- # impact on capacity.
524
- # In the future we may want to use a dynamic limit, e.g. 10% of workers may be down at
525
- # a time.
526
- return if @children.pending_workers?
527
- return if @children.workers.any?(&:exiting?)
528
-
529
- if outdated_worker = @children.workers.find { |w| w.generation < @children.mold.generation }
530
- logger.info("worker=#{outdated_worker.nr} pid=#{outdated_worker.pid} restarting")
531
- outdated_worker.soft_kill(:QUIT)
532
- return # That's all folks
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.acknowlege_promotion(@control_socket[1])
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
- spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
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, "listen loop error", e) if readers[0]
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?(children, logger)
11
- if limit = @limits[children.last_generation]
12
- if worker = children.fresh_workers.find { |w| w.requests_count >= limit }
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end