pitchfork 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,6 +16,7 @@
16
16
  #include "child_subreaper.h"
17
17
 
18
18
  void init_pitchfork_httpdate(void);
19
+ void init_pitchfork_memory_page(VALUE);
19
20
 
20
21
  #define UH_FL_CHUNKED 0x1
21
22
  #define UH_FL_HASBODY 0x2
@@ -960,7 +961,7 @@ static VALUE HttpParser_rssget(VALUE self)
960
961
  assert(!NIL_P(var) && "missed global field"); \
961
962
  } while (0)
962
963
 
963
- void Init_pitchfork_http(void)
964
+ RUBY_FUNC_EXPORTED void Init_pitchfork_http(void)
964
965
  {
965
966
  VALUE mPitchfork;
966
967
 
@@ -1024,5 +1025,6 @@ void Init_pitchfork_http(void)
1024
1025
 
1025
1026
  init_epollexclusive(mPitchfork);
1026
1027
  init_child_subreaper(mPitchfork);
1028
+ init_pitchfork_memory_page(mPitchfork);
1027
1029
  }
1028
1030
  #undef SET_GLOBAL
@@ -4,29 +4,32 @@
4
4
  module Pitchfork
5
5
  # This class keep tracks of the state of all the master children.
6
6
  class Children
7
- attr_reader :mold
7
+ attr_reader :mold, :service
8
8
  attr_accessor :last_generation
9
9
 
10
10
  def initialize
11
11
  @last_generation = 0
12
- @children = {} # All children, including molds, indexed by PID.
12
+ @children = {} # All children, including molds and services, indexed by PID.
13
13
  @workers = {} # Workers indexed by their `nr`.
14
14
  @molds = {} # Molds, index by PID.
15
15
  @mold = nil # The latest mold, if any.
16
+ @service = nil
16
17
  @pending_workers = {} # Pending workers indexed by their `nr`.
17
18
  @pending_molds = {} # Worker promoted to mold, not yet acknowledged
18
19
  end
19
20
 
20
- def refresh
21
- @workers.each_value(&:refresh)
22
- @molds.each_value(&:refresh)
23
- end
24
-
25
21
  def register(child)
26
22
  # Children always start as workers, never molds, so we know they have a `#nr`.
23
+ unless child.nr
24
+ raise "[BUG] Trying to register a child without an `nr`: #{child.inspect}"
25
+ end
27
26
  @pending_workers[child.nr] = @workers[child.nr] = child
28
27
  end
29
28
 
29
+ def register_service(service)
30
+ @service = service
31
+ end
32
+
30
33
  def register_mold(mold)
31
34
  @pending_molds[mold.pid] = mold
32
35
  @children[mold.pid] = mold
@@ -44,6 +47,11 @@ module Pitchfork
44
47
  @pending_molds[mold.pid] = mold
45
48
  @children[mold.pid] = mold
46
49
  return mold
50
+ when Message::ServiceSpawned
51
+ service = @service
52
+ service.update(message)
53
+ @children[service.pid] = service
54
+ return service
47
55
  end
48
56
 
49
57
  child = @children[message.pid] || (message.nr && @workers[message.nr])
@@ -78,12 +86,17 @@ module Pitchfork
78
86
  @pending_molds.delete(child.pid)
79
87
  @molds.delete(child.pid)
80
88
  @workers.delete(child.nr)
89
+
81
90
  if @mold == child
82
91
  @pending_workers.reject! do |nr, worker|
83
92
  worker.generation == @mold.generation
84
93
  end
85
94
  @mold = nil
86
95
  end
96
+
97
+ if @service == child
98
+ @service = nil
99
+ end
87
100
  end
88
101
  child
89
102
  end
@@ -154,13 +167,5 @@ module Pitchfork
154
167
  def workers_count
155
168
  @workers.size
156
169
  end
157
-
158
- def total_pss
159
- total_pss = MemInfo.new(Process.pid).pss
160
- @children.each do |_, worker|
161
- total_pss += worker.meminfo.pss if worker.meminfo
162
- end
163
- total_pss
164
- end
165
170
  end
166
171
  end
@@ -52,6 +52,8 @@ module Pitchfork
52
52
  "repead unknown process (#{status.inspect})"
53
53
  elsif worker.mold?
54
54
  "mold pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
55
+ elsif worker.service?
56
+ "service pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
55
57
  else
