JimJeon: This PR fixes #107 
JimJeon: Python 2.7 Build fails, I'll fix it.
youknowone: Thanks! It seems everything goes well. Installing this module for py2 will work: https://pypi.org/project/mock/
JimJeon: It was already in the setup.py package list. I just added some compatibility codes.
codecov[bot]: # [Codecov](https://codecov.io/gh/youknowone/ring/pull/121?src=pr&el=h1) Report
> Merging [#121](https://codecov.io/gh/youknowone/ring/pull/121?src=pr&el=desc) into [master](https://codecov.io/gh/youknowone/ring/commit/14a2ed761c5ce773eea9d9ca076f48203c561fb2?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `97.14%`.


```diff
@@            Coverage Diff             @@
##           master     #121      +/-   ##
==========================================
- Coverage   98.02%   98.02%   -<.01%     
==========================================
  Files          14       14              
  Lines        1415     1463      +48     
==========================================
+ Hits         1387     1434      +47     
- Misses         28       29       +1
```




diff --git a/ring/func/lru_cache.py b/ring/func/lru_cache.py
index 47e3649..7333811 100644
--- a/ring/func/lru_cache.py
+++ b/ring/func/lru_cache.py
@@ -2,6 +2,7 @@
 # This file follows PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 as CPython
 # does.
 
+import time
 from threading import RLock
 
 try:
@@ -12,13 +13,15 @@ except ImportError:
         "CacheInfo", ["hits", "misses", "maxsize", "currsize"])
 
 SENTINEL = object()  # unique object used to signal cache misses
-PREV, NEXT, KEY, RESULT = 0, 1, 2, 3  # names for the link fields
+PREV, NEXT, KEY, RESULT, EXPIRE = 0, 1, 2, 3, 4  # names for the link fields
 FULL, HITS, MISSES = 0, 1, 2  # names for stat
 
 
 class LruCache(object):
     """Created by breaking down functools.lru_cache from CPython 3.7.0."""
 
+    now = time.time
+
     def __init__(self, maxsize):
         cache = {}
         cache_get = cache.get  # bound method to lookup a key or return None
@@ -26,16 +29,23 @@ class LruCache(object):
         lock = RLock()     # because linkedlist updates aren't threadsafe
         self.root = []   # root of the circular doubly linked list
         # initialize by pointing to self
-        self.root[:] = [self.root, self.root, None, None]
+        self.root[:] = [self.root, self.root, None, None, None]
         stat = [False, 0, 0]
 
+        def expiration_time(expire):
+            if expire is None:
+                return expire
+            return self.now() + expire
+
         def get(key):
+            _now = self.now()
             with lock:
                 link = cache_get(key)
-                if link is not None:
+                if link is not None and \
+                        (link[EXPIRE] is None or _now < link[EXPIRE]):
                     root = self.root
                     # Move the link to the front of the circular queue
-                    link_prev, link_next, _key, result = link
+                    link_prev, link_next, _key, result, _ = link
                     link_prev[NEXT] = link_next
                     link_next[PREV] = link_prev
                     last = root[PREV]
@@ -46,25 +56,40 @@ class LruCache(object):
                     return result
                 else:
                     stat[MISSES] += 1
+                    if link is not None:
+                        _delete(key)
             return SENTINEL
 
+        def _delete(key):
+            oldresult = self.root[RESULT]  # noqa
+            # delete from the linked list
+            link = cache[key]
+            link_prev, link_next, _, _, _ = link
+            link_prev[NEXT] = link_next
+            link_next[PREV] = link_prev
+
+            # delete from the cache
+            del cache[key]
+            stat[FULL] = False
+
         def delete(key):
             with lock:
-                oldresult = self.root[RESULT]  # noqa
-                del cache[key]
-                stat[FULL] = False
+                _delete(key)
 
-        def set(key, result):
+        def set(key, result, expire=None):
+            expired_time = expiration_time(expire)
             with lock:
                 link = cache_get(key)
                 if link is not None:
                     # Update link to store the new result
-                    link[-1] = result
+                    link[RESULT] = result
+                    link[EXPIRE] = expired_time
                 elif stat[FULL]:
                     # Use the old root to store the new key and result.
                     oldroot = self.root
                     oldroot[KEY] = key
                     oldroot[RESULT] = result
+                    oldroot[EXPIRE] = expired_time
                     # Empty the oldest link and make it the new root.
                     # Keep a reference to the old key and old result to
                     # prevent their ref counts from going to zero during the
@@ -85,7 +110,7 @@ class LruCache(object):
                     root = self.root
                     # Put result in a new link at the front of the queue.
                     last = root[PREV]
-                    link = [last, root, key, result]
+                    link = [last, root, key, result, expired_time]
                     last[NEXT] = root[PREV] = cache[key] = link
                     # Use the cache_len bound method instead of the len() function
                     # which could potentially be wrapped in an lru_cache itself.
@@ -116,14 +141,17 @@ class LruCache(object):
             with lock:
                 return key in cache
 
-        def touch(key):
+        def touch(key, expire=None):
+            expired_time = expiration_time(expire)
             with lock:
                 root = self.root
                 link = cache_get(key)
                 if link is None:
                     raise KeyError
                 # Move the link to the front of the circular queue
-                link_prev, link_next, _key, result = link
+                link_prev, link_next, _key, result, _ = link
+                # Also, update expiration time
+                link[EXPIRE] = expired_time
                 link_prev[NEXT] = link_next
                 link_next[PREV] = link_prev
                 last = root[PREV]
