diff --git a/openmmtools/cache.py b/openmmtools/cache.py
index f6d3ede..87ec8d6 100644
--- a/openmmtools/cache.py
+++ b/openmmtools/cache.py
@@ -17,7 +17,7 @@ Provide cache classes to handle creation of OpenMM Context objects.
 import copy
 import collections
 
-from simtk import openmm
+from simtk import openmm, unit
 
 from openmmtools import integrators
 
@@ -114,12 +114,26 @@ class LRUCache(object):
     @time_to_live.setter
     def time_to_live(self, new_time_to_live):
         # Update entries only if we are changing the ttl.
-        ttl_diff = new_time_to_live - self._ttl
-        if ttl_diff == 0:
+        if new_time_to_live == self._ttl:
             return
+
+        # Update expiration of cache entries.
         for entry in self._data.values():
-            entry.expiration += ttl_diff
-        self._remove_expired()
+            # If there was no time to live before, just let entries
+            # expire in new_time_to_live accesses
+            if self._ttl is None:
+                entry.expiration = self._n_access + new_time_to_live
+            # If we don't want expiration anymore, delete the field.
+            # This way we save memory in case there are a lot of entries.
+            elif new_time_to_live is None:
+                del entry.expiration
+            # Otherwise just add/subtract the difference.
+            else:
+                entry.expiration += new_time_to_live - self._ttl
+
+        # Purge cache only if there is a time to live.
+        if new_time_to_live is not None:
+            self._remove_expired()
         self._ttl = new_time_to_live
 
     def empty(self):
@@ -366,11 +380,18 @@ class ContextCache(object):
             thermodynamic_state_id = self._generate_state_id(thermodynamic_state)
             matching_context_ids = [context_id for context_id in self._lru
                                     if context_id[0] == thermodynamic_state_id]
-            if len(matching_context_ids) > 0:
-                context = self._lru[matching_context_ids[0]]  # Return first found.
+            if len(matching_context_ids) == 0:
+                # We have to create a new Context.
+                integrator = self._get_default_integrator(thermodynamic_state.temperature)
+            elif len(matching_context_ids) == 1:
+                # Only one match.
+                context = self._lru[matching_context_ids[0]]
             else:
-                # We have to create a new Context. Use a likely-to-be-used Integrator.
-                integrator = integrators.GeodesicBAOABIntegrator(temperature=thermodynamic_state.temperature)
+                # Multiple matches, prefer non-default Integrator.
+                for context_id in matching_context_ids:
+                    if context_id[1] != self._default_integrator_id():
+                        context = self._lru[context_id]
+                        break
 
         if context is None:
             # Determine the Context id matching the pair state-integrator.
@@ -447,6 +468,7 @@ class ContextCache(object):
 
         """
         standard_integrator = copy.deepcopy(integrator)
+        integrators.RestorableIntegrator.restore_interface(standard_integrator)
         for attribute, std_value in cls._COMPATIBLE_INTEGRATOR_ATTRIBUTES.items():
             try:
                 getattr(standard_integrator, 'set' + attribute)(std_value)
@@ -459,7 +481,13 @@ class ContextCache(object):
         """Return a unique key for the ThermodynamicState."""
         # We take advantage of the cached _standard_system_hash property
         # to generate a compatible hash for the thermodynamic state.
-        return str(thermodynamic_state._standard_system_hash)
+        return thermodynamic_state._standard_system_hash
+
+    @classmethod
+    def _generate_integrator_id(cls, integrator):
+        """Return a unique key for the given Integrator."""
+        standard_integrator = cls._standardize_integrator(integrator)
+        return openmm.XmlSerializer.serialize(standard_integrator).__hash__()
 
     @classmethod
     def _generate_context_id(cls, thermodynamic_state, integrator):
@@ -472,10 +500,25 @@ class ContextCache(object):
 
         """
         state_id = cls._generate_state_id(thermodynamic_state)
-        standard_integrator = cls._standardize_integrator(integrator)
-        integrator_id = openmm.XmlSerializer.serialize(standard_integrator).__hash__()
+        integrator_id = cls._generate_integrator_id(integrator)
         return state_id, integrator_id
 
+    @staticmethod
+    def _get_default_integrator(temperature):
+        """Return a new instance of the default integrator."""
+        # Use a likely-to-be-used Integrator.
+        return integrators.GeodesicBAOABIntegrator(temperature=temperature)
+
+    @classmethod
+    def _default_integrator_id(cls):
+        """Return the unique key of the default integrator."""
+        if cls._cached_default_integrator_id is None:
+            default_integrator = cls._get_default_integrator(300*unit.kelvin)
+            default_integrator_id = cls._generate_integrator_id(default_integrator)
+            cls._cached_default_integrator_id = default_integrator_id
+        return cls._cached_default_integrator_id
+    _cached_default_integrator_id = None
+
 
 # =============================================================================
 # DUMMY CONTEXT CACHE
