-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Client signals #2313 #2429
Client signals #2313 #2429
Changes from 4 commits
e946ec5
1a01001
2c17707
b2f24e9
971ad5c
4ce2534
fcf7427
e7bbc52
7a0015d
ef573a2
151ec9b
d3ae4a9
c45725c
078c728
ea5d363
607db37
b4a5f03
3640aec
436e8eb
e1da600
1dc1a37
8449a96
8fd0e9d
3d3caac
bd5c8e5
c232cdf
031bb3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ClientSession publishes a set of signals to track the HTTP request execution |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import sys | ||
import traceback | ||
import warnings | ||
from types import SimpleNamespace | ||
|
||
from multidict import CIMultiDict, MultiDict, MultiDictProxy, istr | ||
from yarl import URL | ||
|
@@ -28,6 +29,7 @@ | |
strip_auth_from_url) | ||
from .http import WS_KEY, WebSocketReader, WebSocketWriter | ||
from .http_websocket import WSHandshakeError, ws_ext_gen, ws_ext_parse | ||
from .signals import FuncSignal, Signal | ||
from .streams import FlowControlDataQueue | ||
|
||
|
||
|
@@ -96,6 +98,7 @@ def __init__(self, *, connector=None, loop=None, cookies=None, | |
|
||
if cookies is not None: | ||
self._cookie_jar.update_cookies(cookies) | ||
|
||
self._connector = connector | ||
self._connector_owner = connector_owner | ||
self._default_auth = auth | ||
|
@@ -108,6 +111,15 @@ def __init__(self, *, connector=None, loop=None, cookies=None, | |
self._auto_decompress = auto_decompress | ||
self._trust_env = trust_env | ||
|
||
self._on_request_start = Signal() | ||
self._on_request_end = Signal() | ||
self._on_request_exception = Signal() | ||
self._on_request_redirect = FuncSignal() | ||
self._on_request_headers_sent = FuncSignal() | ||
self._on_request_content_sent = FuncSignal() | ||
self._on_request_headers_received = FuncSignal() | ||
self._on_request_content_received = FuncSignal() | ||
|
||
# Convert to list of tuples | ||
if headers: | ||
headers = CIMultiDict(headers) | ||
|
@@ -161,7 +173,8 @@ def _request(self, method, url, *, | |
verify_ssl=None, | ||
fingerprint=None, | ||
ssl_context=None, | ||
proxy_headers=None): | ||
proxy_headers=None, | ||
trace_context=None): | ||
|
||
# NOTE: timeout clamps existing connect and read timeouts. We cannot | ||
# set the default to None because we need to detect if the user wants | ||
|
@@ -218,6 +231,18 @@ def _request(self, method, url, *, | |
handle = tm.start() | ||
|
||
url = URL(url) | ||
|
||
if trace_context is None: | ||
trace_context = SimpleNamespace() | ||
|
||
yield from self.on_request_start.send( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. await |
||
trace_context, | ||
method, | ||
url.host, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whole URL object maybe? Query part might be interested for tracer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I will do that |
||
url.port, | ||
headers | ||
) | ||
|
||
timer = tm.timer() | ||
try: | ||
with timer: | ||
|
@@ -261,12 +286,20 @@ def _request(self, method, url, *, | |
proxy=proxy, proxy_auth=proxy_auth, timer=timer, | ||
session=self, auto_decompress=self._auto_decompress, | ||
verify_ssl=verify_ssl, fingerprint=fingerprint, | ||
ssl_context=ssl_context, proxy_headers=proxy_headers) | ||
ssl_context=ssl_context, proxy_headers=proxy_headers, | ||
on_headers_sent=self.on_request_headers_sent, | ||
on_content_sent=self.on_request_content_sent, | ||
on_headers_received=self.on_request_headers_received, | ||
on_content_received=self.on_request_content_received, | ||
trace_context=trace_context) | ||
|
||
# connection timeout | ||
try: | ||
with CeilTimeout(self._conn_timeout, loop=self._loop): | ||
conn = yield from self._connector.connect(req) | ||
conn = yield from self._connector.connect( | ||
req, | ||
trace_context=trace_context | ||
) | ||
except asyncio.TimeoutError as exc: | ||
raise ServerTimeoutError( | ||
'Connection timeout ' | ||
|
@@ -291,6 +324,9 @@ def _request(self, method, url, *, | |
# redirects | ||
if resp.status in ( | ||
301, 302, 303, 307, 308) and allow_redirects: | ||
|
||
self.on_request_redirect.send(trace_context, resp) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is intended usage for the signal? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea was to give a way to trace when a redirect happens. Completly agree that the signal parameters are not enough consistency to give enough information to the user, lets add the What do you mean with :: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will do the same with the |
||
|
||
redirects += 1 | ||
history.append(resp) | ||
if max_redirects and redirects >= max_redirects: | ||
|
@@ -354,15 +390,17 @@ def _request(self, method, url, *, | |
handle.cancel() | ||
|
||
resp._history = tuple(history) | ||
yield from self.on_request_end.send(trace_context, resp) | ||
return resp | ||
|
||
except Exception: | ||
except Exception as e: | ||
# cleanup timer | ||
tm.close() | ||
if handle: | ||
handle.cancel() | ||
handle = None | ||
|
||
yield from self.on_request_exception.send(trace_context, e) | ||
raise | ||
|
||
def ws_connect(self, url, *, | ||
|
@@ -654,6 +692,78 @@ def loop(self): | |
"""Session's loop.""" | ||
return self._loop | ||
|
||
@property | ||
def on_request_start(self): | ||
return self._on_request_start | ||
|
||
@property | ||
def on_request_redirect(self): | ||
return self._on_request_redirect | ||
|
||
@property | ||
def on_request_end(self): | ||
return self._on_request_end | ||
|
||
@property | ||
def on_request_exception(self): | ||
return self._on_request_exception | ||
|
||
# connector signals | ||
|
||
@property | ||
def on_request_queued_start(self): | ||
return self._connector.on_queued_start | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think every session should have own signals. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oks I can see right now the idea of the In another way worries me a bit the Should the |
||
|
||
@property | ||
def on_request_queued_end(self): | ||
return self._connector.on_queued_end | ||
|
||
@property | ||
def on_request_createconn_start(self): | ||
return self._connector.on_createconn_start | ||
|
||
@property | ||
def on_request_createconn_end(self): | ||
return self._connector.on_createconn_end | ||
|
||
@property | ||
def on_request_reuseconn(self): | ||
return self._connector.on_reuseconn | ||
|
||
@property | ||
def on_request_resolvehost_start(self): | ||
return self._connector.on_resolvehost_start | ||
|
||
@property | ||
def on_request_resolvehost_end(self): | ||
return self._connector.on_resolvehost_end | ||
|
||
@property | ||
def on_request_dnscache_hit(self): | ||
return self._connector.on_dnscache_hit | ||
|
||
@property | ||
def on_request_dnscache_miss(self): | ||
return self._connector.on_dnscache_miss | ||
|
||
# req resp signals | ||
|
||
@property | ||
def on_request_headers_sent(self): | ||
return self._on_request_headers_sent | ||
|
||
@property | ||
def on_request_content_sent(self): | ||
return self._on_request_content_sent | ||
|
||
@property | ||
def on_request_headers_received(self): | ||
return self._on_request_headers_received | ||
|
||
@property | ||
def on_request_content_received(self): | ||
return self._on_request_content_received | ||
|
||
def detach(self): | ||
"""Detach connector from session without closing the former. | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the API is wrong here.
User will never send a
trace_context
intoasync with session.get()
explicitly.It's another level of abstraction.
What the user will do is setting up session properly on initialization stage by substribing on signals and (optionally) providing a trace context factory for creating a new container for user data.
I even doubt if we need a factory parameter, at least at current stage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rationale behind this implementation is the following one:
Give the proper freedom to the developer to have a grain control of his requests calls building trace context for each request.
Perhaps
This example shows how the same ClientSession is used to make different queries that might have divergent traces.
In case the user is keen on share information between all requests that belong to the same
ClientSession
might use a closure pattern, perhaps:I can see that your point about forcing the user to populate each request call can be less kindy, but from my experience have the way to pass a context that has information about the current execution is a must. Also, take into account that having this granularity allows the user to implement Session or Requests context.
Another solution would pass for implement two different contexts, one for the session and another one for the request. But IMHO overcomplicates right now the implementation.
Thoughts ?