pitchfork 0.7.0 → 0.8.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: 05372d35dd4784eb22e204b614b1c7add13fd0e298495543c5395e2f03b42073
4
- data.tar.gz: 14f241efeb95774f6de6e9fc8926815eb7f10c25965a09a0700f77dae75dbb92
3
+ metadata.gz: 9c9d47a7cd3604f0807a41882322b4461edf2a990439e152dfa2d94bb731eff7
4
+ data.tar.gz: 1a38267df9cff0493452fa88c65cb4c9a502643aaf05c8e0b60544f93e9f6e4f
5
5
  SHA512:
6
- metadata.gz: f26ebeeb3bc9f7533d25cc41b29a317fb8a80ce108a1859657adfa80b9eb8f88812cfe85dfecf0fbd8093b6e91f45c6ce51ab7b9a15864d96b9d0f5f77835786
7
- data.tar.gz: 8d1f09bb24226f2c89b2db2d549c28508a9f2ea716a1612fcc3367d27445657b9d9da281e623af696dc803acd649847a724366306caf672dbc523a3e7495579b
6
+ metadata.gz: e8318fc2ae118a7e4a89f65e76e0634d306abd1838f0e4957f638bc870ddd202bc72c006fe3b93b6eec52f1765daea43b24b68eb7b8249b5b21b28a8b9abd51b
7
+ data.tar.gz: 5f23586cf49e29649496e15577ce7344c477b99878bfd6756f685f2a96ad40fc202d4f3cf97dad8183dd50b2489423c1fcfd48e20303d4eafae6b7f1db586e9b
@@ -0,0 +1,28 @@
1
+ // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2
+ // README at: https://github.com/devcontainers/templates/tree/main/src/ruby
3
+ {
4
+ "name": "Ruby",
5
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6
+ "build": {
7
+ // Path is relative to the devcontainer.json file.
8
+ "dockerfile": "../Dockerfile"
9
+ },
10
+ "features": {
11
+ "ghcr.io/devcontainers/features/github-cli:1": {}
12
+ },
13
+
14
+ // Features to add to the dev container. More info: https://containers.dev/features.
15
+ // "features": {},
16
+
17
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
18
+ // "forwardPorts": [],
19
+
20
+ // Use 'postCreateCommand' to run commands after the container is created.
21
+ "postCreateCommand": "bundle install"
22
+
23
+ // Configure tool-specific properties.
24
+ // "customizations": {},
25
+
26
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
27
+ // "remoteUser": "root"
28
+ }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.8.0
4
+
5
+ - Add an `after_monitor_ready` callback, called in the monitor process at end of boot.
6
+ - Implement `Pitchfork.prevent_fork` for use in background threads that synchronize native locks with the GVL released.
7
+
3
8
  # 0.7.0
4
9
 
5
10
  - Set nicer `proctile` to better see the state of the process tree at a glance.
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:3.2
1
+ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bookworm
2
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.7.0)
4
+ pitchfork (0.8.0)
5
5
  rack (>= 2.0)
6
6
  raindrops (~> 0.7)
7
7
 
@@ -10,7 +10,7 @@ GEM
10
10
  specs:
11
11
  minitest (5.15.0)
12
12
  nio4r (2.5.9)
13
- puma (6.3.0)
13
+ puma (6.3.1)
14
14
  nio4r (~> 2.0)
15
15
  rack (3.0.8)
16
16
  raindrops (0.20.1)
@@ -253,12 +253,23 @@ The default Logger will log its output to STDERR.
253
253
  Because pitchfork several callbacks around the lifecycle of workers.
254
254
  It is often necessary to use these callbacks to close inherited connection after fork.
255
255
 
256
- Note that when reforking is available, the `pitchfork` master process won't load your application
257
- at all. As such for hooks executed in the master, you may need to explicitly load the parts of your
256
+ Note that when reforking is available, the `pitchfork` monitor process won't load your application
257
+ at all. As such for hooks executed in the monitor, you may need to explicitly load the parts of your
258
258
  application that are used in hooks.
259
259
 
260
260
  `pitchfork` also don't attempt to rescue hook errors. Raising from a worker hook will crash the worker,
