Agent: Add request_cache decorator
This commit is contained in:
parent
2305a9d413
commit
c3e9690280
|
@ -0,0 +1,43 @@
|
|||
from functools import wraps
|
||||
|
||||
from .timer import Timer
|
||||
|
||||
|
||||
def request_cache(ttl: float):
|
||||
"""
|
||||
This is a decorator that allows a single response of a function to be cached with an expiration
|
||||
time (TTL). The first call to the function is executed and the response is cached. Subsequent
|
||||
calls to the function result in the cached value being returned until the TTL elapses. Once the
|
||||
TTL elapses, the cache is considered stale and the decorated function will be called, its
|
||||
response cached, and the TTL reset.
|
||||
|
||||
An example usage of this decorator is to wrap a function that makes frequent slow calls to an
|
||||
external resource, such as an HTTP request to a remote endpoint. If the most up-to-date
|
||||
information is not need, this decorator provides a simple way to cache the response for a
|
||||
certain amount of time.
|
||||
|
||||
Example:
|
||||
@request_cache(600)
|
||||
def raining_outside():
|
||||
return requests.get(f"https://weather.service.api/check_for_rain/{MY_ZIP_CODE}")
|
||||
|
||||
:param ttl: The time-to-live in seconds for the cached return value
|
||||
:return: The return value of the decorated function, or the cached return value if the TTL has
|
||||
not elapsed.
|
||||
"""
|
||||
|
||||
def decorator(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
if wrapper.timer.is_expired():
|
||||
wrapper.cached_value = fn(*args, **kwargs)
|
||||
wrapper.timer.set(ttl)
|
||||
|
||||
return wrapper.cached_value
|
||||
|
||||
wrapper.cached_value = None
|
||||
wrapper.timer = Timer()
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
|
@ -0,0 +1,78 @@
|
|||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from infection_monkey.utils.decorators import request_cache
|
||||
from infection_monkey.utils.timer import Timer
|
||||
|
||||
|
||||
class MockTimer(Timer):
|
||||
def __init__(self):
|
||||
self._time_remaining = 0
|
||||
self._set_time = 0
|
||||
|
||||
def set(self, timeout_sec: float):
|
||||
self._time_remaining = timeout_sec
|
||||
self._set_time = timeout_sec
|
||||
|
||||
def set_expired(self):
|
||||
self._time_remaining = 0
|
||||
|
||||
@property
|
||||
def time_remaining(self) -> float:
|
||||
return self._time_remaining
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset the timer without changing the timeout
|
||||
"""
|
||||
self._time_remaining = self._set_time
|
||||
|
||||
|
||||
class MockTimerFactory:
|
||||
def __init__(self):
|
||||
self._instance = None
|
||||
|
||||
def __call__(self):
|
||||
if self._instance is None:
|
||||
mt = MockTimer()
|
||||
self._instance = mt
|
||||
|
||||
return self._instance
|
||||
|
||||
def reset(self):
|
||||
self._instance = None
|
||||
|
||||
|
||||
mock_timer_factory = MockTimerFactory()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_timer(monkeypatch):
|
||||
mock_timer_factory.reset
|
||||
|
||||
monkeypatch.setattr("infection_monkey.utils.decorators.Timer", mock_timer_factory)
|
||||
|
||||
return mock_timer_factory()
|
||||
|
||||
|
||||
def test_request_cache(mock_timer):
|
||||
mock_request = MagicMock(side_effect=lambda: time.time())
|
||||
|
||||
@request_cache(10)
|
||||
def make_request():
|
||||
return mock_request()
|
||||
|
||||
t1 = make_request()
|
||||
t2 = make_request()
|
||||
|
||||
assert t1 == t2
|
||||
|
||||
mock_timer.set_expired()
|
||||
|
||||
t3 = make_request()
|
||||
t4 = make_request()
|
||||
|
||||
assert t3 != t1
|
||||
assert t3 == t4
|
Loading…
Reference in New Issue