excess_flow 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +35 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +4 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +54 -0
- data/LICENSE.txt +201 -0
- data/README.md +174 -0
- data/Rakefile +8 -0
- data/bin/console +12 -0
- data/bin/setup +8 -0
- data/excess_flow.gemspec +28 -0
- data/lib/excess_flow/configuration.rb +67 -0
- data/lib/excess_flow/configuration_error.rb +22 -0
- data/lib/excess_flow/constants.rb +27 -0
- data/lib/excess_flow/failed_execution.rb +19 -0
- data/lib/excess_flow/fixed_window_strategy.rb +55 -0
- data/lib/excess_flow/global_mutex.rb +54 -0
- data/lib/excess_flow/rate_limited_execution_result.rb +32 -0
- data/lib/excess_flow/redis_connection.rb +61 -0
- data/lib/excess_flow/sliding_window_strategy.rb +77 -0
- data/lib/excess_flow/strategy.rb +46 -0
- data/lib/excess_flow/throttle_configuration.rb +76 -0
- data/lib/excess_flow/throttled_executor.rb +47 -0
- data/lib/excess_flow.rb +120 -0
- metadata +115 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::Configuration
|
19
|
+
#
|
20
|
+
# Holds configuration for rate limiter with writeable attributes allowing
|
21
|
+
# dynamic change of configuration during runtime
|
22
|
+
class Configuration
|
23
|
+
attr_accessor(
|
24
|
+
:connection_pool,
|
25
|
+
:connection_timeout,
|
26
|
+
:redis_url,
|
27
|
+
:sentinels
|
28
|
+
)
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@connection_pool = extract_connection_pool
|
32
|
+
@connection_timeout = extract_connection_timeout
|
33
|
+
@redis_url = extract_redis_url
|
34
|
+
@sentinels = process_sentinels
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def extract_connection_pool
|
40
|
+
ENV.fetch(
|
41
|
+
'EXCESS_FLOW_CONNECTION_POOL',
|
42
|
+
ExcessFlow::DEFAULT_CONNECTION_POOL
|
43
|
+
).to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_connection_timeout
|
47
|
+
ENV.fetch(
|
48
|
+
'EXCESS_FLOW_CONNECTION_TIMEOUT',
|
49
|
+
ExcessFlow::DEFAULT_CONNECTION_TIMEOUT
|
50
|
+
).to_i
|
51
|
+
end
|
52
|
+
|
53
|
+
def extract_redis_url
|
54
|
+
ENV.fetch(
|
55
|
+
'EXCESS_FLOW_REDIS_URL',
|
56
|
+
ExcessFlow::DEFAULT_REDIS_URL
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_sentinels
|
61
|
+
ENV.fetch('EXCESS_FLOW_REDIS_SENTINELS', '').split(',').map do |sentinel|
|
62
|
+
host, port = sentinel.split(':')
|
63
|
+
{ host: host, port: port.to_i }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::ConfigurationError
|
19
|
+
#
|
20
|
+
# Error used in ThrottleConfiguration class
|
21
|
+
class ConfigurationError < StandardError; end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
CONFIGURATION_ERROR_MESSAGE = 'Invalid arguments provided. Please refer to README.md'
|
19
|
+
COUNTER_PREFIX = 'excess_flow::counter::'
|
20
|
+
DEFAULT_CONNECTION_POOL = 100
|
21
|
+
DEFAULT_CONNECTION_TIMEOUT = 3
|
22
|
+
DEFAULT_REDIS_URL = 'redis://localhost:6379/1'
|
23
|
+
LOCK_PREFIX = 'excess_flow::lock::'
|
24
|
+
MUTEX_LOCK_TIME = 1
|
25
|
+
MUTEX_SLEEP_TIME = 1 / 100_000
|
26
|
+
VERSION = '1.0.0'
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
class FailedExecution; end
|
19
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::FixedWindowStrategy
|
19
|
+
#
|
20
|
+
# Definition of fixed window rate limiting strategy and it's behaviour
|
21
|
+
# implementations. Fixed window allows only N requests for a given key to be
|
22
|
+
# done in a O time window where O start is defined at the time of first
|
23
|
+
# request. Window expiration starts with the first request for a given key.
|
24
|
+
# Once expired it will reset back to 0.
|
25
|
+
class FixedWindowStrategy < ExcessFlow::Strategy
|
26
|
+
def within_rate_limit?
|
27
|
+
ExcessFlow::GlobalMutex.locked(lock_key: configuration.lock_key) do
|
28
|
+
if current_requests < configuration.limit
|
29
|
+
bump_counter
|
30
|
+
start_expiration_window
|
31
|
+
|
32
|
+
true
|
33
|
+
else
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def bump_counter
|
42
|
+
ExcessFlow.redis { |r| r.incr(configuration.counter_key) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def current_requests
|
46
|
+
@current_requests ||= ExcessFlow.redis { |r| r.get(configuration.counter_key) }.to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
def start_expiration_window
|
50
|
+
return if current_requests.zero?
|
51
|
+
|
52
|
+
ExcessFlow.redis { |r| r.expire(configuration.counter_key, configuration.ttl) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::GlobalMutex
|
19
|
+
#
|
20
|
+
# Attempts to set up exclusive lock to execute a block of code. If another
|
21
|
+
# lock is in place then GlobalMutex will wait till lock is available. Always
|
22
|
+
# returns result of execution. GlobalMutex does not guarantee order of execution;
|
23
|
+
# it will only guarantee that only one thread for a given lock_key is running to
|
24
|
+
# avoid race conditions.
|
25
|
+
class GlobalMutex
|
26
|
+
attr_reader :lock_key
|
27
|
+
|
28
|
+
def self.locked(lock_key:, &block)
|
29
|
+
new(lock_key).locked(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(lock_key)
|
33
|
+
@lock_key = lock_key
|
34
|
+
end
|
35
|
+
|
36
|
+
def locked(&block)
|
37
|
+
sleep(ExcessFlow::MUTEX_SLEEP_TIME) until lock
|
38
|
+
result = block.call
|
39
|
+
unlock
|
40
|
+
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def lock
|
47
|
+
ExcessFlow.redis { |r| r.set(lock_key, 1, nx: true, ex: ExcessFlow::MUTEX_LOCK_TIME) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def unlock
|
51
|
+
ExcessFlow.redis { |r| r.expire(lock_key, 0) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::RateLimitedExecutionResult
|
19
|
+
#
|
20
|
+
# Wrapper containing result of rate limited execution
|
21
|
+
class RateLimitedExecutionResult
|
22
|
+
attr_reader :result
|
23
|
+
|
24
|
+
def initialize(result)
|
25
|
+
@result = result
|
26
|
+
end
|
27
|
+
|
28
|
+
def success?
|
29
|
+
!result.is_a?(FailedExecution)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::RedisConnection
|
19
|
+
#
|
20
|
+
# Wrapper around ConnectionPool and Redis to provide connectivity
|
21
|
+
# to Redis with desired configuration and sane connection pool
|
22
|
+
module RedisConnection
|
23
|
+
module_function
|
24
|
+
|
25
|
+
def connection_pool
|
26
|
+
@connection_pool = ConnectionPool.new(connection_pool_options) do
|
27
|
+
Redis.new(connection_options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def connection_options
|
32
|
+
{
|
33
|
+
url: redis_url,
|
34
|
+
sentinels: sentinels
|
35
|
+
}.delete_if { |_k, v| v.nil? || v.empty? }
|
36
|
+
end
|
37
|
+
|
38
|
+
def connection_pool_options
|
39
|
+
{
|
40
|
+
size: pool_size,
|
41
|
+
timeout: connection_timeout
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def connection_timeout
|
46
|
+
ExcessFlow.configuration.connection_timeout
|
47
|
+
end
|
48
|
+
|
49
|
+
def pool_size
|
50
|
+
ExcessFlow.configuration.connection_pool
|
51
|
+
end
|
52
|
+
|
53
|
+
def redis_url
|
54
|
+
ExcessFlow.configuration.redis_url
|
55
|
+
end
|
56
|
+
|
57
|
+
def sentinels
|
58
|
+
ExcessFlow.configuration.sentinels
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::SlidingWindowStrategy
|
19
|
+
#
|
20
|
+
# Definition of sliging window rate limiting strategy and it's behaviour
|
21
|
+
# implementations. Sliding window allows only N requests for a given key to
|
22
|
+
# be done in a trailing O time window where O start is defined as `now` -
|
23
|
+
# `window_size`. Window expiration starts with the first request for a given
|
24
|
+
# key. Once expired it will reset back to 0.
|
25
|
+
class SlidingWindowStrategy < ExcessFlow::Strategy
|
26
|
+
def within_rate_limit?
|
27
|
+
ExcessFlow::GlobalMutex.locked(lock_key: configuration.lock_key) do
|
28
|
+
cleanup_stale_counters
|
29
|
+
|
30
|
+
if current_requests < configuration.limit
|
31
|
+
bump_counter
|
32
|
+
start_expiration_window
|
33
|
+
|
34
|
+
true
|
35
|
+
else
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def bump_counter
|
44
|
+
ExcessFlow.redis do |r|
|
45
|
+
r.zadd(
|
46
|
+
configuration.counter_key,
|
47
|
+
current_timestamp,
|
48
|
+
current_timestamp
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def cleanup_stale_counters
|
54
|
+
ExcessFlow.redis do |r|
|
55
|
+
r.zremrangebyscore(configuration.counter_key, 0, "(#{window_start}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def current_requests
|
60
|
+
@current_requests ||= ExcessFlow.redis { |r| r.zcount(configuration.counter_key, '-inf', '+inf') }
|
61
|
+
end
|
62
|
+
|
63
|
+
def current_timestamp
|
64
|
+
@current_timestamp ||= (Time.now.to_f * 100_000).to_i
|
65
|
+
end
|
66
|
+
|
67
|
+
def start_expiration_window
|
68
|
+
return if current_requests.zero?
|
69
|
+
|
70
|
+
ExcessFlow.redis { |r| r.expire(configuration.counter_key, configuration.ttl) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def window_start
|
74
|
+
@window_start ||= current_timestamp - (configuration.ttl * 100_000)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::Strategy
|
19
|
+
#
|
20
|
+
# Base class for implementing different strategies for rate limiting.
|
21
|
+
class Strategy
|
22
|
+
attr_reader :configuration
|
23
|
+
|
24
|
+
def self.execute(configuration:, &block)
|
25
|
+
new(configuration).execute(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(configuration)
|
29
|
+
@configuration = configuration
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute
|
33
|
+
result = if within_rate_limit?
|
34
|
+
yield
|
35
|
+
else
|
36
|
+
FailedExecution.new
|
37
|
+
end
|
38
|
+
|
39
|
+
RateLimitedExecutionResult.new(result)
|
40
|
+
end
|
41
|
+
|
42
|
+
def within_rate_limit?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::ThrottleConfiguration
|
19
|
+
#
|
20
|
+
# Wrapper class for throttle execution result that does provide some basic
|
21
|
+
# transformation of provided values.
|
22
|
+
class ThrottleConfiguration
|
23
|
+
MANDATORY_KEYS = %i[
|
24
|
+
key
|
25
|
+
limit
|
26
|
+
ttl
|
27
|
+
].freeze
|
28
|
+
|
29
|
+
OPTIONAL_KEYS = %i[
|
30
|
+
strategy
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
attr_reader :key, :limit, :ttl
|
34
|
+
|
35
|
+
def initialize(args)
|
36
|
+
@raw_args = args
|
37
|
+
validate_args
|
38
|
+
|
39
|
+
args.each do |key, value|
|
40
|
+
instance_variable_set("@#{key}", value) unless value.nil?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def counter_key
|
45
|
+
ExcessFlow::COUNTER_PREFIX + key
|
46
|
+
end
|
47
|
+
|
48
|
+
def lock_key
|
49
|
+
ExcessFlow::LOCK_PREFIX + key
|
50
|
+
end
|
51
|
+
|
52
|
+
def strategy
|
53
|
+
case @strategy
|
54
|
+
when :fixed_window then ExcessFlow::FixedWindowStrategy
|
55
|
+
when :sliding_window then ExcessFlow::SlidingWindowStrategy
|
56
|
+
else ExcessFlow::FixedWindowStrategy
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def allowed_keys_passed_in?
|
63
|
+
(@raw_args.keys - (MANDATORY_KEYS + OPTIONAL_KEYS)).empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
def mandatory_keys_are_present?
|
67
|
+
(MANDATORY_KEYS - @raw_args.keys).empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_args
|
71
|
+
return if allowed_keys_passed_in? && mandatory_keys_are_present?
|
72
|
+
|
73
|
+
raise ExcessFlow::ConfigurationError, CONFIGURATION_ERROR_MESSAGE
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 ConvertKit, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module ExcessFlow
|
18
|
+
# == ExcessFlow::ThrottledExecutor
|
19
|
+
#
|
20
|
+
# Wrapper service class that will take care of initialization of configuration
|
21
|
+
# object and will execute on correct throttling strategy.
|
22
|
+
class ThrottledExecutor
|
23
|
+
attr_reader :args
|
24
|
+
|
25
|
+
def self.select_strategy_and_execute(args, &block)
|
26
|
+
new(args).select_strategy_and_execute(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(args)
|
30
|
+
@args = args
|
31
|
+
end
|
32
|
+
|
33
|
+
def select_strategy_and_execute(&block)
|
34
|
+
strategy.execute(configuration: configuration, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def configuration
|
40
|
+
@configuration ||= ExcessFlow::ThrottleConfiguration.new(args)
|
41
|
+
end
|
42
|
+
|
43
|
+
def strategy
|
44
|
+
configuration.strategy
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|