pitchfork 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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