pitchfork 0.1.1 → 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 +13 -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_response.rb +13 -6
- data/lib/pitchfork/http_server.rb +91 -85
- 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
@@ -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
|
-
# 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
|
530
|
+
return unless @children.mold
|
535
531
|
|
536
|
-
#
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
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
|
|
@@ -572,22 +569,11 @@ module Pitchfork
|
|
572
569
|
end
|
573
570
|
|
574
571
|
def e103_response_write(client, headers)
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
headers.each_pair do |k, vs|
|
582
|
-
next if !vs || vs.empty?
|
583
|
-
values = vs.to_s.split("\n".freeze)
|
584
|
-
values.each do |v|
|
585
|
-
response << "#{k}: #{v}\r\n"
|
586
|
-
end
|
587
|
-
end
|
588
|
-
response << "\r\n".freeze
|
589
|
-
response << "HTTP/1.1 ".freeze if @request.response_start_sent
|
590
|
-
client.write(response)
|
572
|
+
rss = @request.response_start_sent
|
573
|
+
buf = rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n"
|
574
|
+
headers.each { |key, value| append_header(buf, key, value) }
|
575
|
+
buf << (rss ? "\r\nHTTP/1.1 ".freeze : "\r\n".freeze)
|
576
|
+
client.write(buf)
|
591
577
|
end
|
592
578
|
|
593
579
|
def e100_response_write(client, env)
|
@@ -678,6 +664,7 @@ module Pitchfork
|
|
678
664
|
|
679
665
|
def init_mold_process(worker)
|
680
666
|
proc_name "mold (gen: #{worker.generation})"
|
667
|
+
after_promotion.call(self, worker)
|
681
668
|
readers = [worker]
|
682
669
|
trap(:QUIT) { nuke_listeners!(readers) }
|
683
670
|
readers
|
@@ -713,6 +700,12 @@ module Pitchfork
|
|
713
700
|
client = false if client == :wait_readable
|
714
701
|
if client
|
715
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
|
716
709
|
when Message
|
717
710
|
worker.update(client)
|
718
711
|
else
|
@@ -721,11 +714,21 @@ module Pitchfork
|
|
721
714
|
end
|
722
715
|
worker.tick = time_now.to_i
|
723
716
|
end
|
724
|
-
return if worker.mold? # We've been promoted we can exit the loop
|
725
717
|
end
|
726
718
|
|
727
719
|
# timeout so we can .tick and keep parent from SIGKILL-ing us
|
728
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
|
+
|
729
732
|
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
730
733
|
rescue => e
|
731
734
|
Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
|
@@ -735,10 +738,9 @@ module Pitchfork
|
|
735
738
|
def mold_loop(mold)
|
736
739
|
readers = init_mold_process(mold)
|
737
740
|
waiter = prep_readers(readers)
|
738
|
-
mold.
|
739
|
-
|
741
|
+
mold.declare_promotion(@control_socket[1])
|
742
|
+
@promotion_lock.unlock
|
740
743
|
ready = readers.dup
|
741
|
-
# TODO: mold ready callback?
|
742
744
|
|
743
745
|
begin
|
744
746
|
mold.tick = time_now.to_i
|
@@ -750,7 +752,11 @@ module Pitchfork
|
|
750
752
|
when false
|
751
753
|
# no message, keep looping
|
752
754
|
when Message::SpawnWorker
|
753
|
-
|
755
|
+
begin
|
756
|
+
spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
|
757
|
+
rescue => error
|
758
|
+
raise BootFailure, error.message
|
759
|
+
end
|
754
760
|
else
|
755
761
|
logger.error("Unexpected mold message #{message.inspect}")
|
756
762
|
end
|
@@ -760,7 +766,7 @@ module Pitchfork
|
|
760
766
|
mold.tick = time_now.to_i
|
761
767
|
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
762
768
|
rescue => e
|
763
|
-
Pitchfork.log_error(@logger, "
|
769
|
+
Pitchfork.log_error(@logger, "mold loop error", e) if readers[0]
|
764
770
|
end while readers[0]
|
765
771
|
end
|
766
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
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
@@ -7,14 +7,13 @@ module Pitchfork
|
|
7
7
|
# releases of pitchfork. Knowledge of this class is generally not
|
8
8
|
# not needed for most users of pitchfork.
|
9
9
|
#
|
10
|
-
# Some users may want to access it in the
|
10
|
+
# Some users may want to access it in the after_promotion/after_fork hooks.
|
11
11
|
# See the Pitchfork::Configurator RDoc for examples.
|
12
12
|
class Worker
|
13
13
|
# :stopdoc:
|
14
14
|
EXIT_SIGNALS = [:QUIT, :TERM]
|
15
|
-
@generation = 0
|
16
15
|
attr_accessor :nr, :pid, :generation
|
17
|
-
attr_reader :master
|
16
|
+
attr_reader :master, :requests_count
|
18
17
|
|
19
18
|
def initialize(nr, pid: nil, generation: 0)
|
20
19
|
@nr = nr
|
@@ -23,6 +22,7 @@ module Pitchfork
|
|
23
22
|
@mold = false
|
24
23
|
@to_io = @master = nil
|
25
24
|
@exiting = false
|
25
|
+
@requests_count = 0
|
26
26
|
if nr
|
27
27
|
build_raindrops(nr)
|
28
28
|
else
|
@@ -42,13 +42,17 @@ module Pitchfork
|
|
42
42
|
@exiting
|
43
43
|
end
|
44
44
|
|
45
|
+
def outdated?
|
46
|
+
CURRENT_GENERATION_DROP[0] > @generation
|
47
|
+
end
|
48
|
+
|
45
49
|
def update(message)
|
46
50
|
message.class.members.each do |member|
|
47
51
|
send("#{member}=", message.public_send(member))
|
48
52
|
end
|
49
53
|
|
50
54
|
case message
|
51
|
-
when Message::WorkerPromoted
|
55
|
+
when Message::WorkerPromoted
|
52
56
|
promoted!
|
53
57
|
end
|
54
58
|
end
|
@@ -60,9 +64,10 @@ module Pitchfork
|
|
60
64
|
@master.close
|
61
65
|
end
|
62
66
|
|
63
|
-
def
|
67
|
+
def declare_promotion(control_socket)
|
64
68
|
message = Message::WorkerPromoted.new(@nr, Process.pid, generation)
|
65
69
|
control_socket.sendmsg(message)
|
70
|
+
CURRENT_GENERATION_DROP[0] = @generation
|
66
71
|
end
|
67
72
|
|
68
73
|
def promote(generation)
|
@@ -73,11 +78,17 @@ module Pitchfork
|
|
73
78
|
send_message_nonblock(Message::SpawnWorker.new(new_worker.nr))
|
74
79
|
end
|
75
80
|
|
81
|
+
def promote!
|
82
|
+
@generation += 1
|
83
|
+
promoted!
|
84
|
+
end
|
85
|
+
|
76
86
|
def promoted!
|
77
87
|
@mold = true
|
78
88
|
@nr = nil
|
79
89
|
@drop_offset = 0
|
80
90
|
@tick_drop = MOLD_DROP
|
91
|
+
self
|
81
92
|
end
|
82
93
|
|
83
94
|
def mold?
|
@@ -169,15 +180,11 @@ module Pitchfork
|
|
169
180
|
end
|
170
181
|
|
171
182
|
def reset
|
172
|
-
@
|
173
|
-
end
|
174
|
-
|
175
|
-
def requests_count
|
176
|
-
@requests_drop[@drop_offset]
|
183
|
+
@requests_count = 0
|
177
184
|
end
|
178
185
|
|
179
|
-
def increment_requests_count
|
180
|
-
@
|
186
|
+
def increment_requests_count(by = 1)
|
187
|
+
@requests_count += by
|
181
188
|
end
|
182
189
|
|
183
190
|
# called in both the master (reaping worker) and worker (SIGQUIT handler)
|
@@ -215,9 +222,9 @@ module Pitchfork
|
|
215
222
|
end
|
216
223
|
|
217
224
|
MOLD_DROP = Raindrops.new(1)
|
225
|
+
CURRENT_GENERATION_DROP = Raindrops.new(1)
|
218
226
|
PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
|
219
227
|
TICK_DROPS = []
|
220
|
-
REQUEST_DROPS = []
|
221
228
|
|
222
229
|
class << self
|
223
230
|
# Since workers are created from another process, we have to
|
@@ -228,7 +235,6 @@ module Pitchfork
|
|
228
235
|
def preallocate_drops(workers_count)
|
229
236
|
0.upto(workers_count / PER_DROP) do |i|
|
230
237
|
TICK_DROPS[i] = Raindrops.new(PER_DROP)
|
231
|
-
REQUEST_DROPS[i] = Raindrops.new(PER_DROP)
|
232
238
|
end
|
233
239
|
end
|
234
240
|
end
|
@@ -237,8 +243,7 @@ module Pitchfork
|
|
237
243
|
drop_index = drop_nr / PER_DROP
|
238
244
|
@drop_offset = drop_nr % PER_DROP
|
239
245
|
@tick_drop = TICK_DROPS[drop_index] ||= Raindrops.new(PER_DROP)
|
240
|
-
@
|
241
|
-
@tick_drop[@drop_offset] = @requests_drop[@drop_offset] = 0
|
246
|
+
@tick_drop[@drop_offset] = 0
|
242
247
|
end
|
243
248
|
end
|
244
249
|
end
|
data/lib/pitchfork.rb
CHANGED
@@ -57,40 +57,15 @@ module Pitchfork
|
|
57
57
|
Object.const_get(File.basename(ru, '.rb').capitalize)
|
58
58
|
end
|
59
59
|
|
60
|
-
if $DEBUG
|
61
|
-
require 'pp'
|
62
|
-
pp({ :inner_app => inner_app })
|
63
|
-
end
|
64
|
-
|
65
|
-
return inner_app unless server.default_middleware
|
66
|
-
|
67
|
-
middleware = { # order matters
|
68
|
-
ContentLength: nil,
|
69
|
-
Chunked: nil,
|
70
|
-
CommonLogger: [ $stderr ],
|
71
|
-
ShowExceptions: nil,
|
72
|
-
Lint: nil,
|
73
|
-
TempfileReaper: nil,
|
74
|
-
}
|
75
|
-
|
76
|
-
# return value, matches rackup defaults based on env
|
77
|
-
# Pitchfork does not support persistent connections, but Rainbows!
|
78
|
-
# and Zbatery both do. Users accustomed to the Rack::Server default
|
79
|
-
# middlewares will need ContentLength/Chunked middlewares.
|
80
60
|
case ENV["RACK_ENV"]
|
81
61
|
when "development"
|
82
|
-
|
83
|
-
|
84
|
-
|
62
|
+
Rack::Builder.new do
|
63
|
+
use(Rack::Lint)
|
64
|
+
run inner_app
|
65
|
+
end.to_app
|
85
66
|
else
|
86
|
-
|
67
|
+
inner_app
|
87
68
|
end
|
88
|
-
Rack::Builder.new do
|
89
|
-
middleware.each do |m, args|
|
90
|
-
use(Rack.const_get(m), *args) if Rack.const_defined?(m)
|
91
|
-
end
|
92
|
-
run inner_app
|
93
|
-
end.to_app
|
94
69
|
end
|
95
70
|
end
|
96
71
|
|
@@ -146,13 +121,32 @@ module Pitchfork
|
|
146
121
|
raise
|
147
122
|
end
|
148
123
|
end
|
124
|
+
|
125
|
+
def self.clean_fork(&block)
|
126
|
+
# We fork from a thread to start with a clean stack.
|
127
|
+
# If we didn't the base stack would grow after each refork
|
128
|
+
# putting an effective limit on the number of generations.
|
129
|
+
parent_thread = Thread.current
|
130
|
+
Thread.new do
|
131
|
+
current_thread = Thread.current
|
132
|
+
# We copy over any thread state it might have
|
133
|
+
parent_thread.keys.each do |key|
|
134
|
+
current_thread[key] = parent_thread[key]
|
135
|
+
end
|
136
|
+
parent_thread.thread_variables.each do |variable|
|
137
|
+
current_thread.thread_variable_set(variable, parent_thread.thread_variable_get(variable))
|
138
|
+
end
|
139
|
+
|
140
|
+
fork(&block)
|
141
|
+
end.value
|
142
|
+
end
|
149
143
|
# :startdoc:
|
150
144
|
end
|
151
145
|
# :enddoc:
|
152
146
|
|
153
147
|
%w(
|
154
148
|
const socket_helper stream_input tee_input mem_info children message http_parser
|
155
|
-
refork_condition
|
149
|
+
refork_condition configurator tmpio http_response worker http_server
|
156
150
|
).each do |s|
|
157
151
|
require_relative "pitchfork/#{s}"
|
158
152
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pitchfork
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: raindrops
|
@@ -96,13 +96,13 @@ files:
|
|
96
96
|
- lib/pitchfork/children.rb
|
97
97
|
- lib/pitchfork/configurator.rb
|
98
98
|
- lib/pitchfork/const.rb
|
99
|
+
- lib/pitchfork/flock.rb
|
99
100
|
- lib/pitchfork/http_parser.rb
|
100
101
|
- lib/pitchfork/http_response.rb
|
101
102
|
- lib/pitchfork/http_server.rb
|
102
103
|
- lib/pitchfork/launcher.rb
|
103
104
|
- lib/pitchfork/mem_info.rb
|
104
105
|
- lib/pitchfork/message.rb
|
105
|
-
- lib/pitchfork/mold_selector.rb
|
106
106
|
- lib/pitchfork/preread_input.rb
|
107
107
|
- lib/pitchfork/refork_condition.rb
|
108
108
|
- lib/pitchfork/select_waiter.rb
|
@@ -133,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
133
|
- !ruby/object:Gem::Version
|
134
134
|
version: '0'
|
135
135
|
requirements: []
|
136
|
-
rubygems_version: 3.
|
136
|
+
rubygems_version: 3.4.6
|
137
137
|
signing_key:
|
138
138
|
specification_version: 4
|
139
139
|
summary: Rack HTTP server for fast clients and Unix
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pitchfork
|
4
|
-
module MoldSelector
|
5
|
-
class LeastSharedMemory
|
6
|
-
def call(server)
|
7
|
-
workers = server.children.fresh_workers
|
8
|
-
if workers.empty?
|
9
|
-
server.logger.info("No current generation workers yet")
|
10
|
-
return
|
11
|
-
end
|
12
|
-
candidate = workers.shift
|
13
|
-
|
14
|
-
workers.each do |worker|
|
15
|
-
if worker.meminfo.shared_memory < candidate.meminfo.shared_memory
|
16
|
-
# We suppose that a worker with a lower amount of shared memory
|
17
|
-
# has warmed up more caches & such, hence is closer to stabilize
|
18
|
-
# making it a better candidate.
|
19
|
-
candidate = worker
|
20
|
-
end
|
21
|
-
end
|
22
|
-
parent_meminfo = server.children.mold&.meminfo || MemInfo.new(Process.pid)
|
23
|
-
cow_efficiency = candidate.meminfo.cow_efficiency(parent_meminfo)
|
24
|
-
server.logger.info("worker=#{candidate.nr} pid=#{candidate.pid} selected as new mold shared_memory_kb=#{candidate.meminfo.shared_memory} cow=#{cow_efficiency.round(1)}%")
|
25
|
-
candidate
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|