pitchfork 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3a7c87f91b13b474f5c204a251c380cd091190f3ecfca8ac3107d697e883f12
4
- data.tar.gz: '00116558f20d20e4aad0cda6a1866fc7104e3712df27b1668470c52346812258'
3
+ metadata.gz: 05372d35dd4784eb22e204b614b1c7add13fd0e298495543c5395e2f03b42073
4
+ data.tar.gz: 14f241efeb95774f6de6e9fc8926815eb7f10c25965a09a0700f77dae75dbb92
5
5
  SHA512:
6
- metadata.gz: 2b51ac98fdc2fc2d83a72c6a55c5e26e42996b43bb6e33815e8958c6f5c729f11b17676b9b8bdb098d75b53ebac5bc98114f2bfa864550b3111162824ae6597f
7
- data.tar.gz: 5bbf64cf105b20930fd6627001abfb610c9f87ad8510b35a7ab1fcb5c5ccd5437eec011eb6253aefdf87f7d7bc353ac2abbaeb30ac6afad91ddd48fbe81f475c
6
+ metadata.gz: f26ebeeb3bc9f7533d25cc41b29a317fb8a80ce108a1859657adfa80b9eb8f88812cfe85dfecf0fbd8093b6e91f45c6ce51ab7b9a15864d96b9d0f5f77835786
7
+ data.tar.gz: 8d1f09bb24226f2c89b2db2d549c28508a9f2ea716a1612fcc3367d27445657b9d9da281e623af696dc803acd649847a724366306caf672dbc523a3e7495579b
@@ -10,7 +10,6 @@ jobs:
10
10
  fail-fast: false
11
11
  matrix:
12
12
  os: ["ubuntu-latest"]
13
- redis: ["6.2"]
14
13
  ruby: ["3.2", "3.1", "3.0", "2.7", "2.6"]
15
14
  runs-on: ubuntu-latest
16
15
  steps:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.7.0
4
+
5
+ - Set nicer `proctile` to better see the state of the process tree at a glance.
6
+ - Pass the last request env to `after_request_complete` callback.
7
+ - Fix the slow rollout of workers on a new generation.
8
+ - Expose `Pitchfork::Info.fork_safe?` and `Pitchfork::Info.no_longer_fork_safe!`.
9
+
3
10
  # 0.6.0
4
11
 
5
12
  - Expose `Pitchfork::Info.workers_count` and `.live_workers_count` to be consumed by application health checks.
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
1
  FROM ruby:3.2
2
- RUN apt-get update -y && apt-get install -y ragel socat netcat smem apache2-utils
2
+ RUN apt-get update -y && apt-get install -y ragel socat netcat-traditional smem apache2-utils
3
3
  WORKDIR /app
4
4
  CMD [ "bash" ]
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pitchfork (0.6.0)
4
+ pitchfork (0.7.0)
5
5
  rack (>= 2.0)
6
6
  raindrops (~> 0.7)
7
7
 
@@ -9,8 +9,8 @@ GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
11
  minitest (5.15.0)
12
- nio4r (2.5.8)
13
- puma (6.1.1)
12
+ nio4r (2.5.9)
13
+ puma (6.3.0)
14
14
  nio4r (~> 2.0)
15
15
  rack (3.0.8)
16
16
  raindrops (0.20.1)
@@ -371,7 +371,7 @@ Called in the worker processes after a request has completed.
371
371
  Can be used for out of band work, or to exit unhealthy workers.
372
372
 
