mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 19:27:04 +01:00
shadow: fail closed instead of raising on unreadable lock/fact inputs
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
The enforce loop has no per-tick guard, so an OSError/ValueError from paused_by or read_now would crash-loop the daemon and stop enforced billing. Such a tick now bills shadow time unpaused (Q8) and logs a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
This commit is contained in:
parent
4cf77b61c6
commit
79e5e9fc26
@ -24,6 +24,7 @@ Overlapping sources bill the wall clock once and split it evenly, so
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
@ -44,6 +45,8 @@ CLAIMS_DIR: Final = Path("/run/user/1000/gatelock/claims")
|
|||||||
_MAX_AGE_SECONDS: Final = 15
|
_MAX_AGE_SECONDS: Final = 15
|
||||||
FAILCLOSED_KEY: Final = "failclosed:watcher"
|
FAILCLOSED_KEY: Final = "failclosed:watcher"
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ShadowVerdict:
|
class ShadowVerdict:
|
||||||
@ -90,12 +93,18 @@ def observe_shadow(owners: dict[int, str], *, stamp: float) -> ShadowVerdict:
|
|||||||
stamp: Epoch seconds of this tick, the freshness reference.
|
stamp: Epoch seconds of this tick, the freshness reference.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The tick's shadow verdict.
|
The tick's shadow verdict; the fail-closed one if an input raised.
|
||||||
"""
|
"""
|
||||||
paused = paused_by(CLAIMS_DIR)
|
# The enforce loop has no per-tick guard, so a raise here would crash-loop
|
||||||
if paused is not None:
|
# the daemon and stop *enforced* billing. An unreadable input bills, unpaused.
|
||||||
return ShadowVerdict(paused_by=paused, keys=())
|
try:
|
||||||
fact = read_now(NOW_PATH, stamp, max_age=_MAX_AGE_SECONDS)
|
paused = paused_by(CLAIMS_DIR)
|
||||||
|
if paused is not None:
|
||||||
|
return ShadowVerdict(paused_by=paused, keys=())
|
||||||
|
fact = read_now(NOW_PATH, stamp, max_age=_MAX_AGE_SECONDS)
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
logger.warning("Shadow inputs unreadable (%s); failing closed.", exc)
|
||||||
|
fact = None
|
||||||
return ShadowVerdict(paused_by=None, keys=shadow_keys(owners, fact))
|
return ShadowVerdict(paused_by=None, keys=shadow_keys(owners, fact))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,19 @@ class TestObserveShadow:
|
|||||||
assert verdict.keys == ("domain:youtube.com",)
|
assert verdict.keys == ("domain:youtube.com",)
|
||||||
read.assert_called_once_with(_shadow.NOW_PATH, T0, max_age=15)
|
read.assert_called_once_with(_shadow.NOW_PATH, T0, max_age=15)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("where", ["paused_by", "read_now"])
|
||||||
|
def test_a_failing_input_fails_closed_instead_of_raising(self, where: str) -> None:
|
||||||
|
"""The enforce loop has no per-tick guard: a raise would crash-loop it."""
|
||||||
|
with (
|
||||||
|
patch.object(_shadow, "paused_by", return_value=None),
|
||||||
|
patch.object(_shadow, "read_now", return_value=None),
|
||||||
|
patch.object(_shadow, where, side_effect=PermissionError("denied")),
|
||||||
|
):
|
||||||
|
assert observe_shadow({1: "app:440"}, stamp=T0) == ShadowVerdict(
|
||||||
|
paused_by=None, keys=("app:440",)
|
||||||
|
)
|
||||||
|
assert observe_shadow({}, stamp=T0).keys == (FAILCLOSED_KEY,)
|
||||||
|
|
||||||
def test_root_paths_are_the_desktop_users(self) -> None:
|
def test_root_paths_are_the_desktop_users(self) -> None:
|
||||||
assert str(_shadow.NOW_PATH) == "/run/user/1000/leisure-watch/now.json"
|
assert str(_shadow.NOW_PATH) == "/run/user/1000/leisure-watch/now.json"
|
||||||
assert str(_shadow.CLAIMS_DIR) == "/run/user/1000/gatelock/claims"
|
assert str(_shadow.CLAIMS_DIR) == "/run/user/1000/gatelock/claims"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user