screen-locker/screen_locker/_bonus_lock.py
Krzysztof kuhy Rudnicki d1817675c4 earner-bonus: add the new modules, unit and tests missed by 7851d59
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 20:44:01 +02:00

39 lines
1.3 KiB
Python

"""One writer at a time for the shutdown bonus state.
The daily reset and the live bonus pass both read ``shutdown_base.json``,
push ``/etc/shutdown-schedule.conf`` with a read-add-write and stamp the file.
They run from three processes -- the locker at login, the 15-minute
``workout-sync`` pass and the ledger-triggered ``earner-bonus`` pass -- so two
of them can see the same unstamped earner and each add its time. Holding this
lock across check, write and stamp makes the second one see the stamp.
A dedicated file, not the state file itself: ``_save_state`` truncates that.
"""
from __future__ import annotations
from contextlib import contextmanager
import fcntl
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
__all__ = ["bonus_lock"]
@contextmanager
def bonus_lock(state_file: Path) -> Iterator[None]:
"""Hold an exclusive lock beside *state_file* for the ``with`` body.
Blocks until any other holder is done; the passes it serialises take a
second or two, so waiting is cheaper than skipping a bonus.
"""
with state_file.with_suffix(".lock").open("a") as handle:
fcntl.flock(handle, fcntl.LOCK_EX)
try:
yield
finally:
fcntl.flock(handle, fcntl.LOCK_UN)