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

Type hints for the remaining two files in synapse.http. #11164

Merged
merged 9 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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/11164.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add type hints so that `synapse.http` passes `mypy` checks.
12 changes: 2 additions & 10 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ no_implicit_optional = True

files =
scripts-dev/sign_json,
synapse/__init__.py,
synapse/api,
synapse/appservice,
synapse/config,
Expand All @@ -31,16 +32,7 @@ files =
synapse/federation,
synapse/groups,
synapse/handlers,
synapse/http/additional_resource.py,
synapse/http/client.py,
synapse/http/federation/matrix_federation_agent.py,
synapse/http/federation/srv_resolver.py,
synapse/http/federation/well_known_resolver.py,
synapse/http/matrixfederationclient.py,
synapse/http/proxyagent.py,
synapse/http/servlet.py,
synapse/http/server.py,
synapse/http/site.py,
synapse/http,
synapse/logging,
synapse/metrics,
synapse/module_api,
Expand Down
12 changes: 9 additions & 3 deletions synapse/http/connectproxyclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ def __init__(
def __repr__(self):
return "<HTTPConnectProxyEndpoint %s>" % (self._proxy_endpoint,)

def connect(self, protocolFactory: ClientFactory):
# Mypy encounters a false positive here: it complains that ClientFactory
# is incompatible with IProtocolFactory. But ClientFactory inherits from
# Factory, which implements IProtocolFactory. So I think this is a bug
# in mypy-zope.
def connect(self, protocolFactory: ClientFactory): # type: ignore[override]
f = HTTPProxiedClientFactory(
self._host, self._port, protocolFactory, self._proxy_creds
)
Expand Down Expand Up @@ -119,13 +123,15 @@ def __init__(
self.dst_port = dst_port
self.wrapped_factory = wrapped_factory
self.proxy_creds = proxy_creds
self.on_connection = defer.Deferred()
self.on_connection: "defer.Deferred[None]" = defer.Deferred()

def startedConnecting(self, connector):
return self.wrapped_factory.startedConnecting(connector)

def buildProtocol(self, addr):
wrapped_protocol = self.wrapped_factory.buildProtocol(addr)
if wrapped_protocol is None:
raise TypeError("buildProtocol produced None instead of a Protocol")

return HTTPConnectProtocol(
self.dst_host,
Expand Down Expand Up @@ -235,7 +241,7 @@ def __init__(
self.host = host
self.port = port
self.proxy_creds = proxy_creds
self.on_connected = defer.Deferred()
self.on_connected: "defer.Deferred[None]" = defer.Deferred()

def connectionMade(self):
logger.debug("Connected to proxy, sending CONNECT")
Expand Down
52 changes: 34 additions & 18 deletions synapse/http/request_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

import logging
import threading
import traceback
from typing import Dict, Set, Tuple

from prometheus_client.core import Counter, Histogram

Expand Down Expand Up @@ -105,19 +107,14 @@
["method", "servlet"],
)

# The set of all in flight requests, set[RequestMetrics]
_in_flight_requests = set()
_in_flight_requests: Set["RequestMetrics"] = set()

# Protects the _in_flight_requests set from concurrent access
_in_flight_requests_lock = threading.Lock()


def _get_in_flight_counts():
"""Returns a count of all in flight requests by (method, server_name)

Returns:
dict[tuple[str, str], int]
"""
def _get_in_flight_counts() -> Dict[Tuple[str, ...], float]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really really really want to say:

Suggested change
def _get_in_flight_counts() -> Dict[Tuple[str, ...], float]:
def _get_in_flight_counts() -> Dict[Tuple[str, str], float]:

But I guess it's OK (I can see why you had to do this) and it's only the metrics.

"""Returns a count of all in flight requests by (method, server_name)"""
# Cast to a list to prevent it changing while the Prometheus
# thread is collecting metrics
with _in_flight_requests_lock:
Expand All @@ -127,11 +124,12 @@ def _get_in_flight_counts():
rm.update_metrics()

# Map from (method, name) -> int, the number of in flight requests of that
# type
counts = {}
# type. The key type is Tuple[str, str], but we leavethe length unspecified
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved
# for compatability with LaterGauge's annotations.
counts: Dict[Tuple[str, ...], float] = {}
for rm in reqs:
key = (rm.method, rm.name)
counts[key] = counts.get(key, 0) + 1
counts[key] = counts.get(key, 0.0) + 1.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I don't think you need to have changed this line because int is a subtype of float... (I guess it shouldn't hurt anything though? Probably?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may have been a desperate attempt to solve the type mismatch (the one that Tuple[str, ...] eventually solved). I'll see if it passes after reverting this.

(I resisted the temptation to just use a collections.Counter here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work OOTB because Dict[K, int] is not a subtype of Dict[K, float] (Dict is invariant in both parameters). But I think some Mapping will do the trick.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had a go at this in 8ee9d35. What do you reckon?


return counts

Expand All @@ -145,15 +143,21 @@ def _get_in_flight_counts():


class RequestMetrics:
def start(self, time_sec, name, method):
self.start = time_sec
def start(self, time_sec: float, name: str, method: str) -> None:
self.start_ts = time_sec
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reckon it's worth start_ts_sec whilst you're here? We for some reason love milliseconds as a time unit in Synapse and usually try to mark the ones that are in seconds so that it's obvious...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh bahaha, it was a name clash with the method? That's pretty funny, but it's single-use presumably so you never normally notice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. If you call start() you'll be unable to call it a second time on that instance. (Without doing Class.start(instance, ...), anyway)

self.start_context = current_context()
self.name = name
self.method = method

# _request_stats records resource usage that we have already added
# to the "in flight" metrics.
self._request_stats = self.start_context.get_resource_usage()
if self.start_context:
# _request_stats records resource usage that we have already added
# to the "in flight" metrics.
self._request_stats = self.start_context.get_resource_usage()
else:
logger.error(
"Tried to start a RequestMetric from the sentinel context.\n%s",
"".join(traceback.format_stack()),
)

with _in_flight_requests_lock:
_in_flight_requests.add(self)
Expand All @@ -169,12 +173,18 @@ def stop(self, time_sec, response_code, sent_bytes):
tag = context.tag

if context != self.start_context:
logger.warning(
logger.error(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hesitant to change this to error.
It's certainly bad but it's not the server admin's fault. Unless it's because Sentry will only pick up on errors? I guess that makes sense and that's fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, @squahtx's advice was to get these in Sentry and then we can fix them up.

"Context have unexpectedly changed %r, %r",
context,
self.start_context,
)
return
else:
logger.error(
"Trying to stop RequestMetrics in the sentinel context.\n%s",
"".join(traceback.format_stack()),
)
return

response_code = str(response_code)

Expand All @@ -183,7 +193,7 @@ def stop(self, time_sec, response_code, sent_bytes):
response_count.labels(self.method, self.name, tag).inc()

response_timer.labels(self.method, self.name, tag, response_code).observe(
time_sec - self.start
time_sec - self.start_ts
)

resource_usage = context.get_resource_usage()
Expand Down Expand Up @@ -213,6 +223,12 @@ def stop(self, time_sec, response_code, sent_bytes):

def update_metrics(self):
"""Updates the in flight metrics with values from this request."""
if not self.start_context:
logger.error(
"Tried to update a RequestMetric from the sentinel context.\n%s",
"".join(traceback.format_stack()),
)
return
new_stats = self.start_context.get_resource_usage()

diff = new_stats - self._request_stats
Expand Down
4 changes: 2 additions & 2 deletions synapse/logging/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def __init__(self) -> None:
self.scope = None
self.tag = None

def __str__(self):
def __str__(self) -> str:
return "sentinel"

def copy_to(self, record):
Expand All @@ -241,7 +241,7 @@ def add_database_scheduled(self, sched_sec):
def record_event_fetch(self, event_count):
pass

def __bool__(self):
def __bool__(self) -> Literal[False]:
return False


Expand Down