rack-berater 0.1.0 → 0.3.2
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/lib/rack/berater/prioritizer.rb +75 -0
- data/lib/rack/berater/rails_prioritizer.rb +17 -0
- data/lib/rack/berater/railtie.rb +5 -1
- data/lib/rack/berater/version.rb +1 -1
- data/lib/rack/berater.rb +17 -10
- data/lib/rack-berater.rb +1 -1
- data/spec/limiter_spec.rb +53 -31
- data/spec/prioritizer_spec.rb +218 -0
- data/spec/rails_prioritizer_spec.rb +95 -0
- data/spec/railtie_spec.rb +16 -0
- data/spec/rescuer_spec.rb +54 -44
- metadata +39 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9e1b9ca0c696948b6d7ec973e1855c71a73fe9ca59d0833107e57295d2d9f21
|
|
4
|
+
data.tar.gz: fcbbeef6465c6f7cefa0c110401e4c80d6462ea3adc69a7a1e3975e19e9f8355
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d00a14d8136ac96cac236c4bb2ff55c98871dc32cb85b30d8800b4784526cb27c53e54979703f554cb1698ea99093ea77584222dab213f4cbbbe6aeba1b2e71
|
|
7
|
+
data.tar.gz: 03b030982e003903461f929853138f7c4dcbffbfbcccb2aba1335ffac6e2aa9aeb0b265c4c13d3ebc6f1e04ef4acc1c942606c9d57f44cf2003c3a46e42b8c64
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'rack'
|
|
2
|
+
|
|
3
|
+
module Rack
|
|
4
|
+
class Berater
|
|
5
|
+
class Prioritizer
|
|
6
|
+
ENV_KEY = 'berater_priority'
|
|
7
|
+
HEADER = 'X-Berater-Priority'
|
|
8
|
+
|
|
9
|
+
def initialize(app, options = {})
|
|
10
|
+
@app = app
|
|
11
|
+
@header = options[:header] || HEADER
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
priority = env[@header] || env["HTTP_#{@header.upcase.tr('-', '_')}"]
|
|
16
|
+
|
|
17
|
+
if priority
|
|
18
|
+
set_priority(priority)
|
|
19
|
+
return @app.call(env)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
cache_key = cache_key_for(env)
|
|
23
|
+
cached_priority = cache_get(cache_key)
|
|
24
|
+
|
|
25
|
+
if cached_priority
|
|
26
|
+
set_priority(cached_priority)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@app.call(env).tap do |status, headers, body|
|
|
30
|
+
app_priority = headers.delete(@header) if headers
|
|
31
|
+
|
|
32
|
+
if app_priority && app_priority != cached_priority
|
|
33
|
+
# update cache for next time
|
|
34
|
+
cache_set(cache_key, app_priority)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
ensure
|
|
38
|
+
Thread.current[ENV_KEY] = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.current_priority
|
|
42
|
+
Thread.current[ENV_KEY]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
def set_priority(priority)
|
|
48
|
+
Thread.current[ENV_KEY] = priority
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def cache_key_for(env)
|
|
52
|
+
[
|
|
53
|
+
env[Rack::REQUEST_METHOD].downcase,
|
|
54
|
+
|
|
55
|
+
# normalize RESTful paths
|
|
56
|
+
env['PATH_INFO'].gsub(%r{/[0-9]+(/|$)}, '/x\1'),
|
|
57
|
+
].join(':')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@@cache = {}
|
|
61
|
+
def cache_get(key)
|
|
62
|
+
synchronize { @@cache[key] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cache_set(key, priority)
|
|
66
|
+
synchronize { @@cache[key] = priority }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@@lock = Thread::Mutex.new
|
|
70
|
+
def synchronize(&block)
|
|
71
|
+
@@lock.synchronize(&block)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'action_controller/metal'
|
|
2
|
+
require 'action_dispatch'
|
|
3
|
+
|
|
4
|
+
module Rack
|
|
5
|
+
class Berater
|
|
6
|
+
class RailsPrioritizer < Prioritizer
|
|
7
|
+
def cache_key_for(env)
|
|
8
|
+
Rails.application.routes.recognize_path(
|
|
9
|
+
env[Rack::PATH_INFO],
|
|
10
|
+
method: env[Rack::REQUEST_METHOD],
|
|
11
|
+
).values_at(:controller, :action).compact.join('#')
|
|
12
|
+
rescue ActionController::RoutingError
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/rack/berater/railtie.rb
CHANGED
|
@@ -3,7 +3,11 @@ require 'rails/railtie'
|
|
|
3
3
|
module Rack
|
|
4
4
|
class Berater
|
|
5
5
|
class Railtie < Rails::Railtie
|
|
6
|
-
initializer
|
|
6
|
+
initializer 'rack.berater' do |app|
|
|
7
|
+
if ::Berater.middleware.include?(::Berater::Middleware::LoadShedder)
|
|
8
|
+
app.middleware.use Rack::Berater::RailsPrioritizer
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
app.middleware.use Rack::Berater
|
|
8
12
|
end
|
|
9
13
|
end
|
data/lib/rack/berater/version.rb
CHANGED
data/lib/rack/berater.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
3
|
-
require
|
|
4
|
-
require
|
|
1
|
+
require 'berater'
|
|
2
|
+
require 'rack'
|
|
3
|
+
require 'rack/berater/version'
|
|
4
|
+
require 'set'
|
|
5
5
|
|
|
6
6
|
module Rack
|
|
7
7
|
class Berater
|
|
8
|
-
autoload :
|
|
8
|
+
autoload :Prioritizer, 'rack/berater/prioritizer'
|
|
9
|
+
autoload :RailsPrioritizer, 'rack/berater/rails_prioritizer'
|
|
10
|
+
autoload :Railtie, 'rack/berater/railtie'
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
ERRORS = Set[ ::Berater::Overloaded ]
|
|
11
13
|
|
|
12
14
|
def initialize(app, options = {})
|
|
13
15
|
@app = app
|
|
@@ -27,23 +29,23 @@ module Rack
|
|
|
27
29
|
when String
|
|
28
30
|
options[:body]
|
|
29
31
|
else
|
|
30
|
-
raise ArgumentError,
|
|
32
|
+
raise ArgumentError, 'invalid :body option: #{options[:body]}'
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# configure headers
|
|
34
36
|
if @options[:body]
|
|
35
|
-
@options[:headers][Rack::CONTENT_TYPE] =
|
|
37
|
+
@options[:headers][Rack::CONTENT_TYPE] = 'text/plain'
|
|
36
38
|
end
|
|
37
39
|
@options[:headers].update(options.fetch(:headers, {}))
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
def call(env)
|
|
41
43
|
if enabled?(env)
|
|
42
|
-
|
|
44
|
+
limit(env) { @app.call(env) }
|
|
43
45
|
else
|
|
44
46
|
@app.call(env)
|
|
45
47
|
end
|
|
46
|
-
rescue *
|
|
48
|
+
rescue *ERRORS => e
|
|
47
49
|
[
|
|
48
50
|
@options[:status_code],
|
|
49
51
|
@options[:headers],
|
|
@@ -57,5 +59,10 @@ module Rack
|
|
|
57
59
|
return false unless @limiter
|
|
58
60
|
@enabled.nil? ? true : @enabled.call(env)
|
|
59
61
|
end
|
|
62
|
+
|
|
63
|
+
def limit(env, &block)
|
|
64
|
+
limiter = @limiter.respond_to?(:call) ? @limiter.call(env) : @limiter
|
|
65
|
+
limiter.limit(&block)
|
|
66
|
+
end
|
|
60
67
|
end
|
|
61
68
|
end
|
data/lib/rack-berater.rb
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
require
|
|
1
|
+
require 'rack/berater'
|
data/spec/limiter_spec.rb
CHANGED
|
@@ -9,86 +9,108 @@ describe Rack::Berater do
|
|
|
9
9
|
Rack::Builder.new do
|
|
10
10
|
use Rack::Lint
|
|
11
11
|
run (lambda do |env|
|
|
12
|
-
[200, {
|
|
12
|
+
[200, {'Content-Type' => 'text/plain'}, ['OK']]
|
|
13
13
|
end)
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
|
-
let(:response) { get
|
|
16
|
+
let(:response) { get '/' }
|
|
17
17
|
|
|
18
|
-
shared_examples
|
|
19
|
-
it
|
|
18
|
+
shared_examples 'works nominally' do
|
|
19
|
+
it 'has the correct status code' do
|
|
20
20
|
expect(response.status).to eq 200
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
it
|
|
23
|
+
it 'has the correct headers' do
|
|
24
24
|
expect(response.headers).to eq({
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
'Content-Type' => 'text/plain',
|
|
26
|
+
'Content-Length' => '2',
|
|
27
27
|
})
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
it
|
|
31
|
-
expect(response.body).to eq
|
|
30
|
+
it 'has the correct body' do
|
|
31
|
+
expect(response.body).to eq 'OK'
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
context
|
|
35
|
+
context 'without a limiter' do
|
|
36
36
|
before { Berater.test_mode = :fail }
|
|
37
37
|
|
|
38
|
-
include_examples
|
|
38
|
+
include_examples 'works nominally'
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
describe
|
|
42
|
-
|
|
41
|
+
describe 'limiter option' do
|
|
42
|
+
context 'when limiter is a limiter' do
|
|
43
|
+
let(:limiter) { ::Berater::Unlimiter.new }
|
|
44
|
+
|
|
45
|
+
include_examples 'works nominally'
|
|
46
|
+
|
|
47
|
+
it 'calls the limiter' do
|
|
48
|
+
expect(limiter).to receive(:limit).and_call_original
|
|
49
|
+
response
|
|
50
|
+
end
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
context 'when operating beyond limits' do
|
|
53
|
+
before { Berater.test_mode = :fail }
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
it 'returns an error' do
|
|
56
|
+
expect(response.status).to eq 429
|
|
57
|
+
end
|
|
58
|
+
end
|
|
49
59
|
end
|
|
50
60
|
|
|
51
|
-
context
|
|
52
|
-
|
|
61
|
+
context 'when limiter is a proc' do
|
|
62
|
+
let(:limiter_instance) { ::Berater::Unlimiter.new }
|
|
63
|
+
let(:limiter) { Proc.new { limiter_instance } }
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
include_examples 'works nominally'
|
|
66
|
+
|
|
67
|
+
it 'calls the proc with env' do
|
|
68
|
+
expect(limiter).to receive(:call).with(Hash).and_call_original
|
|
69
|
+
response
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'when operating beyond limits' do
|
|
73
|
+
before { Berater.test_mode = :fail }
|
|
74
|
+
|
|
75
|
+
it 'returns an error' do
|
|
76
|
+
expect(response.status).to eq 429
|
|
77
|
+
end
|
|
56
78
|
end
|
|
57
79
|
end
|
|
58
80
|
end
|
|
59
81
|
|
|
60
|
-
describe
|
|
82
|
+
describe 'enabled? option' do
|
|
61
83
|
after { expect(response.status).to eq 200 }
|
|
62
84
|
|
|
63
85
|
let(:enabled?) { double }
|
|
64
86
|
|
|
65
|
-
context
|
|
87
|
+
context 'when there is a limiter' do
|
|
66
88
|
let(:limiter) { ::Berater::Unlimiter.new }
|
|
67
89
|
|
|
68
|
-
it
|
|
90
|
+
it 'should be called with the env hash' do
|
|
69
91
|
expect(enabled?).to receive(:call) do |env|
|
|
70
92
|
expect(env).to be_a Hash
|
|
71
|
-
expect(Rack::Request.new(env).path).to eq
|
|
93
|
+
expect(Rack::Request.new(env).path).to eq '/'
|
|
72
94
|
end
|
|
73
95
|
end
|
|
74
96
|
|
|
75
|
-
context
|
|
76
|
-
it
|
|
97
|
+
context 'when enabled' do
|
|
98
|
+
it 'should call the limiter' do
|
|
77
99
|
expect(enabled?).to receive(:call).and_return(true)
|
|
78
100
|
expect(limiter).to receive(:limit).and_call_original
|
|
79
101
|
end
|
|
80
102
|
end
|
|
81
103
|
|
|
82
|
-
context
|
|
83
|
-
it
|
|
104
|
+
context 'when disabled' do
|
|
105
|
+
it 'should not call the limiter' do
|
|
84
106
|
expect(enabled?).to receive(:call).and_return(false)
|
|
85
107
|
expect(limiter).not_to receive(:limit)
|
|
86
108
|
end
|
|
87
109
|
end
|
|
88
110
|
end
|
|
89
111
|
|
|
90
|
-
context
|
|
91
|
-
it
|
|
112
|
+
context 'when there is no limiter' do
|
|
113
|
+
it 'should not call enabled?' do
|
|
92
114
|
expect(enabled?).not_to receive(:call)
|
|
93
115
|
end
|
|
94
116
|
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
describe Rack::Berater::Prioritizer do
|
|
2
|
+
after do
|
|
3
|
+
cache.clear
|
|
4
|
+
Thread.current[described_class::ENV_KEY] = nil
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
let(:cache) { described_class.class_variable_get(:@@cache) }
|
|
8
|
+
|
|
9
|
+
describe '#call' do
|
|
10
|
+
subject { described_class.new(app) }
|
|
11
|
+
|
|
12
|
+
let(:app) { ->(*) { [200, {'Content-Type' => 'text/plain'}, ['OK']] } }
|
|
13
|
+
let(:cache_key) { subject.send(:cache_key_for, env) }
|
|
14
|
+
let(:env) { Rack::MockRequest.env_for('/') }
|
|
15
|
+
|
|
16
|
+
specify 'sanity check' do
|
|
17
|
+
expect(app).to receive(:call).and_call_original
|
|
18
|
+
|
|
19
|
+
Rack::Lint.new(subject).call(env)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'checks the cache' do
|
|
23
|
+
is_expected.to receive(:cache_get)
|
|
24
|
+
|
|
25
|
+
subject.call(env)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
context 'with a cached priority' do
|
|
29
|
+
before do
|
|
30
|
+
allow(subject).to receive(:cache_get).with(cache_key).and_return(priority)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
let(:priority) { '3' }
|
|
34
|
+
|
|
35
|
+
after { subject.call(env) }
|
|
36
|
+
|
|
37
|
+
it 'sets the priority accordingly' do
|
|
38
|
+
is_expected.to receive(:set_priority).with(priority)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'updates the global priority during the request' do
|
|
42
|
+
expect(app).to receive(:call) do
|
|
43
|
+
expect(described_class.current_priority).to eq priority
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'resets the priority after the request completes' do
|
|
48
|
+
subject.call(env)
|
|
49
|
+
expect(described_class.current_priority).to be nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context 'with an incoming priority header' do
|
|
54
|
+
let(:env) do
|
|
55
|
+
Rack::MockRequest.env_for(
|
|
56
|
+
'/',
|
|
57
|
+
described_class::HEADER => priority,
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
let(:priority) { '2' }
|
|
61
|
+
|
|
62
|
+
after { subject.call(env) }
|
|
63
|
+
|
|
64
|
+
it 'uses the header' do
|
|
65
|
+
is_expected.to receive(:set_priority).with(priority)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'ignores any cached value' do
|
|
69
|
+
allow(subject).to receive(:cache_get).with(cache_key).and_return('123')
|
|
70
|
+
is_expected.to receive(:set_priority).with(priority)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'resets the priority after the request completes' do
|
|
74
|
+
subject.call(env)
|
|
75
|
+
expect(described_class.current_priority).to be nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context 'when the app returns a priority header' do
|
|
80
|
+
let(:app) do
|
|
81
|
+
->(*) { [200, { described_class::HEADER => priority }, ['OK']] }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
let(:priority) { '5' }
|
|
85
|
+
|
|
86
|
+
after { subject.call(env) }
|
|
87
|
+
|
|
88
|
+
it 'caches the priority' do
|
|
89
|
+
is_expected.to receive(:cache_set).with(cache_key, priority)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'removes the header' do
|
|
93
|
+
_, headers, _ = subject.call(env)
|
|
94
|
+
expect(headers).not_to include described_class::HEADER
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'updates the cache when a different priority is returned' do
|
|
98
|
+
expect(subject).to receive(:cache_get).and_return('123')
|
|
99
|
+
is_expected.to receive(:cache_set).with(cache_key, priority)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'does not update the cache when the same priority is returned' do
|
|
103
|
+
expect(subject).to receive(:cache_get).and_return(priority)
|
|
104
|
+
is_expected.not_to receive(:cache_set)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'does not update the cache when no priority is returned' do
|
|
109
|
+
is_expected.not_to receive(:cache_set)
|
|
110
|
+
subject.call(env)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '#cache_key_for' do
|
|
115
|
+
subject{ described_class.new(nil).send(:cache_key_for, env) }
|
|
116
|
+
|
|
117
|
+
context 'with a basic env' do
|
|
118
|
+
let(:env) { Rack::MockRequest.env_for('/') }
|
|
119
|
+
|
|
120
|
+
it 'combines the verb and path' do
|
|
121
|
+
is_expected.to match %r{get:/$}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context 'with a different verb' do
|
|
126
|
+
let(:env) { Rack::MockRequest.env_for('/', method: 'PUT') }
|
|
127
|
+
|
|
128
|
+
it 'combines the verb and path' do
|
|
129
|
+
is_expected.to match %r{put:/$}
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
context 'with a RESTful path' do
|
|
134
|
+
let(:env) { Rack::MockRequest.env_for('/user/123') }
|
|
135
|
+
|
|
136
|
+
it 'normalizes the id' do
|
|
137
|
+
is_expected.to match %r{get:/user/x$}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
context 'with a RESTful path and trailing slash' do
|
|
142
|
+
let(:env) { Rack::MockRequest.env_for('/user/123/') }
|
|
143
|
+
|
|
144
|
+
it 'normalizes the id and keeps the trailing slash' do
|
|
145
|
+
is_expected.to match %r{get:/user/x/$}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
context 'with a very RESTful path' do
|
|
150
|
+
let(:env) { Rack::MockRequest.env_for('/user/123/friend/456') }
|
|
151
|
+
|
|
152
|
+
it 'normalizes both ids' do
|
|
153
|
+
is_expected.to match %r{get:/user/x/friend/x$}
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
context 'as Rack middleware' do
|
|
159
|
+
def call(path = '/')
|
|
160
|
+
get(path).body
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
let(:app) do
|
|
164
|
+
headers = {
|
|
165
|
+
'Content-Type' => 'text/plain',
|
|
166
|
+
described_class::HEADER => app_priority,
|
|
167
|
+
}.compact
|
|
168
|
+
|
|
169
|
+
Rack::Builder.new do
|
|
170
|
+
use Rack::Lint
|
|
171
|
+
use Rack::Berater::Prioritizer
|
|
172
|
+
|
|
173
|
+
run (lambda do |env|
|
|
174
|
+
[200, headers, [ Rack::Berater::Prioritizer.current_priority.to_s ]]
|
|
175
|
+
end)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
let(:app_priority) { nil }
|
|
180
|
+
|
|
181
|
+
it 'starts empty' do
|
|
182
|
+
expect(call).to be_empty
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'parses incoming priority header' do
|
|
186
|
+
header described_class::HEADER, '7'
|
|
187
|
+
|
|
188
|
+
expect(call).to eq '7'
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context 'when app returns a priority header' do
|
|
192
|
+
let(:app_priority) { '8' }
|
|
193
|
+
|
|
194
|
+
it 'parses the priority returned from the app' do
|
|
195
|
+
expect(call).to be_empty
|
|
196
|
+
expect(cache.values).to include app_priority
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'uses the cached priority for subsequent calls' do
|
|
200
|
+
expect(call).to be_empty
|
|
201
|
+
expect(call).to eq app_priority
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# context 'when two different endpoints are called' do
|
|
206
|
+
# fit 'parses and caches each priority' do
|
|
207
|
+
# @app_priority = '6'
|
|
208
|
+
# expect(call('/six')).to be_empty
|
|
209
|
+
|
|
210
|
+
# expect(call('/six')).to eq '6'
|
|
211
|
+
|
|
212
|
+
# @app_priority = '9'
|
|
213
|
+
# expect(call('/nine')).to be_empty
|
|
214
|
+
# expect(call('/nine')).to '9'
|
|
215
|
+
# end
|
|
216
|
+
# end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require 'rspec/rails'
|
|
2
|
+
|
|
3
|
+
describe Rack::Berater::RailsPrioritizer do
|
|
4
|
+
before do
|
|
5
|
+
class EchoController < ActionController::Base
|
|
6
|
+
def index
|
|
7
|
+
render plain: Rack::Berater::Prioritizer.current_priority
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def six
|
|
11
|
+
response.set_header(Rack::Berater::Prioritizer::HEADER, '6')
|
|
12
|
+
index
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def nine
|
|
16
|
+
response.set_header(Rack::Berater::Prioritizer::HEADER, '9')
|
|
17
|
+
index
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Rails.application = Class.new(Rails::Application) do
|
|
22
|
+
config.eager_load = false
|
|
23
|
+
config.hosts.clear # disable hostname filtering
|
|
24
|
+
# config.logger = ActiveSupport::Logger.new($stdout)
|
|
25
|
+
end
|
|
26
|
+
Rails.application.middleware.use described_class
|
|
27
|
+
Rails.initialize!
|
|
28
|
+
|
|
29
|
+
Rails.application.routes.draw do
|
|
30
|
+
get '/' => 'echo#index'
|
|
31
|
+
get '/six' => 'echo#six'
|
|
32
|
+
post '/nine' => 'echo#nine'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
let(:app) { Rails.application }
|
|
37
|
+
let(:middleware) { described_class.new(app) }
|
|
38
|
+
|
|
39
|
+
after do
|
|
40
|
+
cache.clear
|
|
41
|
+
Thread.current[described_class::ENV_KEY] = nil
|
|
42
|
+
Rails.application = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
let(:cache) { described_class.class_variable_get(:@@cache) }
|
|
46
|
+
|
|
47
|
+
describe '#cache_key_for' do
|
|
48
|
+
subject { described_class.new(app).method(:cache_key_for) }
|
|
49
|
+
|
|
50
|
+
it 'uses the controller and action name' do
|
|
51
|
+
expect(
|
|
52
|
+
subject.call(Rack::MockRequest.env_for('/'))
|
|
53
|
+
).to match /echo#index/
|
|
54
|
+
|
|
55
|
+
expect(
|
|
56
|
+
subject.call(Rack::MockRequest.env_for('/six'))
|
|
57
|
+
).to match /echo#six/
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
subject.call(Rack::MockRequest.env_for('/nine', method: 'POST'))
|
|
61
|
+
).to match /echo#nine/
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'falls back to Rack style names' do
|
|
65
|
+
expect(
|
|
66
|
+
subject.call(Rack::MockRequest.env_for('/nine'))
|
|
67
|
+
).to match %r{get:/nine}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'when a priority header is sent' do
|
|
72
|
+
before { header described_class::HEADER, priority }
|
|
73
|
+
|
|
74
|
+
let(:priority) { '6' }
|
|
75
|
+
|
|
76
|
+
it 'sets the priority' do
|
|
77
|
+
expect(get('/six').body).to eq priority
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'when the app returns a priority' do
|
|
82
|
+
it 'does not know the first time the controller is called' do
|
|
83
|
+
expect(get('/six').body).to be_empty
|
|
84
|
+
expect(post('/nine').body).to be_empty
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'caches the repsonses for the second time' do
|
|
88
|
+
expect(get('/six').body).to be_empty
|
|
89
|
+
expect(post('/nine').body).to be_empty
|
|
90
|
+
|
|
91
|
+
expect(get('/six').body).to eq '6'
|
|
92
|
+
expect(post('/nine').body).to eq '9'
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'rails'
|
|
2
|
+
require 'rack/berater/railtie'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Rack::Berater::Railtie do
|
|
5
|
+
subject { Rails.initialize! }
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
Rails.application = Class.new(Rails::Application) do
|
|
9
|
+
config.eager_load = false
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'adds middleware automatically' do
|
|
14
|
+
expect(subject.middleware).to include(Rack::Berater)
|
|
15
|
+
end
|
|
16
|
+
end
|
data/spec/rescuer_spec.rb
CHANGED
|
@@ -4,34 +4,34 @@ describe Rack::Berater do
|
|
|
4
4
|
use Rack::Lint
|
|
5
5
|
run (lambda do |env|
|
|
6
6
|
Berater::Unlimiter() do
|
|
7
|
-
[200, {
|
|
7
|
+
[200, {'Content-Type' => 'text/plain'}, ['OK']]
|
|
8
8
|
end
|
|
9
9
|
end)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
|
-
let(:response) { get
|
|
12
|
+
let(:response) { get '/' }
|
|
13
13
|
|
|
14
|
-
shared_examples
|
|
15
|
-
it
|
|
14
|
+
shared_examples 'works nominally' do
|
|
15
|
+
it 'has the correct status code' do
|
|
16
16
|
expect(response.status).to eq 200
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
it
|
|
19
|
+
it 'has the correct headers' do
|
|
20
20
|
expect(response.headers).to eq({
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
'Content-Type' => 'text/plain',
|
|
22
|
+
'Content-Length' => '2',
|
|
23
23
|
})
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
it
|
|
27
|
-
expect(response.body).to eq
|
|
26
|
+
it 'has the correct body' do
|
|
27
|
+
expect(response.body).to eq 'OK'
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
context
|
|
32
|
-
include_examples
|
|
31
|
+
context 'without middleware' do
|
|
32
|
+
include_examples 'works nominally'
|
|
33
33
|
|
|
34
|
-
it
|
|
34
|
+
it 'does not catch limit errors' do
|
|
35
35
|
Berater.test_mode = :fail
|
|
36
36
|
expect {
|
|
37
37
|
response
|
|
@@ -39,105 +39,115 @@ describe Rack::Berater do
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
context
|
|
42
|
+
context 'with middleware using default settings' do
|
|
43
43
|
before { app.use described_class }
|
|
44
44
|
|
|
45
|
-
include_examples
|
|
45
|
+
include_examples 'works nominally'
|
|
46
46
|
|
|
47
|
-
it
|
|
47
|
+
it 'catches and transforms limit errors' do
|
|
48
48
|
Berater.test_mode = :fail
|
|
49
49
|
expect(response.status).to eq 429
|
|
50
|
-
expect(response.body).to eq
|
|
50
|
+
expect(response.body).to eq 'Too Many Requests'
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
context
|
|
54
|
+
context 'with middleware using custom settings' do
|
|
55
55
|
before do
|
|
56
56
|
app.use described_class, options
|
|
57
57
|
Berater.test_mode = :fail
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
context
|
|
61
|
-
context
|
|
60
|
+
context 'with a custom body' do
|
|
61
|
+
context 'with body nil' do
|
|
62
62
|
let(:options) { { body: nil } }
|
|
63
63
|
|
|
64
|
-
it
|
|
65
|
-
expect(response.body).to eq
|
|
64
|
+
it 'falls back to the default' do
|
|
65
|
+
expect(response.body).to eq 'Too Many Requests'
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
context
|
|
69
|
+
context 'with body disabled' do
|
|
70
70
|
let(:options) { { body: false } }
|
|
71
71
|
|
|
72
|
-
it
|
|
72
|
+
it 'should not send a body' do
|
|
73
73
|
expect(response.body).to be_empty
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
it
|
|
76
|
+
it 'should not send the Content-Type header' do
|
|
77
77
|
expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
context
|
|
82
|
-
let(:body) {
|
|
81
|
+
context 'with a string' do
|
|
82
|
+
let(:body) { 'none shall pass!' }
|
|
83
83
|
let(:options) { { body: body } }
|
|
84
84
|
|
|
85
|
-
it
|
|
85
|
+
it 'should send the custom string' do
|
|
86
86
|
expect(response.body).to eq body
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
|
+
|
|
90
|
+
context 'with an erroneous value' do
|
|
91
|
+
let(:options) { { body: 123 } }
|
|
92
|
+
|
|
93
|
+
it 'should raise an error' do
|
|
94
|
+
expect {
|
|
95
|
+
response
|
|
96
|
+
}.to raise_error(ArgumentError)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
89
99
|
end
|
|
90
100
|
|
|
91
|
-
context
|
|
92
|
-
context
|
|
93
|
-
let(:options) { { headers: { Rack::CACHE_CONTROL =>
|
|
101
|
+
context 'with custom headers' do
|
|
102
|
+
context 'with an extra header' do
|
|
103
|
+
let(:options) { { headers: { Rack::CACHE_CONTROL => 'no-cache' } } }
|
|
94
104
|
|
|
95
|
-
it
|
|
105
|
+
it 'should contain the default headers' do
|
|
96
106
|
expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
|
|
97
107
|
end
|
|
98
108
|
|
|
99
|
-
it
|
|
109
|
+
it 'should also contain the custom header' do
|
|
100
110
|
expect(response.headers).to include(options[:headers])
|
|
101
111
|
end
|
|
102
112
|
end
|
|
103
113
|
|
|
104
|
-
context
|
|
105
|
-
let(:options) { { headers: { Rack::CONTENT_TYPE =>
|
|
114
|
+
context 'with a new content type' do
|
|
115
|
+
let(:options) { { headers: { Rack::CONTENT_TYPE => 'application/json' } } }
|
|
106
116
|
|
|
107
|
-
it
|
|
117
|
+
it 'should override the Content-Type header' do
|
|
108
118
|
expect(response.headers).to include(options[:headers])
|
|
109
119
|
end
|
|
110
120
|
end
|
|
111
121
|
end
|
|
112
122
|
|
|
113
|
-
context
|
|
123
|
+
context 'with custom status code' do
|
|
114
124
|
let(:options) { { status_code: 503 } }
|
|
115
125
|
|
|
116
|
-
it
|
|
126
|
+
it 'catches and transforms limit errors' do
|
|
117
127
|
expect(response.status).to eq 503
|
|
118
|
-
expect(response.body).to eq
|
|
128
|
+
expect(response.body).to eq 'Service Unavailable'
|
|
119
129
|
end
|
|
120
130
|
end
|
|
121
131
|
end
|
|
122
132
|
|
|
123
|
-
context
|
|
133
|
+
context 'with custom error type' do
|
|
124
134
|
before do
|
|
125
135
|
app.use described_class
|
|
126
136
|
expect(Berater::Limiter).to receive(:new).and_raise(IOError)
|
|
127
137
|
end
|
|
128
138
|
|
|
129
|
-
it
|
|
139
|
+
it 'normally crashes the app' do
|
|
130
140
|
expect { response }.to raise_error(IOError)
|
|
131
141
|
end
|
|
132
142
|
|
|
133
|
-
context
|
|
143
|
+
context 'when an error type is registered with middleware' do
|
|
134
144
|
around do |example|
|
|
135
|
-
Rack::Berater::
|
|
145
|
+
Rack::Berater::ERRORS << IOError
|
|
136
146
|
example.run
|
|
137
|
-
Rack::Berater::
|
|
147
|
+
Rack::Berater::ERRORS.delete(IOError)
|
|
138
148
|
end
|
|
139
149
|
|
|
140
|
-
it
|
|
150
|
+
it 'catches and transforms limit errors' do
|
|
141
151
|
expect(response.status).to eq 429
|
|
142
152
|
end
|
|
143
153
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack-berater
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Pepper
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2021-
|
|
11
|
+
date: 2021-11-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: berater
|
|
@@ -80,6 +80,20 @@ dependencies:
|
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rails
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
83
97
|
- !ruby/object:Gem::Dependency
|
|
84
98
|
name: rspec
|
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,6 +108,20 @@ dependencies:
|
|
|
94
108
|
- - ">="
|
|
95
109
|
- !ruby/object:Gem::Version
|
|
96
110
|
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rspec-rails
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
97
125
|
- !ruby/object:Gem::Dependency
|
|
98
126
|
name: simplecov
|
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -116,9 +144,14 @@ extra_rdoc_files: []
|
|
|
116
144
|
files:
|
|
117
145
|
- lib/rack-berater.rb
|
|
118
146
|
- lib/rack/berater.rb
|
|
147
|
+
- lib/rack/berater/prioritizer.rb
|
|
148
|
+
- lib/rack/berater/rails_prioritizer.rb
|
|
119
149
|
- lib/rack/berater/railtie.rb
|
|
120
150
|
- lib/rack/berater/version.rb
|
|
121
151
|
- spec/limiter_spec.rb
|
|
152
|
+
- spec/prioritizer_spec.rb
|
|
153
|
+
- spec/rails_prioritizer_spec.rb
|
|
154
|
+
- spec/railtie_spec.rb
|
|
122
155
|
- spec/rescuer_spec.rb
|
|
123
156
|
homepage: https://github.com/dpep/rack-berater
|
|
124
157
|
licenses:
|
|
@@ -139,10 +172,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
139
172
|
- !ruby/object:Gem::Version
|
|
140
173
|
version: '0'
|
|
141
174
|
requirements: []
|
|
142
|
-
rubygems_version: 3.1.
|
|
175
|
+
rubygems_version: 3.1.6
|
|
143
176
|
signing_key:
|
|
144
177
|
specification_version: 4
|
|
145
178
|
summary: Rack::Berater
|
|
146
179
|
test_files:
|
|
180
|
+
- spec/rails_prioritizer_spec.rb
|
|
181
|
+
- spec/railtie_spec.rb
|
|
147
182
|
- spec/rescuer_spec.rb
|
|
183
|
+
- spec/prioritizer_spec.rb
|
|
148
184
|
- spec/limiter_spec.rb
|