mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 15:27:17 +00:00
feat: add timer for metrics purposes
This commit is contained in:
parent
54a331cc96
commit
a9003993fa
@ -59,7 +59,7 @@ class Lock(LazyLogging):
|
|||||||
>>> configuration = Configuration()
|
>>> configuration = Configuration()
|
||||||
>>> try:
|
>>> try:
|
||||||
>>> with Lock(args, RepositoryId("x86_64", "aur-clone"), configuration):
|
>>> with Lock(args, RepositoryId("x86_64", "aur-clone"), configuration):
|
||||||
>>> perform_actions()
|
>>> do_something()
|
||||||
>>> except Exception as exception:
|
>>> except Exception as exception:
|
||||||
>>> handle_exceptions(exception)
|
>>> handle_exceptions(exception)
|
||||||
"""
|
"""
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import time
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
@ -28,6 +27,7 @@ from multiprocessing import Process, Queue
|
|||||||
from threading import Lock, Thread
|
from threading import Lock, Thread
|
||||||
|
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.models.metrics_timer import MetricsTimer
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.process_status import ProcessStatus
|
from ahriman.models.process_status import ProcessStatus
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
@ -90,11 +90,9 @@ class Spawn(Thread, LazyLogging):
|
|||||||
process_id(str): process unique identifier
|
process_id(str): process unique identifier
|
||||||
queue(Queue[ProcessStatus | None]): output queue
|
queue(Queue[ProcessStatus | None]): output queue
|
||||||
"""
|
"""
|
||||||
start_time = time.monotonic()
|
with MetricsTimer() as timer:
|
||||||
result = callback(args, repository_id)
|
result = callback(args, repository_id)
|
||||||
stop_time = time.monotonic()
|
consumed_time = timer.elapsed
|
||||||
|
|
||||||
consumed_time = int(1000 * (stop_time - start_time))
|
|
||||||
|
|
||||||
queue.put(ProcessStatus(process_id, result, consumed_time))
|
queue.put(ProcessStatus(process_id, result, consumed_time))
|
||||||
|
|
||||||
@ -271,7 +269,7 @@ class Spawn(Thread, LazyLogging):
|
|||||||
"""
|
"""
|
||||||
for terminated in iter(self.queue.get, None):
|
for terminated in iter(self.queue.get, None):
|
||||||
self.logger.info("process %s has been terminated with status %s, consumed time %ss",
|
self.logger.info("process %s has been terminated with status %s, consumed time %ss",
|
||||||
terminated.process_id, terminated.status, terminated.consumed_time / 1000)
|
terminated.process_id, terminated.status, terminated.consumed_time)
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
process = self.active.pop(terminated.process_id, None)
|
process = self.active.pop(terminated.process_id, None)
|
||||||
|
93
src/ahriman/models/metrics_timer.py
Normal file
93
src/ahriman/models/metrics_timer.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import time
|
||||||
|
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import Literal, Self
|
||||||
|
|
||||||
|
from ahriman.core.exceptions import InitializeError
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsTimer:
|
||||||
|
"""
|
||||||
|
metrics implementation
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
start_time(float | None): timer start time in monotonic time
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
This class implements simple timer which allows to measure the time elapsed of the function. Usually it should
|
||||||
|
be used like::
|
||||||
|
|
||||||
|
>>> with MetricsTimer() as timer:
|
||||||
|
>>> do_something()
|
||||||
|
>>> print("Time elapsed for first function: %f", timer.elapsed)
|
||||||
|
>>> do_something_different()
|
||||||
|
>>> print("Time elapsed for all functions: %f", timer.elapsed)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""
|
||||||
|
default constructor
|
||||||
|
"""
|
||||||
|
self.start_time: float | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def elapsed(self) -> float:
|
||||||
|
"""
|
||||||
|
get elapsed time since the start of the timer
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: time elapsed in seconds
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InitializeError: in case if timer was not initialized correctly
|
||||||
|
"""
|
||||||
|
if self.start_time is None:
|
||||||
|
raise InitializeError("Timer must be started in the context manager")
|
||||||
|
|
||||||
|
stop_time = time.monotonic()
|
||||||
|
consumed_time_ms = int(1000 * (stop_time - self.start_time))
|
||||||
|
return consumed_time_ms / 1000
|
||||||
|
|
||||||
|
def __enter__(self) -> Self:
|
||||||
|
"""
|
||||||
|
start timer context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: always instance of self
|
||||||
|
"""
|
||||||
|
self.start_time = time.monotonic()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type: type[Exception] | None, exc_val: Exception | None,
|
||||||
|
exc_tb: TracebackType) -> Literal[False]:
|
||||||
|
"""
|
||||||
|
finish timer context
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exc_type(type[Exception] | None): exception type name if any
|
||||||
|
exc_val(Exception | None): exception raised if any
|
||||||
|
exc_tb(TracebackType): exception traceback if any
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Literal[False]: always ``False`` (do not suppress any exception)
|
||||||
|
"""
|
||||||
|
return False
|
@ -28,9 +28,9 @@ class ProcessStatus:
|
|||||||
Attributes:
|
Attributes:
|
||||||
process_id(str): unique process identifier
|
process_id(str): unique process identifier
|
||||||
status(bool): process exit code status
|
status(bool): process exit code status
|
||||||
consumed_time(int): consumed time in ms
|
consumed_time(float): consumed time in seconds
|
||||||
"""
|
"""
|
||||||
|
|
||||||
process_id: str
|
process_id: str
|
||||||
status: bool
|
status: bool
|
||||||
consumed_time: int
|
consumed_time: float
|
||||||
|
@ -23,6 +23,8 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Literal, ParamSpec
|
from typing import Literal, ParamSpec
|
||||||
|
|
||||||
|
from ahriman.models.metrics_timer import MetricsTimer
|
||||||
|
|
||||||
|
|
||||||
Params = ParamSpec("Params")
|
Params = ParamSpec("Params")
|
||||||
|
|
||||||
@ -94,25 +96,25 @@ class Waiter:
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
interval(float): 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(float): 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
|
means infinite timeout
|
||||||
"""
|
"""
|
||||||
|
|
||||||
wait_timeout: float
|
wait_timeout: float
|
||||||
start_time: float = field(default_factory=time.monotonic, kw_only=True)
|
|
||||||
interval: float = field(default=10, kw_only=True)
|
interval: float = field(default=10, kw_only=True)
|
||||||
|
|
||||||
def is_timed_out(self) -> bool:
|
def is_timed_out(self, elapsed: float) -> bool:
|
||||||
"""
|
"""
|
||||||
check if timer is out
|
check if timer is out
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
elapsed(float): elapsed time in seconds
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: ``True`` in case current monotonic time is more than :attr:`start_time` and :attr:`wait_timeout`
|
bool: ``True`` in case current monotonic time is more than :attr:`start_time` and :attr:`wait_timeout`
|
||||||
doesn't equal to 0
|
doesn't equal to 0
|
||||||
"""
|
"""
|
||||||
since_start = time.monotonic() - self.start_time
|
return self.wait_timeout != 0 and elapsed > self.wait_timeout
|
||||||
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) -> WaiterResult:
|
def wait(self, in_progress: Callable[Params, bool], *args: Params.args, **kwargs: Params.kwargs) -> WaiterResult:
|
||||||
"""
|
"""
|
||||||
@ -126,9 +128,10 @@ class Waiter:
|
|||||||
Returns:
|
Returns:
|
||||||
WaiterResult: waiter result object
|
WaiterResult: waiter result object
|
||||||
"""
|
"""
|
||||||
while not (timed_out := self.is_timed_out()) and in_progress(*args, **kwargs):
|
with MetricsTimer() as timer:
|
||||||
|
while not (timed_out := self.is_timed_out(timer.elapsed)) and in_progress(*args, **kwargs):
|
||||||
time.sleep(self.interval)
|
time.sleep(self.interval)
|
||||||
took = time.monotonic() - self.start_time
|
took = timer.elapsed
|
||||||
|
|
||||||
if timed_out:
|
if timed_out:
|
||||||
return WaiterTimedOut(took)
|
return WaiterTimedOut(took)
|
||||||
|
42
tests/ahriman/models/test_metrics_timer.py
Normal file
42
tests/ahriman/models/test_metrics_timer.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ahriman.core.exceptions import InitializeError
|
||||||
|
from ahriman.models.metrics_timer import MetricsTimer
|
||||||
|
|
||||||
|
|
||||||
|
def test_elapsed() -> None:
|
||||||
|
"""
|
||||||
|
must return elapsed time
|
||||||
|
"""
|
||||||
|
with MetricsTimer() as timer:
|
||||||
|
value1 = timer.elapsed
|
||||||
|
time.sleep(0.1)
|
||||||
|
value2 = timer.elapsed
|
||||||
|
assert value2 > value1
|
||||||
|
|
||||||
|
|
||||||
|
def test_elapsed_exception() -> None:
|
||||||
|
"""
|
||||||
|
must raise InitializeError if timer wasn't started in the context manager
|
||||||
|
"""
|
||||||
|
timer = MetricsTimer()
|
||||||
|
with pytest.raises(InitializeError):
|
||||||
|
assert timer.elapsed
|
||||||
|
|
||||||
|
|
||||||
|
def test_enter() -> None:
|
||||||
|
"""
|
||||||
|
must start timer with context manager
|
||||||
|
"""
|
||||||
|
with MetricsTimer() as timer:
|
||||||
|
assert timer.start_time > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_exit_with_exception() -> None:
|
||||||
|
"""
|
||||||
|
must exit from context manager if an exception is raised
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
with MetricsTimer():
|
||||||
|
raise ValueError()
|
@ -1,5 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import time
|
|
||||||
|
|
||||||
from ahriman.models.waiter import Waiter, WaiterResult, WaiterTaskFinished, WaiterTimedOut
|
from ahriman.models.waiter import Waiter, WaiterResult, WaiterTaskFinished, WaiterTimedOut
|
||||||
|
|
||||||
@ -37,17 +36,17 @@ def test_is_timed_out() -> None:
|
|||||||
"""
|
"""
|
||||||
must correctly check if timer runs out
|
must correctly check if timer runs out
|
||||||
"""
|
"""
|
||||||
assert Waiter(-1).is_timed_out()
|
assert Waiter(-1).is_timed_out(0.0)
|
||||||
assert Waiter(1, start_time=time.monotonic() - 10.0).is_timed_out()
|
assert Waiter(1).is_timed_out(42.0)
|
||||||
assert not Waiter(1, start_time=time.monotonic() + 10.0).is_timed_out()
|
assert not Waiter(1).is_timed_out(0.42)
|
||||||
|
|
||||||
|
|
||||||
def test_is_timed_out_infinite() -> None:
|
def test_is_timed_out_infinite() -> None:
|
||||||
"""
|
"""
|
||||||
must treat 0 wait timeout as infinite
|
must treat 0 wait timeout as infinite
|
||||||
"""
|
"""
|
||||||
assert not Waiter(0).is_timed_out()
|
assert not Waiter(0).is_timed_out(0.0)
|
||||||
assert not Waiter(0, start_time=time.monotonic() - 10.0).is_timed_out()
|
assert not Waiter(0).is_timed_out(-1.0)
|
||||||
|
|
||||||
|
|
||||||
def test_wait() -> None:
|
def test_wait() -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user