56
58
  "worker=#{worker.nr rescue 'unknown'} pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
57
59
  end
@@ -75,6 +77,8 @@ module Pitchfork
75
77
  :check_client_connection => false,
76
78
  :rewindable_input => true,
77
79
  :client_body_buffer_size => Pitchfork::Const::MAX_BODY,
80
+ :before_service_worker_ready => nil,
81
+ :before_service_worker_exit => nil,
78
82
  }
79
83
  #:startdoc:
80
84
 
@@ -176,6 +180,14 @@ module Pitchfork
176
180
  set_hook(:after_request_complete, block_given? ? block : args[0], 3)
177
181
  end
178
182
 
183
+ def before_service_worker_ready(&block)
184
+ set_hook(:before_service_worker_ready, block, 2)
185
+ end
186
+
187
+ def before_service_worker_exit(&block)
188
+ set_hook(:before_service_worker_exit, block, 2)
189
+ end
190
+
179
191
  def timeout(seconds, cleanup: 2)
180
192
  soft_timeout = set_int(:soft_timeout, seconds, 3)
181
193
  cleanup_timeout = set_int(:cleanup_timeout, cleanup, 2)
@@ -27,7 +27,6 @@ module Pitchfork
27
27
  EMPTY_ARRAY = [].freeze
28
28
  @@input_class = Pitchfork::TeeInput
29
29
  @@check_client_connection = false
30
- @@tcpi_inspect_ok = Socket.const_defined?(:TCP_INFO)
31
30
 
32
31
  def self.input_class
33
32
  @@input_class
@@ -108,80 +107,26 @@ module Pitchfork
108
107
  env.include?('rack.hijack_io')
109
108
  end
110
109
 
111
- if Raindrops.const_defined?(:TCP_Info)
112
- TCPI = Raindrops::TCP_Info.allocate
113
-
110
+ if Socket.const_defined?(:TCP_INFO) # Linux
114
111
  def check_client_connection(socket) # :nodoc:
115
112
  if TCPSocket === socket
116
- # Raindrops::TCP_Info#get!, #state (reads struct tcp_info#tcpi_state)
117
- raise Errno::EPIPE, "client closed connection".freeze,
118
- EMPTY_ARRAY if closed_state?(TCPI.get!(socket).state)
119
- else
120
- write_http_header(socket)
121
- end
122
- end
123
-
124
- if Raindrops.const_defined?(:TCP)
125
- # raindrops 0.18.0+ supports FreeBSD + Linux using the same names
126
- # Evaluate these hash lookups at load time so we can
127
- # generate an opt_case_dispatch instruction
128
- eval <<-EOS
129
- def closed_state?(state) # :nodoc:
130
- case state
131
- when #{Raindrops::TCP[:ESTABLISHED]}
132
- false
133
- when #{Raindrops::TCP.values_at(
134
- :CLOSE_WAIT, :TIME_WAIT, :CLOSE, :LAST_ACK, :CLOSING).join(',')}
135
- true
136
- else
137
- false
138
- end
139
- end
140
- EOS
141
- else
142
- # raindrops before 0.18 only supported TCP_INFO under Linux
143
- def closed_state?(state) # :nodoc:
144
- case state
145
- when 1 # ESTABLISHED
146
- false
147
- when 8, 6, 7, 9, 11 # CLOSE_WAIT, TIME_WAIT, CLOSE, LAST_ACK, CLOSING
148
- true
149
- else
150
- false
113
+ begin
114
+ tcp_info = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO)
115
+ rescue IOError, SystemCallError
116
+ return write_http_header(socket)
151
117
  end
152
- end
153
- end
154
- else
155
118
 
156
- # Ruby 2.2+ can show struct tcp_info as a string Socket::Option#inspect.
157
- # Not that efficient, but probably still better than doing unnecessary
158
- # work after a client gives up.
159
- def check_client_connection(socket) # :nodoc:
160
- if TCPSocket === socket && @@tcpi_inspect_ok
161
- opt = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).inspect
162
- if opt =~ /\bstate=(\S+)/
163
- raise Errno::EPIPE, "client closed connection".freeze,
164
- EMPTY_ARRAY if closed_state_str?($1)
165
- else
166
- @@tcpi_inspect_ok = false
167
- write_http_header(socket)
119
+ case tcp_info.data.unpack1("C")
120
+ when 6, 7, 8, 9, 11 # TIME_WAIT, CLOSE, CLOSE_WAIT, LAST_ACK, CLOSING
121
+ raise Errno::EPIPE, "client closed connection", EMPTY_ARRAY
168
122
  end
