pitchfork 0.1.1 → 0.2.0
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 +13 -0
- data/Dockerfile +1 -1
- data/Gemfile.lock +7 -6
- data/Rakefile +10 -2
- data/docs/CONFIGURATION.md +30 -32
- data/examples/pitchfork.conf.rb +2 -2
- data/exe/pitchfork +1 -8
- data/ext/pitchfork_http/epollexclusive.h +13 -17
- data/ext/pitchfork_http/pitchfork_http.c +196 -192
- data/ext/pitchfork_http/pitchfork_http.rl +23 -19
- data/lib/pitchfork/configurator.rb +28 -36
- data/lib/pitchfork/flock.rb +51 -0
- data/lib/pitchfork/http_response.rb +13 -6
- data/lib/pitchfork/http_server.rb +91 -85
- data/lib/pitchfork/refork_condition.rb +3 -3
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +21 -16
- data/lib/pitchfork.rb +25 -31
- metadata +4 -4
- data/lib/pitchfork/mold_selector.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9f15127612a30958ee43b9667990a0728b5fdfc46e13285bdfa1706085eb67d
|
4
|
+
data.tar.gz: '08ba863ccf209644093b83ee905305615cfef046c4cd5e11e74f16540c722ad7'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c73e64f686acde16c5bb50df83a3a8596d9a3f578318cdc55bc3d8dbee1a0d7dd8534e627b5baa4994f3f30d4917ffe7d4e5d405850d068f7523cf52c21113b
|
7
|
+
data.tar.gz: 80c872414fe8289c7c98cb9949309f76aeec63b3f3de85f09349839f873721cdb41c12bfa4620c7863d62526533c7e4ee900b7f5461917d348fa79d4c5977e4a
|
data/.github/workflows/ci.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
- Remove default middlewares.
|
4
|
+
- Refork indefinitely when `refork_after` is set, unless the last element is `false`.
|
5
|
+
- Remove `mold_selector`. The promotion logic has been moved inside workers (#38).
|
6
|
+
- Add the `after_promotion` callback.
|
7
|
+
- Removed the `before_fork` callback.
|
8
|
+
- Fork workers and molds with a clean stack to allow more generations. (#30)
|
9
|
+
|
10
|
+
# 0.1.2
|
11
|
+
|
12
|
+
- Improve Ruby 3.2 and Rack 3 compatibility.
|
13
|
+
|
14
|
+
# 0.1.1
|
15
|
+
|
3
16
|
- Fix `extconf.rb` to move the extension in the right place on gem install. (#18)
|
4
17
|
|
5
18
|
# 0.1.0
|
data/Dockerfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,26 +1,27 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pitchfork (0.
|
4
|
+
pitchfork (0.2.0)
|
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.
|
11
|
+
minitest (5.15.0)
|
12
12
|
nio4r (2.5.8)
|
13
|
-
puma (
|
13
|
+
puma (6.1.1)
|
14
14
|
nio4r (~> 2.0)
|
15
|
-
rack (3.0.
|
16
|
-
raindrops (0.20.
|
15
|
+
rack (3.0.7)
|
16
|
+
raindrops (0.20.1)
|
17
17
|
rake (13.0.6)
|
18
|
-
rake-compiler (1.2.
|
18
|
+
rake-compiler (1.2.1)
|
19
19
|
rake
|
20
20
|
|
21
21
|
PLATFORMS
|
22
22
|
aarch64-linux
|
23
23
|
arm64-darwin-21
|
24
|
+
arm64-darwin-22
|
24
25
|
x86_64-linux
|
25
26
|
|
26
27
|
DEPENDENCIES
|
data/Rakefile
CHANGED
@@ -11,6 +11,14 @@ Rake::TestTask.new("test:unit") do |t|
|
|
11
11
|
t.warning = true
|
12
12
|
end
|
13
13
|
|
14
|
+
Rake::TestTask.new("test:integration") do |t|
|
15
|
+
t.libs << "test"
|
16
|
+
t.libs << "lib"
|
17
|
+
t.test_files = FileList["test/integration/**/test_*.rb"]
|
18
|
+
t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
|
19
|
+
t.warning = true
|
20
|
+
end
|
21
|
+
|
14
22
|
namespace :test do
|
15
23
|
# It's not so much that these tests are slow, but they tend to fork
|
16
24
|
# and/or register signal handlers, so they if something goes wrong
|
@@ -32,7 +40,7 @@ namespace :test do
|
|
32
40
|
# It's quite hard to work with and it would be good to convert all this
|
33
41
|
# to Ruby integration tests, but while pitchfork is a moving target it's
|
34
42
|
# preferable to edit the test suite as little as possible.
|
35
|
-
task
|
43
|
+
task legacy_integration: :compile do
|
36
44
|
File.write("test/integration/random_blob", File.read("/dev/random", 1_000_000))
|
37
45
|
lib = File.expand_path("lib", __dir__)
|
38
46
|
path = "#{File.expand_path("exe", __dir__)}:#{ENV["PATH"]}"
|
@@ -67,6 +75,6 @@ task :ragel do
|
|
67
75
|
end
|
68
76
|
end
|
69
77
|
|
70
|
-
task test: %i(test:unit test:slow test:integration)
|
78
|
+
task test: %i(test:unit test:slow test:integration test:legacy_integration)
|
71
79
|
|
72
80
|
task default: %i(ragel compile test)
|
data/docs/CONFIGURATION.md
CHANGED
@@ -238,20 +238,27 @@ application that are used in hooks.
|
|
238
238
|
`pitchfork` also don't attempt to rescue hook errors. Raising from a worker hook will crash the worker,
|
239
239
|
and raising from a master hook will bring the whole cluster down.
|
240
240
|
|
241
|
-
### `
|
241
|
+
### `after_promotion`
|
242
242
|
|
243
243
|
```ruby
|
244
|
-
|
244
|
+
after_promotion do |server, mold|
|
245
245
|
Database.disconnect!
|
246
246
|
end
|
247
247
|
```
|
248
248
|
|
249
|
-
Called in the context of the
|
249
|
+
Called in the context of the mold when initially spawned or promotted.
|
250
|
+
|
251
|
+
It's usage is similar to a `before_fork` callback found on other servers
|
252
|
+
but it is called once on promotion rather than before forking each worker.
|
253
|
+
|
250
254
|
For most protocols connections can be closed after fork, but some
|
251
255
|
stateful protocols require to close connections before fork.
|
252
256
|
|
253
257
|
That is the case for instance of many SQL databases protocols.
|
254
258
|
|
259
|
+
This is also the callback in which memory optimizations, such as
|
260
|
+
heap compaction should be done.
|
261
|
+
|
255
262
|
### `after_fork`
|
256
263
|
|
257
264
|
```ruby
|
@@ -263,6 +270,19 @@ end
|
|
263
270
|
Called in the worker after forking. Generally used to close inherited connections
|
264
271
|
or to restart backgrounds threads for libraries that don't do it automatically.
|
265
272
|
|
273
|
+
### `after_promotion`
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
after_promotion do |server, mold|
|
277
|
+
NetworkClient.disconnect!
|
278
|
+
4.times { GC.start } # promote surviving objects to oldgen
|
279
|
+
GC.compact
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
Called in the worker after it was promoted into a mold. Generally used to shutdown
|
284
|
+
open connections and file descriptors, as well as to perform memory optimiations
|
285
|
+
such as compacting the heap, trimming memory etc.
|
266
286
|
|
267
287
|
### `after_worker_ready`
|
268
288
|
|
@@ -302,9 +322,15 @@ once at least one worker processed `50` requests.
|
|
302
322
|
|
303
323
|
Each element is a limit for the next generation. On the example above a new generation
|
304
324
|
is triggered when a worker has processed 50 requests, then the second generation when
|
305
|
-
a worker from the new generation processed an additional 100 requests and finally after
|
325
|
+
a worker from the new generation processed an additional 100 requests and finally after *every*
|
306
326
|
1000 requests.
|
307
327
|
|
328
|
+
If you don't want unlimited reforking, you can set `false` as the last element of the array:
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
refork_after [50, 100, 1000, false]
|
332
|
+
```
|
333
|
+
|
308
334
|
Generally speaking Copy-on-Write efficiency tend to degrade fast during the early requests,
|
309
335
|
and then less and less frequently.
|
310
336
|
|
@@ -314,36 +340,8 @@ By default automatic reforking isn't enabled.
|
|
314
340
|
|
315
341
|
Make sure to read the [fork safety guide](FORK_SAFETY.md) before enabling reforking.
|
316
342
|
|
317
|
-
### `mold_selector`
|
318
|
-
|
319
|
-
Sets the mold selector implementation.
|
320
|
-
|
321
|
-
```ruby
|
322
|
-
mold_selector do |server|
|
323
|
-
candidate = server.children.workers.sample # return an random worker
|
324
|
-
server.logger.info("worker=#{worker.nr} pid=#{worker.pid} selected as new mold")
|
325
|
-
candidate
|
326
|
-
end
|
327
|
-
```
|
328
|
-
|
329
|
-
The has access to `server.children` a `Pitchfork::Children` instance.
|
330
|
-
This object can be used to introspect the state of the cluster and select the most
|
331
|
-
appropriate worker to be used as the new mold from which workers will be reforked.
|
332
|
-
|
333
|
-
The default implementation selects the worker with the least
|
334
|
-
amount of shared memory. This heuristic aim to select the most
|
335
|
-
warmed up worker.
|
336
|
-
|
337
|
-
This should be considered a very advanced API and it is discouraged
|
338
|
-
to use it unless you are confident you have a clear understanding
|
339
|
-
of pitchfork's architecture.
|
340
|
-
|
341
343
|
## Rack Features
|
342
344
|
|
343
|
-
### `default_middleware`
|
344
|
-
|
345
|
-
Sets whether to add Pitchfork's default middlewares. Defaults to `true`.
|
346
|
-
|
347
345
|
### `early_hints`
|
348
346
|
|
349
347
|
Sets whether to enable the proposed early hints Rack API. Defaults to `false`.
|
data/examples/pitchfork.conf.rb
CHANGED
@@ -23,9 +23,9 @@ check_client_connection false
|
|
23
23
|
# local variable to guard against running a hook multiple times
|
24
24
|
run_once = true
|
25
25
|
|
26
|
-
|
26
|
+
after_promotion do |server, worker|
|
27
27
|
# the following is highly recommended for Rails
|
28
|
-
# as there's no need for the
|
28
|
+
# as there's no need for the mold process to hold a connection
|
29
29
|
defined?(ActiveRecord::Base) and
|
30
30
|
ActiveRecord::Base.connection.disconnect!
|
31
31
|
|
data/exe/pitchfork
CHANGED
@@ -6,7 +6,6 @@ require 'optparse'
|
|
6
6
|
ENV["RACK_ENV"] ||= "development"
|
7
7
|
rackup_opts = Pitchfork::Configurator::RACKUP
|
8
8
|
options = rackup_opts[:options]
|
9
|
-
set_no_default_middleware = true
|
10
9
|
|
11
10
|
op = OptionParser.new("", 24, ' ') do |opts|
|
12
11
|
cmd = File.basename($0)
|
@@ -59,11 +58,6 @@ op = OptionParser.new("", 24, ' ') do |opts|
|
|
59
58
|
ENV["RACK_ENV"] = e
|
60
59
|
end
|
61
60
|
|
62
|
-
opts.on("-N", "--no-default-middleware",
|
63
|
-
"do not load middleware implied by RACK_ENV") do |e|
|
64
|
-
rackup_opts[:no_default_middleware] = true if set_no_default_middleware
|
65
|
-
end
|
66
|
-
|
67
61
|
opts.on("-s", "--server SERVER",
|
68
62
|
"this flag only exists for compatibility") do |s|
|
69
63
|
warn "-s/--server only exists for compatibility with rackup"
|
@@ -101,7 +95,6 @@ op = OptionParser.new("", 24, ' ') do |opts|
|
|
101
95
|
opts.parse! ARGV
|
102
96
|
end
|
103
97
|
|
104
|
-
set_no_default_middleware = false
|
105
98
|
app = Pitchfork.builder(ARGV[0] || 'config.ru', op)
|
106
99
|
op = nil
|
107
100
|
|
@@ -113,4 +106,4 @@ if $DEBUG
|
|
113
106
|
})
|
114
107
|
end
|
115
108
|
|
116
|
-
Pitchfork::HttpServer.new(app, options).start.join
|
109
|
+
exit(Pitchfork::HttpServer.new(app, options).start(false).join)
|
@@ -64,9 +64,8 @@ static VALUE prep_readers(VALUE cls, VALUE readers)
|
|
64
64
|
|
65
65
|
#if USE_EPOLL
|
66
66
|
struct ep_wait {
|
67
|
-
struct epoll_event
|
67
|
+
struct epoll_event event;
|
68
68
|
rb_io_t *fptr;
|
69
|
-
int maxevents;
|
70
69
|
int timeout_msec;
|
71
70
|
};
|
72
71
|
|
@@ -74,8 +73,14 @@ static void *do_wait(void *ptr) /* runs w/o GVL */
|
|
74
73
|
{
|
75
74
|
struct ep_wait *epw = ptr;
|
76
75
|
|
77
|
-
|
78
|
-
|
76
|
+
/*
|
77
|
+
* Linux delivers epoll events in the order received, and using
|
78
|
+
* maxevents=1 ensures we pluck one item off ep->rdllist
|
79
|
+
* at-a-time (c.f. fs/eventpoll.c in linux.git, it's quite
|
80
|
+
* easy-to-understand for anybody familiar with Ruby C).
|
81
|
+
*/
|
82
|
+
return (void *)(long)epoll_wait(epw->fptr->fd, &epw->event, 1,
|
83
|
+
epw->timeout_msec);
|
79
84
|
}
|
80
85
|
|
81
86
|
/* :nodoc: */
|
@@ -84,14 +89,10 @@ static VALUE
|
|
84
89
|
get_readers(VALUE epio, VALUE ready, VALUE readers, VALUE timeout_msec)
|
85
90
|
{
|
86
91
|
struct ep_wait epw;
|
87
|
-
long
|
88
|
-
VALUE buf;
|
92
|
+
long n;
|
89
93
|
|
90
94
|
Check_Type(ready, T_ARRAY);
|
91
95
|
Check_Type(readers, T_ARRAY);
|
92
|
-
epw.maxevents = RARRAY_LENINT(readers);
|
93
|
-
buf = rb_str_buf_new(sizeof(struct epoll_event) * epw.maxevents);
|
94
|
-
epw.events = (struct epoll_event *)RSTRING_PTR(buf);
|
95
96
|
epio = rb_io_get_io(epio);
|
96
97
|
GetOpenFile(epio, epw.fptr);
|
97
98
|
|
@@ -99,17 +100,12 @@ get_readers(VALUE epio, VALUE ready, VALUE readers, VALUE timeout_msec)
|
|
99
100
|
n = (long)rb_thread_call_without_gvl(do_wait, &epw, RUBY_UBF_IO, NULL);
|
100
101
|
if (n < 0) {
|
101
102
|
if (errno != EINTR) rb_sys_fail("epoll_wait");
|
102
|
-
|
103
|
-
|
104
|
-
/* Linux delivers events in order received */
|
105
|
-
for (i = 0; i < n; i++) {
|
106
|
-
struct epoll_event *ev = &epw.events[i];
|
107
|
-
VALUE obj = rb_ary_entry(readers, ev->data.u64);
|
103
|
+
} else if (n > 0) { /* maxevents is hardcoded to 1 */
|
104
|
+
VALUE obj = rb_ary_entry(readers, epw.event.data.u64);
|
108
105
|
|
109
106
|
if (RTEST(obj))
|
110
107
|
rb_ary_push(ready, obj);
|
111
|
-
}
|
112
|
-
rb_str_resize(buf, 0);
|
108
|
+
} /* n == 0 : timeout */
|
113
109
|
return Qfalse;
|
114
110
|
}
|
115
111
|
#endif /* USE_EPOLL */
|