@@ -523,7 +566,7 @@ class DummyContextCache(object):
 # GLOBAL CONTEXT CACHE
 # =============================================================================
 
-global_context_cache = ContextCache(capacity=3, time_to_live=50)
+global_context_cache = ContextCache(capacity=None, time_to_live=None)
 
 
 # =============================================================================
diff --git a/openmmtools/integrators.py b/openmmtools/integrators.py
index ecdb460..ce87c38 100644
--- a/openmmtools/integrators.py
+++ b/openmmtools/integrators.py
@@ -38,6 +38,7 @@ this program.  If not, see <http://www.gnu.org/licenses/>.
 import numpy as np
 import logging
 import re
+import zlib
 
 import simtk.unit
 
@@ -213,9 +214,18 @@ class RestorableIntegrator(mm.CustomIntegrator,PrettyPrintableIntegrator):
 
     @staticmethod
     def _compute_class_hash(integrator_class):
-        """Return a numeric hash for the integrator class."""
-        # We need to convert to float because some digits may be lost in the conversion
-        return float(hash(integrator_class.__name__))
+        """Return a numeric hash for the integrator class.
+
+        The hash will become part of the Integrator serialization,
+        so it is important for it consistent across processes in case
+        the integrator is sent to a remote worker. The hash() built-in
+        function is seeded by the PYTHONHASHSEED environmental variable,
+        so we can't use it here.
+
+        We also need to convert to float because some digits may be
+        lost in the conversion.
+        """
+        return float(zlib.adler32(integrator_class.__name__.encode()))
 
 
 class ThermostatedIntegrator(RestorableIntegrator):

Context cache failure to access `set*`
Unfortunately, I don't have a minimal example yet, but I did want to note that a few days ago, as I was trying to use the `ContextCache` with a `celery` worker, I ran into a strange bug. Using the `AlchemicalNonequilibriumLangevinIntegrator`, whenever it tried to recover a context and integrator, I got an exception that there was no attribute `set*` (including `setTemperature`, etc.).

 After some debugging, I noticed that the `AlchemicalNonequilibriumLangevinIntegrator` was becoming a regular `CustomIntegrator` somehow, but I'm not quite sure how or why.

This isn't super high priority, since I have a workaround for now, but since the `ContextCache` API is really awesome and convenient, it would be great to figure out what's going on.
diff --git a/openmmtools/tests/test_cache.py b/openmmtools/tests/test_cache.py
index de9b008..ef8a8ef 100644
--- a/openmmtools/tests/test_cache.py
+++ b/openmmtools/tests/test_cache.py
@@ -124,14 +124,32 @@ def test_lru_cache_capacity_property():
 
 def test_lru_cache_time_to_live_property():
     """Decreasing the time to live updates the expiration of elements."""
-    cache = LRUCache(time_to_live=50)
-    for i in range(4):
-        cache[str(i)] = i
+    def add_4_elements(_cache):
+        for i in range(4):
+            _cache[str(i)] = i
+
+    cache = LRUCache(time_to_live=None)
+    add_4_elements(cache)
     assert len(cache) == 4
+
+    # Setting time to live to 1 cause all 4 entries to expire on next access.
     cache.time_to_live = 1
-    assert len(cache) == 1
+    assert len(cache) == 4
     assert cache.time_to_live == 1
-    assert '3' in cache
+    cache['4'] = 4
+    assert len(cache) == 1
+    assert '4' in cache
+
+    # Increase time_to_live.
+    cache.time_to_live = 2
+    add_4_elements(cache)
+    assert len(cache) == 2
+    assert '2' in cache and '3' in cache
+
+    # Setting it back to None makes it limitless.
+    cache.time_to_live = None
+    add_4_elements(cache)
+    assert len(cache) == 4
 
 
 # =============================================================================
@@ -226,19 +244,25 @@ class TestContextCache(object):
         cache = ContextCache()
         state1, state2 = self.incompatible_states[:2]
 
-        # First we create a Context in state1.
-        cache.get_context(state1, copy.deepcopy(self.verlet_2fs))
+        # If there are no previous Contexts, a default integrator is used to create a new one.
+        context, default_integrator = cache.get_context(state1)
         assert len(cache) == 1
 
