pitchfork 0.6.0 → 0.7.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 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