Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions hathor/healthcheck/resources/healthcheck.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import asyncio

from healthcheck import Healthcheck, HealthcheckCallbackResponse, HealthcheckInternalComponent, HealthcheckStatus
from healthcheck import (
Healthcheck,
HealthcheckCallbackResponse,
HealthcheckInternalComponent,
HealthcheckResponse,
HealthcheckStatus,
)
from twisted.internet.defer import Deferred, succeed
from twisted.python.failure import Failure
from twisted.web.http import Request
from twisted.web.server import NOT_DONE_YET

from hathor.api_util import Resource, get_arg_default, get_args
from hathor.cli.openapi_files.register import register_resource
Expand All @@ -24,6 +34,28 @@ class HealthcheckResource(Resource):
def __init__(self, manager: HathorManager):
self.manager = manager

def _render_error(self, failure: Failure, request: Request) -> None:
request.setResponseCode(500)
request.write(json_dumpb({
'status': 'fail',
'reason': f'Internal Error: {failure.getErrorMessage()}',
'traceback': failure.getTraceback()
}))
request.finish()

def _render_success(self, result: HealthcheckResponse, request: Request) -> None:
raw_args = get_args(request)
strict_status_code = get_arg_default(raw_args, 'strict_status_code', '0') == '1'

if strict_status_code:
request.setResponseCode(200)
else:
status_code = result.get_http_status_code()
request.setResponseCode(status_code)

request.write(json_dumpb(result.to_json()))
request.finish()

def render_GET(self, request):
""" GET request /health/
Returns the health status of the fullnode
Expand All @@ -34,24 +66,26 @@ def render_GET(self, request):

:rtype: string (json)
"""
raw_args = get_args(request)
strict_status_code = get_arg_default(raw_args, 'strict_status_code', '0') == '1'

sync_component = HealthcheckInternalComponent(
name='sync',
)
sync_component.add_healthcheck(lambda: sync_healthcheck(self.manager))

healthcheck = Healthcheck(name='hathor-core', components=[sync_component])
status = asyncio.get_event_loop().run_until_complete(healthcheck.run())

if strict_status_code:
request.setResponseCode(200)
# The asyncio loop will be running in case the option --x-asyncio-reactor is used
# XXX: We should remove this if when the asyncio reactor becomes the default and the only option
if asyncio.get_event_loop().is_running():
future = asyncio.ensure_future(healthcheck.run())
deferred = Deferred.fromFuture(future)
else:
status_code = status.get_http_status_code()
request.setResponseCode(status_code)
status = asyncio.get_event_loop().run_until_complete(healthcheck.run())
deferred = succeed(status)

deferred.addCallback(self._render_success, request)
deferred.addErrback(self._render_error, request)

return json_dumpb(status.to_json())
return NOT_DONE_YET


HealthcheckResource.openapi = {
Expand Down
53 changes: 52 additions & 1 deletion tests/resources/healthcheck/test_healthcheck.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
from unittest.mock import ANY

from twisted.internet.defer import inlineCallbacks
from twisted.internet.defer import Deferred, inlineCallbacks

from hathor.healthcheck.resources.healthcheck import HealthcheckResource
from hathor.manager import HathorManager
Expand Down Expand Up @@ -39,6 +40,56 @@ def test_get_no_recent_activity(self):
}
})

def test_with_running_asyncio_loop(self):
"""Test with a running asyncio loop.

This is a simulation of how this endpoint should behave in production when the
--x-asyncio-reactor is provided to hathor-core, because this causes the reactor to run
an asyncio loop.
"""
# This deferred will be used solely to make sure the test doesn't finish before the async code
done = Deferred()

def set_done(_):
done.callback(None)

def set_done_fail(failure):
done.errback(failure)

# This will be called from inside the async method to perform the web request
# while a running asyncio loop is present
@inlineCallbacks
def get_health():
response = yield self.web.get('/health')
return response.json_value()

async def run():
data = get_health()
# When the request is done, we make sure the response is as expected
data.addCallback(self.assertEqual, {
'status': 'fail',
'description': ANY,
'checks': {
'sync': [{
'componentType': 'internal',
'componentName': 'sync',
'status': 'fail',
'output': HathorManager.UnhealthinessReason.NO_RECENT_ACTIVITY,
'time': ANY
}]
}
})
# We succeed the "done" deferred if everything is ok
data.addCallback(set_done)
# We fail the "done" deferred if something goes wrong. This includes the assertion above failing.
data.addErrback(set_done_fail)

# This will make sure we have a running asyncio loop
asyncio.get_event_loop().run_until_complete(run())

# Return the deferred so the test doesn't finish before the async code
return done

@inlineCallbacks
def test_strict_status_code(self):
"""Make sure the 'strict_status_code' parameter is working.
Expand Down