You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CHANGELOG.md
+24Lines changed: 24 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,3 +1,27 @@
1
+
## HEAD (unreleased)
2
+
3
+
## 0.6.3
4
+
5
+
- Fix `NoMethodError: undefined method 'logger' for Rails:Module` when Rails is defined as a Module, but is not a full Rails app (https://github.com/zombocom/rack-timeout/pull/180)
6
+
7
+
## 0.6.2
8
+
9
+
- Migrate CI from Travis CI to GitHub Actions (https://github.com/zombocom/rack-timeout/pull/182)
10
+
- Rails 7+ support (https://github.com/zombocom/rack-timeout/pull/184)
11
+
12
+
## 0.6.1
13
+
14
+
- RACK_TIMEOUT_TERM_ON_TIMEOUT can be set to zero to disable (https://github.com/sharpstone/rack-timeout/pull/161)
15
+
- Update the gemspec's homepage to the current repo URL(https://github.com/zombocom/rack-timeout/pull/183)
16
+
17
+
## 0.6.0
18
+
19
+
- Allow sending SIGTERM to workers on timeout (https://github.com/sharpstone/rack-timeout/pull/157)
20
+
21
+
0.5.2
22
+
=====
23
+
- Rails 6 support (#147)
24
+
1
25
0.5.1
2
26
=====
3
27
- Fixes setting ENV vars to false or 0 would not disable a timeout
Because of the aforementioned issues, it's recommended you set library-specific timeouts and leave Rack::Timeout as a last resort measure. Library timeouts will generally take care of IO issues and abort the operation safely. See [The Ultimate Guide to Ruby Timeouts][ruby-timeouts].
Copy file name to clipboardExpand all lines: doc/settings.md
+52Lines changed: 52 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,3 +47,55 @@ This extra time is called *wait overtime* and can be set via `wait_overtime`. It
47
47
Keep in mind that Heroku [recommends][uploads] uploading large files directly to S3, so as to prevent the dyno from being blocked for too long and hence unable to handle further incoming requests.
If your application timeouts fire frequently then [they can cause your application to enter a corrupt state](https://www.schneems.com/2017/02/21/the-oldest-bug-in-ruby-why-racktimeout-might-hose-your-server/). One option for resetting that bad state is to restart the entire process. If you are running in an environment with multiple processes (such as `puma -w 2`) then when a process is sent a `SIGTERM` it will exit. The webserver then knows how to restart the process. For more information on process restart behavior see:
-[License to SIGKILL](https://www.sitepoint.com/license-to-sigkill/)
57
+
58
+
**Puma SIGTERM behavior** When a Puma worker receives a `SIGTERM` it will begin to shut down, but not exit right away. It stops accepting new requests and waits for any existing requests to finish before fully shutting down. This means that only the request that experiences a timeout will be interupted, all other in-flight requests will be allowed to run until they return or also are timed out.
59
+
60
+
After the worker process exists will Puma's parent process know to boot a replacement worker. While one process is restarting, another can still serve requests (if you have more than 1 worker process per server/dyno). Between when a process exits and when a new process boots, there will be a reduction in throughput. If all processes are restarting, then incoming requests will be blocked while new processes boot.
61
+
62
+
**How to enable** To enable this behavior you can set `term_on_timeout: 1` to an integer value. If you set it to one, then the first time the process encounters a timeout, it will receive a SIGTERM.
**Caution** If you use this setting inside of a webserver without enabling multi-process mode, then it will exit the entire server when it fires:
71
+
72
+
- ✅ `puma -w 2 -t 5` This is OKAY
73
+
- ❌ `puma -t 5` This is NOT OKAY
74
+
75
+
If you're using a `config/puma.rb` file then make sure you are calling `workers` configuration DSL. You should see multiple workers when the server boots:
76
+
77
+
```
78
+
[3922] Puma starting in cluster mode...
79
+
[3922] * Version 4.3.0 (ruby 2.6.5-p114), codename: Mysterious Traveller
80
+
[3922] * Min threads: 0, max threads: 16
81
+
[3922] * Environment: development
82
+
[3922] * Process workers: 2
83
+
[3922] * Phased restart available
84
+
[3922] * Listening on tcp://0.0.0.0:9292
85
+
[3922] Use Ctrl-C to stop
86
+
[3922] - Worker 0 (pid: 3924) booted, phase: 0
87
+
[3922] - Worker 1 (pid: 3925) booted, phase: 0
88
+
```
89
+
90
+
> ✅ Notice how it says it is booting in "cluster mode" and how it gives PIDs for two worker processes at the bottom.
91
+
92
+
**How to decide the term_on_timeout value** If you set to a higher value such as `5` then rack-timeout will wait until the process has experienced five timeouts before restarting the process. Setting this value to a higher number means the application restarts processes less frequently, so throughput will be less impacted. If you set it to too high of a number, then the underlying issue of the application being put into a bad state will not be effectively mitigated.
93
+
94
+
95
+
**How do I know when a process is being restarted by rack-timeout?** This exception error should be visible in the logs:
96
+
97
+
```
98
+
Request ran for longer than 1000ms, sending SIGTERM to process 3925
99
+
```
100
+
101
+
> Note: Since the worker waits for all in-flight requests to finish (with puma) you may see multiple SIGTERMs to the same PID before it exits, this means that multiple requests timed out.
:wait_timeout,# How long the request is allowed to have waited before reaching rack. If exceeded, the request is 'expired', i.e. dropped entirely without being passed down to the application.
87
88
:wait_overtime,# Additional time over @wait_timeout for requests with a body, like POST requests. These may take longer to be received by the server before being passed down to the application, but should not be expired.
88
89
:service_past_wait# when false, reduces the request's computed timeout from the service_timeout value if the complete request lifetime (wait + service) would have been longer than wait_timeout (+ wait_overtime when applicable). When true, always uses the service_timeout value. we default to false under the assumption that the router would drop a request that's not responded within wait_timeout, thus being there no point in servicing beyond seconds_service_left (see code further down) up until service_timeout.
90
+
:term_on_timeout
89
91
:exclude# exclude routes with those paths in them from being processed
90
92
:only# only process requests coming from those paths
@only=only == [] ? ENV.fetch("RACK_TIMEOUT_ONLY",[]) : only
102
+
103
+
Thread.main['RACK_TIMEOUT_COUNT'] ||= 0
104
+
if@term_on_timeout
105
+
raise"Current Runtime does not support processes"unless ::Process.respond_to?(:fork)
106
+
end
99
107
@app=app
100
108
end
101
109
@@ -117,7 +125,9 @@ def call(env)
117
125
seconds_waited=0ifseconds_waited < 0# make up for potential time drift between the routing server and the application server
118
126
final_wait_timeout=wait_timeout + effective_overtime# how long the request will be allowed to have waited
119
127
seconds_service_left=final_wait_timeout - seconds_waited# first calculation of service timeout (relevant if request doesn't get expired, may be overriden later)
120
-
info.wait,info.timeout=seconds_waited,final_wait_timeout# updating the info properties; info.timeout will be the wait timeout at this point
128
+
info.wait=seconds_waited# updating the info properties; info.timeout will be the wait timeout at this point
129
+
info.timeout=final_wait_timeout
130
+
121
131
ifseconds_service_left <= 0# expire requests that have waited for too long in the queue (as they are assumed to have been dropped by the web server / routing layer at this point)
122
132
RT._set_state!env,:expired
123
133
raiseRequestExpiryError.new(env),"Request older than #{info.ms(:timeout)}."
@@ -130,7 +140,7 @@ def call(env)
130
140
# compute actual timeout to be used for this request; if service_past_wait is true, this is just service_timeout. If false (the default), and wait time was determined, we'll use the shortest value between seconds_service_left and service_timeout. See comment above at service_past_wait for justification.
131
141
info.timeout=service_timeout# nice and simple, when service_past_wait is true, not so much otherwise:
RT._set_state!env,:ready# we're good to go, but have done nothing yet
135
145
136
146
heartbeat_event=nil# init var so it's in scope for following proc
@@ -143,7 +153,22 @@ def call(env)
143
153
144
154
timeout=RT::Scheduler::Timeout.newdo |app_thread| # creates a timeout instance responsible for timing out the request. the given block runs if timed out
145
155
register_state_change.call:timed_out
146
-
app_thread.raise(RequestTimeoutException.new(env),"Request #{"waited #{info.ms(:wait)}, then "ifinfo.wait}ran for longer than #{info.ms(:timeout)}")
156
+
157
+
message="Request "
158
+
message << "waited #{info.ms(:wait)}, then "ifinfo.wait
159
+
message << "ran for longer than #{info.ms(:timeout)} "
0 commit comments