373
373
  ```ruby
374
- after_request_complete do |server, worker|
374
+ after_request_complete do |server, worker, env|
375
375
  if something_wrong?
376
376
  exit
377
377
  end
data/docs/FORK_SAFETY.md CHANGED
@@ -76,10 +76,12 @@ impact of discovering such bug.
76
76
 
77
77
  ## Known Incompatible Gems
78
78
 
79
- - [The `grpc` isn't fork safe](https://github.com/grpc/grpc/issues/8798) and doesn't provide any before or after fork callback to re-establish connection.
80
- It can only be used in forking environment if the client is never used in the parent before fork.
81
- If you application uses `grpc`, you shouldn't enable reforking.
82
- But frankly, that gem is such a tire fire, you shouldn't use it regardless.
83
- If you really have to consume a gRPC API, you can consider `grpc_kit` as a replacement.
79
+ - The `grpc` isn't fork safe by default, but starting from version `1.57.0`, it does provide an experimental
80
+ fork safe option that requires setting an environment variable before loading the library, and calling
81
+ `GRPC.prefork`, `GRPC.postfork_parent` and `GRPC.postfork_child` around fork calls.
82
+ (https://github.com/grpc/grpc/pull/33430)
84
83
 
85
- No other gem is known to be incompatible, but if you find one please open an issue to add it to the list.
84
+ - The `ruby-vips` gem binds the `libvips` image processing library that isn't fork safe.
85
+ (https://github.com/libvips/libvips/discussions/3577)
86
+
87
+ No other gem is known to be incompatible for now, but if you find one please open an issue to add it to the list.
@@ -158,7 +158,7 @@ module Pitchfork
158
158
  end
159
159
 
160
160
  def after_request_complete(*args, &block)
161
- set_hook(:after_request_complete, block_given? ? block : args[0])
161
+ set_hook(:after_request_complete, block_given? ? block : args[0], 3)
162
162
  end
163
163
 
164
164
  def timeout(seconds, cleanup: 2)
@@ -19,6 +19,10 @@ module Pitchfork
19
19
  nil
20
20
  end
21
21
 
22
+ def to_io
23
+ @file
24
+ end
25
+
22
26
  def unlink
23
27
  File.unlink(@file.path)
24
28
  rescue Errno::ENOENT
@@ -91,7 +91,7 @@ module Pitchfork
91
91
  # in new projects
92
92
  LISTENERS = []
93
93
 
94
- NOOP = '.'
94
+ NOOP = '.'.freeze
95
95
 
96
96
  # :startdoc:
97
97
  # This Hash is considered a stable interface and changing its contents
@@ -129,6 +129,7 @@ module Pitchfork
129
129
  @respawn = false
130
130
  @last_check = Pitchfork.time_now
131
131
  @promotion_lock = Flock.new("pitchfork-promotion")
132
+ Info.keep_io(@promotion_lock)
132
133
 
133
134
  options = options.dup
134
135
  @ready_pipe = options.delete(:ready_pipe)
@@ -137,6 +138,8 @@ module Pitchfork
137
138
  self.config = Pitchfork::Configurator.new(options)
138
139
  self.listener_opts = {}
139
140
 
141
+ proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
142
+
140
143
  # We use @control_socket differently in the master and worker processes:
141
144
  #
142
145
  # * The master process never closes or reinitializes this once
@@ -180,6 +183,7 @@ module Pitchfork
180
183
  # It's also used by newly spawned children to send their soft_signal pipe
181
184
  # to the master when they are spawned.
182
185
  @control_socket.replace(Pitchfork.socketpair)
186
+ Info.keep_ios(@control_socket)
183
187
  @master_pid = $$
184
188
 
185
189
  # setup signal handlers before writing pid file in case people get
@@ -264,6 +268,7 @@ module Pitchfork
264
268
  io = server_cast(io)
265
269
  end
266
270
  logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
271
+ Info.keep_io(io)
267
272
  LISTENERS << io
268
273
  io
269
274
  rescue Errno::EADDRINUSE => err
@@ -287,7 +292,8 @@ module Pitchfork
287
292
  def join
288
293
  @respawn = true
289
294
 
290
- proc_name 'master'
295
+ proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
296
+
291
297
  logger.info "master process ready" # test_exec.rb relies on this message
292
298
  if @ready_pipe
293
299
  begin
@@ -347,7 +353,11 @@ module Pitchfork
347
353
  stop(false)
348
354
  return StopIteration
349
355
  when :USR2 # trigger a promotion
350
- trigger_refork
356
+ if @respawn
357
+ trigger_refork
358
+ else
359
+ logger.error "Can't trigger a refork as the server is shutting down"
360
+ end
351
361
  when :TTIN
352
362
  @respawn = true
353
363
  self.worker_processes += 1
@@ -376,6 +386,7 @@ module Pitchfork
376
386
 
377
387
  # Terminates all workers, but does not exit master process
378
388
  def stop(graceful = true)
389
+ proc_name role: 'monitor', status: 'shutting down'
379
390
  @respawn = false
380
391
  SharedMemory.shutting_down!
381
392
  wait_for_pending_workers
@@ -396,6 +407,8 @@ module Pitchfork
396
407
  end
397
408
 
398
409
  def worker_exit(worker)
410
+ proc_name status: "exiting"
411
+
399
412
  if @before_worker_exit
400
413
  begin
401
414
  @before_worker_exit.call(self, worker)
@@ -514,7 +527,7 @@ module Pitchfork
514
527
 
515
528
  def trigger_refork
516
529
  unless REFORKING_AVAILABLE
517
- logger.error("This system doesn't support PR_SET_CHILD_SUBREAPER, can't promote a worker")
530
+ logger.error("This system doesn't support PR_SET_CHILD_SUBREAPER, can't refork")
518
531
  end
519
532
 
520
533
  unless @children.pending_promotion?
@@ -523,7 +536,6 @@ module Pitchfork
523
536
  else
524
537
  logger.error("No children at all???")
525
538
  end
526
- else
527
539
  end
528
540
  end
529
541
 
@@ -622,8 +634,13 @@ module Pitchfork
622
634
  if workers_to_restart > 0
623
635
  outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
624
636
  outdated_workers.each do |worker|
625
- logger.info("worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation} restarting")
626
- worker.soft_kill(:TERM)
637
+ if worker.soft_kill(:TERM)
638
+ logger.info("Sent SIGTERM to worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation}")
639
+ workers_to_restart -= 1
640
+ else
641
+ logger.info("Failed to send SIGTERM to worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation}")
642
+ end
643
+ break if workers_to_restart <= 0
627
644
  end
628
645
  end
629
646
  end
@@ -678,6 +695,9 @@ module Pitchfork
678
695
  env = nil
679
696
  @request = Pitchfork::HttpParser.new
680
697
  env = @request.read(client)
698
+
699
+ proc_name status: "processing: #{env["PATH_INFO"]}"
700
+
681
701
  timeout_handler.rack_env = env
682
702
  env["pitchfork.timeout"] = timeout_handler
683
703
 
@@ -692,12 +712,12 @@ module Pitchfork
692
712
  status, headers, body = @app.call(env)
693
713
 
694
714
  begin
695
- return if @request.hijacked?
715
+ return env if @request.hijacked?
696
716
 
697
717
  if 100 == status.to_i
698
718
  e100_response_write(client, env)
699
719
  status, headers, body = @app.call(env)
700
- return if @request.hijacked?
720
+ return env if @request.hijacked?
701
721
  end
702
722
  @request.headers? or headers = nil
703
723
  http_response_write(client, status, headers, body, @request)
@@ -712,11 +732,14 @@ module Pitchfork
712
732
  end
713
733
  client.close # flush and uncork socket immediately, no keepalive
714
734
  end
735
+ env
715
736
  rescue => e
716
737
  handle_error(client, e)
738
+ env
717
739
  ensure
718
740
  env["rack.after_reply"].each(&:call) if env
719
741
  timeout_handler.finished
742
+ env
720
743
  end
721
744
 
722
745
  def nuke_listeners!(readers)
@@ -731,6 +754,7 @@ module Pitchfork
731
754
  # traps for USR2, and HUP may be set in the after_fork Proc
732
755
  # by the user.
733
756
  def init_worker_process(worker)
757
+ proc_name role: "(gen:#{worker.generation}) worker[#{worker.nr}]", status: "init"
734
758
  worker.reset
735
759
  worker.register_to_master(@control_socket[1])
736
760
  # we'll re-trap :QUIT and :TERM later for graceful shutdown iff we accept clients
@@ -740,7 +764,6 @@ module Pitchfork
740
764
  (@queue_sigs - exit_sigs).each { |sig| trap(sig, nil) }
741
765
  trap(:CHLD, 'DEFAULT')
742
766
  @sig_queue.clear
743
- proc_name "(gen:#{worker.generation}) worker[#{worker.nr}]"
744
767
  @children = nil
745
768
 
746
769
  after_worker_fork.call(self, worker) # can drop perms and create listeners
@@ -756,7 +779,7 @@ module Pitchfork
756
779
  end
757
780
 
758
781
  def init_mold_process(mold)
759
- proc_name "(gen: #{mold.generation}) mold"
782
+ proc_name role: "(gen:#{mold.generation}) mold", status: "ready"
760
783
  after_mold_fork.call(self, mold)
761
784
  readers = [mold]
762
785
  trap(:QUIT) { nuke_listeners!(readers) }
@@ -785,6 +808,8 @@ module Pitchfork
785
808
  ready = readers.dup
786
809
  @after_worker_ready.call(self, worker)
787
810
 
811
+ proc_name status: "ready"
812
+
788
813
  while readers[0]
789
814
  begin
790
815
  worker.update_deadline(@timeout)
@@ -796,12 +821,16 @@ module Pitchfork
796
821
  if client
797
822
  case client
798
823
  when Message::PromoteWorker
799
- spawn_mold(worker.generation)
824
+ if Info.fork_safe?
825
+ spawn_mold(worker.generation)
826
+ else
827
+ logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
828
+ end
800
829
  when Message
801
830
  worker.update(client)
802
831
  else
803
- process_client(client, prepare_timeout(worker))
804
- @after_request_complete&.call(self, worker)
832
+ request_env = process_client(client, prepare_timeout(worker))
833
+ @after_request_complete&.call(self, worker, request_env)
805
834
  worker.increment_requests_count
806
835
  end
807
836
  worker.update_deadline(@timeout)
@@ -811,7 +840,7 @@ module Pitchfork
811
840
  # timeout so we can update .deadline and keep parent from SIGKILL-ing us
812
841
  worker.update_deadline(@timeout)
813
842
 
814
- if @refork_condition && !worker.outdated?
843
+ if @refork_condition && Info.fork_safe? && !worker.outdated?
815
844
  if @refork_condition.met?(worker, logger)
816
845
  if spawn_mold(worker.generation)
817
846
  logger.info("Refork condition met, promoting ourselves")
@@ -820,6 +849,7 @@ module Pitchfork
820
849
  end
821
850
  end
822
851
 
852
+ proc_name status: "waiting"
823
853
  waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
824
854
  rescue => e
825
855
  Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
@@ -911,6 +941,8 @@ module Pitchfork
911
941
  def build_app!
912
942
  return unless app.respond_to?(:arity)
913
943
 
944
+ proc_name status: "booting"
945
+
914
946
  self.app = case app.arity
915
947
  when 0
916
948
  app.call
@@ -921,9 +953,11 @@ module Pitchfork
921
953
  end
922
954
  end
923
955
 
924
- def proc_name(tag)
925
- $0 = ([ File.basename(START_CTX[0]), tag
926
- ]).concat(START_CTX[:argv]).join(' ')
956
+ def proc_name(role: nil, status: nil)
957
+ @proctitle_role = role if role
958
+ @proctitle_status = status if status
959
+
960
+ Process.setproctitle("#{File.basename(START_CTX[0])} #{@proctitle_role} - #{@proctitle_status}")
927
961
  end
928
962
 
929
963
  def bind_listeners!
@@ -5,10 +5,58 @@ require 'pitchfork/shared_memory'
5
5
  module Pitchfork
6
6
  module Info
7
7
  @workers_count = 0
8
+ @fork_safe = true
9
+ @kept_ios = ObjectSpace::WeakMap.new
8
10
 
9
11
  class << self
10
12
  attr_accessor :workers_count
11
13
 
14
+ def keep_io(io)
15
+ raise ArgumentError, "#{io.inspect} doesn't respond to :to_io" unless io.respond_to?(:to_io)
16
+ @kept_ios[io] = io
17
+ io
18
+ end
19
+
20
+ def keep_ios(ios)
21
+ ios.each { |io| keep_io(io) }
22
+ end
23
+
24
+ def close_all_ios!
25
+ ignored_ios = [$stdin, $stdout, $stderr]
26
+
27
+ @kept_ios.each_value do |io_like|
28
+ ignored_ios << (io_like.is_a?(IO) ? io_like : io_like.to_io)
29
+ end
30
+
31
+ ObjectSpace.each_object(IO) do |io|
32
+ closed = begin
33
+ io.closed?
34
+ rescue IOError
35
+ true
36
+ end
37
+
38
+ if !closed && io.autoclose? && !ignored_ios.include?(io)
39
+ if io.is_a?(TCPSocket)
40
+ # If we inherited a TCP Socket, calling #close directly could send FIN or RST.
41
+ # So we first reopen /dev/null to avoid that.
42
+ io.reopen(File::NULL)
43
+ end
44
+ begin
45
+ io.close
46
+ rescue Errno::EBADF
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def fork_safe?
53
+ @fork_safe
54
+ end
55
+
56
+ def no_longer_fork_safe!
57
+ @fork_safe = false
58
+ end
59
+
12
60
  def live_workers_count
13
61
  now = Pitchfork.time_now(true)
14
62
  (0...workers_count).count do |nr|
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end
@@ -198,7 +198,7 @@ module Pitchfork
198
198
  end
199
199
 
200
200
  def create_socketpair!
201
- @to_io, @master = Pitchfork.socketpair
201
+ @to_io, @master = Info.keep_ios(Pitchfork.socketpair)
202
202
  end
203
203
 
204
204
  def after_fork_in_child
@@ -208,11 +208,14 @@ module Pitchfork
208
208
  private
209
209
 
210
210
  def pipe=(socket)
211
+ raise ArgumentError, "pipe can't be nil" unless socket
212
+ Info.keep_io(socket)
211
213
  @master = MessageSocket.new(socket)
212
214
  end
213
215
 
214
216
  def send_message_nonblock(message)
215
217
  success = false
218
+ return false unless @master
216
219
  begin
217
220
  case @master.sendmsg_nonblock(message, exception: false)
218
221
  when :wait_writable
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.6.0
4
+ version: 0.7.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: 2023-07-18 00:00:00.000000000 Z
11
+ date: 2023-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: raindrops