169
- opt.clear
170
123
  else
171
124
  write_http_header(socket)
172
125
  end
173
126
  end
174
-
175
- def closed_state_str?(state)
176
- case state
177
- when 'ESTABLISHED'
178
- false
179
- # not a typo, ruby maps TCP_CLOSE (no 'D') to state=CLOSED (w/ 'D')
180
- when 'CLOSE_WAIT', 'TIME_WAIT', 'CLOSED', 'LAST_ACK', 'CLOSING'
181
- true
182
- else
183
- false
184
- end
127
+ else
128
+ def check_client_connection(socket) # :nodoc:
129
+ write_http_header(socket)
185
130
  end
186
131
  end
187
132
 
@@ -65,7 +65,7 @@ module Pitchfork
65
65
  append_header(buf, key, value)
66
66
  end
67
67
  end
68
- socket.write(buf << "\r\n".freeze)
68
+ socket.write(buf << "\r\n")
69
69
  end
70
70
 
71
71
  if hijack
@@ -77,7 +77,7 @@ module Pitchfork
77
77
 
78
78
  # :stopdoc:
79
79
  attr_accessor :app, :timeout, :timeout_signal, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
80
- :before_fork, :after_worker_fork, :after_mold_fork,
80
+ :before_fork, :after_worker_fork, :after_mold_fork, :before_service_worker_ready, :before_service_worker_exit,
81
81
  :listener_opts, :children,
82
82
  :orig_app, :config, :ready_pipe,
83
83
  :default_middleware, :early_hints
@@ -95,32 +95,6 @@ module Pitchfork
95
95
 
96
96
  NOOP = '.'
97
97
 
98
- # :startdoc:
99
- # This Hash is considered a stable interface and changing its contents
100
- # will allow you to switch between different installations of Pitchfork
101
- # or even different installations of the same applications without
102
- # downtime. Keys of this constant Hash are described as follows:
103
- #
104
- # * 0 - the path to the pitchfork executable
105
- # * :argv - a deep copy of the ARGV array the executable originally saw
106
- # * :cwd - the working directory of the application, this is where
107
- # you originally started Pitchfork.
108
- # TODO: Can we get rid of this?
109
- START_CTX = {
110
- :argv => ARGV.map(&:dup),
111
- 0 => $0.dup,
112
- }
113
- # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
114
- # and like systems
115
- START_CTX[:cwd] = begin
116
- a = File.stat(pwd = ENV['PWD'])
117
- b = File.stat(Dir.pwd)
118
- a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
119
- rescue
120
- Dir.pwd
121
- end
122
- # :stopdoc:
123
-
124
98
  # Creates a working server on host:port (strange things happen if
125
99
  # port isn't a Number). Use HttpServer::run to start the server and
126
100
  # HttpServer.run.join to join the thread that's processing
@@ -140,7 +114,7 @@ module Pitchfork
140
114
  self.config = Pitchfork::Configurator.new(options)
141
115
  self.listener_opts = {}
142
116
 
143
- proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
117
+ proc_name role: 'monitor', status: ARGV.join(' ')
144
118
 
145
119
  # We use @control_socket differently in the master and worker processes:
146
120
  #
@@ -173,7 +147,7 @@ module Pitchfork
173
147
  :QUIT, :INT, :TERM, :USR2, :TTIN, :TTOU ]
174
148
 
175
149
  Info.workers_count = worker_processes
176
- SharedMemory.preallocate_drops(worker_processes)
150
+ SharedMemory.preallocate_pages(worker_processes)
177
151
  end
178
152
 
179
153
  # Runs the thing. Returns self so you can run join on it
@@ -296,7 +270,7 @@ module Pitchfork
296
270
  def join
297
271
  @respawn = true
298
272
 
299
- proc_name role: 'monitor', status: START_CTX[:argv].join(' ')
273
+ proc_name role: 'monitor', status: ARGV.join(' ')
300
274
 
301
275
  logger.info "master process ready" # test_exec.rb relies on this message
302
276
  if @ready_pipe
@@ -374,6 +348,9 @@ module Pitchfork
374
348
  when Message::MoldSpawned
375
349
  new_mold = @children.update(message)
