Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Refactor request sending to have better excpetions #4358

Merged
merged 5 commits into from
Jan 8, 2019
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
1 change: 1 addition & 0 deletions changelog.d/4358.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add better logging for unexpected errors while sending transactions
18 changes: 18 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,24 @@ def error_dict(self):
)


class RequestSendFailed(RuntimeError):
"""Sending a HTTP request over federation failed due to not being able to
talk to the remote server for some reason.

This exception is used to differentiate "expected" errors that arise due to
networking (e.g. DNS failures, connection timeouts etc), versus unexpected
errors (like programming errors).
"""
def __init__(self, inner_exception, can_retry):
super(RequestSendFailed, self).__init__(
"Failed to send request: %s: %s" % (
type(inner_exception).__name__, inner_exception,
)
)
self.inner_exception = inner_exception
self.can_retry = can_retry


def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
""" Utility method for constructing an error response for client-server
interactions.
Expand Down
19 changes: 14 additions & 5 deletions synapse/federation/transaction_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
from twisted.internet import defer

import synapse.metrics
from synapse.api.errors import FederationDeniedError, HttpResponseException
from synapse.api.errors import (
FederationDeniedError,
HttpResponseException,
RequestSendFailed,
)
from synapse.handlers.presence import format_user_presence_state, get_interested_remotes
from synapse.metrics import (
LaterGauge,
Expand Down Expand Up @@ -518,11 +522,16 @@ def _transaction_transmission_loop(self, destination):
)
except FederationDeniedError as e:
logger.info(e)
except Exception as e:
logger.warn(
"TX [%s] Failed to send transaction: %s",
except RequestSendFailed as e:
logger.warning("(TX [%s] Failed to send transaction: %s", destination, e)

for p, _ in pending_pdus:
logger.info("Failed to send event %s to %s", p.event_id,
destination)
except Exception:
logger.exception(
"TX [%s] Failed to send transaction",
destination,
e,
)
for p, _ in pending_pdus:
logger.info("Failed to send event %s to %s", p.event_id,
Expand Down
105 changes: 72 additions & 33 deletions synapse/http/matrixfederationclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import sys
from io import BytesIO

from six import PY3, string_types
from six import PY3, raise_from, string_types
from six.moves import urllib

import attr
Expand All @@ -41,6 +41,7 @@
Codes,
FederationDeniedError,
HttpResponseException,
RequestSendFailed,
SynapseError,
)
from synapse.http.endpoint import matrix_federation_endpoint
Expand Down Expand Up @@ -231,16 +232,16 @@ def _send_request(
Deferred: resolves with the http response object on success.

Fails with ``HttpResponseException``: if we get an HTTP response
code >= 300.
code >= 300 (except 429).

Fails with ``NotRetryingDestination`` if we are not yet ready
to retry this server.

Fails with ``FederationDeniedError`` if this destination
is not on our federation whitelist

(May also fail with plenty of other Exceptions for things like DNS
failures, connection failures, SSL failures.)
Fails with ``RequestSendFailed`` if there were problems connecting to
the remote, due to e.g. DNS failures, connection timeouts etc.
"""
if timeout:
_sec_timeout = timeout / 1000
Expand Down Expand Up @@ -335,23 +336,74 @@ def _send_request(
reactor=self.hs.get_reactor(),
)

