Merge pull request #3358 from ostcar/restricted_user_cache

Add restricted_data_cache
This commit is contained in:
Oskar Hahn 2017-09-06 18:38:14 +02:00 committed by GitHub
commit 37b2996b6c
2 changed files with 129 additions and 69 deletions

View File

@ -1,12 +1,13 @@
import json import json
import time import time
import warnings import warnings
from collections import Iterable, defaultdict from collections import defaultdict
from typing import Any, Dict, Iterable, List, cast # noqa from typing import Any, Dict, Generator, Iterable, List, Union, cast
from channels import Channel, Group from channels import Channel, Group
from channels.asgi import get_channel_layer from channels.asgi import get_channel_layer
from channels.auth import channel_session_user, channel_session_user_from_http from channels.auth import channel_session_user, channel_session_user_from_http
from django.apps import apps
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction from django.db import transaction
from django.db.models import Model from django.db.models import Model
@ -14,7 +15,7 @@ from django.db.models import Model
from ..core.config import config from ..core.config import config
from ..core.models import Projector from ..core.models import Projector
from .auth import anonymous_is_enabled, has_perm, user_to_collection_user from .auth import anonymous_is_enabled, has_perm, user_to_collection_user
from .cache import startup_cache, websocket_user_cache from .cache import restricted_data_cache, websocket_user_cache
from .collection import Collection, CollectionElement, CollectionElementList from .collection import Collection, CollectionElement, CollectionElementList
@ -89,25 +90,37 @@ def ws_add_site(message: Any) -> None:
# Collect all elements that shoud be send to the client when the websocket # Collect all elements that shoud be send to the client when the websocket
# connection is established. # connection is established.
user = user_to_collection_user(message.user.id) user = user_to_collection_user(message.user.id)
output = [] user_id = user.id if user is not None else 0
for collection in startup_cache.get_collections(): if restricted_data_cache.exists_for_user(user_id):
access_permissions = collection.get_access_permissions() output = restricted_data_cache.get_data(user_id)
restricted_data = access_permissions.get_restricted_data(collection, user) else:
output = []
for collection in get_startup_collections():
access_permissions = collection.get_access_permissions()
restricted_data = access_permissions.get_restricted_data(collection, user)
# At this point restricted_data has to be a list. So we have to tell it mypy # At this point restricted_data has to be a list. So we have to tell it mypy
restricted_data = cast(List[Dict[str, Any]], restricted_data) restricted_data = cast(List[Dict[str, Any]], restricted_data)
for data in restricted_data: for data in restricted_data:
if data is None: if data is None:
# We do not want to send 'deleted' objects on startup. # We do not want to send 'deleted' objects on startup.
# That's why we skip such data. # That's why we skip such data.
continue continue
output.append(
format_for_autoupdate( formatted_data = format_for_autoupdate(
collection_string=collection.collection_string, collection_string=collection.collection_string,
id=int(data['id']), id=data['id'],
action='changed', action='changed',
data=data)) data=data)
output.append(formatted_data)
# Cache restricted data for user
restricted_data_cache.add_element(
user_id,
collection.collection_string,
data['id'],
formatted_data)
# Send all data. # Send all data.
if output: if output:
@ -259,7 +272,23 @@ def send_data(message: Any) -> None:
except ObjectDoesNotExist: except ObjectDoesNotExist:
# The user does not exist. Skip him/her. # The user does not exist. Skip him/her.
continue continue
output = collection_elements.as_autoupdate_for_user(user)
output = []
for collection_element in collection_elements:
formatted_data = collection_element.as_autoupdate_for_user(user)
if formatted_data['action'] == 'changed':
restricted_data_cache.add_element(
user_id or 0,
collection_element.collection_string,
collection_element.id,
formatted_data)
else:
restricted_data_cache.del_element(
user_id or 0,
collection_element.collection_string,
collection_element.id)
output.append(formatted_data)
for channel_name in channel_names: for channel_name in channel_names:
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)}) send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
@ -290,7 +319,7 @@ def send_data(message: Any) -> None:
{'text': json.dumps(output)}) {'text': json.dumps(output)})
def inform_changed_data(instances: Iterable[Model], information: Dict[str, Any]=None) -> None: def inform_changed_data(instances: Union[Iterable[Model], Model], information: Dict[str, Any]=None) -> None:
""" """
Informs the autoupdate system and the caching system about the creation or Informs the autoupdate system and the caching system about the creation or
update of an element. update of an element.
@ -299,8 +328,8 @@ def inform_changed_data(instances: Iterable[Model], information: Dict[str, Any]=
""" """
root_instances = set() root_instances = set()
if not isinstance(instances, Iterable): if not isinstance(instances, Iterable):
# Make sure instances is an iterable
instances = (instances, ) instances = (instances, )
for instance in instances: for instance in instances:
try: try:
root_instances.add(instance.get_root_rest_element()) root_instances.add(instance.get_root_rest_element())
@ -375,13 +404,25 @@ def send_autoupdate(collection_elements: CollectionElementList) -> None:
Helper function, that sends collection_elements through a channel to the Helper function, that sends collection_elements through a channel to the
autoupdate system. autoupdate system.
Before sending the startup_cache is cleared because it is possibly out of
date.
Does nothing if collection_elements is empty. Does nothing if collection_elements is empty.
""" """
if collection_elements: if collection_elements:
startup_cache.clear()
send_or_wait( send_or_wait(
Channel('autoupdate.send_data').send, Channel('autoupdate.send_data').send,
collection_elements.as_channels_message()) collection_elements.as_channels_message())
def get_startup_collections() -> Generator[Collection, None, None]:
"""
Returns all Collections that should be send to the user at startup
"""
for app in apps.get_app_configs():
try:
# Get the method get_startup_elements() from an app.
# This method has to return an iterable of Collection objects.
get_startup_elements = app.get_startup_elements
except AttributeError:
# Skip apps that do not implement get_startup_elements.
continue
yield from get_startup_elements()

View File

@ -1,3 +1,4 @@
import json
from collections import defaultdict from collections import defaultdict
from typing import ( # noqa from typing import ( # noqa
TYPE_CHECKING, TYPE_CHECKING,
@ -9,11 +10,11 @@ from typing import ( # noqa
List, List,
Optional, Optional,
Set, Set,
Union,
) )
from channels import Group from channels import Group
from channels.sessions import session_for_reply_channel from channels.sessions import session_for_reply_channel
from django.apps import apps
from django.core.cache import cache, caches from django.core.cache import cache, caches
if TYPE_CHECKING: if TYPE_CHECKING:
@ -202,60 +203,76 @@ class DjangoCacheWebsocketUserCache(BaseWebsocketUserCache):
cache.set(self.get_cache_key(), data) cache.set(self.get_cache_key(), data)
class StartupCache: class RestrictedDataCache:
""" """
Cache of all data that are required when a client connects via websocket. Caches all Data for a specific users.
The cached values are expected to be formatted for outout via websocket.
""" """
cache_key = "full_data_startup_cache"
def build(self) -> Dict[str, List[str]]: base_cache_key = 'restricted_user_cache'
def add_element(self, user_id: int, collection_string: str, id: int, data: object) -> None:
""" """
Generate the cache by going through all apps. Returns a dict where the Adds one element to the cache. If the cache does not exists for the user,
key is the collection string and the value a list of the full_data from it is created.
the collection elements.
""" """
cache_data = {} # type: Dict[str, List[str]] redis = get_redis_connection()
for app in apps.get_app_configs(): redis.hset(
try: self.get_cache_key(user_id),
# Get the method get_startup_elements() from an app. "{}/{}".format(collection_string, id),
# This method has to return an iterable of Collection objects. json.dumps(data))
get_startup_elements = app.get_startup_elements # type: Callable[[], Iterable[Collection]]
except AttributeError:
# Skip apps that do not implement get_startup_elements.
continue
for collection in get_startup_elements(): def del_element(self, user_id: int, collection_string: str, id: int) -> None:
cache_data[collection.collection_string] = [
collection_element.get_full_data()
for collection_element
in collection.element_generator()]
cache.set(self.cache_key, cache_data, 86400)
return cache_data
def clear(self) -> None:
""" """
Clears the cache. Removes one element from the cache.
Does nothing if the cache does not exist.
""" """
cache.delete(self.cache_key) redis = get_redis_connection()
redis.hdel(
self.get_cache_key(user_id),
"{}/{}".format(collection_string, id))
def get_collections(self) -> Generator['Collection', None, None]: def exists_for_user(self, user_id: int) -> bool:
""" """
Generator that returns all cached Collections. Returns True if the cache for the user exists, else False.
The data is read from the cache if it exists. It builds the cache if it
does not exists.
""" """
from .collection import Collection # noqa redis = get_redis_connection()
data = cache.get(self.cache_key) return redis.exists(self.get_cache_key(user_id))
if data is None:
# The cache does not exist. def get_data(self, user_id: int) -> List[object]:
data = self.build() """
for collection_string, full_data in data.items(): Returns all data for the user.
yield Collection(collection_string, full_data)
The returned value is a list of the elements.
"""
redis = get_redis_connection()
return [json.loads(element) for element in redis.hvals(self.get_cache_key(user_id))]
def get_cache_key(self, user_id: int) -> str:
"""
Returns the cache key for a user.
"""
return cache.make_key('{}:{}'.format(self.base_cache_key, user_id))
startup_cache = StartupCache() class DummyRestrictedDataCache:
"""
Dummy RestrictedDataCache that does nothing.
"""
def add_element(self, user_id: int, collection_string: str, id: int, data: object) -> None:
pass
def del_element(self, user_id: int, collection_string: str, id: int) -> None:
pass
def exists_for_user(self, user_id: int) -> bool:
return False
def get_data(self, user_id: int) -> List[object]:
pass
def use_redis_cache() -> bool: def use_redis_cache() -> bool:
@ -279,5 +296,7 @@ def get_redis_connection() -> Any:
if use_redis_cache(): if use_redis_cache():
websocket_user_cache = RedisWebsocketUserCache() # type: BaseWebsocketUserCache websocket_user_cache = RedisWebsocketUserCache() # type: BaseWebsocketUserCache
restricted_data_cache = RestrictedDataCache() # type: Union[RestrictedDataCache, DummyRestrictedDataCache]
else: else:
websocket_user_cache = DjangoCacheWebsocketUserCache() websocket_user_cache = DjangoCacheWebsocketUserCache()
restricted_data_cache = DummyRestrictedDataCache()