376
350
  logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} spawned")
351
+ when Message::ServiceSpawned
352
+ new_service = @children.update(message)
353
+ logger.info("service pid=#{new_service.pid} gen=#{new_service.generation} spawned")
377
354
  when Message::MoldReady
378
355
  old_molds = @children.molds
379
356
  new_mold = @children.update(message)
@@ -429,6 +406,20 @@ module Pitchfork
429
406
  Process.exit
430
407
  end
431
408
 
409
+ def service_exit(service)
410
+ logger.info "service pid=#{service.pid} gen=#{service.generation} exiting"
411
+ proc_name status: "exiting"
412
+
413
+ if @before_service_worker_exit
414
+ begin
415
+ @before_service_worker_exit.call(self, service)
416
+ rescue => error
417
+ Pitchfork.log_error(logger, "before_service_worker_exit error", error)
418
+ end
419
+ end
420
+ Process.exit
421
+ end
422
+
432
423
  def rewindable_input
433
424
  Pitchfork::HttpParser.input_class.method_defined?(:rewind)
434
425
  end
@@ -590,6 +581,69 @@ module Pitchfork
590
581
  worker
591
582
  end
592
583
 
584
+ def service_loop(service)
585
+ readers = init_service_process(service)
586
+ waiter = prep_readers(readers)
587
+
588
+ ready = readers.dup
589
+
590
+ if @before_service_worker_ready
591
+ begin
592
+ @before_service_worker_ready.call(self, service)
593
+ rescue => error
594
+ Pitchfork.log_error(logger, "before_service_worker_ready", error)
595
+ Process.exit(1)
596
+ end
597
+ end
598
+
599
+ proc_name status: "ready"
600
+
601
+ while readers[0]
602
+ begin
603
+ service.update_deadline(@timeout)
604
+ while sock = ready.shift
605
+ # Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
606
+ # but that will return false
607
+ client = sock.accept_nonblock(exception: false)
608
+
609
+ case client
610
+ when false, :wait_readable
611
+ # no message, keep looping
612
+ end
613
+
614
+ service.update_deadline(@timeout)
615
+ end
616
+
617
+ # timeout so we can update .deadline and keep parent from SIGKILL-ing us
618
+ service.update_deadline(@timeout)
619
+
620
+
621
+ waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
622
+ rescue => e
623
+ Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
624
+ end
625
+ end
626
+ end
627
+
628
+ def spawn_service(service, detach:)
629
+ logger.info("service gen=#{service.generation} spawning...")
630
+
631
+ # We set the deadline before spawning the child so that if for some
632
+ # reason it gets stuck before reaching the worker loop,
633
+ # the monitor process will kill it.
634
+ service.update_deadline(@spawn_timeout)
635
+ @before_fork&.call(self)
636
+ fork_sibling("spawn_service") do
637
+ service.pid = Process.pid
638
+
639
+ after_fork_internal
640
+ service_loop(service)
641
+ service_exit(service)
642
+ end
643
+
644
+ service
645
+ end
646
+
593
647
  def spawn_initial_mold
594
648
  mold = Worker.new(nil)
595
649
  mold.create_socketpair!
@@ -606,6 +660,21 @@ module Pitchfork
606
660
  end
607
661
 
608
662
  def spawn_missing_workers
663
+ if @before_service_worker_ready && !@children.service
664
+ service = Pitchfork::Service.new
665
+ if REFORKING_AVAILABLE
666
+ service.generation = @children.mold&.generation || 0
667
+
668
+ unless @children.mold&.spawn_service(service)
669
+ @logger.error("Failed to send a spawn_service command")
670
+ end
671
+ else
672
+ spawn_service(service, detach: false)
673
+ end
674
+
675
+ @children.register_service(service)
676
+ end
677
+
609
678
  worker_nr = -1
610
679
  until (worker_nr += 1) == @worker_processes
611
680
  if @children.nr_alive?(worker_nr)
@@ -643,9 +712,18 @@ module Pitchfork
643
712
  end
644
713
 
645
714
  def maintain_worker_count
646
- (off = @children.workers_count - worker_processes) == 0 and return
647
- off < 0 and return spawn_missing_workers
648
- @children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:TERM) }
715
+ off = @children.workers_count - worker_processes
716
+ off -= 1 if @before_service_worker_ready && !@children.service
717
+
718
+ if off < 0
719
+ spawn_missing_workers
720
+ elsif off > 0
721
+ @children.each_worker do |worker|
722
+ if worker.nr >= worker_processes
723
+ worker.soft_kill(:TERM)
724
+ end
725
+ end
726
+ end
649
727
  end
