mirror of
https://github.com/GSA/notifications-api.git
synced 2026-02-02 09:15:19 -05:00
Merge pull request #2891 from alphagov/cache-serialised-template
Cache serialised template in Redis and in memory
This commit is contained in:
@@ -13,6 +13,7 @@ from marshmallow import (
|
||||
post_dump
|
||||
)
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from uuid import UUID
|
||||
|
||||
from notifications_utils.recipients import (
|
||||
validate_email_address,
|
||||
@@ -58,6 +59,14 @@ def _validate_datetime_not_in_past(dte, msg="Date cannot be in the past"):
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
class UUIDsAsStringsMixin:
|
||||
@post_dump()
|
||||
def __post_dump(self, data):
|
||||
for key, value in data.items():
|
||||
if isinstance(value, UUID):
|
||||
data[key] = str(value)
|
||||
|
||||
|
||||
class BaseSchema(ma.ModelSchema):
|
||||
|
||||
def __init__(self, load_json=False, *args, **kwargs):
|
||||
@@ -341,7 +350,7 @@ class BaseTemplateSchema(BaseSchema):
|
||||
strict = True
|
||||
|
||||
|
||||
class TemplateSchema(BaseTemplateSchema):
|
||||
class TemplateSchema(BaseTemplateSchema, UUIDsAsStringsMixin):
|
||||
|
||||
created_by_id = field_for(
|
||||
models.Template, 'created_by_id', dump_to='created_by', dump_only=True
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from threading import RLock
|
||||
|
||||
import cachetools
|
||||
from notifications_utils.clients.redis import RequestCache
|
||||
|
||||
from app import redis_store
|
||||
from app.dao import templates_dao
|
||||
|
||||
caches = defaultdict(partial(cachetools.TTLCache, maxsize=1024, ttl=2))
|
||||
locks = defaultdict(RLock)
|
||||
redis_cache = RequestCache(redis_store)
|
||||
|
||||
|
||||
def memory_cache(func):
|
||||
@cachetools.cached(
|
||||
cache=caches[func.__qualname__],
|
||||
lock=locks[func.__qualname__],
|
||||
key=ignore_first_argument_cache_key,
|
||||
)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def ignore_first_argument_cache_key(cls, *args, **kwargs):
|
||||
return cachetools.keys.hashkey(*args, **kwargs)
|
||||
|
||||
|
||||
class SerialisedModel(ABC):
|
||||
|
||||
@@ -40,10 +67,12 @@ class SerialisedTemplate(SerialisedModel):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@memory_cache
|
||||
def from_id_and_service_id(cls, template_id, service_id):
|
||||
return cls(cls.get_dict(template_id, service_id))
|
||||
|
||||
@staticmethod
|
||||
@redis_cache.set('template-{template_id}-version-None')
|
||||
def get_dict(template_id, service_id):
|
||||
from app.schemas import template_schema
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
from freezegun import freeze_time
|
||||
from boto.exception import SQSError
|
||||
|
||||
from app.dao import templates_dao
|
||||
from app.dao.service_sms_sender_dao import dao_update_service_sms_sender
|
||||
from app.models import (
|
||||
ScheduledNotification,
|
||||
@@ -211,6 +212,105 @@ def test_notification_reply_to_text_is_original_value_if_sender_is_changed_after
|
||||
assert notifications[0].reply_to_text == '123456'
|
||||
|
||||
|
||||
def test_should_cache_template_lookups_in_memory(mocker, client, sample_template):
|
||||
|
||||
mock_get_template = mocker.patch(
|
||||
'app.dao.templates_dao.dao_get_template_by_id_and_service_id',
|
||||
wraps=templates_dao.dao_get_template_by_id_and_service_id,
|
||||
)
|
||||
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
|
||||
|
||||
data = {
|
||||
'phone_number': '+447700900855',
|
||||
'template_id': str(sample_template.id),
|
||||
}
|
||||
|
||||
for i in range(5):
|
||||
auth_header = create_authorization_header(service_id=sample_template.service_id)
|
||||
client.post(
|
||||
path='/v2/notifications/sms',
|
||||
data=json.dumps(data),
|
||||
headers=[('Content-Type', 'application/json'), auth_header]
|
||||
)
|
||||
|
||||
assert mock_get_template.call_count == 1
|
||||
assert mock_get_template.call_args_list == [
|
||||
call(service_id=sample_template.service_id, template_id=str(sample_template.id))
|
||||
]
|
||||
assert Notification.query.count() == 5
|
||||
|
||||
|
||||
def test_should_cache_template_lookups_in_redis(mocker, client, sample_template):
|
||||
|
||||
from app.schemas import template_schema
|
||||
|
||||
mock_redis_get = mocker.patch(
|
||||
'app.redis_store.get',
|
||||
return_value=None,
|
||||
)
|
||||
mock_redis_set = mocker.patch(
|
||||
'app.redis_store.set',
|
||||
)
|
||||
|
||||
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
|
||||
|
||||
data = {
|
||||
'phone_number': '+447700900855',
|
||||
'template_id': str(sample_template.id),
|
||||
}
|
||||
|
||||
auth_header = create_authorization_header(service_id=sample_template.service_id)
|
||||
client.post(
|
||||
path='/v2/notifications/sms',
|
||||
data=json.dumps(data),
|
||||
headers=[('Content-Type', 'application/json'), auth_header]
|
||||
)
|
||||
|
||||
expected_key = f'template-{sample_template.id}-version-None'
|
||||
|
||||
assert mock_redis_get.call_args_list == [call(
|
||||
expected_key,
|
||||
)]
|
||||
|
||||
template_dict = template_schema.dump(sample_template).data
|
||||
|
||||
assert len(mock_redis_set.call_args_list) == 1
|
||||
assert mock_redis_set.call_args[0][0] == expected_key
|
||||
assert json.loads(mock_redis_set.call_args[0][1]) == template_dict
|
||||
assert mock_redis_set.call_args[1]['ex'] == 604_800
|
||||
|
||||
|
||||
def test_should_return_template_if_found_in_redis(mocker, client, sample_template):
|
||||
|
||||
from app.schemas import template_schema
|
||||
template_dict = template_schema.dump(sample_template).data
|
||||
|
||||
mocker.patch(
|
||||
'app.redis_store.get',
|
||||
return_value=json.dumps(template_dict).encode('utf-8')
|
||||
)
|
||||
mock_get_template = mocker.patch(
|
||||
'app.dao.templates_dao.dao_get_template_by_id_and_service_id'
|
||||
)
|
||||
|
||||
mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async')
|
||||
|
||||
data = {
|
||||
'phone_number': '+447700900855',
|
||||
'template_id': str(sample_template.id),
|
||||
}
|
||||
|
||||
auth_header = create_authorization_header(service_id=sample_template.service_id)
|
||||
response = client.post(
|
||||
path='/v2/notifications/sms',
|
||||
data=json.dumps(data),
|
||||
headers=[('Content-Type', 'application/json'), auth_header]
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert mock_get_template.called is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("notification_type, key_send_to, send_to",
|
||||
[("sms", "phone_number", "+447700900855"),
|
||||
("email", "email_address", "sample@email.com")])
|
||||
|
||||
Reference in New Issue
Block a user