Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit 7752a3e

Browse files
committed
Merge pull request #13 from codeclimate/pb-check-http-responses
Middleware pattern, tell Faraday to raise
2 parents b149c6f + 601017a commit 7752a3e

File tree

10 files changed

+194
-109
lines changed

10 files changed

+194
-109
lines changed

lib/cc/service.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,6 @@ def self.load_services
3232

3333
ALL_EVENTS = %w[test unit coverage quality vulnerability]
3434

35-
def self.receive(config, payload, invocation_class = Invocation)
36-
statsd = config.delete(:statsd)
37-
logger = config.delete(:logger)
38-
service = new(config, payload)
39-
40-
invocation = invocation_class.new(service, statsd, logger)
41-
invocation.invoke
42-
end
43-
4435
# Tracks the defined services.
4536
def self.services
4637
@services ||= []

lib/cc/service/http.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def http(options = {})
5353

5454
Faraday.new(options) do |b|
5555
b.request(:url_encoded)
56+
b.response(:raise_error)
5657
b.adapter(*Array(options[:adapter] || config[:adapter]))
5758
end
5859
end

lib/cc/service/invocation.rb

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,48 @@
1-
class CC::Service::Invocation
2-
RETRIES = 3
3-
4-
def initialize(service, statsd = nil, logger = nil)
5-
@service = service
6-
@statsd = statsd || NullObject.new
7-
@logger = logger || NullObject.new
8-
end
9-
10-
def invoke
11-
safely { service.receive }
12-
13-
statsd.increment(success_stat)
14-
end
15-
16-
private
17-
18-
attr_reader :service, :statsd, :logger
19-
20-
def safely(&block)
21-
with_retries(RETRIES, &block)
22-
rescue => ex
23-
statsd.increment(error_stat(ex))
24-
logger.error(error_message(ex))
25-
end
26-
27-
def with_retries(retries, &block)
28-
yield
29-
30-
rescue => ex
31-
raise ex if retries.zero?
1+
require 'cc/service/invocation/invocation_chain'
2+
require 'cc/service/invocation/with_retries'
3+
require 'cc/service/invocation/with_metrics'
4+
require 'cc/service/invocation/with_error_handling'
325

33-
retries -= 1
34-
retry
35-
end
36-
37-
def success_stat
38-
"services.invocations.#{slug}"
39-
end
40-
41-
def error_stat(ex)
42-
"services.errors.#{slug}.#{ex.class.name.underscore}"
43-
end
44-
45-
def error_message(ex)
46-
"Exception invoking #{slug} service: (#{ex.class}) #{ex.message}"
47-
end
48-
49-
def slug
50-
service.class.slug
6+
class CC::Service::Invocation
7+
MIDDLEWARE = {
8+
retries: WithRetries,
9+
metrics: WithMetrics,
10+
error_handling: WithErrorHandling,
11+
}
12+
13+
# Build a chain of invocation wrappers which eventually calls receive
14+
# on the given service, then execute that chain.
15+
#
16+
# Order is important. Each call to #with, wraps the last.
17+
#
18+
# Usage:
19+
#
20+
# CC::Service::Invocation.new(service) do |i|
21+
# i.with :retries, 3
22+
# i.with :metrics, $statsd
23+
# i.with :error_handling, Rails.logger
24+
# end
25+
#
26+
# In the above example, service.receive could happen 4 times (once,
27+
# then three retries) before an exception is re-raised up to the
28+
# metrics collector, then up again to the error handling. If the order
29+
# were reversed, the error handling middleware would prevent the other
30+
# middleware from seeing any exceptions at all.
31+
def initialize(service)
32+
@chain = InvocationChain.new { service.receive }
33+
34+
yield(self) if block_given?
35+
36+
@chain.call
37+
end
38+
39+
def with(middleware, *args)
40+
if klass = MIDDLEWARE[middleware]
41+
wrap(klass, *args)
42+
end
5143
end
5244

53-
class NullObject
54-
def method_missing(*)
55-
end
45+
def wrap(klass, *args)
46+
@chain.wrap(klass, *args)
5647
end
5748
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class CC::Service::Invocation
2+
class InvocationChain
3+
def initialize(&block)
4+
@invocation = block
5+
end
6+
7+
def wrap(klass, *args)
8+
@invocation = klass.new(@invocation, *args)
9+
end
10+
11+
def call
12+
@invocation.call
13+
end
14+
end
15+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class CC::Service::Invocation
2+
class WithErrorHandling
3+
def initialize(invocation, logger, prefix = nil)
4+
@invocation = invocation
5+
@logger = logger
6+
@prefix = prefix
7+
end
8+
9+
def call
10+
@invocation.call
11+
rescue => ex
12+
@logger.error(error_message(ex))
13+
end
14+
15+
private
16+
17+
def error_message(ex)
18+
message = "Exception invoking service:"
19+
message << " [#{@prefix}]" if @prefix
20+
message << " (#{ex.class}) #{ex.message}"
21+
end
22+
end
23+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class CC::Service::Invocation
2+
class WithMetrics
3+
def initialize(invocation, statsd, prefix = nil)
4+
@invocation = invocation
5+
@statsd = statsd
6+
@prefix = prefix
7+
end
8+
9+
def call
10+
@invocation.call
11+
@statsd.increment(success_key)
12+
rescue => ex
13+
@statsd.increment(error_key(ex))
14+
raise ex
15+
end
16+
17+
def success_key
18+
["services.invocations", @prefix].compact.join('.')
19+
end
20+
21+
def error_key(ex)
22+
["services.errors", @prefix, "#{ex.class.name.underscore}"].compact.join('.')
23+
end
24+
end
25+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class CC::Service::Invocation
2+
class WithRetries
3+
def initialize(invocation, retries)
4+
@invocation = invocation
5+
@retries = retries
6+
end
7+
8+
def call
9+
@invocation.call
10+
rescue => ex
11+
raise ex if @retries.zero?
12+
13+
@retries -= 1
14+
retry
15+
end
16+
end
17+
end

