excess_flow 1.0.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 +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
|