pitchfork 0.4.1 → 0.6.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/CHANGELOG.md +16 -0
- data/Gemfile.lock +1 -1
- data/benchmark/cow_benchmark.rb +1 -1
- data/docs/Application_Timeouts.md +1 -1
- data/docs/CONFIGURATION.md +92 -11
- data/docs/SIGNALS.md +4 -4
- data/lib/pitchfork/configurator.rb +32 -7
- data/lib/pitchfork/http_server.rb +145 -35
- data/lib/pitchfork/info.rb +30 -0
- data/lib/pitchfork/shared_memory.rb +71 -0
- data/lib/pitchfork/soft_timeout.rb +113 -0
- data/lib/pitchfork/version.rb +1 -1
- data/lib/pitchfork/worker.rb +17 -44
- data/lib/pitchfork.rb +8 -4
- metadata +5 -3
- data/lib/pitchfork/preread_input.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3a7c87f91b13b474f5c204a251c380cd091190f3ecfca8ac3107d697e883f12
|
4
|
+
data.tar.gz: '00116558f20d20e4aad0cda6a1866fc7104e3712df27b1668470c52346812258'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b51ac98fdc2fc2d83a72c6a55c5e26e42996b43bb6e33815e8958c6f5c729f11b17676b9b8bdb098d75b53ebac5bc98114f2bfa864550b3111162824ae6597f
|
7
|
+
data.tar.gz: 5bbf64cf105b20930fd6627001abfb610c9f87ad8510b35a7ab1fcb5c5ccd5437eec011eb6253aefdf87f7d7bc353ac2abbaeb30ac6afad91ddd48fbe81f475c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 0.6.0
|
4
|
+
|
5
|
+
- Expose `Pitchfork::Info.workers_count` and `.live_workers_count` to be consumed by application health checks.
|
6
|
+
- Implement `before_worker_exit` callback.
|
7
|
+
- Make each mold and worker a process group leader.
|
8
|
+
- Get rid of `Pitchfork::PrereadInput`.
|
9
|
+
- Add `Pitchfork.shutting_down?` to allow health check endpoints to fail sooner on graceful shutdowns.
|
10
|
+
- Treat `TERM` as graceful shutdown rather than quick shutdown.
|
11
|
+
- Implement `after_worker_hard_timeout` callback.
|
12
|
+
|
13
|
+
# 0.5.0
|
14
|
+
|
15
|
+
- Added a soft timeout in addition to the historical Unicorn hard timeout.
|
16
|
+
On soft timeout, the `after_worker_timeout` callback is invoked.
|
17
|
+
- Implement `after_request_complete` callback.
|
18
|
+
|
3
19
|
# 0.4.1
|
4
20
|
|
5
21
|
- Avoid a Rack 3 deprecation warning.
|
data/Gemfile.lock
CHANGED
data/benchmark/cow_benchmark.rb
CHANGED
@@ -70,5 +70,5 @@ handle network/server failures.
|
|
70
70
|
The `timeout` mechanism in pitchfork is an extreme solution that should
|
71
71
|
be avoided whenever possible.
|
72
72
|
It will help preserve the platform if your application or a dependency
|
73
|
-
has a bug that cause it to either get stuck or
|
73
|
+
has a bug that cause it to either get stuck or be too slow, but it is not a
|
74
74
|
solution to such bugs, merely a mitigation.
|
data/docs/CONFIGURATION.md
CHANGED
@@ -173,33 +173,55 @@ The following options may be specified (but are generally not needed):
|
|
173
173
|
### `timeout`
|
174
174
|
|
175
175
|
```ruby
|
176
|
-
timeout 10
|
176
|
+
timeout 10, cleanup: 3
|
177
177
|
```
|
178
178
|
|
179
|
-
Sets the timeout
|
180
|
-
Workers handling the request/app.call/response cycle taking longer than
|
181
|
-
this time period will be forcibly killed (via `SIGKILL`).
|
179
|
+
Sets the timeout for worker processes to a number of seconds.
|
182
180
|
|
183
|
-
|
181
|
+
Note that Pitchfork has two layers of timeout.
|
182
|
+
|
183
|
+
A first "soft" timeout will invoke the `after_worker_timeout` from
|
184
|
+
within the worker (but from a background thread) and then call `exit`
|
185
|
+
to terminate the worker cleanly.
|
186
|
+
|
187
|
+
The second "hard" timeout, is the sum of `timeout` and `cleanup`.
|
188
|
+
Workers taking longer than this time period to be ready to handle a new
|
189
|
+
request will be forcibly killed (via `SIGKILL`).
|
190
|
+
|
191
|
+
Neither of these timeout mecanisms should be routinely relied on, and should
|
184
192
|
instead be considered as a last line of defense in case you application
|
185
193
|
is impacted by bugs causing unexpectedly slow response time, or fully stuck
|
186
194
|
processes.
|
187
195
|
|
196
|
+
If some of the application endpoints require an unreasonably large timeout,
|
197
|
+
rather than to increase the global application timeout, it is possible to
|
198
|
+
adjust it on a per request basis via the rack request environment:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
class MyMiddleware
|
202
|
+
def call(env)
|
203
|
+
if slow_endpoint?(env)
|
204
|
+
# Give 10 more seconds
|
205
|
+
env["pitchfork.timeout"]&.extend_deadline(10)
|
206
|
+
end
|
207
|
+
@app.call(env)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
188
212
|
Make sure to read the guide on [application timeouts](Application_Timeouts.md).
|
189
213
|
|
190
|
-
This configuration defaults to a (too) generous 20 seconds
|
191
|
-
|
192
|
-
profile.
|
214
|
+
This configuration defaults to a (too) generous 20 seconds for the soft timeout
|
215
|
+
and an extra 2 seconds for the hard timeout. It is highly recommended to set a
|
216
|
+
stricter one based on your application profile.
|
193
217
|
|
194
|
-
This timeout is enforced by the master process itself and not subject
|
195
|
-
to the scheduling limitations by the worker process.
|
196
218
|
Due the low-complexity, low-overhead implementation, timeouts of less
|
197
219
|
than 3.0 seconds can be considered inaccurate and unsafe.
|
198
220
|
|
199
221
|
For running Pitchfork behind nginx, it is recommended to set
|
200
222
|
"fail_timeout=0" for in your nginx configuration like this
|
201
223
|
to have nginx always retry backends that may have had workers
|
202
|
-
SIGKILL-ed due to timeouts.
|
224
|
+
exit or be SIGKILL-ed due to timeouts.
|
203
225
|
|
204
226
|
```
|
205
227
|
upstream pitchfork_backend {
|
@@ -284,6 +306,51 @@ after_worker_ready do |server, worker|
|
|
284
306
|
end
|
285
307
|
```
|
286
308
|
|
309
|
+
### `after_worker_timeout`
|
310
|
+
|
311
|
+
Called by the worker process when the request timeout is elapsed:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
after_worker_timeout do |server, worker, timeout_info|
|
315
|
+
timeout_info.copy_thread_variables!
|
316
|
+
timeout_info.thread.kill
|
317
|
+
server.logger.error("Request timed out: #{timeout_info.rack_env.inspect}")
|
318
|
+
$stderr.puts timeout_info.thread.backtrace
|
319
|
+
end
|
320
|
+
```
|
321
|
+
|
322
|
+
Note that this callback is invoked from a different thread. You can access the
|
323
|
+
main thread via `timeout_info.thread`, as well as the rack environment via `timeout_info.rack_env`.
|
324
|
+
|
325
|
+
If you need to invoke cleanup code that rely on thread local state, you can copy
|
326
|
+
that state with `timeout_info.copy_thread_variables!`, but it's best avoided as the
|
327
|
+
thread local state could contain thread unsafe objects.
|
328
|
+
|
329
|
+
Also note that at this stage, the thread is still alive, if your callback does
|
330
|
+
substantial work, you may want to kill the thread.
|
331
|
+
|
332
|
+
After the callback is executed the worker will exit with status `0`.
|
333
|
+
|
334
|
+
It is recommended not to do slow operations in this callback, but if you
|
335
|
+
really have to, make sure to configure the `cleanup` timeout so that the
|
336
|
+
callback has time to complete before the "hard" timeout triggers.
|
337
|
+
By default the cleanup timeout is 2 seconds.
|
338
|
+
|
339
|
+
### `after_worker_hard_timeout`
|
340
|
+
|
341
|
+
Called in the master process when a worker hard timeout is elapsed:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
after_worker_timeout do |server, worker|
|
345
|
+
$stderr.puts "Worker hard timeout, pid=#{worker.pid}"
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
Once the callback complete, the worker will be signaled with `SIGKILL`.
|
350
|
+
|
351
|
+
This callback being called in an indication that something is preventing the
|
352
|
+
soft timeout from working.
|
353
|
+
|
287
354
|
### `after_worker_exit`
|
288
355
|
|
289
356
|
Called in the master process after a worker exits.
|
@@ -297,6 +364,20 @@ after_worker_exit do |server, worker, status|
|
|
297
364
|
end
|
298
365
|
```
|
299
366
|
|
367
|
+
### `after_request_complete`
|
368
|
+
|
369
|
+
Called in the worker processes after a request has completed.
|
370
|
+
|
371
|
+
Can be used for out of band work, or to exit unhealthy workers.
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
after_request_complete do |server, worker|
|
375
|
+
if something_wrong?
|
376
|
+
exit
|
377
|
+
end
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
300
381
|
## Reforking
|
301
382
|
|
302
383
|
### `refork_after`
|
data/docs/SIGNALS.md
CHANGED
@@ -6,9 +6,9 @@ processes are documented here as well.
|
|
6
6
|
|
7
7
|
### Master Process
|
8
8
|
|
9
|
-
* `INT
|
9
|
+
* `INT` - quick shutdown, kills all workers immediately
|
10
10
|
|
11
|
-
* `QUIT` - graceful shutdown, waits for workers to finish their
|
11
|
+
* `QUIT/TERM` - graceful shutdown, waits for workers to finish their
|
12
12
|
current request before finishing.
|
13
13
|
|
14
14
|
* `USR2` - trigger a manual refork. A worker is promoted as
|
@@ -29,10 +29,10 @@ 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
|
30
30
|
automatically respawned.
|
31
31
|
|
32
|
-
* `INT
|
32
|
+
* `INT` - Quick shutdown, immediately exit.
|
33
33
|
The master process will respawn a worker to replace this one.
|
34
34
|
Immediate shutdown is still triggered using kill(2) and not the
|
35
35
|
internal pipe as of unicorn 4.8
|
36
36
|
|
37
|
-
* `QUIT` - Gracefully exit after finishing the current request.
|
37
|
+
* `QUIT/TERM` - Gracefully exit after finishing the current request.
|
38
38
|
The master process will respawn a worker to replace this one.
|
@@ -26,9 +26,15 @@ module Pitchfork
|
|
26
26
|
}
|
27
27
|
|
28
28
|
# Default settings for Pitchfork
|
29
|
+
default_logger = Logger.new($stderr)
|
30
|
+
default_logger.formatter = Logger::Formatter.new
|
31
|
+
default_logger.progname = "[Pitchfork]"
|
32
|
+
|
29
33
|
DEFAULTS = {
|
30
|
-
:
|
31
|
-
:
|
34
|
+
:soft_timeout => 20,
|
35
|
+
:cleanup_timeout => 2,
|
36
|
+
:timeout => 22,
|
37
|
+
:logger => default_logger,
|
32
38
|
:worker_processes => 1,
|
33
39
|
:after_worker_fork => lambda { |server, worker|
|
34
40
|
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
|
@@ -36,6 +42,7 @@ module Pitchfork
|
|
36
42
|
:after_mold_fork => lambda { |server, worker|
|
37
43
|
server.logger.info("mold gen=#{worker.generation} pid=#{$$} spawned")
|
38
44
|
},
|
45
|
+
:before_worker_exit => nil,
|
39
46
|
:after_worker_exit => lambda { |server, worker, status|
|
40
47
|
m = if worker.nil?
|
41
48
|
"repead unknown process (#{status.inspect})"
|
@@ -53,6 +60,9 @@ module Pitchfork
|
|
53
60
|
:after_worker_ready => lambda { |server, worker|
|
54
61
|
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} ready")
|
55
62
|
},
|
63
|
+
:after_worker_timeout => nil,
|
64
|
+
:after_worker_hard_timeout => nil,
|
65
|
+
:after_request_complete => nil,
|
56
66
|
:early_hints => false,
|
57
67
|
:refork_condition => nil,
|
58
68
|
:check_client_connection => false,
|
@@ -131,15 +141,30 @@ module Pitchfork
|
|
131
141
|
set_hook(:after_worker_ready, block_given? ? block : args[0])
|
132
142
|
end
|
133
143
|
|
144
|
+
def after_worker_timeout(*args, &block)
|
145
|
+
set_hook(:after_worker_timeout, block_given? ? block : args[0], 3)
|
146
|
+
end
|
147
|
+
|
148
|
+
def after_worker_hard_timeout(*args, &block)
|
149
|
+
set_hook(:after_worker_hard_timeout, block_given? ? block : args[0], 2)
|
150
|
+
end
|
151
|
+
|
152
|
+
def before_worker_exit(*args, &block)
|
153
|
+
set_hook(:before_worker_exit, block_given? ? block : args[0], 2)
|
154
|
+
end
|
155
|
+
|
134
156
|
def after_worker_exit(*args, &block)
|
135
157
|
set_hook(:after_worker_exit, block_given? ? block : args[0], 3)
|
136
158
|
end
|
137
159
|
|
138
|
-
def
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
160
|
+
def after_request_complete(*args, &block)
|
161
|
+
set_hook(:after_request_complete, block_given? ? block : args[0])
|
162
|
+
end
|
163
|
+
|
164
|
+
def timeout(seconds, cleanup: 2)
|
165
|
+
soft_timeout = set_int(:soft_timeout, seconds, 3)
|
166
|
+
cleanup_timeout = set_int(:cleanup_timeout, cleanup, 2)
|
167
|
+
set_int(:timeout, soft_timeout + cleanup_timeout, 5)
|
143
168
|
end
|
144
169
|
|
145
170
|
def worker_processes(nr)
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
2
|
require 'pitchfork/pitchfork_http'
|
3
3
|
require 'pitchfork/flock'
|
4
|
+
require 'pitchfork/soft_timeout'
|
5
|
+
require 'pitchfork/shared_memory'
|
6
|
+
require 'pitchfork/info'
|
4
7
|
|
5
8
|
module Pitchfork
|
6
9
|
# This is the process manager of Pitchfork. This manages worker
|
@@ -8,13 +11,76 @@ module Pitchfork
|
|
8
11
|
# Listener sockets are started in the master process and shared with
|
9
12
|
# forked worker children.
|
10
13
|
class HttpServer
|
14
|
+
class TimeoutHandler
|
15
|
+
class Info
|
16
|
+
attr_reader :thread, :rack_env
|
17
|
+
|
18
|
+
def initialize(thread, rack_env)
|
19
|
+
@thread = thread
|
20
|
+
@rack_env = rack_env
|
21
|
+
end
|
22
|
+
|
23
|
+
def copy_thread_variables!
|
24
|
+
current_thread = Thread.current
|
25
|
+
@thread.keys.each do |key|
|
26
|
+
current_thread[key] = @thread[key]
|
27
|
+
end
|
28
|
+
@thread.thread_variables.each do |variable|
|
29
|
+
current_thread.thread_variable_set(variable, @thread.thread_variable_get(variable))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_writer :rack_env, :timeout_request # :nodoc:
|
35
|
+
|
36
|
+
def initialize(server, worker, callback) # :nodoc:
|
37
|
+
@server = server
|
38
|
+
@worker = worker
|
39
|
+
@callback = callback
|
40
|
+
@rack_env = nil
|
41
|
+
@timeout_request = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
"#<Pitchfork::HttpServer::TimeoutHandler##{object_id}>"
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(original_thread) # :nodoc:
|
49
|
+
begin
|
50
|
+
@server.logger.error("worker=#{@worker.nr} pid=#{@worker.pid} timed out, exiting")
|
51
|
+
if @callback
|
52
|
+
@callback.call(@server, @worker, Info.new(original_thread, @rack_env))
|
53
|
+
end
|
54
|
+
rescue => error
|
55
|
+
Pitchfork.log_error(@server.logger, "after_worker_timeout error", error)
|
56
|
+
end
|
57
|
+
@server.worker_exit(@worker)
|
58
|
+
end
|
59
|
+
|
60
|
+
def finished # :nodoc:
|
61
|
+
@timeout_request.finished
|
62
|
+
end
|
63
|
+
|
64
|
+
def deadline
|
65
|
+
@timeout_request.deadline
|
66
|
+
end
|
67
|
+
|
68
|
+
def extend_deadline(extra_time)
|
69
|
+
extra_time = Integer(extra_time)
|
70
|
+
@worker.deadline += extra_time
|
71
|
+
@timeout_request.extend_deadline(extra_time)
|
72
|
+
self
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
11
76
|
# :stopdoc:
|
12
|
-
attr_accessor :app, :timeout, :worker_processes,
|
77
|
+
attr_accessor :app, :timeout, :soft_timeout, :cleanup_timeout, :worker_processes,
|
13
78
|
:after_worker_fork, :after_mold_fork,
|
14
79
|
:listener_opts, :children,
|
15
80
|
:orig_app, :config, :ready_pipe,
|
16
81
|
:default_middleware, :early_hints
|
17
|
-
attr_writer :after_worker_exit, :after_worker_ready, :
|
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
|
18
84
|
|
19
85
|
attr_reader :logger
|
20
86
|
include Pitchfork::SocketHelper
|
@@ -100,7 +166,9 @@ module Pitchfork
|
|
100
166
|
# list of signals we care about and trap in master.
|
101
167
|
@queue_sigs = [
|
102
168
|
:QUIT, :INT, :TERM, :USR2, :TTIN, :TTOU ]
|
103
|
-
|
169
|
+
|
170
|
+
Info.workers_count = worker_processes
|
171
|
+
SharedMemory.preallocate_drops(worker_processes)
|
104
172
|
end
|
105
173
|
|
106
174
|
# Runs the thing. Returns self so you can run join on it
|
@@ -249,7 +317,7 @@ module Pitchfork
|
|
249
317
|
if REFORKING_AVAILABLE && @respawn && @children.molds.empty?
|
250
318
|
logger.info("No mold alive, shutting down")
|
251
319
|
@exit_status = 1
|
252
|
-
@sig_queue << :
|
320
|
+
@sig_queue << :TERM
|
253
321
|
@respawn = false
|
254
322
|
end
|
255
323
|
|
@@ -269,10 +337,12 @@ module Pitchfork
|
|
269
337
|
end
|
270
338
|
|
271
339
|
master_sleep(sleep_time) if sleep
|
272
|
-
when :QUIT # graceful shutdown
|
273
|
-
|
340
|
+
when :QUIT, :TERM # graceful shutdown
|
341
|
+
SharedMemory.shutting_down!
|
342
|
+
logger.info "#{message} received, starting graceful shutdown"
|
274
343
|
return StopIteration
|
275
|
-
when :
|
344
|
+
when :INT # immediate shutdown
|
345
|
+
SharedMemory.shutting_down!
|
276
346
|
logger.info "#{message} received, starting immediate shutdown"
|
277
347
|
stop(false)
|
278
348
|
return StopIteration
|
@@ -296,7 +366,7 @@ module Pitchfork
|
|
296
366
|
logger.info("mold pid=#{new_mold.pid} gen=#{new_mold.generation} ready")
|
297
367
|
old_molds.each do |old_mold|
|
298
368
|
logger.info("Terminating old mold pid=#{old_mold.pid} gen=#{old_mold.generation}")
|
299
|
-
old_mold.soft_kill(:
|
369
|
+
old_mold.soft_kill(:TERM)
|
300
370
|
end
|
301
371
|
else
|
302
372
|
logger.error("Unexpected message in sig_queue #{message.inspect}")
|
@@ -307,14 +377,15 @@ module Pitchfork
|
|
307
377
|
# Terminates all workers, but does not exit master process
|
308
378
|
def stop(graceful = true)
|
309
379
|
@respawn = false
|
380
|
+
SharedMemory.shutting_down!
|
310
381
|
wait_for_pending_workers
|
311
382
|
self.listeners = []
|
312
383
|
limit = Pitchfork.time_now + timeout
|
313
384
|
until @children.workers.empty? || Pitchfork.time_now > limit
|
314
385
|
if graceful
|
315
|
-
soft_kill_each_child(:
|
386
|
+
soft_kill_each_child(:TERM)
|
316
387
|
else
|
317
|
-
kill_each_child(:
|
388
|
+
kill_each_child(:INT)
|
318
389
|
end
|
319
390
|
if monitor_loop(false) == StopIteration
|
320
391
|
return StopIteration
|
@@ -324,6 +395,17 @@ module Pitchfork
|
|
324
395
|
@promotion_lock.unlink
|
325
396
|
end
|
326
397
|
|
398
|
+
def worker_exit(worker)
|
399
|
+
if @before_worker_exit
|
400
|
+
begin
|
401
|
+
@before_worker_exit.call(self, worker)
|
402
|
+
rescue => error
|
403
|
+
Pitchfork.log_error(logger, "before_worker_exit error", error)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
Process.exit
|
407
|
+
end
|
408
|
+
|
327
409
|
def rewindable_input
|
328
410
|
Pitchfork::HttpParser.input_class.method_defined?(:rewind)
|
329
411
|
end
|
@@ -394,25 +476,39 @@ module Pitchfork
|
|
394
476
|
|
395
477
|
# forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File
|
396
478
|
def murder_lazy_workers
|
397
|
-
next_sleep = @timeout - 1
|
398
479
|
now = Pitchfork.time_now(true)
|
480
|
+
next_sleep = @timeout - 1
|
481
|
+
|
399
482
|
@children.workers.each do |worker|
|
400
|
-
|
401
|
-
0 ==
|
402
|
-
diff = now - tick
|
403
|
-
tmp = @timeout - diff
|
404
|
-
if tmp >= 0
|
405
|
-
next_sleep > tmp and next_sleep = tmp
|
483
|
+
deadline = worker.deadline
|
484
|
+
if 0 == deadline # worker is idle
|
406
485
|
next
|
486
|
+
elsif deadline > now # worker still has time
|
487
|
+
time_left = deadline - now
|
488
|
+
if time_left < next_sleep
|
489
|
+
next_sleep = time_left
|
490
|
+
end
|
491
|
+
next
|
492
|
+
else # worker is out of time
|
493
|
+
next_sleep = 0
|
494
|
+
if worker.mold?
|
495
|
+
logger.error "mold pid=#{worker.pid} timed out, killing"
|
496
|
+
else
|
497
|
+
logger.error "worker=#{worker.nr} pid=#{worker.pid} timed out, killing"
|
498
|
+
end
|
499
|
+
|
500
|
+
if @after_worker_hard_timeout
|
501
|
+
begin
|
502
|
+
@after_worker_hard_timeout.call(self, worker)
|
503
|
+
rescue => error
|
504
|
+
Pitchfork.log_error(@logger, "after_worker_hard_timeout callback", error)
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
kill_worker(:KILL, worker.pid) # take no prisoners for hard timeout violations
|
407
509
|
end
|
408
|
-
next_sleep = 0
|
409
|
-
if worker.mold?
|
410
|
-
logger.error "mold pid=#{worker.pid} timeout (#{diff}s > #{@timeout}s), killing"
|
411
|
-
else
|
412
|
-
logger.error "worker=#{worker.nr} pid=#{worker.pid} timeout (#{diff}s > #{@timeout}s), killing"
|
413
|
-
end
|
414
|
-
kill_worker(:KILL, worker.pid) # take no prisoners for timeout violations
|
415
510
|
end
|
511
|
+
|
416
512
|
next_sleep <= 0 ? 1 : next_sleep
|
417
513
|
end
|
418
514
|
|
@@ -451,6 +547,7 @@ module Pitchfork
|
|
451
547
|
|
452
548
|
after_fork_internal
|
453
549
|
worker_loop(worker)
|
550
|
+
worker_exit(worker)
|
454
551
|
end
|
455
552
|
|
456
553
|
worker
|
@@ -509,7 +606,7 @@ module Pitchfork
|
|
509
606
|
def maintain_worker_count
|
510
607
|
(off = @children.workers_count - worker_processes) == 0 and return
|
511
608
|
off < 0 and return spawn_missing_workers
|
512
|
-
@children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:
|
609
|
+
@children.each_worker { |w| w.nr >= worker_processes and w.soft_kill(:TERM) }
|
513
610
|
end
|
514
611
|
|
515
612
|
def restart_outdated_workers
|
@@ -526,7 +623,7 @@ module Pitchfork
|
|
526
623
|
outdated_workers = @children.workers.select { |w| !w.exiting? && w.generation < @children.mold.generation }
|
527
624
|
outdated_workers.each do |worker|
|
528
625
|
logger.info("worker=#{worker.nr} pid=#{worker.pid} gen=#{worker.generation} restarting")
|
529
|
-
worker.soft_kill(:
|
626
|
+
worker.soft_kill(:TERM)
|
530
627
|
end
|
531
628
|
end
|
532
629
|
end
|
@@ -577,9 +674,12 @@ module Pitchfork
|
|
577
674
|
|
578
675
|
# once a client is accepted, it is processed in its entirety here
|
579
676
|
# in 3 easy steps: read request, call app, write app response
|
580
|
-
def process_client(client)
|
677
|
+
def process_client(client, timeout_handler)
|
678
|
+
env = nil
|
581
679
|
@request = Pitchfork::HttpParser.new
|
582
680
|
env = @request.read(client)
|
681
|
+
timeout_handler.rack_env = env
|
682
|
+
env["pitchfork.timeout"] = timeout_handler
|
583
683
|
|
584
684
|
if early_hints
|
585
685
|
env["rack.early_hints"] = lambda do |headers|
|
@@ -616,6 +716,7 @@ module Pitchfork
|
|
616
716
|
handle_error(client, e)
|
617
717
|
ensure
|
618
718
|
env["rack.after_reply"].each(&:call) if env
|
719
|
+
timeout_handler.finished
|
619
720
|
end
|
620
721
|
|
621
722
|
def nuke_listeners!(readers)
|
@@ -632,7 +733,7 @@ module Pitchfork
|
|
632
733
|
def init_worker_process(worker)
|
633
734
|
worker.reset
|
634
735
|
worker.register_to_master(@control_socket[1])
|
635
|
-
# we'll re-trap :QUIT later for graceful shutdown iff we accept clients
|
736
|
+
# we'll re-trap :QUIT and :TERM later for graceful shutdown iff we accept clients
|
636
737
|
exit_sigs = [ :QUIT, :TERM, :INT ]
|
637
738
|
exit_sigs.each { |sig| trap(sig) { exit!(0) } }
|
638
739
|
exit!(0) if (@sig_queue & exit_sigs)[0]
|
@@ -650,6 +751,7 @@ module Pitchfork
|
|
650
751
|
readers = LISTENERS.dup
|
651
752
|
readers << worker
|
652
753
|
trap(:QUIT) { nuke_listeners!(readers) }
|
754
|
+
trap(:TERM) { nuke_listeners!(readers) }
|
653
755
|
readers
|
654
756
|
end
|
655
757
|
|
@@ -658,6 +760,7 @@ module Pitchfork
|
|
658
760
|
after_mold_fork.call(self, mold)
|
659
761
|
readers = [mold]
|
660
762
|
trap(:QUIT) { nuke_listeners!(readers) }
|
763
|
+
trap(:TERM) { nuke_listeners!(readers) }
|
661
764
|
readers
|
662
765
|
end
|
663
766
|
|
@@ -684,7 +787,7 @@ module Pitchfork
|
|
684
787
|
|
685
788
|
while readers[0]
|
686
789
|
begin
|
687
|
-
worker.
|
790
|
+
worker.update_deadline(@timeout)
|
688
791
|
while sock = ready.shift
|
689
792
|
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
690
793
|
# but that will return false
|
@@ -697,15 +800,16 @@ module Pitchfork
|
|
697
800
|
when Message
|
698
801
|
worker.update(client)
|
699
802
|
else
|
700
|
-
process_client(client)
|
803
|
+
process_client(client, prepare_timeout(worker))
|
804
|
+
@after_request_complete&.call(self, worker)
|
701
805
|
worker.increment_requests_count
|
702
806
|
end
|
703
|
-
worker.
|
807
|
+
worker.update_deadline(@timeout)
|
704
808
|
end
|
705
809
|
end
|
706
810
|
|
707
|
-
# timeout so we can .
|
708
|
-
worker.
|
811
|
+
# timeout so we can update .deadline and keep parent from SIGKILL-ing us
|
812
|
+
worker.update_deadline(@timeout)
|
709
813
|
|
710
814
|
if @refork_condition && !worker.outdated?
|
711
815
|
if @refork_condition.met?(worker, logger)
|
@@ -754,7 +858,7 @@ module Pitchfork
|
|
754
858
|
|
755
859
|
while readers[0]
|
756
860
|
begin
|
757
|
-
mold.
|
861
|
+
mold.update_deadline(@timeout)
|
758
862
|
while sock = ready.shift
|
759
863
|
# Pitchfork::Worker#accept_nonblock is not like accept(2) at all,
|
760
864
|
# but that will return false
|
@@ -774,7 +878,7 @@ module Pitchfork
|
|
774
878
|
end
|
775
879
|
|
776
880
|
# timeout so we can .tick and keep parent from SIGKILL-ing us
|
777
|
-
mold.
|
881
|
+
mold.update_deadline(@timeout)
|
778
882
|
waiter.get_readers(ready, readers, @timeout * 500) # to milliseconds, but halved
|
779
883
|
rescue => e
|
780
884
|
Pitchfork.log_error(@logger, "mold loop error", e) if readers[0]
|
@@ -832,5 +936,11 @@ module Pitchfork
|
|
832
936
|
listeners.each { |addr| listen(addr) }
|
833
937
|
raise ArgumentError, "no listeners" if LISTENERS.empty?
|
834
938
|
end
|
939
|
+
|
940
|
+
def prepare_timeout(worker)
|
941
|
+
handler = TimeoutHandler.new(self, worker, @after_worker_timeout)
|
942
|
+
handler.timeout_request = SoftTimeout.request(@soft_timeout, handler)
|
943
|
+
handler
|
944
|
+
end
|
835
945
|
end
|
836
946
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pitchfork/shared_memory'
|
4
|
+
|
5
|
+
module Pitchfork
|
6
|
+
module Info
|
7
|
+
@workers_count = 0
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :workers_count
|
11
|
+
|
12
|
+
def live_workers_count
|
13
|
+
now = Pitchfork.time_now(true)
|
14
|
+
(0...workers_count).count do |nr|
|
15
|
+
SharedMemory.worker_deadline(nr).value > now
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns true if the server is shutting down.
|
20
|
+
# This can be useful to implement health check endpoints, so they
|
21
|
+
# can fail immediately after TERM/QUIT/INT was received by the master
|
22
|
+
# process.
|
23
|
+
# Otherwise they may succeed while Pitchfork is draining requests causing
|
24
|
+
# more requests to be sent.
|
25
|
+
def shutting_down?
|
26
|
+
SharedMemory.shutting_down?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'raindrops'
|
4
|
+
|
5
|
+
module Pitchfork
|
6
|
+
module SharedMemory
|
7
|
+
extend self
|
8
|
+
|
9
|
+
PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
|
10
|
+
CURRENT_GENERATION_OFFSET = 0
|
11
|
+
SHUTDOWN_OFFSET = 1
|
12
|
+
MOLD_TICK_OFFSET = 2
|
13
|
+
WORKER_TICK_OFFSET = 3
|
14
|
+
|
15
|
+
DROPS = [Raindrops.new(PER_DROP)]
|
16
|
+
|
17
|
+
def current_generation
|
18
|
+
DROPS[0][CURRENT_GENERATION_OFFSET]
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_generation=(value)
|
22
|
+
DROPS[0][CURRENT_GENERATION_OFFSET] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def shutting_down!
|
26
|
+
DROPS[0][SHUTDOWN_OFFSET] = 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def shutting_down?
|
30
|
+
DROPS[0][SHUTDOWN_OFFSET] > 0
|
31
|
+
end
|
32
|
+
|
33
|
+
class Field
|
34
|
+
def initialize(offset)
|
35
|
+
@drop = DROPS.fetch(offset / PER_DROP)
|
36
|
+
@offset = offset % PER_DROP
|
37
|
+
end
|
38
|
+
|
39
|
+
def value
|
40
|
+
@drop[@offset]
|
41
|
+
end
|
42
|
+
|
43
|
+
def value=(value)
|
44
|
+
@drop[@offset] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def mold_deadline
|
49
|
+
self[MOLD_TICK_OFFSET]
|
50
|
+
end
|
51
|
+
|
52
|
+
def worker_deadline(worker_nr)
|
53
|
+
self[WORKER_TICK_OFFSET + worker_nr]
|
54
|
+
end
|
55
|
+
|
56
|
+
def [](offset)
|
57
|
+
Field.new(offset)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Since workers are created from another process, we have to
|
61
|
+
# pre-allocate the drops so they are shared between everyone.
|
62
|
+
#
|
63
|
+
# However this doesn't account for TTIN signals that increase the
|
64
|
+
# number of workers, but we should probably remove that feature too.
|
65
|
+
def preallocate_drops(workers_count)
|
66
|
+
0.upto(((WORKER_TICK_OFFSET + workers_count) / PER_DROP.to_f).ceil) do |i|
|
67
|
+
DROPS[i] ||= Raindrops.new(PER_DROP)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pitchfork
|
4
|
+
# :stopdoc:
|
5
|
+
module SoftTimeout
|
6
|
+
extend self
|
7
|
+
|
8
|
+
CONDVAR = ConditionVariable.new
|
9
|
+
QUEUE = Queue.new
|
10
|
+
QUEUE_MUTEX = Mutex.new
|
11
|
+
TIMEOUT_THREAD_MUTEX = Mutex.new
|
12
|
+
@timeout_thread = nil
|
13
|
+
private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX
|
14
|
+
|
15
|
+
class Request
|
16
|
+
attr_reader :deadline, :thread
|
17
|
+
|
18
|
+
def initialize(thread, timeout, block)
|
19
|
+
@thread = thread
|
20
|
+
@deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
21
|
+
@block = block
|
22
|
+
|
23
|
+
@mutex = Mutex.new
|
24
|
+
@done = false # protected by @mutex
|
25
|
+
end
|
26
|
+
|
27
|
+
def extend_deadline(timeout)
|
28
|
+
@deadline += timeout
|
29
|
+
QUEUE_MUTEX.synchronize do
|
30
|
+
CONDVAR.signal
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def done?
|
36
|
+
@mutex.synchronize do
|
37
|
+
@done
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def expired?(now)
|
42
|
+
now >= @deadline
|
43
|
+
end
|
44
|
+
|
45
|
+
def interrupt
|
46
|
+
@mutex.synchronize do
|
47
|
+
unless @done
|
48
|
+
begin
|
49
|
+
@block.call(@thread)
|
50
|
+
ensure
|
51
|
+
@done = true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def finished
|
58
|
+
@mutex.synchronize do
|
59
|
+
@done = true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def request(sec, callback)
|
65
|
+
ensure_timeout_thread_created
|
66
|
+
request = Request.new(Thread.current, sec, callback)
|
67
|
+
QUEUE_MUTEX.synchronize do
|
68
|
+
QUEUE << request
|
69
|
+
CONDVAR.signal
|
70
|
+
end
|
71
|
+
request
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def create_timeout_thread
|
77
|
+
watcher = Thread.new do
|
78
|
+
requests = []
|
79
|
+
while true
|
80
|
+
until QUEUE.empty? and !requests.empty? # wait to have at least one request
|
81
|
+
req = QUEUE.pop
|
82
|
+
requests << req unless req.done?
|
83
|
+
end
|
84
|
+
closest_deadline = requests.min_by(&:deadline).deadline
|
85
|
+
|
86
|
+
now = 0.0
|
87
|
+
QUEUE_MUTEX.synchronize do
|
88
|
+
while (now = Process.clock_gettime(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty?
|
89
|
+
CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
requests.each do |req|
|
94
|
+
req.interrupt if req.expired?(now)
|
95
|
+
end
|
96
|
+
requests.reject!(&:done?)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
watcher.name = "Pitchfork::Timeout"
|
100
|
+
watcher
|
101
|
+
end
|
102
|
+
|
103
|
+
def ensure_timeout_thread_created
|
104
|
+
unless @timeout_thread and @timeout_thread.alive?
|
105
|
+
TIMEOUT_THREAD_MUTEX.synchronize do
|
106
|
+
unless @timeout_thread and @timeout_thread.alive?
|
107
|
+
@timeout_thread = create_timeout_thread
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/pitchfork/version.rb
CHANGED
data/lib/pitchfork/worker.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
require
|
2
|
+
require 'pitchfork/shared_memory'
|
3
3
|
|
4
4
|
module Pitchfork
|
5
5
|
# This class and its members can be considered a stable interface
|
@@ -24,7 +24,8 @@ module Pitchfork
|
|
24
24
|
@exiting = false
|
25
25
|
@requests_count = 0
|
26
26
|
if nr
|
27
|
-
|
27
|
+
@deadline_drop = SharedMemory.worker_deadline(nr)
|
28
|
+
self.deadline = 0
|
28
29
|
else
|
29
30
|
promoted!
|
30
31
|
end
|
@@ -47,7 +48,7 @@ module Pitchfork
|
|
47
48
|
end
|
48
49
|
|
49
50
|
def outdated?
|
50
|
-
|
51
|
+
SharedMemory.current_generation > @generation
|
51
52
|
end
|
52
53
|
|
53
54
|
def update(message)
|
@@ -73,7 +74,7 @@ module Pitchfork
|
|
73
74
|
def finish_promotion(control_socket)
|
74
75
|
message = Message::MoldReady.new(@nr, @pid, generation)
|
75
76
|
control_socket.sendmsg(message)
|
76
|
-
|
77
|
+
SharedMemory.current_generation = @generation
|
77
78
|
end
|
78
79
|
|
79
80
|
def promote(generation)
|
@@ -92,8 +93,8 @@ module Pitchfork
|
|
92
93
|
def promoted!
|
93
94
|
@mold = true
|
94
95
|
@nr = nil
|
95
|
-
@
|
96
|
-
|
96
|
+
@deadline_drop = SharedMemory.mold_deadline
|
97
|
+
self.deadline = 0
|
97
98
|
self
|
98
99
|
end
|
99
100
|
|
@@ -167,22 +168,18 @@ module Pitchfork
|
|
167
168
|
super || (!@nr.nil? && @nr == other)
|
168
169
|
end
|
169
170
|
|
171
|
+
def update_deadline(timeout)
|
172
|
+
self.deadline = Pitchfork.time_now(true) + timeout
|
173
|
+
end
|
174
|
+
|
170
175
|
# called in the worker process
|
171
|
-
def
|
172
|
-
|
173
|
-
MOLD_DROP[0] = value
|
174
|
-
else
|
175
|
-
@tick_drop[@drop_offset] = value
|
176
|
-
end
|
176
|
+
def deadline=(value) # :nodoc:
|
177
|
+
@deadline_drop.value = value
|
177
178
|
end
|
178
179
|
|
179
180
|
# called in the master process
|
180
|
-
def
|
181
|
-
|
182
|
-
MOLD_DROP[0]
|
183
|
-
else
|
184
|
-
@tick_drop[@drop_offset]
|
185
|
-
end
|
181
|
+
def deadline # :nodoc:
|
182
|
+
@deadline_drop.value
|
186
183
|
end
|
187
184
|
|
188
185
|
def reset
|
@@ -195,6 +192,7 @@ module Pitchfork
|
|
195
192
|
|
196
193
|
# called in both the master (reaping worker) and worker (SIGQUIT handler)
|
197
194
|
def close # :nodoc:
|
195
|
+
self.deadline = 0
|
198
196
|
@master.close if @master
|
199
197
|
@to_io.close if @to_io
|
200
198
|
end
|
@@ -221,35 +219,10 @@ module Pitchfork
|
|
221
219
|
else
|
222
220
|
success = true
|
223
221
|
end
|
224
|
-
rescue Errno::EPIPE, Errno::ECONNRESET
|
222
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ENOTCONN
|
225
223
|
# worker will be reaped soon
|
226
224
|
end
|
227
225
|
success
|
228
226
|
end
|
229
|
-
|
230
|
-
MOLD_DROP = Raindrops.new(1)
|
231
|
-
CURRENT_GENERATION_DROP = Raindrops.new(1)
|
232
|
-
PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
|
233
|
-
TICK_DROPS = []
|
234
|
-
|
235
|
-
class << self
|
236
|
-
# Since workers are created from another process, we have to
|
237
|
-
# pre-allocate the drops so they are shared between everyone.
|
238
|
-
#
|
239
|
-
# However this doesn't account for TTIN signals that increase the
|
240
|
-
# number of workers, but we should probably remove that feature too.
|
241
|
-
def preallocate_drops(workers_count)
|
242
|
-
0.upto(workers_count / PER_DROP) do |i|
|
243
|
-
TICK_DROPS[i] = Raindrops.new(PER_DROP)
|
244
|
-
end
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def build_raindrops(drop_nr)
|
249
|
-
drop_index = drop_nr / PER_DROP
|
250
|
-
@drop_offset = drop_nr % PER_DROP
|
251
|
-
@tick_drop = TICK_DROPS[drop_index] ||= Raindrops.new(PER_DROP)
|
252
|
-
@tick_drop[@drop_offset] = 0
|
253
|
-
end
|
254
227
|
end
|
255
228
|
end
|
data/lib/pitchfork.rb
CHANGED
@@ -26,9 +26,10 @@ module Pitchfork
|
|
26
26
|
# application dispatch. This is always raised with an empty backtrace
|
27
27
|
# since there is nothing in the application stack that is responsible
|
28
28
|
# for client shutdowns/disconnects. This exception is visible to Rack
|
29
|
-
# applications
|
30
|
-
#
|
31
|
-
#
|
29
|
+
# applications. This is a subclass of the standard EOFError class and
|
30
|
+
# applications should not rescue it explicitly, but rescue EOFError instead.
|
31
|
+
# Such an error is likely an indication that the reverse proxy in front
|
32
|
+
# of Pitchfork isn't properly buffering requests.
|
32
33
|
ClientShutdown = Class.new(EOFError)
|
33
34
|
|
34
35
|
BootFailure = Class.new(StandardError)
|
@@ -120,8 +121,11 @@ module Pitchfork
|
|
120
121
|
end
|
121
122
|
end
|
122
123
|
|
123
|
-
def self.clean_fork(&block)
|
124
|
+
def self.clean_fork(setpgid: true, &block)
|
124
125
|
if pid = Process.fork
|
126
|
+
if setpgid
|
127
|
+
Process.setpgid(pid, pid) # Make into a group leader
|
128
|
+
end
|
125
129
|
return pid
|
126
130
|
end
|
127
131
|
|
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.6.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-
|
11
|
+
date: 2023-07-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: raindrops
|
@@ -101,13 +101,15 @@ files:
|
|
101
101
|
- lib/pitchfork/http_parser.rb
|
102
102
|
- lib/pitchfork/http_response.rb
|
103
103
|
- lib/pitchfork/http_server.rb
|
104
|
+
- lib/pitchfork/info.rb
|
104
105
|
- lib/pitchfork/launcher.rb
|
105
106
|
- lib/pitchfork/mem_info.rb
|
106
107
|
- lib/pitchfork/message.rb
|
107
|
-
- lib/pitchfork/preread_input.rb
|
108
108
|
- lib/pitchfork/refork_condition.rb
|
109
109
|
- lib/pitchfork/select_waiter.rb
|
110
|
+
- lib/pitchfork/shared_memory.rb
|
110
111
|
- lib/pitchfork/socket_helper.rb
|
112
|
+
- lib/pitchfork/soft_timeout.rb
|
111
113
|
- lib/pitchfork/stream_input.rb
|
112
114
|
- lib/pitchfork/tee_input.rb
|
113
115
|
- lib/pitchfork/tmpio.rb
|
@@ -1,33 +0,0 @@
|
|
1
|
-
# -*- encoding: binary -*-
|
2
|
-
|
3
|
-
module Pitchfork
|
4
|
-
# This middleware is used to ensure input is buffered to memory
|
5
|
-
# or disk (depending on size) before the application is dispatched
|
6
|
-
# by entirely consuming it (from TeeInput) beforehand.
|
7
|
-
#
|
8
|
-
# Usage (in config.ru):
|
9
|
-
#
|
10
|
-
# require 'pitchfork/preread_input'
|
11
|
-
# if defined?(Pitchfork)
|
12
|
-
# use Pitchfork::PrereadInput
|
13
|
-
# end
|
14
|
-
# run YourApp.new
|
15
|
-
class PrereadInput
|
16
|
-
|
17
|
-
# :stopdoc:
|
18
|
-
def initialize(app)
|
19
|
-
@app = app
|
20
|
-
end
|
21
|
-
|
22
|
-
def call(env)
|
23
|
-
buf = ""
|
24
|
-
input = env["rack.input"]
|
25
|
-
if input.respond_to?(:rewind)
|
26
|
-
true while input.read(16384, buf)
|
27
|
-
input.rewind
|
28
|
-
end
|
29
|
-
@app.call(env)
|
30
|
-
end
|
31
|
-
# :startdoc:
|
32
|
-
end
|
33
|
-
end
|