diff --git a/ring/func/sync.py b/ring/func/sync.py
index 496e4b2..07e042b 100644
--- a/ring/func/sync.py
+++ b/ring/func/sync.py
@@ -191,7 +191,7 @@ class LruStorage(fbase.CommonMixinStorage, fbase.StorageMixin):
         return self.backend.has(key)
 
     def set_value(self, key, value, expire):
-        self.backend.set(key, value)
+        self.backend.set(key, value, expire)
 
     def delete_value(self, key):
         try:
@@ -398,7 +398,7 @@ class DiskCacheStorage(fbase.CommonMixinStorage, fbase.StorageMixin):
 
 
 def lru(
-        lru=None, key_prefix=None, coder=None,
+        lru=None, key_prefix=None, expire=None, coder=None,
         user_interface=CacheUserInterface, storage_class=LruStorage,
         maxsize=128, **kwargs):
     """LRU(Least-Recently-Used) cache interface.
@@ -427,7 +427,6 @@ def lru(
     :see: :func:`functools.lru_cache` for LRU cache basics.
     :see: :func:`ring.func.sync.CacheUserInterface` for sub-functions.
     """
-    expire = None
     if lru is None:
         lru = lru_mod.LruCache(maxsize)
         if key_prefix is None:

Expirable LRU cache
For now, `@ring.lru` ignores `expire` parameter.

In this case, expiration maybe not very correct.

A suggestion.
- Put timestamp into 5th paramter of `link`.
- For hot items, it will be expired by checking timestamp at next `get`.
- For cold items, it will be smoothly expired by LRU logic.

LruCache source: https://github.com/youknowone/ring/blob/master/ring/func/lru_cache.py

- Will it need a separated class like PersistantDict and ExpirableDict?
  - Probably not.
diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py
index 54ba45a..d4658eb 100644
--- a/tests/test_lru_cache.py
+++ b/tests/test_lru_cache.py
@@ -7,6 +7,11 @@ except ImportError:
     def _make_key(args, kwds, typed):
         return args, tuple(sorted(kwds.items()))
 
+try:
+    from unittest.mock import MagicMock
+except ImportError:
+    from mock import MagicMock
+
 
 def test_lru_cache_porting():
     def lru_cache(maxsize=128, typed=False):
@@ -67,3 +72,27 @@ def test_lru_object():
         assert SENTINEL is lru.get(c)
 
     lru.cache_info()
+
+
+def test_expire_object():
+    lru = LruCache(3)
+
+    now_mock = MagicMock()
+    now_mock.return_value = 0
+    lru.now = now_mock
+
+    lru.set('a', 10, expire=1)
+    lru.set('b', 20, expire=2)
+    lru.set('c', 30, expire=3)  # a - b - c
+    # Check if cache works well
+    assert lru.get('a') == 10  # b - c - a
+    # Check if 'a' key expired
+    now_mock.return_value = 1
+    assert lru.get('a') == SENTINEL  # b - c
+    # Check if lru logic works well
+    lru.set('d', 40, expire=4)  # b - c - d
+    assert lru.get('b') == 20  # c - d - b
+    # Check if 'c' key not expired
+    assert lru.get('c') == 30  # d - b - c
+    lru.set('e', 50)  # b - c - e
+    assert lru.get('d') == SENTINEL

pip install -e .[dev]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc libmemcached-dev memcached redis-server
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiomcache==0.6.0
aioredis==2.0.1
alabaster==0.7.13
asgiref==3.4.1
async-timeout==4.0.2
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
diskcache==5.6.3
Django==3.2.25
docutils==0.18.1
idna==3.10
imagesize==1.4.1
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.0.3
MarkupSafe==2.0.1
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
patch==1.16
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
Pygments==2.14.0
pylibmc==1.6.3
pymemcache==3.5.2
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-asyncio==0.16.0
pytest-cov==4.0.0
pytest-lazy-fixture==0.5.2
python3-memcached==1.51
pytz==2025.2
redis==4.3.6
requests==2.27.1
-e git+https://github.com/youknowone/ring.git@d3c3408af39e74c6fc9f01e00ae8aa58c2fe2e59#egg=ring
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
sqlparse==0.4.4
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
wirerope==1.0.0
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: ring
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiomcache==0.6.0
      - aioredis==2.0.1
      - alabaster==0.7.13
      - asgiref==3.4.1
      - async-timeout==4.0.2
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - diskcache==5.6.3
      - django==3.2.25
      - docutils==0.18.1
      - idna==3.10
      - imagesize==1.4.1
      - jinja2==3.0.3
      - markupsafe==2.0.1
      - mock==5.2.0
      - patch==1.16
      - pygments==2.14.0
      - pylibmc==1.6.3
      - pymemcache==3.5.2
      - pytest-asyncio==0.16.0
      - pytest-cov==4.0.0
      - pytest-lazy-fixture==0.5.2
      - python3-memcached==1.51
      - pytz==2025.2
      - redis==4.3.6
      - requests==2.27.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - sqlparse==0.4.4
      - tomli==1.2.3
      - urllib3==1.26.20
      - wirerope==1.0.0
prefix: /opt/conda/envs/ring

tests/test_lru_cache.py::test_expire_object
tests/test_lru_cache.py::test_lru_cache_porting tests/test_lru_cache.py::test_lru_object
ring/func/lru_cache.py ring/func/sync.py
ring/func/lru_cache.py ring/func/sync.py