double_restraint 1.0.1 → 1.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f0ee8109419ddc8e93ea3685a0d2f32c3d8b96383afe4c4330bbc866d9ee566
4
- data.tar.gz: 8c92f8eabaae7b7b4e0754d5e968d3fc1ecb045f5b4aa04c2d323f167551f467
3
+ metadata.gz: ea9dbcbe1d5da4480324c5dcfa993aaa72e58786430200ee0aaedbd844d341ba
4
+ data.tar.gz: 42201e52acde9d9ca90b5c0c4ded0350af3910f7aca3e8f97c59010929284030
5
5
  SHA512:
6
- metadata.gz: a7e55a6862bfda0b56a445b37af3fe3d0b486cd2ffde2e4ab2246700496cbe3017f3c737e9c507d1ab3ede613d34fc2137c8679356345a74757bc257010f32a0
7
- data.tar.gz: b80b29ec078b8fe3b48bbb4e5d549684a7e3f56157d29ab58d6fd09e2a20679513ebe250416238b871421f608146271ca07b66c461841a151f095479ce8231f3
6
+ metadata.gz: de78ad1954fc63e5936f0d26d4d02daf188cb594dc366b8dcf62a8941ee574fd06d7d1200ffb88ace43a4ab3c919a945ad738d70ef4af7d0dfea3b1f1db32ffe
7
+ data.tar.gz: 31dc3d878fde21db6c4a636129bb6acc8ef2b11573b0ded621b840968c456ec5b9f2972a8fc104c978af32dc3a43abcc92d358eaa56bebc411d2b82a81b7a761
data/CHANGELOG.md CHANGED
@@ -4,10 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [1.0.1]
7
+ ## 1.0.2
8
+
9
+ ### Fixed
10
+
11
+ - Timeout errors raised by the underlying `Restrainer` Redis calls no longer trigger the long running retry; only timeout errors raised by the block itself do. Previously a matching error raised while releasing the throttle could re-run a block that had already succeeded.
12
+ - Timeout errors that are not `StandardError` subclasses now correctly trigger the long running retry.
13
+ - `limit: 0` now means no executions are allowed (matching `Restrainer` semantics) instead of silently meaning unlimited. Use `limit: nil` (the default) for no limit.
14
+ - The orphaned lock expiration on the underlying restrainers is now derived from `long_running_timeout` so that blocks legitimately running longer than 60 seconds no longer allow the concurrency limits to be exceeded.
15
+
16
+ ## 1.0.1
17
+
8
18
  ### Added
19
+
9
20
  - Methods to expose the pool limits, timeouts and current in use sizes.
10
21
 
11
- ## [1.0.0]
22
+ ## 1.0.0
23
+
12
24
  ### Added
13
- - Everything!
25
+
26
+ - Initial release.
data/README.md CHANGED
@@ -2,21 +2,22 @@
2
2
 
