Skip to content

iCloud calls queue behind a location fetch, because the shared loop is driven synchronously #126

Description

@parawanderer

What happens

Open Settings → check the items in your Apple account while a first import is still resolving
tags, and the screen sits on a spinner for minutes. Until recently it could not be left at all,
and it claimed to be "Looking for a device that can unlock it…" — for an account that had just
been linked and would never need a device.

The lock-in and the wording are fixed. The waiting is not, and this issue is about that.

Why

Every call into Python is serialised by PythonLock, and the reason given in its docstring is
that the iCloud flow and the location fetch share one Apple session, because two sessions would
be two devices to Apple (rule 11).

Sharing the session is not what forces the serialisation. Apple has no objection to two
concurrent requests from one device. What forces it is how the shared event loop is driven:

  • icloud_bridge.openSession reaches into account._evt_loop and reuses FindMy.py's own loop —
    correctly, so there is one session and one identity
  • but both sides then call loop.run_until_complete(...), which means take over this loop and
    block my thread until done
  • run_until_complete cannot be called on a loop that is already running — that is the
    RuntimeError: This event loop is already running the lock exists to prevent

So the serialisation is a property of using the synchronous facade over an async library, not
of the identity. Two network calls awaiting sockets on one loop is precisely what asyncio is for,
and it is being prevented by the calling convention rather than by anything Apple cares about.

It hurts most exactly where the app is slowest: on a first import, tags with no key alignment
record are searched across the full seven-day window, one at a time, and that can hold the lock
for minutes.

Shape of a fix

  • run the loop continuously on its own thread (run_forever) instead of entering it per call
  • submit work with asyncio.run_coroutine_threadsafe(coro, loop), which returns a future each
    caller can wait on independently
  • same account object, same identity, same session, one device — rule 11 untouched

Caveats, before anyone starts

  1. FindMy.py's async account has not been checked for concurrent use. Shared mutable state —
    the anisette provider, session-token refresh, ADI state — could race. Token refresh is the
    obvious hazard: two coroutines both finding an expired token and both re-provisioning. That
    likely needs a narrow lock around authentication specifically, which is a far smaller thing
    to hold than a lock around every call.
  2. It changes the concurrency model for every caller of the bridge at once, so it wants its own
    branch and its own tests rather than riding along with something else.
  3. The UI work already done — an honest caption while waiting, and being able to leave the screen
    — is still needed either way. There will always be a window where a call genuinely is in
    flight.

Not urgent

Nothing is broken by this; the app is slower and less responsive than it needs to be during one
specific window. Worth doing after 1.1.0 rather than in it.


🤖 Written by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    @appIssues regarding the OpenTagViewer Android appbugSomething isn't workinggood first issueGood for newcomers

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions