-
Notifications
You must be signed in to change notification settings - Fork 13
feat: Add cache layer for the datastore adapter #1514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
77855b4
cd876c9
9cf08d1
61a35c4
e2c03ae
ef9d1e9
f767aa0
60fe890
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |||||||
| import typing as t | ||||||||
|
|
||||||||
| from viur.core.config import conf | ||||||||
| from viur.core import utils | ||||||||
| from .types import Entity, Key | ||||||||
|
|
||||||||
| MEMCACHE_MAX_BATCH_SIZE = 30 | ||||||||
|
|
@@ -37,7 +38,7 @@ | |||||||
| ] | ||||||||
|
|
||||||||
|
|
||||||||
| def get(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> t.Union[Entity, list[Entity], None]: | ||||||||
| def get(keys: t.Union[Key, t.Iterable[Key]], namespace: t.Optional[str] = None) -> t.Union[Entity, list[Entity], None]: | ||||||||
| """ | ||||||||
| Reads data form the memcache. | ||||||||
| :param keys: Unique identifier(s) for one or more entry(s). | ||||||||
|
|
@@ -48,23 +49,24 @@ def get(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> t.U | |||||||
| return None | ||||||||
|
|
||||||||
| namespace = namespace or MEMCACHE_NAMESPACE | ||||||||
| single_request = not isinstance(keys, (list, tuple, set)) | ||||||||
| if single_request: | ||||||||
| keys = [keys] | ||||||||
| if not isinstance(keys, (tuple, list, set)): | ||||||||
| keys = (keys,) | ||||||||
| keys = [str(key) for key in keys] # Enforce that all keys are strings | ||||||||
| cached_data = {} | ||||||||
| cached_data_result = {} | ||||||||
| result = [] | ||||||||
| try: | ||||||||
| while keys: | ||||||||
| cached_data |= conf.db.memcache_client.get_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace) | ||||||||
| if cached_data := conf.db_memcache_client.get_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace): | ||||||||
| cached_data_result |= cached_data | ||||||||
| keys = keys[MEMCACHE_MAX_BATCH_SIZE:] | ||||||||
| except Exception as e: | ||||||||
| logging.error(f"""Failed to get keys form the memcache with {e=}""") | ||||||||
| for key, value in cached_data.items(): | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cached data might be uninizialized! |
||||||||
| entity = Entity(Key(key)) | ||||||||
| entity = Entity(Key.from_legacy_urlsafe(key)) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these always str-keys? |
||||||||
| entity |= value | ||||||||
| result.append(entity) | ||||||||
| if single_request: | ||||||||
|
|
||||||||
| if len(result) == 1: | ||||||||
| return result[0] if result else None | ||||||||
| return result if result else None | ||||||||
|
|
||||||||
|
|
@@ -83,18 +85,19 @@ def put( | |||||||
| """ | ||||||||
| if not check_for_memcache(): | ||||||||
| return False | ||||||||
|
|
||||||||
| if not data: | ||||||||
| return False | ||||||||
| namespace = namespace or MEMCACHE_NAMESPACE | ||||||||
| timeout = timeout or MEMCACHE_TIMEOUT | ||||||||
| if isinstance(timeout, datetime.timedelta): | ||||||||
| timeout = timeout.total_seconds() | ||||||||
|
|
||||||||
| if isinstance(data, (list, tuple, set)): | ||||||||
| data = {item.key: item for item in data} | ||||||||
| elif isinstance(data, Entity): | ||||||||
| data = {data.key: data} | ||||||||
| elif not isinstance(data, dict): | ||||||||
| raise TypeError(f"Invalid type {type(data)}. Expected a db.Entity, list or dict.") | ||||||||
|
|
||||||||
| # Add only values to cache <= MEMMAX_SIZE (1.000.000) | ||||||||
| data = {str(key): value for key, value in data.items() if get_size(value) <= MEMCACHE_MAX_SIZE} | ||||||||
|
|
||||||||
|
|
@@ -110,18 +113,19 @@ def put( | |||||||
| return False | ||||||||
|
|
||||||||
|
|
||||||||
| def delete(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> None: | ||||||||
| def delete(keys: t.Union[Key, t.Iterable[Key]], namespace: t.Optional[str] = None) -> None: | ||||||||
| """ | ||||||||
| Deletes an Entry form memcache. | ||||||||
| :param keys: Unique identifier(s) for one or more entry(s). | ||||||||
| :param namespace: Optional namespace to use. | ||||||||
| """ | ||||||||
| if not check_for_memcache(): | ||||||||
| return None | ||||||||
|
|
||||||||
| if not keys: | ||||||||
| return None | ||||||||
| namespace = namespace or MEMCACHE_NAMESPACE | ||||||||
| if not isinstance(keys, list): | ||||||||
| keys = [keys] | ||||||||
| if not isinstance(keys, (tuple, list, set)): | ||||||||
| keys = (keys,) | ||||||||
|
Comment on lines
+127
to
+128
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| keys = [str(key) for key in keys] # Enforce that all keys are strings | ||||||||
| try: | ||||||||
| while keys: | ||||||||
|
|
@@ -160,8 +164,9 @@ def get_size(obj: t.Any) -> int: | |||||||
|
|
||||||||
| def check_for_memcache() -> bool: | ||||||||
| if conf.db.memcache_client is None: | ||||||||
| logging.warning(f"""conf.db.memcache_client is 'None'. It can not be used.""") | ||||||||
| # logging.warning(f"""conf.db.memcache_client is 'None'. It can not be used.""") | ||||||||
| return False | ||||||||
|
|
||||||||
| init_testbed() | ||||||||
| return True | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,9 @@ | |||||||
|
|
||||||||
| from .overrides import entity_from_protobuf, key_from_protobuf | ||||||||
| from .types import Entity, Key, QueryDefinition, SortOrder, current_db_access_log | ||||||||
| from . import cache | ||||||||
| from viur.core.config import conf | ||||||||
| from viur.core import utils | ||||||||
|
|
||||||||
| # patching our key and entity classes | ||||||||
| datastore.helpers.key_from_protobuf = key_from_protobuf | ||||||||
|
|
@@ -35,7 +37,7 @@ def AllocateIDs(kind_name): | |||||||
| return allocate_ids(kind_name)[0] | ||||||||
|
|
||||||||
|
|
||||||||
| def get(keys: t.Union[Key, t.List[Key]]) -> t.Union[t.List[Entity], Entity, None]: | ||||||||
| def get(keys: t.Union[Key, t.Iterable[Key]]) -> t.Union[t.List[Entity], Entity, None]: | ||||||||
| """ | ||||||||
| Retrieves an entity (or a list thereof) from datastore. | ||||||||
| If only a single key has been given we'll return the entity or none in case the key has not been found, | ||||||||
|
|
@@ -45,12 +47,30 @@ def get(keys: t.Union[Key, t.List[Key]]) -> t.Union[t.List[Entity], Entity, None | |||||||
| """ | ||||||||
| _write_to_access_log(keys) | ||||||||
|
|
||||||||
| if isinstance(keys, (list, set, tuple)): | ||||||||
| res_list = list(__client__.get_multi(keys)) | ||||||||
| res_list.sort(key=lambda k: keys.index(k.key) if k else -1) | ||||||||
| return res_list | ||||||||
| keys = utils.ensure_iterable(keys) | ||||||||
| if not keys: | ||||||||
| return None | ||||||||
|
|
||||||||
| return __client__.get(keys) | ||||||||
| cached = cache.get(keys) or () | ||||||||
|
|
||||||||
| if len(keys) == 1: | ||||||||
| if cached: | ||||||||
| return cached | ||||||||
|
|
||||||||
| if res := __client__.get(keys[0]): | ||||||||
| cache.put(res) | ||||||||
|
|
||||||||
| return res | ||||||||
|
|
||||||||
| keys = {key: cached.get(key) for key in keys} | ||||||||
|
|
||||||||
| uncached = __client__.get_multi((k for k, v in keys.items() if v is None)) | ||||||||
| cache.put(uncached) | ||||||||
|
|
||||||||
| for e in uncached: | ||||||||
| keys[e.key] = e | ||||||||
|
|
||||||||
| return list(keys.values()) | ||||||||
|
|
||||||||
|
|
||||||||
| @deprecated(version="3.8.0", reason="Use 'db.get' instead") | ||||||||
|
|
@@ -65,6 +85,7 @@ def put(entities: t.Union[Entity, t.List[Entity]]): | |||||||
| :param entities: The entities to be saved to the datastore. | ||||||||
| """ | ||||||||
| _write_to_access_log(entities) | ||||||||
| cache.put(entities) | ||||||||
| if isinstance(entities, Entity): | ||||||||
| return __client__.put(entities) | ||||||||
|
|
||||||||
|
|
@@ -76,15 +97,24 @@ def Put(entities: t.Union[Entity, t.List[Entity]]) -> t.Union[Entity, None]: | |||||||
| return put(entities) | ||||||||
|
|
||||||||
|
|
||||||||
| def delete(keys: t.Union[Entity, t.List[Entity], Key, t.List[Key]]): | ||||||||
| def delete(keys: t.Union[Entity, t.Iterable[Entity], Key, t.Iterable[Key]]): | ||||||||
| """ | ||||||||
| Deletes the entities with the given key(s) from the datastore. | ||||||||
| :param keys: A Key (or a t.List of Keys) to delete | ||||||||
| """ | ||||||||
|
|
||||||||
| if not keys: | ||||||||
| return None | ||||||||
| _write_to_access_log(keys) | ||||||||
| if not isinstance(keys, (set, list, tuple)): | ||||||||
| return __client__.delete(keys) | ||||||||
|
|
||||||||
| if not isinstance(keys, (tuple, list, set)): | ||||||||
| keys = (keys,) | ||||||||
|
Comment on lines
+109
to
+110
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| keys = [k.key if isinstance(k, Entity) else k for k in keys] | ||||||||
| if not keys: | ||||||||
| return | ||||||||
|
Comment on lines
+112
to
+113
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is tested twice, the keys list cannot be empty when coming to this line
Suggested change
|
||||||||
|
|
||||||||
| cache.delete(keys) | ||||||||
| if len(keys) == 1: | ||||||||
| return __client__.delete(keys[0]) | ||||||||
|
|
||||||||
| return __client__.delete_multi(keys) | ||||||||
|
|
||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.