Fix tasks running counter

This commit is contained in:
Erik Kalkoken
2023-10-07 04:52:03 +00:00
committed by Ariel Rin
parent 36b3077caa
commit 51611e1237
17 changed files with 431 additions and 264 deletions

View File

@@ -4,13 +4,11 @@ import datetime as dt
from typing import NamedTuple, Optional
from .event_series import EventSeries
from .helpers import ItemCounter
# Global series for counting task events.
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
retried_tasks = EventSeries("RETRIED_TASKS")
failed_tasks = EventSeries("FAILED_TASKS")
running_tasks = ItemCounter("running_tasks")
class _TaskCounts(NamedTuple):
@@ -20,7 +18,6 @@ class _TaskCounts(NamedTuple):
total: int
earliest_task: Optional[dt.datetime]
hours: int
running: int
def dashboard_results(hours: int) -> _TaskCounts:
@@ -38,7 +35,6 @@ def dashboard_results(hours: int) -> _TaskCounts:
earliest_events += earliest_if_exists(retried_tasks, earliest)
failed_count = failed_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(failed_tasks, earliest)
running_count = running_tasks.value()
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
@@ -46,5 +42,4 @@ def dashboard_results(hours: int) -> _TaskCounts:
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
running=running_count,
)

View File

@@ -1,12 +1,9 @@
"""Helpers for Task Statistics."""
import logging
from typing import Optional
from redis import Redis, RedisError
from django.core.cache import cache
from allianceauth.utils.cache import get_redis_client
logger = logging.getLogger(__name__)
@@ -37,62 +34,6 @@ class _RedisStub:
pass
class ItemCounter:
"""A process safe item counter.
Args:
- name: Unique name for the counter
- minimum: Counter can not go below the minimum, when set
- redis: A Redis client. Will use AA's cache client by default
"""
CACHE_KEY_BASE = "allianceauth-item-counter"
DEFAULT_CACHE_TIMEOUT = 24 * 3600
def __init__(
self, name: str, minimum: Optional[int] = None, redis: Optional[Redis] = None
) -> None:
if not name:
raise ValueError("Must define a name")
self._name = str(name)
self._minimum = minimum
self._redis = get_redis_client_or_stub() if not redis else redis
@property
def _cache_key(self) -> str:
return f"{self.CACHE_KEY_BASE}-{self._name}"
def reset(self, init_value: int = 0):
"""Reset counter to initial value."""
with self._redis.lock(f"{self.CACHE_KEY_BASE}-reset"):
if self._minimum is not None and init_value < self._minimum:
raise ValueError("Can not reset below minimum")
cache.set(self._cache_key, init_value, self.DEFAULT_CACHE_TIMEOUT)
def incr(self, delta: int = 1):
"""Increment counter by delta."""
try:
cache.incr(self._cache_key, delta)
except ValueError:
pass
def decr(self, delta: int = 1):
"""Decrement counter by delta."""
with self._redis.lock(f"{self.CACHE_KEY_BASE}-decr"):
if self._minimum is not None and self.value() == self._minimum:
return
try:
cache.decr(self._cache_key, delta)
except ValueError:
pass
def value(self) -> Optional[int]:
"""Return current value or None if not yet initialized."""
return cache.get(self._cache_key)
def get_redis_client_or_stub() -> Redis:
"""Return AA's default cache client or a stub if Redis is not available."""
redis = get_redis_client()

View File

@@ -1,15 +1,12 @@
"""Signals for Task Statistics."""
from celery.signals import (
task_failure, task_internal_error, task_postrun, task_prerun, task_retry,
task_success, worker_ready,
task_failure, task_internal_error, task_retry, task_success, worker_ready,
)
from django.conf import settings
from .counters import (
failed_tasks, retried_tasks, running_tasks, succeeded_tasks,
)
from .counters import failed_tasks, retried_tasks, succeeded_tasks
def reset_counters():
@@ -17,7 +14,6 @@ def reset_counters():
succeeded_tasks.clear()
failed_tasks.clear()
retried_tasks.clear()
running_tasks.reset()
def is_enabled() -> bool:
@@ -55,15 +51,3 @@ def record_task_failed(*args, **kwargs):
def record_task_internal_error(*args, **kwargs):
if is_enabled():
failed_tasks.add()
@task_prerun.connect
def record_task_prerun(*args, **kwargs):
if is_enabled():
running_tasks.incr()
@task_postrun.connect
def record_task_postrun(*args, **kwargs):
if is_enabled():
running_tasks.decr()