261
- and raising from a master hook will bring the whole cluster down.
261
+ and raising from a monitor hook will bring the whole cluster down.
262
+
263
+ ### `after_monitor_ready`
264
+
265
+ Called by the monitor process after it's done booting the application and
266
+ spawning the original workers.
267
+
268
+ ```ruby
269
+ after_monitor_ready do |server|
270
+ server.logger.info("Monitor pid=#{Process.pid} ready")
271
+ end
272
+ ```
262
273
 
263
274
  ### `after_mold_fork`
264
275
 
@@ -338,7 +349,7 @@ By default the cleanup timeout is 2 seconds.
338
349
 
339
350
  ### `after_worker_hard_timeout`
340
351
 
341
- Called in the master process when a worker hard timeout is elapsed:
352
+ Called in the monitor process when a worker hard timeout is elapsed:
342
353
 
343
354
  ```ruby
344
355
  after_worker_timeout do |server, worker|
@@ -353,7 +364,7 @@ soft timeout from working.
353
364
 
354
365
  ### `after_worker_exit`
355
366
 
356
- Called in the master process after a worker exits.
367
+ Called in the monitor process after a worker exits.
357
368
 
358
369
  ```ruby
359
370
  after_worker_exit do |server, worker, status|
@@ -60,6 +60,9 @@ module Pitchfork
60
60
  :after_worker_ready => lambda { |server, worker|
61
61
  server.logger.info("worker=#{worker.nr} gen=#{worker.generation} ready")
62
62
  },
63
+ :after_monitor_ready => lambda { |server|
64
+ server.logger.info("Monitor pid=#{Process.pid} ready")
65
+ },
63
66
  :after_worker_timeout => nil,
64
67
  :after_worker_hard_timeout => nil,
65
68
  :after_request_complete => nil,
@@ -141,6 +144,10 @@ module Pitchfork
141
144
  set_hook(:after_worker_ready, block_given? ? block : args[0])
142
145
  end
143
146
 
147
+ def after_monitor_ready(*args, &block)
148
+ set_hook(:after_monitor_ready, block_given? ? block : args[0], 1)
149
+ end
150
+
144
151
  def after_worker_timeout(*args, &block)
145
152
  set_hook(:after_worker_timeout, block_given? ? block : args[0], 3)
146
153
  end
@@ -80,7 +80,7 @@ module Pitchfork
80
80
  :orig_app, :config, :ready_pipe,
81
81
  :default_middleware, :early_hints
82
82
  attr_writer :after_worker_exit, :before_worker_exit, :after_worker_ready, :after_request_complete,
83
- :refork_condition, :after_worker_timeout, :after_worker_hard_timeout
83
+ :refork_condition, :after_worker_timeout, :after_worker_hard_timeout, :after_monitor_ready
84
84
 
85
85
  attr_reader :logger
86
86
  include Pitchfork::SocketHelper
@@ -212,6 +212,8 @@ module Pitchfork
212
212
  wait_for_pending_workers
213
213
  end
214
214
 
215
+ @after_monitor_ready&.call(self)
216
+
215
217
  self
216
218
  end
217
219
 
@@ -29,13 +29,7 @@ module Pitchfork
29
29
  end
30
30
 
31
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)
32
+ if io_open?(io) && io_autoclosed?(io) && !ignored_ios.include?(io)
39
33
  if io.is_a?(TCPSocket)
40
34
  # If we inherited a TCP Socket, calling #close directly could send FIN or RST.
41
35
  # So we first reopen /dev/null to avoid that.
@@ -73,6 +67,20 @@ module Pitchfork
73
67
  def shutting_down?
74
68
  SharedMemory.shutting_down?
75
69
  end
70
+
71
+ private
72
+
73
+ def io_open?(io)
74
+ !io.closed?
75
+ rescue IOError
76
+ false
77
+ end
78
+
79
+ def io_autoclosed?(io)
80
+ io.autoclose?
81
+ rescue IOError
82
+ false
83
+ end
76
84
  end
77
85
  end
78
86
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end
data/lib/pitchfork.rb CHANGED
@@ -36,157 +36,190 @@ module Pitchfork
36
36
 
37
37
  # :stopdoc:
38
38
 