650
728
 
651
729
  def restart_outdated_workers
@@ -658,6 +736,16 @@ module Pitchfork
658
736
  max_pending_workers = (worker_processes * 0.1).ceil
659
737
  workers_to_restart = max_pending_workers - @children.restarting_workers_count
660
738
 
739
+ if service = @children.service
740
+ if service.outdated?
741
+ if service.soft_kill(:TERM)
742
+ logger.info("Sent SIGTERM to service pid=#{service.pid} gen=#{service.generation}")
743
+ else
744
+ logger.info("Failed to send SIGTERM to service pid=#{service.pid} gen=#{service.generation}")
745
+ end
746
+ end
747
+ end
748
+
661
749
  if workers_to_restart > 0
662
750
  outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
663
751
  outdated_workers.each do |worker|
@@ -701,18 +789,14 @@ module Pitchfork
701
789
  rss = @request.response_start_sent
702
790
  buf = (rss ? "103 Early Hints\r\n" : "HTTP/1.1 103 Early Hints\r\n").b
703
791
  headers.each { |key, value| append_header(buf, key, value) }
704
- buf << (rss ? "\r\nHTTP/1.1 ".freeze : "\r\n".freeze)
792
+ buf << (rss ? "\r\nHTTP/1.1 " : "\r\n")
705
793
  client.write(buf)
706
794
  end
707
795
 
708
796
  def e100_response_write(client, env)
709
- # We use String#freeze to avoid allocations under Ruby 2.1+
710
- # Not many users hit this code path, so it's better to reduce the
711
- # constant table sizes even for Ruby 2.0 users who'll hit extra
712
- # allocations here.
713
797
  client.write(@request.response_start_sent ?
714
- "100 Continue\r\n\r\nHTTP/1.1 ".freeze :
715
- "HTTP/1.1 100 Continue\r\n\r\n".freeze)
798
+ "100 Continue\r\n\r\nHTTP/1.1 " :
799
+ "HTTP/1.1 100 Continue\r\n\r\n")
716
800
  env.delete('HTTP_EXPECT')
717
801
  end
718
802
 
@@ -723,7 +807,11 @@ module Pitchfork
723
807
  @request = Pitchfork::HttpParser.new
724
808
  env = @request.read(client)
725
809
 
726
- proc_name status: "requests: #{worker.requests_count}, processing: #{env["PATH_INFO"]}"
810
+ status = "requests: #{worker.requests_count}, processing: #{env["PATH_INFO"]}"
811
+ if request_id = env["HTTP_X_REQUEST_ID"]
812
+ status += ", request_id: #{request_id}"
813
+ end
814
+ proc_name status: status
727
815
 
728
816
  env["pitchfork.worker"] = worker
729
817
  timeout_handler.rack_env = env
@@ -817,6 +905,18 @@ module Pitchfork
817
905
  readers
818
906
  end
819
907
 
908
+ def init_service_process(service)
909
+ proc_name role: "(gen:#{service.generation}) mold", status: "init"
910
+ LISTENERS.each(&:close) # Don't appear as listening to incoming requests
911
+ service.register_to_master(@control_socket[1])
912
+ readers = [service]
913
+ trap(:QUIT) { nuke_listeners!(readers) }
914
+ trap(:TERM) { nuke_listeners!(readers) }
915
+ trap(:INT) { nuke_listeners!(readers); exit!(0) }
916
+ proc_name role: "(gen:#{service.generation}) service", status: "ready"
917
+ readers
918
+ end
919
+
820
920
  def init_mold_process(mold)
821
921
  proc_name role: "(gen:#{mold.generation}) mold", status: "init"
822
922
  after_mold_fork.call(self, mold)
@@ -830,7 +930,7 @@ module Pitchfork
830
930
 
831
931
  if Pitchfork.const_defined?(:Waiter)
832
932
  def prep_readers(readers)
833
- Pitchfork::Waiter.prep_readers(readers)
933
+ Pitchfork::Info.keep_io(Pitchfork::Waiter.prep_readers(readers))
834
934
  end
835
935
  else
836
936
  require_relative 'select_waiter'
@@ -858,24 +958,25 @@ module Pitchfork
858
958
  # Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
