mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
better water
This commit is contained in:
parent
6cc9ca9674
commit
8a0c9a4c13
@ -114,30 +114,37 @@ class Lock(LazyLogging):
|
||||
return
|
||||
self._pid_file = self.path.open("a+")
|
||||
|
||||
def _watch(self) -> None:
|
||||
def _watch(self) -> bool:
|
||||
"""
|
||||
watch until lock disappear
|
||||
|
||||
Returns:
|
||||
bool: True in case if file is locked and False otherwise
|
||||
"""
|
||||
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
||||
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
||||
# but platform-specific, and we only need to check if file exists
|
||||
if self._pid_file is None:
|
||||
return
|
||||
return False
|
||||
|
||||
waiter = Waiter(self.wait_timeout)
|
||||
waiter.wait(lambda fd: not self.perform_lock(fd), self._pid_file.fileno())
|
||||
return bool(waiter.wait(lambda fd: not self.perform_lock(fd), self._pid_file.fileno()))
|
||||
|
||||
def _write(self) -> None:
|
||||
def _write(self, *, is_locked: bool = False) -> None:
|
||||
"""
|
||||
write pid to the lock file
|
||||
|
||||
Args:
|
||||
is_locked(bool, optional): indicates if file was already locked or not (Default value = False)
|
||||
|
||||
Raises:
|
||||
DuplicateRunError: if it cannot lock PID file
|
||||
"""
|
||||
if self._pid_file is None:
|
||||
return
|
||||
if not self.perform_lock(self._pid_file.fileno()):
|
||||
raise DuplicateRunError
|
||||
if not is_locked:
|
||||
if not self.perform_lock(self._pid_file.fileno()):
|
||||
raise DuplicateRunError
|
||||
|
||||
self._pid_file.seek(0) # reset position and remove file content if any
|
||||
self._pid_file.truncate()
|
||||
@ -182,8 +189,8 @@ class Lock(LazyLogging):
|
||||
if self.force: # remove lock if force flag is set
|
||||
self.clear()
|
||||
self._open()
|
||||
self._watch()
|
||||
self._write()
|
||||
is_locked = self._watch()
|
||||
self._write(is_locked=is_locked)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
"""
|
||||
|
@ -21,27 +21,87 @@ import time
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import ParamSpec
|
||||
from typing import Literal, ParamSpec
|
||||
|
||||
|
||||
Params = ParamSpec("Params")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WaiterResult:
|
||||
"""
|
||||
representation of a waiter result. This class should not be used directly, use derivatives instead
|
||||
|
||||
Attributes:
|
||||
took(float): consumed time in seconds
|
||||
"""
|
||||
|
||||
took: float
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""
|
||||
indicates whether the waiter completed with success or not
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __float__(self) -> float:
|
||||
"""
|
||||
extract time spent to retrieve the result in seconds
|
||||
|
||||
Returns:
|
||||
float: consumed time in seconds
|
||||
"""
|
||||
return self.took
|
||||
|
||||
|
||||
class WaiterTaskFinished(WaiterResult):
|
||||
"""
|
||||
a waiter result used to notify that the task has been completed successfully
|
||||
"""
|
||||
|
||||
def __bool__(self) -> Literal[True]:
|
||||
"""
|
||||
indicates whether the waiter completed with success or not
|
||||
|
||||
Returns:
|
||||
Literal[True]: always False
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class WaiterTimedOut(WaiterResult):
|
||||
"""
|
||||
a waiter result used to notify that the waiter run out of time
|
||||
"""
|
||||
|
||||
def __bool__(self) -> Literal[False]:
|
||||
"""
|
||||
indicates whether the waiter completed with success or not
|
||||
|
||||
Returns:
|
||||
Literal[False]: always False
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Waiter:
|
||||
"""
|
||||
simple waiter implementation
|
||||
|
||||
Attributes:
|
||||
interval(int): interval in seconds between checks
|
||||
interval(float): interval in seconds between checks
|
||||
start_time(float): monotonic time of the waiter start. More likely must not be assigned explicitly
|
||||
wait_timeout(int): timeout in seconds to wait for. Negative value will result in immediate exit. Zero value
|
||||
wait_timeout(float): timeout in seconds to wait for. Negative value will result in immediate exit. Zero value
|
||||
means infinite timeout
|
||||
"""
|
||||
|
||||
wait_timeout: int
|
||||
wait_timeout: float
|
||||
start_time: float = field(default_factory=time.monotonic, kw_only=True)
|
||||
interval: int = field(default=10, kw_only=True)
|
||||
interval: float = field(default=10, kw_only=True)
|
||||
|
||||
def is_timed_out(self) -> bool:
|
||||
"""
|
||||
@ -51,10 +111,10 @@ class Waiter:
|
||||
bool: True in case current monotonic time is more than :attr:`start_time` and :attr:`wait_timeout`
|
||||
doesn't equal to 0
|
||||
"""
|
||||
since_start: float = time.monotonic() - self.start_time
|
||||
since_start = time.monotonic() - self.start_time
|
||||
return self.wait_timeout != 0 and since_start > self.wait_timeout
|
||||
|
||||
def wait(self, in_progress: Callable[Params, bool], *args: Params.args, **kwargs: Params.kwargs) -> float:
|
||||
def wait(self, in_progress: Callable[Params, bool], *args: Params.args, **kwargs: Params.kwargs) -> WaiterResult:
|
||||
"""
|
||||
wait until requirements are not met
|
||||
|
||||
@ -64,9 +124,12 @@ class Waiter:
|
||||
**kwargs(Params.kwargs): keyword arguments for check call
|
||||
|
||||
Returns:
|
||||
float: consumed time in seconds
|
||||
WaiterResult: consumed time in seconds
|
||||
"""
|
||||
while not self.is_timed_out() and in_progress(*args, **kwargs):
|
||||
while not (timed_out := self.is_timed_out()) and in_progress(*args, **kwargs):
|
||||
time.sleep(self.interval)
|
||||
took = time.monotonic() - self.start_time
|
||||
|
||||
return time.monotonic() - self.start_time
|
||||
if timed_out:
|
||||
return WaiterTimedOut(took)
|
||||
return WaiterTaskFinished(took)
|
||||
|
@ -98,7 +98,7 @@ def test_write(lock: Lock) -> None:
|
||||
"""
|
||||
with NamedTemporaryFile("a+") as pid_file:
|
||||
lock._pid_file = pid_file
|
||||
lock._write()
|
||||
lock._write(is_locked=False)
|
||||
|
||||
assert int(lock._pid_file.readline()) == os.getpid()
|
||||
|
||||
@ -107,7 +107,7 @@ def test_write_skip(lock: Lock) -> None:
|
||||
"""
|
||||
must skip write to file if no path set
|
||||
"""
|
||||
lock._write()
|
||||
lock._write(is_locked=False)
|
||||
|
||||
|
||||
def test_write_locked(lock: Lock, mocker: MockerFixture) -> None:
|
||||
@ -117,7 +117,18 @@ def test_write_locked(lock: Lock, mocker: MockerFixture) -> None:
|
||||
mocker.patch("ahriman.application.lock.Lock.perform_lock", return_value=False)
|
||||
with pytest.raises(DuplicateRunError):
|
||||
lock._pid_file = MagicMock()
|
||||
lock._write()
|
||||
lock._write(is_locked=False)
|
||||
|
||||
|
||||
def test_write_locked_before(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip lock in case if file was locked before
|
||||
"""
|
||||
lock_mock = mocker.patch("ahriman.application.lock.Lock.perform_lock")
|
||||
lock._pid_file = MagicMock()
|
||||
|
||||
lock._write(is_locked=True)
|
||||
lock_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_check_user(lock: Lock, mocker: MockerFixture) -> None:
|
||||
@ -226,14 +237,14 @@ def test_lock(lock: Lock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
clear_mock = mocker.patch("ahriman.application.lock.Lock.clear")
|
||||
open_mock = mocker.patch("ahriman.application.lock.Lock._open")
|
||||
watch_mock = mocker.patch("ahriman.application.lock.Lock._watch")
|
||||
watch_mock = mocker.patch("ahriman.application.lock.Lock._watch", return_value=True)
|
||||
write_mock = mocker.patch("ahriman.application.lock.Lock._write")
|
||||
|
||||
lock.lock()
|
||||
clear_mock.assert_not_called()
|
||||
open_mock.assert_called_once_with()
|
||||
watch_mock.assert_called_once_with()
|
||||
write_mock.assert_called_once_with()
|
||||
write_mock.assert_called_once_with(is_locked=True)
|
||||
|
||||
|
||||
def test_lock_clear(lock: Lock, mocker: MockerFixture) -> None:
|
||||
|
@ -1,6 +1,36 @@
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from ahriman.models.waiter import Waiter
|
||||
from ahriman.models.waiter import Waiter, WaiterResult, WaiterTaskFinished, WaiterTimedOut
|
||||
|
||||
|
||||
def test_result_to_float() -> None:
|
||||
"""
|
||||
must convert waiter result to float
|
||||
"""
|
||||
assert float(WaiterResult(4.2)) == 4.2
|
||||
|
||||
|
||||
def test_result_not_implemented() -> None:
|
||||
"""
|
||||
must raise NotImplementedError for abstract class
|
||||
"""
|
||||
with pytest.raises(NotImplementedError):
|
||||
assert bool(WaiterResult(4.2))
|
||||
|
||||
|
||||
def test_result_success_to_bool() -> None:
|
||||
"""
|
||||
must convert success waiter result to bool
|
||||
"""
|
||||
assert bool(WaiterTaskFinished(4.2))
|
||||
|
||||
|
||||
def test_result_failure_to_bool() -> None:
|
||||
"""
|
||||
must convert failure waiter result to bool
|
||||
"""
|
||||
assert not bool(WaiterTimedOut(4.2))
|
||||
|
||||
|
||||
def test_is_timed_out() -> None:
|
||||
@ -22,8 +52,26 @@ def test_is_timed_out_infinite() -> None:
|
||||
|
||||
def test_wait() -> None:
|
||||
"""
|
||||
must wait until file will disappear
|
||||
must wait for success result
|
||||
"""
|
||||
results = iter([True, False])
|
||||
waiter = Waiter(1, interval=1)
|
||||
assert waiter.wait(lambda: next(results)) > 0
|
||||
waiter = Waiter(1, interval=0.1)
|
||||
assert float(waiter.wait(lambda: next(results))) > 0
|
||||
|
||||
|
||||
def test_wait_timeout() -> None:
|
||||
"""
|
||||
must return WaiterTimedOut on timeout
|
||||
"""
|
||||
results = iter([True, False])
|
||||
waiter = Waiter(-1, interval=0.1)
|
||||
assert isinstance(waiter.wait(lambda: next(results)), WaiterTimedOut)
|
||||
|
||||
|
||||
def test_wait_success() -> None:
|
||||
"""
|
||||
must return WaiterTaskFinished on success
|
||||
"""
|
||||
results = iter([True, False])
|
||||
waiter = Waiter(1, interval=0.1)
|
||||
assert isinstance(waiter.wait(lambda: next(results)), WaiterTaskFinished)
|
||||
|
Loading…
Reference in New Issue
Block a user