Skip to content

Commit 0e67002

Browse files
authored
Merge pull request #192 from malmeloo/fix/blocking-http-call
feat: do not block event loop on local Anisette init
2 parents 7e8e032 + 6fc280e commit 0e67002

File tree

1 file changed

+60
-19
lines changed

1 file changed

+60
-19
lines changed

findmy/reports/anisette.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import base64
67
import locale
78
import logging
@@ -31,7 +32,7 @@ class LocalAnisetteMapping(TypedDict):
3132
"""JSON mapping representing state of a local Anisette provider."""
3233

3334
type: Literal["aniLocal"]
34-
prov_data: str
35+
prov_data: str | None
3536

3637

3738
AnisetteMapping = Union[RemoteAnisetteMapping, LocalAnisetteMapping]
@@ -286,7 +287,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, util.abc.Serializable[LocalAni
286287
def __init__(
287288
self,
288289
*,
289-
state_blob: BinaryIO | None = None,
290+
state_blob: BytesIO | None = None,
290291
libs_path: str | Path | None = None,
291292
) -> None:
292293
"""Initialize the provider."""
@@ -295,41 +296,75 @@ def __init__(
295296
if isinstance(libs_path, str):
296297
libs_path = Path(libs_path)
297298

298-
if libs_path is None or not libs_path.is_file():
299+
# we do not yet initialize Anisette in order to prevent blocking the event loop,
300+
# since the anisette library will download the required libraries synchronously.
301+
self._ani: Anisette | None = None
302+
303+
self._ani_data: AnisetteHeaders | None = None
304+
self._libs_path: Path | None = libs_path
305+
self._state_blob: BytesIO | None = state_blob
306+
307+
@property
308+
def _is_new_session(self) -> bool:
309+
return self._state_blob is None
310+
311+
async def _get_ani(self) -> Anisette:
312+
if self._ani is not None:
313+
return self._ani
314+
315+
if self._libs_path is None or not self._libs_path.is_file():
299316
logger.info(
300317
"The Anisette engine will download libraries required for operation, "
301318
"this may take a few seconds...",
302319
)
303-
if libs_path is None:
320+
if self._libs_path is None:
304321
logger.info(
305322
"To speed up future local Anisette initializations, "
306323
"provide a filesystem path to load the libraries from.",
307324
)
308325

309326
files: list[BinaryIO | Path] = []
310-
if state_blob is not None:
311-
files.append(state_blob)
312-
if libs_path is not None and libs_path.exists():
313-
files.append(libs_path)
327+
if self._state_blob is not None:
328+
files.append(self._state_blob)
329+
if self._libs_path is not None and self._libs_path.exists():
330+
files.append(self._libs_path)
314331

315-
self._ani = Anisette.load(*files)
316-
self._ani_data: AnisetteHeaders | None = None
317-
self._libs_path: Path | None = libs_path
332+
loop = asyncio.get_running_loop()
333+
ani = await loop.run_in_executor(None, Anisette.load, *files)
334+
is_provisioned = await loop.run_in_executor(None, lambda: ani.is_provisioned)
335+
336+
if self._libs_path is not None:
337+
ani.save_libs(self._libs_path)
318338

319-
if libs_path is not None:
320-
self._ani.save_libs(libs_path)
321-
if state_blob is not None and not self._ani.is_provisioned:
339+
if not self._is_new_session and not is_provisioned:
322340
logger.warning(
323341
"The Anisette state that was loaded has not yet been provisioned. "
324342
"Was the previous session saved properly?",
325343
)
326344

345+
# pre-provision to ensure that the VM has initialized
346+
await loop.run_in_executor(None, ani.provision)
347+
348+
self._ani = ani
349+
return ani
350+
327351
@override
328352
def to_json(self, dst: str | Path | None = None, /) -> LocalAnisetteMapping:
329353
"""See :meth:`BaseAnisetteProvider.serialize`."""
330-
with BytesIO() as buf:
331-
self._ani.save_provisioning(buf)
332-
prov_data = base64.b64encode(buf.getvalue()).decode("utf-8")
354+
if self._ani is None:
355+
# Anisette has not been called yet, so the future has not yet resolved.
356+
# We don't want to wait here, so we just return the original state blob.
357+
# If the state blob is None, this means we have a new session that has not
358+
# been provisioned yet, so we will not save the provisioning data.
359+
if self._state_blob is None:
360+
prov_data = None
361+
else:
362+
prov_data = base64.b64encode(self._state_blob.getvalue()).decode("utf-8")
363+
else:
364+
# Anisette has been initialized, so we can save the provisioning data.
365+
with BytesIO() as buf:
366+
self._ani.save_provisioning(buf)
367+
prov_data = base64.b64encode(buf.getvalue()).decode("utf-8")
333368

334369
return util.files.save_and_return_json(
335370
{
@@ -352,7 +387,8 @@ def from_json(
352387

353388
assert val["type"] == "aniLocal"
354389

355-
state_blob = BytesIO(base64.b64decode(val["prov_data"]))
390+
prov_data = val["prov_data"]
391+
state_blob = None if prov_data is None else BytesIO(base64.b64decode(prov_data))
356392

357393
return cls(state_blob=state_blob, libs_path=libs_path)
358394

@@ -365,7 +401,12 @@ async def get_headers(
365401
with_client_info: bool = False,
366402
) -> dict[str, str]:
367403
"""See :meth:`BaseAnisetteProvider.get_headers`."""
368-
self._ani_data = self._ani.get_data()
404+
ani = await self._get_ani()
405+
406+
# run in executor to prevent blocking the event loop,
407+
# since get_data may make blocking network requests.
408+
loop = asyncio.get_running_loop()
409+
self._ani_data = await loop.run_in_executor(None, ani.get_data)
369410

370411
return await super().get_headers(user_id, device_id, serial, with_client_info)
371412

0 commit comments

Comments
 (0)