View File

@@ -4,11 +4,7 @@ from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.counters import (
dashboard_results,
succeeded_tasks,
retried_tasks,
failed_tasks,
running_tasks,
dashboard_results, failed_tasks, retried_tasks, succeeded_tasks,
)
@@ -32,7 +28,6 @@ class TestDashboardResults(TestCase):
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
failed_tasks.add()
running_tasks.reset(8)
# when
results = dashboard_results(hours=1)
# then
@@ -41,14 +36,12 @@ class TestDashboardResults(TestCase):
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
self.assertEqual(results.running, 8)
def test_should_work_with_no_data(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
running_tasks.reset()
# when
results = dashboard_results(hours=1)
# then
@@ -57,4 +50,3 @@ class TestDashboardResults(TestCase):
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)
self.assertEqual(results.running, 0)

View File

@@ -4,125 +4,11 @@ from unittest.mock import patch
from redis import RedisError
from allianceauth.authentication.task_statistics.helpers import (
ItemCounter, _RedisStub, get_redis_client_or_stub,
_RedisStub, get_redis_client_or_stub,
)
MODULE_PATH = "allianceauth.authentication.task_statistics.helpers"
COUNTER_NAME = "test-counter"
class TestItemCounter(TestCase):
def test_can_create_counter(self):
# when
counter = ItemCounter(COUNTER_NAME)
# then
self.assertIsInstance(counter, ItemCounter)
def test_can_reset_counter_to_default(self):
# given
counter = ItemCounter(COUNTER_NAME)
# when
counter.reset()
# then
self.assertEqual(counter.value(), 0)
def test_can_reset_counter_to_custom_value(self):
# given
counter = ItemCounter(COUNTER_NAME)
# when
counter.reset(42)
# then
self.assertEqual(counter.value(), 42)
def test_can_increment_counter_by_default(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(0)
# when
counter.incr()
# then
self.assertEqual(counter.value(), 1)
def test_can_increment_counter_by_custom_value(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(0)
# when
counter.incr(8)
# then
self.assertEqual(counter.value(), 8)
def test_can_decrement_counter_by_default(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(9)
# when
counter.decr()
# then
self.assertEqual(counter.value(), 8)
def test_can_decrement_counter_by_custom_value(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(9)
# when
counter.decr(8)
# then
self.assertEqual(counter.value(), 1)
def test_can_decrement_counter_below_zero(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(0)
# when
counter.decr(1)
# then
self.assertEqual(counter.value(), -1)
def test_can_not_decrement_counter_below_minimum(self):
# given
counter = ItemCounter(COUNTER_NAME, minimum=0)
counter.reset(0)
# when
counter.decr(1)
# then
self.assertEqual(counter.value(), 0)
def test_can_not_reset_counter_below_minimum(self):
# given
counter = ItemCounter(COUNTER_NAME, minimum=0)
# when/then
with self.assertRaises(ValueError):
counter.reset(-1)
def test_can_not_init_without_name(self):
# when/then
with self.assertRaises(ValueError):
ItemCounter(name="")
def test_can_ignore_invalid_values_when_incrementing(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(0)
# when
with patch(MODULE_PATH + ".cache.incr") as m:
m.side_effect = ValueError
counter.incr()
# then
self.assertEqual(counter.value(), 0)
def test_can_ignore_invalid_values_when_decrementing(self):
# given
counter = ItemCounter(COUNTER_NAME)
counter.reset(1)
# when
with patch(MODULE_PATH + ".cache.decr") as m:
m.side_effect = ValueError
counter.decr()
# then
self.assertEqual(counter.value(), 1)
class TestGetRedisClient(TestCase):
def test_should_return_mock_if_redis_not_available_1(self):