Agent: Add request_cache decorator

This commit is contained in:
Mike Salvatore 2022-02-17 14:25:03 -05:00
parent 2305a9d413
commit c3e9690280
2 changed files with 121 additions and 0 deletions

View File

@ -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

View File

@ -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