Skip to content

Commit

Permalink
Dramatically speed up CachedOrderedDict.__init__() (#625)
Browse files Browse the repository at this point in the history
* Dramatically speed up CachedOrderedDict.__init__()

Previously, we were making multiple round trips to Redis, one per
`dict_key`. Now, we make a single bulk call to Redis.

* Fix type annotations for Python < 3.10

* Bump version number

* Preserve the Open-Closed Principle with name mangling

1. https://youtu.be/miGolgp9xq8?t=2086
2. https://stackoverflow.com/a/38534939
  • Loading branch information
brainix authored Feb 11, 2022
1 parent 4bb931a commit f7fcd3a
Show file tree
Hide file tree
Showing 10 changed files with 35 additions and 24 deletions.
2 changes: 1 addition & 1 deletion pottery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@


__title__: Final[str] = 'pottery'
__version__: Final[str] = '2.3.6'
__version__: Final[str] = '2.3.7'
__description__: Final[str] = __doc__.split(sep='\n\n', maxsplit=1)[0]
__url__: Final[str] = 'https://github.com/brainix/pottery'
__author__: Final[str] = 'Rajiv Bakulesh Shah'
Expand Down
2 changes: 1 addition & 1 deletion pottery/bloom.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
from typing_extensions import final

from .annotations import F
from .annotations import JSONTypes
from .base import Container
from .base import JSONTypes


# TODO: When we drop support for Python 3.7, stop using @_store_on_self(). Use
Expand Down
41 changes: 26 additions & 15 deletions pottery/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
from typing import Collection
from typing import Hashable
from typing import Iterable
from typing import Mapping
from typing import NamedTuple
from typing import Tuple
from typing import TypeVar
from typing import Union
from typing import cast

from redis import Redis
Expand All @@ -39,15 +42,20 @@
# from typing import Final
from typing_extensions import Final

from .base import JSONTypes
from .annotations import JSONTypes
from .base import _default_redis
from .base import logger
from .base import random_key
from .dict import InitArg
from .dict import InitIter
from .dict import RedisDict


F = TypeVar('F', bound=Callable[..., JSONTypes])

UpdateMap = Mapping[JSONTypes, Union[JSONTypes, object]]
UpdateItem = Tuple[JSONTypes, Union[JSONTypes, object]]
UpdateIter = Iterable[UpdateItem]
UpdateArg = Union[UpdateMap, UpdateIter]

_DEFAULT_TIMEOUT: Final[int] = 60 # seconds


Expand All @@ -68,9 +76,6 @@ def _arg_hash(*args: Hashable, **kwargs: Hashable) -> int:
return hash((args, kwargs_items))


F = TypeVar('F', bound=Callable[..., JSONTypes])


def redis_cache(*, # NoQA: C901
redis: Redis | None = None,
key: str | None = None,
Expand Down Expand Up @@ -248,13 +253,14 @@ def __init__(self,
)
for dict_key, encoded_value in zip(dict_keys, encoded_values):
if encoded_value is None:
value = self._SENTINEL
self._misses.add(dict_key)
value = self._SENTINEL
else:
value = self._cache._decode(encoded_value)
item = (dict_key, value)
items.append(item)
return super().__init__(items)
super().__init__()
self.__update(items)

def misses(self) -> Collection[JSONTypes]:
return frozenset(self._misses)
Expand All @@ -265,7 +271,7 @@ def __setitem__(self,
value: JSONTypes | object,
) -> None:
'Set self[dict_key] to value.'
if value is not self._SENTINEL:
if value is not self._SENTINEL: # pragma: no cover
self._cache[dict_key] = value
self._misses.discard(dict_key)
return super().__setitem__(dict_key, value)
Expand Down Expand Up @@ -318,24 +324,29 @@ def __retry(self, callable: Callable[[], Any], *, try_num: int = 0) -> Any:
raise

@_set_expiration
def update(self, arg: InitArg = tuple(), **kwargs: JSONTypes) -> None: # type: ignore
def update(self, # type: ignore
arg: UpdateArg = tuple(),
**kwargs: JSONTypes | object,
) -> None:
'''D.update([E, ]**F) -> None. Update D from dict/iterable E and F.
If E is present and has an .items() method, then does: for k in E: D[k] = E[k]
If E is present and lacks an .items() method, then does: for k, v in E: D[k] = v
In either case, this is followed by: for k in F: D[k] = F[k]
The base class, OrderedDict, has an .update() method that works just
fine. The trouble is that it executes multiple calls to .__setitem__()
therefore multiple round trips to Redis. This overridden .update()
makes a single bulk call to Redis.
fine. The trouble is that it executes multiple calls to
self.__setitem__() therefore multiple round trips to Redis. This
overridden .update() makes a single bulk call to Redis.
'''
to_cache = {}
if isinstance(arg, collections.abc.Mapping):
arg = arg.items()
items = itertools.chain(cast(InitIter, arg), kwargs.items())
items = itertools.chain(arg, kwargs.items())
for dict_key, value in items:
if value is not self._SENTINEL:
to_cache[dict_key] = value
self._misses.discard(dict_key)
self._misses.discard(cast(JSONTypes, dict_key))
super().__setitem__(dict_key, value)
self._cache.update(to_cache)

__update = update
2 changes: 1 addition & 1 deletion pottery/counter.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from redis.client import Pipeline
from typing_extensions import Counter

from .base import JSONTypes
from .annotations import JSONTypes
from .dict import RedisDict


Expand Down
2 changes: 1 addition & 1 deletion pottery/deque.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from redis import Redis
from redis.client import Pipeline

from .base import JSONTypes
from .annotations import JSONTypes
from .exceptions import InefficientAccessWarning
from .list import RedisList

Expand Down
2 changes: 1 addition & 1 deletion pottery/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
from redis import Redis
from redis.client import Pipeline

from .annotations import JSONTypes
from .base import Container
from .base import Iterable_
from .base import JSONTypes
from .exceptions import InefficientAccessWarning
from .exceptions import KeyExistsError

Expand Down
2 changes: 1 addition & 1 deletion pottery/hyper.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@

from redis import Redis

from .annotations import JSONTypes
from .annotations import RedisValues
from .base import Container
from .base import JSONTypes
from .base import random_key


Expand Down
2 changes: 1 addition & 1 deletion pottery/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
from typing_extensions import final

from .annotations import F
from .annotations import JSONTypes
from .base import Container
from .base import JSONTypes
from .exceptions import InefficientAccessWarning
from .exceptions import KeyExistsError

Expand Down
2 changes: 1 addition & 1 deletion pottery/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

from redis import WatchError

from .annotations import JSONTypes
from .base import Container
from .base import JSONTypes
from .exceptions import QueueEmptyError
from .timer import ContextTimer

Expand Down
2 changes: 1 addition & 1 deletion pottery/set.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
from redis.client import Pipeline
from typing_extensions import Literal

from .annotations import JSONTypes
from .base import Container
from .base import Iterable_
from .base import JSONTypes
from .exceptions import InefficientAccessWarning
from .exceptions import KeyExistsError

Expand Down

0 comments on commit f7fcd3a

Please sign in to comment.