with Measure(self.clock, "outbound_request"):
response = yield make_deferred_yieldable(
request_deferred,
try:
with Measure(self.clock, "outbound_request"):
response = yield make_deferred_yieldable(
request_deferred,
)
except DNSLookupError as e:
raise_from(RequestSendFailed(e, can_retry=retry_on_dns_fail), e)
except Exception as e:
raise_from(RequestSendFailed(e, can_retry=True), e)

logger.info(
"{%s} [%s] Got response headers: %d %s",
request.txn_id,
request.destination,
response.code,
response.phrase.decode('ascii', errors='replace'),
)

if 200 <= response.code < 300:
pass
else:
# :'(
# Update transactions table?
d = treq.content(response)
d = timeout_deferred(
d,
timeout=_sec_timeout,
reactor=self.hs.get_reactor(),
)

try:
body = yield make_deferred_yieldable(d)
except Exception as e:
# Eh, we're already going to raise an exception so lets
# ignore if this fails.
logger.warn(
"{%s} [%s] Failed to get error response: %s %s: %s",
request.txn_id,
request.destination,
request.method,
url_str,
_flatten_response_never_received(e),
)
body = None

e = HttpResponseException(
response.code, response.phrase, body
)

# Retry if the error is a 429 (Too Many Requests),
# otherwise just raise a standard HttpResponseException
if response.code == 429:
raise_from(RequestSendFailed(e, can_retry=True), e)
else:
raise e

break
except Exception as e:
except RequestSendFailed as e:
logger.warn(
"{%s} [%s] Request failed: %s %s: %s",
request.txn_id,
request.destination,
request.method,
url_str,
_flatten_response_never_received(e),
_flatten_response_never_received(e.inner_exception),
)

if not retry_on_dns_fail and isinstance(e, DNSLookupError):
if not e.can_retry:
raise

if retries_left and not timeout:
Expand All @@ -376,29 +428,16 @@ def _send_request(
else:
raise

logger.info(
"{%s} [%s] Got response headers: %d %s",
request.txn_id,
request.destination,
response.code,
response.phrase.decode('ascii', errors='replace'),
)

if 200 <= response.code < 300:
pass
else:
# :'(
# Update transactions table?
d = treq.content(response)
d = timeout_deferred(
d,
timeout=_sec_timeout,
reactor=self.hs.get_reactor(),
)
body = yield make_deferred_yieldable(d)
raise HttpResponseException(
response.code, response.phrase, body
)
except Exception as e:
logger.warn(
"{%s} [%s] Request failed: %s %s: %s",
request.txn_id,
request.destination,
request.method,
url_str,
_flatten_response_never_received(e),
)
raise

defer.returnValue(response)

Expand Down
7 changes: 4 additions & 3 deletions synapse/rest/media/v1/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
FederationDeniedError,
HttpResponseException,
NotFoundError,
RequestSendFailed,
SynapseError,
)
from synapse.metrics.background_process_metrics import run_as_background_process
Expand Down Expand Up @@ -372,10 +373,10 @@ def _download_remote_file(self, server_name, media_id, file_id):
"allow_remote": "false",
}
)
except twisted.internet.error.DNSLookupError as e:
logger.warn("HTTP error fetching remote media %s/%s: %r",
except RequestSendFailed as e:
logger.warn("Request failed fetching remote media %s/%s: %r",
server_name, media_id, e)
raise NotFoundError()
raise SynapseError(502, "Failed to fetch remote media")

except HttpResponseException as e:
logger.warn("HTTP error fetching remote media %s/%s: %s",
Expand Down
13 changes: 10 additions & 3 deletions tests/http/test_fedclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from twisted.web.client import ResponseNeverReceived
from twisted.web.http import HTTPChannel

from synapse.api.errors import RequestSendFailed
from synapse.http.matrixfederationclient import (
MatrixFederationHttpClient,
MatrixFederationRequest,
Expand Down Expand Up @@ -49,7 +50,8 @@ def test_dns_error(self):
self.pump()

f = self.failureResultOf(d)
self.assertIsInstance(f.value, DNSLookupError)
self.assertIsInstance(f.value, RequestSendFailed)
self.assertIsInstance(f.value.inner_exception, DNSLookupError)

def test_client_never_connect(self):
"""
Expand All @@ -76,7 +78,11 @@ def test_client_never_connect(self):
self.reactor.advance(10.5)
f = self.failureResultOf(d)

self.assertIsInstance(f.value, (ConnectingCancelledError, TimeoutError))
self.assertIsInstance(f.value, RequestSendFailed)
self.assertIsInstance(
f.value.inner_exception,
(ConnectingCancelledError, TimeoutError),
)

def test_client_connect_no_response(self):
"""
Expand Down Expand Up @@ -107,7 +113,8 @@ def test_client_connect_no_response(self):
self.reactor.advance(10.5)
f = self.failureResultOf(d)

self.assertIsInstance(f.value, ResponseNeverReceived)
self.assertIsInstance(f.value, RequestSendFailed)
self.assertIsInstance(f.value.inner_exception, ResponseNeverReceived)

def test_client_gets_headers(self):
"""
Expand Down