# Path: aredis/cache.py
# class Cache(BasicCache):
#     """Provides basic caching"""
# 
#     async def get(self, key, param=None):
#         identity = self._gen_identity(key, param)
#         res = await self.client.get(identity)
#         if res:
#             res = self._unpack(res)
#         return res
# 
#     async def set(self, key, value, param=None, expire_time=None):
#         identity = self._gen_identity(key, param)
#         value = self._pack(value)
#         return await self.client.set(identity, value, ex=expire_time)
# 
#     async def set_many(self, data, param=None, expire_time=None):
#         async with await self.client.pipeline() as pipeline:
#             for key, value in data.items():
#                 identity = self._gen_identity(key, param)
#                 value = self._pack(value)
#                 await pipeline.set(identity, value, expire_time)
#             return await pipeline.execute()
# 
# class HerdCache(BasicCache):
#     """
#     Cache that handles thundering herd problem
#     (https://en.wikipedia.org/wiki/Thundering_herd_problem)
#     by cacheing expiration time in set instead of directly using expire
#     operation of redis.
#     This kind of cache is suitable for low consistency scene where update
#     operations are expensive.
#     """
# 
#     def __init__(self, client, app='', identity_generator_class=IdentityGenerator,
#                  compressor_class=Compressor, serializer_class=Serializer,
#                  default_herd_timeout=1, extend_herd_timeout=1, encoding='utf-8'):
#         self.default_herd_timeout = default_herd_timeout
#         self.extend_herd_timeout = extend_herd_timeout
#         super(HerdCache, self).__init__(client, app, identity_generator_class,
#                                         compressor_class, serializer_class,
#                                         encoding)
# 
#     async def set(self, key, value, param=None, expire_time=None, herd_timeout=None):
#         """
#         Uses key and param to generate identity and pack the content,
#         expire the key within real_timeout if expire_time is given.
#         real_timeout is equal to the sum of expire_time and herd_time.
#         The content is cached with expire_time.
#         """
#         identity = self._gen_identity(key, param)
#         expected_expired_ts = int(time.time())
#         if expire_time:
#             expected_expired_ts += expire_time
#         expected_expired_ts += herd_timeout or self.default_herd_timeout
#         value = self._pack([value, expected_expired_ts])
#         return await self.client.set(identity, value, ex=expire_time)
# 
#     async def set_many(self, data, param=None, expire_time=None, herd_timeout=None):
#         async with await self.client.pipeline() as pipeline:
#             for key, value in data.items():
#                 identity = self._gen_identity(key, param)
#                 expected_expired_ts = int(time.time())
#                 if expire_time:
#                     expected_expired_ts += expire_time
#                 expected_expired_ts += herd_timeout or self.default_herd_timeout
#                 value = self._pack([value, expected_expired_ts])
#                 await pipeline.set(identity, value, ex=expire_time)
#             return await pipeline.execute()
# 
#     async def get(self, key, param=None, extend_herd_timeout=None):
#         """
#         Uses key or identity generated from key and param to get cached
#         content and expiratiom time.
#         Uses time.now() to check expiration. If not expired, returns unpacked
#         content. Otherwise returns None and sets cache with extended timeout
#         """
#         identity = self._gen_identity(key, param)
#         res = await self.client.get(identity)
#         if res:
#             res, timeout = self._unpack(res)
#             now = int(time.time())
#             if timeout <= now:
#                 extend_timeout = extend_herd_timeout or self.extend_herd_timeout
#                 expected_expired_ts = now + extend_timeout
#                 value = self._pack([res, expected_expired_ts])
#                 await self.client.set(identity, value, extend_timeout)
#                 return None
#         return res


import asyncio
import pytest
import time
from aredis.cache import Cache, HerdCache

class TestHerdCache:

    app = 'test_cache'
    key = 'test_key'
    data = {str(i): i for i in range(3)}

    def expensive_work(self, data):
        return data

    @pytest.mark.asyncio(forbid_global_loop=True)
    async def test_set(self, r):
        await r.flushdb()
        cache = HerdCache(r, self.app, default_herd_timeout=1,
                          extend_herd_timeout=1)
        now = int(time.time())
        res = await cache.set(self.key,
                              self.expensive_work(self.data),
                              self.data)
        assert res
        identity = cache._gen_identity(self.key, self.data)
        content = await r.get(identity)
        content, expect_expire_time = cache._unpack(content)
        # supposed equal to 1, but may there be latency
        assert expect_expire_time - now <= 1
        assert content == self.data

    @pytest.mark.asyncio(forbid_global_loop=True)
    async def test_get(self, r):
        await r.flushdb()