service_test.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,24 @@
2020
require 'cc/services'
2121
CC::Service.load_services
2222

23+
class WithResponseLogging
24+
def initialize(invocation)
25+
@invocation = invocation
26+
end
27+
28+
def call
29+
@invocation.call.tap { |r| p r }
30+
end
31+
end
32+
2333
def test_service(klass, config)
2434
repo_name = ENV["REPO_NAME"] || "App"
2535

26-
klass.receive(config, name: :test, repo_name: repo_name)
36+
service = klass.new(config, name: :test, repo_name: repo_name)
37+
38+
CC::Service::Invocation.new(service) do |i|
39+
i.wrap(WithResponseLogging)
40+
end
2741
end
2842

2943
if webhook_url = ENV["SLACK_WEBHOOK_URL"]

test/invocation_test.rb

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,83 @@ class TestInvocation < Test::Unit::TestCase
44
def test_success
55
service = FakeService.new
66

7-
CC::Service::Invocation.new(service).invoke
7+
CC::Service::Invocation.new(service)
88

99
assert_equal 1, service.receive_count
1010
end
1111

12-
def test_failure
12+
def test_retries
1313
service = FakeService.new
1414
service.raise_on_receive = true
15+
error_occurred = false
1516

16-
CC::Service::Invocation.new(service).invoke
17+
begin
18+
CC::Service::Invocation.new(service) do |i|
19+
i.with :retries, 3
20+
end
21+
rescue
22+
error_occurred = true
23+
end
1724

18-
# First call + N retries
19-
expected_count = 1 + CC::Service::Invocation::RETRIES
20-
assert_equal expected_count, service.receive_count
25+
assert error_occurred
26+
assert_equal 1 + 3, service.receive_count
2127
end
2228

23-
def test_success_metrics
29+
def test_metrics
2430
statsd = FakeStatsd.new
25-
logger = FakeLogger.new
26-
service = FakeService.new
2731

28-
CC::Service::Invocation.new(service, statsd, logger).invoke
32+
CC::Service::Invocation.new(FakeService.new) do |i|
33+
i.with :metrics, statsd, "a_prefix"
34+
end
2935

3036
assert_equal 1, statsd.incremented_keys.length
31-
assert_match /services\.invocations/, statsd.incremented_keys.first
32-
assert_empty logger.logged_errors
37+
assert_equal "services.invocations.a_prefix", statsd.incremented_keys.first
3338
end
3439

35-
def test_failure_metrics
40+
def test_metrics_on_errors
3641
statsd = FakeStatsd.new
42+
service = FakeService.new
43+
service.raise_on_receive = true
44+
error_occurred = false
45+
46+
begin
47+
CC::Service::Invocation.new(service) do |i|
48+
i.with :metrics, statsd, "a_prefix"
49+
end
50+
rescue
51+
error_occurred = true
52+
end
53+
54+
assert error_occurred
55+
assert_equal 1, statsd.incremented_keys.length
56+
assert_match /^services\.errors\.a_prefix/, statsd.incremented_keys.first
57+
end
58+
59+
def test_error_handling
3760
logger = FakeLogger.new
3861
service = FakeService.new
3962
service.raise_on_receive = true
4063

41-
CC::Service::Invocation.new(service, statsd, logger).invoke
64+
CC::Service::Invocation.new(service) do |i|
65+
i.with :error_handling, logger, "a_prefix"
66+
end
67+
68+
assert_equal 1, logger.logged_errors.length
69+
assert_match /^Exception invoking service: \[a_prefix\]/, logger.logged_errors.first
70+
end
71+
72+
def test_multiple_middleware
73+
service = FakeService.new
74+
service.raise_on_receive = true
75+
logger = FakeLogger.new
76+
77+
CC::Service::Invocation.new(service) do |i|
78+
i.with :retries, 3
79+
i.with :error_handling, logger
80+
end
4281

43-
refute_empty statsd.incremented_keys
44-
refute_empty logger.logged_errors
45-
assert_match /services\.errors/, statsd.incremented_keys.first
82+
assert_equal 1 + 3, service.receive_count
83+
assert_equal 1, logger.logged_errors.length
4684
end
4785

4886
private
@@ -51,10 +89,6 @@ class FakeService
5189
attr_reader :receive_count
5290
attr_accessor :raise_on_receive
5391

54-
def self.slug
55-
"fake-service"
56-
end
57-
5892
def initialize
5993
@receive_count = 0
6094
end

test/service_test.rb

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,4 @@ def test_validates_events
66
CC::Service.new(:foo, {}, {})
77
end
88
end
9-
10-
def test_receive
11-
ret = CC::Service.receive(
12-
{ statsd: "statsd", logger: "logger" },
13-
{ name: :test, foo: "bar" },
14-
FakeInvocation
15-
)
16-
17-
assert ret[:invoked]
18-
assert ret[:service].is_a?(CC::Service)
19-
assert_equal "test", ret[:service].event
20-
assert_equal "bar", ret[:service].payload["foo"]
21-
assert_equal "statsd", ret[:statsd]
22-
assert_equal "logger", ret[:logger]
23-
end
24-
25-
FakeInvocation = Struct.new(:service, :statsd, :logger) do
26-
def invoke
27-
{
28-
invoked: true,
29-
service: service,
30-
statsd: statsd,
31-
logger: logger
32-
}
33-
end
34-
end
359
end

0 commit comments

Comments
 (0)