859
959
  # but that will return false
860
960
  client = sock.accept_nonblock(exception: false)
861
- client = false if client == :wait_readable
862
- if client
863
- case client
864
- when Message::PromoteWorker
865
- if Info.fork_safe?
866
- spawn_mold(worker)
867
- else
868
- logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
869
- end
870
- when Message
871
- worker.update(client)
961
+
962
+ case client
963
+ when false, :wait_readable
964
+ # no message, keep looping
965
+ when Message::PromoteWorker
966
+ if Info.fork_safe?
967
+ spawn_mold(worker)
872
968
  else
873
- request_env = process_client(client, worker, prepare_timeout(worker))
874
- @after_request_complete&.call(self, worker, request_env)
875
- worker.increment_requests_count
969
+ logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
876
970
  end
877
- worker.update_deadline(@timeout)
971
+ when Message
972
+ worker.update(client)
973
+ else
974
+ request_env = process_client(client, worker, prepare_timeout(worker))
975
+ worker.increment_requests_count
976
+ @after_request_complete&.call(self, worker, request_env)
878
977
  end
978
+
979
+ worker.update_deadline(@timeout)
879
980
  end
880
981
 
881
982
  # timeout so we can update .deadline and keep parent from SIGKILL-ing us
@@ -958,6 +1059,22 @@ module Pitchfork
958
1059
  rescue => error
959
1060
  raise BootFailure, error.message
960
1061
  end
1062
+ when Message::SpawnService
1063
+ retries = 1
1064
+ begin
1065
+ spawn_service(Service.new(generation: mold.generation), detach: true)
1066
+ rescue ForkFailure
1067
+ if retries > 0
1068
+ @logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a service. Retrying.")
1069
+ retries -= 1
1070
+ retry
1071
+ else
1072
+ @logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a service twice in a row. Corrupted mold process?")
1073
+ Process.exit(1)
1074
+ end
1075
+ rescue => error
1076
+ raise BootFailure, error.message
1077
+ end
961
1078
  else
962
1079
  logger.error("Unexpected mold message #{message.inspect}")
963
1080
  end
@@ -1004,7 +1121,7 @@ module Pitchfork
1004
1121
  @proctitle_role = role if role
1005
1122
  @proctitle_status = status if status
1006
1123
 
1007
- Process.setproctitle("#{File.basename(START_CTX[0])} #{@proctitle_role} - #{@proctitle_status}")
1124
+ Process.setproctitle("#{File.basename($PROGRAM_NAME)} #{@proctitle_role} - #{@proctitle_status}")
1008
1125
  end
1009
1126
 
1010
1127
  def bind_listeners!
@@ -1012,7 +1129,6 @@ module Pitchfork
1012
1129
  if listeners.empty?
1013
1130
  listeners << Pitchfork::Const::DEFAULT_LISTEN
1014
1131
  @init_listeners << Pitchfork::Const::DEFAULT_LISTEN
1015
- START_CTX[:argv] << "-l#{Pitchfork::Const::DEFAULT_LISTEN}"
1016
1132
  end
1017
1133
  listeners.each { |addr| listen(addr) }
1018
1134
  raise ArgumentError, "no listeners" if LISTENERS.empty?
@@ -122,12 +122,16 @@ module Pitchfork
122
122
 
123
123
  Message = Class.new(Struct)
124
124
  class Message
125
- SpawnWorker = Message.new(:nr)
126
- WorkerSpawned = Message.new(:nr, :pid, :generation, :pipe)
127
- PromoteWorker = Message.new(:generation)
128
- MoldSpawned = Message.new(:nr, :pid, :generation, :pipe)
129
- MoldReady = Message.new(:nr, :pid, :generation)
125
+ SpawnWorker = new(:nr)
126
+ WorkerSpawned = new(:nr, :pid, :generation, :pipe)
127
+ PromoteWorker = new(:generation)
130
128
 
131
- SoftKill = Message.new(:signum)
129
+ MoldSpawned = new(:nr, :pid, :generation, :pipe)
130
+ MoldReady = new(:nr, :pid, :generation)
131
+
132
+ SpawnService = new(:_) # Struct.new requires at least 1 member on Ruby < 3.3
133
+ ServiceSpawned = new(:pid, :generation, :pipe)
134
+
135
+ SoftKill = new(:signum)
132
136
  end
133
137
  end