-
Notifications
You must be signed in to change notification settings - Fork 524
Reduce StripeClient() cold start latency for serverless environments #1834
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
base: master
Are you sure you want to change the base?
Changes from all commits
8cf83cd
f81e575
38ef34d
759d95a
bc2901a
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 |
|---|---|---|
|
|
@@ -65,61 +65,11 @@ def _now_ms(): | |
|
|
||
|
|
||
| def new_default_http_client(*args: Any, **kwargs: Any) -> "HTTPClient": | ||
| """ | ||
| This method creates and returns a new HTTPClient based on what libraries are available. It uses the following precedence rules: | ||
|
|
||
| 1. Urlfetch (this is provided by Google App Engine, so if it's present you probably want it) | ||
| 2. Requests (popular library, the top priority for all environments outside Google App Engine, but not always present) | ||
| 3. Pycurl (another library, not always present, not as preferred as Requests but at least it verifies SSL certs) | ||
| 4. urllib with a warning (basically always present, a reasonable final default) | ||
|
|
||
| For performance, it only imports what it's actually going to use. But, it re-calculates every time its called, so probably save its result instead of calling it multiple times. | ||
| """ | ||
| try: | ||
| from google.appengine.api import urlfetch # type: ignore # noqa: F401 | ||
| except ImportError: | ||
| pass | ||
| else: | ||
| return UrlFetchClient(*args, **kwargs) | ||
|
|
||
| try: | ||
| import requests # noqa: F401 | ||
| except ImportError: | ||
| pass | ||
| else: | ||
| return RequestsClient(*args, **kwargs) | ||
|
|
||
| try: | ||
| import pycurl # type: ignore # noqa: F401 | ||
| except ImportError: | ||
| pass | ||
| else: | ||
| return PycurlClient(*args, **kwargs) | ||
|
|
||
| return UrllibClient(*args, **kwargs) | ||
| return _default_sync_client(*args, **kwargs) | ||
|
|
||
|
|
||
| def new_http_client_async_fallback(*args: Any, **kwargs: Any) -> "HTTPClient": | ||
| """ | ||
| Similar to `new_default_http_client` above, this returns a client that can handle async HTTP requests, if available. | ||
| """ | ||
|
|
||
| try: | ||
| import httpx # noqa: F401 | ||
| import anyio # noqa: F401 | ||
| except ImportError: | ||
| pass | ||
| else: | ||
| return HTTPXClient(*args, **kwargs) | ||
|
|
||
| try: | ||
| import aiohttp # noqa: F401 | ||
| except ImportError: | ||
| pass | ||
| else: | ||
| return AIOHTTPClient(*args, **kwargs) | ||
|
|
||
| return NoImportFoundAsyncClient(*args, **kwargs) | ||
| return _default_async_client(*args, **kwargs) | ||
|
|
||
|
|
||
| class HTTPClient(object): | ||
|
|
@@ -1550,3 +1500,74 @@ async def request_stream_async( | |
|
|
||
| async def close_async(self): | ||
| self.raise_async_client_import_error() | ||
|
|
||
|
|
||
| # --- Client resolution --- | ||
| # Detect available HTTP libraries at module load time so the expensive imports | ||
| # (e.g. requests, httpx) happen during Python's init phase rather than when | ||
| # StripeClient() is constructed. This matters in environments like AWS Lambda | ||
| # where module loading has a generous timeout (10s) but handler invocation | ||
| # does not (often 3s). | ||
| # | ||
| # Sync client precedence: | ||
| # 1. Urlfetch (Google App Engine — if present, you probably want it) | ||
| # 2. Requests (popular, top priority outside GAE) | ||
| # 3. Pycurl (verifies SSL certs, but less preferred than Requests) | ||
| # 4. urllib (stdlib fallback, basically always present) | ||
| # | ||
| # Async client precedence: | ||
| # 1. httpx + anyio (both required) | ||
| # 2. aiohttp | ||
| # 3. NoImportFoundAsyncClient (raises on use) | ||
| # | ||
| # To add a new client: define the class above, then add it to the appropriate | ||
| # cascade below. The resolved class is stored directly — new_default_http_client() | ||
| # and new_http_client_async_fallback() just call it. | ||
|
|
||
|
|
||
| def _resolve_sync_client(): | ||
| try: | ||
|
Member
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 was digging into this a little because I was worried that reintroducing this import chain to the hot path would undo the problems we were solving in #1427. Those users and the lambda one have sort of opposite goals- a hot reloading django server wants to defer imports as much as possible and lambda wants to frontload as much as it can. So we want to be careful to balance the needs of both users. I think the httplib checks are cheap enough that putting them back in the hot path isn't the end of the world. But, we might be able to have our cake and eat it too! There's a performance cost to using from importlib.util import find_spec
find_spec('requests') # ModuleSpec(name='requests', loader=<...
find_spec('missing') # NoneSo I think the fix here is to rewrite this function (and the async one) to check importability of libraries and then return the correct class (to be instantiated as needed). The downside to this is that inside the clients themselves, they re-import their respective libraries: class RequestsClient(HTTPClient):
name = "requests"
def __init__(
self,
...
_lib=None, # used for internal unit testing
):
...
if _lib is None:
import requests
_lib = requests
self.requests = _libThis is ~free when we've already successfully run a So! I would maybe use The other option is to control this behavior with an env var, since the two groups want opposite behavior and you can't otherwise change the behavior of |
||
| from google.appengine.api import urlfetch # type: ignore # noqa: F401 | ||
|
|
||
| return UrlFetchClient | ||
| except ImportError: | ||
| pass | ||
|
|
||
| try: | ||
| import requests # noqa: F401 | ||
|
|
||
| return RequestsClient | ||
| except ImportError: | ||
| pass | ||
|
|
||
| try: | ||
| import pycurl # type: ignore # noqa: F401 | ||
|
|
||
| return PycurlClient | ||
| except ImportError: | ||
| pass | ||
|
|
||
| return UrllibClient | ||
|
|
||
|
|
||
| def _resolve_async_client(): | ||
| try: | ||
| import httpx # noqa: F401 | ||
| import anyio # noqa: F401 | ||
|
|
||
| return HTTPXClient | ||
| except ImportError: | ||
| pass | ||
|
|
||
| try: | ||
| import aiohttp # noqa: F401 | ||
|
|
||
| return AIOHTTPClient | ||
| except ImportError: | ||
| pass | ||
|
|
||
| return NoImportFoundAsyncClient | ||
|
|
||
|
|
||
| _default_sync_client = _resolve_sync_client() | ||
|
Member
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 had a PR comment saying we should pull these because it wasn't obvious what they were doing- I didn't clock that |
||
| _default_async_client = _resolve_async_client() | ||
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.
Given that we presumably pay the cost to import
uuidduring import phase not runtime phase, does this change affect the bottom line much?Also it's a little dumb, but it would be nice if we could re-insert the dashes so it looks like a UUID. It's useful for readability in workbench.
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.
Technically yes, but I was seeing ~1.8s for importing uuid in a CPU-constrained docker, which is just under 20% of the init phase budget. I think it would be better to keep uuid out if we can here, but I can also see about adding the dashes back in.