throttle_machines 0.1.1 → 0.2.4
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/README.md +3 -0
- data/lib/throttle_machines/async_breaker.rb +12 -0
- data/lib/throttle_machines/async_limiter.rb +7 -117
- data/lib/throttle_machines/async_support.rb +75 -0
- data/lib/throttle_machines/controller_helpers.rb +2 -2
- data/lib/throttle_machines/engine.rb +1 -6
- data/lib/throttle_machines/hedged_breaker.rb +2 -2
- data/lib/throttle_machines/hedged_request.rb +2 -2
- data/lib/throttle_machines/instrumentation.rb +19 -19
- data/lib/throttle_machines/middleware.rb +1 -2
- data/lib/throttle_machines/native_speedup.rb +98 -0
- data/lib/throttle_machines/rack_middleware/allow2_ban.rb +11 -22
- data/lib/throttle_machines/rack_middleware/ban_filter.rb +29 -0
- data/lib/throttle_machines/rack_middleware/blocklist.rb +5 -18
- data/lib/throttle_machines/rack_middleware/configuration.rb +14 -14
- data/lib/throttle_machines/rack_middleware/fail2_ban.rb +20 -29
- data/lib/throttle_machines/rack_middleware/list_filter.rb +39 -0
- data/lib/throttle_machines/rack_middleware/safelist.rb +5 -18
- data/lib/throttle_machines/rack_middleware.rb +2 -4
- data/lib/throttle_machines/storage/base.rb +2 -23
- data/lib/throttle_machines/storage/memory.rb +7 -113
- data/lib/throttle_machines/storage/null.rb +0 -21
- data/lib/throttle_machines/storage/redis/gcra.lua +1 -1
- data/lib/throttle_machines/storage/redis/increment_counter.lua +1 -1
- data/lib/throttle_machines/storage/redis/peek_gcra.lua +1 -1
- data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +1 -1
- data/lib/throttle_machines/storage/redis/token_bucket.lua +1 -1
- data/lib/throttle_machines/storage/redis.rb +5 -68
- data/lib/throttle_machines/version.rb +1 -1
- data/lib/throttle_machines.rb +87 -21
- metadata +74 -15
- data/Rakefile +0 -12
- data/lib/throttle_machines/storage/redis/get_breaker_state.lua +0 -23
- data/lib/throttle_machines/storage/redis/record_breaker_failure.lua +0 -24
- data/lib/throttle_machines/storage/redis/record_breaker_success.lua +0 -16
data/lib/throttle_machines.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'timeout'
|
|
5
5
|
require 'zeitwerk'
|
|
6
|
-
require 'active_support/
|
|
6
|
+
require 'active_support/core_ext/class/attribute'
|
|
7
7
|
|
|
8
8
|
# Ecosystem dependencies
|
|
9
9
|
require 'chrono_machines'
|
|
@@ -15,27 +15,53 @@ loader.ignore("#{__dir__}/throttle_machines/engine.rb") unless defined?(Rails::E
|
|
|
15
15
|
loader.setup
|
|
16
16
|
|
|
17
17
|
module ThrottleMachines
|
|
18
|
-
|
|
18
|
+
class Configuration
|
|
19
|
+
attr_accessor :default_limit, :default_period, :default_storage, :clock,
|
|
20
|
+
:default_algorithm, :instrumentation_enabled, :instrumentation_backend,
|
|
21
|
+
:_storage_instance
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@default_limit = 100
|
|
25
|
+
@default_period = 60 # 1 minute
|
|
26
|
+
@default_storage = :memory
|
|
27
|
+
@default_algorithm = :fixed_window
|
|
28
|
+
@clock = nil
|
|
29
|
+
@instrumentation_enabled = true
|
|
30
|
+
@instrumentation_backend = nil
|
|
31
|
+
@fiber_safe = false
|
|
32
|
+
@_storage_instance = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fiber_safe
|
|
36
|
+
@fiber_safe
|
|
37
|
+
end
|
|
19
38
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
def fiber_safe=(value)
|
|
40
|
+
@fiber_safe = !!value
|
|
41
|
+
ThrottleMachines.load_async_runtime! if @fiber_safe && ThrottleMachines.respond_to?(:load_async_runtime!)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias async_enabled fiber_safe
|
|
45
|
+
alias async_enabled= fiber_safe=
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@config = Configuration.new
|
|
28
49
|
|
|
29
50
|
class << self
|
|
30
51
|
# Delegate monotonic time to BreakerMachines for consistency
|
|
31
52
|
delegate :monotonic_time, to: :BreakerMachines
|
|
32
53
|
|
|
54
|
+
def config
|
|
55
|
+
@config
|
|
56
|
+
end
|
|
57
|
+
|
|
33
58
|
def configure
|
|
34
59
|
yield(config) if block_given?
|
|
35
60
|
|
|
36
61
|
# Apply instrumentation settings
|
|
37
62
|
Instrumentation.enabled = config.instrumentation_enabled
|
|
38
63
|
Instrumentation.backend = config.instrumentation_backend if config.instrumentation_backend
|
|
64
|
+
load_async_runtime! if config.fiber_safe
|
|
39
65
|
end
|
|
40
66
|
|
|
41
67
|
def storage
|
|
@@ -65,32 +91,52 @@ module ThrottleMachines
|
|
|
65
91
|
control
|
|
66
92
|
end
|
|
67
93
|
|
|
68
|
-
def limit(key, limit:, period:, algorithm:
|
|
94
|
+
def limit(key, limit:, period:, algorithm: nil, &block)
|
|
69
95
|
limiter = limiter(key, limit: limit, period: period, algorithm: algorithm)
|
|
70
|
-
limiter.throttle!(&)
|
|
96
|
+
limiter.throttle!(&block)
|
|
71
97
|
end
|
|
72
98
|
|
|
73
|
-
def break_circuit(key, failures:, timeout:, &)
|
|
74
|
-
# Delegate to
|
|
99
|
+
def break_circuit(key, failures:, timeout:, &block)
|
|
100
|
+
# Delegate to BreakerMachines; use reset_timeout for open duration
|
|
75
101
|
breaker = BreakerMachines::Circuit.new(
|
|
76
|
-
key
|
|
102
|
+
key,
|
|
77
103
|
failure_threshold: failures,
|
|
78
|
-
|
|
104
|
+
reset_timeout: timeout
|
|
79
105
|
)
|
|
80
|
-
breaker.call(&)
|
|
106
|
+
breaker.call(&block)
|
|
81
107
|
end
|
|
82
108
|
|
|
83
|
-
def retry_with(max_attempts: 3, backoff: :exponential, &)
|
|
109
|
+
def retry_with(max_attempts: 3, backoff: :exponential, &block)
|
|
84
110
|
# Delegate to chrono_machines
|
|
85
111
|
policy_options = {
|
|
86
112
|
max_attempts: max_attempts,
|
|
87
113
|
jitter_factor: backoff == :exponential ? 1.0 : 0.0
|
|
88
114
|
}
|
|
89
|
-
ChronoMachines.retry(policy_options, &)
|
|
115
|
+
ChronoMachines.retry(policy_options, &block)
|
|
90
116
|
end
|
|
91
117
|
|
|
92
|
-
def limiter(key, limit:, period:, algorithm:
|
|
93
|
-
Limiter.new(
|
|
118
|
+
def limiter(key, limit:, period:, algorithm: nil)
|
|
119
|
+
Limiter.new(
|
|
120
|
+
key,
|
|
121
|
+
limit: limit,
|
|
122
|
+
period: period,
|
|
123
|
+
algorithm: algorithm || config.default_algorithm,
|
|
124
|
+
storage: storage
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def async_breaker(...)
|
|
129
|
+
ThrottleMachines::AsyncBreaker.new(...)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def load_async_runtime!
|
|
133
|
+
require 'async'
|
|
134
|
+
require 'async/task'
|
|
135
|
+
true
|
|
136
|
+
rescue LoadError => e
|
|
137
|
+
raise unless e.message.include?('async')
|
|
138
|
+
|
|
139
|
+
raise LoadError, "The 'async' gem is required for fiber_safe mode. Add `gem 'async'` to your Gemfile."
|
|
94
140
|
end
|
|
95
141
|
|
|
96
142
|
private
|
|
@@ -131,4 +177,24 @@ module ThrottleMachines
|
|
|
131
177
|
|
|
132
178
|
CircuitOpenError = BreakerMachines::CircuitOpenError
|
|
133
179
|
RetryExhaustedError = ChronoMachines::MaxRetriesExceededError
|
|
180
|
+
|
|
181
|
+
# Back-compat wrapper: use BreakerMachines as the circuit implementation
|
|
182
|
+
Breaker = BreakerMachines::Circuit
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Optional speedups (order matters: most specific first)
|
|
186
|
+
# These are loaded after Zeitwerk setup to allow graceful fallback.
|
|
187
|
+
|
|
188
|
+
# Native Rust extension for performance-critical algorithms
|
|
189
|
+
begin
|
|
190
|
+
require_relative "throttle_machines/native_speedup"
|
|
191
|
+
rescue LoadError
|
|
192
|
+
# Native extension not available, pure Ruby will be used
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Async/Fiber support when Async gem is loaded
|
|
196
|
+
begin
|
|
197
|
+
require_relative "throttle_machines/async_support"
|
|
198
|
+
rescue LoadError
|
|
199
|
+
# Async gem not available
|
|
134
200
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: throttle_machines
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 8.0.4
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 8.0.4
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: concurrent-ruby
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,28 +71,84 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0.
|
|
74
|
+
version: '0.19'
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '0.
|
|
81
|
+
version: '0.19'
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: chrono_machines
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
|
-
- - "
|
|
86
|
+
- - "~>"
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '0.
|
|
88
|
+
version: '0.9'
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
|
-
- - "
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.9'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: minitest
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '5.16'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '5.16'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rb_sys
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.9'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.9'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rake
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
94
129
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '0
|
|
130
|
+
version: '13.0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '13.0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: rake-compiler
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '1.3'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '1.3'
|
|
96
152
|
description: ThrottleMachines provides ultra-thin, elegant rate limiting with temporal
|
|
97
153
|
precision for distributed systems.
|
|
98
154
|
email:
|
|
@@ -103,9 +159,10 @@ extra_rdoc_files: []
|
|
|
103
159
|
files:
|
|
104
160
|
- LICENSE
|
|
105
161
|
- README.md
|
|
106
|
-
- Rakefile
|
|
107
162
|
- lib/throttle_machines.rb
|
|
163
|
+
- lib/throttle_machines/async_breaker.rb
|
|
108
164
|
- lib/throttle_machines/async_limiter.rb
|
|
165
|
+
- lib/throttle_machines/async_support.rb
|
|
109
166
|
- lib/throttle_machines/control.rb
|
|
110
167
|
- lib/throttle_machines/controller_helpers.rb
|
|
111
168
|
- lib/throttle_machines/dependency_error.rb
|
|
@@ -115,11 +172,14 @@ files:
|
|
|
115
172
|
- lib/throttle_machines/instrumentation.rb
|
|
116
173
|
- lib/throttle_machines/limiter.rb
|
|
117
174
|
- lib/throttle_machines/middleware.rb
|
|
175
|
+
- lib/throttle_machines/native_speedup.rb
|
|
118
176
|
- lib/throttle_machines/rack_middleware.rb
|
|
119
177
|
- lib/throttle_machines/rack_middleware/allow2_ban.rb
|
|
178
|
+
- lib/throttle_machines/rack_middleware/ban_filter.rb
|
|
120
179
|
- lib/throttle_machines/rack_middleware/blocklist.rb
|
|
121
180
|
- lib/throttle_machines/rack_middleware/configuration.rb
|
|
122
181
|
- lib/throttle_machines/rack_middleware/fail2_ban.rb
|
|
182
|
+
- lib/throttle_machines/rack_middleware/list_filter.rb
|
|
123
183
|
- lib/throttle_machines/rack_middleware/request.rb
|
|
124
184
|
- lib/throttle_machines/rack_middleware/safelist.rb
|
|
125
185
|
- lib/throttle_machines/rack_middleware/throttle.rb
|
|
@@ -129,12 +189,9 @@ files:
|
|
|
129
189
|
- lib/throttle_machines/storage/null.rb
|
|
130
190
|
- lib/throttle_machines/storage/redis.rb
|
|
131
191
|
- lib/throttle_machines/storage/redis/gcra.lua
|
|
132
|
-
- lib/throttle_machines/storage/redis/get_breaker_state.lua
|
|
133
192
|
- lib/throttle_machines/storage/redis/increment_counter.lua
|
|
134
193
|
- lib/throttle_machines/storage/redis/peek_gcra.lua
|
|
135
194
|
- lib/throttle_machines/storage/redis/peek_token_bucket.lua
|
|
136
|
-
- lib/throttle_machines/storage/redis/record_breaker_failure.lua
|
|
137
|
-
- lib/throttle_machines/storage/redis/record_breaker_success.lua
|
|
138
195
|
- lib/throttle_machines/storage/redis/token_bucket.lua
|
|
139
196
|
- lib/throttle_machines/throttled_error.rb
|
|
140
197
|
- lib/throttle_machines/version.rb
|
|
@@ -146,6 +203,8 @@ metadata:
|
|
|
146
203
|
source_code_uri: https://github.com/seuros/throttle_machines
|
|
147
204
|
changelog_uri: https://github.com/seuros/throttle_machines/blob/main/CHANGELOG.md
|
|
148
205
|
rubygems_mfa_required: 'true'
|
|
206
|
+
cargo_crate_name: throttle_machines_native
|
|
207
|
+
cargo_manifest_path: ext/throttle_machines_native/ffi/Cargo.toml
|
|
149
208
|
rdoc_options: []
|
|
150
209
|
require_paths:
|
|
151
210
|
- lib
|
|
@@ -153,14 +212,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
153
212
|
requirements:
|
|
154
213
|
- - ">="
|
|
155
214
|
- !ruby/object:Gem::Version
|
|
156
|
-
version:
|
|
215
|
+
version: 4.0.0
|
|
157
216
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
217
|
requirements:
|
|
159
218
|
- - ">="
|
|
160
219
|
- !ruby/object:Gem::Version
|
|
161
220
|
version: '0'
|
|
162
221
|
requirements: []
|
|
163
|
-
rubygems_version:
|
|
222
|
+
rubygems_version: 4.0.20
|
|
164
223
|
specification_version: 4
|
|
165
224
|
summary: Advanced Rate limiting for Ruby applications
|
|
166
225
|
test_files: []
|
data/Rakefile
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'bundler/gem_tasks'
|
|
4
|
-
require 'rake/testtask'
|
|
5
|
-
|
|
6
|
-
Rake::TestTask.new(:test) do |t|
|
|
7
|
-
t.libs << 'test'
|
|
8
|
-
t.test_files = FileList['test/**/*_test.rb'].exclude('test/dummy/**/*')
|
|
9
|
-
t.verbose = false
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
task default: :test
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
local data = redis.call('HGETALL', KEYS[1])
|
|
2
|
-
if #data == 0 then
|
|
3
|
-
return {}
|
|
4
|
-
end
|
|
5
|
-
|
|
6
|
-
local state = {}
|
|
7
|
-
for i = 1, #data, 2 do
|
|
8
|
-
state[data[i]] = data[i + 1]
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
-- Auto-transition from open to half-open if timeout passed
|
|
12
|
-
if state['state'] == 'open' and state['opens_at'] then
|
|
13
|
-
local now = tonumber(ARGV[1])
|
|
14
|
-
local opens_at = tonumber(state['opens_at'])
|
|
15
|
-
|
|
16
|
-
if now >= opens_at then
|
|
17
|
-
redis.call('HSET', KEYS[1], 'state', 'half_open', 'half_open_attempts', '0')
|
|
18
|
-
state['state'] = 'half_open'
|
|
19
|
-
state['half_open_attempts'] = '0'
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
return state
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
local state = redis.call('HGET', KEYS[1], 'state') or 'closed'
|
|
2
|
-
local now = ARGV[3]
|
|
3
|
-
local timeout = tonumber(ARGV[2])
|
|
4
|
-
|
|
5
|
-
if state == 'half_open' then
|
|
6
|
-
-- Failure in half-open state, just re-open the circuit
|
|
7
|
-
redis.call('HMSET', KEYS[1],
|
|
8
|
-
'state', 'open',
|
|
9
|
-
'opens_at', tonumber(now) + timeout,
|
|
10
|
-
'last_failure', now
|
|
11
|
-
)
|
|
12
|
-
else -- state is 'closed' or nil
|
|
13
|
-
local failures = redis.call('HINCRBY', KEYS[1], 'failures', 1)
|
|
14
|
-
redis.call('HSET', KEYS[1], 'last_failure', now)
|
|
15
|
-
|
|
16
|
-
if failures >= tonumber(ARGV[1]) then
|
|
17
|
-
redis.call('HMSET', KEYS[1],
|
|
18
|
-
'state', 'open',
|
|
19
|
-
'opens_at', tonumber(now) + timeout
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
redis.call('EXPIRE', KEYS[1], timeout * 2)
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
local state = redis.call('HGET', KEYS[1], 'state')
|
|
2
|
-
|
|
3
|
-
if state == 'half_open' then
|
|
4
|
-
-- Increment half-open attempts and potentially close the circuit
|
|
5
|
-
local attempts = redis.call('HINCRBY', KEYS[1], 'half_open_attempts', 1)
|
|
6
|
-
|
|
7
|
-
if attempts >= tonumber(ARGV[1]) then
|
|
8
|
-
redis.call('DEL', KEYS[1])
|
|
9
|
-
end
|
|
10
|
-
elseif state == 'closed' then
|
|
11
|
-
-- Reset failure count on success in closed state
|
|
12
|
-
local failures = redis.call('HGET', KEYS[1], 'failures')
|
|
13
|
-
if failures and tonumber(failures) > 0 then
|
|
14
|
-
redis.call('HSET', KEYS[1], 'failures', 0)
|
|
15
|
-
end
|
|
16
|
-
end
|