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 dataclasses import dataclass, replace
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
@ -44,6 +45,8 @@ CLAIMS_DIR: Final = Path("/run/user/1000/gatelock/claims")
|
||||
_MAX_AGE_SECONDS: Final = 15
|
||||
FAILCLOSED_KEY: Final = "failclosed:watcher"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
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.
|
||||
|
||||
Returns:
|
||||
The tick's shadow verdict.
|
||||
The tick's shadow verdict; the fail-closed one if an input raised.
|
||||
"""
|
||||
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)
|
||||
# The enforce loop has no per-tick guard, so a raise here would crash-loop
|
||||
# the daemon and stop *enforced* billing. An unreadable input bills, unpaused.
|
||||
try:
|
||||
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))
|
||||
|
||||
|
||||
|
||||
@ -85,6 +85,19 @@ class TestObserveShadow:
|
||||
assert verdict.keys == ("domain:youtube.com",)
|
||||
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:
|
||||
assert str(_shadow.NOW_PATH) == "/run/user/1000/leisure-watch/now.json"
|
||||
assert str(_shadow.CLAIMS_DIR) == "/run/user/1000/gatelock/claims"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user