pitchfork 0.10.0 → 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +6 -7
- data/Rakefile +0 -1
- data/docs/CONFIGURATION.md +17 -2
- data/docs/DESIGN.md +1 -1
- data/docs/FORK_SAFETY.md +14 -8
- data/docs/PHILOSOPHY.md +2 -2
- data/docs/REFORKING.md +6 -6
- data/docs/SIGNALS.md +3 -3
- data/ext/pitchfork_http/epollexclusive.h +15 -7
- data/ext/pitchfork_http/extconf.rb +2 -0
- data/lib/pitchfork/children.rb +12 -8
- data/lib/pitchfork/configurator.rb +19 -0
- data/lib/pitchfork/http_parser.rb +6 -1
- data/lib/pitchfork/http_response.rb +10 -2
- data/lib/pitchfork/http_server.rb +115 -26
- data/lib/pitchfork/info.rb +1 -1
- data/lib/pitchfork/refork_condition.rb +7 -1
- data/lib/pitchfork/shared_memory.rb +6 -1
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +14 -10
- data/lib/pitchfork.rb +1 -37
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5dc129b2e6e6e940be0f9e388400a376614a65a125a64b68c73b919744c433b3
|
4
|
+
data.tar.gz: b12e1d5f360edc7159567060c095b40510e8e89c54dae058882742c29c830f78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a43b0204dc0e77a4d28007dbfd89e9645724a4198ea6ec7ffa0895aefaf075bd42d4303c37d0f7e61afb550b665b78ab6b22ca9f934ef2d3721883acf0a68cd
|
7
|
+
data.tar.gz: 7eb41807d6971ac948ee264b1f70fd2e16afcf8f289655073c876b752a53a405b9705e97509f5be82c519ee12694ecf1910b18a20cce8aad3ce9a4ffd3af9ac6
|
data/.github/workflows/ci.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 0.11.1
|
4
|
+
|
5
|
+
- Fix Ruby 3.4-dev compatibility.
|
6
|
+
|
7
|
+
# 0.11.0
|
8
|
+
|
9
|
+
- Drop invalid response headers.
|
10
|
+
- Enforce `spawn_timeout` for molds too.
|
11
|
+
- Gracefully shutdown the server if the mold appear to be corrupted (#79).
|
12
|
+
- Add more information in proctitle when forking a new sibbling.
|
13
|
+
- Add a `before_fork` callback called before forking new molds and new workers.
|
14
|
+
|
3
15
|
# 0.10.0
|
4
16
|
|
5
17
|
- Include requests count in workers proctitle.
|
data/Gemfile.lock
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pitchfork (0.
|
4
|
+
pitchfork (0.11.1)
|
5
5
|
rack (>= 2.0)
|
6
6
|
raindrops (~> 0.7)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
minitest (5.
|
12
|
-
nio4r (2.
|
13
|
-
puma (6.
|
11
|
+
minitest (5.22.2)
|
12
|
+
nio4r (2.7.0)
|
13
|
+
puma (6.4.2)
|
14
14
|
nio4r (~> 2.0)
|
15
|
-
rack (3.0.
|
15
|
+
rack (3.0.9.1)
|
16
16
|
raindrops (0.20.1)
|
17
17
|
rake (13.0.6)
|
18
18
|
rake-compiler (1.2.1)
|
@@ -20,8 +20,7 @@ GEM
|
|
20
20
|
|
21
21
|
PLATFORMS
|
22
22
|
aarch64-linux
|
23
|
-
arm64-darwin
|
24
|
-
arm64-darwin-22
|
23
|
+
arm64-darwin
|
25
24
|
x86_64-linux
|
26
25
|
|
27
26
|
DEPENDENCIES
|
data/Rakefile
CHANGED
@@ -42,7 +42,6 @@ namespace :test do
|
|
42
42
|
# preferable to edit the test suite as little as possible.
|
43
43
|
task legacy_integration: :compile do
|
44
44
|
File.write("test/integration/random_blob", File.read("/dev/random", 1_000_000))
|
45
|
-
lib = File.expand_path("lib", __dir__)
|
46
45
|
path = "#{File.expand_path("exe", __dir__)}:#{ENV["PATH"]}"
|
47
46
|
old_path = ENV["PATH"]
|
48
47
|
ENV["PATH"] = "#{path}:#{old_path}"
|
data/docs/CONFIGURATION.md
CHANGED
@@ -241,10 +241,10 @@ for more details on nginx upstream configuration.
|
|
241
241
|
### `spawn_timeout`
|
242
242
|
|
243
243
|
```ruby
|
244
|
-
|
244
|
+
spawn_timeout 5
|
245
245
|
```
|
246
246
|
|
247
|
-
Sets the timeout for a newly spawned worker to be ready after being spawned.
|
247
|
+
Sets the timeout for a newly spawned worker or mold to be ready after being spawned.
|
248
248
|
|
249
249
|
This timeout is a safeguard against various low-level fork safety bugs that could cause
|
250
250
|
a process to dead-lock.
|
@@ -284,6 +284,16 @@ after_monitor_ready do |server|
|
|
284
284
|
end
|
285
285
|
```
|
286
286
|
|
287
|
+
### `before_fork`
|
288
|
+
|
289
|
+
Called by the mold before forking a new workers, and by workers before they spawn a new mold.
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
before_fork do |server|
|
293
|
+
server.logger.info("About to fork, closing connections!")
|
294
|
+
end
|
295
|
+
```
|
296
|
+
|
287
297
|
### `after_mold_fork`
|
288
298
|
|
289
299
|
```ruby
|
@@ -307,6 +317,11 @@ That is the case for instance of many SQL databases protocols.
|
|
307
317
|
This is also the callback in which memory optimizations, such as
|
308
318
|
heap compaction should be done.
|
309
319
|
|
320
|
+
This callback is also a good place to check for potential corruption
|
321
|
+
issues caused by forking. If you detect something wrong, you can
|
322
|
+
call `Process.exit`, and this mold won't be used, another one will be
|
323
|
+
spawned later. e.g. you can check `Socket.getaddrinfo` still works, etc.
|
324
|
+
|
310
325
|
### `after_worker_fork`
|
311
326
|
|
312
327
|
```ruby
|
data/docs/DESIGN.md
CHANGED
@@ -49,7 +49,7 @@
|
|
49
49
|
nothing to accept().
|
50
50
|
|
51
51
|
* Since non-blocking accept() is used, there can be a thundering
|
52
|
-
herd when an occasional client connects when application
|
52
|
+
herd when an occasional client connects when the application
|
53
53
|
*is not busy*. The thundering herd problem should not affect
|
54
54
|
applications that are running all the time since worker processes
|
55
55
|
will only select()/accept() outside of the application dispatch.
|
data/docs/FORK_SAFETY.md
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
Because `pitchfork` is a preforking server, your application code and libraries
|
4
4
|
must be fork safe.
|
5
5
|
|
6
|
-
Generally code might be fork-unsafe for one of two reasons
|
6
|
+
Generally, code might be fork-unsafe for one of two reasons.
|
7
7
|
|
8
8
|
## Inherited Connection
|
9
9
|
|
10
|
-
When a process is forked, any open file
|
10
|
+
When a process is forked, any open file descriptors (sockets, files, pipes, etc)
|
11
11
|
end up shared between the parent and child process. This is never what you
|
12
12
|
want, so any code keeping persistent connections should close them either
|
13
13
|
before or after the fork happens.
|
@@ -22,20 +22,24 @@ reopen connections and restart threads:
|
|
22
22
|
```ruby
|
23
23
|
# pitchfork.conf.rb
|
24
24
|
|
25
|
-
|
25
|
+
before_fork do
|
26
26
|
Sequel::DATABASES.each(&:disconnect)
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
after_mold_fork do
|
30
30
|
SomeLibary.connection.close
|
31
31
|
end
|
32
|
+
|
33
|
+
after_worker_fork do
|
34
|
+
SomeOtherLibary.connection.close
|
35
|
+
end
|
32
36
|
```
|
33
37
|
|
34
38
|
The documentation of any database client or network library you use should be
|
35
39
|
read with care to figure out how to disconnect it, and whether it is best to
|
36
40
|
do it before or after fork.
|
37
41
|
|
38
|
-
Since the most common Ruby application servers `Puma`, `Unicorn` and `Passenger`
|
42
|
+
Since the most common Ruby application servers like `Puma`, `Unicorn` and `Passenger`
|
39
43
|
have forking at least as an option, the requirements are generally well documented.
|
40
44
|
|
41
45
|
However what is novel with `Pitchfork`, is that processes can be forked more than once.
|
@@ -57,7 +61,7 @@ So any libraries that spawn a background thread for periodical work may need to
|
|
57
61
|
that a fork happened and that it should restart its thread.
|
58
62
|
|
59
63
|
Just like with connections, some libraries take on them to automatically restart their background
|
60
|
-
thread when they detect a fork happened.
|
64
|
+
thread when they detect that a fork happened.
|
61
65
|
|
62
66
|
# Refork Safety
|
63
67
|
|
@@ -67,7 +71,7 @@ but not work in Pitchfork when reforking is enabled.
|
|
67
71
|
This is because it is not uncommon for network connections or background threads to only be
|
68
72
|
initialized upon the first request. As such they're not inherited on the first fork.
|
69
73
|
|
70
|
-
However when reforking is enabled, new processes
|
74
|
+
However when reforking is enabled, new processes are forked out of a warmed up process, as such
|
71
75
|
any lazily created connection is much more likely to have been created.
|
72
76
|
|
73
77
|
As such, if you enable reforking for the first time, it is heavily recommended to first do it
|
@@ -84,4 +88,6 @@ impact of discovering such bug.
|
|
84
88
|
- The `ruby-vips` gem binds the `libvips` image processing library that isn't fork safe.
|
85
89
|
(https://github.com/libvips/libvips/discussions/3577)
|
86
90
|
|
87
|
-
|
91
|
+
- Any gem binding with `libgobject`, such as the `gda` gem, likely aren't fork safe.
|
92
|
+
|
93
|
+
No other gem is known to be incompatible for now, but if you find one, please open an issue to add it to the list.
|
data/docs/PHILOSOPHY.md
CHANGED
@@ -80,8 +80,8 @@ Suitable options include `nginx`, `caddy` and likely several others.
|
|
80
80
|
One of the main advantages of threaded servers over preforking servers is their
|
81
81
|
lower memory usage.
|
82
82
|
|
83
|
-
However `pitchfork` solves this with its reforking feature. If enabled and properly configured
|
84
|
-
it very significantly increase Copy-on-Write performance, closing the gap with threaded servers.
|
83
|
+
However `pitchfork` solves this with its reforking feature. If enabled and properly configured,
|
84
|
+
it can very significantly increase Copy-on-Write performance, closing the gap with threaded servers.
|
85
85
|
|
86
86
|
## Assume Modern Deployment Methods
|
87
87
|
|
data/docs/REFORKING.md
CHANGED
@@ -21,7 +21,7 @@ forked processes are essentially free.
|
|
21
21
|
So in theory, preforking servers shouldn't use more memory than threaded servers.
|
22
22
|
|
23
23
|
However, in a Ruby process, there is generally a lot of memory regions that are lazily initialized.
|
24
|
-
This
|
24
|
+
This includes the Ruby Virtual Machine inline caches, JITed code if you use YJIT, and also
|
25
25
|
some common patterns in applications, such as memoization:
|
26
26
|
|
27
27
|
```ruby
|
@@ -35,13 +35,13 @@ end
|
|
35
35
|
However, since workers are forked right after boot, most codepaths have never been executed,
|
36
36
|
so most of these caches are not yet initialized.
|
37
37
|
|
38
|
-
As more code
|
39
|
-
of shared memory of a
|
38
|
+
As more code gets executed, more and more memory pages get invalidated. If you were to graph the ratio
|
39
|
+
of shared memory of a Ruby process over time, you'd likely see a logarithmic curve, with a quick degradation
|
40
40
|
during the first few processed request as the most common code paths get warmed up, and then a stabilization.
|
41
41
|
|
42
42
|
### Reforking
|
43
43
|
|
44
|
-
That is where reforking helps. Since
|
44
|
+
That is where reforking helps. Since most of these invalidations only happen when a code path is executed for the
|
45
45
|
first time, if you take a warmed up worker out of rotation, and use it to fork new workers, warmed up pages will
|
46
46
|
be shared again, and most of them won't be invalidated anymore.
|
47
47
|
|
@@ -123,9 +123,9 @@ PID COMMAND
|
|
123
123
|
```
|
124
124
|
|
125
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).
|
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).
|
126
|
+
This means that any descendant process that is orphaned will be re-parented as a child of the master process rather than a child of the init process (pid 1).
|
127
127
|
|
128
|
-
With this in mind, the mold forks twice to create an orphaned process that will get re-attached to the master,
|
128
|
+
With this in mind, the mold forks twice to create an orphaned process that will get re-attached to the master process,
|
129
129
|
effectively forking a sibling rather than a child. Similarly, workers do the same when forking new molds.
|
130
130
|
This technique eases killing previous generations of molds and workers.
|
131
131
|
|
data/docs/SIGNALS.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
## Signal handling
|
2
2
|
|
3
|
-
In general, signals need only be sent to the master process. However,
|
3
|
+
In general, signals need to only be sent to the master process. However,
|
4
4
|
the signals Pitchfork uses internally to communicate with the worker
|
5
5
|
processes are documented here as well.
|
6
6
|
|
@@ -22,8 +22,8 @@ processes are documented here as well.
|
|
22
22
|
### Worker Processes
|
23
23
|
|
24
24
|
Note: the master uses a pipe to signal workers
|
25
|
-
instead of `kill(2)` for most cases. Using signals still
|
26
|
-
remains supported for external tools/libraries
|
25
|
+
instead of `kill(2)` for most cases. Using signals still works and
|
26
|
+
remains supported for external tools/libraries, however.
|
27
27
|
|
28
28
|
Sending signals directly to the worker processes should not normally be
|
29
29
|
needed. If the master process is running, any exited worker will be
|
@@ -21,6 +21,16 @@
|
|
21
21
|
# define USE_EPOLL (0)
|
22
22
|
#endif
|
23
23
|
|
24
|
+
#ifndef HAVE_RB_IO_DESCRIPTOR /* Ruby < 3.1 */
|
25
|
+
static int rb_io_descriptor(VALUE io)
|
26
|
+
{
|
27
|
+
rb_io_t *fptr;
|
28
|
+
GetOpenFile(io, fptr);
|
29
|
+
rb_io_check_closed(fptr);
|
30
|
+
return fptr->fd;
|
31
|
+
}
|
32
|
+
#endif
|
33
|
+
|
24
34
|
#if USE_EPOLL
|
25
35
|
/*
|
26
36
|
* :nodoc:
|
@@ -54,8 +64,7 @@ static VALUE prep_readers(VALUE cls, VALUE readers)
|
|
54
64
|
*/
|
55
65
|
e.events = EPOLLEXCLUSIVE | EPOLLIN;
|
56
66
|
io = rb_io_get_io(io);
|
57
|
-
|
58
|
-
rc = epoll_ctl(epfd, EPOLL_CTL_ADD, fptr->fd, &e);
|
67
|
+
rc = epoll_ctl(epfd, EPOLL_CTL_ADD, rb_io_descriptor(io), &e);
|
59
68
|
if (rc < 0) rb_sys_fail("epoll_ctl");
|
60
69
|
}
|
61
70
|
return epio;
|
@@ -65,7 +74,7 @@ static VALUE prep_readers(VALUE cls, VALUE readers)
|
|
65
74
|
#if USE_EPOLL
|
66
75
|
struct ep_wait {
|
67
76
|
struct epoll_event event;
|
68
|
-
|
77
|
+
VALUE io;
|
69
78
|
int timeout_msec;
|
70
79
|
};
|
71
80
|
|
@@ -79,7 +88,7 @@ static void *do_wait(void *ptr) /* runs w/o GVL */
|
|
79
88
|
* at-a-time (c.f. fs/eventpoll.c in linux.git, it's quite
|
80
89
|
* easy-to-understand for anybody familiar with Ruby C).
|
81
90
|
*/
|
82
|
-
return (void *)(long)epoll_wait(epw->
|
91
|
+
return (void *)(long)epoll_wait(rb_io_descriptor(epw->io), &epw->event, 1,
|
83
92
|
epw->timeout_msec);
|
84
93
|
}
|
85
94
|
|
@@ -93,11 +102,10 @@ get_readers(VALUE epio, VALUE ready, VALUE readers, VALUE timeout_msec)
|
|
93
102
|
|
94
103
|
Check_Type(ready, T_ARRAY);
|
95
104
|
Check_Type(readers, T_ARRAY);
|
96
|
-
|
97
|
-
GetOpenFile(epio, epw.fptr);
|
98
|
-
|
105
|
+
epw.io = rb_io_get_io(epio);
|
99
106
|
epw.timeout_msec = NUM2INT(timeout_msec);
|
100
107
|
n = (long)rb_thread_call_without_gvl(do_wait, &epw, RUBY_UBF_IO, NULL);
|
108
|
+
RB_GC_GUARD(epw.io);
|
101
109
|
if (n < 0) {
|
102
110
|
if (errno != EINTR) rb_sys_fail("epoll_wait");
|
103
111
|
} else if (n > 0) { /* maxevents is hardcoded to 1 */
|
@@ -3,6 +3,8 @@ require 'mkmf'
|
|
3
3
|
|
4
4
|
have_const("PR_SET_CHILD_SUBREAPER", "sys/prctl.h")
|
5
5
|
have_func("rb_enc_interned_str", "ruby.h") # Ruby 3.0+
|
6
|
+
have_func("rb_io_descriptor", "ruby.h") # Ruby 3.1+
|
7
|
+
|
6
8
|
if RUBY_VERSION.start_with?('3.0.')
|
7
9
|
# https://bugs.ruby-lang.org/issues/18772
|
8
10
|
$CFLAGS << ' -DRB_ENC_INTERNED_STR_NULL_CHECK=1 '
|
data/lib/pitchfork/children.rb
CHANGED
@@ -66,6 +66,11 @@ module Pitchfork
|
|
66
66
|
@workers.key?(nr)
|
67
67
|
end
|
68
68
|
|
69
|
+
def abandon(worker)
|
70
|
+
@workers.delete(worker.nr)
|
71
|
+
@pending_workers.delete(worker.nr)
|
72
|
+
end
|
73
|
+
|
69
74
|
def reap(pid)
|
70
75
|
if child = @children.delete(pid)
|
71
76
|
@pending_workers.delete(child.nr)
|
@@ -73,6 +78,9 @@ module Pitchfork
|
|
73
78
|
@molds.delete(child.pid)
|
74
79
|
@workers.delete(child.nr)
|
75
80
|
if @mold == child
|
81
|
+
@pending_workers.reject! do |nr, worker|
|
82
|
+
worker.generation == @mold.generation
|
83
|
+
end
|
76
84
|
@mold = nil
|
77
85
|
end
|
78
86
|
end
|
@@ -99,6 +107,10 @@ module Pitchfork
|
|
99
107
|
@molds.values
|
100
108
|
end
|
101
109
|
|
110
|
+
def empty?
|
111
|
+
@children.empty?
|
112
|
+
end
|
113
|
+
|
102
114
|
def each(&block)
|
103
115
|
@children.each_value(&block)
|
104
116
|
end
|
@@ -126,14 +138,6 @@ module Pitchfork
|
|
126
138
|
end
|
127
139
|
end
|
128
140
|
|
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
|
-
|
137
141
|
def workers
|
138
142
|
@workers.values
|
139
143
|
end
|
@@ -34,9 +34,11 @@ module Pitchfork
|
|
34
34
|
:soft_timeout => 20,
|
35
35
|
:cleanup_timeout => 2,
|
36
36
|
:spawn_timeout => 10,
|
37
|
+
:timeout_signal => -> (_pid) { :KILL },
|
37
38
|
:timeout => 22,
|
38
39
|
:logger => default_logger,
|
39
40
|
:worker_processes => 1,
|
41
|
+
:before_fork => nil,
|
40
42
|
:after_worker_fork => lambda { |server, worker|
|
41
43
|
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
|
42
44
|
},
|
@@ -133,6 +135,10 @@ module Pitchfork
|
|
133
135
|
set[:logger] = obj
|
134
136
|
end
|
135
137
|
|
138
|
+
def before_fork(*args, &block)
|
139
|
+
set_hook(:before_fork, block_given? ? block : args[0], 1)
|
140
|
+
end
|
141
|
+
|
136
142
|
def after_worker_fork(*args, &block)
|
137
143
|
set_hook(:after_worker_fork, block_given? ? block : args[0])
|
138
144
|
end
|
@@ -175,6 +181,19 @@ module Pitchfork
|
|
175
181
|
set_int(:timeout, soft_timeout + cleanup_timeout, 5)
|
176
182
|
end
|
177
183
|
|
184
|
+
def timeout_signal(*args, &block)
|
185
|
+
if block_given?
|
186
|
+
set_hook(:timeout_signal, block, 1)
|
187
|
+
elsif args.first.respond_to?(:call)
|
188
|
+
set_hook(:timeout_signal, args.first, 1)
|
189
|
+
elsif args.first.is_a?(Symbol)
|
190
|
+
signal = args.first
|
191
|
+
set_hook(:timeout_signal, ->(_pid) { signal }, 1)
|
192
|
+
else
|
193
|
+
raise ArgumentError, "timeout_signal must be a symbol or a proc"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
178
197
|
def spawn_timeout(seconds)
|
179
198
|
set_int(:spawn_timeout, seconds, 1)
|
180
199
|
end
|
@@ -193,7 +193,12 @@ module Pitchfork
|
|
193
193
|
|
194
194
|
# called by ext/pitchfork_http/pitchfork_http.rl via rb_funcall
|
195
195
|
def self.is_chunked?(v) # :nodoc:
|
196
|
-
vals = v.split(
|
196
|
+
vals = v.split(',')
|
197
|
+
vals.each do |val|
|
198
|
+
val.strip!
|
199
|
+
val.downcase!
|
200
|
+
end
|
201
|
+
|
197
202
|
if vals.pop == 'chunked'.freeze
|
198
203
|
return true unless vals.include?('chunked'.freeze)
|
199
204
|
raise Pitchfork::HttpParserError, 'double chunked', []
|
@@ -14,6 +14,8 @@ module Pitchfork
|
|
14
14
|
STATUS_CODES = defined?(Rack::Utils::HTTP_STATUS_CODES) ?
|
15
15
|
Rack::Utils::HTTP_STATUS_CODES : {}
|
16
16
|
|
17
|
+
ILLEGAL_HEADER_VALUE = /[\x00-\x08\x0A-\x1F]/
|
18
|
+
|
17
19
|
# internal API, code will always be common-enough-for-even-old-Rack
|
18
20
|
def err_response(code, response_start_sent)
|
19
21
|
"#{response_start_sent ? '' : 'HTTP/1.1 '}" \
|
@@ -23,10 +25,16 @@ module Pitchfork
|
|
23
25
|
def append_header(buf, key, value)
|
24
26
|
case value
|
25
27
|
when Array # Rack 3
|
26
|
-
value.each
|
28
|
+
value.each do |v|
|
29
|
+
next if ILLEGAL_HEADER_VALUE.match?(v)
|
30
|
+
buf << "#{key}: #{v}\r\n"
|
31
|
+
end
|
27
32
|
when /\n/ # Rack 2
|
28
33
|
# avoiding blank, key-only cookies with /\n+/
|
29
|
-
value.split(/\n+/).each
|
34
|
+
value.split(/\n+/).each do |v|
|
35
|
+
next if ILLEGAL_HEADER_VALUE.match?(v)
|
36
|
+
buf << "#{key}: #{v}\r\n"
|
37
|
+
end
|
30
38
|
else
|
31
39
|
buf << "#{key}: #{value}\r\n"
|
32
40
|
end
|
@@ -74,8 +74,8 @@ module Pitchfork
|
|
74
74
|
end
|
75
75
|
|
76
76
|
# :stopdoc:
|
77
|
-
attr_accessor :app, :timeout, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
|
78
|
-
:after_worker_fork, :after_mold_fork,
|
77
|
+
attr_accessor :app, :timeout, :timeout_signal, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
|
78
|
+
:before_fork, :after_worker_fork, :after_mold_fork,
|
79
79
|
:listener_opts, :children,
|
80
80
|
:orig_app, :config, :ready_pipe,
|
81
81
|
:default_middleware, :early_hints
|
@@ -201,7 +201,7 @@ module Pitchfork
|
|
201
201
|
else
|
202
202
|
build_app!
|
203
203
|
bind_listeners!
|
204
|
-
after_mold_fork.call(self, Worker.new(nil, pid: $$).promoted!)
|
204
|
+
after_mold_fork.call(self, Worker.new(nil, pid: $$).promoted!(@spawn_timeout))
|
205
205
|
end
|
206
206
|
|
207
207
|
if sync
|
@@ -315,7 +315,7 @@ module Pitchfork
|
|
315
315
|
end
|
316
316
|
end
|
317
317
|
stop # gracefully shutdown all workers on our way out
|
318
|
-
logger.info "master complete"
|
318
|
+
logger.info "master complete status=#{@exit_status}"
|
319
319
|
@exit_status
|
320
320
|
end
|
321
321
|
|
@@ -368,7 +368,7 @@ module Pitchfork
|
|
368
368
|
when Message::WorkerSpawned
|
369
369
|
worker = @children.update(message)
|
370
370
|
# TODO: should we send a message to the worker to acknowledge?
|
371
|
-
logger.info "worker=#{worker.nr} pid=#{worker.pid} registered"
|
371
|
+
logger.info "worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation} registered"
|
372
372
|
when Message::MoldSpawned
|
373
373
|
new_mold = @children.update(message)
|
374
374
|
logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} spawned")
|
@@ -394,7 +394,7 @@ module Pitchfork
|
|
394
394
|
wait_for_pending_workers
|
395
395
|
self.listeners = []
|
396
396
|
limit = Pitchfork.time_now + timeout
|
397
|
-
until @children.
|
397
|
+
until @children.empty? || Pitchfork.time_now > limit
|
398
398
|
if graceful
|
399
399
|
@children.soft_kill_all(:TERM)
|
400
400
|
else
|
@@ -404,11 +404,17 @@ module Pitchfork
|
|
404
404
|
return StopIteration
|
405
405
|
end
|
406
406
|
end
|
407
|
-
|
407
|
+
|
408
|
+
@children.each do |child|
|
409
|
+
if child.pid
|
410
|
+
@children.hard_kill(@timeout_signal.call(child.pid), child)
|
411
|
+
end
|
412
|
+
end
|
408
413
|
@promotion_lock.unlink
|
409
414
|
end
|
410
415
|
|
411
416
|
def worker_exit(worker)
|
417
|
+
logger.info "worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation} exiting"
|
412
418
|
proc_name status: "exiting"
|
413
419
|
|
414
420
|
if @before_worker_exit
|
@@ -494,8 +500,8 @@ module Pitchfork
|
|
494
500
|
now = Pitchfork.time_now(true)
|
495
501
|
next_sleep = @timeout - 1
|
496
502
|
|
497
|
-
@children.
|
498
|
-
deadline =
|
503
|
+
@children.each do |child|
|
504
|
+
deadline = child.deadline
|
499
505
|
if 0 == deadline # worker is idle
|
500
506
|
next
|
501
507
|
elsif deadline > now # worker still has time
|
@@ -506,28 +512,34 @@ module Pitchfork
|
|
506
512
|
next
|
507
513
|
else # worker is out of time
|
508
514
|
next_sleep = 0
|
509
|
-
hard_timeout(
|
515
|
+
hard_timeout(child)
|
510
516
|
end
|
511
517
|
end
|
512
518
|
|
513
519
|
next_sleep <= 0 ? 1 : next_sleep
|
514
520
|
end
|
515
521
|
|
516
|
-
def hard_timeout(
|
517
|
-
if
|
522
|
+
def hard_timeout(child)
|
523
|
+
if child.pid.nil? # Not yet registered, likely never spawned
|
524
|
+
logger.error "worker=#{child.nr} timed out during spawn, abandoning"
|
525
|
+
@children.abandon(worker)
|
526
|
+
return
|
527
|
+
end
|
528
|
+
|
529
|
+
if @after_worker_hard_timeout && !child.mold?
|
518
530
|
begin
|
519
|
-
@after_worker_hard_timeout.call(self,
|
531
|
+
@after_worker_hard_timeout.call(self, child)
|
520
532
|
rescue => error
|
521
533
|
Pitchfork.log_error(@logger, "after_worker_hard_timeout callback", error)
|
522
534
|
end
|
523
535
|
end
|
524
536
|
|
525
|
-
if
|
526
|
-
logger.error "mold pid=#{
|
537
|
+
if child.mold?
|
538
|
+
logger.error "mold pid=#{child.pid} gen=#{child.generation} timed out, killing"
|
527
539
|
else
|
528
|
-
logger.error "worker=#{
|
540
|
+
logger.error "worker=#{child.nr} pid=#{child.pid} gen=#{child.generation} timed out, killing"
|
529
541
|
end
|
530
|
-
@children.
|
542
|
+
@children.hard_kill(@timeout_signal.call(child.pid), child) # take no prisoners for hard timeout violations
|
531
543
|
end
|
532
544
|
|
533
545
|
def trigger_refork
|
@@ -563,7 +575,8 @@ module Pitchfork
|
|
563
575
|
# reason it gets stuck before reaching the worker loop,
|
564
576
|
# the monitor process will kill it.
|
565
577
|
worker.update_deadline(@spawn_timeout)
|
566
|
-
|
578
|
+
@before_fork&.call(self)
|
579
|
+
fork_sibling("spawn_worker") do
|
567
580
|
worker.pid = Process.pid
|
568
581
|
|
569
582
|
after_fork_internal
|
@@ -598,6 +611,8 @@ module Pitchfork
|
|
598
611
|
worker = Pitchfork::Worker.new(worker_nr)
|
599
612
|
|
600
613
|
if REFORKING_AVAILABLE
|
614
|
+
worker.generation = @children.mold&.generation || 0
|
615
|
+
|
601
616
|
unless @children.mold&.spawn_worker(worker)
|
602
617
|
@logger.error("Failed to send a spawn_worker command")
|
603
618
|
end
|
@@ -707,6 +722,7 @@ module Pitchfork
|
|
707
722
|
|
708
723
|
proc_name status: "requests: #{worker.requests_count}, processing: #{env["PATH_INFO"]}"
|
709
724
|
|
725
|
+
env["pitchfork.worker"] = worker
|
710
726
|
timeout_handler.rack_env = env
|
711
727
|
env["pitchfork.timeout"] = timeout_handler
|
712
728
|
|
@@ -784,15 +800,18 @@ module Pitchfork
|
|
784
800
|
readers << worker
|
785
801
|
trap(:QUIT) { nuke_listeners!(readers) }
|
786
802
|
trap(:TERM) { nuke_listeners!(readers) }
|
803
|
+
trap(:INT) { nuke_listeners!(readers); exit!(0) }
|
787
804
|
readers
|
788
805
|
end
|
789
806
|
|
790
807
|
def init_mold_process(mold)
|
791
|
-
proc_name role: "(gen:#{mold.generation}) mold", status: "
|
808
|
+
proc_name role: "(gen:#{mold.generation}) mold", status: "init"
|
792
809
|
after_mold_fork.call(self, mold)
|
793
810
|
readers = [mold]
|
794
811
|
trap(:QUIT) { nuke_listeners!(readers) }
|
795
812
|
trap(:TERM) { nuke_listeners!(readers) }
|
813
|
+
trap(:INT) { nuke_listeners!(readers); exit!(0) }
|
814
|
+
proc_name role: "(gen:#{mold.generation}) mold", status: "ready"
|
796
815
|
readers
|
797
816
|
end
|
798
817
|
|
@@ -831,7 +850,7 @@ module Pitchfork
|
|
831
850
|
case client
|
832
851
|
when Message::PromoteWorker
|
833
852
|
if Info.fork_safe?
|
834
|
-
spawn_mold(worker
|
853
|
+
spawn_mold(worker)
|
835
854
|
else
|
836
855
|
logger.error("worker=#{worker.nr} gen=#{worker.generation} is no longer fork safe, can't refork")
|
837
856
|
end
|
@@ -852,8 +871,8 @@ module Pitchfork
|
|
852
871
|
if @refork_condition && Info.fork_safe? && !worker.outdated?
|
853
872
|
if @refork_condition.met?(worker, logger)
|
854
873
|
proc_name status: "requests: #{worker.requests_count}, spawning mold"
|
855
|
-
if spawn_mold(worker
|
856
|
-
logger.info("Refork condition met, promoting ourselves")
|
874
|
+
if spawn_mold(worker)
|
875
|
+
logger.info("worker=#{worker.nr} gen=#{worker.generation} Refork condition met, promoting ourselves")
|
857
876
|
end
|
858
877
|
@refork_condition.backoff!
|
859
878
|
end
|
@@ -867,13 +886,17 @@ module Pitchfork
|
|
867
886
|
end
|
868
887
|
end
|
869
888
|
|
870
|
-
def spawn_mold(
|
889
|
+
def spawn_mold(worker)
|
871
890
|
return false unless @promotion_lock.try_lock
|
872
891
|
|
892
|
+
worker.update_deadline(@spawn_timeout)
|
893
|
+
|
894
|
+
@before_fork&.call(self)
|
895
|
+
|
873
896
|
begin
|
874
|
-
|
875
|
-
mold = Worker.new(nil, pid: Process.pid, generation:
|
876
|
-
mold.promote!
|
897
|
+
fork_sibling("spawn_mold") do
|
898
|
+
mold = Worker.new(nil, pid: Process.pid, generation: worker.generation)
|
899
|
+
mold.promote!(@spawn_timeout)
|
877
900
|
mold.start_promotion(@control_socket[1])
|
878
901
|
mold_loop(mold)
|
879
902
|
end
|
@@ -907,8 +930,18 @@ module Pitchfork
|
|
907
930
|
when false
|
908
931
|
# no message, keep looping
|
909
932
|
when Message::SpawnWorker
|
933
|
+
retries = 1
|
910
934
|
begin
|
911
935
|
spawn_worker(Worker.new(message.nr, generation: mold.generation), detach: true)
|
936
|
+
rescue ForkFailure
|
937
|
+
if retries > 0
|
938
|
+
@logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a worker. Retrying.")
|
939
|
+
retries -= 1
|
940
|
+
retry
|
941
|
+
else
|
942
|
+
@logger.fatal("mold pid=#{mold.pid} gen=#{mold.generation} Failed to spawn a worker twice in a row. Corrupted mold process?")
|
943
|
+
Process.exit(1)
|
944
|
+
end
|
912
945
|
rescue => error
|
913
946
|
raise BootFailure, error.message
|
914
947
|
end
|
@@ -977,5 +1010,61 @@ module Pitchfork
|
|
977
1010
|
handler.timeout_request = SoftTimeout.request(@soft_timeout, handler)
|
978
1011
|
handler
|
979
1012
|
end
|
1013
|
+
|
1014
|
+
FORK_TIMEOUT = 5
|
1015
|
+
|
1016
|
+
def fork_sibling(role, &block)
|
1017
|
+
if REFORKING_AVAILABLE
|
1018
|
+
r, w = Pitchfork::Info.keep_ios(IO.pipe)
|
1019
|
+
# We double fork so that the new worker is re-attached back
|
1020
|
+
# to the master.
|
1021
|
+
# This requires either PR_SET_CHILD_SUBREAPER which is exclusive to Linux 3.4
|
1022
|
+
# or the master to be PID 1.
|
1023
|
+
if middle_pid = FORK_LOCK.synchronize { Process.fork } # parent
|
1024
|
+
w.close
|
1025
|
+
# We need to wait(2) so that the middle process doesn't end up a zombie.
|
1026
|
+
# The process only call fork again an exit so it should be pretty fast.
|
1027
|
+
# However it might need to execute some `Process._fork` or `at_exit` callbacks,
|
1028
|
+
# as well as Ruby's cleanup procedure to run finalizers etc, and there is a risk
|
1029
|
+
# of deadlock.
|
1030
|
+
# So in case it takes more than 5 seconds to exit, we kill it.
|
1031
|
+
# TODO: rather than to busy loop here, we handle it in the worker/mold loop
|
1032
|
+
process_wait_with_timeout(middle_pid, FORK_TIMEOUT)
|
1033
|
+
pid_str = r.gets
|
1034
|
+
r.close
|
1035
|
+
if pid_str
|
1036
|
+
Integer(pid_str)
|
1037
|
+
else
|
1038
|
+
raise ForkFailure, "fork_sibling didn't succeed in #{FORK_TIMEOUT} seconds"
|
1039
|
+
end
|
1040
|
+
else # first child
|
1041
|
+
r.close
|
1042
|
+
Process.setproctitle("<pitchfork fork_sibling(#{role})>")
|
1043
|
+
pid = Pitchfork.clean_fork do
|
1044
|
+
# detach into a grand child
|
1045
|
+
w.close
|
1046
|
+
yield
|
1047
|
+
end
|
1048
|
+
w.puts(pid)
|
1049
|
+
w.close
|
1050
|
+
exit
|
1051
|
+
end
|
1052
|
+
else
|
1053
|
+
Pitchfork.clean_fork(&block)
|
1054
|
+
end
|
1055
|
+
end
|
1056
|
+
|
1057
|
+
def process_wait_with_timeout(pid, timeout)
|
1058
|
+
(timeout * 50).times do
|
1059
|
+
_, status = Process.waitpid2(pid, Process::WNOHANG)
|
1060
|
+
return status if status
|
1061
|
+
sleep 0.02 # 50 * 20ms => 1s
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
# The process didn't exit in the allotted time, so we kill it.
|
1065
|
+
Process.kill(@timeout_signal.call(pid), pid)
|
1066
|
+
_, status = Process.waitpid2(pid)
|
1067
|
+
status
|
1068
|
+
end
|
980
1069
|
end
|
981
1070
|
end
|
data/lib/pitchfork/info.rb
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
module Pitchfork
|
4
4
|
module ReforkCondition
|
5
|
+
@backoff_delay = 10.0
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :backoff_delay
|
9
|
+
end
|
10
|
+
|
5
11
|
class RequestsCount
|
6
12
|
def initialize(request_counts)
|
7
13
|
@limits = request_counts
|
@@ -31,7 +37,7 @@ module Pitchfork
|
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
|
-
def backoff!(delay =
|
40
|
+
def backoff!(delay = ReforkCondition.backoff_delay)
|
35
41
|
@backoff_until = Pitchfork.time_now + delay
|
36
42
|
end
|
37
43
|
end
|
@@ -10,7 +10,8 @@ module Pitchfork
|
|
10
10
|
CURRENT_GENERATION_OFFSET = 0
|
11
11
|
SHUTDOWN_OFFSET = 1
|
12
12
|
MOLD_TICK_OFFSET = 2
|
13
|
-
|
13
|
+
MOLD_PROMOTION_TICK_OFFSET = 3
|
14
|
+
WORKER_TICK_OFFSET = 4
|
14
15
|
|
15
16
|
DROPS = [Raindrops.new(PER_DROP)]
|
16
17
|
|
@@ -49,6 +50,10 @@ module Pitchfork
|
|
49
50
|
self[MOLD_TICK_OFFSET]
|
50
51
|
end
|
51
52
|
|
53
|
+
def mold_promotion_deadline
|
54
|
+
self[MOLD_PROMOTION_TICK_OFFSET]
|
55
|
+
end
|
56
|
+
|
52
57
|
def worker_deadline(worker_nr)
|
53
58
|
self[WORKER_TICK_OFFSET + worker_nr]
|
54
59
|
end
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
@@ -27,7 +27,7 @@ module Pitchfork
|
|
27
27
|
@deadline_drop = SharedMemory.worker_deadline(nr)
|
28
28
|
self.deadline = 0
|
29
29
|
else
|
30
|
-
promoted!
|
30
|
+
promoted!(nil)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
@@ -55,6 +55,13 @@ module Pitchfork
|
|
55
55
|
message.class.members.each do |member|
|
56
56
|
send("#{member}=", message.public_send(member))
|
57
57
|
end
|
58
|
+
|
59
|
+
case message
|
60
|
+
when Message::MoldSpawned
|
61
|
+
@deadline_drop = SharedMemory.mold_promotion_deadline
|
62
|
+
when Message::MoldReady
|
63
|
+
@deadline_drop = SharedMemory.mold_deadline
|
64
|
+
end
|
58
65
|
end
|
59
66
|
|
60
67
|
def register_to_master(control_socket)
|
@@ -75,6 +82,7 @@ module Pitchfork
|
|
75
82
|
message = Message::MoldReady.new(@nr, @pid, generation)
|
76
83
|
control_socket.sendmsg(message)
|
77
84
|
SharedMemory.current_generation = @generation
|
85
|
+
@deadline_drop = SharedMemory.mold_deadline
|
78
86
|
end
|
79
87
|
|
80
88
|
def promote(generation)
|
@@ -85,16 +93,16 @@ module Pitchfork
|
|
85
93
|
send_message_nonblock(Message::SpawnWorker.new(new_worker.nr))
|
86
94
|
end
|
87
95
|
|
88
|
-
def promote!
|
96
|
+
def promote!(timeout)
|
89
97
|
@generation += 1
|
90
|
-
promoted!
|
98
|
+
promoted!(timeout)
|
91
99
|
end
|
92
100
|
|
93
|
-
def promoted!
|
101
|
+
def promoted!(timeout)
|
94
102
|
@mold = true
|
95
103
|
@nr = nil
|
96
|
-
@deadline_drop = SharedMemory.
|
97
|
-
|
104
|
+
@deadline_drop = SharedMemory.mold_promotion_deadline
|
105
|
+
update_deadline(timeout) if timeout
|
98
106
|
self
|
99
107
|
end
|
100
108
|
|
@@ -143,10 +151,6 @@ module Pitchfork
|
|
143
151
|
Process.kill(sig, pid)
|
144
152
|
end
|
145
153
|
|
146
|
-
def hard_timeout!
|
147
|
-
hard_kill(:KILL)
|
148
|
-
end
|
149
|
-
|
150
154
|
# this only runs when the Rack app.call is not running
|
151
155
|
# act like a listener
|
152
156
|
def accept_nonblock(exception: nil) # :nodoc:
|
data/lib/pitchfork.rb
CHANGED
@@ -33,6 +33,7 @@ module Pitchfork
|
|
33
33
|
ClientShutdown = Class.new(EOFError)
|
34
34
|
|
35
35
|
BootFailure = Class.new(StandardError)
|
36
|
+
ForkFailure = Class.new(StandardError)
|
36
37
|
|
37
38
|
# :stopdoc:
|
38
39
|
|
@@ -196,43 +197,6 @@ module Pitchfork
|
|
196
197
|
end
|
197
198
|
end
|
198
199
|
|
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
|
-
# 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)
|
212
|
-
else # first child
|
213
|
-
Process.setproctitle("<pitchfork fork_sibling>")
|
214
|
-
clean_fork(&block) # detach into a grand child
|
215
|
-
exit
|
216
|
-
end
|
217
|
-
else
|
218
|
-
clean_fork(&block)
|
219
|
-
end
|
220
|
-
|
221
|
-
nil # it's tricky to return the PID
|
222
|
-
end
|
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
|
-
|
236
200
|
def time_now(int = false)
|
237
201
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, int ? :second : :float_second)
|
238
202
|
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.
|
4
|
+
version: 0.11.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: raindrops
|
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
140
|
requirements: []
|
141
|
-
rubygems_version: 3.
|
141
|
+
rubygems_version: 3.5.5
|
142
142
|
signing_key:
|
143
143
|
specification_version: 4
|
144
144
|
summary: Rack HTTP server for fast clients and Unix
|