Skip to content

Commit dc7d946

Browse files
mars-fpurelogiq
authored andcommittedJul 11, 2017
Return HTTP 502 from failed phabricator heartbeat checks
1 parent 2dadc64 commit dc7d946

5 files changed

+129
-3
lines changed
 

‎landoapi/dockerflow.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@
77
"""
88

99
import json
10+
import logging
1011

12+
import requests
1113
from flask import Blueprint, current_app, jsonify
1214

15+
from landoapi.phabricator_client import PhabricatorClient, \
16+
PhabricatorAPIException
17+
18+
logger = logging.getLogger(__name__)
19+
1320
dockerflow = Blueprint('dockerflow', __name__)
1421

1522

@@ -21,8 +28,16 @@ def heartbeat():
2128
and return a 200 iff those services and the app itself are
2229
performing normally. Return a 5XX if something goes wrong.
2330
"""
24-
# TODO check backing services
25-
return '', 200
31+
phab = PhabricatorClient(api_key='')
32+
try:
33+
phab.check_connection()
34+
except PhabricatorAPIException as exc:
35+
logger.warning(
36+
'heartbeat: problem, Phabricator API connection', exc_info=exc
37+
)
38+
return 'heartbeat: problem', 502
39+
logger.info('heartbeat: ok, all services are up')
40+
return 'heartbeat: ok', 200
2641

2742

2843
@dockerflow.route('/__lbheartbeat__')

‎landoapi/phabricator_client.py

+18
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

5+
import logging
56
import os
67
import requests
78

89
from landoapi.utils import extract_rawdiff_id_from_uri
910

11+
logger = logging.getLogger(__name__)
12+
1013

1114
class PhabricatorClient:
1215
""" A class to interface with Phabricator's Conduit API.
@@ -145,6 +148,21 @@ def get_revision_repo(self, revision):
145148
"""
146149
return self.get_repo(revision['repositoryPHID'])
147150

151+
def check_connection(self):
152+
"""Test the Phabricator API connection with conduit.ping.
153+
154+
Will return success iff the response has a HTTP status code of 200, the
155+
JSON response is a well-formed Phabricator API response, and if there
156+
is no connection error (like a hostname lookup error or timeout).
157+
158+
Raises a PhabricatorAPIException on error.
159+
"""
160+
try:
161+
self._GET('/conduit.ping')
162+
except (requests.ConnectionError, requests.Timeout) as exc:
163+
logging.debug("error calling 'conduit.ping': %s", exc)
164+
raise PhabricatorAPIException from exc
165+
148166
def _request(self, url, data=None, params=None, method='GET'):
149167
data = data if data else {}
150168
data['api.token'] = self.api_key

‎tests/conftest.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
import json
55

6+
import logging
67
import pytest
78
import requests_mock
89

@@ -58,7 +59,13 @@ def init_app(self, app):
5859

5960

6061
@pytest.fixture
61-
def app(versionfile, docker_env_vars, disable_migrations):
62+
def disable_log_output():
63+
"""Disable Python standard logging output to the console."""
64+
logging.disable(logging.CRITICAL)
65+
66+
67+
@pytest.fixture
68+
def app(versionfile, docker_env_vars, disable_migrations, disable_log_output):
6269
"""Needed for pytest-flask."""
6370
app = create_app(versionfile.strpath)
6471
return app.app

‎tests/test_dockerflow.py

+33
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

55
import json
6+
import requests
7+
8+
import requests_mock
9+
10+
from tests.canned_responses.phabricator.revisions import CANNED_EMPTY_RESULT
11+
from tests.utils import phab_url
612

713

814
def test_dockerflow_lb_endpoint_returns_200(client):
@@ -18,3 +24,30 @@ def test_dockerflow_version_endpoint_response(client):
1824
def test_dockerflow_version_matches_disk_contents(client, versionfile):
1925
response = client.get('/__version__')
2026
assert response.json == json.load(versionfile.open())
27+
28+
29+
def test_heartbeat_returns_200_if_phabricator_api_is_up(client):
30+
json_response = CANNED_EMPTY_RESULT.copy()
31+
with requests_mock.mock() as m:
32+
m.get(phab_url('conduit.ping'), status_code=200, json=json_response)
33+
34+
response = client.get('/__heartbeat__')
35+
36+
assert m.called
37+
assert response.status_code == 200
38+
39+
40+
def test_heartbeat_returns_http_502_if_phabricator_ping_returns_error(client):
41+
error_json = {
42+
"result": None,
43+
"error_code": "ERR-CONDUIT-CORE",
44+
"error_info": "BOOM"
45+
}
46+
47+
with requests_mock.mock() as m:
48+
m.get(phab_url('conduit.ping'), status_code=500, json=error_json)
49+
50+
response = client.get('/__heartbeat__')
51+
52+
assert m.called
53+
assert response.status_code == 502

‎tests/test_phabricator_client.py

+53
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"""
55
Tests for the PhabricatorClient
66
"""
7+
78
import pytest
9+
import requests
810
import requests_mock
911

1012
from landoapi.phabricator_client import PhabricatorClient, \
@@ -122,6 +124,57 @@ def test_get_latest_patch_for_revision(phabfactory):
122124
assert returned_patch == patch
123125

124126

127+
def test_check_connection_success():
128+
phab = PhabricatorClient(api_key='api-key')
129+
success_json = CANNED_EMPTY_RESULT.copy()
130+
with requests_mock.mock() as m:
131+
m.get(phab_url('conduit.ping'), status_code=200, json=success_json)
132+
phab.check_connection()
133+
assert m.called
134+
135+
136+
def test_raise_exception_if_ping_encounters_connection_error():
137+
phab = PhabricatorClient(api_key='api-key')
138+
with requests_mock.mock() as m:
139+
# Test with the generic ConnectionError, which is a superclass for
140+
# other connection error types.
141+
m.get(phab_url('conduit.ping'), exc=requests.ConnectionError)
142+
143+
with pytest.raises(PhabricatorAPIException):
144+
phab.check_connection()
145+
assert m.called
146+
147+
148+
def test_raise_exception_if_api_ping_times_out():
149+
phab = PhabricatorClient(api_key='api-key')
150+
with requests_mock.mock() as m:
151+
# Test with the generic Timeout exception, which all other timeout
152+
# exceptions derive from.
153+
m.get(phab_url('conduit.ping'), exc=requests.Timeout)
154+
155+
with pytest.raises(PhabricatorAPIException):
156+
phab.check_connection()
157+
assert m.called
158+
159+
160+
def test_raise_exception_if_api_returns_error_json_response():
161+
phab = PhabricatorClient(api_key='api-key')
162+
error_json = {
163+
"result": None,
164+
"error_code": "ERR-CONDUIT-CORE",
165+
"error_info": "BOOM"
166+
}
167+
168+
with requests_mock.mock() as m:
169+
# Test with the generic Timeout exception, which all other timeout
170+
# exceptions derive from.
171+
m.get(phab_url('conduit.ping'), status_code=500, json=error_json)
172+
173+
with pytest.raises(PhabricatorAPIException):
174+
phab.check_connection()
175+
assert m.called
176+
177+
125178
def test_phabricator_exception():
126179
""" Ensures that the PhabricatorClient converts JSON errors from Phabricator
127180
into proper exceptions with the error_code and error_message in tact.

0 commit comments

Comments
 (0)
Failed to load comments.