attempt 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGES.md +11 -0
- data/attempt.gemspec +3 -3
- data/doc/FIBER_TIMEOUT_ADDITION.md +65 -0
- data/doc/IMPROVEMENTS.md +64 -0
- data/doc/TIMEOUT_STRATEGIES.md +178 -0
- data/examples/test_fiber_timeout.rb +92 -0
- data/examples/test_improvements.rb +65 -0
- data/examples/test_timeout_strategies.rb +57 -0
- data/lib/attempt.rb +412 -37
- data/spec/attempt_spec.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +10 -17
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f057412efe5346a902612b68cd819e70584c0ff17ec0632c0743fdd5d80c9bd3
|
4
|
+
data.tar.gz: ece1895a0bd1c7db99bbe4855ebbdb550847ff9ed68c5b62bb823ec14ad492f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2c6c961d046e695a10557fabf7e0184bbd9243b38d2dc4786d04a90cf09aa5518a6285895c8add58ef23cfaa18d4aad8857b6cacb079fd2e5e13d2b605a83a5
|
7
|
+
data.tar.gz: bd9ebfa2aefd7abfcc9b5023349de8f609a15a1dd0ec62e35dd39ab0dc761ef355740252b30c06497eabd479fb7796c575235023a9b2a2e59a08bf32180943b8
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## 0.7.0 - 14-Jul-2025
|
2
|
+
* Refactored to include more argument validations, more explicit error
|
3
|
+
messages, and better thread safety.
|
4
|
+
* Add the configuratoin, timeout_enabled? and effective_timeout methods.
|
5
|
+
* Removed the safe_timeout dependency.
|
6
|
+
* When using the timeout option there is now a "thread_strategies" option
|
7
|
+
that affects how it behaves exactly. This option takes "auto", "custom",
|
8
|
+
"thread", "process", "fiber" and "ruby_timeout" as possible options.
|
9
|
+
* See the markdown files under the "doc" directory and/or the example
|
10
|
+
code in the "examples" directory for more details.
|
11
|
+
|
1
12
|
## 0.6.3 - 26-Jun-2024
|
2
13
|
* Rubocop cleanup.
|
3
14
|
|
data/attempt.gemspec
CHANGED
@@ -2,7 +2,7 @@ require 'rubygems'
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
spec.name = 'attempt'
|
5
|
-
spec.version = '0.
|
5
|
+
spec.version = '0.7.0'
|
6
6
|
spec.author = 'Daniel J. Berger'
|
7
7
|
spec.license = 'Apache-2.0'
|
8
8
|
spec.email = 'djberg96@gmail.com'
|
@@ -20,14 +20,14 @@ Gem::Specification.new do |spec|
|
|
20
20
|
'bug_tracker_uri' => 'https://github.com/djberg96/attempt/issues',
|
21
21
|
'wiki_uri' => 'https://github.com/djberg96/attempt/wiki',
|
22
22
|
'rubygems_mfa_required' => 'true',
|
23
|
-
'github_repo' => 'https://github.com/djberg96/attempt'
|
23
|
+
'github_repo' => 'https://github.com/djberg96/attempt',
|
24
|
+
'funding_uri' => 'https://github.com/sponsors/djberg96'
|
24
25
|
}
|
25
26
|
|
26
27
|
spec.add_development_dependency('rake')
|
27
28
|
spec.add_development_dependency('rubocop')
|
28
29
|
|
29
30
|
spec.add_dependency('structured_warnings', '~> 0.4.0')
|
30
|
-
spec.add_dependency('safe_timeout', '~> 0.0.5')
|
31
31
|
spec.add_dependency('rspec', '~> 3.9')
|
32
32
|
|
33
33
|
spec.description = <<-EOF
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Fiber Timeout Strategy Addition
|
2
|
+
|
3
|
+
## Summary
|
4
|
+
|
5
|
+
Added a new `:fiber` timeout strategy option to the Attempt library that provides a lightweight alternative to thread-based timeouts using Ruby's Fiber cooperative scheduling.
|
6
|
+
|
7
|
+
## Implementation Details
|
8
|
+
|
9
|
+
### New Method: `execute_with_fiber_timeout`
|
10
|
+
- Uses `AttemptTimeout.fiber_timeout` method
|
11
|
+
- Converts `AttemptTimeout::Error` to `Timeout::Error` for consistency
|
12
|
+
- Integrates seamlessly with existing timeout strategy infrastructure
|
13
|
+
|
14
|
+
### Enhanced `AttemptTimeout.fiber_timeout`
|
15
|
+
- **Hybrid Approach**: Automatically detects if code is fiber-cooperative
|
16
|
+
- **Pure Fiber Mode**: For truly cooperative code that yields control
|
17
|
+
- **Fiber+Thread Hybrid**: For blocking operations that need fiber benefits
|
18
|
+
- **Graceful Fallback**: Falls back to thread-based approach when needed
|
19
|
+
|
20
|
+
## Key Advantages
|
21
|
+
|
22
|
+
1. **Lowest Overhead**: No thread creation overhead for cooperative code
|
23
|
+
2. **Cooperative Scheduling**: Works well with fiber-based applications
|
24
|
+
3. **Hybrid Compatibility**: Works with both cooperative and blocking code
|
25
|
+
4. **Memory Efficient**: Lower memory footprint than thread-based timeouts
|
26
|
+
5. **Seamless Integration**: Drop-in replacement for other timeout strategies
|
27
|
+
|
28
|
+
## Usage Examples
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# Basic fiber timeout
|
32
|
+
attempt(timeout: 5, timeout_strategy: :fiber) { operation }
|
33
|
+
|
34
|
+
# Configuration with fiber strategy
|
35
|
+
attempt_obj = Attempt.new(
|
36
|
+
tries: 3,
|
37
|
+
timeout: 10,
|
38
|
+
timeout_strategy: :fiber
|
39
|
+
)
|
40
|
+
|
41
|
+
# Performance-conscious applications
|
42
|
+
attempt(timeout: 1, timeout_strategy: :fiber) { lightweight_operation }
|
43
|
+
```
|
44
|
+
|
45
|
+
## Performance Characteristics
|
46
|
+
|
47
|
+
- **Best For**: Lightweight operations, cooperative code, high-frequency timeouts
|
48
|
+
- **Memory**: Lowest memory usage among all strategies
|
49
|
+
- **CPU**: Minimal CPU overhead for cooperative operations
|
50
|
+
- **Compatibility**: Works in all Ruby environments that support Fibers
|
51
|
+
|
52
|
+
## Integration Points
|
53
|
+
|
54
|
+
1. **Strategy Selection**: Added `:fiber` to timeout strategy options
|
55
|
+
2. **Auto Fallback**: Included in automatic strategy selection chain
|
56
|
+
3. **Documentation**: Updated all documentation to include fiber strategy
|
57
|
+
4. **Testing**: Comprehensive test suite validates fiber timeout behavior
|
58
|
+
|
59
|
+
## Backward Compatibility
|
60
|
+
|
61
|
+
- Fully backward compatible - existing code continues to work unchanged
|
62
|
+
- Opt-in feature - only used when explicitly specified
|
63
|
+
- Consistent error handling and API surface
|
64
|
+
|
65
|
+
The fiber timeout strategy provides developers with a high-performance, low-overhead option for timeout handling, especially valuable in fiber-based applications and scenarios requiring many concurrent timeout operations.
|
data/doc/IMPROVEMENTS.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Improvements Made to the Attempt Library
|
2
|
+
|
3
|
+
## Summary of Enhancements
|
4
|
+
|
5
|
+
The code has been significantly improved with better error handling, validation, maintainability, and new features while maintaining backward compatibility.
|
6
|
+
|
7
|
+
## Key Improvements
|
8
|
+
|
9
|
+
### 1. **Better Parameter Validation**
|
10
|
+
- Added comprehensive validation for all constructor parameters
|
11
|
+
- Clear error messages for invalid parameters (negative tries, intervals, etc.)
|
12
|
+
- Type checking for all numeric parameters
|
13
|
+
|
14
|
+
### 2. **Improved Thread Safety & State Management**
|
15
|
+
- Removed destructive modification of `@tries` during execution
|
16
|
+
- Local variables track attempt state instead of modifying instance variables
|
17
|
+
- Instance can be safely reused multiple times
|
18
|
+
|
19
|
+
### 3. **Enhanced Error Handling & Logging**
|
20
|
+
- More informative error messages that include error class and message
|
21
|
+
- Better support for different logger types (IO, Logger objects)
|
22
|
+
- Graceful fallback for missing dependencies (safe_timeout)
|
23
|
+
- Changed default exception level from `Exception` to `StandardError` (safer)
|
24
|
+
|
25
|
+
### 4. **Better Timeout Support**
|
26
|
+
- Supports both boolean and numeric timeout values
|
27
|
+
- More intuitive timeout configuration
|
28
|
+
- Proper fallback when safe_timeout gem is not available
|
29
|
+
|
30
|
+
### 5. **New Utility Methods**
|
31
|
+
- `timeout_enabled?` - Check if timeouts are configured
|
32
|
+
- `effective_timeout` - Get the actual timeout value being used
|
33
|
+
- `configuration` - Inspect current configuration settings
|
34
|
+
|
35
|
+
### 6. **Improved Code Organization**
|
36
|
+
- Better separation of public and private methods
|
37
|
+
- More descriptive method names
|
38
|
+
- Comprehensive documentation improvements
|
39
|
+
- Better code structure and readability
|
40
|
+
|
41
|
+
### 7. **Enhanced Kernel Module Method**
|
42
|
+
- Better documentation with more examples
|
43
|
+
- Explicit parameter validation
|
44
|
+
- Support for all new features
|
45
|
+
|
46
|
+
## Backward Compatibility
|
47
|
+
|
48
|
+
All existing functionality remains intact:
|
49
|
+
- All original test cases pass
|
50
|
+
- Same API surface for basic usage
|
51
|
+
- All original configuration options work as before
|
52
|
+
|
53
|
+
## Performance Improvements
|
54
|
+
|
55
|
+
- Reduced method calls during retry loops
|
56
|
+
- More efficient interval management
|
57
|
+
- Better resource cleanup
|
58
|
+
|
59
|
+
## Code Quality
|
60
|
+
|
61
|
+
- Better adherence to Ruby best practices
|
62
|
+
- More comprehensive error handling
|
63
|
+
- Improved documentation and examples
|
64
|
+
- Better separation of concerns
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# Improved Timeout Strategies in Attempt Library
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
The Attempt library now includes multiple timeout strategies to provide more relia## Performance Comparison
|
6
|
+
|
7
|
+
- **Process**: Highest reliability, highest overhead
|
8
|
+
- **Custom**: Good reliability, low overhead
|
9
|
+
- **Thread**: Good reliability, very low overhead
|
10
|
+
- **Fiber**: Good reliability, lowest overhead (for cooperative code)
|
11
|
+
- **Ruby Timeout**: Lowest reliability, lowest overheadmeout behavior for arbitrary blocks of code.
|
12
|
+
|
13
|
+
## Problems with Ruby's Standard Timeout
|
14
|
+
|
15
|
+
Ruby's built-in `Timeout` module has several well-known issues:
|
16
|
+
|
17
|
+
1. **Thread Safety**: Uses `Thread#raise` which can interrupt critical sections
|
18
|
+
2. **Resource Leaks**: Abrupt termination can leave resources in inconsistent states
|
19
|
+
3. **Unreliable**: May not work with blocking C extensions
|
20
|
+
4. **Memory Issues**: Can cause memory corruption in some cases
|
21
|
+
|
22
|
+
## Available Timeout Strategies
|
23
|
+
|
24
|
+
### 1. `:auto` (Default)
|
25
|
+
Automatically selects the best available strategy based on the environment.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
attempt(timeout: 5, timeout_strategy: :auto) { risky_operation }
|
29
|
+
```
|
30
|
+
|
31
|
+
### 2. `:custom`
|
32
|
+
Uses our custom `AttemptTimeout` implementation that's safer than Ruby's `Timeout`.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
attempt(timeout: 5, timeout_strategy: :custom) { risky_operation }
|
36
|
+
```
|
37
|
+
|
38
|
+
**Advantages:**
|
39
|
+
- Safer than Ruby's Timeout
|
40
|
+
- Uses `Thread#join` instead of `Thread#raise`
|
41
|
+
- More graceful thread termination
|
42
|
+
|
43
|
+
### 3. `:thread`
|
44
|
+
Improved thread-based timeout with better cleanup.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
attempt(timeout: 5, timeout_strategy: :thread) { risky_operation }
|
48
|
+
```
|
49
|
+
|
50
|
+
**Advantages:**
|
51
|
+
- Better error handling than standard timeout
|
52
|
+
- Proper thread cleanup
|
53
|
+
- Works in most environments
|
54
|
+
|
55
|
+
### 4. `:process`
|
56
|
+
Fork-based timeout (most reliable for I/O operations).
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
attempt(timeout: 5, timeout_strategy: :process) { risky_operation }
|
60
|
+
```
|
61
|
+
|
62
|
+
**Advantages:**
|
63
|
+
- Most reliable for blocking I/O operations
|
64
|
+
- Complete isolation from main process
|
65
|
+
- Works with C extensions that don't respond to signals
|
66
|
+
|
67
|
+
**Limitations:**
|
68
|
+
- Not available on all platforms (Windows, some Ruby implementations)
|
69
|
+
- Higher overhead due to process creation
|
70
|
+
- Results must be serializable with Marshal
|
71
|
+
|
72
|
+
### 5. `:fiber`
|
73
|
+
Fiber-based timeout (lightweight, cooperative scheduling).
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
attempt(timeout: 5, timeout_strategy: :fiber) { risky_operation }
|
77
|
+
```
|
78
|
+
|
79
|
+
**Advantages:**
|
80
|
+
- Very lightweight - no thread creation overhead
|
81
|
+
- Good for cooperative code that yields control
|
82
|
+
- Hybrid implementation works with most code
|
83
|
+
|
84
|
+
**Limitations:**
|
85
|
+
- Pure fiber approach only works with cooperative code
|
86
|
+
- Falls back to fiber+thread hybrid for blocking operations
|
87
|
+
- Newer feature, less battle-tested
|
88
|
+
|
89
|
+
### 6. `:ruby_timeout`
|
90
|
+
Uses Ruby's standard Timeout module (for compatibility).
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
attempt(timeout: 5, timeout_strategy: :ruby_timeout) { risky_operation }
|
94
|
+
```
|
95
|
+
|
96
|
+
**Use only when:**
|
97
|
+
- You need exact compatibility with existing code
|
98
|
+
- Other strategies don't work in your environment
|
99
|
+
|
100
|
+
## Strategy Selection Guidelines
|
101
|
+
|
102
|
+
### For I/O Operations (Network, File I/O)
|
103
|
+
```ruby
|
104
|
+
# Best: Process-based (most reliable)
|
105
|
+
attempt(timeout: 30, timeout_strategy: :process) { Net::HTTP.get(uri) }
|
106
|
+
|
107
|
+
# Alternative: Custom timeout
|
108
|
+
attempt(timeout: 30, timeout_strategy: :custom) { Net::HTTP.get(uri) }
|
109
|
+
```
|
110
|
+
|
111
|
+
### For CPU-Intensive Operations
|
112
|
+
```ruby
|
113
|
+
# Best: Thread-based, custom, or fiber
|
114
|
+
attempt(timeout: 10, timeout_strategy: :thread) { expensive_calculation }
|
115
|
+
attempt(timeout: 10, timeout_strategy: :fiber) { cooperative_calculation }
|
116
|
+
```
|
117
|
+
|
118
|
+
### For Lightweight Operations
|
119
|
+
```ruby
|
120
|
+
# Best: Fiber (lowest overhead)
|
121
|
+
attempt(timeout: 5, timeout_strategy: :fiber) { quick_operation }
|
122
|
+
|
123
|
+
# Alternative: Custom
|
124
|
+
attempt(timeout: 5, timeout_strategy: :custom) { quick_operation }
|
125
|
+
```
|
126
|
+
|
127
|
+
### For General Use
|
128
|
+
```ruby
|
129
|
+
# Let the library choose automatically
|
130
|
+
attempt(timeout: 5) { some_operation }
|
131
|
+
```
|
132
|
+
|
133
|
+
## Configuration Examples
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
# Configure timeout strategy for an instance
|
137
|
+
attempt_obj = Attempt.new(
|
138
|
+
tries: 3,
|
139
|
+
timeout: 30,
|
140
|
+
timeout_strategy: :process
|
141
|
+
)
|
142
|
+
|
143
|
+
# Use with specific strategy
|
144
|
+
attempt_obj.attempt { risky_network_call }
|
145
|
+
|
146
|
+
# Or use the kernel method
|
147
|
+
attempt(tries: 5, timeout: 10, timeout_strategy: :custom) do
|
148
|
+
# Your code here
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
## Performance Comparison
|
153
|
+
|
154
|
+
- **Process**: Highest reliability, highest overhead
|
155
|
+
- **Custom**: Good reliability, low overhead
|
156
|
+
- **Thread**: Good reliability, very low overhead
|
157
|
+
- **Ruby Timeout**: Lowest reliability, lowest overhead
|
158
|
+
|
159
|
+
## Migration Guide
|
160
|
+
|
161
|
+
Existing code will continue to work unchanged:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
# This still works exactly as before
|
165
|
+
attempt(timeout: 5) { some_operation }
|
166
|
+
```
|
167
|
+
|
168
|
+
To use improved timeout strategies:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# Add timeout_strategy parameter
|
172
|
+
attempt(timeout: 5, timeout_strategy: :process) { some_operation }
|
173
|
+
|
174
|
+
# Or use the lightweight fiber strategy
|
175
|
+
attempt(timeout: 5, timeout_strategy: :fiber) { cooperative_operation }
|
176
|
+
```
|
177
|
+
|
178
|
+
The `:auto` strategy provides the best balance of reliability and compatibility for most use cases.
|
@@ -0,0 +1,92 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'lib/attempt'
|
4
|
+
|
5
|
+
puts "=== Testing Fiber Timeout Strategy ==="
|
6
|
+
|
7
|
+
# Test 1: Basic fiber timeout functionality
|
8
|
+
puts "\n1. Testing fiber timeout with fast operation:"
|
9
|
+
begin
|
10
|
+
result = attempt(tries: 1, timeout: 2, timeout_strategy: :fiber) do
|
11
|
+
sleep 0.1
|
12
|
+
"Fiber timeout test completed!"
|
13
|
+
end
|
14
|
+
puts "✓ #{result}"
|
15
|
+
rescue => e
|
16
|
+
puts "✗ Error: #{e.message}"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Test 2: Fiber timeout that should timeout
|
20
|
+
puts "\n2. Testing fiber timeout that should timeout:"
|
21
|
+
begin
|
22
|
+
start_time = Time.now
|
23
|
+
attempt(tries: 1, timeout: 0.5, timeout_strategy: :fiber) do
|
24
|
+
sleep 2 # This should timeout
|
25
|
+
"Should not reach here"
|
26
|
+
end
|
27
|
+
puts "✗ Should have timed out"
|
28
|
+
rescue Timeout::Error => e
|
29
|
+
elapsed = Time.now - start_time
|
30
|
+
puts "✓ Timed out as expected: #{e.message} (elapsed: #{elapsed.round(2)}s)"
|
31
|
+
rescue => e
|
32
|
+
puts "✗ Unexpected error: #{e.class}: #{e.message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Test 3: Compare fiber vs thread timeout performance
|
36
|
+
puts "\n3. Performance comparison:"
|
37
|
+
require 'benchmark'
|
38
|
+
|
39
|
+
operations = 10
|
40
|
+
|
41
|
+
puts "Fiber strategy:"
|
42
|
+
fiber_time = Benchmark.realtime do
|
43
|
+
operations.times do
|
44
|
+
attempt(tries: 1, timeout: 1, timeout_strategy: :fiber) do
|
45
|
+
sleep 0.01
|
46
|
+
"done"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
puts " #{operations} operations: #{fiber_time.round(4)}s"
|
51
|
+
|
52
|
+
puts "Thread strategy:"
|
53
|
+
thread_time = Benchmark.realtime do
|
54
|
+
operations.times do
|
55
|
+
attempt(tries: 1, timeout: 1, timeout_strategy: :thread) do
|
56
|
+
sleep 0.01
|
57
|
+
"done"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
puts " #{operations} operations: #{thread_time.round(4)}s"
|
62
|
+
|
63
|
+
puts "Custom strategy:"
|
64
|
+
custom_time = Benchmark.realtime do
|
65
|
+
operations.times do
|
66
|
+
attempt(tries: 1, timeout: 1, timeout_strategy: :custom) do
|
67
|
+
sleep 0.01
|
68
|
+
"done"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
puts " #{operations} operations: #{custom_time.round(4)}s"
|
73
|
+
|
74
|
+
# Test 4: Configuration inspection
|
75
|
+
puts "\n4. Configuration with fiber strategy:"
|
76
|
+
attempt_obj = Attempt.new(tries: 3, timeout: 5, timeout_strategy: :fiber)
|
77
|
+
config = attempt_obj.configuration
|
78
|
+
puts "Configuration: #{config}"
|
79
|
+
|
80
|
+
# Test 5: Error handling in fiber timeout
|
81
|
+
puts "\n5. Error handling in fiber timeout:"
|
82
|
+
begin
|
83
|
+
attempt(tries: 1, timeout: 2, timeout_strategy: :fiber) do
|
84
|
+
raise StandardError, "Test error in fiber"
|
85
|
+
end
|
86
|
+
rescue StandardError => e
|
87
|
+
puts "✓ Error properly caught: #{e.message}"
|
88
|
+
rescue => e
|
89
|
+
puts "✗ Unexpected error type: #{e.class}: #{e.message}"
|
90
|
+
end
|
91
|
+
|
92
|
+
puts "\n=== Fiber timeout strategy tests completed ==="
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'lib/attempt'
|
4
|
+
|
5
|
+
puts "=== Testing Improved Attempt Library ==="
|
6
|
+
|
7
|
+
# Test 1: Basic functionality
|
8
|
+
puts "\n1. Basic retry functionality:"
|
9
|
+
begin
|
10
|
+
counter = 0
|
11
|
+
result = attempt(tries: 3, interval: 0.1) do
|
12
|
+
counter += 1
|
13
|
+
puts " Attempt #{counter}"
|
14
|
+
raise "Simulated error" if counter < 3
|
15
|
+
"Success!"
|
16
|
+
end
|
17
|
+
puts " Result: #{result}"
|
18
|
+
rescue => e
|
19
|
+
puts " Final error: #{e.message}"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Test 2: Parameter validation (our improvement)
|
23
|
+
puts "\n2. Parameter validation:"
|
24
|
+
begin
|
25
|
+
Attempt.new(tries: -1)
|
26
|
+
rescue ArgumentError => e
|
27
|
+
puts " ✓ Caught invalid tries: #{e.message}"
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
Attempt.new(interval: -5)
|
32
|
+
rescue ArgumentError => e
|
33
|
+
puts " ✓ Caught invalid interval: #{e.message}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Test 3: Configuration inspection (our improvement)
|
37
|
+
puts "\n3. Configuration inspection:"
|
38
|
+
attempt_obj = Attempt.new(tries: 5, interval: 2, increment: 1)
|
39
|
+
puts " Configuration: #{attempt_obj.configuration}"
|
40
|
+
puts " Timeout enabled: #{attempt_obj.timeout_enabled?}"
|
41
|
+
|
42
|
+
# Test 4: Better error messaging (our improvement)
|
43
|
+
puts "\n4. Improved error logging:"
|
44
|
+
begin
|
45
|
+
attempt(tries: 2, interval: 0.1, warnings: false) do
|
46
|
+
raise StandardError, "This is a test error"
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
puts " Final error class: #{e.class}"
|
50
|
+
puts " Final error message: #{e.message}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Test 5: Timeout with numeric value (our improvement)
|
54
|
+
puts "\n5. Numeric timeout:"
|
55
|
+
begin
|
56
|
+
result = attempt(tries: 1, timeout: 0.1) do
|
57
|
+
sleep 0.05 # This should succeed
|
58
|
+
"Completed within timeout"
|
59
|
+
end
|
60
|
+
puts " ✓ #{result}"
|
61
|
+
rescue Timeout::Error
|
62
|
+
puts " ✗ Timed out unexpectedly"
|
63
|
+
end
|
64
|
+
|
65
|
+
puts "\n=== All tests completed ==="
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'lib/attempt'
|
4
|
+
require 'benchmark'
|
5
|
+
|
6
|
+
puts "=== Testing Different Timeout Strategies ==="
|
7
|
+
|
8
|
+
def blocking_operation(duration)
|
9
|
+
start_time = Time.now
|
10
|
+
while Time.now - start_time < duration
|
11
|
+
# Simulate CPU-intensive work
|
12
|
+
1000.times { Math.sqrt(rand) }
|
13
|
+
end
|
14
|
+
"Completed after #{duration}s"
|
15
|
+
end
|
16
|
+
|
17
|
+
def io_operation(duration)
|
18
|
+
sleep duration
|
19
|
+
"IO completed after #{duration}s"
|
20
|
+
end
|
21
|
+
|
22
|
+
strategies = [:auto, :custom, :thread, :process, :fiber, :ruby_timeout]
|
23
|
+
|
24
|
+
strategies.each do |strategy|
|
25
|
+
puts "\n--- Testing #{strategy.to_s.upcase} strategy ---"
|
26
|
+
|
27
|
+
# Test 1: Operation that completes within timeout
|
28
|
+
begin
|
29
|
+
result = attempt(tries: 1, timeout: 2, timeout_strategy: strategy) do
|
30
|
+
io_operation(0.1)
|
31
|
+
end
|
32
|
+
puts "✓ Fast operation: #{result}"
|
33
|
+
rescue => e
|
34
|
+
puts "✗ Fast operation failed: #{e.message}"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Test 2: Operation that times out
|
38
|
+
begin
|
39
|
+
time = Benchmark.realtime do
|
40
|
+
attempt(tries: 1, timeout: 0.5, timeout_strategy: strategy) do
|
41
|
+
io_operation(2) # This should timeout
|
42
|
+
end
|
43
|
+
end
|
44
|
+
puts "✗ Timeout test failed - should have timed out"
|
45
|
+
rescue Timeout::Error => e
|
46
|
+
puts "✓ Timeout worked: #{e.message}"
|
47
|
+
rescue => e
|
48
|
+
puts "✗ Unexpected error: #{e.class}: #{e.message}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Test configuration inspection
|
53
|
+
puts "\n--- Configuration Test ---"
|
54
|
+
attempt_obj = Attempt.new(tries: 3, timeout: 5, timeout_strategy: :process)
|
55
|
+
puts "Configuration: #{attempt_obj.configuration}"
|
56
|
+
|
57
|
+
puts "\n=== All timeout strategy tests completed ==="
|
data/lib/attempt.rb
CHANGED
@@ -1,18 +1,123 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require 'timeout'
|
5
|
-
else
|
6
|
-
require 'safe_timeout'
|
7
|
-
end
|
8
|
-
|
3
|
+
require 'timeout'
|
9
4
|
require 'structured_warnings'
|
10
5
|
|
6
|
+
# Custom timeout implementation that's more reliable than Ruby's Timeout module
|
7
|
+
class AttemptTimeout
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
# More reliable timeout using Thread + sleep instead of Timeout.timeout
|
11
|
+
# This approach is safer as it doesn't use Thread#raise
|
12
|
+
def self.timeout(seconds, &block)
|
13
|
+
return yield if seconds.nil? || seconds <= 0
|
14
|
+
|
15
|
+
result = nil
|
16
|
+
exception = nil
|
17
|
+
|
18
|
+
thread = Thread.new do
|
19
|
+
begin
|
20
|
+
result = yield
|
21
|
+
rescue => e
|
22
|
+
exception = e
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if thread.join(seconds)
|
27
|
+
# Thread completed within timeout
|
28
|
+
raise exception if exception
|
29
|
+
result
|
30
|
+
else
|
31
|
+
# Thread timed out
|
32
|
+
thread.kill # More graceful than Thread#raise
|
33
|
+
thread.join # Wait for cleanup
|
34
|
+
raise Error, "execution expired after #{seconds} seconds"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Alternative: Fiber-based timeout (lightweight, cooperative)
|
39
|
+
# Note: This only works for code that yields control back to the main thread
|
40
|
+
def self.fiber_timeout(seconds, &block)
|
41
|
+
return yield if seconds.nil? || seconds <= 0
|
42
|
+
|
43
|
+
# For blocks that don't naturally yield, we need a different approach
|
44
|
+
# We'll use a hybrid fiber + thread approach for better compatibility
|
45
|
+
if fiber_compatible_block?(&block)
|
46
|
+
fiber_only_timeout(seconds, &block)
|
47
|
+
else
|
48
|
+
fiber_thread_hybrid_timeout(seconds, &block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Pure fiber-based timeout for cooperative code
|
53
|
+
def self.fiber_only_timeout(seconds, &block)
|
54
|
+
fiber = Fiber.new(&block)
|
55
|
+
start_time = Time.now
|
56
|
+
|
57
|
+
loop do
|
58
|
+
elapsed = Time.now - start_time
|
59
|
+
if elapsed > seconds
|
60
|
+
raise Error, "execution expired after #{seconds} seconds"
|
61
|
+
end
|
62
|
+
|
63
|
+
begin
|
64
|
+
result = fiber.resume
|
65
|
+
return result unless fiber.alive?
|
66
|
+
rescue FiberError
|
67
|
+
# Fiber is dead, which means it completed
|
68
|
+
break
|
69
|
+
end
|
70
|
+
|
71
|
+
# Small sleep to prevent busy waiting and allow other operations
|
72
|
+
sleep 0.001
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Hybrid approach: fiber in a thread with timeout
|
77
|
+
def self.fiber_thread_hybrid_timeout(seconds, &block)
|
78
|
+
result = nil
|
79
|
+
exception = nil
|
80
|
+
|
81
|
+
thread = Thread.new do
|
82
|
+
fiber = Fiber.new do
|
83
|
+
begin
|
84
|
+
result = yield
|
85
|
+
rescue => e
|
86
|
+
exception = e
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Resume the fiber until completion
|
91
|
+
while fiber.alive?
|
92
|
+
fiber.resume
|
93
|
+
Thread.pass # Allow other threads to run
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if thread.join(seconds)
|
98
|
+
raise exception if exception
|
99
|
+
result
|
100
|
+
else
|
101
|
+
thread.kill
|
102
|
+
thread.join(0.1)
|
103
|
+
raise Error, "execution expired after #{seconds} seconds"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Simple heuristic to determine if a block is likely to be fiber-compatible
|
108
|
+
# (This is a basic implementation - in practice, this is hard to determine)
|
109
|
+
def self.fiber_compatible_block?(&block)
|
110
|
+
# For now, assume most blocks are not naturally fiber-cooperative
|
111
|
+
# In practice, you'd want more sophisticated detection
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
11
116
|
# The Attempt class encapsulates methods related to multiple attempts at
|
12
117
|
# running the same method before actually failing.
|
13
118
|
class Attempt
|
14
119
|
# The version of the attempt library.
|
15
|
-
VERSION = '0.
|
120
|
+
VERSION = '0.7.0'
|
16
121
|
|
17
122
|
# Warning raised if an attempt fails before the maximum number of tries
|
18
123
|
# has been reached.
|
@@ -39,6 +144,10 @@ class Attempt
|
|
39
144
|
# If set, the code block is further wrapped in a timeout block.
|
40
145
|
attr_accessor :timeout
|
41
146
|
|
147
|
+
# Strategy to use for timeout implementation
|
148
|
+
# Options: :auto, :custom, :thread, :process, :fiber, :ruby_timeout
|
149
|
+
attr_accessor :timeout_strategy
|
150
|
+
|
42
151
|
# Determines which exception level to check when looking for errors to
|
43
152
|
# retry. The default is 'Exception' (i.e. all errors).
|
44
153
|
attr_accessor :level
|
@@ -49,29 +158,35 @@ class Attempt
|
|
49
158
|
# Creates and returns a new +Attempt+ object. The supported keyword options
|
50
159
|
# are as follows:
|
51
160
|
#
|
52
|
-
# * tries - The number of attempts to make before giving up. The default is 3.
|
53
|
-
# * interval - The delay in seconds between each attempt. The default is 60.
|
161
|
+
# * tries - The number of attempts to make before giving up. Must be positive. The default is 3.
|
162
|
+
# * interval - The delay in seconds between each attempt. Must be non-negative. The default is 60.
|
54
163
|
# * log - An IO handle or Logger instance where warnings/errors are logged to. The default is nil.
|
55
|
-
# * increment - The amount to increment the interval between tries. The default is 0.
|
56
|
-
# * level - The level of exception to be caught. The default is
|
164
|
+
# * increment - The amount to increment the interval between tries. Must be non-negative. The default is 0.
|
165
|
+
# * level - The level of exception to be caught. The default is StandardError (recommended over Exception).
|
57
166
|
# * warnings - Boolean value that indicates whether or not errors are treated as warnings
|
58
167
|
# until the maximum number of attempts has been made. The default is true.
|
59
|
-
# * timeout -
|
60
|
-
#
|
168
|
+
# * timeout - Timeout in seconds to automatically wrap your proc in a Timeout block.
|
169
|
+
# Must be positive if provided. The default is nil (no timeout).
|
170
|
+
# * timeout_strategy - Strategy for timeout implementation. Options: :auto (default), :custom, :thread, :process, :fiber, :ruby_timeout
|
61
171
|
#
|
62
172
|
# Example:
|
63
173
|
#
|
64
|
-
# a = Attempt.new(tries: 5, increment: 10, timeout:
|
174
|
+
# a = Attempt.new(tries: 5, increment: 10, timeout: 30, timeout_strategy: :process)
|
65
175
|
# a.attempt{ http.get("http://something.foo.com") }
|
66
176
|
#
|
177
|
+
# Raises ArgumentError if any parameters are invalid.
|
178
|
+
#
|
67
179
|
def initialize(**options)
|
68
|
-
@tries = options[:tries] || 3
|
69
|
-
@interval = options[:interval] || 60
|
70
|
-
@log = options[:log]
|
71
|
-
@increment = options[:increment] || 0
|
72
|
-
@timeout = options[:timeout]
|
73
|
-
@
|
74
|
-
@
|
180
|
+
@tries = validate_tries(options[:tries] || 3)
|
181
|
+
@interval = validate_interval(options[:interval] || 60)
|
182
|
+
@log = validate_log(options[:log])
|
183
|
+
@increment = validate_increment(options[:increment] || 0)
|
184
|
+
@timeout = validate_timeout(options[:timeout])
|
185
|
+
@timeout_strategy = options[:timeout_strategy] || :auto
|
186
|
+
@level = options[:level] || StandardError # More appropriate default than Exception
|
187
|
+
@warnings = options.fetch(:warnings, true) # More explicit than ||
|
188
|
+
|
189
|
+
freeze_configuration if options[:freeze_config]
|
75
190
|
end
|
76
191
|
|
77
192
|
# Attempt to perform the operation in the provided block up to +tries+
|
@@ -80,38 +195,291 @@ class Attempt
|
|
80
195
|
# You will not typically use this method directly, but the Kernel#attempt
|
81
196
|
# method instead.
|
82
197
|
#
|
198
|
+
# Returns the result of the block if successful.
|
199
|
+
# Raises the last caught exception if all attempts fail.
|
200
|
+
#
|
83
201
|
def attempt(&block)
|
84
|
-
|
202
|
+
raise ArgumentError, 'No block given' unless block_given?
|
203
|
+
|
204
|
+
attempts_made = 0
|
205
|
+
current_interval = @interval
|
206
|
+
max_tries = @tries
|
207
|
+
|
85
208
|
begin
|
86
|
-
|
87
|
-
|
209
|
+
attempts_made += 1
|
210
|
+
|
211
|
+
result = if timeout_enabled?
|
212
|
+
execute_with_timeout(&block)
|
88
213
|
else
|
89
214
|
yield
|
90
215
|
end
|
216
|
+
|
217
|
+
return result
|
218
|
+
|
91
219
|
rescue @level => err
|
92
|
-
|
93
|
-
if @tries > 0
|
94
|
-
msg = "Error on attempt # #{count}: #{err}; retrying"
|
95
|
-
count += 1
|
96
|
-
warn Warning, msg if @warnings
|
97
|
-
|
98
|
-
if @log # Accept an IO or Logger object
|
99
|
-
@log.respond_to?(:puts) ? @log.puts(msg) : @log.warn(msg)
|
100
|
-
end
|
220
|
+
remaining_tries = max_tries - attempts_made
|
101
221
|
|
102
|
-
|
103
|
-
|
222
|
+
if remaining_tries > 0
|
223
|
+
log_retry_attempt(attempts_made, err)
|
224
|
+
sleep current_interval if current_interval > 0
|
225
|
+
current_interval += @increment if @increment && @increment > 0
|
104
226
|
retry
|
227
|
+
else
|
228
|
+
log_final_failure(attempts_made, err)
|
229
|
+
raise
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns true if this attempt instance has been configured to use timeouts
|
235
|
+
def timeout_enabled?
|
236
|
+
!@timeout.nil? && @timeout != false
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns the effective timeout value (handles both boolean and numeric values)
|
240
|
+
def effective_timeout
|
241
|
+
return nil unless timeout_enabled?
|
242
|
+
@timeout.is_a?(Numeric) ? @timeout : 10 # Default timeout if true was passed
|
243
|
+
end
|
244
|
+
|
245
|
+
# Returns a summary of the current configuration
|
246
|
+
def configuration
|
247
|
+
{
|
248
|
+
tries: @tries,
|
249
|
+
interval: @interval,
|
250
|
+
increment: @increment,
|
251
|
+
timeout: @timeout,
|
252
|
+
timeout_strategy: @timeout_strategy,
|
253
|
+
level: @level,
|
254
|
+
warnings: @warnings,
|
255
|
+
log: @log&.class&.name
|
256
|
+
}
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
# Execute the block with appropriate timeout mechanism
|
262
|
+
# Uses multiple strategies for better reliability
|
263
|
+
def execute_with_timeout(&block)
|
264
|
+
timeout_value = effective_timeout
|
265
|
+
return yield unless timeout_value
|
266
|
+
|
267
|
+
case @timeout_strategy
|
268
|
+
when :custom
|
269
|
+
execute_with_custom_timeout(timeout_value, &block)
|
270
|
+
when :thread
|
271
|
+
execute_with_thread_timeout(timeout_value, &block)
|
272
|
+
when :process
|
273
|
+
execute_with_process_timeout(timeout_value, &block)
|
274
|
+
when :fiber
|
275
|
+
execute_with_fiber_timeout(timeout_value, &block)
|
276
|
+
when :ruby_timeout
|
277
|
+
Timeout.timeout(timeout_value, &block)
|
278
|
+
else # :auto
|
279
|
+
execute_with_auto_timeout(timeout_value, &block)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Automatic timeout strategy selection
|
284
|
+
def execute_with_auto_timeout(timeout_value, &block)
|
285
|
+
# Try custom timeout first (most reliable)
|
286
|
+
begin
|
287
|
+
return execute_with_custom_timeout(timeout_value, &block)
|
288
|
+
rescue NameError, NoMethodError
|
289
|
+
# Fall back to other strategies
|
290
|
+
execute_with_fallback_timeout(timeout_value, &block)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Custom timeout using our AttemptTimeout class
|
295
|
+
def execute_with_custom_timeout(timeout_value, &block)
|
296
|
+
begin
|
297
|
+
return AttemptTimeout.timeout(timeout_value, &block)
|
298
|
+
rescue AttemptTimeout::Error => e
|
299
|
+
raise Timeout::Error, e.message # Convert to expected exception type
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Fallback timeout implementation using multiple strategies
|
304
|
+
def execute_with_fallback_timeout(timeout_value, &block)
|
305
|
+
# Strategy 2: Process-based timeout (most reliable for blocking operations)
|
306
|
+
if respond_to?(:system) && (!defined?(RUBY_ENGINE) || RUBY_ENGINE != 'jruby')
|
307
|
+
return execute_with_process_timeout(timeout_value, &block)
|
308
|
+
end
|
309
|
+
|
310
|
+
# Strategy 3: Fiber-based timeout (lightweight alternative)
|
311
|
+
begin
|
312
|
+
return execute_with_fiber_timeout(timeout_value, &block)
|
313
|
+
rescue NameError, NoMethodError
|
314
|
+
# Fiber support may not be available in all Ruby versions
|
315
|
+
end
|
316
|
+
|
317
|
+
# Strategy 4: Thread-based timeout with better error handling
|
318
|
+
return execute_with_thread_timeout(timeout_value, &block)
|
319
|
+
rescue
|
320
|
+
# Strategy 5: Last resort - use Ruby's Timeout (least reliable)
|
321
|
+
Timeout.timeout(timeout_value, &block)
|
322
|
+
end
|
323
|
+
|
324
|
+
# Process-based timeout - most reliable for I/O operations
|
325
|
+
def execute_with_process_timeout(timeout_value, &block)
|
326
|
+
reader, writer = IO.pipe
|
327
|
+
|
328
|
+
pid = fork do
|
329
|
+
reader.close
|
330
|
+
begin
|
331
|
+
result = yield
|
332
|
+
Marshal.dump(result, writer)
|
333
|
+
rescue => e
|
334
|
+
Marshal.dump({error: e}, writer)
|
335
|
+
ensure
|
336
|
+
writer.close
|
105
337
|
end
|
106
|
-
raise
|
107
338
|
end
|
339
|
+
|
340
|
+
writer.close
|
341
|
+
|
342
|
+
if Process.waitpid(pid, Process::WNOHANG)
|
343
|
+
# Process completed immediately
|
344
|
+
result = Marshal.load(reader)
|
345
|
+
else
|
346
|
+
# Wait for timeout
|
347
|
+
if IO.select([reader], nil, nil, timeout_value)
|
348
|
+
Process.waitpid(pid)
|
349
|
+
result = Marshal.load(reader)
|
350
|
+
else
|
351
|
+
Process.kill('TERM', pid)
|
352
|
+
Process.waitpid(pid)
|
353
|
+
raise Timeout::Error, "execution expired after #{timeout_value} seconds"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
reader.close
|
358
|
+
|
359
|
+
if result.is_a?(Hash) && result[:error]
|
360
|
+
raise result[:error]
|
361
|
+
end
|
362
|
+
|
363
|
+
result
|
364
|
+
rescue Errno::ECHILD, NotImplementedError
|
365
|
+
# Fork not available, fall back to thread-based
|
366
|
+
execute_with_thread_timeout(timeout_value, &block)
|
367
|
+
end
|
368
|
+
|
369
|
+
# Improved thread-based timeout
|
370
|
+
def execute_with_thread_timeout(timeout_value, &block)
|
371
|
+
result = nil
|
372
|
+
exception = nil
|
373
|
+
completed = false
|
374
|
+
|
375
|
+
thread = Thread.new do
|
376
|
+
begin
|
377
|
+
result = yield
|
378
|
+
rescue => e
|
379
|
+
exception = e
|
380
|
+
ensure
|
381
|
+
completed = true
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# Wait for completion or timeout
|
386
|
+
unless thread.join(timeout_value)
|
387
|
+
thread.kill
|
388
|
+
thread.join(0.1) # Give thread time to clean up
|
389
|
+
raise Timeout::Error, "execution expired after #{timeout_value} seconds"
|
390
|
+
end
|
391
|
+
|
392
|
+
raise exception if exception
|
393
|
+
result
|
394
|
+
end
|
395
|
+
|
396
|
+
# Fiber-based timeout - lightweight alternative
|
397
|
+
def execute_with_fiber_timeout(timeout_value, &block)
|
398
|
+
begin
|
399
|
+
return AttemptTimeout.fiber_timeout(timeout_value, &block)
|
400
|
+
rescue AttemptTimeout::Error => e
|
401
|
+
raise Timeout::Error, e.message # Convert to expected exception type
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
# Log retry attempt information
|
406
|
+
def log_retry_attempt(attempt_number, error)
|
407
|
+
msg = "Attempt #{attempt_number} failed: #{error.class}: #{error.message}; retrying"
|
408
|
+
|
409
|
+
warn Warning, msg if @warnings
|
410
|
+
log_message(msg)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Log final failure information
|
414
|
+
def log_final_failure(total_attempts, error)
|
415
|
+
msg = "All #{total_attempts} attempts failed. Final error: #{error.class}: #{error.message}"
|
416
|
+
log_message(msg)
|
417
|
+
end
|
418
|
+
|
419
|
+
# Helper method to handle logging to various output types
|
420
|
+
def log_message(message)
|
421
|
+
return unless @log
|
422
|
+
|
423
|
+
if @log.respond_to?(:warn)
|
424
|
+
@log.warn(message)
|
425
|
+
elsif @log.respond_to?(:puts)
|
426
|
+
@log.puts(message)
|
427
|
+
elsif @log.respond_to?(:write)
|
428
|
+
@log.write("#{message}\n")
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# Validation methods for better error handling
|
433
|
+
def validate_tries(tries)
|
434
|
+
unless tries.is_a?(Integer) && tries > 0
|
435
|
+
raise ArgumentError, "tries must be a positive integer, got: #{tries.inspect}"
|
436
|
+
end
|
437
|
+
tries
|
438
|
+
end
|
439
|
+
|
440
|
+
def validate_interval(interval)
|
441
|
+
unless interval.is_a?(Numeric) && interval >= 0
|
442
|
+
raise ArgumentError, "interval must be a non-negative number, got: #{interval.inspect}"
|
443
|
+
end
|
444
|
+
interval
|
445
|
+
end
|
446
|
+
|
447
|
+
def validate_increment(increment)
|
448
|
+
unless increment.is_a?(Numeric) && increment >= 0
|
449
|
+
raise ArgumentError, "increment must be a non-negative number, got: #{increment.inspect}"
|
450
|
+
end
|
451
|
+
increment
|
452
|
+
end
|
453
|
+
|
454
|
+
def validate_timeout(timeout)
|
455
|
+
return nil if timeout.nil?
|
456
|
+
return false if timeout == false
|
457
|
+
|
458
|
+
unless timeout.is_a?(Numeric) && timeout > 0
|
459
|
+
raise ArgumentError, "timeout must be a positive number or nil, got: #{timeout.inspect}"
|
460
|
+
end
|
461
|
+
timeout
|
462
|
+
end
|
463
|
+
|
464
|
+
def validate_log(log)
|
465
|
+
return nil if log.nil?
|
466
|
+
|
467
|
+
unless log.respond_to?(:puts) || log.respond_to?(:warn) || log.respond_to?(:write)
|
468
|
+
raise ArgumentError, "log must respond to :puts, :warn, or :write methods"
|
469
|
+
end
|
470
|
+
log
|
471
|
+
end
|
472
|
+
|
473
|
+
def freeze_configuration
|
474
|
+
instance_variables.each { |var| instance_variable_get(var).freeze }
|
475
|
+
freeze
|
108
476
|
end
|
109
477
|
end
|
110
478
|
|
111
479
|
# Extend the Kernel module with a simple interface for the Attempt class.
|
112
480
|
module Kernel
|
113
481
|
# :call-seq:
|
114
|
-
# attempt(tries: 3, interval: 60, timeout: 10){ # some op }
|
482
|
+
# attempt(tries: 3, interval: 60, timeout: 10, **options){ # some op }
|
115
483
|
#
|
116
484
|
# Attempt to perform the operation in the provided block up to +tries+
|
117
485
|
# times, sleeping +interval+ between each try. By default the number
|
@@ -126,12 +494,19 @@ module Kernel
|
|
126
494
|
#
|
127
495
|
# This is really just a convenient wrapper for Attempt.new + Attempt#attempt.
|
128
496
|
#
|
497
|
+
# All options supported by Attempt.new are also supported here.
|
498
|
+
#
|
129
499
|
# Example:
|
130
500
|
#
|
131
501
|
# # Make 3 attempts to connect to the database, 60 seconds apart.
|
132
502
|
# attempt{ DBI.connect(dsn, user, passwd) }
|
133
503
|
#
|
504
|
+
# # Make 5 attempts with exponential backoff
|
505
|
+
# attempt(tries: 5, interval: 1, increment: 2) { risky_operation }
|
506
|
+
#
|
134
507
|
def attempt(**kwargs, &block)
|
508
|
+
raise ArgumentError, 'No block given' unless block_given?
|
509
|
+
|
135
510
|
object = Attempt.new(**kwargs)
|
136
511
|
object.attempt(&block)
|
137
512
|
end
|
data/spec/attempt_spec.rb
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attempt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel J. Berger
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
ORVCZpRuCPpmC8qmqxUnARDArzucjaclkxjLWvCVHeFa9UP7K3Nl9oTjJNv+7/jM
|
36
36
|
WZs4eecIcUc4tKdHxcAJ0MO/Dkqq7hGaiHpwKY76wQ1+8xAh
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date:
|
38
|
+
date: 2025-07-14 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: rake
|
@@ -79,20 +79,6 @@ dependencies:
|
|
79
79
|
- - "~>"
|
80
80
|
- !ruby/object:Gem::Version
|
81
81
|
version: 0.4.0
|
82
|
-
- !ruby/object:Gem::Dependency
|
83
|
-
name: safe_timeout
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
85
|
-
requirements:
|
86
|
-
- - "~>"
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: 0.0.5
|
89
|
-
type: :runtime
|
90
|
-
prerelease: false
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
92
|
-
requirements:
|
93
|
-
- - "~>"
|
94
|
-
- !ruby/object:Gem::Version
|
95
|
-
version: 0.0.5
|
96
82
|
- !ruby/object:Gem::Dependency
|
97
83
|
name: rspec
|
98
84
|
requirement: !ruby/object:Gem::Requirement
|
@@ -126,6 +112,12 @@ files:
|
|
126
112
|
- Rakefile
|
127
113
|
- attempt.gemspec
|
128
114
|
- certs/djberg96_pub.pem
|
115
|
+
- doc/FIBER_TIMEOUT_ADDITION.md
|
116
|
+
- doc/IMPROVEMENTS.md
|
117
|
+
- doc/TIMEOUT_STRATEGIES.md
|
118
|
+
- examples/test_fiber_timeout.rb
|
119
|
+
- examples/test_improvements.rb
|
120
|
+
- examples/test_timeout_strategies.rb
|
129
121
|
- lib/attempt.rb
|
130
122
|
- spec/attempt_spec.rb
|
131
123
|
homepage: https://github.com/djberg96/attempt
|
@@ -140,6 +132,7 @@ metadata:
|
|
140
132
|
wiki_uri: https://github.com/djberg96/attempt/wiki
|
141
133
|
rubygems_mfa_required: 'true'
|
142
134
|
github_repo: https://github.com/djberg96/attempt
|
135
|
+
funding_uri: https://github.com/sponsors/djberg96
|
143
136
|
post_install_message:
|
144
137
|
rdoc_options: []
|
145
138
|
require_paths:
|
@@ -155,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
155
148
|
- !ruby/object:Gem::Version
|
156
149
|
version: '0'
|
157
150
|
requirements: []
|
158
|
-
rubygems_version: 3.
|
151
|
+
rubygems_version: 3.5.22
|
159
152
|
signing_key:
|
160
153
|
specification_version: 4
|
161
154
|
summary: A thin wrapper for begin + rescue + sleep + retry
|
metadata.gz.sig
CHANGED
Binary file
|