39
- # This returns a lambda to pass in as the app, this does not "build" the
40
- # app The returned lambda will be called when it is
41
- # time to build the app.
42
- def self.builder(ru, op)
43
- # allow Configurator to parse cli switches embedded in the ru file
44
- op = Pitchfork::Configurator::RACKUP.merge!(:file => ru, :optparse => op)
45
- if ru =~ /\.ru$/ && !defined?(Rack::Builder)
46
- abort "rack and Rack::Builder must be available for processing #{ru}"
39
+ FORK_LOCK = Monitor.new
40
+ @socket_type = :SOCK_SEQPACKET
41
+
42
+ class << self
43
+ # :startdoc:
44
+
45
+ # Prevent Pitchfork from forking new children for the duration of the block.
46
+ #
47
+ # If you have background threads calling code that synchronize native locks,
48
+ # while the GVL is released, forking while they are held could leak to
49
+ # corrupted children.
50
+ #
51
+ # One example of this is `getaddrinfo(3)`, so opening a connection from a
52
+ # background thread has a chance to produce stuck children.
53
+ #
54
+ # To avoid this you can wrap such code in `Pitchfork.prevent_fork`:
55
+ #
56
+ # def heartbeat_thread
57
+ # @heartbeat_thread ||= Thread.new do
58
+ # loop do
59
+ # Pitchfork.prevent_fork do
60
+ # heartbeat
61
+ # end
62
+ # sleep 10
63
+ # end
64
+ # end
65
+ # end
66
+ #
67
+ def prevent_fork(&block)
68
+ FORK_LOCK.synchronize(&block)
47
69
  end
48
70
 
49
- # always called after config file parsing, may be called after forking
50
- lambda do |_, server|
51
- inner_app = case ru
52
- when /\.ru$/
53
- raw = File.read(ru)
54
- raw.sub!(/^__END__\n.*/, '')
55
- eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
56
- else
57
- require ru
58
- Object.const_get(File.basename(ru, '.rb').capitalize)
71
+ # :stopdoc:
72
+
73
+ # This returns a lambda to pass in as the app, this does not "build" the
74
+ # app The returned lambda will be called when it is
75
+ # time to build the app.
76
+ def builder(ru, op)
77
+ # allow Configurator to parse cli switches embedded in the ru file
78
+ op = Pitchfork::Configurator::RACKUP.merge!(:file => ru, :optparse => op)
79
+ if ru =~ /\.ru$/ && !defined?(Rack::Builder)
80
+ abort "rack and Rack::Builder must be available for processing #{ru}"
59
81
  end
60
82
 
61
- Rack::Builder.new do
62
- use(Rack::ContentLength)
63
- use(Pitchfork::Chunked)
64
- use(Rack::Lint) if ENV["RACK_ENV"] == "development"
65
- use(Rack::TempfileReaper)
66
- run inner_app
67
- end.to_app
83
+ # always called after config file parsing, may be called after forking
84
+ lambda do |_, server|
85
+ inner_app = case ru
86
+ when /\.ru$/
87
+ raw = File.read(ru)
88
+ raw.sub!(/^__END__\n.*/, '')
89
+ eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
90
+ else
91
+ require ru
92
+ Object.const_get(File.basename(ru, '.rb').capitalize)
93
+ end
94
+
95
+ Rack::Builder.new do
96
+ use(Rack::ContentLength)
97
+ use(Pitchfork::Chunked)
98
+ use(Rack::Lint) if ENV["RACK_ENV"] == "development"
99
+ use(Rack::TempfileReaper)
100
+ run inner_app
101
+ end.to_app
102
+ end
68
103
  end
69
- end
70
104
 
71
- # returns an array of strings representing TCP listen socket addresses
72
- # and Unix domain socket paths. This is useful for use with
73
- # Raindrops::Middleware under Linux: https://yhbt.net/raindrops/
74
- def self.listener_names
75
- Pitchfork::HttpServer::LISTENERS.map do |io|
76
- Pitchfork::SocketHelper.sock_name(io)
105
+ # returns an array of strings representing TCP listen socket addresses
106
+ # and Unix domain socket paths. This is useful for use with
107
+ # Raindrops::Middleware under Linux: https://yhbt.net/raindrops/
108
+ def listener_names
109
+ Pitchfork::HttpServer::LISTENERS.map do |io|
110
+ Pitchfork::SocketHelper.sock_name(io)
111
+ end
77
112
  end
78
- end
79
113
 
