pitchfork 0.9.0 → 0.10.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: eb4e22969b9f2c38717f0cfa7a3c966995814156a17589bc06bdc609b7ad6e32
4
- data.tar.gz: dbf7833c26ef94962abbd71d66c63e40dd0f7f405ee82951723ad3b4cbfa864f
3
+ metadata.gz: 0f23c677bd64eb8ca7a040cb2d41dab4047e05ae849da16af8e537748bfb6dbe
4
+ data.tar.gz: 890bac5b67c23d6f0aacf0d9130999cf69ac0bc5a2f1ea06bdd142d67c0606ac
5
5
  SHA512:
6
- metadata.gz: 0f0c029a01bc999d90421fe0ed77f76c6f1e56a9f3ec8eaa4ab6722f40eb0ee7f9aa0f004044e4c97e93a593cdba0d7a6bab01ef6b075440c76012f05e5cccd4
7
- data.tar.gz: 4ae4d319ffd2acbf99ad29156622510c951ca095c20074f8415376d6e217be1b959e68e3245590cce62de66f3f8af8f0aae469fec842865dd4dcfe1b060e3db9
6
+ metadata.gz: 39c33bbbe865ba22bebc8b6b1e823f71d0ba6b2787f06af26f0ffe3d4251c781850826fcbcdf1dc13b1734919770e35dc57815d2de057f18d97ba354f247a3e7
7
+ data.tar.gz: 94e5ae39e129af1fd950c9a77e094bac5bda586f7358115fd8cf25bb3c6ee2079bb4654ebd4e6ed5e9ff498b461559da07d7b44acc0147fcac5c9a4695b7c62c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.10.0
4
+
5
+ - Include requests count in workers proctitle.
6
+
3
7
  # 0.9.0
4
8
 
5
9
  - Implement `spawn_timeout` to protect against bugs causing workers to get stuck before they reach ready state.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pitchfork (0.9.0)
4
+ pitchfork (0.10.0)
5
5
  rack (>= 2.0)
6
6
  raindrops (~> 0.7)
7
7
 
data/docs/REFORKING.md CHANGED
@@ -66,7 +66,10 @@ PID COMMAND
66
66
  105 \_ pitchfork (gen:0) worker[3]
67
67
  ```
68
68
 
69
- When a reforking is triggered, one of the workers is selected to fork a new `mold`.
69
+ As the diagram shows, while workers are forked from the mold, they become children of the master process.
70
+ We'll see how does that work [later](#forking-sibling-processes).
71
+
72
+ When a reforking is triggered, one of the workers is selected to fork a new `mold`:
70
73
 
71
74
  ```
72
75
  PID COMMAND
@@ -79,6 +82,9 @@ PID COMMAND
79
82
  105 \_ pitchfork (gen:1) mold
