pitchfork 0.4.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|