80
- def self.log_error(logger, prefix, exc)
81
- message = exc.message
82
- message = message.dump if /[[:cntrl:]]/ =~ message
83
- logger.error "#{prefix}: #{message} (#{exc.class})"
84
- exc.backtrace.each { |line| logger.error(line) }
85
- end
114
+ def log_error(logger, prefix, exc)
115
+ message = exc.message
116
+ message = message.dump if /[[:cntrl:]]/ =~ message
117
+ logger.error "#{prefix}: #{message} (#{exc.class})"
118
+ exc.backtrace.each { |line| logger.error(line) }
119
+ end
86
120
 
87
- F_SETPIPE_SZ = 1031 if RUBY_PLATFORM =~ /linux/
88
-
89
- def self.pipe # :nodoc:
90
- IO.pipe.each do |io|
91
- # shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft
92
- # limits.
93
- if defined?(F_SETPIPE_SZ)
94
- begin
95
- io.fcntl(F_SETPIPE_SZ, Raindrops::PAGE_SIZE)
96
- rescue Errno::EINVAL
97
- # old kernel
98
- rescue Errno::EPERM
99
- # resizes fail if Linux is close to the pipe limit for the user
100
- # or if the user does not have permissions to resize
121
+ F_SETPIPE_SZ = 1031 if RUBY_PLATFORM =~ /linux/
122
+
123
+ def pipe # :nodoc:
124
+ IO.pipe.each do |io|
125
+ # shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft
126
+ # limits.
127
+ if defined?(F_SETPIPE_SZ)
128
+ begin
129
+ io.fcntl(F_SETPIPE_SZ, Raindrops::PAGE_SIZE)
130
+ rescue Errno::EINVAL
131
+ # old kernel
132
+ rescue Errno::EPERM
133
+ # resizes fail if Linux is close to the pipe limit for the user
134
+ # or if the user does not have permissions to resize
135
+ end
101
136
  end
102
137
  end
103
138
  end
104
- end
105
139
 
106
- @socket_type = :SOCK_SEQPACKET
107
- def self.socketpair
108
- pair = UNIXSocket.socketpair(@socket_type).map { |s| MessageSocket.new(s) }
109
- pair[0].close_write
110
- pair[1].close_read
111
- pair
112
- rescue Errno::EPROTONOSUPPORT
113
- if @socket_type == :SOCK_SEQPACKET
114
- # macOS and very old linuxes don't support SOCK_SEQPACKET (SCTP).
115
- # In such case we can fallback to SOCK_STREAM (TCP)
116
- warn("SEQPACKET (SCTP) isn't supported, falling back to STREAM")
117
- @socket_type = :SOCK_STREAM
118
- retry
119
- else
120
- raise
140
+ def socketpair
141
+ pair = UNIXSocket.socketpair(@socket_type).map { |s| MessageSocket.new(s) }
142
+ pair[0].close_write
143
+ pair[1].close_read
144
+ pair
145
+ rescue Errno::EPROTONOSUPPORT
146
+ if @socket_type == :SOCK_SEQPACKET
147
+ # macOS and very old linuxes don't support SOCK_SEQPACKET (SCTP).
148
+ # In such case we can fallback to SOCK_STREAM (TCP)
149
+ warn("SEQPACKET (SCTP) isn't supported, falling back to STREAM")
150
+ @socket_type = :SOCK_STREAM
151
+ retry
152
+ else
153
+ raise
154
+ end
121
155
  end
122
- end
123
156
 
124
- def self.clean_fork(setpgid: true, &block)
125
- if pid = Process.fork
126
- if setpgid
127
- Process.setpgid(pid, pid) # Make into a group leader
157
+ def clean_fork(setpgid: true, &block)
158
+ if pid = FORK_LOCK.synchronize { Process.fork }
159
+ if setpgid
160
+ Process.setpgid(pid, pid) # Make into a group leader
161
+ end
162
+ return pid
128
163
  end
129
- return pid
130
- end
131
164
 