-        # When we don't specify the integrator, it first looks for cached Contexts.
+        # Now we create another Context in state1 with a different integrator.
+        assert type(self.verlet_2fs) is not type(default_integrator)  # test precondition
+        cache.get_context(state1, copy.deepcopy(self.verlet_2fs))
+        assert len(cache) == 2
+
+        # When we don't specify the integrator, it first looks
+        # for cached Contexts, and non-default contexts are preferred.
         context, integrator = cache.get_context(state1)
-        assert len(cache) == 1
+        assert len(cache) == 2
         assert state1.is_context_compatible(context)
-        assert isinstance(integrator, openmm.VerletIntegrator)
+        assert isinstance(integrator, type(self.verlet_2fs)), type(integrator)
 
         # With an incompatible state, a new Context is created.
         cache.get_context(state2)
-        assert len(cache) == 2
+        assert len(cache) == 3
 
     def test_cache_capacity_ttl(self):
         """Check that the cache capacity and time_to_live work as expected."""

docs/environment.yml
pip install -e .[dev]
parse_log_pytest
environment.yml
nose pymbar pytest
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster @ file:///home/ktietz/src/ci/alabaster_1611921544520/work
attrs==22.2.0
Babel @ file:///tmp/build/80754af9/babel_1620871417480/work
brotlipy==0.7.0
certifi==2021.5.30
cffi @ file:///tmp/build/80754af9/cffi_1625814693874/work
charset-normalizer @ file:///tmp/build/80754af9/charset-normalizer_1630003229654/work
colorama @ file:///tmp/build/80754af9/colorama_1607707115595/work
cryptography @ file:///tmp/build/80754af9/cryptography_1635366128178/work
Cython @ file:///tmp/build/80754af9/cython_1626256602391/work
docutils @ file:///tmp/build/80754af9/docutils_1620827982266/work
idna @ file:///tmp/build/80754af9/idna_1637925883363/work
imagesize @ file:///tmp/build/80754af9/imagesize_1637939814114/work
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig==1.1.1
Jinja2 @ file:///opt/conda/conda-bld/jinja2_1647436528585/work
MarkupSafe @ file:///tmp/build/80754af9/markupsafe_1621528150516/work
nose==1.3.7
numexpr==2.8.1
numpy @ file:///tmp/build/80754af9/numpy_and_numpy_base_1603483703303/work
numpydoc @ file:///tmp/build/80754af9/numpydoc_1605117425582/work
OpenMM==7.4.2
-e git+https://github.com/choderalab/openmmtools.git@fe82cd1b81ae2026ad0299430293e27c0128335e#egg=openmmtools
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pandas==1.1.5
ParmEd==3.2.0
pluggy==1.0.0
py==1.11.0
pycparser @ file:///tmp/build/80754af9/pycparser_1636541352034/work
Pygments @ file:///opt/conda/conda-bld/pygments_1644249106324/work
pymbar==4.0.3
pyOpenSSL @ file:///opt/conda/conda-bld/pyopenssl_1643788558760/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
PySocks @ file:///tmp/build/80754af9/pysocks_1605305763431/work
pytest==7.0.1
python-dateutil @ file:///tmp/build/80754af9/python-dateutil_1626374649649/work
pytz==2021.3
requests @ file:///opt/conda/conda-bld/requests_1641824580448/work
scipy @ file:///tmp/build/80754af9/scipy_1597686635649/work
six @ file:///tmp/build/80754af9/six_1644875935023/work
snowballstemmer @ file:///tmp/build/80754af9/snowballstemmer_1637937080595/work
Sphinx @ file:///opt/conda/conda-bld/sphinx_1643644169832/work
sphinxcontrib-applehelp @ file:///home/ktietz/src/ci/sphinxcontrib-applehelp_1611920841464/work
sphinxcontrib-devhelp @ file:///home/ktietz/src/ci/sphinxcontrib-devhelp_1611920923094/work
sphinxcontrib-htmlhelp @ file:///tmp/build/80754af9/sphinxcontrib-htmlhelp_1623945626792/work
sphinxcontrib-jsmath @ file:///home/ktietz/src/ci/sphinxcontrib-jsmath_1611920942228/work
sphinxcontrib-qthelp @ file:///home/ktietz/src/ci/sphinxcontrib-qthelp_1611921055322/work
sphinxcontrib-serializinghtml @ file:///tmp/build/80754af9/sphinxcontrib-serializinghtml_1624451540180/work
tomli==1.2.3
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3 @ file:///opt/conda/conda-bld/urllib3_1643638302206/work
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: openmmtools
channels:
  - omnia
  - 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
  - alabaster=0.7.12=pyhd3eb1b0_0
  - babel=2.9.1=pyhd3eb1b0_0
  - blas=1.0=openblas
  - brotlipy=0.7.0=py36h27cfd23_1003
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - cffi=1.14.6=py36h400218f_0
  - charset-normalizer=2.0.4=pyhd3eb1b0_0
  - colorama=0.4.4=pyhd3eb1b0_0
  - cryptography=35.0.0=py36hd23ed53_0
  - cython=0.29.24=py36h295c915_0
  - docutils=0.17.1=py36h06a4308_1
  - fftw3f=3.3.4=2
  - idna=3.3=pyhd3eb1b0_0
  - imagesize=1.3.0=pyhd3eb1b0_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - jinja2=3.0.3=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgfortran-ng=7.5.0=ha8ba4b0_17
  - libgfortran4=7.5.0=ha8ba4b0_17
  - libgomp=11.2.0=h1234567_1
  - libopenblas=0.3.18=hf726d26_0
  - libstdcxx-ng=11.2.0=h1234567_1
  - markupsafe=2.0.1=py36h27cfd23_0
  - ncurses=6.4=h6a678d5_0
  - numpy=1.19.2=py36h6163131_0
  - numpy-base=1.19.2=py36h75fe3a5_0
  - numpydoc=1.1.0=pyhd3eb1b0_1
  - openmm=7.4.2=py36_cuda101_rc_1
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pandas=1.1.5=py36ha9443f7_0
  - parmed=3.2.0=py36_0
  - pip=21.2.2=py36h06a4308_0
  - pycparser=2.21=pyhd3eb1b0_0
  - pygments=2.11.2=pyhd3eb1b0_0
  - pyopenssl=22.0.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pysocks=1.7.1=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - python-dateutil=2.8.2=pyhd3eb1b0_0
  - pytz=2021.3=pyhd3eb1b0_0
  - readline=8.2=h5eee18b_0
  - requests=2.27.1=pyhd3eb1b0_0
  - scipy=1.5.2=py36habc2bb6_0
  - setuptools=58.0.4=py36h06a4308_0
  - six=1.16.0=pyhd3eb1b0_1
  - snowballstemmer=2.2.0=pyhd3eb1b0_0
  - sphinx=4.4.0=pyhd3eb1b0_0
  - sphinxcontrib-applehelp=1.0.2=pyhd3eb1b0_0
  - sphinxcontrib-devhelp=1.0.2=pyhd3eb1b0_0
  - sphinxcontrib-htmlhelp=2.0.0=pyhd3eb1b0_0
  - sphinxcontrib-jsmath=1.0.1=pyhd3eb1b0_0
  - sphinxcontrib-qthelp=1.0.3=pyhd3eb1b0_0
  - sphinxcontrib-serializinghtml=1.1.5=pyhd3eb1b0_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - urllib3=1.26.8=pyhd3eb1b0_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:
      - attrs==22.2.0
      - iniconfig==1.1.1
      - nose==1.3.7
      - numexpr==2.8.1
      - pluggy==1.0.0
      - py==1.11.0
      - pymbar==4.0.3
      - pytest==7.0.1
      - tomli==1.2.3