3
3
  [![Continuous Integration](https://github.com/bdurand/double_restraint/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/double_restraint/actions/workflows/continuous_integration.yml)
4
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+ [![Gem Version](https://badge.fury.io/rb/double_restraint.svg)](https://badge.fury.io/rb/double_restraint)
5
6
 
6
- This gem implements a pattern for interacting with external services in a way that prevents performance issues with those services from taking down your application. It builds atop the [restrainer gem](https://github.com/weheartit/restrainer) which requires a Redis server to coordinate processes.
7
+ This gem implements a pattern for interacting with external services in a way that prevents performance issues with those services from taking down your application. It builds atop the [restrainer gem](https://github.com/bdurand/restrainer) which requires a Redis server to coordinate processes.
7
8
 
8
9
  ## Usage
9
10
 
10
- Suppose you have a web application that calls a web service for something, and at some point that web service starts to have latency issues and requests take several seconds to return. Eventually most your application threads will be be waiting on the web service and your application will be completely unresponsive.
11
+ Suppose you have a web application that calls a web service for something, and at some point that web service starts to have latency issues and requests take several seconds to return. Eventually most of your application threads will be waiting on the web service and your application will be completely unresponsive.
11
12
 
12
- If the external service uses timeouts, you could mitigate the issue of locking up your application by setting a low timeout so that requests to the sevice fail fast. However, if some requests to the service take just a little longer even in a health system, then you will be artificially preventing these requests from succeeding.
13
+ If the external service uses timeouts, you could mitigate the issue of locking up your application by setting a low timeout so that requests to the service fail fast. However, if some requests to the service take just a little longer even in a healthy system, then you will be artificially preventing these requests from succeeding.
13
14
 
14
- With the [restrainer gem](https://github.com/weheartit/restrainer) you can throttle the number of concurrent requests to a service so that, if there is a problem with that service, only a limited number of application threads would be affected:
15
+ With the [restrainer gem](https://github.com/bdurand/restrainer) you can throttle the number of concurrent requests to a service so that, if there is a problem with that service, only a limited number of application threads would be affected:
15
16
 
16
17
  ```ruby
17
18
  restrainer = Restrainer.new("MyWebService", limit: 10)
18
19
  begin
19
- restainer.throttle do
20
+ restrainer.throttle do
20
21
  MyWebService.new.call(arguments)
21
22
  end
22
23
  rescue Restrainer::ThrottledError
@@ -26,7 +27,7 @@ end
26
27
 
27
28
  However, this can lead to problems if you set the limit too low. You could end up in a situation where peak traffic sends more requests than the limit you set. This will end up artificially limiting the external calls and returning errors to users.
28
29
 
29
- This gem combines both solutions and lets you set two levels of timeouts and a limit on how many concurrent requests can use the longer timeout. You can be more aggressive with both your fail fast timeout and the limit on concurrent processes without affecting requests in a health system.
30
+ This gem combines both solutions and lets you set two levels of timeouts and a limit on how many concurrent requests can use the longer timeout. You can be more aggressive with both your fail fast timeout and the limit on concurrent processes without affecting requests in a healthy system.
30
31
 
31
32
  ```ruby
32
33
  restraint = DoubleRestraint.new("MyWebService", timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
@@ -40,12 +41,12 @@ end
40
41
  ```
41
42
 
42
43
  * The `timeout` value should be set to a low value that works for most requests in a healthy system.
43
- * The `long_running_timeout` value should be set to a higher value that works for all requests in a health system.
44
+ * The `long_running_timeout` value should be set to a higher value that works for all requests in a healthy system.
44
45
  * The `long_running_limit` value is the maximum number of concurrent requests that are allowed using the higher timeout.
45
46
 
46
47
  The `execute` call will call the block with the `timeout` value. If the block raises a timeout error, then it will be called again with the `long_running_timeout` value inside a `Restrainer`. If there are too many concurrent requests, then a `Restrainer::ThrottledError` will be raised.
47
48
 
48
- The effect of this is that if there are latency issues in `MyWebService`, then the requests will fail fast. Only a handful of requests will be allowed to execute with the higher timeout value so the impact on the overall system will be very limited. On a healthy system, you shouldn't seen any artificially generated errors as long as your `timeout` is set properly.
49
+ The effect of this is that if there are latency issues in `MyWebService`, then the requests will fail fast. Only a handful of requests will be allowed to execute with the higher timeout value so the impact on the overall system will be very limited. On a healthy system, you shouldn't see any artificially generated errors as long as your `timeout` is set properly.
49
50
 
50
51
  The `execute` block **must** be idempotent since it can be run twice by one call to `execute`.
51
52
 
@@ -61,7 +62,7 @@ By default, a timeout is identified by any error that inherits from `Timeout::Er
61
62
  restraint = DoubleRestraint.new("MyWebService", timeout_errors: [Faraday::TimeoutError], timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
62
63
  ```
63
64
 
64
- Finally, you need to specify the Redis instance to use. By default this uses the value specified for the [restrainer gem](https://github.com/weheartit/restrainer).
65
+ Finally, you need to specify the Redis instance to use. By default this uses the value specified for the [restrainer gem](https://github.com/bdurand/restrainer).
65
66
 
66
67
  ```ruby
67
68
  # set the global Redis instance
@@ -74,10 +75,25 @@ Restrainer.redis{ connection_pool.redis }
74
75
  However, you can also specify the Redis instance directly on the `DoubleRestraint` instance.
75
76
 
76
77
  ```ruby
77
- restraint = DoubleRestraint.new("MyWebService", redis: Redis.new(url: redis_url)), timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
78
+ restraint = DoubleRestraint.new("MyWebService", redis: Redis.new(url: redis_url), timeout: 0.5, long_running_timeout: 5.0, long_running_limit: 5)
78
79
  ```
79
80
 
80
- You can peek at the current pool sizes as well if
81
+ You can peek at the current pool sizes as well if you want:
82
+
83
+ ```ruby
84
+ # Number of processes currently using a slot in the default pool
85
+ restraint.default_pool_size
86
+
87
+ # Number of processes currently using a slot in the long running pool
88
+ restraint.long_running_pool_size
89
+
90
+ # Get the percentage capacity being used as a whole. Note that this only
91
+ # makes sense if a limit is set on the default pool (default_pool_limit
92
+ # returns -1 when the pool is unlimited).
93
+ total_pool_used = restraint.default_pool_size + restraint.long_running_pool_size
94
+ total_pool_capacity = restraint.default_pool_limit + restraint.long_running_pool_limit
95
+ total_pool_used.to_f / total_pool_capacity
96
+ ```
81
97
 
82
98
  ## Installation
83
99
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.0.2
@@ -8,6 +8,12 @@ Gem::Specification.new do |spec|
8
8
  spec.homepage = "https://github.com/bdurand/double_restraint"
9
9
  spec.license = "MIT"
10
10
 
11
+ spec.metadata = {
12
+ "homepage_uri" => spec.homepage,
13
+ "source_code_uri" => spec.homepage,
14
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
15
+ }
16
+
11
17
  # Specify which files should be added to the gem when it is released.
12
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
13
19
  ignore_files = %w[
@@ -24,7 +30,7 @@ Gem::Specification.new do |spec|
24
30
 
25
31
  spec.require_paths = ["lib"]
26
32
 
27
- spec.add_dependency "restrainer"
33
+ spec.add_dependency "restrainer", ">= 1.1.3"
28
34
 
29
35
  spec.add_development_dependency "bundler"
30
36
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
3
4
  require "restrainer"
4
5
 
5
6
  class DoubleRestraint
@@ -8,11 +9,11 @@ class DoubleRestraint
8
9
  # @param name [String, Symbol] The name of the restraint.
9
10
  # @param timeout [Numeric] The first timeout that will be yielded to the block.
10
11
  # @param long_running_timeout [Numeric] The timeout that will be yielded to the block if
11
- # the block times out the first time it is excuted.
12
- # @param long_running_limit [Integer] The maximum number of times the block can be run
12
+ # the block times out the first time it is executed.
13
+ # @param long_running_limit [Integer] The maximum number of concurrent executions of the block
13
14
  # with the long running timeout across all processes.
14
- # @param limit [Integer] The maximum of number of times the block can be run with the initial
15
- # timeout across all processes.
15
+ # @param limit [Integer] The maximum number of concurrent executions of the block with the
16
+ # initial timeout across all processes. If this is nil, then no limit will be applied.
16
17
  # @param timeout_errors [Array<Module>] List of errors that will be considered a timeout.
17
18
  # This needs to be customized depending on what the code in the block could throw to
18
19
  # indicate a timeout has occurred.
@@ -22,9 +23,15 @@ class DoubleRestraint
22
23
  @timeout = timeout
23
24
  @long_running_timeout = long_running_timeout
24
25
  @timeout_errors = Array(timeout_errors)
25
- limit = -1 if limit.to_i <= 0
26
- @restrainer = Restrainer.new("DoubleRestrainer(#{name})", limit: limit, redis: redis)
27
- @long_running_restrainer = Restrainer.new("DoubleRestrainer(#{name}).long_running", limit: long_running_limit, redis: redis)
26
+ limit = -1 if limit.nil?
27
+ # Slots in the underlying restrainers expire as a safeguard against orphaned locks,
28
+ # so the expiration must comfortably exceed the longest time a block can legitimately
29
+ # hold a slot.
30
+ restrainer_timeout = [60, long_running_timeout.to_f * 2].max
31
+ # The restrainer names use "DoubleRestrainer" rather than "DoubleRestraint" for
32
+ # backward compatibility with the Redis keys created by earlier versions of the gem.
33
+ @restrainer = Restrainer.new("DoubleRestrainer(#{name})", limit: limit, timeout: restrainer_timeout, redis: redis)
34
+ @long_running_restrainer = Restrainer.new("DoubleRestrainer(#{name}).long_running", limit: long_running_limit, timeout: restrainer_timeout, redis: redis)
28
35
  end
29
36
 
30
37
  # Execute a block of code. The block will be yielded with the timeout value. If the block raises
@@ -32,21 +39,26 @@ class DoubleRestraint
32
39
  # must be idempotent since it can be run twice.
33
40
  #
34
41
  # @yieldparam [Numeric] the timeout value to use in the block.
35
- # @raise [Restrainer::ThrottleError] if too many concurrent processes are trying to use the restraint.
36
- def execute(&block)
37
- begin
38
- @restrainer.throttle do
39
- yield @timeout
40
- end
41
- rescue => e
42
- if @timeout_errors.any? { |error_class| e.is_a?(error_class) }
43
- @long_running_restrainer.throttle do
44
- yield @long_running_timeout
45
- end
46
- else
47
- raise e
42
+ # @return [Object] the value returned by the block.
43
+ # @raise [Restrainer::ThrottledError] if too many concurrent processes are trying to use the restraint.
44
+ def execute
45
+ timed_out = false
46
+ result = @restrainer.throttle do
47
+ yield @timeout
48
+ rescue *@timeout_errors
49
+ # Just flag the timeout here so the retry happens after the throttle
50
+ # block exits and releases its slot in the default pool.
51
+ timed_out = true
52
+ nil
53
+ end
54
+
55
+ if timed_out
56
+ result = @long_running_restrainer.throttle do
57
+ yield @long_running_timeout
48
58
  end
49
59
  end
60
+
61
+ result
50
62
  end
51
63
 
52
64
  # Get the current size of the default pool. This can be useful in
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: double_restraint
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-02-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: restrainer
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '0'
18
+ version: 1.1.3
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '0'
25
+ version: 1.1.3
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: bundler
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +37,6 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
- description:
42
40
  email:
43
41
  - bbdurand@gmail.com
44
42
  executables: []
@@ -46,7 +44,7 @@ extensions: []
46
44
  extra_rdoc_files: []
47
45
  files:
48
46
  - CHANGELOG.md
49
- - MIT-LICENSE
47
+ - MIT-LICENSE.txt
50
48
  - README.md
51
49
  - VERSION
52
50
  - double_restraint.gemspec
@@ -54,8 +52,10 @@ files:
54
52
  homepage: https://github.com/bdurand/double_restraint
55
53
  licenses:
56
54
  - MIT
57
- metadata: {}
58
- post_install_message:
55
+ metadata:
56
+ homepage_uri: https://github.com/bdurand/double_restraint
57
+ source_code_uri: https://github.com/bdurand/double_restraint
58
+ changelog_uri: https://github.com/bdurand/double_restraint/blob/main/CHANGELOG.md
59
59
  rdoc_options: []
60
60
  require_paths:
61
61
  - lib
@@ -70,8 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
70
  - !ruby/object:Gem::Version
71
71
  version: '0'
72
72
  requirements: []
73
- rubygems_version: 3.0.3
74
- signing_key:
73
+ rubygems_version: 4.0.3
75
74
  specification_version: 4
76
75
  summary: Throttling mechanism for safely dealing with external resources so that latency
77
76
  does not take down your application.
File without changes