rack_jwt_aegis 1.1.0 → 1.2.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
- data/.ruby-version +1 -0
- data/.yardopts +6 -12
- data/CHANGELOG.md +105 -6
- data/README.md +60 -34
- data/lib/rack_jwt_aegis/cache_adapter.rb +4 -4
- data/lib/rack_jwt_aegis/circuit_breaker.rb +51 -0
- data/lib/rack_jwt_aegis/configuration.rb +216 -67
- data/lib/rack_jwt_aegis/jwt_validator.rb +22 -0
- data/lib/rack_jwt_aegis/middleware.rb +44 -7
- data/lib/rack_jwt_aegis/multi_tenant_validator.rb +49 -2
- data/lib/rack_jwt_aegis/rbac_manager.rb +28 -60
- data/lib/rack_jwt_aegis/request_context.rb +14 -1
- data/lib/rack_jwt_aegis/version.rb +1 -1
- data/lib/rack_jwt_aegis.rb +3 -3
- metadata +24 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5246ed44e15045cae4b9ec776d95b29e3f30f40008b41081c44acc61105e3de
|
|
4
|
+
data.tar.gz: 0d72626e7d23203a12ff37dbbf1516664f2043a52423404a9fdd43cd9ddc85ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2ac0d480d1e060e36e114fccfb373c6c6602913f49f227c94d24feb112590f877146e903c3dfdc726c2dc2c130ae9f1c183a968febba522e1607e2483cfba09
|
|
7
|
+
data.tar.gz: 2c349695ae61b01a65fd87f8de710d85f356e29b0328a639ea5aef5548c40140aaa4395a5245fb0374caabdaa9dd11abf14c88a161275f4f4bd3b0f233bda621
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.2
|
data/.yardopts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
--
|
|
2
|
-
--readme README.md
|
|
3
|
-
--main README.md
|
|
4
|
-
--markup=markdown
|
|
1
|
+
--title "RackJwtAegis Rack Middleware API Documentation""
|
|
5
2
|
--protected
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
|
|
9
|
-
--charset utf-8
|
|
10
|
-
lib/**/*.rb
|
|
3
|
+
--markup markdown
|
|
4
|
+
--readme README.md
|
|
5
|
+
'lib/**/*.rb'
|
|
11
6
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
CODE_OF_CONDUCT.md
|
|
7
|
+
--files CHANGELOG.md LICENSE.txt CODE_OF_CONDUCT.md
|
|
8
|
+
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,103 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Unreleased
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## [1.2.0] - 2026-08-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `skip_options_requests` to bypass JWT authentication for CORS preflight
|
|
10
|
+
requests while leaving ordinary requests protected.
|
|
11
|
+
- Documented the intended middleware ordering: `Rack::Cors` must run before
|
|
12
|
+
`RackJwtAegis::Middleware`.
|
|
13
|
+
|
|
14
|
+
## [1.1.1] - 2026-06-13
|
|
15
|
+
|
|
16
|
+
### 🚀 Added
|
|
17
|
+
|
|
18
|
+
#### Method-Aware Skip Routes
|
|
19
|
+
|
|
20
|
+
- Added `skip_routes` to support request-aware public route matching by path and HTTP verb.
|
|
21
|
+
- Kept `skip_paths` as a backward-compatible alias for all-method skips.
|
|
22
|
+
- Normalizes route rules once at configuration time so middleware checks stay fast per request.
|
|
23
|
+
|
|
24
|
+
#### Route Skipping Behavior
|
|
25
|
+
|
|
26
|
+
- Middleware now evaluates `METHOD + path` for skip decisions, so `POST /login` can be public while `GET /login` remains protected.
|
|
27
|
+
- Added support for blank verb lists to mean all methods on a route entry.
|
|
28
|
+
- Updated the Pink House API initializer to use the new `skip_routes` shape.
|
|
29
|
+
|
|
30
|
+
### 🔧 Fixed
|
|
31
|
+
|
|
32
|
+
#### Simplified RBAC Cache Strategy
|
|
33
|
+
|
|
34
|
+
- **Streamlined Configuration**: Removed legacy cache configuration options to eliminate confusion
|
|
35
|
+
- **Removed**: `cache_store`, `cache_options`, and `cache_write_enabled` (deprecated in favor of dedicated RBAC cache stores)
|
|
36
|
+
- **Required**: When `rbac_enabled: true`, both `rbac_cache_store` and `permissions_cache_store` must be explicitly configured
|
|
37
|
+
- **Simplified**: Consistent naming with `rbac_cache_store_options` and `permissions_cache_store_options`
|
|
38
|
+
|
|
39
|
+
#### Cache Store Validation
|
|
40
|
+
|
|
41
|
+
- **Mandatory Configuration**: RBAC cache stores are now required when RBAC is enabled
|
|
42
|
+
- Prevents runtime errors from missing cache configuration
|
|
43
|
+
- Provides clear error messages during initialization: "rbac_cache_store and permissions_cache_store are required when RBAC is enabled"
|
|
44
|
+
- Ensures production deployments are properly configured for performance
|
|
45
|
+
|
|
46
|
+
#### Code Cleanup
|
|
47
|
+
|
|
48
|
+
- **Removed Complexity**: Eliminated dual cache mode logic and fallback mechanisms
|
|
49
|
+
- Simplified `RbacManager#setup_cache_adapters` method
|
|
50
|
+
- Removed conditional cache write logic that added complexity
|
|
51
|
+
- Cleaner permission checking flow with consistent cache behavior
|
|
52
|
+
|
|
53
|
+
#### Developer Experience
|
|
54
|
+
|
|
55
|
+
- **Clear Configuration**: Explicit cache store requirements eliminate guesswork
|
|
56
|
+
- **Better Error Messages**: Configuration validation happens at startup, not at runtime
|
|
57
|
+
- **Consistent Naming**: Standardized cache option parameter names across all adapters
|
|
58
|
+
|
|
59
|
+
### 🏗️ Technical Details
|
|
60
|
+
|
|
61
|
+
#### Breaking Changes (Minor - Configuration Only)
|
|
62
|
+
|
|
63
|
+
- **Removed Configuration Options**:
|
|
64
|
+
- `cache_store` → Use `rbac_cache_store` and `permissions_cache_store` instead
|
|
65
|
+
- `cache_options` → Use `rbac_cache_store_options` and `permissions_cache_store_options` instead
|
|
66
|
+
- `cache_write_enabled` → Cache writing is now always enabled when RBAC is active
|
|
67
|
+
|
|
68
|
+
#### Migration Guide
|
|
69
|
+
|
|
70
|
+
**Before (v1.1.0):**
|
|
71
|
+
```ruby
|
|
72
|
+
use RackJwtAegis::Middleware, {
|
|
73
|
+
jwt_secret: ENV['JWT_SECRET'],
|
|
74
|
+
rbac_enabled: true,
|
|
75
|
+
cache_store: :redis,
|
|
76
|
+
cache_options: { url: ENV['REDIS_URL'] },
|
|
77
|
+
cache_write_enabled: true
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**After (v1.1.1):**
|
|
82
|
+
```ruby
|
|
83
|
+
use RackJwtAegis::Middleware, {
|
|
84
|
+
jwt_secret: ENV['JWT_SECRET'],
|
|
85
|
+
rbac_enabled: true,
|
|
86
|
+
rbac_cache_store: :redis,
|
|
87
|
+
rbac_cache_store_options: { url: ENV['REDIS_URL'] },
|
|
88
|
+
permissions_cache_store: :redis,
|
|
89
|
+
permissions_cache_store_options: { url: ENV['REDIS_URL'] }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Benefits
|
|
94
|
+
|
|
95
|
+
- **🚀 Simpler Configuration**: No more dual cache modes or conditional logic
|
|
96
|
+
- **🛡️ Fail-Fast Validation**: Configuration errors caught at startup
|
|
97
|
+
- **📈 Better Performance**: Dedicated cache stores optimized for their specific use cases
|
|
98
|
+
- **🧹 Cleaner Code**: Reduced complexity in cache management logic
|
|
99
|
+
|
|
100
|
+
---
|
|
7
101
|
|
|
8
102
|
## [1.1.0] - 2025-08-14
|
|
9
103
|
|
|
@@ -71,11 +165,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
71
165
|
#### Developer Experience
|
|
72
166
|
|
|
73
167
|
- **Better Error Messages**: Clear configuration error descriptions
|
|
168
|
+
|
|
74
169
|
```ruby
|
|
75
170
|
# Example error message:
|
|
76
|
-
"RBAC permissions must be a Hash with role-id keys, not Array.
|
|
171
|
+
"RBAC permissions must be a Hash with role-id keys, not Array.
|
|
77
172
|
Expected format: {\"role-id\": [\"resource:method\", ...]}, but got: Array"
|
|
78
173
|
```
|
|
174
|
+
|
|
79
175
|
- **Migration Support**: Catches common mistakes when upgrading cache format
|
|
80
176
|
- **Improved Documentation**: Cleaner YARD output with better Markdown rendering
|
|
81
177
|
|
|
@@ -96,6 +192,7 @@ Expected format: {\"role-id\": [\"resource:method\", ...]}, but got: Array"
|
|
|
96
192
|
#### Updating RBAC Cache Format
|
|
97
193
|
|
|
98
194
|
**Before (v1.0.x):**
|
|
195
|
+
|
|
99
196
|
```ruby
|
|
100
197
|
Rails.cache.write("permissions", {
|
|
101
198
|
'last_update' => Time.now.to_i,
|
|
@@ -107,6 +204,7 @@ Rails.cache.write("permissions", {
|
|
|
107
204
|
```
|
|
108
205
|
|
|
109
206
|
**After (v1.1.0+):**
|
|
207
|
+
|
|
110
208
|
```ruby
|
|
111
209
|
Rails.cache.write("permissions", {
|
|
112
210
|
'last_update' => Time.now.to_i,
|
|
@@ -294,8 +392,8 @@ use RackJwtAegis::Middleware, {
|
|
|
294
392
|
pathname_slug_pattern: /^\/api\/v1\/([^\/]+)\//,
|
|
295
393
|
rbac_enabled: true,
|
|
296
394
|
rbac_cache_store: :redis,
|
|
297
|
-
|
|
298
|
-
|
|
395
|
+
permissions_cache_store: :memory,
|
|
396
|
+
cached_permissions_ttl: 300,
|
|
299
397
|
cache_write_enabled: true,
|
|
300
398
|
skip_paths: [/^\/health/, /^\/metrics/, /^\/api\/public/],
|
|
301
399
|
custom_payload_validation: ->(payload) { payload['active'] == true },
|
|
@@ -369,6 +467,7 @@ This 1.0.0 release represents a production-ready JWT authentication middleware w
|
|
|
369
467
|
|
|
370
468
|
**Deprecations**: None (initial release).
|
|
371
469
|
|
|
470
|
+
[1.1.1]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.1.1
|
|
372
471
|
[1.1.0]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.1.0
|
|
373
472
|
[1.0.2]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.2
|
|
374
473
|
[1.0.1]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.1
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/rack_jwt_aegis)
|
|
4
4
|
[](https://github.com/kanutocd/rack_jwt_aegis/actions)
|
|
5
5
|
[](https://codecov.io/gh/kanutocd/rack_jwt_aegis)
|
|
6
|
-
[](https://www.ruby-lang.org/en/)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
9
|
JWT authentication and authorization middleware for hierarchical multi-tenant Rack applications with 2-level tenant support.
|
|
@@ -15,7 +15,8 @@ JWT authentication and authorization middleware for hierarchical multi-tenant Ra
|
|
|
15
15
|
- Subdomain-based tenant isolation for top-level tenants
|
|
16
16
|
- URL pathname slug access control for sub-level tenants
|
|
17
17
|
- **RBAC (Role-Based Access Control)** with flexible role extraction from JWT payloads
|
|
18
|
-
- Configurable
|
|
18
|
+
- Configurable route exclusions for public endpoints
|
|
19
|
+
- Optional automatic bypass for browser CORS preflight requests
|
|
19
20
|
- **Flexible payload mapping** for custom JWT claim names
|
|
20
21
|
- Custom payload validation
|
|
21
22
|
- Debug mode for development
|
|
@@ -79,15 +80,36 @@ Rack JWT Aegis includes a command-line tool for generating secure JWT secrets:
|
|
|
79
80
|
### Rails Application
|
|
80
81
|
|
|
81
82
|
```ruby
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
# config/initializers/cors.rb
|
|
84
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
|
85
|
+
allow do
|
|
86
|
+
origins 'https://app.example.com'
|
|
87
|
+
resource '*', headers: :any,
|
|
88
|
+
methods: %i[get post put patch delete options head]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# config/initializers/rack_jwt_aegis.rb
|
|
93
|
+
Rails.application.config.middleware.insert_after Rack::Cors, RackJwtAegis::Middleware, {
|
|
84
94
|
jwt_secret: ENV['JWT_SECRET'],
|
|
85
95
|
validate_tenant_id: true,
|
|
86
96
|
tenant_id_header_name: 'X-Tenant-Id',
|
|
87
|
-
|
|
97
|
+
# Browser OPTIONS preflight requests do not carry bearer credentials.
|
|
98
|
+
skip_options_requests: true,
|
|
99
|
+
skip_routes: [
|
|
100
|
+
{ path: '/api/v1/login', verbs: [:post] },
|
|
101
|
+
{ path: '/health' }
|
|
102
|
+
]
|
|
88
103
|
}
|
|
89
104
|
```
|
|
90
105
|
|
|
106
|
+
`Rack::Cors` should run before `RackJwtAegis::Middleware` in the Rack stack.
|
|
107
|
+
It can then validate the request origin, requested method, and requested
|
|
108
|
+
headers and handle valid preflight requests before JWT authentication runs.
|
|
109
|
+
`skip_options_requests: true` is an additional middleware-level safeguard: it
|
|
110
|
+
skips JWT authentication for `OPTIONS` requests only. It does not make any
|
|
111
|
+
non-`OPTIONS` request public, and it does not replace CORS validation.
|
|
112
|
+
|
|
91
113
|
### Sinatra Application
|
|
92
114
|
|
|
93
115
|
```ruby
|
|
@@ -97,7 +119,10 @@ Rack JWT Aegis includes a command-line tool for generating secure JWT secrets:
|
|
|
97
119
|
jwt_secret: ENV['JWT_SECRET'],
|
|
98
120
|
validate_tenant_id: true,
|
|
99
121
|
tenant_id_header_name: 'X-Tenant-Id',
|
|
100
|
-
|
|
122
|
+
skip_routes: [
|
|
123
|
+
{ path: '/login', verbs: [:post] },
|
|
124
|
+
{ path: '/health' }
|
|
125
|
+
]
|
|
101
126
|
}
|
|
102
127
|
```
|
|
103
128
|
|
|
@@ -142,33 +167,33 @@ RackJwtAegis::Middleware.new(app, {
|
|
|
142
167
|
role_ids: :role_ids,
|
|
143
168
|
},
|
|
144
169
|
|
|
145
|
-
#
|
|
146
|
-
|
|
170
|
+
# Route Configuration
|
|
171
|
+
skip_options_requests: false, # Set true when Rack::Cors handles browser preflight requests
|
|
172
|
+
skip_routes: [
|
|
173
|
+
{ path: '/health' },
|
|
174
|
+
{ path: '/api/v1/login', verbs: [:post] }
|
|
175
|
+
],
|
|
147
176
|
pathname_slug_pattern: /^\/api\/v1\/([^\/]+)\//, # Default pattern
|
|
148
177
|
|
|
149
178
|
# RBAC Configuration
|
|
150
|
-
rbac_enabled: true,
|
|
151
|
-
rbac_cache_store: :redis,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
#
|
|
161
|
-
rbac_cache_store: :redis, # For RBAC permissions data
|
|
162
|
-
rbac_cache_options: { url: ENV['REDIS_URL'] },
|
|
163
|
-
permission_cache_store: :memory, # For cached user permissions
|
|
164
|
-
permission_cache_options: {},
|
|
179
|
+
rbac_enabled: true, # Default: false
|
|
180
|
+
rbac_cache_store: :redis, # Required when RBAC enabled. Default: :memory
|
|
181
|
+
rbac_cache_store_options: { url: ENV['REDIS_URL'] }, # Cache Store specific options. Default: {}
|
|
182
|
+
|
|
183
|
+
cached_permissions_ttl: 3600, # Default: 1800 (30 minutes) - TTL for cached user permissions
|
|
184
|
+
permissions_cache_store: :solid_cache, # Required when RBAC enabled. Default: :memory
|
|
185
|
+
permissions_cache_store_options: {}, # Cache Store specific options. Default: {}
|
|
186
|
+
|
|
187
|
+
# Or can also be the same Redis instance
|
|
188
|
+
# permissions_cache_store: :redis, # Required when RBAC enabled. Default: :memory
|
|
189
|
+
# permissions_cache_store_options: { url: ENV['REDIS_URL'] },
|
|
165
190
|
|
|
166
191
|
# Response Customization
|
|
167
192
|
unauthorized_response: { error: 'Authentication required' },
|
|
168
193
|
forbidden_response: { error: 'Access denied' },
|
|
169
194
|
|
|
170
195
|
# Debugging
|
|
171
|
-
debug_mode: Rails.env.development? # Default: false
|
|
196
|
+
debug_mode: Rails.env.development? # Default: when in Rails, Rails.env.development? otherwise false
|
|
172
197
|
})
|
|
173
198
|
```
|
|
174
199
|
|
|
@@ -404,18 +429,19 @@ All role values are normalized to strings internally for consistent matching aga
|
|
|
404
429
|
|
|
405
430
|
```ruby
|
|
406
431
|
# Memory cache (development/testing)
|
|
407
|
-
config.
|
|
432
|
+
config.rbac_cache_store = :memory # Default. When in Rails, it will default to Rails.application.config.cache_store
|
|
433
|
+
config.permissions_cache_store = :memory # Default. When in Rails, it will default to Rails.application.config.cache_store
|
|
408
434
|
|
|
409
435
|
# Redis cache
|
|
410
|
-
config.
|
|
411
|
-
config.
|
|
436
|
+
config.rbac_cache_store = :redis
|
|
437
|
+
config.rbac_cache_store_options = { url: ENV['REDIS_URL'] }
|
|
412
438
|
|
|
413
439
|
# Memcached cache
|
|
414
|
-
config.
|
|
415
|
-
config.
|
|
440
|
+
config.rbac_cache_store = :memcached
|
|
441
|
+
config.rbac_cache_store_options = { servers: ['localhost:11211'] }
|
|
416
442
|
|
|
417
443
|
# Solid Cache (Rails 8+)
|
|
418
|
-
config.
|
|
444
|
+
config.rbac_cache_store = :solid_cache
|
|
419
445
|
```
|
|
420
446
|
|
|
421
447
|
#### Separate Cache Stores
|
|
@@ -425,11 +451,11 @@ You can configure separate cache stores for RBAC permissions data and cached use
|
|
|
425
451
|
```ruby
|
|
426
452
|
# Use Redis for RBAC data (shared across instances)
|
|
427
453
|
config.rbac_cache_store = :redis
|
|
428
|
-
config.
|
|
454
|
+
config.rbac_cache_store_options = { url: ENV['REDIS_URL'] }
|
|
429
455
|
|
|
430
456
|
# Use memory for user permission cache (faster local access)
|
|
431
|
-
config.
|
|
432
|
-
config.
|
|
457
|
+
config.permissions_cache_store = :memory
|
|
458
|
+
config.permissions_cache_store_options = {}
|
|
433
459
|
```
|
|
434
460
|
|
|
435
461
|
## RBAC Cache Format
|
|
@@ -506,7 +532,7 @@ When RBAC is enabled, the middleware expects permissions to be stored in the cac
|
|
|
506
532
|
"12345:acme-group.localhost.local/api/v1/company/sales/invoices:post": 1640995200
|
|
507
533
|
}
|
|
508
534
|
```
|
|
509
|
-
TTL configurable via `
|
|
535
|
+
TTL configurable via `cached_permissions_ttl` option (default: 30 minutes)
|
|
510
536
|
|
|
511
537
|
### Cache Invalidation Strategy
|
|
512
538
|
|
|
@@ -4,13 +4,13 @@ module RackJwtAegis
|
|
|
4
4
|
class CacheAdapter
|
|
5
5
|
def self.build(store_type, options = {})
|
|
6
6
|
case store_type
|
|
7
|
-
when :memory
|
|
7
|
+
when :memory, :memory_store
|
|
8
8
|
MemoryAdapter.new(options)
|
|
9
|
-
when :redis
|
|
9
|
+
when :redis, :redis_cache_store
|
|
10
10
|
RedisAdapter.new(options)
|
|
11
|
-
when :memcached
|
|
11
|
+
when :memcached, :mem_cache_store
|
|
12
12
|
MemcachedAdapter.new(options)
|
|
13
|
-
when :solid_cache
|
|
13
|
+
when :solid_cache, :solid_cache_store
|
|
14
14
|
SolidCacheAdapter.new(options)
|
|
15
15
|
else
|
|
16
16
|
raise ConfigurationError, "Unsupported cache store: #{store_type}"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ratomic'
|
|
4
|
+
|
|
5
|
+
module RackJwtAegis
|
|
6
|
+
class CircuitBreaker
|
|
7
|
+
STATE_FAILURE_COUNT = 'failure_count'
|
|
8
|
+
STATE_OPENED_AT = 'opened_at'
|
|
9
|
+
|
|
10
|
+
def initialize(failure_threshold:, cooldown_seconds:)
|
|
11
|
+
@failure_threshold = failure_threshold.to_i
|
|
12
|
+
@cooldown_seconds = cooldown_seconds.to_i
|
|
13
|
+
@state = Ratomic::Map.new
|
|
14
|
+
reset
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def allow_request?
|
|
18
|
+
return true unless open?
|
|
19
|
+
return false unless cooldown_elapsed?
|
|
20
|
+
|
|
21
|
+
reset
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def record_success
|
|
26
|
+
reset
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def record_failure
|
|
30
|
+
failures = @state.increment(STATE_FAILURE_COUNT)
|
|
31
|
+
@state[STATE_OPENED_AT] = Time.now.to_f if failures >= @failure_threshold
|
|
32
|
+
failures
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def open?
|
|
36
|
+
!@state[STATE_OPENED_AT].nil?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def reset
|
|
42
|
+
@state[STATE_FAILURE_COUNT] = 0
|
|
43
|
+
@state.delete(STATE_OPENED_AT)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cooldown_elapsed?
|
|
47
|
+
opened_at = @state[STATE_OPENED_AT]
|
|
48
|
+
opened_at && (Time.now.to_f - opened_at) >= @cooldown_seconds
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|