prefix: /opt/conda/envs/openmmtools

openmmtools/tests/test_cache.py::test_lru_cache_time_to_live_property openmmtools/tests/test_cache.py::TestContextCache::test_get_context_any_integrator
openmmtools/tests/test_cache.py::test_lru_cache_cache_entry_unpacking openmmtools/tests/test_cache.py::test_lru_cache_maximum_capacity openmmtools/tests/test_cache.py::test_lru_cache_eliminate_least_recently_used openmmtools/tests/test_cache.py::test_lru_cache_access_to_live openmmtools/tests/test_cache.py::test_lru_cache_capacity_property openmmtools/tests/test_cache.py::TestContextCache::test_copy_integrator_state openmmtools/tests/test_cache.py::TestContextCache::test_generate_compatible_context_key openmmtools/tests/test_cache.py::TestContextCache::test_generate_incompatible_context_key openmmtools/tests/test_cache.py::TestContextCache::test_get_compatible_context openmmtools/tests/test_cache.py::TestContextCache::test_get_incompatible_context openmmtools/tests/test_cache.py::TestContextCache::test_cache_capacity_ttl openmmtools/tests/test_cache.py::TestContextCache::test_platform_property
openmmtools/integrators.py openmmtools/cache.py
openmmtools/integrators.py openmmtools/cache.py