Improvements to the matrix cache (#1340)

* Improvements to the matrix cache

- Add a timelimit for the matrix in the cache.
Once the timelimit is reached, the matrix can be removed.

- If a DatasetAccessError occurs, then remove the dataset
from the matrix cache.

Fixes #1322
This commit is contained in:
bmccandless
2020-04-02 13:44:11 -07:00
committed by GitHub
parent fa0164fdf6
commit 308ee64f30
9 changed files with 260 additions and 61 deletions
+3 -5
View File
@@ -239,8 +239,7 @@ def get_api_resources(bp_api):
class Server: class Server:
def __init__(self, matrix_data_cache_manager, annotations, app_config): def __init__(self, app_config):
self.app = Flask(__name__, static_folder="../common/web/static") self.app = Flask(__name__, static_folder="../common/web/static")
self._before_adding_routes(app_config) self._before_adding_routes(app_config)
self.app.json_encoder = Float32JSONEncoder self.app.json_encoder = Float32JSONEncoder
@@ -268,9 +267,8 @@ class Server:
resources = get_api_resources(bp_api) resources = get_api_resources(bp_api)
self.app.register_blueprint(resources.blueprint) self.app.register_blueprint(resources.blueprint)
self.app.add_url_rule("/d/<dataset>/", "dataset_index", dataset_index, methods=["GET"]) self.app.add_url_rule("/d/<dataset>/", "dataset_index", dataset_index, methods=["GET"])
self.app.matrix_data_cache_manager = app_config.matrix_data_cache_manager
self.app.matrix_data_cache_manager = matrix_data_cache_manager self.app.annotations = app_config.user_annotations
self.app.annotations = annotations
self.app.app_config = app_config self.app.app_config = app_config
def _before_adding_routes(self, app_config): def _before_adding_routes(self, app_config):
+4 -7
View File
@@ -11,7 +11,6 @@ from flask_cors import CORS
from server.common.utils import sort_options from server.common.utils import sort_options
from server.common.errors import DatasetAccessError, ConfigurationError from server.common.errors import DatasetAccessError, ConfigurationError
from server.data_common.matrix_loader import MatrixDataCacheManager
from server.common.app_config import AppConfig from server.common.app_config import AppConfig
from server.common.default_config import default_config from server.common.default_config import default_config
from server.app.app import Server from server.app.app import Server
@@ -276,8 +275,8 @@ class CliLaunchServer(Server):
the CLI runs a local web server, and needs to enable a few more features. the CLI runs a local web server, and needs to enable a few more features.
""" """
def __init__(self, matrix_data_cache_manager, annotations, app_config): def __init__(self, app_config):
super().__init__(matrix_data_cache_manager, annotations, app_config) super().__init__(app_config)
def _before_adding_routes(self, app_config): def _before_adding_routes(self, app_config):
self.app.config["COMPRESS_MIMETYPES"] = [ self.app.config["COMPRESS_MIMETYPES"] = [
@@ -388,12 +387,11 @@ def launch(
# process the configuration # process the configuration
# any errors will be thrown as an exception. # any errors will be thrown as an exception.
# any info messages will be passed to the messagefn function. # any info messages will be passed to the messagefn function.
matrix_data_cache_manager = MatrixDataCacheManager()
def messagefn(message): def messagefn(message):
click.echo("[cellxgene] " + message) click.echo("[cellxgene] " + message)
app_config.complete_config(matrix_data_cache_manager, messagefn) app_config.complete_config(messagefn)
# Use a default secret if one is not provided # Use a default secret if one is not provided
if not app_config.server__flask_secret_key: if not app_config.server__flask_secret_key:
@@ -403,10 +401,9 @@ def launch(
raise click.ClickException(e) raise click.ClickException(e)
handle_scripts(scripts) handle_scripts(scripts)
user_annotations = app_config.user_annotations
# create the server # create the server
server = CliLaunchServer(matrix_data_cache_manager, user_annotations, app_config) server = CliLaunchServer(app_config)
if not app_config.server__verbose: if not app_config.server__verbose:
log = logging.getLogger("werkzeug") log = logging.getLogger("werkzeug")
+19 -8
View File
@@ -72,6 +72,7 @@ class AppConfig(object):
self.multi_dataset__index = dc["multi_dataset"]["index"] self.multi_dataset__index = dc["multi_dataset"]["index"]
self.multi_dataset__allowed_matrix_types = dc["multi_dataset"]["allowed_matrix_types"] self.multi_dataset__allowed_matrix_types = dc["multi_dataset"]["allowed_matrix_types"]
self.multi_dataset__matrix_cache__max_datasets = dc["multi_dataset"]["matrix_cache"]["max_datasets"] self.multi_dataset__matrix_cache__max_datasets = dc["multi_dataset"]["matrix_cache"]["max_datasets"]
self.multi_dataset__matrix_cache__timelimit_s = dc["multi_dataset"]["matrix_cache"]["timelimit_s"]
self.single_dataset__datapath = dc["single_dataset"]["datapath"] self.single_dataset__datapath = dc["single_dataset"]["datapath"]
self.single_dataset__obs_names = dc["single_dataset"]["obs_names"] self.single_dataset__obs_names = dc["single_dataset"]["obs_names"]
@@ -108,6 +109,9 @@ class AppConfig(object):
# The annotation object is created during complete_config and stored here. # The annotation object is created during complete_config and stored here.
self.user_annotations = None self.user_annotations = None
# The matrix data cache manager is created during the complete_config and stored here.
self.matrix_data_cache_manager = None
# Set to true when config_completed is called # Set to true when config_completed is called
self.is_completed = False self.is_completed = False
@@ -146,12 +150,10 @@ class AppConfig(object):
self.is_completed = False self.is_completed = False
def complete_config(self, matrix_data_cache_manager=None, messagefn=None): def complete_config(self, messagefn=None):
"""The configure options are checked, and any additional setup based on the config """The configure options are checked, and any additional setup based on the config
parameters is done""" parameters is done"""
if matrix_data_cache_manager is None:
matrix_data_cache_manager = MatrixDataCacheManager()
if messagefn is None: if messagefn is None:
def noop(message): def noop(message):
@@ -162,7 +164,7 @@ class AppConfig(object):
# TODO: to give better error messages we can add a mapping between where each config # TODO: to give better error messages we can add a mapping between where each config
# attribute originated (e.g. command line argument or config file), then in the error # attribute originated (e.g. command line argument or config file), then in the error
# messages we can give correct context for attributes with bad value. # messages we can give correct context for attributes with bad value.
context = dict(matrix_cache=matrix_data_cache_manager, messagefn=messagefn) context = dict(messagefn=messagefn)
self.handle_server(context) self.handle_server(context)
self.handle_single_dataset(context) self.handle_single_dataset(context)
@@ -242,6 +244,10 @@ class AppConfig(object):
if self.multi_dataset__dataroot is not None: if self.multi_dataset__dataroot is not None:
raise ConfigurationError("must supply only one of datapath or dataroot") raise ConfigurationError("must supply only one of datapath or dataroot")
# create the matrix data cache manager:
if self.matrix_data_cache_manager is None:
self.matrix_data_cache_manager = MatrixDataCacheManager(max_cached=1, timelimit_s=None)
# preload this data set # preload this data set
matrix_data_loader = MatrixDataLoader(self.single_dataset__datapath) matrix_data_loader = MatrixDataLoader(self.single_dataset__datapath)
try: try:
@@ -278,6 +284,7 @@ class AppConfig(object):
self.__check_attr("multi_dataset__index", (type(None), bool, str)) self.__check_attr("multi_dataset__index", (type(None), bool, str))
self.__check_attr("multi_dataset__allowed_matrix_types", (tuple, list)) self.__check_attr("multi_dataset__allowed_matrix_types", (tuple, list))
self.__check_attr("multi_dataset__matrix_cache__max_datasets", int) self.__check_attr("multi_dataset__matrix_cache__max_datasets", int)
self.__check_attr("multi_dataset__matrix_cache__timelimit_s", (type(None), int, float))
if self.multi_dataset__dataroot is None: if self.multi_dataset__dataroot is None:
return return
@@ -289,8 +296,12 @@ class AppConfig(object):
except ValueError: except ValueError:
raise ConfigurationError(f'Invalid matrix type in "allowed_matrix_types": {mtype}') raise ConfigurationError(f'Invalid matrix type in "allowed_matrix_types": {mtype}')
# matrix cache # create the matrix data cache manager:
MatrixDataCacheManager.set_max_datasets(self.multi_dataset__matrix_cache__max_datasets) if self.matrix_data_cache_manager is None:
self.matrix_data_cache_manager = MatrixDataCacheManager(
max_cached=self.multi_dataset__matrix_cache__max_datasets,
timelimit_s=self.multi_dataset__matrix_cache__timelimit_s,
)
def handle_user_annotations(self, context): def handle_user_annotations(self, context):
self.__check_attr("user_annotations__enable", bool) self.__check_attr("user_annotations__enable", bool)
@@ -328,7 +339,7 @@ class AppConfig(object):
# if the user has specified a fixed label file, go ahead and validate it # if the user has specified a fixed label file, go ahead and validate it
# so that we can remove errors early in the process. # so that we can remove errors early in the process.
if self.single_dataset__datapath and self.user_annotations__local_file_csv__file: if self.single_dataset__datapath and self.user_annotations__local_file_csv__file:
with context["matrix_cache"].data_adaptor(self.single_dataset__datapath, self) as data_adaptor: with self.matrix_data_cache_manager.data_adaptor(self.single_dataset__datapath, self) as data_adaptor:
data_adaptor.check_new_labels(self.user_annotations.read_labels(data_adaptor)) data_adaptor.check_new_labels(self.user_annotations.read_labels(data_adaptor))
if self.user_annotations__ontology__enable or self.user_annotations__ontology__obo_location: if self.user_annotations__ontology__enable or self.user_annotations__ontology__obo_location:
@@ -372,7 +383,7 @@ class AppConfig(object):
self.__check_attr("diffexp__lfc_cutoff", float) self.__check_attr("diffexp__lfc_cutoff", float)
if self.single_dataset__datapath: if self.single_dataset__datapath:
with context["matrix_cache"].data_adaptor(self.single_dataset__datapath, self) as data_adaptor: with self.matrix_data_cache_manager.data_adaptor(self.single_dataset__datapath, self) as data_adaptor:
if self.diffexp__enable and data_adaptor.parameters.get("diffexp_may_be_slow", False): if self.diffexp__enable and data_adaptor.parameters.get("diffexp_may_be_slow", False):
context["messagefn"]( context["messagefn"](
f"CAUTION: due to the size of your dataset, " f"CAUTION: due to the size of your dataset, "
+4
View File
@@ -36,6 +36,10 @@ multi_dataset:
# is evicted from the cache first. # is evicted from the cache first.
max_datasets: 5 max_datasets: 5
# A matrix is automatically removed from the cache after timelimit_s number of seconds.
# If timelimit_s is set to None, then there is no time limit.
timelimit_s: 30
single_dataset: single_dataset:
datapath: null datapath: null
obs_names: null obs_names: null
+90 -35
View File
@@ -47,7 +47,7 @@ class MatrixDataCacheItem(object):
# necessary to hold the reader lock after an exception, since # necessary to hold the reader lock after an exception, since
# the release will occur when the context exits. # the release will occur when the context exits.
self.data_lock.w_demote() self.data_lock.w_demote()
raise e raise DatasetAccessError(str(e))
# demote the write lock to a read lock. # demote the write lock to a read lock.
self.data_lock.w_demote() self.data_lock.w_demote()
@@ -64,90 +64,145 @@ class MatrixDataCacheItem(object):
self.data_adaptor.cleanup() self.data_adaptor.cleanup()
self.data_adaptor = None self.data_adaptor = None
def attempt_delete(self):
"""Delete, but only if the write lock can be immediately locked. Return True if the delete happened"""
if self.data_lock.w_acquire_non_blocking():
if self.data_adaptor:
try:
self.data_adaptor.cleanup()
self.data_adaptor = None
except Exception:
# catch all exceptions to ensure the lock is released
pass
self.data_lock.w_release()
return True
else:
return False
class MatrixDataCacheInfo(object):
def __init__(self, cache_item, timestamp):
# The MatrixDataCacheItem in the cache
self.cache_item = cache_item
# The last time the cache_item was accessed
self.last_access = timestamp
# The number of times the cache_item was accessed (used for testing)
self.num_access = 1
class MatrixDataCacheManager(object): class MatrixDataCacheManager(object):
"""A class to manage the cached datasets. This is intended to be used as a context manager """A class to manage the cached datasets. This is intended to be used as a context manager
for handling api requests. When the context is created, the data_adator is either loaded or for handling api requests. When the context is created, the data_adator is either loaded or
retrieved from a cache. In either case, the reader lock is taken during this time, and release retrieved from a cache. In either case, the reader lock is taken during this time, and release
when the context ends. This class currently implements a simple least recently used cache, when the context ends. This class currently implements a simple least recently used cache,
which can delete a dataset from the cache to make room for a new oneo which can delete a dataset from the cache to make room for a new one.
This is the intended usage pattern: This is the intended usage pattern:
m = MatrixDataCacheManager() m = MatrixDataCacheManager(max_cached=..., timelimmit_s = ...)
with m.data_adaptor(location, app_config) as data_adaptor: with m.data_adaptor(location, app_config) as data_adaptor:
# use the data_adaptor for some operation # use the data_adaptor for some operation
""" """
# The number of datasets to cache. When MAX_CACHED is reached, the least recently used # FIXME: If the number of active datasets exceeds the max_cached, then each request could
# cache is replaced with the newly requested one.
# TODO: This is very simple. This can be improved by taking into account how much space is actually
# taken by each dataset, instead of arbitrarily picking a max datasets to cache.
# Also, this should be controlled by a configuration parameter.
MAX_CACHED = 5
@staticmethod
def set_max_datasets(max_cached):
MatrixDataCacheManager.MAX_CACHED = max_cached
# FIXME: If the number of active datasets exceeds the MAX_CACHED, then each request could
# lead to a dataset being deleted and a new only being opened: the cache will get thrashed. # lead to a dataset being deleted and a new only being opened: the cache will get thrashed.
# In this case, we may need to send back a 503 (Server Unavailable), or some other error message. # In this case, we may need to send back a 503 (Server Unavailable), or some other error message.
# FIXME: If the actual dataset is changed. E.g. a new set of datafiles replaces an existing set, # NOTE: If the actual dataset is changed. E.g. a new set of datafiles replaces an existing set,
# then the cache will not react to this. Ideally this would invalidate the cache. One solution is # then the cache will not react to this, however once the cache time limit is reached, the dataset
# to keep a small metadata file associated with each dataset, which contains versioning information. # will automatically be refreshed.
# When the dataset is accessed, the current version can be compared with the cached version, and if
# there is a mismatch, then the cache can be refreshed.
def __init__(self): def __init__(self, max_cached, timelimit_s=None):
# key is location, value is tuple of (MatrixDataCacheItem, last_accessed) # key is location, value is a MatrixDataCacheInfo
self.datasets = {} self.datasets = {}
# lock to protect the datasets
self.lock = threading.Lock() self.lock = threading.Lock()
# The number of datasets to cache. When max_cached is reached, the least recently used
# cache is replaced with the newly requested one.
# TODO: This is very simple. This can be improved by taking into account how much space is actually
# taken by each dataset, instead of arbitrarily picking a max datasets to cache.
self.max_cached = max_cached
# items are automatically removed from the cache once this time limit is reached
self.timelimit_s = timelimit_s
@contextmanager @contextmanager
def data_adaptor(self, location, app_config): def data_adaptor(self, location, app_config):
# create a loader for to this location if it does not already exist # create a loader for to this location if it does not already exist
delete_adaptor = None delete_adaptor = None
data_adaptor = None data_adaptor = None
cache_item = None
with self.lock: with self.lock:
value = self.datasets.get(location) self.evict_old_datasets()
if value is not None: info = self.datasets.get(location)
cache_item = value[0] if info is not None:
last_accessed = time.time() info.last_access = time.time()
self.datasets[location] = (cache_item, last_accessed) info.num_access += 1
data_adaptor = cache_item.acquire_existing() self.datasets[location] = info
data_adaptor = info.cache_item.acquire_existing()
cache_item = info.cache_item
if data_adaptor is None: if data_adaptor is None:
while True: while True:
# find the last access times for each loader if len(self.datasets) < self.max_cached:
if len(self.datasets) < self.MAX_CACHED:
break break
items = list(self.datasets.items()) items = list(self.datasets.items())
sorted(items, key=lambda x: x[1][1]) items = sorted(items, key=lambda x: x[1].last_access)
# close the least recently used loader # close the least recently used loader
oldest = items[0] oldest = items[0]
oldest_cache = oldest[1][0] oldest_cache = oldest[1].cache_item
oldest_key = oldest[0] oldest_key = oldest[0]
del self.datasets[oldest_key] del self.datasets[oldest_key]
delete_adaptor = oldest_cache delete_adaptor = oldest_cache
last_accessed = time.time()
loader = MatrixDataLoader(location, app_config=app_config) loader = MatrixDataLoader(location, app_config=app_config)
cache_item = MatrixDataCacheItem(loader) cache_item = MatrixDataCacheItem(loader)
self.datasets[location] = (cache_item, last_accessed) item = MatrixDataCacheInfo(cache_item, time.time())
self.datasets[location] = item
try: try:
assert(cache_item)
if delete_adaptor: if delete_adaptor:
delete_adaptor.delete() delete_adaptor.delete()
if data_adaptor is None: if data_adaptor is None:
data_adaptor = cache_item.acquire_and_open(app_config) data_adaptor = cache_item.acquire_and_open(app_config)
yield data_adaptor yield data_adaptor
finally: except DatasetAccessError:
cache_item.release() cache_item.release()
with self.lock:
del self.datasets[location]
cache_item.delete()
cache_item = None
raise
finally:
if cache_item:
cache_item.release()
def evict_old_datasets(self):
# must be called with the lock held
if self.timelimit_s is None:
return
now = time.time()
to_del = []
for key, info in self.datasets.items():
if (now - info.last_access) > self.timelimit_s:
# remove the data_cache when if it has been in the cache too long
to_del.append((key, info))
for key, info in to_del:
# try and get the write_lock for the dataset.
# if this returns false, it means the dataset is being used, and should
# not be removed.
if info.cache_item.attempt_delete():
del self.datasets[key]
class MatrixDataType(Enum): class MatrixDataType(Enum):
+10
View File
@@ -100,6 +100,16 @@ class RWLock(object):
self.d_lock.acquire() self.d_lock.acquire()
self.w_lock.acquire() self.w_lock.acquire()
def w_acquire_non_blocking(self):
# if d_lock and w_lock can be acquired without blocking, acquire and return True,
# else immediately return False.
if self.d_lock.acquire(blocking=False):
if self.w_lock.acquire(blocking=False):
return True
else:
self.d_lock.release()
return False
def w_release(self): def w_release(self):
self.w_lock.release() self.w_lock.release()
self.d_lock.release() self.d_lock.release()
+2
View File
@@ -250,6 +250,8 @@ class CxgAdaptor(DataAdaptor):
with ServerTiming.time(f"layout.lsuri"): with ServerTiming.time(f"layout.lsuri"):
pemb = self.get_path("emb") pemb = self.get_path("emb")
embeddings = [os.path.basename(p) for (p, t) in self.lsuri(pemb) if t == "array"] embeddings = [os.path.basename(p) for (p, t) in self.lsuri(pemb) if t == "array"]
if len(embeddings) == 0:
raise DatasetAccessError("cxg matrix missing embeddings")
return embeddings return embeddings
@staticmethod @staticmethod
+4 -6
View File
@@ -23,7 +23,6 @@ sys.path.append(SERVERDIR)
try: try:
from server.common.app_config import AppConfig from server.common.app_config import AppConfig
from server.app.app import Server from server.app.app import Server
from server.data_common.matrix_loader import MatrixDataCacheManager
from server.common.data_locator import DataLocator from server.common.data_locator import DataLocator
except Exception: except Exception:
logging.critical("Exception importing server modules", exc_info=True) logging.critical("Exception importing server modules", exc_info=True)
@@ -31,8 +30,8 @@ except Exception:
class WSGIServer(Server): class WSGIServer(Server):
def __init__(self, matrix_data_cache_manager, annotations, app_config): def __init__(self, app_config):
super().__init__(matrix_data_cache_manager, annotations, app_config) super().__init__(app_config)
def _before_adding_routes(self, app_config): def _before_adding_routes(self, app_config):
csp = {"default-src": "'self' 'unsafe-inline' 'unsafe-eval'", "img-src": ["'self'", "data:"]} csp = {"default-src": "'self' 'unsafe-inline' 'unsafe-eval'", "img-src": ["'self'", "data:"]}
@@ -75,8 +74,7 @@ try:
multi_dataset__allowed_matrix_types=["cxg"], multi_dataset__allowed_matrix_types=["cxg"],
) )
matrix_data_cache_manager = MatrixDataCacheManager() app_config.complete_config(logging.info)
app_config.complete_config(matrix_data_cache_manager, logging.info)
if not app_config.server__flask_secret_key: if not app_config.server__flask_secret_key:
logging.critical( logging.critical(
@@ -86,7 +84,7 @@ try:
user_annotations = app_config.user_annotations user_annotations = app_config.user_annotations
server = WSGIServer(matrix_data_cache_manager, user_annotations, app_config) server = WSGIServer(app_config)
debug = False debug = False
application = server.app application = server.app
+124
View File
@@ -0,0 +1,124 @@
import unittest
from server.data_common.matrix_loader import MatrixDataCacheManager
from server.common.app_config import AppConfig
from server.common.errors import DatasetAccessError
import tempfile
import shutil
import os
import time
class MatrixCacheTest(unittest.TestCase):
def setup(self):
pass
def make_temporay_datasets(self, dirname, num):
source = "test/test_datasets/pbmc3k.cxg"
for i in range(num):
target = os.path.join(dirname, str(i) + ".cxg")
shutil.copytree(source, target)
def use_dataset(self, matrix_cache, dirname, app_config, dataset_index):
with matrix_cache.data_adaptor(os.path.join(dirname, str(dataset_index) + ".cxg"), app_config) as adaptor:
pass
return adaptor
def use_dataset_with_error(self, matrix_cache, dirname, app_config, dataset_index):
try:
with matrix_cache.data_adaptor(os.path.join(dirname, str(dataset_index) + ".cxg"), app_config):
raise DatasetAccessError("something bad happened")
except DatasetAccessError:
# the MatrixDataCacheManager rethrows the exception, so catch and ignore
pass
def get_datasets(self, matrix_cache, dirname):
datasets = matrix_cache.datasets
result = {}
for k, v in datasets.items():
# filter out the dirname and the .cxg from the name
newk = int(k[len(dirname) + 1 : -4])
result[newk] = v
return result
def check_datasets(self, matrix_cache, dirname, expected):
res = self.get_datasets(matrix_cache, dirname)
actual = res.keys()
self.assertSetEqual(set(actual), set(expected))
def test_basic(self):
with tempfile.TemporaryDirectory() as dirname:
self.make_temporay_datasets(dirname, 5)
app_config = AppConfig()
m = MatrixDataCacheManager(max_cached=3, timelimit_s=None)
# should have only dataset 0
self.use_dataset(m, dirname, app_config, 0)
self.check_datasets(m, dirname, [0])
# should have datasets 0, 1
self.use_dataset(m, dirname, app_config, 1)
self.check_datasets(m, dirname, [0, 1])
# should have datasets 0, 1, 2
self.use_dataset(m, dirname, app_config, 2)
self.check_datasets(m, dirname, [0, 1, 2])
# should have datasets 1, 2, 3
self.use_dataset(m, dirname, app_config, 3)
self.check_datasets(m, dirname, [1, 2, 3])
# use dataset 1, making is more recent than dataset 2
self.use_dataset(m, dirname, app_config, 1)
self.check_datasets(m, dirname, [1, 2, 3])
# use dataset 4, should have 1,3,4
self.use_dataset(m, dirname, app_config, 4)
self.check_datasets(m, dirname, [1, 3, 4])
# use dataset 4 a few more times, get the count to 3
self.use_dataset(m, dirname, app_config, 4)
self.use_dataset(m, dirname, app_config, 4)
datasets = self.get_datasets(m, dirname)
self.assertEqual(datasets[1].num_access, 2)
self.assertEqual(datasets[3].num_access, 1)
self.assertEqual(datasets[4].num_access, 3)
def test_timelimit(self):
with tempfile.TemporaryDirectory() as dirname:
self.make_temporay_datasets(dirname, 2)
app_config = AppConfig()
m = MatrixDataCacheManager(max_cached=3, timelimit_s=1)
adaptor = self.use_dataset(m, dirname, app_config, 0)
adaptor1 = self.use_dataset(m, dirname, app_config, 0)
self.assertTrue(adaptor is adaptor1)
# wait until the timelimit expires and check that there is a new adaptor
time.sleep(1.1)
adaptor2 = self.use_dataset(m, dirname, app_config, 0)
self.assertTrue(adaptor is not adaptor2)
self.check_datasets(m, dirname, [0])
# now load a different dataset and see if dataset 0 gets evicted
time.sleep(1.1)
self.use_dataset(m, dirname, app_config, 1)
self.check_datasets(m, dirname, [1])
def test_access_error(self):
with tempfile.TemporaryDirectory() as dirname:
self.make_temporay_datasets(dirname, 1)
app_config = AppConfig()
m = MatrixDataCacheManager(max_cached=3, timelimit_s=1)
# use the 0 datasets
self.use_dataset(m, dirname, app_config, 0)
self.check_datasets(m, dirname, [0])
# use the 0 datasets, but this time a DatasetAccessError is raised.
# verify that dataset is removed from the cache.
self.use_dataset_with_error(m, dirname, app_config, 0)
self.check_datasets(m, dirname, [])