safe_request_timeout 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT_LICENSE.txt +22 -0
- data/README.md +165 -0
- data/VERSION +1 -0
- data/dependabot.yml +12 -0
- data/lib/safe_request_timeout/active_record_hook.rb +19 -0
- data/lib/safe_request_timeout/hooks.rb +65 -0
- data/lib/safe_request_timeout/rack_middleware.rb +20 -0
- data/lib/safe_request_timeout/railtie.rb +33 -0
- data/lib/safe_request_timeout/sidekiq_middleware.rb +17 -0
- data/lib/safe_request_timeout/version.rb +5 -0
- data/lib/safe_request_timeout.rb +130 -0
- data/safe_request_timeout.gemspec +34 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 742cdb842214b5b66fa2fb127c79005ce2b3072776c59a20564433a9f5e82a8c
|
4
|
+
data.tar.gz: 592e4127ace138dadeabcf7e9dc5bbb129a20e8895e600a009e588249cea668f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 61be0a04e1f993c83dd520c151d7a76817be66eff64200898d336cfda5e70668c53a320462a5c161818e017ad17c6ab39c11fc2ceae1405c5ca1682eb511b17e
|
7
|
+
data.tar.gz: bcf57aafa0db0db508283d5c6f49ec611a6b3366e19542a014015f731e623c3dfc1f9eda4b239661f24b673a9310887355a37dbed568489fe7961ebda8d108dc
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## 1.0.0
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- Initial release.
|
data/MIT_LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2023 Brian Durand
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Request Timeout
|
2
|
+
|
3
|
+
[![Continuous Integration](https://github.com/bdurand/safe_request_timeout/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/safe_request_timeout/actions/workflows/continuous_integration.yml)
|
4
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
|
5
|
+
|
6
|
+
This gem provides a safe and convenient mechanism for adding a timeout mechanism to a block of code. The gem ensures that the timeout is safe to call and will not raise timeout errors from random places in your code which can leave your application in an indeterminate state.
|
7
|
+
|
8
|
+
It is designed to work in situations where there is a general timeout needed on some kind of request. For instance, consider a Rack HTTP request. This request may be behind a web server running in a separate process with it's own timeout where it sends an error back to the client when the application is taking too long to process the request. However, your Ruby application won't know anything about this and will continue processing the request and generating a response for a client that is no longer going to receive the response which just wastes server resources.
|
9
|
+
|
10
|
+
When requests start timing out due to an external issue like a slow database query, then it is more difficult to recover and can cascade an isolated issue into a general site outage. Often the timeouts you have on resources like database connections won't cover this case either since individual queries might never hit the timeout limit.
|
11
|
+
|
12
|
+
Unlike the `Timeout` class in the Ruby standard library, this code is very explicit about where timeout errors can be raised, so you don't need to worry about a timeout error in an unexpected place leaving your application in an indeterminate state.
|
13
|
+
|
14
|
+
There is built in support for Rails applications. For other frameworks you will need to add some middleware and hooks to implement the timeout mechanism.
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
You can wrap code in a timout block.
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
SafeRequestTimeout.timeout(15) do
|
22
|
+
...
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
By itself, this won't do anything. Unlike normal timeouts, there is no background process that will kill the operation after a defined period. Instead, you will need to periodically call `SafeRequestTimeout.check_timeout!` from within your code. Calling this method within a timeout block will raise an error if the time spent in that block has exceeded the max allowed. Calling it outside of a timeout block will do nothing.
|
27
|
+
|
28
|
+
It is generally best to call the `check_timeout!` method before doing an expensive operation since there's no point in timing out after the work has already been done. This method will also clear the current timeout, so you don't have to worry about it generating a cascading series of timeout errors in any error handling code.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
SafeRequestTimeout.timeout(5) do
|
32
|
+
1000.times do
|
33
|
+
# This will raise an error if the loop takes longer than 5 seconds.
|
34
|
+
SafeRequestTimeout.check_timeout!
|
35
|
+
do_somthing
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
You can also set a timeout value retroactively from within a `timeout` block. You can use this feature to change the timeout based on application state.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# Setting a timeout of nil will set up a block that will never timout.
|
44
|
+
SafeRequestTimeout.timeout(nil) do
|
45
|
+
# Set the timeout duration to 5 seconds for non-admin users
|
46
|
+
SafeRequestTimeout.set_timeout(5) unless current_user.admin?
|
47
|
+
|
48
|
+
do_something
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
You can also set the timeout duration with a `Proc` that will be evaluated at runtime.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
SafeRequestTimeout.timeout(lambda { CurrentUser.new.admin? ? nil : 5 })
|
56
|
+
...
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
You can clear the timeout if you want to ensure a block of code can run without begin timed out (i.e. if you need to run cleanup code).
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
SafeRequestTimeout.timeout(5) do
|
64
|
+
begin
|
65
|
+
do_something
|
66
|
+
ensure
|
67
|
+
SafeRequestTimeout.clear_timeout
|
68
|
+
cleanup_request
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
### Hooks
|
74
|
+
|
75
|
+
You can add hooks into other classes to check the current timeout if you don't want to have to sprinkle `SafeRequestTimeout.check_timeout!` throughout your code. To do this, use the `SafeRequestTimeout.add_timeout!` method. You need to specify the class and methods where you want to add the timeout hooks:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# Add a timeout check to the MyDriver#make_request method.
|
79
|
+
SafeRequestTimeout::Hooks.add_timeout!(MyDriver, [:make_request])
|
80
|
+
```
|
81
|
+
|
82
|
+
### Rack Middleware
|
83
|
+
|
84
|
+
This gem ships with Rack middleware that can set up a timeout block on all Rack requests. In a Rack application you would use this code to add a 15 second timeout to all requests to `app`.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
RackBuilder.new do
|
88
|
+
use SafeRequestTimeout::RackMiddleware, 15
|
89
|
+
run app
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
If you want to customize the timeout per request, you can call `SafeRequestTimeout.set_timeout` inside your request handling to change the value for the current request. You can also define the timeout duration with a `Proc` which will be called at runtime with the `env` object passed for the request.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
RackBuilder.new do
|
97
|
+
use SafeRequestTimeout::RackMiddleware, lambda { |env|
|
98
|
+
10 unless Rack::Request.new(env).path.start_with?("/admin")
|
99
|
+
}
|
100
|
+
run app
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
### Sidekiq Middleware
|
105
|
+
|
106
|
+
This gem ships with Sidekiq middleware that can add timeout support to Sidekiq workers. The middleware needs to be added to the server middleware in the Sidekiq initialization.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
Sidekiq.configure_server do |config|
|
110
|
+
config.server_middleware do |chain|
|
111
|
+
chain.add SafeRequestTimeout::SidekiqMiddleware
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
You can then specify a timeout per worker with the `safe_request_timeout` sidekiq option.
|
117
|
+
|
118
|
+
```
|
119
|
+
class SlowWorker
|
120
|
+
include Sidekiq::Worker
|
121
|
+
|
122
|
+
# Set a 15 second timeout for the worker to finish.
|
123
|
+
sidekiq_options safe_request_timeout: 15
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### Rails
|
128
|
+
|
129
|
+
This gem comes with built in support for Rails applications.
|
130
|
+
|
131
|
+
- The Rack middleware is added to the middleware chain. There is no timeout value set by default. You can specify a global one by setting `safe_request_timout.rack_timeout` in your Rails configuration.
|
132
|
+
|
133
|
+
- If Sidekiq is being used, then the Sidekiq middleware is added. Sidekiq workers can specify a timeout with the `safe_request_timeout` option.
|
134
|
+
|
135
|
+
- A timeout block is added around ActiveJob execution. Jobs can specify a timeout by calling `SafeRequestTimeout.set_timeout` in the `perform` method or in a `before_perform` callback.
|
136
|
+
|
137
|
+
- A timeout check is added on all ActiveRecord queries. The timeout is cleared when a database transaction is committed so that you won't unexpectedly timeout a request after making persistent changes. You can disable these hooks by setting `safe_request_timeout.active_record_hook` to false in your Rails configuration.
|
138
|
+
|
139
|
+
## Installation
|
140
|
+
|
141
|
+
Add this line to your application's Gemfile:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
gem 'safe_request_timeout'
|
145
|
+
```
|
146
|
+
|
147
|
+
And then execute:
|
148
|
+
```bash
|
149
|
+
$ bundle
|
150
|
+
```
|
151
|
+
|
152
|
+
Or install it yourself as:
|
153
|
+
```bash
|
154
|
+
$ gem install safe_request_timeout
|
155
|
+
```
|
156
|
+
|
157
|
+
## Contributing
|
158
|
+
|
159
|
+
Open a pull request on GitHub.
|
160
|
+
|
161
|
+
Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
|
162
|
+
|
163
|
+
## License
|
164
|
+
|
165
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/dependabot.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Dependabot update strategy
|
2
|
+
version: 2
|
3
|
+
updates:
|
4
|
+
- package-ecosystem: bundler
|
5
|
+
directory: "/"
|
6
|
+
schedule:
|
7
|
+
interval: daily
|
8
|
+
allow:
|
9
|
+
# Automatically keep all runtime dependencies updated
|
10
|
+
- dependency-name: "*"
|
11
|
+
dependency-type: "production"
|
12
|
+
versioning-strategy: lockfile-only
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafeRequestTimeout
|
4
|
+
class ActiveRecordHook
|
5
|
+
class << self
|
6
|
+
# Add the timeout hook to the connection class.
|
7
|
+
#
|
8
|
+
# @param connection_class [Class] The class to add the timeout hook to.
|
9
|
+
# @return [void]
|
10
|
+
def add_timeout!(connection_class = nil)
|
11
|
+
connection_class ||= ::ActiveRecord::Base.connection.class
|
12
|
+
|
13
|
+
SafeRequestTimeout::Hooks.add_timeout!(connection_class, [:exec_query])
|
14
|
+
|
15
|
+
SafeRequestTimeout::Hooks.clear_timeout!(connection_class, [:commit_db_transaction])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafeRequestTimeout
|
4
|
+
# Hooks into other classes from other libraries with timeout blocks. This allows
|
5
|
+
# timeouts to be automatically checked before making requests to external services.
|
6
|
+
module Hooks
|
7
|
+
class << self
|
8
|
+
# Hooks into a class by surrounding specified instance methods with timeout checks.
|
9
|
+
def add_timeout!(klass, methods, module_name = nil)
|
10
|
+
hooks_module = create_module(klass, module_name, "AddTimeout")
|
11
|
+
|
12
|
+
Array(methods).each do |method_name|
|
13
|
+
hooks_module.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
14
|
+
def #{method_name}(#{splat_args})
|
15
|
+
SafeRequestTimeout.check_timeout!
|
16
|
+
super(#{splat_args})
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
end
|
20
|
+
|
21
|
+
klass.prepend(hooks_module)
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear_timeout!(klass, methods, module_name = nil)
|
25
|
+
hooks_module = create_module(klass, module_name, "ClearTimeout")
|
26
|
+
|
27
|
+
Array(methods).each do |method_name|
|
28
|
+
hooks_module.class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
29
|
+
def #{method_name}(#{splat_args})
|
30
|
+
SafeRequestTimeout.clear_timeout
|
31
|
+
super(#{splat_args})
|
32
|
+
end
|
33
|
+
RUBY
|
34
|
+
end
|
35
|
+
|
36
|
+
klass.prepend(hooks_module)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def create_module(klass, module_name, module_type)
|
42
|
+
# Create a module that will be prepended to the specified class.
|
43
|
+
unless module_name
|
44
|
+
camelized_name = name.to_s.gsub(/[^a-z0-9]+([a-z0-9])/i) { |m| m[m.length - 1, m.length].upcase }
|
45
|
+
camelized_name = "#{camelized_name[0].upcase}#{camelized_name[1, camelized_name.length]}"
|
46
|
+
module_name = "#{klass.name.split("::").join}#{camelized_name}#{module_type}"
|
47
|
+
end
|
48
|
+
|
49
|
+
if const_defined?(module_name)
|
50
|
+
raise ArgumentError.new("Cannot create duplicate #{module_name} for hooking #{name} into #{klass.name}")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Dark arts & witchery to dynamically generate the module methods.
|
54
|
+
const_set(module_name, Module.new)
|
55
|
+
end
|
56
|
+
|
57
|
+
def splat_args
|
58
|
+
# The method of overriding kwargs changed in ruby 2.7
|
59
|
+
ruby_major, ruby_minor, _ = RUBY_VERSION.split(".").collect(&:to_i)
|
60
|
+
ruby_3_args = (ruby_major >= 3 || (ruby_major == 2 && ruby_minor >= 7))
|
61
|
+
(ruby_3_args ? "..." : "*args, &block")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafeRequestTimeout
|
4
|
+
# Rack middleware that adds a timeout block to all requests.
|
5
|
+
class RackMiddleware
|
6
|
+
# @param app [Object] The Rack application to wrap.
|
7
|
+
# @param timeout [Integer, Proc, nil] The timeout in seconds.
|
8
|
+
def initialize(app, timeout = nil)
|
9
|
+
@app = app
|
10
|
+
@timeout = timeout
|
11
|
+
@timeout_block = true if timeout.is_a?(Proc) && timeout.arity == 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
SafeRequestTimeout.timeout(@timeout_block ? @timeout.call(env) : @timeout) do
|
16
|
+
@app.call(env)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafeRequestTimeout
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
config.safe_request_timeout = ActiveSupport::OrderedOptions.new
|
6
|
+
config.safe_request_timeout.active_record_hook = true
|
7
|
+
config.safe_request_timeout.rack_timeout = nil
|
8
|
+
|
9
|
+
initializer "safe_request_timeout" do |app|
|
10
|
+
if app.config.safe_request_timeout.active_record_hook
|
11
|
+
ActiveSupport.on_load(:active_record) do
|
12
|
+
SafeRequestTimeout::ActiveRecordHook.add_timeout!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if defined?(ActiveJob::Base.around_perform)
|
17
|
+
ActiveJob::Base.around_perform do |job, block|
|
18
|
+
SafeRequestTimeout.timeout(nil, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
app.middleware.use SafeRequestTimeout::RackMiddleware, app.config.safe_request_timeout.rack_timeout
|
23
|
+
|
24
|
+
if defined?(Sidekiq.server?) && Sidekiq.server?
|
25
|
+
Sidekiq.configure_server do |sidekiq_config|
|
26
|
+
sidekiq_config.server_middleware do |chain|
|
27
|
+
chain.add SafeRequestTimeout::SidekiqMiddleware
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafeRequestTimeout
|
4
|
+
# Sidekiq server middleware that wraps job execution with a timeout. The timeout
|
5
|
+
# is set in a job's "safe_request_timeout" option.
|
6
|
+
class SidekiqMiddleware
|
7
|
+
if defined?(Sidekiq::ServerMiddleware)
|
8
|
+
include Sidekiq::ServerMiddleware
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(job_instance, job_payload, queue)
|
12
|
+
SafeRequestTimeout.timeout(job_payload["safe_request_timeout"]) do
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
# This module adds the capability to add a general timeout to any block of code.
|
6
|
+
# Unlike the Timeout module, an error is not raised to indicate a timeout.
|
7
|
+
# Instead, the `timed_out?` method can be used to check if the block of code
|
8
|
+
# has taken longer than the specified duration so the application can take
|
9
|
+
# the appropriate action.
|
10
|
+
#
|
11
|
+
# This is a safer alternative to the Timeout module because it does not fork new
|
12
|
+
# threads or risk raising errors from unexpected places.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# SafeRequestTimeout.timeout(5) do
|
16
|
+
# # calling check_timeout! will raise an error if the block has taken
|
17
|
+
# # longer than 5 seconds to execute.
|
18
|
+
# SafeRequestTimeout.check_timeout!
|
19
|
+
# end
|
20
|
+
module SafeRequestTimeout
|
21
|
+
class TimeoutError < ::Timeout::Error
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# Execute the given block with a timeout. If the block takes longer than the specified
|
26
|
+
# duration to execute, then the `timed_out?`` method will return true within the block.
|
27
|
+
#
|
28
|
+
# @param duration [Integer] the number of seconds to wait before timing out
|
29
|
+
# @yield the block to execute
|
30
|
+
# @yieldreturn [Object] the result of the block
|
31
|
+
def timeout(duration, &block)
|
32
|
+
duration = duration.call if duration.respond_to?(:call)
|
33
|
+
|
34
|
+
previous_start_at = Thread.current[:safe_request_timeout_started_at]
|
35
|
+
previous_timeout_at = Thread.current[:safe_request_timeout_timeout_at]
|
36
|
+
|
37
|
+
start_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
38
|
+
timeout_at = start_at + duration if duration
|
39
|
+
if timeout_at && previous_timeout_at && previous_timeout_at < timeout_at
|
40
|
+
timeout_at = previous_timeout_at
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
Thread.current[:safe_request_timeout_started_at] = start_at
|
45
|
+
Thread.current[:safe_request_timeout_timeout_at] = timeout_at
|
46
|
+
yield
|
47
|
+
ensure
|
48
|
+
Thread.current[:safe_request_timeout_started_at] = previous_start_at
|
49
|
+
Thread.current[:safe_request_timeout_timeout_at] = previous_timeout_at
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check if the current timeout block has timed out.
|
54
|
+
#
|
55
|
+
# @return [Boolean] true if the current timeout block has timed out
|
56
|
+
def timed_out?
|
57
|
+
timeout_at = Thread.current[:safe_request_timeout_timeout_at]
|
58
|
+
!!timeout_at && Process.clock_gettime(Process::CLOCK_MONOTONIC) > timeout_at
|
59
|
+
end
|
60
|
+
|
61
|
+
# Raise an error if the current timeout block has timed out. If there is no timeout block,
|
62
|
+
# then this method does nothing. If an error is raised, then the current timeout
|
63
|
+
# is cleared to prevent the error from being raised multiple times.
|
64
|
+
#
|
65
|
+
# @return [void]
|
66
|
+
# @raise [SafeRequestTimeout::TimeoutError] if the current timeout block has timed out
|
67
|
+
def check_timeout!
|
68
|
+
if timed_out?
|
69
|
+
Thread.current[:safe_request_timeout_timeout_at] = nil
|
70
|
+
raise TimeoutError.new("after #{time_elapsed.round(6)} seconds")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get the number of seconds remaining in the current timeout block or nil if there is no
|
75
|
+
# timeout block.
|
76
|
+
#
|
77
|
+
# @return [Float, nil] the number of seconds remaining in the current timeout block
|
78
|
+
def time_remaining
|
79
|
+
timeout_at = Thread.current[:safe_request_timeout_timeout_at]
|
80
|
+
[timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0.0].max if timeout_at
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the number of seconds elapsed in the current timeout block or nil if there is no
|
84
|
+
# timeout block.
|
85
|
+
#
|
86
|
+
# @return [Float, nil] the number of seconds elapsed in the current timeout block began
|
87
|
+
def time_elapsed
|
88
|
+
start_at = Thread.current[:safe_request_timeout_started_at]
|
89
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_at if start_at
|
90
|
+
end
|
91
|
+
|
92
|
+
# Set the duration for the current timeout block. This is useful if you want to set the duration
|
93
|
+
# after the timeout block has started. The timer for the timeout block will restart whenever
|
94
|
+
# a new duration is set.
|
95
|
+
#
|
96
|
+
# @return [void]
|
97
|
+
def set_timeout(duration)
|
98
|
+
if Thread.current[:safe_request_timeout_started_at]
|
99
|
+
start_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
100
|
+
duration = duration.call if duration.respond_to?(:call)
|
101
|
+
timeout_at = start_at + duration if duration
|
102
|
+
Thread.current[:safe_request_timeout_started_at] = start_at
|
103
|
+
Thread.current[:safe_request_timeout_timeout_at] = timeout_at
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Clear the current timeout. If a block is passed, then the timeout will be cleared
|
108
|
+
# only for the duration of the block.
|
109
|
+
#
|
110
|
+
# @yield the block to execute if one is given
|
111
|
+
# @yieldreturn [Object] the result of the block
|
112
|
+
def clear_timeout(&block)
|
113
|
+
if block
|
114
|
+
timeout(nil, &block)
|
115
|
+
else
|
116
|
+
set_timeout(nil)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
require_relative "safe_request_timeout/hooks"
|
123
|
+
require_relative "safe_request_timeout/active_record_hook"
|
124
|
+
require_relative "safe_request_timeout/rack_middleware"
|
125
|
+
require_relative "safe_request_timeout/sidekiq_middleware"
|
126
|
+
require_relative "safe_request_timeout/version"
|
127
|
+
|
128
|
+
if defined?(Rails::Railtie)
|
129
|
+
require_relative "safe_request_timeout/railtie"
|
130
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = "safe_request_timeout"
|
3
|
+
spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
|
4
|
+
spec.authors = ["Brian Durand"]
|
5
|
+
spec.email = ["bbdurand@gmail.com"]
|
6
|
+
|
7
|
+
spec.summary = "Mechanism for safely aborting long-running requests after a specified timeout."
|
8
|
+
spec.homepage = "https://github.com/bdurand/safe_request_timeout"
|
9
|
+
spec.license = "MIT"
|
10
|
+
|
11
|
+
# Specify which files should be added to the gem when it is released.
|
12
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
13
|
+
ignore_files = %w[
|
14
|
+
.
|
15
|
+
Appraisals
|
16
|
+
Gemfile
|
17
|
+
Gemfile.lock
|
18
|
+
Rakefile
|
19
|
+
bin/
|
20
|
+
gemfiles/
|
21
|
+
spec/
|
22
|
+
]
|
23
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
25
|
+
end
|
26
|
+
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_dependency "redis"
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler"
|
32
|
+
|
33
|
+
spec.required_ruby_version = ">= 2.5"
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: safe_request_timeout
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Durand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-06-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- bbdurand@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- CHANGELOG.md
|
49
|
+
- MIT_LICENSE.txt
|
50
|
+
- README.md
|
51
|
+
- VERSION
|
52
|
+
- dependabot.yml
|
53
|
+
- lib/safe_request_timeout.rb
|
54
|
+
- lib/safe_request_timeout/active_record_hook.rb
|
55
|
+
- lib/safe_request_timeout/hooks.rb
|
56
|
+
- lib/safe_request_timeout/rack_middleware.rb
|
57
|
+
- lib/safe_request_timeout/railtie.rb
|
58
|
+
- lib/safe_request_timeout/sidekiq_middleware.rb
|
59
|
+
- lib/safe_request_timeout/version.rb
|
60
|
+
- safe_request_timeout.gemspec
|
61
|
+
homepage: https://github.com/bdurand/safe_request_timeout
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '2.5'
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubygems_version: 3.4.12
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Mechanism for safely aborting long-running requests after a specified timeout.
|
84
|
+
test_files: []
|