80
83
  ```
81
84
 
85
+ Again, while the mold was forked from a worker, it becomes a child of the master process.
86
+ We'll see how does that work [later](#forking-sibling-processes).
87
+
82
88
  When that new mold is ready, `pitchfork` terminates the old mold and starts a slow rollout of older workers and replace them with fresh workers
83
89
  forked from the mold:
84
90
 
@@ -104,7 +110,7 @@ PID COMMAND
104
110
 
105
111
  etc.
106
112
 
107
- ### Forking Sibling Processes
113
+ ### Forking Sibling Processes
108
114
 
109
115
  Normally on unix systems, when calling `fork(2)`, the newly created process is a child of the original one, so forking from the mold should create
110
116
  a process tree such as:
@@ -119,5 +125,8 @@ PID COMMAND
119
125
  However the `pitchfork` master process registers itself as a "child subreaper" via [`PR_SET_CHILD_SUBREAPER`](https://man7.org/linux/man-pages/man2/prctl.2.html).
120
126
  This means any descendant process that is orphaned will be re-parented as a child of the master rather than a child of the init process (pid 1).
121
127
 
122
- With this in mind, the mold fork twice to create an orphaned process that will get re-attached to the master, effectively forking a sibling rather than a child.
123
- The need for `PR_SET_CHILD_SUBREAPER` is the main reason why reforking is only available on Linux.
128
+ With this in mind, the mold forks twice to create an orphaned process that will get re-attached to the master,
129
+ effectively forking a sibling rather than a child. Similarly, workers do the same when forking new molds.
130
+ This technique eases killing previous generations of molds and workers.
131
+
132
+ The need for `PR_SET_CHILD_SUBREAPER` is the main reason why reforking is only available on Linux.
@@ -107,6 +107,33 @@ module Pitchfork
107
107
  @workers.each_value(&block)
108
108
  end
109
109
 
110
+ def soft_kill_all(sig)
111
+ each do |child|
112
+ child.soft_kill(sig)
113
+ end
114
+ end
115
+
116
+ def hard_kill(sig, child)
117
+ child.hard_kill(sig)
118
+ rescue Errno::ESRCH
119
+ reap(child.pid)
120
+ child.close
121
+ end
122
+
123
+ def hard_kill_all(sig)
124
+ each do |child|
125
+ hard_kill(sig, child)
126
+ end
127
+ end
128
+
129
+ def hard_timeout(child)
130
+ child.hard_timeout!
131
+ rescue Errno::ESRCH
132
+ reap(child.pid)
133
+ child.close
134
+ true
135
+ end
136
+
110
137
  def workers
111
138
  @workers.values
112
139
  end
@@ -396,15 +396,15 @@ module Pitchfork
396
396
  limit = Pitchfork.time_now + timeout
397
397
  until @children.workers.empty? || Pitchfork.time_now > limit
398
398
  if graceful
399
- soft_kill_each_child(:TERM)
399
+ @children.soft_kill_all(:TERM)
400
400
  else
401
- kill_each_child(:INT)
401
+ @children.hard_kill_all(:INT)
402
402
  end
403
403
  if monitor_loop(false) == StopIteration
404
404
  return StopIteration
405
405
  end
406
406
  end
407
- kill_each_child(:KILL)
407
+ @children.hard_kill_all(:KILL)
408
408
  @promotion_lock.unlink
409
409
  end
410
410
 
@@ -506,25 +506,28 @@ module Pitchfork
506
506
  next
507
507
  else # worker is out of time
508
508
  next_sleep = 0
509
- if worker.mold?
510
- logger.error "mold pid=#{worker.pid} timed out, killing"
511
- else
512
- logger.error "worker=#{worker.nr} pid=#{worker.pid} timed out, killing"
513
- end
509
+ hard_timeout(worker)
510
+ end
511
+ end
514
512
 
515
- if @after_worker_hard_timeout
516
- begin
517
- @after_worker_hard_timeout.call(self, worker)
518
- rescue => error
519
- Pitchfork.log_error(@logger, "after_worker_hard_timeout callback", error)
520
- end
521
- end
513
+ next_sleep <= 0 ? 1 : next_sleep
514
+ end
522
515
 
523
- kill_worker(:KILL, worker.pid) # take no prisoners for hard timeout violations
516
+ def hard_timeout(worker)
517
+ if @after_worker_hard_timeout
518
+ begin
519
+ @after_worker_hard_timeout.call(self, worker)
520
+ rescue => error
521
+ Pitchfork.log_error(@logger, "after_worker_hard_timeout callback", error)
524
522
  end
525
523
  end
526
524
 
527
- next_sleep <= 0 ? 1 : next_sleep
525
+ if worker.mold?
526
+ logger.error "mold pid=#{worker.pid} timed out, killing"
527
+ else
528
+ logger.error "worker=#{worker.nr} pid=#{worker.pid} timed out, killing"
529
+ end
530
+ @children.hard_timeout(worker) # take no prisoners for hard timeout violations
528
531
  end
529
532
 
530
533
  def trigger_refork
@@ -697,12 +700,12 @@ module Pitchfork
697
700
 
698
701
  # once a client is accepted, it is processed in its entirety here
699
702
  # in 3 easy steps: read request, call app, write app response
700
- def process_client(client, timeout_handler)
703
+ def process_client(client, worker, timeout_handler)
701
704
  env = nil
702
705
  @request = Pitchfork::HttpParser.new
703
706
  env = @request.read(client)
704
707
 
705
- proc_name status: "processing: #{env["PATH_INFO"]}"
708
+ proc_name status: "requests: #{worker.requests_count}, processing: #{env["PATH_INFO"]}"
706
709
 
707
710
  timeout_handler.rack_env = env
708
711
  env["pitchfork.timeout"] = timeout_handler
@@ -835,7 +838,7 @@ module Pitchfork
835
838
  when Message
836
839
  worker.update(client)
837
840
  else
838
- request_env = process_client(client, prepare_timeout(worker))
841
+ request_env = process_client(client, worker, prepare_timeout(worker))
839
842
  @after_request_complete&.call(self, worker, request_env)
840
843
  worker.increment_requests_count
841
844
  end
@@ -848,6 +851,7 @@ module Pitchfork
848
851
 
849
852
  if @refork_condition && Info.fork_safe? && !worker.outdated?
850
853
  if @refork_condition.met?(worker, logger)
854
+ proc_name status: "requests: #{worker.requests_count}, spawning mold"
851
855
  if spawn_mold(worker.generation)
852
856
  logger.info("Refork condition met, promoting ourselves")
853
857
  end
@@ -855,7 +859,7 @@ module Pitchfork
855
859
  end
856
860
  end
857
861
 
858
- proc_name status: "waiting"
862
+ proc_name status: "requests: #{worker.requests_count}, waiting"
859
863
  waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
860
864
  rescue => e
861
865
  Pitchfork.log_error(@logger, "listen loop error", e) if readers[0]
@@ -930,15 +934,6 @@ module Pitchfork
930
934
  worker = @children.reap(wpid) and worker.close rescue nil
931
935
  end
932
936
 
933
- # delivers a signal to each worker
934
- def kill_each_child(signal)
935
- @children.each { |w| kill_worker(signal, w.pid) }
936
- end
937
-
938
- def soft_kill_each_child(signal)
939
- @children.each { |worker| worker.soft_kill(signal) }
940
- end
941
-
942
937
  # returns an array of string names for the given listener array
943
938
  def listener_names(listeners = LISTENERS)
944
939
  listeners.map { |io| sock_name(io) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end
@@ -139,6 +139,14 @@ module Pitchfork
139
139
  success
140
140
  end
141
141
 
142
+ def hard_kill(sig)
143
+ Process.kill(sig, pid)
144
+ end
145
+
146
+ def hard_timeout!
147
+ hard_kill(:KILL)
148
+ end
149
+
142
150
  # this only runs when the Rack app.call is not running
143
151
  # act like a listener
144
152
  def accept_nonblock(exception: nil) # :nodoc:
data/lib/pitchfork.rb CHANGED
@@ -204,8 +204,13 @@ module Pitchfork
204
204
  # or the master to be PID 1.
205
205
  if middle_pid = FORK_LOCK.synchronize { Process.fork } # parent
206
206
  # We need to wait(2) so that the middle process doesn't end up a zombie.
207
- Process.wait(middle_pid)
207
+ # The process only call fork again an exit so it should be pretty fast.
208
+ # However it might need to execute some `Process._fork` or `at_exit` callbacks,
209
+ # so it case it takes more than 5 seconds to exit, we kill it with SIGBUS
210
+ # to produce a crash report, as this is indicative of a nasty bug.
211
+ process_wait_with_timeout(middle_pid, 5, :BUS)
208
212
  else # first child
213
+ Process.setproctitle("<pitchfork fork_sibling>")
209
214
  clean_fork(&block) # detach into a grand child
210
215
  exit
211
216
  end
@@ -216,6 +221,18 @@ module Pitchfork
216
221
  nil # it's tricky to return the PID
217
222
  end
218
223
 
224
+ def process_wait_with_timeout(pid, timeout, timeout_signal = :KILL)
225
+ (timeout * 200).times do
226
+ status = Process.wait(pid, Process::WNOHANG)
227
+ return status if status
228
+ sleep 0.005 # 200 * 5ms => 1s
229
+ end
230
+
231
+ # The process didn't exit in the allotted time, so we kill it.
232
+ Process.kill(timeout_signal, pid)
233
+ Process.wait(pid)
234
+ end
235
+
219
236
  def time_now(int = false)
220
237
  Process.clock_gettime(Process::CLOCK_MONOTONIC, int ? :second : :float_second)
221
238
  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.9.0
4
+ version: 0.10.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-09-28 00:00:00.000000000 Z
11
+ date: 2023-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: raindrops