mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
A real StrongLifts session on 2026-08-10 never reached this machine, so it had to be logged as a manual workout -- spending a manual budget slot on a workout that was genuinely verified. The session was in Firebase the whole time; four separate defects each hid it. - pull_synced_workout() read a hardcoded devices/phone/log.json. Sinceffc6c72the phone pushes under a per-install uuid, so that directory has been frozen since 2026-07-17 and the PC kept returning a stale session from 07-27 as if it were current. Both readers now walk EVERY device directory; reinstalls keep minting new ones, so no single path can be right for long. - A session is identified by SHAPE (it has `exercises`), not by a `kind` field the phone never writes. That filter is why the one reader that did walk every directory discarded real sessions, and it also kept PC-origin runnerup/phone_verified records from being handed to a caller expecting a session. - FirebaseSyncError is a SIBLING of GitHubSyncError, not a subclass, so `except GitHubSyncError` never caught it and a DNS failure took the whole locker down (status=1/FAILURE, then a restart loop). Catch the common parent; a pull failure degrades to a warning. - A GitHub token is no longer required to sync. Firebase has been primary sinceeb4ff01, so a Firebase-only machine was reporting "sync is OFF" while syncing fine. Sync also only ran once per locker start, so a workout finished after login was invisible until the next login. workout-sync.timer runs the new headless --sync-only every 15 minutes; remove_systemd.sh now removes every unit the installer creates, not just two of them. workout_app: the 2026-08-05 reinstall wiped progression because the DB seeds factory defaults before the backup is restored, and the first write then exported those defaults over the only off-device copy. Storage permission is now settled before the DB opens, a defaults-only DB refuses to overwrite a richer backup, and the three swallowed storage errors say what broke. Sessions that exist only in Firebase/GitHub are restored into local history, so a reinstalled app has the history it actually earned rather than starting at whatever survived. Matched on (date, start_time), so re-syncing never duplicates and local rows always win. Adds a target-reps editor: progression can only ever RAISE reps, so a rep target a defaults re-seed set wrong could not be corrected from the UI at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JS5V44jkHZiH1wC7BH5h5n
101 lines
3.5 KiB
Python
101 lines
3.5 KiB
Python
"""Bounded retry for sync calls that run before the network is up.
|
|
|
|
Every sync entry point here is reached from the morning routine, which systemd
|
|
starts immediately after boot and after resume — routinely a few seconds before
|
|
DHCP/DNS have settled. A single-shot call in that window fails for a reason
|
|
that has nothing to do with the sync itself, and (before this module) reported
|
|
it as a token/permissions problem.
|
|
|
|
Retrying at the application layer rather than ordering the unit after
|
|
``network-online.target``: the same race happens on *resume*, where boot
|
|
ordering does not apply, and the user services involved cannot depend on a
|
|
system target anyway.
|
|
|
|
Any :class:`RemoteSyncError` is retried, deliberately including
|
|
:class:`RepoNotFoundError`. Catching the common parent rather than
|
|
:class:`GitHubSyncError` matters since the Firebase cutover:
|
|
:class:`FirebaseSyncError` is a *sibling* of :class:`GitHubSyncError`, not a
|
|
subclass, so catching the latter let every Firebase failure escape uncaught and
|
|
take the whole locker down (``status=1/FAILURE``) instead of degrading to "no
|
|
synced workout". Classifying "transient" vs "permanent" would mean
|
|
depending on which exception ``crdt_sync`` chains where, which is internal to
|
|
that library and has changed before; a few wasted seconds on a genuinely
|
|
misconfigured token is a much cheaper mistake than skipping a real retry.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from time import sleep
|
|
from typing import TYPE_CHECKING, TypeVar
|
|
|
|
from crdt_sync import RemoteSyncError
|
|
|
|
from screen_locker._constants import (
|
|
SYNC_RETRY_ATTEMPTS,
|
|
SYNC_RETRY_DELAY_SECONDS,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def with_sync_retry(
|
|
operation: Callable[[], T],
|
|
*,
|
|
description: str,
|
|
attempts: int = SYNC_RETRY_ATTEMPTS,
|
|
delay_seconds: float = SYNC_RETRY_DELAY_SECONDS,
|
|
) -> T:
|
|
"""Run *operation*, retrying a failed sync with exponential backoff.
|
|
|
|
Args:
|
|
operation: The zero-argument sync call to run.
|
|
description: Human phrase naming the operation, used in the retry
|
|
logs (e.g. ``"push PC workouts"``).
|
|
attempts: Total tries, including the first. Values below 1 still run
|
|
the operation exactly once.
|
|
delay_seconds: Base backoff; attempt *n* waits ``delay * 2**(n-1)``.
|
|
|
|
Returns:
|
|
Whatever *operation* returns.
|
|
|
|
Raises:
|
|
RemoteSyncError: If every attempt failed. The caller keeps its own
|
|
handling — this only buys time, it never hides the outcome.
|
|
"""
|
|
last_attempt = max(attempts, 1)
|
|
# Every attempt but the last swallows-and-waits; the last one is run
|
|
# outside the loop so its failure propagates with no unreachable branch.
|
|
for attempt in range(1, last_attempt):
|
|
try:
|
|
return operation()
|
|
except RemoteSyncError as exc:
|
|
wait = delay_seconds * (2 ** (attempt - 1))
|
|
_logger.warning(
|
|
"Could not %s (attempt %d/%d): %s — retrying in %.0fs; "
|
|
"this is normal right after boot or resume, when the "
|
|
"network is not up yet",
|
|
description,
|
|
attempt,
|
|
last_attempt,
|
|
exc,
|
|
wait,
|
|
)
|
|
sleep(wait)
|
|
|
|
try:
|
|
return operation()
|
|
except RemoteSyncError as exc:
|
|
_logger.warning(
|
|
"Could not %s after %d attempt(s): %s — giving up",
|
|
description,
|
|
last_attempt,
|
|
exc,
|
|
)
|
|
raise
|