132
- begin
133
- # Pitchfork recursively refork the worker processes.
134
- # Because of this we need to unwind the stack before resuming execution
135
- # in the child, otherwise on each generation the available stack space would
136
- # get smaller and smaller until it's basically 0.
137
- #
138
- # The very first version of this method used to call fork from a new
139
- # thread, however this can cause issues with some native gems that rely on
140
- # pthread_atfork(3) or pthread_mutex_lock(3), as the new main thread would
141
- # now be different.
142
- #
143
- # A second version used to fork from a new fiber, but fibers have a much smaller
144
- # stack space (https://bugs.ruby-lang.org/issues/3187), so it would break large applications.
145
- #
146
- # The latest version now use `throw` to unwind the stack after the fork, it however
147
- # restrict it to be called only inside `handle_clean_fork`.
148
- if Thread.current[:pitchfork_handle_clean_fork]
149
- throw self, block
150
- else
151
- while block
152
- block = catch(self) do
153
- Thread.current[:pitchfork_handle_clean_fork] = true
154
- block.call
155
- nil
165
+ begin
166
+ # Pitchfork recursively refork the worker processes.
167
+ # Because of this we need to unwind the stack before resuming execution
168
+ # in the child, otherwise on each generation the available stack space would
169
+ # get smaller and smaller until it's basically 0.
170
+ #
171
+ # The very first version of this method used to call fork from a new
172
+ # thread, however this can cause issues with some native gems that rely on
173
+ # pthread_atfork(3) or pthread_mutex_lock(3), as the new main thread would
174
+ # now be different.
175
+ #
176
+ # A second version used to fork from a new fiber, but fibers have a much smaller
177
+ # stack space (https://bugs.ruby-lang.org/issues/3187), so it would break large applications.
178
+ #
179
+ # The latest version now use `throw` to unwind the stack after the fork, it however
180
+ # restrict it to be called only inside `handle_clean_fork`.
181
+ if Thread.current[:pitchfork_handle_clean_fork]
182
+ throw self, block
183
+ else
184
+ while block
185
+ block = catch(self) do
186
+ Thread.current[:pitchfork_handle_clean_fork] = true
187
+ block.call
188
+ nil
189
+ end
156
190
  end
157
191
  end
192
+ rescue
193
+ abort
194
+ else
195
+ exit
158
196
  end
159
- rescue
160
- abort
161
- else
162
- exit
163
197
  end
164
- end
165
198
 
166
- def self.fork_sibling(&block)
167
- if REFORKING_AVAILABLE
168
- # We double fork so that the new worker is re-attached back
169
- # to the master.
170
- # This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
171
- # or the master to be PID 1.
172
- if middle_pid = Process.fork # parent
173
- # We need to wait(2) so that the middle process doesn't end up a zombie.
174
- Process.wait(middle_pid)
175
- else # first child
176
- clean_fork(&block) # detach into a grand child
177
- exit
199
+ def fork_sibling(&block)
200
+ if REFORKING_AVAILABLE
201
+ # We double fork so that the new worker is re-attached back
202
+ # to the master.
203
+ # This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
204
+ # or the master to be PID 1.
205
+ if middle_pid = FORK_LOCK.synchronize { Process.fork } # parent
206
+ # We need to wait(2) so that the middle process doesn't end up a zombie.
207
+ Process.wait(middle_pid)
208
+ else # first child
209
+ clean_fork(&block) # detach into a grand child
210
+ exit
211
+ end
212
+ else
213
+ clean_fork(&block)
178
214
  end
179
- else
180
- clean_fork(&block)
181
- end
182
215
 
183
- nil # it's tricky to return the PID
184
- end
216
+ nil # it's tricky to return the PID
217
+ end
185
218
 
186
- def self.time_now(int = false)
187
- Process.clock_gettime(Process::CLOCK_MONOTONIC, int ? :second : :float_second)
219
+ def time_now(int = false)
220
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, int ? :second : :float_second)
221
+ end
188
222
  end
189
- # :startdoc:
190
223
  end
191
224
  # :enddoc:
192
225
 
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.7.0
4
+ version: 0.8.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-08-18 00:00:00.000000000 Z
11
+ date: 2023-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: raindrops
@@ -49,6 +49,7 @@ extensions:
49
49
  - ext/pitchfork_http/extconf.rb
50
50
  extra_rdoc_files: []
51
51
  files:
52
+ - ".devcontainer/devcontainer.json"
52
53
  - ".git-blame-ignore-revs"
53
54
  - ".gitattributes"
54
55
  - ".github/workflows/ci.yml"