# Path: aredis/cache.py
# class Cache(BasicCache):
#     """Provides basic caching"""
# 
#     async def get(self, key, param=None):
#         identity = self._gen_identity(key, param)
#         res = await self.client.get(identity)
#         if res:
#             res = self._unpack(res)
#         return res
# 
#     async def set(self, key, value, param=None, expire_time=None):
#         identity = self._gen_identity(key, param)
#         value = self._pack(value)
#         return await self.client.set(identity, value, ex=expire_time)
# 
#     async def set_many(self, data, param=None, expire_time=None):
#         async with await self.client.pipeline() as pipeline:
#             for key, value in data.items():
#                 identity = self._gen_identity(key, param)
#                 value = self._pack(value)
#                 await pipeline.set(identity, value, expire_time)
#             return await pipeline.execute()
# 
# class HerdCache(BasicCache):
#     """
#     Cache that handles thundering herd problem
#     (https://en.wikipedia.org/wiki/Thundering_herd_problem)
#     by cacheing expiration time in set instead of directly using expire
#     operation of redis.
#     This kind of cache is suitable for low consistency scene where update
#     operations are expensive.
#     """
# 
#     def __init__(self, client, app='', identity_generator_class=IdentityGenerator,
#                  compressor_class=Compressor, serializer_class=Serializer,
#                  default_herd_timeout=1, extend_herd_timeout=1, encoding='utf-8'):
#         self.default_herd_timeout = default_herd_timeout
#         self.extend_herd_timeout = extend_herd_timeout
#         super(HerdCache, self).__init__(client, app, identity_generator_class,
#                                         compressor_class, serializer_class,
#                                         encoding)
# 
#     async def set(self, key, value, param=None, expire_time=None, herd_timeout=None):
#         """
#         Uses key and param to generate identity and pack the content,
#         expire the key within real_timeout if expire_time is given.
#         real_timeout is equal to the sum of expire_time and herd_time.
#         The content is cached with expire_time.
#         """
#         identity = self._gen_identity(key, param)
#         expected_expired_ts = int(time.time())
#         if expire_time:
#             expected_expired_ts += expire_time
#         expected_expired_ts += herd_timeout or self.default_herd_timeout
#         value = self._pack([value, expected_expired_ts])
#         return await self.client.set(identity, value, ex=expire_time)
# 
#     async def set_many(self, data, param=None, expire_time=None, herd_timeout=None):
#         async with await self.client.pipeline() as pipeline:
#             for key, value in data.items():
#                 identity = self._gen_identity(key, param)
#                 expected_expired_ts = int(time.time())
#                 if expire_time:
#                     expected_expired_ts += expire_time
#                 expected_expired_ts += herd_timeout or self.default_herd_timeout
#                 value = self._pack([value, expected_expired_ts])
#                 await pipeline.set(identity, value, ex=expire_time)
#             return await pipeline.execute()
# 
#     async def get(self, key, param=None, extend_herd_timeout=None):
#         """
#         Uses key or identity generated from key and param to get cached
#         content and expiratiom time.
#         Uses time.now() to check expiration. If not expired, returns unpacked
#         content. Otherwise returns None and sets cache with extended timeout
#         """
#         identity = self._gen_identity(key, param)
#         res = await self.client.get(identity)
#         if res:
#             res, timeout = self._unpack(res)
#             now = int(time.time())
#             if timeout <= now:
#                 extend_timeout = extend_herd_timeout or self.extend_herd_timeout
#                 expected_expired_ts = now + extend_timeout
#                 value = self._pack([res, expected_expired_ts])
#                 await self.client.set(identity, value, extend_timeout)
#                 return None
#         return res


# Path: tests/client/test_cache.py
import asyncio
import pytest
import time
from aredis.cache import Cache, HerdCache


class TestHerdCache:

    app = 'test_cache'
    key = 'test_key'
    data = {str(i): i for i in range(3)}

    def expensive_work(self, data):
        return data

    @pytest.mark.asyncio(forbid_global_loop=True)
    async def test_set(self, r):
        await r.flushdb()
        cache = HerdCache(r, self.app, default_herd_timeout=1,
                          extend_herd_timeout=1)
        now = int(time.time())
        res = await cache.set(self.key,
                              self.expensive_work(self.data),
                              self.data)
        assert res
        identity = cache._gen_identity(self.key, self.data)
        content = await r.get(identity)
        content, expect_expire_time = cache._unpack(content)
        # supposed equal to 1, but may there be latency
        assert expect_expire_time - now <= 1
        assert content == self.data

    @pytest.mark.asyncio(forbid_global_loop=True)
    async def test_get(self, r):
        await r.flushdb()

        cache = HerdCache(r, self.app, default_herd_timeout=1,