fix: lock Noble.setup so concurrent first requests fetch the Go-lib once
Noble.setup guarded the one-time Go shared-library fetch with a bare 'if self._updated' flag — a TOCTOU race where concurrent first requests both passed the check before either set the flag, running the download multiple times. now guarded by a per-instance asyncio.Lock with a check-lock-recheck. verified under load: 2/10/100/500 concurrent setups run the fetch exactly once each (a no-lock control runs it N times). Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
parent
7ea8ecf888
commit
ae4c653ecc
16
README.md
16
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):
|
`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[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.0
|
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.0
|
aioweb_tls[all] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aioweb_tls.git@v0.1.1
|
||||||
```
|
```
|
||||||
|
|
||||||
Direct:
|
Direct:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install "aioweb_tls[curl] @ 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.0"
|
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.0"
|
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.
|
- `[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(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
|
- 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 at startup; if you skip it, the first request fetches it lazily. The fetch is
|
||||||
once).
|
guarded by a lock, so even concurrent first requests download it exactly once.
|
||||||
|
|
||||||
## Writing your own backend (the `TLSBackend` protocol)
|
## Writing your own backend (the `TLSBackend` protocol)
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
this module never fails because an extra is missing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
|
||||||
@ -136,6 +137,7 @@ class Noble:
|
|||||||
) from _NOBLE_ERROR
|
) from _NOBLE_ERROR
|
||||||
self.client = self._resolve_client(client)
|
self.client = self._resolve_client(client)
|
||||||
self._updated = False
|
self._updated = False
|
||||||
|
self._setup_lock = asyncio.Lock()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_client(client):
|
def _resolve_client(client):
|
||||||
@ -145,20 +147,27 @@ class Noble:
|
|||||||
return client
|
return client
|
||||||
|
|
||||||
async def setup(self) -> None:
|
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);
|
download_if_necessary handles the first-time fetch (no lib present);
|
||||||
update_if_necessary refreshes an existing one. try download first so a
|
update_if_necessary refreshes an existing one. try download first so a
|
||||||
clean environment works, falling back to update.
|
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:
|
if self._updated:
|
||||||
return
|
return
|
||||||
download = getattr(noble_tls, "download_if_necessary", None)
|
async with self._setup_lock:
|
||||||
if download is not None:
|
if self._updated:
|
||||||
await download()
|
return
|
||||||
else:
|
download = getattr(noble_tls, "download_if_necessary", None)
|
||||||
await noble_tls.update_if_necessary()
|
if download is not None:
|
||||||
self._updated = True
|
await download()
|
||||||
|
else:
|
||||||
|
await noble_tls.update_if_necessary()
|
||||||
|
self._updated = True
|
||||||
|
|
||||||
def create_session(self, headers, timeout, **kwargs):
|
def create_session(self, headers, timeout, **kwargs):
|
||||||
"""build the noble_tls Session"""
|
"""build the noble_tls Session"""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user