diff --git a/README.md b/README.md index 630507f..b369b77 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,17 @@ you want; importing the package never fails because an extra is missing. `requirements.txt` (pick the extra you need): ``` -aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.0 -aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.0 -aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.0 +aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1 +aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1 +aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1 ``` Direct: ```bash -pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.0" -pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.0" -pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.0" +pip install "aioweb_tls[curl] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1" +pip install "aioweb_tls[noble] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1" +pip install "aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1" ``` - `[curl]` → curl_cffi backend · `[noble]` → noble_tls backend · `[all]` → both. @@ -73,8 +73,8 @@ async with TLSSession(backend=Noble(client="chrome_133")) as s: - `Noble(client="chrome_133")` — accepts a `noble_tls.Client` enum or a string name. - noble_tls downloads a Go shared library on first use. `await s.setup()` fetches it - once at startup; if you skip it, the first request fetches it lazily (guarded to run - once). + once at startup; if you skip it, the first request fetches it lazily. The fetch is + guarded by a lock, so even concurrent first requests download it exactly once. ## Writing your own backend (the `TLSBackend` protocol) diff --git a/src/aioweb_tls/backends.py b/src/aioweb_tls/backends.py index f87a834..be56b88 100644 --- a/src/aioweb_tls/backends.py +++ b/src/aioweb_tls/backends.py @@ -11,6 +11,7 @@ is not installed raises a clear RuntimeError naming the extra to install. import this module never fails because an extra is missing. """ +import asyncio import logging import math @@ -136,6 +137,7 @@ class Noble: ) from _NOBLE_ERROR self.client = self._resolve_client(client) self._updated = False + self._setup_lock = asyncio.Lock() @staticmethod def _resolve_client(client): @@ -145,20 +147,27 @@ class Noble: return client async def setup(self) -> None: - """fetch the noble_tls Go shared library once; idempotent + """fetch the noble_tls Go shared library once; idempotent and concurrency-safe download_if_necessary handles the first-time fetch (no lib present); update_if_necessary refreshes an existing one. try download first so a clean environment works, falling back to update. + + guarded by an asyncio.Lock with a check-lock-recheck so concurrent first + requests don't both run the fetch: the fast path returns once _updated is + set, and only the first caller through the lock does the work. """ if self._updated: return - download = getattr(noble_tls, "download_if_necessary", None) - if download is not None: - await download() - else: - await noble_tls.update_if_necessary() - self._updated = True + async with self._setup_lock: + if self._updated: + return + download = getattr(noble_tls, "download_if_necessary", None) + if download is not None: + await download() + else: + await noble_tls.update_if_necessary() + self._updated = True def create_session(self, headers, timeout, **kwargs): """build the noble_tls Session"""