Repository: Krukov/cashews (language: python)

Issue:
Improve typing for decorators with  - fix test
ref: https://github.com/Krukov/cashews/pull/102

@mnixry 
I will continue working with typing in a separate MR 


Discussion / hints:

Fix patch:
diff --git a/Readme.md b/Readme.md
index 240b898..967bb9c 100644
--- a/Readme.md
+++ b/Readme.md
@@ -26,7 +26,7 @@ scalable and reliable applications. This library intends to make it easy to impl
 - Decorator-based API, just decorate and play
 - Different cache strategies out-of-the-box
 - Support for multiple storage backends ([In-memory](#in-memory), [Redis](#redis), [DiskCache](diskcache))
-- Set ttl with string (2h5m) or with `timedelta`
+- Set TTL as a string ("2h5m"), as `timedelta` or use a function in case TTL depends on key parameters
 - Middlewares
 - Client-side cache (10x faster than simple cache with redis)
 - Bloom filters
@@ -39,7 +39,7 @@ scalable and reliable applications. This library intends to make it easy to impl
 ```python
 from cashews import cache
 
-cache.setup("mem://")  # configure as in-memory cache, but redis is also supported
+cache.setup("mem://")  # configure as in-memory cache, but redis/diskcache is also supported
 
 # use a decorator-based API
 @cache(ttl="3h", key="user:{request.user.uid}")
@@ -48,7 +48,7 @@ async def long_running_function(request):
 
 # or for fine-grained control, use it directly in a function
 async def cache_using_function(request):
-    await cache.set(key=request.user.uid, value=request.user, expire=60)
+    await cache.set(key=request.user.uid, value=request.user, expire="20h")
     ...
 ```
 
@@ -57,9 +57,11 @@ async def cache_using_function(request):
 - [Configuration](#configuration)
 - [Available Backends](#available-backends)
 - [Basic API](#basic-api)
+- [Disable Cache](#disable-cache)
 - [Strategies](#strategies)
   - [Cache condition](#cache-condition)
   - [Keys templating](#template-keys)
+  - [TTL](#ttl)
 - [Cache Invalidation](#cache-invalidation)
   - [Cache invalidation on code change](#cache-invalidation-on-code-change)
 - [Detect the source of a result](#detect-the-source-of-a-result)
@@ -87,10 +89,11 @@ cache = Cache()
 cache.setup(...)
 ```
 
-Optionally, you can disable cache with `enable` parameter:
+Optionally, you can disable cache with `disable`/`enable` parameter:
 
 ```python
 cache.setup("redis://redis/0?enable=1")
+cache.setup("mem://?size=500", disable=True)
 cache.setup("mem://?size=500", enable=False)
 ```
 
@@ -100,9 +103,8 @@ You can setup different Backends based on a prefix:
 cache.setup("redis://redis/0")
 cache.setup("mem://?size=500", prefix="user")
 
-await cache.get("accounts")  # will use redis backend
-await cache.get("user:1")  # will use memory backend
-
+await cache.get("accounts")  # will use the redis backend
+await cache.get("user:1")  # will use the memory backend
 ```
 
 ### Available Backends
@@ -127,14 +129,17 @@ This will use Redis as a storage.
 This backend uses [pickle](https://docs.python.org/3/library/pickle.html) module to serialize
 values, but the cashes can store values with sha1-keyed hash.
 
-Use `hash_key` parameter to protect your application from security vulnerabilities.
+Use `hash_key` and `digestmod` parameter to protect your application from security vulnerabilities.
+The `digestmod` is a hashing algorithm that can be used: `sum`, `md5` (default), `sha1` and `sha256`
+The `hash_key` is a salt for a hash - it can be a secret string.
+
 Pickle can't serialize any type of objects. In case you need to store more complex types
 
 you can use [dill](https://github.com/uqfoundation/dill) - set `pickle_type="dill"`.
 Dill is great, but less performance.
 If you need complex serializer for [sqlalchemy](https://docs.sqlalchemy.org/en/14/core/serializer.html) objects you can set `pickle_type="sqlalchemy"`
 
-Any connections errors are suppressed, to disable it use `safe=False` - a `CacheBackendInteractionException` will be raised
+Any connections errors are suppressed, to disable it use `safe=False` - a `CacheBackendInteractionError` will be raised
 
 If you would like to use [client-side cache](https://redis.io/topics/client-side-caching) set `client_side=True`
 Client side cache will add `cashews:` prefix for each key, to customize it use `client_side_prefix` option.
@@ -162,7 +167,7 @@ Also, it is good choice for client side local storage.
 
 You cat setup disk cache with [FanoutCache parameters](http://www.grantjenks.com/docs/diskcache/api.html#fanoutcache)
 
-** Warning ** `cache.keys_match` and `cache.get_match` does not work with this storage (works only if shards are disabled)
+** Warning ** `cache.scan` and `cache.get_match` does not work with this storage (works only if shards are disabled)
 
 ```python
 cache.setup("disk://")
@@ -182,26 +187,53 @@ cache.setup("mem://")  # configure as in-memory cache
 
 await cache.set(key="key", value=90, expire=60, exist=None)  # -> bool
 await cache.set_raw(key="key", value="str")  # -> bool
+
 await cache.get("key", default=None)  # -> Any
 await cache.get_raw("key")
 await cache.get_many("key1", "key2", default=None)
+async for key, value in cache.get_match("pattern:*", batch_size=100):
+    ...
+
 await cache.incr("key") # -> int
+
 await cache.delete("key")
+await cache.delete_many("key1", "key2")
 await cache.delete_match("pattern:*")
+
 async for key in cache.scan("pattern:*"):
     ...
-async for key, value in cache.get_match("pattern:*", batch_size=100, default=None):
-    ...
 
 await cache.expire("key", timeout=10)
 await cache.get_expire("key")  # -> int seconds to expire
+
 await cache.ping(message=None)  # -> bytes
 await cache.clear()
+
 await cache.is_locked("key", wait=60)  # -> bool
 async with cache.lock("key", expire=10):
     ...
 await cache.set_lock("key", value="value", expire=60)  # -> bool
 await cache.unlock("key", "value")  # -> bool
+
+await cache.close()
+```
+
+### Disable Cache
+
+Cache can be disabled not only at setup, but also in runtime. Cashews allow you to disable/enable any call of cache or specific commands:
+
+```python
+from cashews import cache, Command
+
+cache.setup("mem://")  # configure as in-memory cache
+
+cache.disable(Command.DELETE)
+cache.disable()
+cache.enable(Command.GET, Command.SET)
+cache.enable()
+
+with cache.disabling():
+  ...
 ```
 
 ### Strategies
@@ -259,7 +291,8 @@ from cashews import cache  # or: from cashews import hit
 
 @cache.hit(ttl="2h", cache_hits=100, update_after=2)
 async def get(name):
-    ...
+    value = await api_call()
+    return {"status": value}
 ```
 
 #### Early
@@ -342,8 +375,8 @@ Bloom filter
 ```python
 from cashews import cache
 
-@cache.bloom(name="emails:{email}", capacity=10_000, false_positives=1)
-async def email_exists(email):
+@cache.bloom(capacity=10_000, false_positives=1)
+async def email_exists(email: str) -> bool:
     ...
 
 for email in all_users_emails:
@@ -377,13 +410,13 @@ async def get():
 
 ```
 
-Also caching decorators have parameter `time_condition` - min latency in seconds (can be set like `ttl` - by string or timedelata object)
-for getting result of function call to be cached.
+Also caching decorators have parameter `time_condition` - min latency in seconds (can be set like `ttl`)
+of getting a result of function call to be cached.
 
 ```python
 from cashews import cache
 
-@cache(ttl="1h", time_condition="3s")
+@cache(ttl="1h", time_condition="3s")  # to cache for 1 hour if execution takes more than 3 seconds
 async def get():
     ...
 ```
@@ -515,12 +548,46 @@ await get_name(item)
 # a key will be "price-10.00:USD"
 ```
 
+### TTL
+
+Cache time to live (`ttl`) is a required parameter for all cache decorators. TTL can be:
+
+- an integer as numbers of seconds
+- a `timedelta`
+- a string like in golang e.g `1d2h3m50s`
+- a callable object like a function that receive `args` and `kwargs` of the decorated function and return one of previous format for TTL
+
+Examples:
+
+```python
+from cashews import cache
+from datetime import timedelta
+
+@cache(ttl=60 * 10)
+async def get(item_id: int) -> Item:
+    pass
+
+@cache(ttl=timedelta(minutes=10))
+async def get(item_id: int) -> Item:
+    pass
+
+@cache(ttl="10m")
+async def get(item_id: int) -> Item:
+    pass
+
+def _ttl(item_id: int) -> str:
+    return "2h" if item_id > 10 else "1h"
+
+@cache(ttl=_ttl)
+async def get(item_id: int) -> Item:
+    pass
+```
+
 ### Cache invalidation
 
 Cache invalidation - one of the main Computer Science well known problem.
-That's why `ttl` is a required parameter for all cache decorators.
 
-Sometimes, you want to invalidate cache after some action is triggered.
+Sometimes, you want to invalidate the cache after some action is triggered.
 Consider this example:
 
 ```python
@@ -542,7 +609,21 @@ async def create_item(user):
    ...
 ```
 
-Here, cache for `user_items` and `items` will be invalidated every time `create_item` is called.
+Here, the cache for `user_items` and `items` will be invalidated every time `create_item` is called.
+
+Also you can invalidate future call of cache request by context manager:
+
+```python
+from cashews import invalidate_further
+
+await cache.set("key", "value")
+
+with invalidate_further():
+  await cache.get("key") # will delete cache
+
+
+
+```
 
 #### Cache invalidation on code change
 
@@ -611,45 +692,30 @@ async def get_user(user_id):
 Decorators give us a very simple API but also make it difficult to understand where
 result is coming from - cache or direct call.
 
-To solve this problem `cashews` has `context_cache_detect` context manager:
+To solve this problem `cashews` has `cache_detect` context manager:
 
 ```python
-from cashews import context_cache_detect
+from cashews import cache_detect
 
-with context_cache_detect as detector:
-    response = await decorated_function()
+with cache_detect as detector:
+    response = await something_that_use_cache()
     keys = detector.get()
 print(keys)
-# >>> {"my:key": [{"ttl": 10, "name": "simple", "backend": "redis"}, ], "fail:key": [{"ttl": timedelta(hours=10), "exc": RateLimit}, "name": "fail", "backend": "mem"],}
-```
-
-or you can use `CacheDetect` class:
-
-```python
-from cashews import CacheDetect
-
-cache_detect = CacheDetect()
-await func(_from_cache=cache_detect)
-assert cache_detect.keys == {}
-
-await func(_from_cache=cache_detect)
-assert len(cache_detect.keys) == 1
+# >>> {"my:key": [{"ttl": 10, "name": "simple", "backend": "redis"}, ], "fail:key": [{"ttl": 10, "exc": RateLimit}, "name": "fail", "backend": "mem"],}
 ```
 
-A simple middleware to use it in a web app:
+E.g. A simple middleware to use it in a web app:
 
 ```python
 @app.middleware("http")
 async def add_from_cache_headers(request: Request, call_next):
-    with context_cache_detect as detector:
+    with cache_detect as detector:
         response = await call_next(request)
         if detector.keys:
             key = list(detector.keys.keys())[0]
             response.headers["X-From-Cache"] = key
             expire = await cache.get_expire(key)
             response.headers["X-From-Cache-Expire-In-Seconds"] = str(expire)
-            if "exc" in detector.keys[key]:
-                response.headers["X-From-Cache-Exc"] = str(detector.keys[key]["exc"])
     return response
 ```
 
@@ -664,9 +730,9 @@ from cashews import cache
 logger = logging.getLogger(__name__)
 
 
-async def logging_middleware(call, *args, backend=None, cmd=None, **kwargs):
+async def logging_middleware(call, cmd: Command, backend: Backend, *args, **kwargs):
     key = args[0] if args else kwargs.get("key", kwargs.get("pattern", ""))
-    logger.info("=> Cache request: %s ", cmd, extra={"command": cmd, "cache_key": key})
+    logger.info("=> Cache request: %s ", cmd.value, extra={"args": args, "cache_key": key})
     return await call(*args, **kwargs)
 
 
diff --git a/cashews/__init__.py b/cashews/__init__.py
index ac1267f..3427b39 100644
--- a/cashews/__init__.py
+++ b/cashews/__init__.py
@@ -1,24 +1,28 @@
-from ._cache_condition import NOT_NONE  # noqa
-from .decorators import CacheDetect, CircuitBreakerOpen, RateLimitError, context_cache_detect, fast_condition  # noqa
+from .backend_settings import register_backend  # noqa
+from .cache_condition import NOT_NONE  # noqa
+from .commands import Command  # noqa
+from .decorators import CircuitBreakerOpen, RateLimitError, context_cache_detect, fast_condition  # noqa
 from .exceptions import CacheBackendInteractionError, LockedError  # noqa
 from .formatter import default_formatter, get_template_and_func_for, get_template_for_key  # noqa
 from .helpers import add_prefix, all_keys_lower, memory_limit  # noqa
 from .key import noself  # noqa
-from .validation import set_invalidate_further  # noqa
+from .validation import invalidate_further  # noqa
 from .wrapper import Cache  # noqa
 
 cache_detect = context_cache_detect
 # pylint: disable=invalid-name
 cache = Cache(name="default")
 noself_cache = noself(cache.cache)
-rate_limit = cache.rate_limit
 failover = cache.failover
-circuit_breaker = cache.circuit_breaker
 early = cache.early
 soft = cache.soft
 hit = cache.hit
+
+circuit_breaker = cache.circuit_breaker
 dynamic = cache.dynamic
+rate_limit = cache.rate_limit
 locked = cache.locked
+
 invalidate = cache.invalidate
 invalidate_func = cache.invalidate_func
 
@@ -27,4 +31,4 @@
 mem.setup(
     "mem://?check_interval=1",
     size=1_000_000,
-)  # 1_000_000 * 248(size of small dict) == 31 mb
+)  # 1_000_000 * 248(small dict size) == 31 mb
diff --git a/cashews/_settings.py b/cashews/_settings.py
deleted file mode 100644
index d6ee009..0000000
--- a/cashews/_settings.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from typing import Dict, Union
-from urllib.parse import parse_qsl, urlparse
-
-from .backends.memory import Memory
-
-try:
-    from .backends.client_side import BcastClientSide
-    from .backends.redis import Redis
-except ImportError:
-    BcastClientSide, Redis = None, None
-
-try:
-    import diskcache  # noqa: F401
-except ImportError:
-    DiskCache = None
-else:
-    from .backends.diskcache import DiskCache
-
-
-class BackendNotAvailable(Exception):
-    pass
-
-
-def settings_url_parse(url):
-    params = {}
-    parse_result = urlparse(url)
-    params.update(dict(parse_qsl(parse_result.query)))
-    params = _fix_params_types(params)
-    if parse_result.scheme == "redis" or parse_result.scheme == "rediss":
-        if Redis is None:
-            raise BackendNotAvailable("Redis backend requires `redis` to be installed.")
-        params["backend"] = Redis
-        params["address"] = parse_result._replace(query=None)._replace(fragment=None).geturl()
-    elif parse_result.scheme == "mem":
-        params["backend"] = Memory
-    elif parse_result.scheme == "disk":
-        if DiskCache is None:
-            raise BackendNotAvailable("Disk backend requires `diskcache` to be installed.")
-        params["backend"] = DiskCache
-    elif parse_result.scheme == "":
-        params["backend"] = Memory
-        params["disable"] = True
-    else:
-        raise BackendNotAvailable(f"wrong backend alias {parse_result.scheme}")
-    return params
-
-
-def _fix_params_types(params: Dict[str, str]) -> Dict[str, Union[str, int, bool, float]]:
-    new_params = {}
-    bool_keys = ("safe", "enable", "disable", "client_side")
-    true_values = (
-        "1",
-        "true",
-    )
-    for key, value in params.items():
-        if key.lower() in bool_keys:
-            value = value.lower() in true_values
-        elif value.isdigit():
-            value = int(value)
-        else:
-            try:
-                value = float(value)
-            except ValueError:
-                pass
-        new_params[key.lower()] = value
-    return new_params
diff --git a/cashews/_typing.py b/cashews/_typing.py
index 60d54ca..53deb17 100644
--- a/cashews/_typing.py
+++ b/cashews/_typing.py
@@ -1,6 +1,38 @@
 from datetime import timedelta
-from typing import Any, Callable, Dict, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple, TypeVar, Union
+
+try:
+    from typing import Protocol
+except ImportError:  # 3.7 python
+    from typing_extensions import Protocol
+
+_TTLTypes = Union[int, float, str, timedelta, None]
+TTL = Union[_TTLTypes, Callable[[Any], _TTLTypes]]
+
+
+class CallableCacheCondition(Protocol):
+    def __call__(self, result: Any, args: Tuple, kwargs: Dict[str, Any], key: str = "") -> bool:  # pragma: no cover
+        ...
+
 
-TTL = Union[int, str, timedelta, Callable[[], Union[int, timedelta]]]
-CallableCacheCondition = Callable[[Any, Tuple, Dict, Optional[str]], bool]
 CacheCondition = Union[CallableCacheCondition, str, None]
+
+AsyncCallableResult_T = TypeVar("AsyncCallableResult_T")
+AsyncCallable_T = Callable[..., Awaitable[AsyncCallableResult_T]]
+Decorator = Callable[..., AsyncCallable_T]
+
+if TYPE_CHECKING:  # pragma: no cover
+    from . import Command
+    from .backends.interface import Backend
+
+
+class Middleware(Protocol):
+    def __call__(
+        self,
+        call: AsyncCallable_T,
+        cmd: "Command",
+        backend: "Backend",
+        *args,
+        **kwargs,
+    ) -> Awaitable[AsyncCallableResult_T]:  # pragma: no cover
+        ...
diff --git a/cashews/backend_settings.py b/cashews/backend_settings.py
new file mode 100644
index 0000000..66dab61
--- /dev/null
+++ b/cashews/backend_settings.py
@@ -0,0 +1,87 @@
+from typing import Any, Callable, Dict, Tuple, Type, Union
+from urllib.parse import parse_qsl, urlparse
+
+from .backends.interface import Backend
+from .backends.memory import Memory
+from .exceptions import BackendNotAvailableError
+
+try:
+    from .backends.client_side import BcastClientSide
+    from .backends.redis import Redis
+except ImportError:
+    BcastClientSide, Redis = None, None
+
+try:
+    import diskcache  # noqa: F401
+except ImportError:
+    DiskCache = None
+else:
+    from .backends.diskcache import DiskCache
+
+
+_NO_REDIS_ERROR = "Redis backend requires `redis` to be installed."
+_CUSTOM_ERRORS = {
+    "redis": _NO_REDIS_ERROR,
+    "rediss": _NO_REDIS_ERROR,
+    "disk": "Disk backend requires `diskcache` to be installed.",
+}
+BackendOrFabric = Union[Type[Backend], Callable[..., Backend]]
+_BACKENDS: Dict[str, Tuple[BackendOrFabric, bool]] = {}
+
+
+def register_backend(alias: str, backend_class: BackendOrFabric, pass_uri: bool = False):
+    _BACKENDS[alias] = (backend_class, pass_uri)
+
+
+def _redis_fabric(**params: Any) -> Union[Redis, BcastClientSide]:
+    if params.pop("client_side", None):
+        return BcastClientSide(**params)
+    return Redis(**params)
+
+
+register_backend("mem", Memory)
+if Redis:
+    register_backend("redis", _redis_fabric, pass_uri=True)
+    register_backend("rediss", _redis_fabric, pass_uri=True)
+if DiskCache:
+    register_backend("disk", DiskCache)
+
+
+def settings_url_parse(url: str) -> Tuple[BackendOrFabric, Dict[str, Any]]:
+    parse_result = urlparse(url)
+    params: Dict[str, Any] = dict(parse_qsl(parse_result.query))
+    params = serialize_params(params)
+
+    alias = parse_result.scheme
+    if alias == "":
+        return Memory, {"disable": True}
+
+    if alias not in _BACKENDS:
+        error = _CUSTOM_ERRORS.get(alias, f"wrong backend alias {alias}")
+        raise BackendNotAvailableError(error)
+    backend_class, pass_uri = _BACKENDS[alias]
+    if pass_uri:
+        params["address"] = url.split("?")[0]
+    return backend_class, params
+
+
+def serialize_params(params: Dict[str, str]) -> Dict[str, Union[str, int, bool, float]]:
+    new_params = {}
+    bool_keys = ("safe", "enable", "disable", "client_side")
+    true_values = (
+        "1",
+        "true",
+    )
+    for key, value in params.items():
+        _value: Union[str, int, bool, float]
+        if key.lower() in bool_keys:
+            _value = value.lower() in true_values
+        elif value.isdigit():
+            _value = int(value)
+        else:
+            try:
+                _value = float(value)
+            except ValueError:
+                _value = value
+        new_params[key.lower()] = _value
+    return new_params
diff --git a/cashews/backends/client_side.py b/cashews/backends/client_side.py
index 346d7c8..799e2a1 100644
--- a/cashews/backends/client_side.py
+++ b/cashews/backends/client_side.py
@@ -28,7 +28,7 @@
 
 import asyncio
 import logging
-from typing import Any, AsyncIterator, Dict, Optional, Tuple
+from typing import Any, AsyncIterator, Mapping, Optional, Tuple
 
 from redis.exceptions import ConnectionError as RedisConnectionError
 
@@ -37,6 +37,7 @@
 
 _REDIS_INVALIDATE_CHAN = "__redis__:invalidate"
 _empty = object()
+_empty_in_redis = object()  # set when we know that key not in redis
 _RECONNECT_WAIT = 10
 _DEFAULT_PREFIX = "cashews:"
 BCAST_ON = "CLIENT TRACKING on REDIRECT {client_id} BCAST PREFIX {prefix}"
@@ -52,41 +53,54 @@ class BcastClientSide(Redis):
     https://redis.io/topics/client-side-caching
     """
 
-    name = "redis_mem"
-
     def __init__(self, *args: Any, local_cache=None, client_side_prefix: str = _DEFAULT_PREFIX, **kwargs: Any) -> None:
         self._local_cache = Memory() if local_cache is None else local_cache
         self._prefix = client_side_prefix
         self._recently_update = Memory(size=500, check_interval=5)
-        self.__listen_task = None
-        self._listen_started = None
+        self._listen_task = None
+        self._listen_started = asyncio.Event()
+        self.__listen_stop = asyncio.Event()
         super().__init__(*args, **kwargs)
 
     async def init(self):
         self._listen_started = asyncio.Event()
+        self.__listen_stop = asyncio.Event()
         await self._local_cache.init()
         await self._recently_update.init()
         await super().init()
-        self.__listen_task = asyncio.create_task(self._listen_invalidate_forever())
-        await self._listen_started.wait()
+        self.__is_init = False
+        self._listen_task = asyncio.create_task(self._listen_invalidate_forever())
+        try:
+            await asyncio.wait_for(self._listen_started.wait(), timeout=2)
+        except TimeoutError:
+            if self._listen_task.done():
+                raise self._listen_task.exception()
+            self._listen_task.cancel()
+            raise
+        self.__is_init = True
 
     async def _mark_as_recently_updated(self, key: str):
         await self._recently_update.set(key, True, expire=5)
 
-    async def _get_local_value(self, key: str):
-        if not self._listen_started.is_set():
-            return _empty
-        return await self._local_cache.get(key, default=_empty)
+    def _remove_prefix(self, key: str) -> str:
+        return key[len(self._prefix) :]
+
+    def _add_prefix(self, key: str) -> str:
+        return self._prefix + key
 
     async def _listen_invalidate_forever(self):
-        while True:
+        while not self.__listen_stop.is_set():
             try:
                 await self._listen_invalidate()
             except (RedisConnectionError, ConnectionRefusedError):
                 logger.error("broken connection with redis. Clearing client side storage")
-                self._listen_started = asyncio.Event()
+                self._listen_started.clear()
                 await self._local_cache.clear()
                 await asyncio.sleep(_RECONNECT_WAIT)
+            except Exception:
+                self._listen_started.clear()
+                await self._local_cache.clear()
+                raise
 
     async def _get_channel(self):
         pubsub = self._client.pubsub()
@@ -101,110 +115,133 @@ async def _listen_invalidate(self):
         channel = await self._get_channel()
         self._listen_started.set()
         await self._local_cache.clear()
-        while True:
+        while not self.__listen_stop.is_set():
             message = await channel.get_message(ignore_subscribe_messages=True, timeout=0.1)
-            if message is None or not message.get("data"):
+            if message is None or "data" not in message:
                 continue
-            key = message["data"][0]
-            key = key.decode().lstrip(self._prefix)
-            if not await self._recently_update.get(key):
-                await self._local_cache.delete(key)
-            else:
-                await self._recently_update.delete(key)
+            if message["data"] is None:  # flushdb
+                logger.debug("flush: clear local cache")
+                await self._local_cache.clear()
+                continue
+            for key in message["data"]:
+                key = self._remove_prefix(key.decode())
+                if not await self._recently_update.get(key):
+                    logger.debug("invalidate the key %s", key)
+                    await self._local_cache.delete(key)
+                else:
+                    logger.debug("the key `%s`: recently update", key)
+                    await self._recently_update.delete(key)
 
     async def get(self, key: str, default: Any = None) -> Any:
-        value = await self._get_local_value(key)
-        if value is not _empty:
-            return value
-        value = await super().get(self._prefix + key, default=_empty)
+        if self._listen_started.is_set():
+            value = await self._local_cache.get(key, default=_empty)
+            if value is _empty_in_redis:
+                return default
+            if value is not _empty:
+                return value
+        value = await super().get(self._add_prefix(key), default=_empty)
         if value is not _empty:
             await self._local_cache.set(key, value)
             return value
+        await self._local_cache.set(key, _empty_in_redis)
         return default
 
     async def set(self, key: str, value: Any, *args: Any, **kwargs: Any) -> Any:
-        await self._local_cache.set(key, value, *args, **kwargs)  # not async by the way
+        await self._local_cache.set(key, value, *args, **kwargs)
         await self._mark_as_recently_updated(key)
-        return await super().set(self._prefix + key, value, *args, **kwargs)
+        return await super().set(self._add_prefix(key), value, *args, **kwargs)
 
-    async def set_many(self, pairs: Dict[str, Any], expire: Optional[float] = None):
+    async def set_many(self, pairs: Mapping[str, Any], expire: Optional[float] = None):
         await self._local_cache.set_many(pairs, expire)
         for key in pairs.keys():
             await self._mark_as_recently_updated(key)
-        return await super().set_many({self._prefix + key: value for key, value in pairs.items()}, expire=expire)
+        return await super().set_many({self._add_prefix(key): value for key, value in pairs.items()}, expire=expire)
 
-    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:
-        async for key in super().scan(self._prefix + pattern, batch_size=batch_size):
-            yield key.strip(self._prefix)
+    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:  # type: ignore
+        async for key in super().scan(self._add_prefix(pattern), batch_size=batch_size):
+            yield self._remove_prefix(key)
 
-    async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Any]:
-        missed_keys = {self._prefix + key for key in keys}
+    async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Any, ...]:
+        missed_keys = {self._add_prefix(key) for key in keys}
         values = {key: default for key in keys}
         if self._listen_started.is_set():
             for i, value in enumerate(await self._local_cache.get_many(*keys, default=_empty)):
                 key = keys[i]
-                if value is not _empty:
-                    values[key] = value
-                    missed_keys.remove(self._prefix + key)
+                if value is _empty:
+                    continue
+                if value is _empty_in_redis:
+                    value = default
+                values[key] = value
+                missed_keys.remove(self._add_prefix(key))
         missed_values = await super().get_many(*missed_keys, default=default)
-        missed = dict(zip((key.strip(self._prefix) for key in missed_keys), missed_values))
+        missed = dict(zip((self._remove_prefix(key) for key in missed_keys), missed_values))
         for key, value in missed.items():
             if value is not default:
                 await self._local_cache.set(key, value)
+            else:
+                await self._local_cache.set(key, _empty_in_redis)
         return tuple(missed.get(key, value) for key, value in values.items())
 
-    async def get_match(
-        self, pattern: str, batch_size: int = 100, default: Optional[Any] = None
-    ) -> AsyncIterator[Tuple[str, bytes]]:
-        keys_in_local_cache = set()
-        async for key, value in self._local_cache.get_match(pattern, batch_size=batch_size, default=_empty):
-            keys_in_local_cache.add(key)
-            yield key, value
-        cursor = b"0"
-        while cursor:
-            cursor, keys = await self._client.scan(cursor, match=self._prefix + pattern, count=batch_size)
+    async def get_match(self, pattern: str, batch_size: int = 100) -> AsyncIterator[Tuple[str, bytes]]:  # type: ignore
+        cursor = 0
+        while True:
+            cursor, keys = await self._client.scan(cursor, match=self._add_prefix(pattern), count=batch_size)
             if not keys:
+                if not cursor:
+                    return
                 continue
-            keys = [key.decode() for key in keys]
-            keys = [key for key in keys if key.strip(self._prefix) not in keys_in_local_cache]
-            values = await super().get_many(*keys, default=default)
+            keys = [self._remove_prefix(key.decode()) for key in keys]
+            values = await self.get_many(*keys, default=_empty)
             for key, value in zip(keys, values):
-                yield key.strip(self._prefix), value
+                if value is not _empty:
+                    yield key, value
+            if not cursor:
+                return
 
     async def incr(self, key: str) -> int:
-        await self._local_cache.incr(key)
-        await self._mark_as_recently_updated(key)
-        return await super().incr(self._prefix + key)
+        value = await super().incr(self._add_prefix(key))
+        if value:
+            await self._local_cache.set(key, value)
+            await self._mark_as_recently_updated(key)
+        return value
 
     async def delete(self, key: str) -> bool:
-        await self._local_cache.delete(key)
-        return await super().delete(self._prefix + key)
+        await self._local_cache.set(key, _empty_in_redis)
+        return await super().delete(self._add_prefix(key))
+
+    async def delete_many(self, *keys: str):
+        _keys = []
+        for key in keys:
+            _keys.append(self._add_prefix(key))
+            await self._local_cache.set(key, _empty_in_redis)
+        return await super().delete_many(*_keys)
 
     async def delete_match(self, pattern: str):
         await self._local_cache.delete_match(pattern)
-        return await super().delete_match(self._prefix + pattern)
+        return await super().delete_match(self._add_prefix(pattern))
 
-    async def expire(self, key: str, timeout: Optional[float]):
+    async def expire(self, key: str, timeout: float):
         local_value = await self._local_cache.get(key, default=_empty)
-        if local_value is not _empty:
+        if local_value not in (_empty, _empty_in_redis):
             await self._local_cache.expire(key, timeout)
             await self._mark_as_recently_updated(key)
-        result = await super().expire(self._prefix + key, timeout)
+        result = await super().expire(self._add_prefix(key), timeout)
         return result
 
     async def get_expire(self, key: str) -> int:
         if await self._local_cache.get_expire(key) != -1:
             if self._listen_started.is_set():
                 return await self._local_cache.get_expire(key)
-        expire = await super().get_expire(self._prefix + key)
+        expire = await super().get_expire(self._add_prefix(key))
         await self._local_cache.expire(key, expire)
         return expire
 
     async def exists(self, key) -> bool:
         if self._listen_started.is_set():
-            if await self._local_cache.exists(key):
+            local_value = await self._local_cache.get(key, default=_empty)
+            if local_value not in (_empty, _empty_in_redis):
                 return True
-        return await super().exists(self._prefix + key)
+        return await super().exists(self._add_prefix(key))
 
     async def set_lock(self, key: str, value, expire):
         await self._mark_as_recently_updated(key)
@@ -213,23 +250,24 @@ async def set_lock(self, key: str, value, expire):
         if isinstance(expire, float):
             pexpire = int(expire * 1000)
             expire = None
-        return bool(await self._client.set(self._prefix + key, value, ex=expire, px=pexpire, nx=True))
+        return bool(await self._client.set(self._add_prefix(key), value, ex=expire, px=pexpire, nx=True))
 
     async def unlock(self, key, value):
         await self._local_cache.unlock(key, value)
-        return await super().unlock(self._prefix + key, value)
+        return await super().unlock(self._add_prefix(key), value)
 
     async def get_size(self, key: str) -> int:
-        return await super().get_size(self._prefix + key)
+        return await super().get_size(self._add_prefix(key))
 
     async def clear(self):
         await self._local_cache.clear()
         return await super().clear()
 
-    def close(self):
-        if self.__listen_task is not None:
-            self.__listen_task.cancel()
-            self.__listen_task = None
-        self._local_cache.close()
-        self._recently_update.close()
-        super().close()
+    async def close(self):
+        self.__listen_stop.set()
+        if self._listen_task is not None:
+            await self._listen_task
+            self._listen_task = None
+        await self._local_cache.close()
+        await self._recently_update.close()
+        await super().close()
diff --git a/cashews/backends/diskcache.py b/cashews/backends/diskcache.py
index 79a2ead..0caf4fc 100644
--- a/cashews/backends/diskcache.py
+++ b/cashews/backends/diskcache.py
@@ -1,7 +1,7 @@
 import asyncio
 import re
 from datetime import datetime
-from typing import Any, AsyncIterator, Dict, List, Optional, Tuple
+from typing import Any, AsyncIterator, Dict, List, Mapping, Optional, Tuple
 
 from diskcache import Cache, FanoutCache
 
@@ -11,8 +11,6 @@
 
 
 class DiskCache(Backend):
-    name = "diskcache"
-
     def __init__(self, *args, directory=None, shards=8, **kwargs: Any) -> None:
         self.__is_init = False
         self._set_locks: Dict[str, asyncio.Lock] = {}
@@ -33,8 +31,9 @@ async def _run_in_executor(self, call, *args):
     def is_init(self) -> bool:
         return self.__is_init
 
-    def close(self):
+    async def close(self):
         self._cache.close()
+        self.__is_init = False
 
     async def set(
         self,
@@ -75,10 +74,10 @@ async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Any
     def _get_many(self, keys: List[str], default: Optional[Any] = None):
         return tuple(self._cache.get(key, default=default) for key in keys)
 
-    async def set_many(self, pairs: Dict[str, Any], expire: Optional[float] = None):
+    async def set_many(self, pairs: Mapping[str, Any], expire: Optional[float] = None):
         return await self._run_in_executor(self._set_many, pairs, expire)
 
-    def _set_many(self, pairs: Dict[str, Any], expire: Optional[float] = None):
+    def _set_many(self, pairs: Mapping[str, Any], expire: Optional[float] = None):
         for key, value in pairs.items():
             self._set(key, value, expire=expire)
 
@@ -88,23 +87,19 @@ async def exists(self, key: str) -> bool:
     def _exists(self, key: str) -> bool:
         return key in self._cache
 
-    async def keys_match(self, pattern: str):
+    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:  # type: ignore
         if not self._sharded:
-            for key in await self._run_in_executor(self._keys_match, pattern):
+            for key in await self._run_in_executor(self._scan, pattern):
                 yield key
 
-    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:
-        async for key in self.keys_match(pattern):
-            yield key
-
-    def _keys_match(self, pattern: str):
+    def _scan(self, pattern: str):
         pattern = pattern.replace("*", ".*")
         regexp = re.compile(pattern)
         for key in self._cache.iterkeys():
             if regexp.fullmatch(key):
                 yield key
 
-    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int]:
+    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int, ...]:
         value = await self.get(key, default=0)
         array = Bitarray(str(value))
         result = []
@@ -112,7 +107,7 @@ async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int]:
             result.append(array.get(index, size))
         return tuple(result)
 
-    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1) -> Tuple[int]:
+    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1) -> Tuple[int, ...]:
         value = await self.get(key, default=0)
         array = Bitarray(str(value))
         result = []
@@ -128,19 +123,24 @@ async def incr(self, key: str) -> int:
     async def delete(self, key: str) -> bool:
         return await self._run_in_executor(self._cache.delete, key)
 
+    async def delete_many(self, *keys: str):
+        await self._run_in_executor(self._delete_many, keys)
+
+    def _delete_many(self, keys: List[str]):
+        for key in keys:
+            self._cache.delete(key)
+
     async def delete_match(self, pattern: str):
         return await self._run_in_executor(self._delete_match, pattern)
 
     def _delete_match(self, pattern: str):
-        for key in self._keys_match(pattern):
+        for key in self._scan(pattern):
             self._cache.delete(key)
 
-    async def get_match(
-        self, pattern: str, batch_size: int = None, default: Optional[Any] = None
-    ) -> AsyncIterator[Tuple[str, Any]]:
+    async def get_match(self, pattern: str, batch_size: int = None) -> AsyncIterator[Tuple[str, Any]]:
         if not self._sharded:
-            for key in await self._run_in_executor(self._keys_match, pattern):
-                yield key, await self._run_in_executor(self._cache.get, key, default)
+            for key in await self._run_in_executor(self._scan, pattern):
+                yield key, await self._run_in_executor(self._cache.get, key)
 
     async def expire(self, key: str, timeout: float) -> int:
         return await self._run_in_executor(self._cache.touch, key, timeout)
diff --git a/cashews/backends/interface.py b/cashews/backends/interface.py
index 3a16ece..20dc049 100644
--- a/cashews/backends/interface.py
+++ b/cashews/backends/interface.py
@@ -1,23 +1,27 @@
 import uuid
+from abc import ABCMeta, abstractmethod
 from contextlib import asynccontextmanager
 from typing import Any, AsyncIterator, Mapping, Optional, Tuple
 
+from ..disable_control import ControlMixin
 from ..exceptions import LockedError
 
 
-class Backend:
-    name: str = ""
-    is_init: bool = False
-
-    def __init__(self, *args: Any, **kwargs: Any) -> None:
+class _BackendInterface(metaclass=ABCMeta):
+    @property
+    @abstractmethod
+    def is_init(self) -> bool:
         ...
 
+    @abstractmethod
     async def init(self):
         ...
 
-    def close(self):
+    @abstractmethod
+    async def close(self):
         ...
 
+    @abstractmethod
     async def set(
         self,
         key: str,
@@ -27,70 +31,90 @@ async def set(
     ) -> bool:
         ...
 
+    @abstractmethod
     async def set_raw(self, key: str, value: Any, **kwargs: Any):
         ...
 
+    @abstractmethod
     async def get(self, key: str, default: Optional[Any] = None) -> Any:
         ...
 
+    @abstractmethod
     async def get_raw(self, key: str) -> Any:
         ...
 
+    @abstractmethod
     async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Optional[Any], ...]:
         ...
 
+    @abstractmethod
     async def set_many(self, pairs: Mapping[str, Any], expire: Optional[float] = None):
         ...
 
+    @abstractmethod
     async def exists(self, key: str) -> bool:
         ...
 
-    async def keys_match(self, pattern: str) -> AsyncIterator[str]:
-        ...
-
+    @abstractmethod
     async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:
         ...
 
+    @abstractmethod
     async def incr(self, key: str) -> int:
         ...
 
+    @abstractmethod
     async def delete(self, key: str) -> bool:
         ...
 
+    @abstractmethod
+    async def delete_many(self, *keys: str):
+        ...
+
+    @abstractmethod
     async def delete_match(self, pattern: str):
         ...
 
-    async def get_match(
-        self, pattern: str, batch_size: int = 100, default: Optional[Any] = None
-    ) -> AsyncIterator[Tuple[str, Any]]:
+    @abstractmethod
+    async def get_match(self, pattern: str, batch_size: int = 100) -> AsyncIterator[Tuple[str, Any]]:
         ...
 
+    @abstractmethod
     async def expire(self, key: str, timeout: float):
         ...
 
+    @abstractmethod
     async def get_expire(self, key: str) -> int:
         ...
 
-    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int]:
+    @abstractmethod
+    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int, ...]:
         ...
 
-    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1) -> Tuple[int]:
+    @abstractmethod
+    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1) -> Tuple[int, ...]:
         ...
 
+    @abstractmethod
     async def get_size(self, key: str) -> int:
         """
         Return size in bites that allocated by a value for given key
         """
+        ...
 
+    @abstractmethod
     async def ping(self, message: Optional[bytes] = None) -> bytes:
         ...
 
+    @abstractmethod
     async def clear(self):
         ...
 
+    @abstractmethod
     async def set_lock(self, key: str, value: Any, expire: float) -> bool:
         ...
 
+    @abstractmethod
     async def is_locked(
         self,
         key: str,
@@ -99,6 +123,7 @@ async def is_locked(
     ) -> bool:
         ...
 
+    @abstractmethod
     async def unlock(self, key: str, value: Any) -> bool:
         ...
 
@@ -120,3 +145,8 @@ async def lock(self, key: str, expire: float):
             yield
         finally:
             await self.unlock(key, identifier)
+
+
+class Backend(ControlMixin, _BackendInterface, metaclass=ABCMeta):
+    def __init__(self, *args, **kwargs):
+        super().__init__()
diff --git a/cashews/backends/memory.py b/cashews/backends/memory.py
index e9bbbee..c4bbb73 100644
--- a/cashews/backends/memory.py
+++ b/cashews/backends/memory.py
@@ -2,7 +2,8 @@
 import re
 import time
 from collections import OrderedDict
-from typing import Any, AsyncIterator, Dict, Optional, Tuple
+from contextlib import suppress
+from typing import Any, AsyncIterator, Mapping, Optional, Tuple
 
 from cashews.utils import Bitarray, get_obj_size
 
@@ -18,8 +19,6 @@ class Memory(Backend):
     Inmemory backend lru with ttl
     """
 
-    name = "mem"
-
     def __init__(self, size: int = 1000, check_interval: float = 1):
         self.store: OrderedDict = OrderedDict()
         self._check_interval = check_interval
@@ -42,7 +41,8 @@ async def _remove_expired(self):
         while not self.__remove_expired_stop.is_set():
             for key in dict(self.store):
                 await self.get(key)
-            await asyncio.sleep(self._check_interval)
+            with suppress(TimeoutError):
+                await asyncio.wait_for(self.__remove_expired_stop.wait(), self._check_interval)
 
     async def clear(self):
         self.store = OrderedDict()
@@ -72,23 +72,19 @@ async def get_raw(self, key: str):
     async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Any, ...]:
         return tuple(self._get(key, default=default) for key in keys)
 
-    async def set_many(self, pairs: Dict[str, Any], expire: Optional[float] = None):
+    async def set_many(self, pairs: Mapping[str, Any], expire: Optional[float] = None):
         for key, value in pairs.items():
             self._set(key, value, expire)
 
-    async def keys_match(self, pattern: str) -> AsyncIterator[str]:
+    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:  # type: ignore
         pattern = pattern.replace("*", ".*")
         regexp = re.compile(pattern)
         for key in dict(self.store):
             if regexp.fullmatch(key):
                 yield key
 
-    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:
-        async for key in self.keys_match(pattern):
-            yield key
-
     async def incr(self, key: str):
-        value = int(self._get(key, 0)) + 1
+        value = int(self._get(key, 0)) + 1  # type: ignore
         self._set(key=key, value=value)
         return value
 
@@ -104,15 +100,17 @@ def _delete(self, key: str) -> bool:
             return True
         return False
 
+    async def delete_many(self, *keys: str):
+        for key in keys:
+            self._delete(key)
+
     async def delete_match(self, pattern: str):
-        async for key in self.keys_match(pattern):
+        async for key in self.scan(pattern):
             self._delete(key)
 
-    async def get_match(
-        self, pattern: str, batch_size: int = None, default: Optional[Any] = None
-    ) -> AsyncIterator[Tuple[str, Any]]:
-        async for key in self.keys_match(pattern):
-            yield key, self._get(key, default=default)
+    async def get_match(self, pattern: str, batch_size: int = None) -> AsyncIterator[Tuple[str, Any]]:  # type: ignore
+        async for key in self.scan(pattern):
+            yield key, self._get(key)
 
     async def expire(self, key: str, timeout: float):
         if not self._key_exist(key):
@@ -131,11 +129,11 @@ async def get_expire(self, key: str) -> int:
     async def ping(self, message: Optional[bytes] = None) -> bytes:
         return b"PONG" if message in (None, b"PING") else message  # type: ignore[return-value]
 
-    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int]:
-        array: Bitarray = self._get(key, default=Bitarray("0"))
+    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int, ...]:
+        array: Bitarray = self._get(key, default=Bitarray("0"))  # type: ignore
         return tuple(array.get(index, size) for index in indexes)
 
-    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1) -> Tuple[int]:
+    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1) -> Tuple[int, ...]:
         array: Optional[Bitarray] = self._get(key)
         if array is None:
             array = Bitarray("0")
@@ -159,7 +157,7 @@ def _get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
         if key not in self.store:
             return default
         self.store.move_to_end(key)
-        expire_at, value = self.store.get(key)
+        expire_at, value = self.store[key]
         if expire_at and expire_at < time.time():
             self._delete(key)
             return default
@@ -189,9 +187,9 @@ async def get_size(self, key: str) -> int:
             return get_obj_size(self.store[key])
         return 0
 
-    def close(self):
+    async def close(self):
         self.__remove_expired_stop.set()
         if self.__remove_expired_task:
-            self.__remove_expired_task.cancel()
+            await self.__remove_expired_task
             self.__remove_expired_task = None
-        super().close()
+        self.__is_init = False
diff --git a/cashews/backends/redis/backend.py b/cashews/backends/redis/backend.py
index 128ddc3..39f4d93 100644
--- a/cashews/backends/redis/backend.py
+++ b/cashews/backends/redis/backend.py
@@ -1,5 +1,5 @@
 import asyncio
-from typing import Any, AsyncIterator, Dict, Optional, Tuple
+from typing import Any, AsyncIterator, Dict, Mapping, Optional, Tuple, Type, Union
 
 from redis.asyncio import BlockingConnectionPool
 
@@ -13,12 +13,14 @@
     return 0
 end
 """
+_empty = object()
 # pylint: disable=arguments-differ
 # pylint: disable=abstract-method
 
 
 class _Redis(Backend):
-    name = "redis"
+    _client: Union[Redis, SafeRedis]
+    _client_class: Union[Type[Redis], Type[SafeRedis]]
 
     def __init__(self, address, safe: bool = True, **kwargs: Any) -> None:
         kwargs.pop("local_cache", None)
@@ -34,9 +36,11 @@ def __init__(self, address, safe: bool = True, **kwargs: Any) -> None:
         self._pool_class = kwargs.pop("connection_pool_class", BlockingConnectionPool)
         if self._pool_class == BlockingConnectionPool:
             kwargs["timeout"] = kwargs.pop("wait_for_connection_timeout", 0.1)
-        self._client = None
-        self._sha = {}
-        self._safe = safe
+        self._sha: Dict[str, Any] = {}
+        if not safe:
+            self._client_class = Redis
+        else:
+            self._client_class = SafeRedis
         self._kwargs = kwargs
         self._address = address
         self.__is_init = False
@@ -47,11 +51,7 @@ def is_init(self) -> bool:
         return self.__is_init
 
     async def init(self):
-        if not self._safe:
-            client_class = Redis
-        else:
-            client_class = SafeRedis
-        self._client = client_class(connection_pool=self._pool_class.from_url(self._address, **self._kwargs))
+        self._client = self._client_class(connection_pool=self._pool_class.from_url(self._address, **self._kwargs))
         await self._client.initialize()
         self.__is_init = True
 
@@ -70,7 +70,7 @@ async def set(self, key: str, value: Any, expire: Optional[float] = None, exist=
             expire = None
         return bool(await self._client.set(key, value, ex=expire, px=pexpire, nx=nx, xx=xx))
 
-    async def set_many(self, pairs: Dict[str, Any], expire: Optional[float] = None):
+    async def set_many(self, pairs: Mapping[str, Any], expire: Optional[float] = None):
         await self._client.mset(pairs)
         if expire is not None:
             async with self._client.pipeline(transaction=True) as pipe:
@@ -81,7 +81,7 @@ async def set_many(self, pairs: Dict[str, Any], expire: Optional[float] = None):
     async def get_expire(self, key: str) -> int:
         return await self._client.ttl(key)
 
-    async def expire(self, key: str, timeout: Optional[float]) -> int:
+    async def expire(self, key: str, timeout: float):
         return await self._client.expire(key, int(timeout))
 
     async def set_lock(self, key: str, value, expire: float) -> bool:
@@ -113,45 +113,51 @@ async def unlock(self, key: str, value: Any) -> bool:
         return await self._client.evalsha(self._sha["UNLOCK"], 1, key, value)
 
     async def delete(self, key: str) -> bool:
-        return await self._client.unlink(key)
+        return bool(await self._client.unlink(key))
 
     async def exists(self, key: str) -> bool:
         return bool(await self._client.exists(key))
 
-    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:
-        cursor = b"0"
-        while cursor:
+    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:  # type: ignore
+        cursor = 0
+        while True:
             cursor, keys = await self._client.scan(cursor, match=pattern, count=batch_size)
             for key in keys:
                 yield key.decode()
+            if not cursor:
+                return
 
-    async def keys_match(self, pattern: str):
-        async for key in self.scan(pattern):
-            yield key.encode()
+    async def delete_many(self, *keys: str):
+        await self._client.delete(*keys)
 
     async def delete_match(self, pattern: str):
         if "*" not in pattern:
-            return await self._client.unlink(pattern)
-        cursor = b"0"
-        while cursor:
+            await self._client.unlink(pattern)
+            return
+        cursor = 0
+        while True:
             cursor, keys = await self._client.scan(cursor, match=pattern, count=100)
             if not keys:
+                if not cursor:
+                    return
                 continue
             await self._client.unlink(keys[0], *keys[1:])
-        return True
 
-    async def get_match(
-        self, pattern: str, batch_size: int = 100, default: Optional[Any] = None
-    ) -> AsyncIterator[Tuple[str, bytes]]:
-        cursor = b"0"
-        while cursor:
+    async def get_match(self, pattern: str, batch_size: int = 100) -> AsyncIterator[Tuple[str, Any]]:  # type: ignore
+        cursor = 0
+        while True:
             cursor, keys = await self._client.scan(cursor, match=pattern, count=batch_size)
             if not keys:
+                if not cursor:
+                    return
                 continue
             keys = [key.decode() for key in keys]
-            values = await self.get_many(*keys, default=default)
+            values = await self.get_many(*keys, default=_empty)
             for key, value in zip(keys, values):
-                yield key, value
+                if value is not _empty:  # key can be deleted after scan
+                    yield key, value
+            if not cursor:
+                return
 
     async def get_size(self, key: str) -> int:
         size = await self._client.memory_usage(key) or 0
@@ -160,7 +166,7 @@ async def get_size(self, key: str) -> int:
     async def get(self, key: str, default=None) -> Any:
         return await self._client.get(key)
 
-    async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Optional[Any]]:
+    async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Optional[Any], ...]:
         if not keys:
             return tuple()
         values = await self._client.mget(keys[0], *keys[1:])
@@ -180,7 +186,7 @@ async def get_bits(self, key: str, *indexes: int, size: int = 1):
             bitops.get(fmt=f"u{size}", offset=f"#{index}")
         return tuple(await bitops.execute() or [])
 
-    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1):
+    async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1) -> Tuple[int, ...]:
         bitops = self._client.bitfield(key)
         for index in indexes:
             bitops.incrby(fmt=f"u{size}", offset=f"#{index}", increment=by, overflow="SAT")
@@ -198,6 +204,6 @@ async def set_raw(self, key: str, value: Any, **kwargs: Any):
     async def get_raw(self, key: str) -> Any:
         return await self._client.get(key)
 
-    def close(self):
-        self._client = None
+    async def close(self):
+        await self._client.close()
         self.__is_init = False
diff --git a/cashews/_cache_condition.py b/cashews/cache_condition.py
similarity index 73%
rename from cashews/_cache_condition.py
rename to cashews/cache_condition.py
index 860dcd7..2df9e89 100644
--- a/cashews/_cache_condition.py
+++ b/cashews/cache_condition.py
@@ -3,7 +3,7 @@
 from functools import wraps
 from typing import Any, Callable, Dict, Tuple
 
-from cashews._typing import CacheCondition
+from ._typing import AsyncCallable_T, CacheCondition, CallableCacheCondition, Decorator
 
 
 def _not_none_store_condition(result, args, kwargs, key=None) -> bool:
@@ -27,11 +27,11 @@ def get_cache_condition(condition: CacheCondition) -> Callable[[Any, Tuple, Dict
     return condition
 
 
-_spent = ContextVar("spent", default=0)
+_spent = ContextVar("spent", default=0.0)
 
 
-def create_time_condition(limit):
-    def decorator(func):
+def create_time_condition(limit: float) -> Tuple[CallableCacheCondition, Decorator]:
+    def decorator(func: AsyncCallable_T) -> AsyncCallable_T:
         @wraps(func)
         async def _wrapper(*args, **kwargs):
             start = time.perf_counter()
@@ -42,7 +42,7 @@ async def _wrapper(*args, **kwargs):
 
         return _wrapper
 
-    def condition(result, _args, _kwargs, key):
+    def condition(result: Any, args: Tuple, kwargs: Dict[str, Any], key: str = "") -> bool:
         return _spent.get() > limit
 
     return condition, decorator
diff --git a/cashews/commands.py b/cashews/commands.py
new file mode 100644
index 0000000..d1e95b6
--- /dev/null
+++ b/cashews/commands.py
@@ -0,0 +1,36 @@
+from enum import Enum
+
+
+class Command(Enum):
+
+    GET = "get"
+    GET_MANY = "get_many"
+    GET_RAW = "get_raw"
+    GET_MATCH = "get_match"
+    SET = "set"
+    SET_RAW = "set_raw"
+    SET_MANY = "set_many"
+    DELETE = "delete"
+    DELETE_MANY = "delete_many"
+    DELETE_MATCH = "delete_match"
+
+    EXIST = "exists"
+    SCAN = "scan"
+    INCR = "incr"
+    EXPIRE = "expire"
+    GET_EXPIRE = "get_expire"
+    CLEAR = "clear"
+    SET_LOCK = "set_lock"
+    UNLOCK = "unlock"
+    IS_LOCKED = "is_locked"
+
+    GET_BITS = "get_bits"
+    INCR_BITS = "incr_bits"
+
+    PING = "ping"
+    GET_SIZE = "get_size"
+
+
+ALL = set(Command)
+PATTERN_CMDS = {Command.GET_MATCH, Command.DELETE_MATCH, Command.SCAN}
+RETRIEVE_CMDS = {Command.GET, Command.INCR, Command.GET_MANY, Command.GET_MATCH}
diff --git a/cashews/decorators/__init__.py b/cashews/decorators/__init__.py
index 7a74465..8320764 100644
--- a/cashews/decorators/__init__.py
+++ b/cashews/decorators/__init__.py
@@ -1,4 +1,4 @@
-from .._cache_condition import NOT_NONE
+from ..cache_condition import NOT_NONE
 from .bloom import bloom, dual_bloom  # noqa
 from .cache.defaults import CacheDetect, context_cache_detect  # noqa
 from .cache.early import early  # noqa
diff --git a/cashews/decorators/bloom.py b/cashews/decorators/bloom.py
index a40dfac..6cf8e83 100644
--- a/cashews/decorators/bloom.py
+++ b/cashews/decorators/bloom.py
@@ -1,63 +1,59 @@
 import asyncio
 import math
-import warnings
+from collections import namedtuple
 from functools import wraps
-from typing import Any, Optional, Tuple, Union
+from typing import Any, Iterable, Optional, Tuple, Union
 
-from cashews.utils import get_hashes
-
-from ..backends.interface import Backend
+from .._typing import AsyncCallable_T, Decorator
+from ..backends.interface import _BackendInterface
 from ..key import get_cache_key, get_cache_key_template
+from ..utils import get_indexes
 
 __all__ = ("bloom",)
 
-all_not_zeros = all
+TrueFalsePair = Tuple[int, int]
+IntOrPair = Union[int, TrueFalsePair]
+BloomParams = namedtuple("BloomParams", ("size", "number_of_buckets"))
+
+possible_set = all
 
 
-def all_zeros(values) -> bool:
+def all_zeros(values: Iterable[int]) -> bool:
     return all(v == 0 for v in values)
 
 
-def at_least_one_zero(values) -> bool:
+def not_set(values: Iterable[int]) -> bool:
     return not all(values)
 
 
 def bloom(
-    backend: Backend,
+    backend: _BackendInterface,
+    *,
+    capacity: int,
     name: Optional[str] = None,
-    index_size: Optional[int] = None,
-    number_of_hashes: Optional[int] = None,
-    false_positives: Optional[Union[float, int]] = 1,
-    capacity: Optional[int] = None,
+    false_positives: Union[float, int] = 1,
     check_false_positive: bool = True,
     prefix: str = "bloom",
-):
+) -> Decorator:
     """
     Decorator that can help you to use bloom filter algorithm
 
     @bloom(name="user_name:{name}", false_positives=1, capacity=10_000)
-    async def is_user_exists(name):
+    async def is_user_exists(name) -> bool:
         return name in ....
 
     :param backend: cache backend
     :param name: custom cache key
-    :param index_size: size of bloom filter
-    :param number_of_hashes: the same as k
     :param capacity: the same as n - number of elements
     :param false_positives: Percents of false positive results
     :param check_false_positive: do we need to check if we have positive result
     :param prefix: custom prefix for key, default 'bloom'
     """
+    assert false_positives and capacity
+    assert 0 < false_positives < 100
+    index_size, number_of_buckets = params_for(capacity, false_positives / 100)
 
-    if backend.name == "mem":
-        warnings.warn("Remember that you should fill a bloom filter before using it.")
-
-    if index_size is None:
-        assert false_positives and capacity
-        assert 0 < false_positives < 100
-        index_size, number_of_hashes = params_for(capacity, false_positives / 100)
-
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _name = get_cache_key_template(func, key=name)
         _cache_key = f"{_name}:{index_size}"
         if prefix:
@@ -73,8 +69,8 @@ async def __set(*args: Any, **kwargs: Any):
             if not result:
                 return result
             _bloom_key = get_cache_key(func, _name, args, kwargs)
-            hashes = get_hashes(_bloom_key, number_of_hashes, index_size)
-            await backend.incr_bits(_cache_key, *hashes)
+            indexes = get_indexes(_bloom_key, number_of_buckets, index_size)
+            await backend.incr_bits(_cache_key, *indexes)
             return result
 
         func.set = __set
@@ -82,11 +78,11 @@ async def __set(*args: Any, **kwargs: Any):
         @wraps(func)
         async def _wrap(*args, **kwargs):
             _bloom_key = get_cache_key(func, _name, args, kwargs)
-            hashes = get_hashes(_bloom_key, number_of_hashes, index_size)
+            hashes = get_indexes(_bloom_key, number_of_buckets, index_size)
             values = await backend.get_bits(_cache_key, *hashes)
             if values is None:
                 return await func(*args, **kwargs)
-            if all_not_zeros(values):  # if all bits is set
+            if possible_set(values):  # if all bits is set
                 # false positive
                 if check_false_positive:
                     return await func(*args, **kwargs)
@@ -99,35 +95,34 @@ async def _wrap(*args, **kwargs):
 
 
 def dual_bloom(
-    backend: Backend,
+    backend: _BackendInterface,
+    *,
+    capacity: IntOrPair,
     name: Optional[str] = None,
-    index_size: Union[int, Tuple[int, int], None] = None,
-    number_of_hashes: Union[int, Tuple[int, int], None] = None,
-    false: Union[float, Tuple[float, float], None] = 1,
-    capacity: Union[int, Tuple[int, int], None] = None,
+    false: Optional[IntOrPair] = 1,
     no_collisions: bool = False,
     prefix: str = "dual_bloom",
-):
+) -> Decorator:
     """
     Decorator that can help you to use bloom filter algorithm
-     but this implementation with 2 bloom filters - one for true and 1 for false
+     this is  implementation with 2 bloom filters - one for true and 1 for false
+     That give as ability use bloom filter without pre-filling data
+     but with possible false positive and negative results
 
     @dual_bloom(name="user_name:{name}", false_positives=1, capacity=10_000)
-    async def is_user_exists(name):
+    async def is_user_exists(name) -> bool:
         return name in ....
 
     :param backend: cache backend
-    :param name: custom cache key
-    :param index_size: size of bloom filter
-    :param number_of_hashes: the same as k
     :param capacity: the same as n - number of elements
+    :param name: custom cache key
     :param no_collisions: add value only no collisions
     :param false: Percents of false results
     :param prefix: custom prefix for key, default 'dual_bloom'
     """
-    filters_params = _get_params_for_filters(index_size, number_of_hashes, false, capacity)
+    filters_params = _get_params_for_filters(false, capacity)
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _cache_key = get_cache_key_template(func, key=name)
         if prefix:
             _cache_key = f"{prefix}:{_cache_key}"
@@ -140,10 +135,10 @@ async def _delete(*args: Any, **kwargs: Any) -> None:
             if __delete:
                 await __delete(*args, **kwargs)
             _bloom_key = get_cache_key(func, _cache_key, args, kwargs)
-            hashes_true, hashes_false = _get_hashes(_bloom_key, filters_params)
+            indexes_true, indexes_false = _get_indexes(_bloom_key, *filters_params)
             await asyncio.gather(
-                backend.incr_bits(_true_bloom_key, *hashes_true, by=-1),
-                backend.incr_bits(_false_bloom_key, *hashes_false, by=-1),
+                backend.incr_bits(_true_bloom_key, *indexes_true, by=-1),
+                backend.incr_bits(_false_bloom_key, *indexes_false, by=-1),
             )
 
         func.delete = _delete
@@ -151,22 +146,23 @@ async def _delete(*args: Any, **kwargs: Any) -> None:
         @wraps(func)
         async def _wrap(*args, **kwargs):
             _bloom_key = get_cache_key(func, _cache_key, args, kwargs)
-            hashes_true, hashes_false = _get_hashes(_bloom_key, filters_params)
+            indexes_true, indexes_false = _get_indexes(_bloom_key, *filters_params)
+
             true_values, false_values = await asyncio.gather(
-                backend.get_bits(_true_bloom_key, *hashes_true),
-                backend.get_bits(_false_bloom_key, *hashes_false),
+                backend.get_bits(_true_bloom_key, *indexes_true),
+                backend.get_bits(_false_bloom_key, *indexes_false),
             )
-            if at_least_one_zero(true_values) and at_least_one_zero(false_values):
+            if not_set(true_values) and not_set(false_values):
                 # not set yet
                 result = await func(*args, **kwargs)
                 if result and (not no_collisions or all_zeros(true_values)):
-                    await backend.incr_bits(_true_bloom_key, *hashes_true)
+                    await backend.incr_bits(_true_bloom_key, *indexes_true)
                 if not result and (not no_collisions or all_zeros(false_values)):
-                    await backend.incr_bits(_false_bloom_key, *hashes_false)
+                    await backend.incr_bits(_false_bloom_key, *indexes_false)
                 return result
-            if at_least_one_zero(true_values) and all(false_values):
+            if not_set(true_values) and possible_set(false_values):
                 return False  # can be false Negative
-            if at_least_one_zero(false_values) and all(true_values):
+            if not_set(false_values) and possible_set(true_values):
                 return True  # can be false Positive
             return await func(*args, **kwargs)
 
@@ -176,118 +172,41 @@ async def _wrap(*args, **kwargs):
 
 
 def _get_params_for_filters(
-    index_size: Union[int, Tuple[int, int], None] = None,
-    number_of_hashes: Union[int, Tuple[int, int], None] = None,
-    false: Union[float, Tuple[float, float], None] = 1,
-    capacity: Union[int, Tuple[int, int], None] = None,
-):
-    if index_size is None:
-        assert false and capacity
-        assert 0 < false < 100
-        if isinstance(capacity, int):
-            capacity = (capacity, capacity)
-        if isinstance(false, int):
-            false = (false, false)
-        index_size_false, _number_of_hashes_false = params_for(capacity[0], false[0] / 100)
-        index_size_true, _number_of_hashes_true = params_for(capacity[1], false[1] / 100)
-        index_size = (index_size_true, index_size_false)
-        number_of_hashes = (_number_of_hashes_true, _number_of_hashes_false)
-    else:
-        assert number_of_hashes
-        if isinstance(index_size, int):
-            index_size = (index_size, index_size)
-        if isinstance(number_of_hashes, int):
-            number_of_hashes = (number_of_hashes, number_of_hashes)
-    return list(zip(index_size, number_of_hashes))
-
-
-def _get_hashes(key, filters_params):
-    params_true, params_false = filters_params
-    index_size_true, number_of_hashes_true = params_true
-    index_size_false, number_of_hashes_false = params_false
-    hashes_true = get_hashes(key + "true", number_of_hashes_true, index_size_true)
-    hashes_false = get_hashes(key + "false", number_of_hashes_false, index_size_false)
-    return hashes_true, hashes_false
-
-
-def _counting_bloom(
-    backend: Backend,
-    name: Optional[str] = None,
-    index_size: Optional[int] = None,
-    number_of_hashes: Optional[int] = None,
-    false_positives: Optional[Union[float, int]] = 1,
-    capacity: Optional[int] = None,
-    check_false_positive: bool = True,
-    prefix: str = "counting_bloom",
-):
-    """
-    Decorator that can help you to use bloom count filter algorithm
-    https://en.wikipedia.org/wiki/Counting_Bloom_filter
+    false: Optional[IntOrPair] = 1,
+    capacity: Optional[IntOrPair] = None,
+) -> Tuple[BloomParams, BloomParams]:
+    assert false and capacity
+    assert 0 < false < 100
+    capacity_true, capacity_false = _to_pair(capacity)
+    false_true, false_false = _to_pair(false)
+    return params_for(capacity_true, false_true / 100), params_for(capacity_false, false_false / 100)
 
-    :param backend: cache backend
-    :param name: custom cache key
-    :param index_size: size of bloom filter
-    :param number_of_hashes: the same as k
-    :param capacity: the same as n - number of elements
-    :param false_positives: Percents of false positive results
-    :param prefix: custom prefix for key, default 'counting_bloom'
-    """
-    if index_size is None:
-        assert false_positives and capacity
-        assert 0 < false_positives < 100
-        index_size, number_of_hashes = params_for(capacity, false_positives / 100)
 
-    def _decor(func):
-        _name = get_cache_key_template(func, key=name)
-        _cache_key = prefix + _name + f":{index_size}"
+def _to_pair(value: IntOrPair) -> TrueFalsePair:
+    if isinstance(value, int):
+        return value, value
+    return value
 
-        @wraps(func)
-        async def _wrap(*args, **kwargs):
-            _bloom_key = get_cache_key(func, _name, args, kwargs)
-            hashes = get_hashes(_bloom_key, number_of_hashes, index_size)
-            values = await backend.get_bits(_cache_key, *hashes, size=2)
-            if at_least_one_zero(values):
-                return False
-            if all(values):
-                if not check_false_positive:
-                    return True
-            result = await func(*args, **kwargs)
-            assert isinstance(result, bool)
-            if result:
-                await backend.incr_bits(_cache_key, *hashes, size=2, by=3)
-            else:
-                await backend.incr_bits(_cache_key, *hashes, size=2, by=1)
-            return result
-
-        async def _delete(*args: Any, **kwargs: Any) -> None:
-            _bloom_key = get_cache_key(func, _name, args, kwargs)
-            hashes = get_hashes(_bloom_key, number_of_hashes, index_size)
-            await backend.incr_bits(_cache_key, *hashes, size=2, by=-1)
 
-        func.delete = _wrap.delete = _delete
-
-        async def _set(*args: Any, **kwargs: Any) -> None:
-            _bloom_key = get_cache_key(func, _name, args, kwargs)
-            hashes = get_hashes(_bloom_key, number_of_hashes, index_size)
-            await backend.incr_bits(_cache_key, *hashes, size=2, by=1)
-
-        func.set = _wrap.set = _set
-
-        return _wrap
-
-    return _decor
+def _get_indexes(key: str, params_true: BloomParams, params_false: BloomParams):
+    index_size_true, number_of_buckets_true = params_true
+    index_size_false, number_of_buckets_false = params_false
+    return (
+        get_indexes(key + "true", number_of_buckets_true, index_size_true),
+        get_indexes(key + "false", number_of_buckets_false, index_size_false),
+    )
 
 
-def params_for(size: int, false_positives: float = 0.01):
+def params_for(capacity: int, false_positives: float = 0.01) -> BloomParams:
     """
-    size = n
+    capacity = n
     k=(m * ln 2 / n)
     m = k * n / ln 2
     p = (1-e^{-k * n/m})^{k}
     return m - number of bits in the array and k the number of hash functions
     """
-    m = _count_m(size, false_positives)
-    k = _count_k(m, size)
+    m = _count_m(capacity, false_positives)
+    k = _count_k(m, capacity)
     return m, k
 
 
diff --git a/cashews/decorators/cache/early.py b/cashews/decorators/cache/early.py
index dc5b144..4aec334 100644
--- a/cashews/decorators/cache/early.py
+++ b/cashews/decorators/cache/early.py
@@ -4,25 +4,28 @@
 from functools import wraps
 from typing import Optional
 
-from ..._typing import CallableCacheCondition
-from ...backends.interface import Backend
+from ..._typing import TTL, AsyncCallable_T, CallableCacheCondition, Decorator
+from ...backends.interface import _BackendInterface
 from ...formatter import register_template
 from ...key import get_cache_key, get_cache_key_template
-from .defaults import CacheDetect, _empty, context_cache_detect
+from ...ttl import ttl_to_seconds
+from .defaults import _empty, context_cache_detect
 
 __all__ = ("early",)
+
+
 logger = logging.getLogger(__name__)
 _LOCK_SUFFIX = ":lock"
 
 
 def early(
-    backend: Backend,
-    ttl: int,
+    backend: _BackendInterface,
+    ttl: TTL,
     key: Optional[str] = None,
-    early_ttl: Optional[int] = None,
+    early_ttl: Optional[TTL] = None,
     condition: CallableCacheCondition = lambda *args, **kwargs: True,
     prefix: str = "early",
-):
+) -> Decorator:
     """
     Cache strategy that try to solve Cache stampede problem (https://en.wikipedia.org/wiki/Cache_stampede),
     With hot cache recalculate a result in background near expiration time
@@ -31,31 +34,36 @@ def early(
     :param backend: cache backend
     :param ttl: duration in seconds to store a result
     :param key: custom cache key, may contain alias to args or kwargs passed to a call
+    :param early_ttl: duration in seconds to expire results
     :param condition: callable object that determines whether the result will be saved or not
     :param prefix: custom prefix for key, default 'early'
     """
-    if early_ttl is None:
-        early_ttl = ttl * 0.33
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _key_template = get_cache_key_template(func, key=key, prefix=prefix + ":v2")
         register_template(func, _key_template)
 
         @wraps(func)
-        async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs):
+        async def _wrap(*args, **kwargs):
+            _ttl = ttl_to_seconds(ttl, *args, **kwargs)
+            if early_ttl is None:
+                _early_ttl = _ttl * 0.33
+            else:
+                _early_ttl = ttl_to_seconds(early_ttl, *args, **kwargs)
+
             _cache_key = get_cache_key(func, _key_template, args, kwargs)
             cached = await backend.get(_cache_key, default=_empty)
             if cached is not _empty:
-                _from_cache._set(
+                context_cache_detect._set(
                     _cache_key,
-                    ttl=ttl,
-                    early_ttl=early_ttl,
+                    ttl=_ttl,
+                    early_ttl=_early_ttl,
                     name="early",
                     template=_key_template,
                 )
                 early_expire_at, result = cached
                 if early_expire_at <= datetime.utcnow() and await backend.set(
-                    _cache_key + _LOCK_SUFFIX, "1", expire=early_ttl, exist=False
+                    _cache_key + _LOCK_SUFFIX, "1", expire=_early_ttl, exist=False
                 ):
                     logger.info(
                         "Recalculate cache for %s (exp_at %s)",
@@ -69,8 +77,8 @@ async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs
                             args,
                             kwargs,
                             _cache_key,
-                            ttl,
-                            early_ttl,
+                            _ttl,
+                            _early_ttl,
                             condition,
                             unlock=True,
                         )
@@ -82,8 +90,8 @@ async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs
                 args,
                 kwargs,
                 _cache_key,
-                ttl,
-                early_ttl,
+                _ttl,
+                _early_ttl,
                 condition,
             )
 
@@ -93,7 +101,7 @@ async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs
 
 
 async def _get_result_for_early(
-    backend: Backend, func, args, kwargs, key, ttl: int, early_ttl: int, condition, unlock=False
+    backend: _BackendInterface, func, args, kwargs, key, ttl: int, early_ttl: int, condition, unlock=False
 ):
     try:
         result = await func(*args, **kwargs)
diff --git a/cashews/decorators/cache/fail.py b/cashews/decorators/cache/fail.py
index a9a7cbd..c68e80d 100644
--- a/cashews/decorators/cache/fail.py
+++ b/cashews/decorators/cache/fail.py
@@ -1,11 +1,12 @@
 from functools import wraps
 from typing import Optional, Tuple, Type, Union
 
-from ..._typing import CallableCacheCondition
-from ...backends.interface import Backend
+from ..._typing import TTL, AsyncCallable_T, CallableCacheCondition, Decorator
+from ...backends.interface import _BackendInterface
 from ...formatter import register_template
 from ...key import get_cache_key, get_cache_key_template
-from .defaults import CacheDetect, _empty, context_cache_detect
+from ...ttl import ttl_to_seconds
+from .defaults import _empty, context_cache_detect
 
 __all__ = ("failover",)
 
@@ -22,13 +23,13 @@ def _fast_condition(result, args, kwargs, key=""):
 
 
 def failover(
-    backend: Backend,
-    ttl: int,
+    backend: _BackendInterface,
+    ttl: TTL,
     exceptions: Union[Type[Exception], Tuple[Type[Exception]]] = Exception,
     key: Optional[str] = None,
     condition: CallableCacheCondition = lambda *args, **kwargs: True,
     prefix: str = "fail",
-):
+) -> Decorator:
     """
     Return cache result (at list 1 call of function call should be succeed) if call raised one of given exception,
     :param backend: cache backend
@@ -39,21 +40,22 @@ def failover(
     :param prefix: custom prefix for key, default "fail"
     """
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _key_template = get_cache_key_template(func, key=key, prefix=prefix)
         register_template(func, _key_template)
 
         @wraps(func)
-        async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs):
+        async def _wrap(*args, **kwargs):
+            _ttl = ttl_to_seconds(ttl, *args, **kwargs)
             _cache_key = get_cache_key(func, _key_template, args, kwargs)
             try:
                 result = await func(*args, **kwargs)
             except exceptions as exc:
                 cached = await backend.get(_cache_key, default=_empty)
                 if cached is not _empty:
-                    _from_cache._set(
+                    context_cache_detect._set(
                         _cache_key,
-                        ttl=ttl,
+                        ttl=_ttl,
                         exc=exc,
                         name="failover",
                         template=_key_template,
@@ -62,7 +64,7 @@ async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs
                 raise exc
             else:
                 if condition(result, args, kwargs, key=_cache_key):
-                    await backend.set(_cache_key, result, expire=ttl)
+                    await backend.set(_cache_key, result, expire=_ttl)
                 return result
 
         return _wrap
diff --git a/cashews/decorators/cache/hit.py b/cashews/decorators/cache/hit.py
index 7747438..102132c 100644
--- a/cashews/decorators/cache/hit.py
+++ b/cashews/decorators/cache/hit.py
@@ -2,24 +2,25 @@
 from functools import wraps
 from typing import Optional
 
-from ..._typing import CallableCacheCondition
-from ...backends.interface import Backend
+from ..._typing import TTL, AsyncCallable_T, CallableCacheCondition, Decorator
+from ...backends.interface import _BackendInterface
 from ...formatter import register_template
 from ...key import get_cache_key, get_cache_key_template
-from .defaults import CacheDetect, _empty, context_cache_detect
+from ...ttl import ttl_to_seconds
+from .defaults import _empty, context_cache_detect
 
 __all__ = ("hit",)
 
 
 def hit(
-    backend: Backend,
-    ttl: int,
+    backend: _BackendInterface,
+    ttl: TTL,
     cache_hits: int,
     update_after: Optional[int] = None,
     key: Optional[str] = None,
     condition: CallableCacheCondition = lambda *args, **kwargs: True,
     prefix: str = "hit",
-):
+) -> Decorator:
     """
     Cache call results and drop cache after given numbers of call 'cache_hits'
     :param backend: cache backend
@@ -31,32 +32,33 @@ def hit(
     :param prefix: custom prefix for key, default 'hit'
     """
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _key_template = get_cache_key_template(func, key=key, prefix=prefix)
         register_template(func, _key_template)
 
         @wraps(func)
-        async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs):
+        async def _wrap(*args, **kwargs):
+            _ttl = ttl_to_seconds(ttl, *args, **kwargs)
             _cache_key = get_cache_key(func, _key_template, args, kwargs)
+
             result, hits = await asyncio.gather(
                 backend.get(_cache_key, default=_empty),
                 backend.incr(_cache_key + ":counter"),
             )
             if hits == 1:
-                asyncio.create_task(backend.expire(_cache_key + ":counter", ttl))
+                asyncio.create_task(backend.expire(_cache_key + ":counter", _ttl))
             if result is not _empty and hits and hits <= cache_hits:
-                _from_cache._set(
+                context_cache_detect._set(
                     _cache_key,
-                    ttl=ttl,
+                    ttl=_ttl,
                     cache_hits=cache_hits,
                     name="hit",
-                    backend=backend.name,
                     template=_key_template,
                 )
                 if update_after and hits == update_after:
-                    asyncio.create_task(_get_and_save(func, args, kwargs, backend, _cache_key, ttl, condition))
+                    asyncio.create_task(_get_and_save(func, args, kwargs, backend, _cache_key, _ttl, condition))
                 return result
-            return await _get_and_save(func, args, kwargs, backend, _cache_key, ttl, condition)
+            return await _get_and_save(func, args, kwargs, backend, _cache_key, _ttl, condition)
 
         return _wrap
 
diff --git a/cashews/decorators/cache/simple.py b/cashews/decorators/cache/simple.py
index da17516..2c73af7 100644
--- a/cashews/decorators/cache/simple.py
+++ b/cashews/decorators/cache/simple.py
@@ -1,46 +1,48 @@
 from functools import wraps
 from typing import Optional
 
-from ..._typing import CallableCacheCondition
-from ...backends.interface import Backend
+from ..._typing import TTL, AsyncCallable_T, CallableCacheCondition, Decorator
+from ...backends.interface import _BackendInterface
 from ...formatter import register_template
 from ...key import get_cache_key, get_cache_key_template
-from .defaults import CacheDetect, _empty, context_cache_detect
+from ...ttl import ttl_to_seconds
+from .defaults import _empty, context_cache_detect
 
 __all__ = ("cache",)
 
 
 def cache(
-    backend: Backend,
-    ttl: int,
+    backend: _BackendInterface,
+    ttl: TTL,
     key: Optional[str] = None,
     condition: CallableCacheCondition = lambda *args, **kwargs: True,
     prefix: str = "",
-):
+) -> Decorator:
     """
     Simple cache strategy - trying to return cached result,
     execute wrapped call and store a result with ttl if condition return true
     :param backend: cache backend
-    :param ttl: duration in seconds to store a result
+    :param ttl: duration in seconds to store a result or a callable
     :param key: custom cache key, may contain alias to args or kwargs passed to a call
     :param condition: callable object that determines whether the result will be saved or not
     :param prefix: custom prefix for key
     """
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _key_template = get_cache_key_template(func, key=key, prefix=prefix)
         register_template(func, _key_template)
 
         @wraps(func)
-        async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs):
+        async def _wrap(*args, **kwargs):
+            _ttl = ttl_to_seconds(ttl, *args, **kwargs)
             _cache_key = get_cache_key(func, _key_template, args, kwargs)
             cached = await backend.get(_cache_key, default=_empty)
             if cached is not _empty:
-                _from_cache._set(_cache_key, ttl=ttl, name="simple", template=_key_template)
+                context_cache_detect._set(_cache_key, ttl=_ttl, name="simple", template=_key_template)
                 return cached
             result = await func(*args, **kwargs)
             if condition(result, args, kwargs, key=_cache_key):
-                await backend.set(_cache_key, result, expire=ttl)
+                await backend.set(_cache_key, result, expire=_ttl)
             return result
 
         return _wrap
diff --git a/cashews/decorators/cache/soft.py b/cashews/decorators/cache/soft.py
index 1b5bb04..8ad88b6 100644
--- a/cashews/decorators/cache/soft.py
+++ b/cashews/decorators/cache/soft.py
@@ -3,25 +3,28 @@
 from functools import wraps
 from typing import Optional, Tuple, Type, Union
 
-from ..._typing import CallableCacheCondition
-from ...backends.interface import Backend
+from ..._typing import TTL, AsyncCallable_T, CallableCacheCondition, Decorator
+from ...backends.interface import _BackendInterface
 from ...formatter import register_template
 from ...key import get_cache_key, get_cache_key_template
-from .defaults import CacheDetect, _empty, context_cache_detect
+from ...ttl import ttl_to_seconds
+from .defaults import _empty, context_cache_detect
 
 __all__ = ("soft",)
+
+
 logger = logging.getLogger(__name__)
 
 
 def soft(
-    backend: Backend,
-    ttl: int,
+    backend: _BackendInterface,
+    ttl: TTL,
     key: Optional[str] = None,
-    soft_ttl: Optional[int] = None,
-    exceptions: Union[Type[Exception], Tuple[Type[Exception]]] = Exception,
+    soft_ttl: Optional[TTL] = None,
+    exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = Exception,
     condition: CallableCacheCondition = lambda *args, **kwargs: True,
     prefix: str = "soft",
-):
+) -> Decorator:
     """
     Cache strategy that allow to use pre-expiration
 
@@ -31,24 +34,24 @@ def soft(
     :param condition: callable object that determines whether the result will be saved or not
     :param prefix: custom prefix for key, default 'early'
     """
-    if soft_ttl is None:
-        soft_ttl = ttl * 0.33  # type: ignore[assignment]
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _key_template = get_cache_key_template(func, key=key, prefix=prefix)
         register_template(func, _key_template)
 
         @wraps(func)
-        async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs):
+        async def _wrap(*args, **kwargs):
+            _ttl = ttl_to_seconds(ttl, *args, **kwargs)
+            _soft_ttl = ttl_to_seconds(soft_ttl, *args, **kwargs) or _ttl * 0.33
             _cache_key = get_cache_key(func, _key_template, args, kwargs)
             cached = await backend.get(_cache_key, default=_empty)
             if cached is not _empty:
                 soft_expire_at, result = cached
                 if soft_expire_at > datetime.utcnow():
-                    _from_cache._set(
+                    context_cache_detect._set(
                         _cache_key,
-                        ttl=ttl,
-                        soft_ttl=soft_ttl,
+                        ttl=_ttl,
+                        soft_ttl=_soft_ttl,
                         name="soft",
                         template=_key_template,
                     )
@@ -59,10 +62,10 @@ async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs
             except exceptions:
                 if cached is not _empty:
                     _, result = cached
-                    _from_cache._set(
+                    context_cache_detect._set(
                         _cache_key,
-                        ttl=ttl,
-                        soft_ttl=soft_ttl,
+                        ttl=_ttl,
+                        soft_ttl=_soft_ttl,
                         name="soft",
                         template=_key_template,
                     )
@@ -70,8 +73,8 @@ async def _wrap(*args, _from_cache: CacheDetect = context_cache_detect, **kwargs
                 raise
             else:
                 if condition(result, args, kwargs, _cache_key):
-                    soft_expire_at = datetime.utcnow() + timedelta(seconds=soft_ttl)
-                    await backend.set(_cache_key, [soft_expire_at, result], expire=ttl)
+                    soft_expire_at = datetime.utcnow() + timedelta(seconds=_soft_ttl)
+                    await backend.set(_cache_key, [soft_expire_at, result], expire=_ttl)
                 return result
 
         return _wrap
diff --git a/cashews/decorators/circuit_breaker.py b/cashews/decorators/circuit_breaker.py
index aee3ba8..637ca27 100644
--- a/cashews/decorators/circuit_breaker.py
+++ b/cashews/decorators/circuit_breaker.py
@@ -3,7 +3,8 @@
 from functools import wraps
 from typing import Optional, Tuple, Type, Union
 
-from ..backends.interface import Backend
+from .._typing import TTL, AsyncCallable_T, Decorator
+from ..backends.interface import _BackendInterface
 from ..key import get_cache_key, get_cache_key_template
 
 __all__ = ("circuit_breaker", "CircuitBreakerOpen")
@@ -16,16 +17,16 @@ class CircuitBreakerOpen(Exception):
 
 
 def circuit_breaker(
-    backend: Backend,
+    backend: _BackendInterface,
     errors_rate: int,
-    period: int,
-    ttl: int,
-    half_open_ttl: Optional[int] = None,
+    period: TTL,
+    ttl: TTL,
+    half_open_ttl: Optional[TTL] = None,
     min_calls: int = 1,
-    exceptions: Union[Type[Exception], Tuple[Type[Exception]]] = Exception,
+    exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = Exception,
     key: Optional[str] = None,
     prefix: str = "circuit_breaker",
-):
+) -> Decorator:
     """
     Circuit breaker
     :param backend: cache backend
@@ -39,7 +40,7 @@ def circuit_breaker(
     """
     assert 0 < errors_rate < 100
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _key = ":".join([func.__module__, func.__name__])
         _key_template = get_cache_key_template(func, key=key or _key, prefix=prefix)
 
@@ -82,6 +83,6 @@ def _get_bucket_number(period: int, segments: int) -> int:
     return int((datetime.utcnow().timestamp() % period) / segments)
 
 
-async def _get_buckets_values(backend: Backend, key, segments: int, except_number: int) -> int:
+async def _get_buckets_values(backend: _BackendInterface, key, segments: int, except_number: int) -> int:
     keys = [f"{key}:total:{bucket}" for bucket in range(segments) if bucket != except_number]
     return sum(v for v in await backend.get_many(*keys) if v)
diff --git a/cashews/decorators/locked.py b/cashews/decorators/locked.py
index 35b2e6c..306435f 100644
--- a/cashews/decorators/locked.py
+++ b/cashews/decorators/locked.py
@@ -1,21 +1,23 @@
 from functools import wraps
 from typing import Optional, Union
 
-from ..backends.interface import Backend
+from .._typing import TTL, AsyncCallable_T, Decorator
+from ..backends.interface import _BackendInterface
 from ..exceptions import LockedError
 from ..key import get_cache_key, get_cache_key_template
+from ..ttl import ttl_to_seconds
 
 __all__ = ("locked",)
 
 
 def locked(
-    backend: Backend,
+    backend: _BackendInterface,
     key: Optional[str] = None,
-    ttl: Optional[int] = None,
+    ttl: Optional[TTL] = None,
     max_lock_ttl: int = 10,
     step: Union[float, int] = 0.1,
     prefix: str = "lock",
-):
+) -> Decorator:
     """
     Decorator that can help you to solve Cache stampede problem (https://en.wikipedia.org/wiki/Cache_stampede),
     Lock following function calls till first one will be finished
@@ -24,21 +26,23 @@ def locked(
     :param backend: cache backend
     :param key: custom cache key, may contain alias to args or kwargs passed to a call
     :param ttl: duration to lock wrapped function call
-    :param max_lock_ttl: custom prefix for key, default 'lock'
+    :param max_lock_ttl: default ttl if it not set
+    :param step: duration between lock check
     :param prefix: custom prefix for key, default 'lock'
     """
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         _key_template = get_cache_key_template(func, key=key, prefix=prefix)
 
         @wraps(func)
         async def _wrap(*args, **kwargs):
+            _ttl = ttl_to_seconds(ttl, *args, **kwargs)
             _cache_key = get_cache_key(func, _key_template, args, kwargs)
             try:
-                async with backend.lock(_cache_key, ttl or max_lock_ttl):
+                async with backend.lock(_cache_key, _ttl or max_lock_ttl):
                     return await func(*args, **kwargs)
             except LockedError:
-                if not await backend.is_locked(_cache_key, wait=ttl, step=step):
+                if not await backend.is_locked(_cache_key, wait=_ttl, step=step):
                     return await func(*args, **kwargs)
                 raise
 
diff --git a/cashews/decorators/rate.py b/cashews/decorators/rate.py
index 5c9fe11..1ec5ec4 100644
--- a/cashews/decorators/rate.py
+++ b/cashews/decorators/rate.py
@@ -2,10 +2,12 @@
 from functools import wraps
 from typing import Any, Callable, NoReturn, Optional
 
-from ..backends.interface import Backend
+from .._typing import TTL, AsyncCallable_T, Decorator
+from ..backends.interface import _BackendInterface
 from ..exceptions import RateLimitError
 from ..formatter import register_template
 from ..key import get_cache_key, get_cache_key_template
+from ..ttl import ttl_to_seconds
 
 logger = logging.getLogger(__name__)
 
@@ -15,14 +17,14 @@ def _default_action(*args: Any, **kwargs: Any) -> NoReturn:
 
 
 def rate_limit(
-    backend: Backend,
+    backend: _BackendInterface,
     limit: int,
-    period: int,
-    ttl: int = None,
+    period: TTL,
+    ttl: Optional[TTL] = None,
     key: Optional[str] = None,
     action: Optional[Callable] = None,
     prefix: str = "rate_limit",
-):  # pylint: disable=too-many-arguments
+) -> Decorator:  # pylint: disable=too-many-arguments
     """
     Rate limit for function call. Do not call function if rate limit is reached, and call given action
 
@@ -30,28 +32,31 @@ def rate_limit(
     :param limit: number of calls
     :param period: Period
     :param ttl: time ban, default == period
+    :param key: a cache key template
     :param action: call when rate limit reached, default raise RateLimitError
     :param prefix: custom prefix for key, default 'rate_limit'
     """
     action = _default_action if action is None else action
 
-    def decorator(func):
+    def decorator(func: AsyncCallable_T) -> AsyncCallable_T:
         _key_template = get_cache_key_template(func, key=key, prefix=prefix)
         register_template(func, _key_template)
 
         @wraps(func)
         async def wrapped_func(*args, **kwargs):
+            _ttl = ttl_to_seconds(ttl, *args, **kwargs)
+            _period = ttl_to_seconds(period, *args, **kwargs)
             _cache_key = get_cache_key(func, _key_template, args, kwargs)
 
             requests_count = await backend.incr(key=_cache_key)  # set 1 if not exists
             if requests_count and requests_count > limit:
                 if ttl and requests_count == limit + 1:
-                    await backend.expire(key=_cache_key, timeout=ttl)
+                    await backend.expire(key=_cache_key, timeout=_ttl)
                 logger.info("Rate limit reach for %s", _cache_key)
                 action(*args, **kwargs)
 
             if requests_count == 1:
-                await backend.expire(key=_cache_key, timeout=period)
+                await backend.expire(key=_cache_key, timeout=_period)
 
             return await func(*args, **kwargs)
 
diff --git a/cashews/disable_control.py b/cashews/disable_control.py
index 2747875..8fb6b78 100644
--- a/cashews/disable_control.py
+++ b/cashews/disable_control.py
@@ -1,67 +1,56 @@
 from contextvars import ContextVar
-from typing import Any
+from typing import TYPE_CHECKING, Set
 
-_ALL = "_"
+from ._typing import AsyncCallable_T
+from .commands import ALL, Command
 
+if TYPE_CHECKING:  # pragma: no cover
+    from .backends.interface import Backend
 
-async def _is_disable_middleware(call, *args, backend=None, cmd=None, **kwargs):
-    if backend.is_disable(cmd, _ALL):
-        if cmd == "get":
-            return kwargs.get("default")
+
+async def _is_disable_middleware(call: AsyncCallable_T, cmd: Command, backend: "Backend", *args, **kwargs):
+    if backend.is_disable(cmd):
+        if cmd in (Command.GET, Command.GET_MANY):
+            return kwargs.get("default", None)
         return None
     return await call(*args, **kwargs)
 
 
 class ControlMixin:
-    def __init__(self, *args: Any, **kwargs: Any) -> None:
-        self.__disable = ContextVar(str(id(self)), default=())
-        if "disable" in kwargs:
-            self._set_disable(kwargs.pop("disable"))
-        else:
-            self._set_disable(not kwargs.pop("enable", True))
-        super().__init__(*args, **kwargs)
+    def __init__(self) -> None:
+        self.__disable: ContextVar[Set[Command]] = ContextVar(str(id(self)), default=set())
 
     @property
-    def _disable(self):
-        return list(self.__disable.get(()))
+    def _disable(self) -> Set[Command]:
+        return self.__disable.get()
 
-    def _set_disable(self, value):
-        if value is True:
-            value = [
-                _ALL,
-            ]
-        elif value is False:
-            value = []
-        self.__disable.set(tuple(value))
+    def _set_disable(self, value: Set[Command]) -> None:
+        self.__disable.set(value)
 
-    def is_disable(self, *cmds: str) -> bool:
+    def is_disable(self, *cmds: Command) -> bool:
         _disable = self._disable
         if not cmds and _disable:
             return True
         for cmd in cmds:
-            if cmd.lower() in [c.lower() for c in _disable]:
+            if cmd in _disable:
                 return True
         return False
 
-    def is_enable(self, *cmds) -> bool:
+    def is_enable(self, *cmds: Command) -> bool:
         return not self.is_disable(*cmds)
 
-    def disable(self, *cmds: str) -> None:
-        _disable = self._disable
+    def disable(self, *cmds: Command) -> None:
         if not cmds:
-            _disable = [
-                _ALL,
-            ]
-        if self._disable is False:
-            _disable = []
-        _disable.extend(cmds)
+            _disable = ALL.copy()
+        else:
+            _disable = self._disable.copy()
+            _disable.update(cmds)
         self._set_disable(_disable)
 
-    def enable(self, *cmds: str):
-        _disable = self._disable
+    def enable(self, *cmds: Command) -> None:
         if not cmds:
-            _disable = []
-        for cmd in cmds:
-            if cmd in _disable:
-                _disable.remove(cmd)
+            _disable = set()
+        else:
+            _disable = self._disable.copy()
+            _disable -= set(cmds)
         self._set_disable(_disable)
diff --git a/cashews/exceptions.py b/cashews/exceptions.py
index a5e90bc..4ec4d2e 100644
--- a/cashews/exceptions.py
+++ b/cashews/exceptions.py
@@ -2,12 +2,20 @@ class CacheError(Exception):
     pass
 
 
+class BackendNotAvailableError(CacheError):
+    """For wrong or not available cache alias"""
+
+
+class NotConfiguredError(CacheError):
+    """If cache was not configured"""
+
+
 class UnsupportedPicklerError(CacheError):
     """Unknown or unsupported pickle type."""
 
 
 class UnSecureDataError(CacheError):
-    pass
+    """Unsecure data in cache storage"""
 
 
 class SignIsMissingError(CacheError):
@@ -19,12 +27,12 @@ class WrongKeyError(CacheError):
 
 
 class LockedError(CacheError):
-    pass
+    """Raised if a key already locked"""
 
 
 class CacheBackendInteractionError(CacheError):
-    pass
+    """Raised if redis not available and safe is set to false"""
 
 
 class RateLimitError(CacheError):
-    pass
+    """Raised by @rate_limit if rate limit is reached"""
diff --git a/cashews/formatter.py b/cashews/formatter.py
index a2321a8..f647283 100644
--- a/cashews/formatter.py
+++ b/cashews/formatter.py
@@ -89,9 +89,9 @@ def _decorator(func):
 
     def format_field(self, value, format_spec):
         format_spec, args = self.parse_format_spec(format_spec)
+        value = super().format_field(value, format_spec if format_spec not in self._functions else "")
         if format_spec in self._functions:
             value = self._functions[format_spec](value, *args)
-        value = super().format_field(value, format_spec if format_spec not in self._functions else "")
         return value
 
     @staticmethod
@@ -103,7 +103,7 @@ def parse_format_spec(format_spec):
 
 
 default_formatter = _FuncFormatter(lambda name: "")
-default_formatter._register("len", len)
+default_formatter._register("len", lambda x: str(len(x)))
 
 
 @default_formatter.register("jwt")
diff --git a/cashews/helpers.py b/cashews/helpers.py
index fd7196c..373c716 100644
--- a/cashews/helpers.py
+++ b/cashews/helpers.py
@@ -1,15 +1,22 @@
+from ._typing import AsyncCallable_T, AsyncCallableResult_T, Middleware
+from .backends.interface import Backend
+from .commands import PATTERN_CMDS, Command
 from .key import get_call_values
 from .utils import get_obj_size
 
 
-def add_prefix(prefix: str):
-    async def _middleware(call, *args, backend=None, cmd=None, **kwargs):
-        if cmd.lower() == "get_many":
+def add_prefix(prefix: str) -> Middleware:
+    async def _middleware(
+        call: AsyncCallable_T, cmd: Command, backend: Backend, *args, **kwargs
+    ) -> AsyncCallableResult_T:
+        if cmd == Command.GET_MANY:
             return await call(*[prefix + key for key in args])
         call_values = get_call_values(call, args, kwargs)
-        as_key = "key"
-        if cmd in ["delete_match", "get_match", "keys_match"]:
-            as_key = "pattern"
+        if cmd == Command.SET_MANY:
+            call_values["pairs"] = {prefix + key: value for key, value in call_values["pairs"].items()}
+            return await call(**call_values)
+
+        as_key = "pattern" if cmd in PATTERN_CMDS else "key"
         key = call_values.get(as_key)
         if key:
             call_values[as_key] = prefix + key
@@ -19,14 +26,20 @@ async def _middleware(call, *args, backend=None, cmd=None, **kwargs):
     return _middleware
 
 
-def all_keys_lower():
-    async def _middleware(call, *args, backend=None, cmd=None, **kwargs):
-        if cmd.lower() == "get_many":
+def all_keys_lower() -> Middleware:
+    async def _middleware(
+        call: AsyncCallable_T, cmd: Command, backend: Backend, *args, **kwargs
+    ) -> AsyncCallableResult_T:
+        if cmd == Command.GET_MANY:
             return await call(*[key.lower() for key in args])
         call_values = get_call_values(call, args, kwargs)
-        as_key = "key"
-        if cmd == "delete_match":
-            as_key = "pattern"
+
+        if cmd == Command.SET_MANY:
+            call_values["pairs"] = {key.lower(): value for key, value in call_values["pairs"].items()}
+            return await call(**call_values)
+
+        as_key = "pattern" if cmd in PATTERN_CMDS else "key"
+
         key = call_values.get(as_key)
         if key:
             call_values[as_key] = key.lower()
@@ -36,9 +49,11 @@ async def _middleware(call, *args, backend=None, cmd=None, **kwargs):
     return _middleware
 
 
-def memory_limit(min_bytes=0, max_bytes=None):
-    async def _memory_middleware(call, *args, backend=None, cmd=None, **kwargs):
-        if cmd != "set":
+def memory_limit(min_bytes=0, max_bytes=None) -> Middleware:
+    async def _middleware(
+        call: AsyncCallable_T, cmd: Command, backend: Backend, *args, **kwargs
+    ) -> AsyncCallableResult_T:
+        if cmd != Command.SET:
             return await call(*args, **kwargs)
         call_values = get_call_values(call, args, kwargs)
         value_size = get_obj_size(call_values["value"])
@@ -46,4 +61,4 @@ async def _memory_middleware(call, *args, backend=None, cmd=None, **kwargs):
             return None
         return await call(*args, **kwargs)
 
-    return _memory_middleware
+    return _middleware
diff --git a/cashews/key.py b/cashews/key.py
index aca869c..20a618b 100644
--- a/cashews/key.py
+++ b/cashews/key.py
@@ -1,9 +1,7 @@
 import inspect
-from datetime import timedelta
 from functools import lru_cache
-from typing import Any, Callable, Container, Dict, Optional, Tuple, Union
+from typing import Any, Callable, Container, Dict, Optional, Tuple
 
-from ._typing import TTL
 from .exceptions import WrongKeyError
 from .formatter import _ReplaceFormatter, default_formatter, template_to_pattern
 
@@ -12,43 +10,10 @@
 _ARGS_KWARGS = (_ARGS, _KWARGS)
 
 
-def ttl_to_seconds(ttl: Union[float, None, TTL]) -> Union[int, None, float]:
-    timeout = ttl() if callable(ttl) else ttl
-    if isinstance(timeout, timedelta):
-        return timeout.total_seconds()
-    if isinstance(timeout, str):
-        return _ttl_from_str(timeout)
-    return timeout
-
-
-_STR_TO_DELTA = {
-    "h": timedelta(hours=1),
-    "m": timedelta(minutes=1),
-    "s": timedelta(seconds=1),
-    "d": timedelta(days=1),
-}
-
-
-def _ttl_from_str(ttl: str) -> int:
-    result = 0
-    mul = ""
-    for char in ttl.strip().lower():
-        if char.isdigit():
-            mul += char
-        elif char in _STR_TO_DELTA:
-            result += int(mul) * _STR_TO_DELTA[char].total_seconds()
-            mul = ""
-        else:
-            raise ValueError(f"ttl '{ttl}' has wrong string representation")
-    if mul != "" and not result:
-        return int(mul)
-    return result
-
-
 def get_cache_key(
     func: Callable,
     template: Optional[str] = None,
-    args: Tuple[Any] = (),
+    args: Tuple[Any, ...] = (),
     kwargs: Optional[Dict] = None,
 ) -> str:
     """
diff --git a/cashews/_picklers.py b/cashews/picklers.py
similarity index 100%
rename from cashews/_picklers.py
rename to cashews/picklers.py
diff --git a/cashews/serialize.py b/cashews/serialize.py
index 4462701..80f5aa3 100644
--- a/cashews/serialize.py
+++ b/cashews/serialize.py
@@ -1,19 +1,29 @@
 import hashlib
 import hmac
-from contextlib import suppress
-from typing import Any, Dict, Optional, Tuple, Union
+from typing import Any, Mapping, Optional, Tuple, Union
 
-from ._picklers import DEFAULT_PICKLE, get_pickler
 from .exceptions import SignIsMissingError, UnSecureDataError
+from .picklers import DEFAULT_PICKLE, get_pickler
 
-BLANK_DIGEST = b""
+
+def get_signer(digestmod):
+    def sign(key: bytes, value: bytes):
+        return hmac.new(key, value, digestmod).hexdigest().encode()
+
+    return sign
+
+
+def simple_sign(key: bytes, value: bytes):
+    s = sum(key) + sum(value)
+    return f"{s:x}".encode()
 
 
 class PickleSerializerMixin:
     _digestmods = {
-        b"sha1": hashlib.sha1,
-        b"md5": hashlib.md5,
-        b"sha256": hashlib.sha256,
+        b"sha1": get_signer(hashlib.sha1),
+        b"md5": get_signer(hashlib.md5),
+        b"sha256": get_signer(hashlib.sha256),
+        b"sum": simple_sign,
     }
 
     def __init__(
@@ -26,19 +36,15 @@ def __init__(
         **kwargs: Any,
     ):
         super().__init__(*args, **kwargs)
-        if hash_key is None:
-            digestmod = BLANK_DIGEST
-            self._digestmods = self._digestmods.copy()
-            self._digestmods[BLANK_DIGEST] = lambda value: BLANK_DIGEST  # type: ignore[misc, assignment]
         self._hash_key = _to_bytes(hash_key)
         self._digestmod = _to_bytes(digestmod)
         self._check_repr = check_repr
         self._pickler = get_pickler(pickle_type)
 
     async def get(self, key: str, default: Any = None):
-        return await self._get_value(await super().get(key), key, default=default)
+        return await self._serialize_value(await super().get(key), key, default=default)
 
-    async def _get_value(self, value, key, default=None) -> Any:
+    async def _serialize_value(self, value, key, default=None) -> Any:
         try:
             return self._process_value(value, key, default=default)
         except (self._pickler.PickleError, AttributeError):
@@ -51,39 +57,39 @@ def _process_value(self, value: Union[None, int, bytes], key: str, default=None)
             return value
         if value.isdigit():
             return int(value)
+
+        if not self._hash_key:
+            try:
+                value = self._pickler.loads(value)
+            except self._pickler.PickleError:
+                pass
+            else:
+                if self._check_repr:
+                    repr(value)
+                return value
         try:
-            value = self._split_value_from_signature(value, key)
+            value = self._get_value_without_signature(value, key)
         except SignIsMissingError:
-            return value
+            return default
         value = self._pickler.loads(value)
         if self._check_repr:
             repr(value)
         return value
 
-    def _split_value_from_signature(self, value: bytes, key: str) -> bytes:
-        if self._hash_key:
-            try:
-                sign, value = value.split(b"_", 1)
-            except ValueError as exc:
-                raise SignIsMissingError(f"key: {key}") from exc
-            sign, digestmod = self._get_digestmod(sign)
-            expected_sign = self._get_sign(key, value, digestmod)
-            if expected_sign != sign:
-                raise UnSecureDataError(f"{expected_sign!r} != {sign!r}")
-            return value
-        # Backward compatibility.
-        DeprecationWarning(
-            "If a sign is not used to secure your data, then a value will be pickled and "
-            + "saved without an empty sign prepended. Values saved via 4.x package "
-            + "version without using a sign will not be compatible after the 5.x release."
-        )
-        with suppress(ValueError):
+    def _get_value_without_signature(self, value: bytes, key: str) -> bytes:
+        try:
             sign, value = value.split(b"_", 1)
-            if sign:
-                value = sign + b"_" + value
+        except ValueError as exc:
+            raise SignIsMissingError(f"key: {key}") from exc
+        if not self._hash_key:
+            return value
+        sign, digestmod = self._get_sign_and_digestmod(sign)
+        expected_sign = self._gen_sign(key, value, digestmod)
+        if expected_sign != sign:
+            raise UnSecureDataError(f"{expected_sign!r} != {sign!r}")
         return value
 
-    def _get_digestmod(self, sign: bytes) -> Tuple[bytes, bytes]:
+    def _get_sign_and_digestmod(self, sign: bytes) -> Tuple[bytes, bytes]:
         digestmod = self._digestmod
         if b":" in sign:
             digestmod, sign = sign.split(b":")
@@ -94,7 +100,7 @@ def _get_digestmod(self, sign: bytes) -> Tuple[bytes, bytes]:
     async def get_many(self, *keys: str, default: Optional[Any] = None) -> Any:
         values = []
         for key, value in zip(keys, await super().get_many(*keys, default=default) or [None] * len(keys)):
-            values.append(await self._get_value(value, key))
+            values.append(await self._serialize_value(value, key))
         return tuple(values)
 
     async def set(self, key: str, value: Any, *args: Any, **kwargs: Any):
@@ -103,24 +109,24 @@ async def set(self, key: str, value: Any, *args: Any, **kwargs: Any):
         value = self._pickler.dumps(value)
         return await super().set(key, self._prepend_sign_to_value(key, value), *args, **kwargs)
 
-    async def set_many(self, pairs: Dict[str, Any], *args: Any, **kwargs: Any):
+    async def set_many(self, pairs: Mapping[str, Any], expire: Optional[float] = None):
         transformed_pairs = {}
         for key, value in pairs.items():
             value = self._pickler.dumps(value)
             transformed_pairs[key] = self._prepend_sign_to_value(key, value)
-        return await super().set_many(transformed_pairs, *args, **kwargs)
+        return await super().set_many(transformed_pairs, expire=expire)
 
     def _prepend_sign_to_value(self, key: str, value: bytes) -> bytes:
-        sign = self._get_sign(key, value, self._digestmod)
+        sign = self._gen_sign(key, value, self._digestmod)
         if not sign:
             return value
         return self._digestmod + b":" + sign + b"_" + value
 
-    def _get_sign(self, key: str, value: bytes, digestmod: bytes) -> bytes:
-        if digestmod == BLANK_DIGEST:
-            return BLANK_DIGEST
+    def _gen_sign(self, key: str, value: bytes, digestmod: bytes) -> bytes:
+        if self._hash_key is None:
+            return b""
         value = key.encode() + value
-        return hmac.new(self._hash_key, value, self._digestmods[digestmod]).hexdigest().encode()
+        return self._digestmods[digestmod](self._hash_key, value)
 
     def set_raw(self, *args: Any, **kwargs: Any):
         return super().set(*args, **kwargs)
diff --git a/cashews/ttl.py b/cashews/ttl.py
new file mode 100644
index 0000000..05f22c3
--- /dev/null
+++ b/cashews/ttl.py
@@ -0,0 +1,37 @@
+from datetime import timedelta
+from typing import Union
+
+from cashews._typing import TTL
+
+
+def ttl_to_seconds(ttl: Union[float, None, TTL], *args, **kwargs) -> Union[int, None, float]:
+    timeout = ttl(*args, **kwargs) if callable(ttl) else ttl
+    if isinstance(timeout, timedelta):
+        return timeout.total_seconds()
+    if isinstance(timeout, str):
+        return _ttl_from_str(timeout)
+    return timeout
+
+
+_STR_TO_DELTA = {
+    "h": timedelta(hours=1),
+    "m": timedelta(minutes=1),
+    "s": timedelta(seconds=1),
+    "d": timedelta(days=1),
+}
+
+
+def _ttl_from_str(ttl: str) -> int:
+    result = 0
+    mul = ""
+    for char in ttl.strip().lower():
+        if char.isdigit():
+            mul += char
+        elif char in _STR_TO_DELTA:
+            result += int(mul) * int(_STR_TO_DELTA[char].total_seconds())
+            mul = ""
+        else:
+            raise ValueError(f"ttl '{ttl}' has wrong string representation")
+    if mul != "" and not result:
+        return int(mul)
+    return result
diff --git a/cashews/utils/__init__.py b/cashews/utils/__init__.py
index 9200f48..5aba0b6 100644
--- a/cashews/utils/__init__.py
+++ b/cashews/utils/__init__.py
@@ -3,4 +3,4 @@
 except ImportError:
     from ._bitarray import Bitarray
 from .object_size import get_obj_size
-from .split_hash import get_hashes
+from .split_hash import get_indexes
diff --git a/cashews/utils/split_hash.py b/cashews/utils/split_hash.py
index b3616dd..4de926c 100644
--- a/cashews/utils/split_hash.py
+++ b/cashews/utils/split_hash.py
@@ -1,5 +1,5 @@
-import hashlib
 import zlib
+from typing import Set
 
 algorithms = [
     zlib.crc32,
@@ -20,37 +20,20 @@
     )
 
 
-def get_hashes(key: str, k: int, max_i: int):
+def get_indexes(key: str, number_of_buckets: int, max_index: int) -> Set[int]:
     """
     return array with bit indexes for given value (key) [23, 45, 15]
     """
 
-    assert max_i >= k
+    assert max_index >= number_of_buckets
 
     indexes = set()
-    for i in range(k):
+    for i in range(number_of_buckets):
         ii = i % len(algorithms)
 
-        value = algorithms[ii](f"{key}_{i}".encode()) % max_i
+        value = algorithms[ii](f"{key}_{i}".encode()) % max_index
         while value in indexes:
             i += 1
-            value = algorithms[ii](f"{key}_{i}".encode()) % max_i
+            value = algorithms[ii](f"{key}_{i}".encode()) % max_index
         indexes.add(value)
     return indexes
-    # str_hash = str(_get_string_int_hash(key))
-    # indexes = set()
-    # for _hash in _split_string_for_chunks(str_hash, k):
-    #     value = int(_hash) % max_i
-    #     while value in indexes:
-    #         value += 1
-    #     indexes.add(value)
-    # return indexes
-
-
-def _get_string_int_hash(key):
-    return int(hashlib.sha256(key.encode("utf-8")).hexdigest(), 16)
-
-
-def _split_string_for_chunks(value: str, chunks: int):
-    chunk_size = len(value) // chunks
-    return (value[i : i + chunk_size] for i in range(0, chunk_size * chunks, chunk_size))
diff --git a/cashews/validation.py b/cashews/validation.py
index 40895e5..7545fd0 100644
--- a/cashews/validation.py
+++ b/cashews/validation.py
@@ -1,14 +1,17 @@
 import asyncio
+from contextlib import contextmanager
 from contextvars import ContextVar
 from functools import wraps
 from typing import Any, Callable, Dict, Optional, Union
 
-from .backends.interface import Backend
+from ._typing import AsyncCallable_T
+from .backends.interface import _BackendInterface
+from .commands import RETRIEVE_CMDS, Command
 from .formatter import get_templates_for_func, template_to_pattern
 from .key import get_call_values, get_func_params
 
 
-async def invalidate_func(backend: Backend, func, kwargs: Optional[Dict] = None) -> None:
+async def invalidate_func(backend: _BackendInterface, func, kwargs: Optional[Dict] = None) -> None:
     values = {**{param: "*" for param in get_func_params(func)}, **kwargs}
     for template in get_templates_for_func(func):
         del_template = template_to_pattern(template, **values)
@@ -16,7 +19,7 @@ async def invalidate_func(backend: Backend, func, kwargs: Optional[Dict] = None)
 
 
 def invalidate(
-    backend: Backend,
+    backend: _BackendInterface,
     target: Union[str, Callable],
     args_map: Optional[Dict[str, str]] = None,
     defaults: Optional[Dict[str, Any]] = None,
@@ -24,7 +27,7 @@ def invalidate(
     args_map = args_map or {}
     defaults = defaults or {}
 
-    def _decor(func):
+    def _decor(func: AsyncCallable_T) -> AsyncCallable_T:
         @wraps(func)
         async def _wrap(*args, **kwargs):
             result = await func(*args, **kwargs)
@@ -48,15 +51,30 @@ async def _wrap(*args, **kwargs):
 _INVALIDATE_FURTHER = ContextVar("invalidate", default=False)
 
 
-def set_invalidate_further():
+@contextmanager
+def invalidate_further():
     _INVALIDATE_FURTHER.set(True)
+    try:
+        yield
+    finally:
+        _INVALIDATE_FURTHER.set(False)
 
 
-async def _invalidate_middleware(call, *args, key=None, backend=None, cmd=None, **kwargs):
-    if _INVALIDATE_FURTHER.get() and key is not None and cmd != "delete":
-        asyncio.create_task(backend.delete(key))
-        _INVALIDATE_FURTHER.set(False)
-        return kwargs.get("default")
-    if key is None:
-        return await call(*args, **kwargs)
-    return await call(*args, key=key, **kwargs)
+async def _aiter(num=0):  # pragma: no cover
+    """A trick for typing"""
+    for i in range(num):
+        yield i
+
+
+async def _invalidate_middleware(call, cmd: Command, backend: _BackendInterface, *args, **kwargs):
+    if _INVALIDATE_FURTHER.get() and cmd in RETRIEVE_CMDS:
+        if "key" in kwargs:
+            asyncio.create_task(backend.delete(kwargs["key"]))
+            return kwargs.get("default")
+        if cmd == Command.GET_MATCH:
+            asyncio.create_task(backend.delete_match(kwargs["pattern"]))
+            return _aiter()
+        if cmd == Command.GET_MANY:
+            asyncio.create_task(backend.delete_many(*args))
+            return ()
+    return await call(*args, **kwargs)
diff --git a/cashews/wrapper.py b/cashews/wrapper.py
index ec74e69..325fa2a 100644
--- a/cashews/wrapper.py
+++ b/cashews/wrapper.py
@@ -1,35 +1,38 @@
 import asyncio
 from contextlib import contextmanager
-from functools import partial, wraps
-from typing import Any, AsyncIterator, Callable, Dict, Iterable, Mapping, Optional, Tuple, Type, Union
+from functools import lru_cache, partial, wraps
+from typing import Any, AsyncIterator, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, Type, Union
 
 from . import decorators, validation
-from ._cache_condition import create_time_condition, get_cache_condition
-from ._settings import settings_url_parse
-from ._typing import TTL, CacheCondition
-from .backends.interface import Backend
-from .backends.memory import Memory
-from .disable_control import ControlMixin, _is_disable_middleware
-from .key import ttl_to_seconds
+from ._typing import TTL, AsyncCallable_T, AsyncCallableResult_T, CacheCondition, Middleware
+from .backend_settings import settings_url_parse
+from .backends.interface import Backend, _BackendInterface
+from .cache_condition import create_time_condition, get_cache_condition
+from .commands import Command
+from .disable_control import _is_disable_middleware
+from .exceptions import NotConfiguredError
+from .ttl import ttl_to_seconds
 
 try:
     from .backends.client_side import BcastClientSide
 except ImportError:
-    BcastClientSide = None
+    BcastClientSide = None  # type: ignore
 
 try:
     from .backends.diskcache import DiskCache
 except ImportError:
-    DiskCache = None
+    DiskCache = None  # type: ignore
 
 
 #  pylint: disable=too-many-public-methods
 
 
-def _create_auto_init():
+def _create_auto_init() -> Middleware:
     lock = asyncio.Lock()
 
-    async def _auto_init(call, *args, backend=None, cmd=None, **kwargs):
+    async def _auto_init(
+        call: AsyncCallable_T, cmd: Command, backend: Backend, *args, **kwargs
+    ) -> AsyncCallableResult_T:
         if backend.is_init:
             return await call(*args, **kwargs)
         async with lock:
@@ -41,78 +44,75 @@ async def _auto_init(call, *args, backend=None, cmd=None, **kwargs):
     return _auto_init
 
 
-class Cache(Backend):
+class Cache(_BackendInterface):
     default_prefix = ""
 
-    def __init__(self, name: Optional[str] = None):
-        self._backends: Dict[str, Tuple[Backend, Tuple[Callable]]] = {}  # {key: (backend, middleware)}
-        self._default_middlewares = (
+    def __init__(self, name: str = ""):
+        self._backends: Dict[str, Tuple[Backend, Tuple[Middleware, ...]]] = {}  # {key: (backend, middleware)}
+        self._default_middlewares: Tuple[Middleware, ...] = (
             _is_disable_middleware,
             _create_auto_init(),
             validation._invalidate_middleware,
         )
-        self._name = name
-        self._default_fail_exceptions = Exception
-        self._add_backend(Memory)
+        self.name = name
+        self._default_fail_exceptions: Tuple[Type[Exception], ...] = (Exception,)
 
     detect = decorators.context_cache_detect
 
-    def set_default_fail_exceptions(self, *exc: Type[Exception]):
+    def set_default_fail_exceptions(self, *exc: Type[Exception]) -> None:
         self._default_fail_exceptions = exc
 
-    def disable(self, *cmds: str, prefix: str = ""):
+    def disable(self, *cmds: Command, prefix: str = "") -> None:
         return self._get_backend(prefix).disable(*cmds)
 
-    def disable_all(self, *cmds: str):
-        for backend, _ in self._backends.values():
-            backend.disable(*cmds)
-
-    def enable(self, *cmds: str, prefix: str = ""):
+    def enable(self, *cmds: Command, prefix: str = "") -> None:
         return self._get_backend(prefix).enable(*cmds)
 
-    def enable_all(self, *cmds: str):
-        for backend, _ in self._backends.values():
-            backend.enable(*cmds)
-
     @contextmanager
-    def disabling(self, *cmds: str, prefix: str = ""):
+    def disabling(self, *cmds: Command, prefix: str = ""):
         self.disable(*cmds, prefix=prefix)
-        yield
-        self.enable(*cmds, prefix=prefix)
+        try:
+            yield
+        finally:
+            self.enable(*cmds, prefix=prefix)
 
-    def is_disable(self, *cmds: str, prefix: str = ""):
+    def is_disable(self, *cmds: Command, prefix: str = ""):
         return self._get_backend(prefix).is_disable(*cmds)
 
-    def is_enable(self, *cmds: str, prefix: str = ""):
+    def is_enable(self, *cmds: Command, prefix: str = ""):
         return not self.is_disable(*cmds, prefix=prefix)
 
-    def _get_backend_and_config(self, key: str) -> Tuple[Backend, Tuple[Callable]]:
+    @lru_cache(maxsize=1000)
+    def _get_backend_and_config(self, key: str) -> Tuple[Backend, Tuple[Middleware, ...]]:
         for prefix in sorted(self._backends.keys(), reverse=True):
             if key.startswith(prefix):
                 return self._backends[prefix]
+        if self.default_prefix not in self._backends:
+            raise NotConfiguredError("run `cache.setup(...)` before using cache")
         return self._backends[self.default_prefix]
 
     def _get_backend(self, key: str) -> Backend:
         backend, _ = self._get_backend_and_config(key)
         return backend
 
-    def setup(self, settings_url: str, middlewares: Tuple = (), prefix: str = default_prefix, **kwargs):
-        params = settings_url_parse(settings_url)
+    def setup(self, settings_url: str, middlewares: Tuple = (), prefix: str = default_prefix, **kwargs) -> Backend:
+        backend_class, params = settings_url_parse(settings_url)
         params.update(kwargs)
 
-        if params.pop("client_side", None):
-            params["backend"] = BcastClientSide
-        backend = params.pop("backend")
+        if "disable" in params:
+            disable = params.pop("disable")
+        else:
+            disable = not params.pop("enable", True)
 
-        self._add_backend(backend, middlewares, prefix, **params)
-        return self._backends[prefix][0]
-
-    def _add_backend(self, backend_class: Type[Backend], middlewares=(), prefix: str = default_prefix, **params):
-        class _backend_class(ControlMixin, backend_class):
-            pass
+        backend = backend_class(**params)
+        if disable:
+            backend.disable()
+        self._add_backend(backend, middlewares, prefix)
+        return backend
 
+    def _add_backend(self, backend: Backend, middlewares=(), prefix: str = default_prefix):
         self._backends[prefix] = (
-            _backend_class(**params),
+            backend,
             self._default_middlewares + middlewares,
         )
 
@@ -129,14 +129,14 @@ def is_init(self) -> bool:
                 return False
         return True
 
-    def _with_middlewares(self, cmd: str, key):
+    def _with_middlewares(self, cmd: Command, key: str):
         backend, middlewares = self._get_backend_and_config(key)
         return self._with_middlewares_for_backend(cmd, backend, middlewares)
 
-    def _with_middlewares_for_backend(self, cmd: str, backend, middlewares):
-        call = getattr(backend, cmd)
+    def _with_middlewares_for_backend(self, cmd: Command, backend, middlewares):
+        call = getattr(backend, cmd.value)
         for middleware in middlewares:
-            call = partial(middleware, call, cmd=cmd, backend=backend)
+            call = partial(middleware, call, cmd, backend)
         return call
 
     async def set(
@@ -146,118 +146,117 @@ async def set(
         expire: Union[float, TTL, None] = None,
         exist: Optional[bool] = None,
     ) -> bool:
-        return await self._with_middlewares("set", key)(
+        return await self._with_middlewares(Command.SET, key)(
             key=key, value=value, expire=ttl_to_seconds(expire), exist=exist
         )
 
     async def set_raw(self, key: str, value: Any, **kwargs):
-        return await self._with_middlewares("set_raw", key)(key=key, value=value, **kwargs)
+        return await self._with_middlewares(Command.SET_RAW, key)(key=key, value=value, **kwargs)
 
     async def get(self, key: str, default: Optional[Any] = None) -> Any:
-        return await self._with_middlewares("get", key)(key=key, default=default)
+        return await self._with_middlewares(Command.GET, key)(key=key, default=default)
 
     async def get_raw(self, key: str) -> Any:
-        return await self._with_middlewares("get_raw", key)(key=key)
+        return await self._with_middlewares(Command.GET_RAW, key)(key=key)
 
-    async def keys_match(self, pattern: str) -> AsyncIterator[str]:
+    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:  # type: ignore
         backend, middlewares = self._get_backend_and_config(pattern)
 
-        async def call(_pattern):
-            return backend.keys_match(_pattern)
+        async def call(pattern, batch_size):
+            return backend.scan(pattern, batch_size=batch_size)
 
         for middleware in middlewares:
-            call = partial(middleware, call, cmd="keys_match", backend=backend)
-        async for key in (await call(pattern)):
+            call = partial(middleware, call, Command.SCAN, backend)
+        async for key in (await call(pattern=pattern, batch_size=batch_size)):
             yield key
 
-    async def scan(self, pattern: str, batch_size: int = 100) -> AsyncIterator[str]:
-        backend, middlewares = self._get_backend_and_config(pattern)
-
-        async def call(_pattern):
-            return backend.scan(_pattern)
-
-        for middleware in middlewares:
-            call = partial(middleware, call, cmd="scan", backend=backend)
-        async for key in (await call(pattern)):
-            yield key
-
-    async def get_match(
-        self, pattern: str, batch_size: int = 100, default: Optional[Any] = None
+    async def get_match(  # type: ignore
+        self,
+        pattern: str,
+        batch_size: int = 100,
     ) -> AsyncIterator[Tuple[str, Any]]:
         backend, middlewares = self._get_backend_and_config(pattern)
 
-        async def call(_pattern, _batch_size, _default):
-            return backend.get_match(_pattern, batch_size=_batch_size, default=_default)
+        async def call(pattern, batch_size):
+            return backend.get_match(pattern, batch_size=batch_size)
 
         for middleware in middlewares:
-            call = partial(middleware, call, cmd="get_match", backend=backend)
-        async for key, value in (await call(pattern, batch_size, default)):
+            call = partial(middleware, call, Command.GET_MATCH, backend)
+        async for key, value in (await call(pattern=pattern, batch_size=batch_size)):
             yield key, value
 
-    async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Any]:
-        backends = {}
+    async def get_many(self, *keys: str, default: Optional[Any] = None) -> Tuple[Any, ...]:
+        backends: Dict[Backend, List[str]] = {}
         for key in keys:
             backend = self._get_backend(key)
             backends.setdefault(backend, []).append(key)
-        result = {}
+        result: Dict[str, Any] = {}
         for _keys in backends.values():
-            _values = await self._with_middlewares("get_many", _keys[0])(*_keys, default=default)
+            _values = await self._with_middlewares(Command.GET_MANY, _keys[0])(*_keys, default=default)
             result.update(dict(zip(_keys, _values)))
         return tuple(result.get(key) for key in keys)
 
     async def set_many(self, pairs: Mapping[str, Any], expire: Union[float, TTL, None] = None):
-        backends = {}
+        backends: Dict[Backend, List[str]] = {}
         for key in pairs:
             backend = self._get_backend(key)
             backends.setdefault(backend, []).append(key)
         for backend, keys in backends.items():
             data = {key: pairs[key] for key in keys}
-            await self._with_middlewares("set_many", keys[0])(data, expire=ttl_to_seconds(expire))
+            await self._with_middlewares(Command.SET_MANY, keys[0])(pairs=data, expire=ttl_to_seconds(expire))
 
-    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int]:
-        return await self._with_middlewares("get_bits", key)(key, *indexes, size=size)
+    async def get_bits(self, key: str, *indexes: int, size: int = 1) -> Tuple[int, ...]:
+        return await self._with_middlewares(Command.GET_BITS, key)(key, *indexes, size=size)
 
     async def incr_bits(self, key: str, *indexes: int, size: int = 1, by: int = 1):
-        return await self._with_middlewares("incr_bits", key)(key, *indexes, size=size, by=by)
+        return await self._with_middlewares(Command.INCR_BITS, key)(key, *indexes, size=size, by=by)
 
     async def incr(self, key: str) -> int:
-        return await self._with_middlewares("incr", key)(key=key)
+        return await self._with_middlewares(Command.INCR, key)(key=key)
 
     async def delete(self, key: str):
-        return await self._with_middlewares("delete", key)(key=key)
+        return await self._with_middlewares(Command.DELETE, key)(key=key)
+
+    async def delete_many(self, *keys: str) -> None:
+        backends: Dict[Backend, List[str]] = {}
+        for key in keys:
+            backend = self._get_backend(key)
+            backends.setdefault(backend, []).append(key)
+        for _keys in backends.values():
+            await self._with_middlewares(Command.DELETE_MANY, _keys[0])(*_keys)
 
     async def delete_match(self, pattern: str):
-        return await self._with_middlewares("delete_match", pattern)(pattern=pattern)
+        return await self._with_middlewares(Command.DELETE_MATCH, pattern)(pattern=pattern)
 
     async def expire(self, key: str, timeout: TTL):
-        return await self._with_middlewares("expire", key)(key=key, timeout=ttl_to_seconds(timeout))
+        return await self._with_middlewares(Command.EXPIRE, key)(key=key, timeout=ttl_to_seconds(timeout))
 
     async def get_expire(self, key: str):
-        return await self._with_middlewares("get_expire", key)(key=key)
+        return await self._with_middlewares(Command.GET_EXPIRE, key)(key=key)
 
     async def exists(self, key: str):
-        return await self._with_middlewares("exists", key)(key=key)
+        return await self._with_middlewares(Command.EXIST, key)(key=key)
 
     async def set_lock(self, key: str, value: Any, expire: TTL):
-        return await self._with_middlewares("set_lock", key)(key=key, value=value, expire=ttl_to_seconds(expire))
+        return await self._with_middlewares(Command.SET_LOCK, key)(key=key, value=value, expire=ttl_to_seconds(expire))
 
     async def unlock(self, key: str, value: str):
-        return await self._with_middlewares("unlock", key)(key=key, value=value)
+        return await self._with_middlewares(Command.UNLOCK, key)(key=key, value=value)
 
     async def get_size(self, key: str):
-        return await self._with_middlewares("get_size", key)(key)
+        return await self._with_middlewares(Command.GET_SIZE, key)(key=key)
 
-    async def ping(self, message: Optional[bytes] = None) -> str:
+    async def ping(self, message: Optional[bytes] = None) -> bytes:
         message = b"PING" if message is None else message
-        return await self._with_middlewares("ping", message.decode())(message=message)
+        return await self._with_middlewares(Command.PING, message.decode())(message=message)
 
     async def clear(self):
         for backend, _ in self._backends.values():
-            await self._with_middlewares_for_backend("clear", backend, self._default_middlewares)()
+            await self._with_middlewares_for_backend(Command.CLEAR, backend, self._default_middlewares)()
 
-    def close(self):
+    async def close(self):
         for backend, _ in self._backends.values():
-            backend.close()
+            await backend.close()
 
     async def is_locked(
         self,
@@ -265,7 +264,7 @@ async def is_locked(
         wait: Union[float, None, TTL] = None,
         step: Union[int, float] = 0.1,
     ) -> bool:
-        return await self._with_middlewares("is_locked", key)(key=key, wait=ttl_to_seconds(wait), step=step)
+        return await self._with_middlewares(Command.IS_LOCKED, key)(key=key, wait=ttl_to_seconds(wait), step=step)
 
     def _wrap_on(self, decorator_fabric, upper, **decor_kwargs):
         if upper:
@@ -273,7 +272,7 @@ def _wrap_on(self, decorator_fabric, upper, **decor_kwargs):
         return self._wrap(decorator_fabric, **decor_kwargs)
 
     def _wrap(self, decorator_fabric, lock=False, time_condition=None, **decor_kwargs):
-        def _decorator(func):
+        def _decorator(func: AsyncCallable_T) -> AsyncCallable_T:
             if time_condition is not None:
                 condition, _decor = create_time_condition(time_condition)
                 func = _decor(func)
@@ -289,13 +288,12 @@ async def _call(*args, **kwargs):
                 else:
                     return await decorator(*args, **kwargs)
 
-            _call.direct = func
             return _call
 
         return _decorator
 
     def _wrap_with_condition(self, decorator_fabric, condition, lock=False, time_condition=None, **decor_kwargs):
-        def _decorator(func):
+        def _decorator(func: AsyncCallable_T) -> AsyncCallable_T:
             _condition = condition
             if time_condition is not None:
                 _condition, _decor = create_time_condition(time_condition)
@@ -320,7 +318,6 @@ def new_condition(result, _args, _kwargs, key):
 
                 return result
 
-            _call.direct = func
             return _call
 
         return _decorator
@@ -340,7 +337,7 @@ def __call__(
             decorators.cache,
             upper,
             lock=lock,
-            ttl=ttl_to_seconds(ttl),
+            ttl=ttl,
             key=key,
             condition=get_cache_condition(condition),
             time_condition=ttl_to_seconds(time_condition),
@@ -361,7 +358,7 @@ def failover(
         exceptions = exceptions or self._default_fail_exceptions
         return self._wrap_with_condition(
             decorators.failover,
-            ttl=ttl_to_seconds(ttl),
+            ttl=ttl,
             exceptions=exceptions,
             key=key,
             condition=get_cache_condition(condition),
@@ -382,9 +379,9 @@ def early(
         return self._wrap_on(
             decorators.early,
             upper,
-            ttl=ttl_to_seconds(ttl),
+            ttl=ttl,
             key=key,
-            early_ttl=ttl_to_seconds(early_ttl),
+            early_ttl=early_ttl,
             condition=get_cache_condition(condition),
             time_condition=ttl_to_seconds(time_condition),
             prefix=prefix,
@@ -395,7 +392,7 @@ def soft(
         ttl: TTL,
         key: Optional[str] = None,
         soft_ttl: Optional[TTL] = None,
-        exceptions: Union[Type[Exception], Tuple[Type[Exception]]] = Exception,
+        exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = Exception,
         condition: CacheCondition = None,
         time_condition: Optional[TTL] = None,
         prefix: str = "soft",
@@ -404,7 +401,7 @@ def soft(
         return self._wrap_on(
             decorators.soft,
             upper,
-            ttl=ttl_to_seconds(ttl),
+            ttl=ttl,
             key=key,
             soft_ttl=ttl_to_seconds(soft_ttl),
             exceptions=exceptions,
@@ -427,7 +424,7 @@ def hit(
         return self._wrap_on(
             decorators.hit,
             upper,
-            ttl=ttl_to_seconds(ttl),
+            ttl=ttl,
             cache_hits=cache_hits,
             update_after=update_after,
             key=key,
@@ -448,7 +445,7 @@ def dynamic(
         return self._wrap_on(
             decorators.hit,
             upper,
-            ttl=ttl_to_seconds(ttl),
+            ttl=ttl,
             cache_hits=3,
             update_after=1,
             key=key,
@@ -477,17 +474,19 @@ def circuit_breaker(
         errors_rate: int,
         period: TTL,
         ttl: TTL,
-        exceptions: Union[Type[Exception], Tuple[Type[Exception]], None] = None,
+        half_open_ttl: TTL = None,
+        exceptions: Union[Type[Exception], Tuple[Type[Exception], ...], None] = None,
         key: Optional[str] = None,
         prefix: str = "circuit_breaker",
     ):
-        exceptions = exceptions or self._default_fail_exceptions
+        _exceptions = exceptions or self._default_fail_exceptions
         return decorators.circuit_breaker(
             backend=self,
             errors_rate=errors_rate,
             period=ttl_to_seconds(period),
             ttl=ttl_to_seconds(ttl),
-            exceptions=exceptions,
+            half_open_ttl=ttl_to_seconds(half_open_ttl),
+            exceptions=_exceptions,
             key=key,
             prefix=prefix,
         )
@@ -503,8 +502,8 @@ def rate_limit(
         return decorators.rate_limit(
             backend=self,
             limit=limit,
-            period=ttl_to_seconds(period),
-            ttl=ttl_to_seconds(ttl),
+            period=period,
+            ttl=ttl,
             action=action,
             prefix=prefix,
         )
@@ -518,7 +517,7 @@ def locked(
     ):
         return decorators.locked(
             backend=self,
-            ttl=ttl_to_seconds(ttl),
+            ttl=ttl,
             key=key,
             step=step,
             prefix=prefix,
@@ -526,19 +525,16 @@ def locked(
 
     def bloom(
         self,
+        *,
+        capacity: int,
         name: Optional[str] = None,
-        index_size: Optional[int] = None,
-        number_of_hashes: Optional[int] = None,
         false_positives: Optional[Union[float, int]] = 1,
-        capacity: Optional[int] = None,
         check_false_positive: bool = True,
         prefix: str = "bloom",
     ):
         return decorators.bloom(
             backend=self,
             name=name,
-            index_size=index_size,
-            number_of_hashes=number_of_hashes,
             false_positives=false_positives,
             capacity=capacity,
             check_false_positive=check_false_positive,
@@ -547,19 +543,16 @@ def bloom(
 
     def dual_bloom(
         self,
+        *,
+        capacity: int,
         name: Optional[str] = None,
-        index_size: Optional[int] = None,
-        number_of_hashes: Optional[int] = None,
         false: Optional[Union[float, int]] = 1,
         no_collisions: bool = False,
-        capacity: Optional[int] = None,
         prefix: str = "dual_bloom",
     ):
         return decorators.dual_bloom(
             backend=self,
             name=name,
-            index_size=index_size,
-            number_of_hashes=number_of_hashes,
             false=false,
             no_collisions=no_collisions,
             capacity=capacity,
diff --git a/examples/bench_app.py b/examples/bench_app.py
index 4232b93..690d16e 100644
--- a/examples/bench_app.py
+++ b/examples/bench_app.py
@@ -1,18 +1,15 @@
 import asyncio
 import random
 import string
+import time
 from datetime import timedelta
 
 from fastapi import FastAPI
 
-from cashews import Cache, LockedError, RateLimitError, cache, context_cache_detect, mem, utils  # noqa: F401
+from cashews import Cache, Command, LockedError, RateLimitError, cache, context_cache_detect, mem, utils  # noqa: F401
 
 app = FastAPI()
-cache.setup("mem://")
-# cache.setup("disk://")
-# cache.setup("redis://?safe=True&maxsize=20&create_connection_timeout=0.01")
-# cache.setup("redis://?safe=True&maxsize=20&create_connection_timeout=0.01", client_side=True)
-# cache.setup("redis://?safe=True&maxsize=20&create_connection_timeout=0.01", client_side=True, local_cache=Cache("disk").setup("disk://"))  # noqa: 139
+cache.setup("redis://", client_side=True)
 
 
 @app.get("/")
@@ -30,12 +27,40 @@ async def early():
 
 
 @app.get("/hit")
-@cache.hit(ttl=timedelta(minutes=1), cache_hits=100, update_before=80)
+@cache.hit(ttl=timedelta(minutes=1), cache_hits=100, update_after=80)
 async def hit():
     await asyncio.sleep(1)
     return "".join([random.choice(string.ascii_letters) for _ in range(10)])
 
 
+@app.middleware("http")
+async def add_process_time_header(request, call_next):
+    start_time = time.perf_counter()
+    response = await call_next(request)
+    process_time = time.perf_counter() - start_time
+    response.headers["X-Process-Time"] = str(process_time)
+    return response
+
+
+@app.middleware("http")
+async def add_from_cache_headers(request, call_next):
+    with cache.detect as detector:
+        response = await call_next(request)
+        if request.method.lower() != "get":
+            return response
+        if detector.calls:
+            response.headers["X-From-Cache-keys"] = ";".join(detector.calls.keys())
+    return response
+
+
+@app.middleware("http")
+async def disable_middleware(request, call_next):
+    if request.headers.get("X-No-Cache"):
+        with cache.disabling(Command.GET):
+            return await call_next(request)
+    return await call_next(request)
+
+
 if __name__ == "__main__":
     import uvicorn
 
diff --git a/setup.cfg b/setup.cfg
index a260976..b02149d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,12 +22,15 @@ classifiers =
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
+    Programming Language :: Python :: 3.11
 
 [options]
 python_requires = >=3.7
 packages = find:
 include_package_data = true
 zip_safe = false
+install_requires =
+    typing_extensions;python_version=='3.7'
 
 [options.extras_require]
 redis =
diff --git a/tox.ini b/tox.ini
index e872571..172a341 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,9 @@
 [tox]
 envlist =
-    {py37,py38,py39,310}
-    {py37,py38,py39,py310}-redis
-    {py37,py38,py39,py310}-diskcache
-    {py37,py38,py39,py310}-integration
+    {py37,py38,py39,py310,py311}
+    {py37,py38,py39,py310,py311}-redis
+    {py37,py38,py39,py310,py311}-diskcache
+    {py37,py38,py39,py310,py311}-integration
     coverage
 skip_missing_interpreters = true
 
@@ -12,22 +12,22 @@ setenv =
     PYTHONPATH = {toxinidir}/tests
     COVERAGE_FILE = {toxworkdir}/.coverage.{envname}
     MARKER = not redis and not integration and not diskcache
-    {py,py37,py38,py39,py310}-redis:       MARKER = redis and not diskcache
-    {py,py37,py38,py39,py310}-diskcache:   MARKER = diskcache and not redis
-    {py,py37,py38,py39,py310}-integration: MARKER = integration
+    {py,py37,py38,py39,py310,py311}-redis:       MARKER = redis and not diskcache
+    {py,py37,py38,py39,py310,py311}-diskcache:   MARKER = diskcache and not redis
+    {py,py37,py38,py39,py310,py311}-integration: MARKER = integration
 deps =
     pytest
     pytest-asyncio
     pytest-cov
     hypothesis
-    {py,py37,py38,py39,py310}-integration: aiohttp
-    {py,py37,py38,py39,py310}-integration: fastapi
-    {py,py37,py38,py39,py310}-integration: requests
-    {py,py37,py38,py39,py310}-integration: dill
-    {py,py37,py38,py39,py310}-integration: SQLAlchemy
+    {py,py37,py38,py39,py310,py311}-integration: aiohttp
+    {py,py37,py38,py39,py310,py311}-integration: fastapi
+    {py,py37,py38,py39,py310,py311}-integration: httpx
+    {py,py37,py38,py39,py310,py311}-integration: dill
+    {py,py37,py38,py39,py310,py311}-integration: SQLAlchemy
 extras =
-    {py,py37,py38,py39}-redis: redis,speedup
-    {py,py37,py38,py39}-diskcache: diskcache
+    {py,py37,py38,py39,py310,py311}-redis: redis,speedup
+    {py,py37,py38,py39,py310,py311}-diskcache: diskcache
 commands =
     pytest --cov --cov-fail-under=0 \
     -m "{env:MARKER}" \
@@ -45,7 +45,7 @@ commands =
     coverage report
     coverage xml -o {toxworkdir}/coverage.xml
 depends =
-    {py37,py38,py39,py310}
-    {py37,py38,py39,py310}-redis
-    {py37,py38,py39}-diskcache
-    {py37,py38,py39,py310}-integration
+    {py37,py38,py39,py310,py311}
+    {py37,py38,py39,py310,py311}-redis
+    {py37,py38,py39,py310,py311}-diskcache
+    {py37,py38,py39,py310,py311}-integration


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index de55335..ba2bb24 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -24,7 +24,7 @@ jobs:
     strategy:
       max-parallel: 4
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10"]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
 
     steps:
       - uses: actions/checkout@v2
diff --git a/tests/conftest.py b/tests/conftest.py
index 033755c..45cba39 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,7 +5,7 @@
 
 import pytest
 
-if TYPE_CHECKING:
+if TYPE_CHECKING:  # pragma: no cover
     from cashews.backends.interface import Backend
 
 
@@ -30,19 +30,10 @@ def redis_dsn():
 
 @pytest.fixture
 def backend_factory():
-    backend = None
-
     async def factory(backend_cls: type[Backend], *args, **kwargs):
-        nonlocal backend
-
         backend = backend_cls(*args, **kwargs)
         await backend.init()
         await backend.clear()
-
         return backend
 
-    try:
-        yield factory
-    finally:
-        assert backend is not None, "Fixture `backend_factory` wasn't called."
-        backend.close()
+    return factory
diff --git a/tests/test_backend.py b/tests/test_backend.py
index 8a7cab4..fc830be 100644
--- a/tests/test_backend.py
+++ b/tests/test_backend.py
@@ -1,5 +1,5 @@
 import asyncio
-import sys
+from uuid import uuid4
 
 import pytest
 import pytest_asyncio
@@ -15,6 +15,8 @@
     params=[
         "memory",
         pytest.param("redis", marks=pytest.mark.redis),
+        pytest.param("redis_hash", marks=pytest.mark.redis),
+        pytest.param("redis_cs", marks=pytest.mark.redis),
         pytest.param("diskcache", marks=pytest.mark.diskcache),
     ],
 )
@@ -23,13 +25,24 @@ async def _cache(request, redis_dsn, backend_factory):
         from cashews.backends.diskcache import DiskCache
 
         backend = await backend_factory(DiskCache, shards=0)
-        yield backend
     elif request.param == "redis":
         from cashews.backends.redis import Redis
 
-        yield await backend_factory(Redis, redis_dsn, hash_key=None)
+        backend = await backend_factory(Redis, redis_dsn, hash_key=None)
+    elif request.param == "redis_hash":
+        from cashews.backends.redis import Redis
+
+        backend = await backend_factory(Redis, redis_dsn, hash_key=uuid4().hex)
+    elif request.param == "redis_cs":
+        from cashews.backends.client_side import BcastClientSide
+
+        backend = await backend_factory(BcastClientSide, redis_dsn, hash_key=None)
     else:
-        yield await backend_factory(Memory)
+        backend = await backend_factory(Memory)
+    try:
+        yield backend
+    finally:
+        await backend.close()
 
 
 async def test_set_get(cache):
@@ -52,7 +65,7 @@ async def test_set_exist(cache):
 
 
 async def test_set_many(cache):
-    await cache.set_many({"key1": "value1", "key2": "value2"})
+    await cache.set_many({"key1": "value1", "key2": "value2"}, expire=1)
     assert await cache.get("key1", "value1")
     assert await cache.get("key2", "value2")
 
@@ -67,7 +80,7 @@ async def test_incr(cache):
     assert await cache.get("incr") == 2
 
 
-async def test_incr_setted(cache):
+async def test_incr_set(cache):
     await cache.set("incr", "test")
     with pytest.raises(Exception):
         assert await cache.incr("incr") == 1
@@ -87,7 +100,7 @@ async def test_exists(cache):
 async def test_expire(cache):
     await cache.set("key", b"value", expire=0.01)
     assert await cache.get("key") == b"value"
-    await asyncio.sleep(0.011)
+    await asyncio.sleep(0.1)
     assert await cache.get("key") is None
 
 
@@ -99,6 +112,18 @@ async def test_get_set_expire(cache):
     assert await cache.get_expire("key") == 1
 
 
+async def test_delete_many(cache: Backend):
+    await cache.set("key1", b"value")
+    await cache.set("key2", b"value")
+    await cache.set("key3", b"value")
+
+    await cache.delete_many("key1", "key2", "key4")
+
+    assert await cache.get("key1") is None
+    assert await cache.get("key2") is None
+    assert await cache.get("key3") == b"value"
+
+
 async def test_delete_match(cache: Backend):
     await cache.set("pref:test:test", b"value")
     await cache.set("pref:value:test", b"value2")
@@ -158,12 +183,7 @@ async def test_get_match(cache: Backend):
 
 async def test_get_size(cache: Backend):
     await cache.set("test", b"1")
-    assert await cache.get_size("test") in (
-        sys.getsizeof((None, b"1")) + sys.getsizeof(b"1") + sys.getsizeof(None),  # ordered dict
-        66,  # redis 6
-        72,  # redis 7
-        -1,  # diskcache
-    )
+    assert isinstance(await cache.get_size("test"), int)
 
 
 async def test_get_bits(cache: Backend):
diff --git a/tests/test_blooms.py b/tests/test_blooms.py
index 89b7f57..77e1eae 100644
--- a/tests/test_blooms.py
+++ b/tests/test_blooms.py
@@ -4,7 +4,6 @@
 import pytest_asyncio
 
 from cashews.backends.memory import Memory
-from cashews.decorators.bloom import _counting_bloom as counting_bloom
 from cashews.decorators.bloom import bloom, dual_bloom
 
 pytestmark = pytest.mark.asyncio
@@ -99,30 +98,3 @@ async def func(k):
     call.reset_mock()
     assert await func(70) is True
     call.assert_called_with(70)
-
-
-async def test_bloom_counting(backend):
-    n = 100
-    call = Mock()
-
-    @counting_bloom(backend=backend, name="name:{k}", false_positives=0.1, capacity=n)
-    async def func(k):
-        call(k)
-        return k > (n / 2)
-
-    for i in range(n):
-        await func.set(i)
-
-    call.reset_mock()
-    for i in range(n):
-        assert await func(i) is (i > (n / 2))
-
-    await func.delete(10)
-    await func.delete(70)
-
-    assert await func(10) is False
-    call.assert_called_with(10)
-
-    call.reset_mock()
-    assert await func(70) is True
-    call.assert_called_with(70)
diff --git a/tests/test_cache.py b/tests/test_cache.py
index c77201f..c7faa5f 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -1,11 +1,12 @@
 import asyncio
+from typing import Optional
 from unittest.mock import Mock
 
 import pytest
 import pytest_asyncio
 
 from cashews import decorators, noself
-from cashews.backends.memory import Backend, Memory
+from cashews.backends.memory import Memory
 from cashews.formatter import _REGISTER, get_templates_for_func
 
 pytestmark = pytest.mark.asyncio
@@ -22,7 +23,6 @@ class CustomError(Exception):
     params=[
         "memory",
         pytest.param("redis", marks=pytest.mark.redis),
-        # pytest.param("redis_cs", marks=pytest.mark.redis),
         pytest.param("diskcache", marks=pytest.mark.diskcache),
     ],
 )
@@ -34,7 +34,7 @@ async def _backend(request, redis_dsn, backend_factory):
     elif request.param == "redis":
         from cashews.backends.redis import Redis
 
-        yield await backend_factory(Redis, redis_dsn, max_connections=20)
+        yield await backend_factory(Redis, redis_dsn, max_connections=20, safe=False, socket_timeout=10)
     else:
         yield await backend_factory(backend_cls=Memory)
 
@@ -211,6 +211,36 @@ async def func(resp=b"ok"):
     assert mock.call_count == 3
 
 
+async def test_cache_simple_ttl(backend):
+    mock = Mock()
+
+    def _ttl(resp=b"ok"):
+        if resp == b"ok":
+            return 0.01
+        return "2h"
+
+    @decorators.cache(backend, ttl=_ttl)
+    async def func(resp=b"ok"):
+        mock()
+        return resp
+
+    await func()
+    await func()
+
+    assert mock.call_count == 1
+    await asyncio.sleep(0.02)
+
+    await func()
+    assert mock.call_count == 2
+
+    await func(b"notok")
+    assert mock.call_count == 3
+
+    await asyncio.sleep(0.02)
+    await func(b"notok")
+    assert mock.call_count == 3
+
+
 async def test_early_cache_simple(backend):
     @decorators.early(backend, ttl=EXPIRE, key="key")
     async def func(resp=b"ok"):
@@ -272,10 +302,8 @@ async def func(resp=b"ok"):
         await func(b"notok")
 
 
+@pytest.mark.xfail
 async def test_early_cache_parallel(backend):
-    if backend.name in ("redis", "diskcache"):
-        pytest.skip("fail in ci with slow redis or disk")
-
     mock = Mock()
 
     @decorators.early(backend, ttl=0.1, early_ttl=0.05, key="key")
@@ -288,11 +316,11 @@ async def func(resp=b"ok"):
 
     assert mock.call_count == 1
 
-    for _ in range(8):  # 8 * 0.01 = 0.08 - 1 more time should be executed
+    for _ in range(8):
         await asyncio.sleep(0.01)
         await asyncio.gather(*[func() for _ in range(10)])
 
-    assert mock.call_count == 2
+    assert mock.call_count in (2, 3, 4)  # depends on backend
 
 
 async def test_lock_cache_parallel(backend):
@@ -325,7 +353,11 @@ async def func(resp=b"ok"):
 
 
 async def test_lock_cache_broken_backend():
-    backend = Mock(wraps=Backend())
+    class BrokenMemoryBackend(Memory):
+        async def ping(self, message: Optional[bytes] = None) -> bytes:
+            raise Exception("broken")
+
+    backend = Mock(wraps=BrokenMemoryBackend())
     mock = Mock()
 
     @decorators.locked(backend, key="key", step=0.01)
@@ -343,19 +375,19 @@ async def func(resp=b"ok"):
 async def test_hit_cache(backend):
     mock = Mock()
 
-    @decorators.hit(backend, ttl=10, cache_hits=10, key="test")
+    @decorators.hit(backend, ttl=1000, cache_hits=10, key="test")
     async def func(resp=b"ok"):
         mock(resp)
         return resp
 
     await func()  # cache
     await asyncio.gather(*[func() for _ in range(10)])  # get 10 hits
-    assert mock.call_count == 1
+    assert mock.call_count in [1, 2]
     await func()  # cache
-    assert mock.call_count == 2
+    assert mock.call_count in [2, 3]
 
     await asyncio.gather(*[func() for _ in range(10)])
-    assert mock.call_count in [2, 3]
+    assert mock.call_count in [2, 3, 4]
 
 
 async def test_hit_cache_early(backend):
@@ -379,23 +411,6 @@ async def func(resp=b"ok"):
     assert mock.call_count == 3
 
 
-async def test_cache_detect_simple(backend):
-    @decorators.cache(backend, ttl=EXPIRE, key="key")
-    async def func(resp=b"ok"):
-        return resp
-
-    cache_detect = decorators.CacheDetect()
-    assert await func(_from_cache=cache_detect) == b"ok"
-    assert cache_detect.keys == {}
-
-    await asyncio.sleep(0)
-    assert await func(b"notok", _from_cache=cache_detect) == b"ok"
-    assert len(cache_detect.keys) == 1
-    assert list(cache_detect.keys.keys()) == [
-        "key",
-    ]
-
-
 async def test_context_cache_detect_simple(backend):
     @decorators.cache(backend, ttl=EXPIRE, key="key")
     async def func(resp=b"ok"):
diff --git a/tests/test_client_side_cache.py b/tests/test_client_side_cache.py
index a56810f..76f622c 100644
--- a/tests/test_client_side_cache.py
+++ b/tests/test_client_side_cache.py
@@ -23,17 +23,20 @@ async def test_set_get_bcast(create_cache):
     caches_local = Memory()
     caches = await create_cache(caches_local)
 
-    await cachef.set("key", b"value", expire=0.1)
+    await cachef.set("cashews", b"value", expire=0.1)
     await asyncio.sleep(0.01)  # skip init signal about invalidation
-    assert await cachef.get("key") == b"value"
-    assert await caches.get("key") == b"value"
-    assert await cachef_local.get("key") == b"value"
-    assert await caches_local.get("key") == b"value"
+    assert await cachef.get("cashews") == b"value"
+    assert await caches.get("cashews") == b"value"
+    assert await cachef_local.get("cashews") == b"value"
+    assert await caches_local.get("cashews") == b"value"
     await asyncio.sleep(0.2)
-    assert await cachef_local.get("key") is None
-    assert await caches_local.get("key") is None
+    assert await cachef_local.get("cashews") is None
+    assert await caches_local.get("cashews") is None
 
-    assert await caches.get("key") is None
+    assert await caches.get("cashews") is None
+
+    await caches.close()
+    await cachef.close()
 
 
 async def test_set_none_bcast(create_cache):
@@ -42,15 +45,23 @@ async def test_set_none_bcast(create_cache):
     caches_local = Memory()
     caches = await create_cache(caches_local)
 
-    await cachef.set("key", None, expire=0.1)
+    assert await caches.get("key") is None
+    assert not await caches.exists("key")
+    assert await caches_local.exists("key")
+    assert await caches.get("key") is None
+
+    await cachef.set("key", None, expire=10000)
     await asyncio.sleep(0.01)  # skip init signal about invalidation
-    assert await cachef_local.exists("key")
     assert await cachef.exists("key")
+    assert await cachef_local.exists("key")
 
     assert await caches.get("key") is None
     assert await caches.exists("key")
     assert await caches_local.exists("key")
 
+    await caches.close()
+    await cachef.close()
+
 
 async def test_del_bcast(create_cache):
     cachef_local = Memory()
@@ -89,72 +100,62 @@ async def test_rewrite_bcast(create_cache):
     assert await caches.get("key") is None
     assert await cachef.get("key") is None
 
+    assert await cachef.incr("key") == 1
+    assert await caches.incr("key") == 2
 
-@pytest.mark.xfail
-async def test_simple_cmd_bcast(create_cache):
-    local = Memory()
-    cache = await create_cache(local)
-
-    await cache.incr("key:2")
-    assert await cache.get("key:2") == 1
-    await cache.delete("key:2")
-    assert await cache.get("key:2") is None
-    assert await local.get("key:2") is None
-
-    await cache.set("key:1", "test", 10)
-    assert await cache.get("key:1") == "test"
-    assert await local.get("key:1") == "test"
-    await cache.expire("key:1", 100)
-    assert await cache.get_expire("key:1") > 10
-    assert await local.get_expire("key:1") > 10
-    assert await cache.get("key:1") == "test"
-    assert await local.get("key:1") == "test"
-
-    await cache.clear()
-    cache.close()
+    await caches.close()
+    await cachef.close()
 
 
 async def test_simple_cmd_bcast_many(create_cache):
+    from cashews.backends.client_side import _empty_in_redis
+
     local = Memory()
     cache = await create_cache(local)
     await cache.set("key:1", "test")
-    assert await cache.get("key:1") == "test"
+    await cache.set("key:2", "test2")
+
+    await cache.get("key:1")
     assert await local.get("key:1") == "test"
 
-    assert await cache.get_many("key:1", "key:2") == ("test", None)
+    assert await cache.get_many("key:1", "key:2", "key:3") == ("test", "test2", None)
+    assert await local.get("key:2") == "test2"
+    assert await local.get("key:3") is _empty_in_redis
 
     async for key in cache.scan("key:*"):
-        assert key == "key:1"
+        assert key in ("key:1", "key:2")
         break
     else:
         assert False
 
     async for key, value in cache.get_match("key:*"):
-        assert key == "key:1"
-        assert value == "test"
+        assert key in ("key:1", "key:2")
+        assert value in ("test", "test2")
         break
     else:
         assert False
 
     await local.clear()
 
-    assert await cache.get_many("key:1", "key:2") == ("test", None)
+    assert await cache.get_many("key:1", "key:3") == ("test", None)
 
     async for key in cache.scan("key:*"):
-        assert key == "key:1"
+        assert key in ("key:1", "key:2")
 
     async for key, value in cache.get_match("key:*"):
-        assert key == "key:1"
-        assert value == "test"
+        assert key in ("key:1", "key:2")
+        assert value in ("test", "test2")
 
     assert await local.get("key:1") == "test"
 
-    assert await cache.delete_match("key:*")
+    await cache.delete_match("key:*")
     assert await cache.get("key:1") is None
-    assert await local.get("key:1") is None
+    assert await local.get("key:1") is _empty_in_redis
 
     async for _ in cache.scan("key:*"):
         assert False
 
     async for _ in cache.get_match("key:*"):
         assert False
+
+    await cache.close()
diff --git a/tests/test_disable_control.py b/tests/test_disable_control.py
new file mode 100644
index 0000000..f113a88
--- /dev/null
+++ b/tests/test_disable_control.py
@@ -0,0 +1,188 @@
+import asyncio
+from unittest.mock import Mock
+
+import pytest
+
+from cashews.backends.memory import Memory
+from cashews.commands import Command
+from cashews.wrapper import Cache
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.fixture(name="target")
+def _target():
+    return Mock(wraps=Memory())
+
+
+@pytest.fixture(name="cache")
+def _cache(target):
+    cache = Cache()
+    cache._add_backend(target)
+    return cache
+
+
+async def test_disable_cmd(cache):
+    await cache.init("mem://localhost")
+    cache.disable(Command.INCR)
+    await cache.set("test", 10)
+    await cache.incr("test")
+    assert await cache.get("test") == 10
+
+    cache.enable(Command.INCR)
+    await cache.incr("test")
+    assert await cache.get("test") == 11
+
+
+async def test_disable_context_manager(cache):
+    await cache.init("mem://localhost")
+    with cache.disabling(Command.INCR):
+        await cache.set("test", 10)
+        await cache.incr("test")
+    assert await cache.get("test") == 10
+
+
+async def test_init_disable(cache):
+    cache.setup("mem://localhost?disable=1")
+    assert cache.is_disable()
+
+
+async def test_init_enable(cache):
+    cache.setup("mem://localhost?enable=1")
+    assert cache.is_enable()
+    assert not cache.is_disable()
+
+
+async def test_prefix(cache):
+    cache.setup("mem://localhost")
+    cache.setup("://", prefix="-", disable=True)
+    assert not cache.is_disable()
+    assert cache.is_disable(prefix="-")
+
+    await cache.set("key", "value")
+    await cache.set("-:key", "-value")
+
+    assert await cache.get("key") == "value"
+    assert await cache.get("-:key") is None
+    assert await cache.get("-:key", default="def") == "def"
+
+
+async def test_disable_ctz(cache):
+    await cache.init("mem://localhost")
+    cache.enable()
+
+    async def test():
+        await cache.set("test", "1")
+        assert await cache.get("test") == "1"
+        cache.disable(Command.SET)
+        await cache.set("test", "2")
+
+    await asyncio.create_task(test())
+    assert await cache.get("test") == "1"
+    await cache.set("test", "3")
+    assert await cache.get("test") == "3"
+
+
+async def test_disable_decorators(cache: Cache, target):
+    cache.disable()
+    data = (i for i in range(10))
+
+    @cache(ttl=1)
+    @cache.soft(ttl=1)
+    @cache.failover(ttl=1)
+    @cache.hit(ttl=1, cache_hits=1)
+    @cache.circuit_breaker(ttl=1, errors_rate=1, period=1)
+    @cache.rate_limit(ttl=1, limit=1, period=1)
+    @cache.early(ttl=1)
+    @cache.dynamic()
+    @cache.locked(ttl=1)
+    async def func():
+        return next(data)
+
+    assert await func() == 0
+    assert await func() == 1
+    target.get.assert_not_called()
+    target.set.assert_not_called()
+
+    cache.enable()
+    assert await func() == 2
+    assert await func() == 2
+
+
+async def test_disable_bloom(cache: Cache, target: Mock):
+    cache.disable()
+
+    @cache.bloom(capacity=10)
+    async def func():
+        return True
+
+    await func.set()
+    assert await func()
+    target.incr_bits.assert_not_called()
+    target.get_bits.assert_not_called()
+
+    cache.enable()
+    await func.set()
+    assert await func()
+    target.incr_bits.assert_called()
+    target.get_bits.assert_called()
+
+
+async def test_disable_decorators_get(cache: Cache):
+    data = (i for i in range(10))
+    await cache.init("mem://localhost")
+
+    @cache(ttl=1)
+    async def func():
+        return next(data)
+
+    assert cache.is_enable()
+    assert cache.is_enable(Command.SET, prefix="cache")
+    assert cache.is_enable(Command.GET, prefix="cache")
+    assert cache.is_enable(Command.SET, prefix="")
+    assert await func() == 0
+    assert await func() == 0
+
+    cache.disable(Command.GET)
+
+    assert not cache.is_enable(Command.GET)
+    assert cache.is_enable(Command.SET)
+
+    assert await func() == 1
+    assert await func() == 2
+
+    cache.enable(Command.GET)
+    assert await func() == 2
+
+
+async def test_disable_decorator_set(cache: Cache):
+    data = (i for i in range(10))
+    cache.disable(Command.SET)
+
+    @cache(ttl=1)
+    async def func():
+        return next(data)
+
+    assert await func() == 0
+    assert await func() == 1
+
+    cache.enable(Command.SET)
+    assert await func() == 2
+    assert await func() == 2
+
+
+async def test_disable_and_get_enable(cache: Cache):
+    data = (i for i in range(10))
+    cache.enable()
+
+    @cache(ttl=1)
+    async def func():
+        return next(data)
+
+    assert await func() == 0
+    assert await func() == 0
+    cache.disable()
+    assert await func() == 1
+    assert await func() == 2
+    cache.enable(Command.GET)
+    assert await func() == 0
diff --git a/tests/test_invalidate.py b/tests/test_invalidate.py
index 24d0695..f211020 100644
--- a/tests/test_invalidate.py
+++ b/tests/test_invalidate.py
@@ -4,7 +4,7 @@
 import pytest
 
 from cashews import Cache
-from cashews.validation import invalidate_func, set_invalidate_further
+from cashews.validation import invalidate_func, invalidate_further
 
 pytestmark = pytest.mark.asyncio
 
@@ -80,7 +80,7 @@ async def func2(arg, default=True):
 
 async def test_invalidate_decor_complicate(cache: Cache):
     @cache(ttl=1, key="arg-{arg}:{key}:{flag}")
-    async def func(arg, key=b"test", flag=True, **kwargs):
+    async def func(arg, key="test", flag=True, **kwargs):
         return random.random()
 
     @cache.invalidate(func, args_map={"arg": "a"}, defaults={"flag": True})
@@ -89,13 +89,13 @@ async def func2(a):
 
     first_call = await func("test")
     not_invalidate = await func("test", flag=False)
-    second_call = await func("test", b"key", flag=True)
+    second_call = await func("test", "key", flag=True)
     last_call = await func("test2", None)
     await asyncio.sleep(0)
 
     assert first_call != second_call != last_call
     assert first_call == await func("test")
-    assert second_call == await func("test", b"key")
+    assert second_call == await func("test", "key")
     assert last_call == await func("test2", None)
     assert not_invalidate == await func("test", flag=False)
 
@@ -103,7 +103,7 @@ async def func2(a):
 
     await asyncio.sleep(0)
     assert first_call == await func("test")
-    assert second_call == await func("test", b"key")
+    assert second_call == await func("test", "key")
     assert last_call == await func("test2", None)
     assert not_invalidate == await func("test", flag=False)
 
@@ -122,21 +122,62 @@ async def func2(a):
     assert not_invalidate == await func("test", flag=False)
 
 
-async def test_invalidate_further(cache):
+async def test_invalidate_further_decorator(cache):
     @cache(ttl=10000)
     async def func():
         return random.random()
 
-    async def clear():
-        set_invalidate_further()
-        return await func()
-
     first = await func()
     assert await func() == first
 
-    on_clear = await asyncio.create_task(clear())
+    with invalidate_further():
+        on_clear = await func()
     assert on_clear is not None
     assert on_clear != first  # the key was deleted
     second = await func()
     assert second != first
     assert await func() == second
+
+
+async def test_invalidate_further_get(cache):
+
+    await cache.set("key", "value")
+    assert await cache.get("key") == "value"
+
+    with invalidate_further():
+        assert await cache.set("key", "value2")
+        assert await cache.set("key2", "value2")
+        assert await cache.get("key") is None
+
+    await asyncio.sleep(0)
+    assert await cache.get("key") is None
+    assert await cache.get("key2") == "value2"
+    assert await cache.set("key", "value3")
+    assert await cache.get("key") == "value3"
+
+
+async def test_invalidate_further_get_many(cache):
+
+    await cache.set("key", "value")
+    await cache.set("key2", "value2")
+
+    assert await cache.get_many("key", "key2") == ("value", "value2")
+
+    with invalidate_further():
+        assert await cache.get_many("key") == (None,)
+
+    await asyncio.sleep(0)
+    assert await cache.get_many("key", "key2") == (None, "value2")
+
+
+async def test_invalidate_further_get_match(cache):
+
+    await cache.set("key1", "value1")
+    await cache.set("key2", "value2")
+    assert {k: v async for k, v in cache.get_match("key*")} == {"key1": "value1", "key2": "value2"}
+
+    with invalidate_further():
+        assert {k: v async for k, v in cache.get_match("key1*")} == {}
+
+    await asyncio.sleep(0)
+    assert {k: v async for k, v in cache.get_match("key*")} == {"key2": "value2"}
diff --git a/tests/test_key.py b/tests/test_key.py
index 221cff2..5dfa993 100644
--- a/tests/test_key.py
+++ b/tests/test_key.py
@@ -4,7 +4,8 @@
 
 from cashews.exceptions import WrongKeyError
 from cashews.formatter import default_formatter, get_template_and_func_for, get_template_for_key, register_template
-from cashews.key import get_cache_key, get_cache_key_template, ttl_to_seconds
+from cashews.key import get_cache_key, get_cache_key_template
+from cashews.ttl import ttl_to_seconds
 
 
 async def func1(a):
@@ -158,7 +159,7 @@ async def func(user):
             "{arg2}:{kwarg1}:{kwarg3}",
             "2:K1:",
         ),
-        (("a1", "a2"), {"kwarg1": "test"}, "{kwarg1:len}", "4"),
+        (("a1", "a2"), {"kwarg1": 1234}, "{kwarg1:len}", "4"),
         (
             ("a1", "a2"),
             {"user": type("user", (), {"name": "test"})()},
@@ -218,7 +219,6 @@ def test_get_key_template_error():
     ("ttl", "expect"),
     (
         (timedelta(seconds=10), 10),
-        (lambda: timedelta(hours=1), 60 * 60),
         (10, 10),
         (100.1, 100.1),
         ("10s", 10),
@@ -226,7 +226,6 @@ def test_get_key_template_error():
         ("10m1s", 60 * 10 + 1),
         ("1", 1),
         ("80", 80),
-        (lambda: "1h", 60 * 60),
     ),
 )
 def test_ttl_to_seconds(ttl, expect):
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 3756776..0708152 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -4,7 +4,6 @@
 
 from cashews import Cache
 from cashews.backends.memory import Memory
-from cashews.disable_control import ControlMixin
 from cashews.helpers import add_prefix, all_keys_lower, memory_limit
 
 pytestmark = pytest.mark.asyncio
@@ -12,7 +11,7 @@
 
 @pytest.fixture(name="target")
 def _target() -> Mock:
-    class MemCache(ControlMixin, Memory):
+    class MemCache(Memory):
         pass
 
     return Mock(wraps=MemCache())
@@ -21,13 +20,12 @@ class MemCache(ControlMixin, Memory):
 @pytest.fixture(name="cache")
 def __cache(target):
     _cache = Cache()
-    _cache._add_backend(Memory)
-    _cache._backends[""] = (target, _cache._backends[""][1])
+    _cache._add_backend(target)
     return _cache
 
 
 async def test_all_keys_lower(cache: Cache, target):
-    cache._backends[""] = cache._backends[""][0], (all_keys_lower(),)
+    cache._add_backend(target, (all_keys_lower(),))
     await cache.get(key="KEY")
     target.get.assert_called_once_with(key="key", default=None)
 
@@ -38,12 +36,17 @@ async def test_all_keys_lower(cache: Cache, target):
         exist=None,
         expire=None,
     )
+    await cache.set_many({"KEY": "value"})
+    target.set_many.assert_called_once_with(
+        pairs={"key": "value"},
+        expire=None,
+    )
     await cache.ping()
     target.ping.assert_called_once_with(message=b"PING")
 
 
 async def test_memory_limit(cache: Cache, target):
-    cache._backends[""] = cache._backends[""][0], (memory_limit(min_bytes=52, max_bytes=75),)
+    cache._add_backend(target, (memory_limit(min_bytes=52, max_bytes=75),))
 
     await cache.set(key="key", value="v")
     target.set.assert_not_called()
@@ -64,7 +67,7 @@ async def test_memory_limit(cache: Cache, target):
 
 
 async def test_add_prefix(cache: Cache, target):
-    cache._backends[""] = cache._backends[""][0], (add_prefix("prefix!"),)
+    cache._add_backend(target, (add_prefix("prefix!"),))
 
     await cache.get(key="key")
     target.get.assert_called_once_with(key="prefix!key", default=None)
@@ -81,12 +84,18 @@ async def test_add_prefix(cache: Cache, target):
 
 
 async def test_add_prefix_get_many(cache: Cache, target):
-    cache._backends[""] = cache._backends[""][0], (add_prefix("prefix!"),)
+    cache._add_backend(target, (add_prefix("prefix!"),))
     await cache.get_many("key")
     target.get_many.assert_called_once_with("prefix!key")
 
 
+async def test_add_prefix_set_many(cache: Cache, target):
+    cache._add_backend(target, (add_prefix("prefix!"),))
+    await cache.set_many({"key": "value"})
+    target.set_many.assert_called_once_with(pairs={"prefix!key": "value"}, expire=None)
+
+
 async def test_add_prefix_delete_match(cache: Cache, target):
-    cache._backends[""] = cache._backends[""][0], (add_prefix("prefix!"),)
+    cache._add_backend(target, (add_prefix("prefix!"),))
     await cache.delete_match("key")
     target.delete_match.assert_called_once_with(pattern="prefix!key")
diff --git a/tests/test_pickle_serializer.py b/tests/test_pickle_serializer.py
index 4ba1c41..dc6568a 100644
--- a/tests/test_pickle_serializer.py
+++ b/tests/test_pickle_serializer.py
@@ -39,7 +39,7 @@ async def delete(self, *keys):
     async def get_many(self, *keys, default=None):
         return [self.store.get(key, default) for key in keys]
 
-    async def set_many(self, pairs, **kwargs):
+    async def set_many(self, pairs, expire=None):
         self.store.update(pairs)
 
 
@@ -50,21 +50,24 @@ class Cache(PickleSerializerMixin, DummyCache):
 @pytest_asyncio.fixture(
     name="cache",
     params=[
-        "default",
-        pytest.param("redis", marks=pytest.mark.redis),
-        pytest.param("dill", marks=pytest.mark.integration),
-        pytest.param("sqlalchemy", marks=pytest.mark.integration),
+        "default_md5",
+        "default_sum",
+        "default_sha256",
+        pytest.param("redis_md5", marks=pytest.mark.redis),
+        pytest.param("dill_sum", marks=pytest.mark.integration),
+        pytest.param("sqlalchemy_sha1", marks=pytest.mark.integration),
     ],
 )
 async def _cache(request, redis_dsn):
-    if request.param == "redis":
+    pickle_type, digestmod = request.param.split("_")
+    if pickle_type == "redis":
         from cashews.backends.redis import Redis
 
-        redis = Redis(redis_dsn, hash_key=b"test", safe=True)
+        redis = Redis(redis_dsn, hash_key=b"test", safe=False, digestmod=digestmod)
         await redis.init()
         await redis.clear()
         return redis
-    return Cache(hash_key="test", digestmod="md5", pickle_type=request.param)
+    return Cache(hash_key="test", digestmod=digestmod, pickle_type=pickle_type)
 
 
 @pytest.mark.parametrize(
@@ -162,7 +165,7 @@ async def test_serialize_array_diff_value(value, cache):
 async def test_unsecure_value(value, cache):
     await cache.set_raw("key", value)
     with pytest.raises(UnSecureDataError):
-        await cache.get("key")
+        assert not await cache.get("key")
 
 
 async def test_unsecure_value_many(cache):
@@ -186,7 +189,7 @@ async def test_replace_values(cache):
 async def test_pickle_error_value(cache):
     await cache.set_raw(
         "key",
-        cache._get_sign("key", b"no_pickle_data", b"md5") + b"_" + b"no_pickle_data",
+        cache._gen_sign("key", b"no_pickle_data", cache._digestmod) + b"_" + b"no_pickle_data",
     )
     assert await cache.get("key") is None
 
@@ -234,3 +237,26 @@ async def test_no_hash(key, value):
     cache = Cache()
     await cache.set(key, value)
     assert await cache.get(key) == value
+
+
+async def test_cache_from_hash_to_no_hash():
+    val = Decimal("10.2")
+    cache = Cache(hash_key="test")
+    await cache.set("key", val)
+
+    assert await cache.get("key") == val
+    cache_no_hash = Cache()
+    cache_no_hash.store = cache.store
+    assert await cache_no_hash.get("key") == val
+
+
+async def test_cache_from_no_hash_to_hash():
+    val = Decimal("10.2")
+    cache = Cache()
+    await cache.set("key", val)
+
+    assert await cache.get("key") == val
+    cache_hash = Cache(hash_key="test")
+    cache_hash.store = cache.store
+
+    assert await cache_hash.get("key") is None
diff --git a/tests/test_redis_down.py b/tests/test_redis_down.py
index e2af832..24dc8c6 100644
--- a/tests/test_redis_down.py
+++ b/tests/test_redis_down.py
@@ -42,8 +42,6 @@ async def test_safe_redis(redis_backend):
 
     async for _ in redis.get_match("*"):
         assert False
-    async for _ in redis.keys_match("*"):
-        assert False
 
     assert await redis.delete("test") == 0
     await redis.delete_match("*")
@@ -64,7 +62,7 @@ async def test_unsafe_redis_down(redis_backend):
 async def test_cache_decorators_on_redis_down(redis_backend):
     mock = Mock(return_value="val")
     cache = Cache()
-    cache._add_backend(redis_backend, safe=True, address="redis://localhost:9223", hash_key=None)
+    cache._add_backend(redis_backend(safe=True, address="redis://localhost:9223", hash_key=None))
 
     @cache(ttl=1)
     @cache.failover(1)
diff --git a/tests/test_serialize.py b/tests/test_serialize.py
deleted file mode 100644
index 1edfd16..0000000
--- a/tests/test_serialize.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import random
-from contextlib import nullcontext
-
-import pytest
-from _pytest.fixtures import SubRequest
-
-from cashews.serialize import PickleSerializerMixin, SignIsMissingError, UnSecureDataError
-
-
-class TestPickleSerializerMixin:
-    @pytest.fixture(params=[None, "sample_sign"])
-    def pickle_serializer_mixin(self, request: SubRequest) -> PickleSerializerMixin:
-        return PickleSerializerMixin(
-            hash_key=request.param,
-            check_repr=random.choice([True, False]),
-        )
-
-    @pytest.mark.parametrize(
-        ("key", "value", "value_has_empty_sign", "value_has_valid_sign"),
-        [
-            (
-                "without sign and without underscore separator",
-                b"spam eggs",
-                False,
-                True,
-            ),
-            (
-                "with sign as an empty string and with underscore separator",
-                b"_spam eggs",  # Check backward compatibility: when `hash_key` was not used.
-                True,
-                False,
-            ),
-        ],
-    )
-    def test__split_value_from_signature(
-        self,
-        pickle_serializer_mixin: PickleSerializerMixin,
-        key: str,
-        value: bytes,
-        value_has_empty_sign: bool,
-        value_has_valid_sign: bool,
-    ) -> None:
-        ctx = nullcontext()
-        if pickle_serializer_mixin._hash_key:
-            if value_has_empty_sign and not value_has_valid_sign:
-                # If we use a `hash_key`, but a value is signed with an empty
-                # or a wrong string, then we expect `UnSecureDataError`.
-                ctx = pytest.raises(UnSecureDataError)
-            elif not value_has_empty_sign:
-                # If we use a `hash_key`, but a value is not signed, then we expect `SignIsMissingError`.
-                ctx = pytest.raises(SignIsMissingError, match=key)
-
-        with ctx:
-            assert pickle_serializer_mixin._split_value_from_signature(value, key) == b"spam eggs"
diff --git a/tests/test_settings_url.py b/tests/test_settings_url.py
index 2c71942..df0d086 100644
--- a/tests/test_settings_url.py
+++ b/tests/test_settings_url.py
@@ -1,22 +1,24 @@
 import pytest
 
-from cashews._settings import BackendNotAvailable, settings_url_parse
+from cashews.backend_settings import BackendNotAvailableError, settings_url_parse
 from cashews.backends.memory import Memory
 
 
 @pytest.mark.parametrize(
     ("url", "params"),
     (
-        ("://", {"backend": Memory, "disable": True}),
-        ("mem://", {"backend": Memory}),
+        ("://", {"disable": True}),
+        ("mem://", {}),
         (
             "mem://?size=10&check_interval=0.01",
-            {"backend": Memory, "size": 10, "check_interval": 0.01},
+            {"size": 10, "check_interval": 0.01},
         ),
     ),
 )
 def test_url(url, params):
-    assert settings_url_parse(url) == params
+    backend_class, _params = settings_url_parse(url)
+    assert backend_class is Memory
+    assert params == _params
 
 
 @pytest.mark.parametrize(
@@ -30,7 +32,7 @@ def test_url(url, params):
     ),
 )
 def test_url_but_backend_dependency_is_not_installed(url, error):
-    with pytest.raises(BackendNotAvailable) as excinfo:
+    with pytest.raises(BackendNotAvailableError) as excinfo:
         settings_url_parse(url)
 
     assert str(excinfo.value) == error
@@ -42,60 +44,59 @@ def test_url_but_backend_dependency_is_not_installed(url, error):
     (
         (
             "redis://localhost:9000/0",
-            {"backend": None, "address": "redis://localhost:9000/0"},
+            {"address": "redis://localhost:9000/0"},
         ),
         (
             "redis://password@localhost:9000",
-            {"backend": None, "address": "redis://password@localhost:9000"},
+            {"address": "redis://password@localhost:9000"},
         ),
         (
             "redis://localhost/0/?password=password",
             {
-                "backend": None,
                 "address": "redis://localhost/0/",
                 "password": "password",
             },
         ),
         (
-            "redis://localhost/0/?hash_key=secret&password=test&safe=1&minsize=3&create_connection_timeout=0.1&pickle_type=dill",  # noqa: E501
+            "redis://localhost/0/?hash_key=secret&password=test&safe=1&minsize=3&create_connection_timeout=0.1",  # noqa: E501
             {
-                "backend": None,
                 "address": "redis://localhost/0/",
                 "hash_key": "secret",
                 "password": "test",
                 "safe": True,
                 "minsize": 3,
                 "create_connection_timeout": 0.1,
-                "pickle_type": "dill",
             },
         ),
         (
             "redis://localhost:9000?",
-            {"backend": None, "address": "redis://localhost:9000"},
+            {"address": "redis://localhost:9000"},
         ),
     ),
 )
 def test_url_with_redis_as_backend(url, params):
     from cashews.backends.redis import Redis
 
-    params["backend"] = Redis
-    assert settings_url_parse(url) == params
+    backend_class, _params = settings_url_parse(url)
+    assert isinstance(backend_class(**params), Redis)
+    assert params == _params
 
 
 @pytest.mark.diskcache
 @pytest.mark.parametrize(
     ("url", "params"),
     (
-        ("disk://", {"backend": None}),
-        ("disk://?size_limit=1000", {"backend": None, "size_limit": 1000}),
+        ("disk://", {}),
+        ("disk://?size_limit=1000", {"size_limit": 1000}),
         (
             "disk://?directory=/tmp/cache&timeout=1&shards=0",
-            {"backend": None, "directory": "/tmp/cache", "timeout": 1, "shards": 0},
+            {"directory": "/tmp/cache", "timeout": 1, "shards": 0},
         ),
     ),
 )
 def test_url_with_diskcache_as_backend(url, params):
     from cashews.backends.diskcache import DiskCache
 
-    params["backend"] = DiskCache
-    assert settings_url_parse(url) == params
+    backend_class, _params = settings_url_parse(url)
+    assert backend_class is DiskCache
+    assert params == _params
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 0751048..9531647 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,4 +1,4 @@
-from cashews.utils import Bitarray, get_hashes
+from cashews.utils import Bitarray, get_indexes
 
 
 def test_bitarray_get_size_1():
@@ -63,24 +63,24 @@ def test_bitarray_incr_over():
 
 
 def test_get_hashes_params():
-    assert len(get_hashes("test", 3, 3)) == 3
-    assert len(get_hashes("test", 1, 100)) == 1
-    assert len(get_hashes("test", 77, 100)) == 77
-    assert len(get_hashes("long string hash", 77, 100)) == 77
-    assert get_hashes("test", 1, 100).pop() < 100
+    assert len(get_indexes("test", 3, 3)) == 3
+    assert len(get_indexes("test", 1, 100)) == 1
+    assert len(get_indexes("test", 77, 100)) == 77
+    assert len(get_indexes("long string hash", 77, 100)) == 77
+    assert get_indexes("test", 1, 100).pop() < 100
 
 
 def test_get_hashes():
-    assert get_hashes("test", 10, 1000) == get_hashes("test", 10, 1000)
-    assert get_hashes("test", 2, 2) == get_hashes("test", 2, 2)
-
-    assert get_hashes("test", 2, 5) != get_hashes("test", 2, 2)
-    assert get_hashes("test", 3, 100) != get_hashes("tset", 3, 100)
-    assert get_hashes("test", 3, 100) != get_hashes("t", 3, 100)
-    assert get_hashes("test", 3, 100) != get_hashes("tes", 3, 100)
-    assert get_hashes("test", 3, 100) != get_hashes("test2", 3, 100)
-    assert not get_hashes("test", 5, 100).intersection(get_hashes("a", 5, 100))
-    assert not get_hashes("test", 5, 100).intersection(get_hashes("test2", 5, 100))
-
-    assert get_hashes("test", 3, 3) == get_hashes("a", 3, 3)  # it is ok to have collisions in this case
-    assert len(get_hashes("test", 20, 100)) == len(set(get_hashes("test", 20, 100)))
+    assert get_indexes("test", 10, 1000) == get_indexes("test", 10, 1000)
+    assert get_indexes("test", 2, 2) == get_indexes("test", 2, 2)
+
+    assert get_indexes("test", 2, 5) != get_indexes("test", 2, 2)
+    assert get_indexes("test", 3, 100) != get_indexes("tset", 3, 100)
+    assert get_indexes("test", 3, 100) != get_indexes("t", 3, 100)
+    assert get_indexes("test", 3, 100) != get_indexes("tes", 3, 100)
+    assert get_indexes("test", 3, 100) != get_indexes("test2", 3, 100)
+    assert not get_indexes("test", 5, 100).intersection(get_indexes("a", 5, 100))
+    assert not get_indexes("test", 5, 100).intersection(get_indexes("test2", 5, 100))
+
+    assert get_indexes("test", 3, 3) == get_indexes("a", 3, 3)  # it is ok to have collisions in this case
+    assert len(get_indexes("test", 20, 100)) == len(set(get_indexes("test", 20, 100)))
diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py
index 3754b57..7a96c76 100644
--- a/tests/test_wrapper.py
+++ b/tests/test_wrapper.py
@@ -3,9 +3,9 @@
 
 import pytest
 
-from cashews._cache_condition import NOT_NONE
 from cashews.backends.memory import Memory
-from cashews.disable_control import ControlMixin
+from cashews.cache_condition import NOT_NONE
+from cashews.exceptions import NotConfiguredError
 from cashews.formatter import get_templates_for_func
 from cashews.wrapper import Cache, _create_auto_init
 
@@ -14,39 +14,16 @@
 
 @pytest.fixture(name="target")
 def _target():
-    class New(ControlMixin, Memory):
-        pass
-
-    return Mock(wraps=New())
+    return Mock(wraps=Memory())
 
 
 @pytest.fixture(name="cache")
 def __cache(target):
     _cache = Cache()
-    _cache._add_backend(Memory)
-    _cache._backends[""] = (target, _cache._backends[""][1])
+    _cache._add_backend(target)
     return _cache
 
 
-async def test_init_disable(cache):
-    await cache.init("mem://localhost?disable=1")
-    assert cache.is_disable()
-
-
-async def test_prefix(cache):
-    await cache.init("mem://localhost")
-    await cache.init("://", prefix="-")
-    assert not cache.is_disable()
-    assert cache.is_disable(prefix="-")
-
-    await cache.set("key", "value")
-    await cache.set("-:key", "-value")
-
-    assert await cache.get("key") == "value"
-    assert await cache.get("-:key") is None
-    assert await cache.get("-:key", default="def") == "def"
-
-
 async def test_prefix_many(cache):
     await cache.init("mem://")
     await cache.init("mem://", prefix="-")
@@ -60,122 +37,6 @@ async def test_prefix_many(cache):
     assert await cache.get_many("key", "-:key") == ("value", "-value")
 
 
-async def test_disable_cmd(cache):
-    await cache.init("mem://localhost")
-    cache.disable("incr")
-    await cache.set("test", 10)
-    await cache.incr("test")
-    assert await cache.get("test") == 10
-
-    cache.enable("incr")
-    await cache.incr("test")
-    assert await cache.get("test") == 11
-
-
-async def test_disable_ctz(cache):
-    await cache.init("mem://localhost")
-    cache.enable()
-
-    async def test():
-        await cache.set("test", "1")
-        assert await cache.get("test") == "1"
-        cache.disable("set")
-        await cache.set("test", "2")
-
-    await asyncio.create_task(test())
-    assert await cache.get("test") == "1"
-    await cache.set("test", "3")
-    assert await cache.get("test") == "3"
-
-
-async def test_disable_decorators(cache: Cache, target):
-    cache.disable()
-    data = (i for i in range(10))
-
-    @cache(ttl=1)
-    @cache.soft(ttl=1)
-    @cache.failover(ttl=1)
-    @cache.hit(ttl=1, cache_hits=1)
-    @cache.circuit_breaker(ttl=1, errors_rate=1, period=1)
-    @cache.rate_limit(ttl=1, limit=1, period=1)
-    @cache.early(ttl=1)
-    @cache.dynamic()
-    @cache.locked(ttl=1)
-    async def func():
-        return next(data)
-
-    assert await func() == 0
-    assert await func() == 1
-    target.get.assert_not_called()
-    target.set.assert_not_called()
-
-    cache.enable()
-    assert await func() == 2
-    assert await func() == 2
-
-
-async def test_disable_bloom(cache: Cache, target):
-    cache.disable()
-
-    @cache.bloom(index_size=10, number_of_hashes=1)
-    async def func():
-        return True
-
-    await func.set()
-    assert await func()
-    target.incr_bits.assert_not_called()
-    target.get_bits.assert_not_called()
-
-    cache.enable()
-    await func.set()
-    assert await func()
-    target.incr_bits.assert_called()
-    target.get_bits.assert_called()
-
-
-async def test_disable_decorators_get(cache: Cache):
-    data = (i for i in range(10))
-    await cache.init("mem://localhost")
-
-    @cache(ttl=1)
-    async def func():
-        return next(data)
-
-    assert cache.is_enable()
-    assert cache.is_enable("set", prefix="cache")
-    assert cache.is_enable("get", prefix="cache")
-    assert cache.is_enable("set", prefix="")
-    assert await func() == 0
-    assert await func() == 0
-
-    cache.disable("get")
-
-    assert not cache.is_enable("get")
-    assert cache.is_enable("set")
-
-    assert await func() == 1
-    assert await func() == 2
-
-    cache.enable("get")
-    assert await func() == 2
-
-
-async def test_disable_decorators_set(cache: Cache):
-    data = (i for i in range(10))
-    cache.disable("set")
-
-    @cache(ttl=1)
-    async def func():
-        return next(data)
-
-    assert await func() == 0
-    assert await func() == 1
-
-    cache.enable("set")
-    assert await func() == 2
-    assert await func() == 2
-
-
 async def test_init(cache):
     await cache.init("mem://localhost")
     assert cache.is_init
@@ -200,15 +61,21 @@ async def _set():
     target.init.assert_called_once()
 
 
-async def test_smoke_cmds(cache: Cache, target):
+async def test_smoke_cmds(cache: Cache, target: Mock):
     await cache.set(key="key", value={"any": True}, expire=60, exist=None)
     target.set.assert_called_once_with(key="key", value={"any": True}, expire=60, exist=None)
 
+    await cache.set_raw(key="key2", value={"any": True}, expire=60, exist=None)
+    target.set_raw.assert_called_once_with(key="key2", value={"any": True}, expire=60, exist=None)
+
     await cache.get("key")  # -> Any
     target.get.assert_called_once_with(key="key", default=None)
 
+    await cache.get_raw("key")  # -> Any
+    target.get_raw.assert_called_once_with(key="key")
+
     await cache.set_many({"key1": "value1", "key2": "value2"}, expire=60)
-    target.set_many.assert_called_once_with({"key1": "value1", "key2": "value2"}, expire=60)
+    target.set_many.assert_called_once_with(pairs={"key1": "value1", "key2": "value2"}, expire=60)
 
     await cache.get_many("key1", "key2")
     target.get_many.assert_called_once_with("key1", "key2", default=None)
@@ -219,6 +86,12 @@ async def test_smoke_cmds(cache: Cache, target):
     await cache.delete("key")
     target.delete.assert_called_once_with(key="key")
 
+    await cache.delete_match("key*")
+    target.delete_match.assert_called_once_with(pattern="key*")
+
+    await cache.delete_many("key", "key2")
+    target.delete_many.assert_called_once_with("key", "key2")
+
     await cache.expire(key="key", timeout=10)
     target.expire.assert_called_once_with(key="key", timeout=10)
 
@@ -249,11 +122,15 @@ async def test_smoke_cmds(cache: Cache, target):
     await cache.incr_bits("key", 1, 2, 3, size=2)
     target.incr_bits.assert_called_once_with("key", 1, 2, 3, size=2, by=1)
 
-    [key async for key in cache.keys_match("key:*")]
-    target.keys_match.assert_called_once_with("key:*")
+    await cache.set("key", "value")
+    assert [key async for key in cache.scan("key*")] == ["key"]
+    target.scan.assert_called_once_with("key*", batch_size=100)
+
+    assert [key_value async for key_value in cache.get_match("key*")] == [("key", "value")]
+    target.get_match.assert_called_once_with("key*", batch_size=100)
 
-    [key_value async for key_value in cache.get_match("key:*")]
-    target.get_match.assert_called_once_with("key:*", batch_size=100, default=None)
+    await cache.get_size("key")
+    target.get_size.assert_called_once_with(key="key")
 
 
 async def test_disable_cache_on_fail_return(cache: Cache):
@@ -384,3 +261,10 @@ async def func():
 
     assert await func() is None
     assert mock.call_count == 2
+
+
+async def test_no_setup():
+    cache = Cache()
+
+    with pytest.raises(NotConfiguredError):
+        await cache.get("test")