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 +4 -4
- data/.devcontainer/devcontainer.json +28 -0
- data/CHANGELOG.md +5 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +2 -2
- data/docs/CONFIGURATION.md +16 -5
- data/lib/pitchfork/configurator.rb +7 -0
- data/lib/pitchfork/http_server.rb +3 -1
- data/lib/pitchfork/info.rb +15 -7
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork.rb +158 -125
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c9d47a7cd3604f0807a41882322b4461edf2a990439e152dfa2d94bb731eff7
|
4
|
+
data.tar.gz: 1a38267df9cff0493452fa88c65cb4c9a502643aaf05c8e0b60544f93e9f6e4f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pitchfork (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.
|
13
|
+
puma (6.3.1)
|
14
14
|
nio4r (~> 2.0)
|
15
15
|
rack (3.0.8)
|
16
16
|
raindrops (0.20.1)
|
data/docs/CONFIGURATION.md
CHANGED
@@ -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`
|
257
|
-
at all. As such for hooks executed in the
|
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
|
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
|
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
|
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
|
|
data/lib/pitchfork/info.rb
CHANGED
@@ -29,13 +29,7 @@ module Pitchfork
|
|
29
29
|
end
|
30
30
|
|
31
31
|
ObjectSpace.each_object(IO) do |io|
|
32
|
-
|
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
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork.rb
CHANGED
@@ -36,157 +36,190 @@ module Pitchfork
|
|
36
36
|
|
37
37
|
# :stopdoc:
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
184
|
-
|
216
|
+
nil # it's tricky to return the PID
|
217
|
+
end
|
185
218
|
|
186
|
-
|
187
|
-
|
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.
|
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
|
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"
|