pitchfork 0.7.0 → 0.8.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 +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"
|