Skip to content

Commit

Permalink
Improve tile source locking.
Browse files Browse the repository at this point in the history
Before, if two different tile sources using the same cache were being
opened simultaneously, they would be opened sequentially due to the
locking semantics.  This also prevented opening the same source twice.

Now, when a tile source is not in cache, a separate per-key lock is
created for opening that source.  This prevents the same source from
being opened twice but allows different sources to be opened
concurrently.
  • Loading branch information
manthey committed Oct 28, 2021
1 parent b6c6d42 commit 18576d9
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Improvements
- Improve warnings on inefficient tiff files ([#668](../../pull/668))
- Add more options to setFrameQuad ([#669](../../pull/669), [#670](../../pull/670))
- Improved concurrency of opening multiple distinct tile sources ([#671](../../pull/671))

## Version 1.8.3

Expand Down
48 changes: 27 additions & 21 deletions large_image/cache_util/cache.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
###############################################################################
# Copyright Kitware Inc.
#
# Licensed under the Apache License, Version 2.0 ( the "License" );
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################

import functools
import threading

try:
import resource
Expand All @@ -27,6 +12,8 @@
_tileCache = None
_tileLock = None

_cacheLockKeyToken = '_cacheLock_key'


# If we have a resource module, ask to use as many file handles as the hard
# limit allows, then calculate how may tile sources we can have open based on
Expand Down Expand Up @@ -182,16 +169,35 @@ def __call__(cls, *args, **kwargs): # noqa - N805
key = cls.__name__ + ' ' + key
with cacheLock:
try:
return cache[key]
result = cache[key]
if (not isinstance(result, tuple) or len(result) != 2 or
result[0] != _cacheLockKeyToken):
return result
cacheLockForKey = result[1]
except KeyError:
# By passing and handling the cache miss outside of the
# exception, any exceptions while trying to populate the cache
# will not be reported in the cache exception context.
pass
instance = super().__call__(*args, **kwargs)
cache[key] = instance
cacheLockForKey = threading.Lock()
cache[key] = (_cacheLockKeyToken, cacheLockForKey)
with cacheLockForKey:
with cacheLock:
try:
result = cache[key]
if (not isinstance(result, tuple) or len(result) != 2 or
result[0] != _cacheLockKeyToken):
return result
except KeyError:
pass
try:
instance = super().__call__(*args, **kwargs)
except Exception:
with cacheLock:
del cache[key]
raise
instance._classkey = key

with cacheLock:
cache[key] = instance
return instance


Expand Down
19 changes: 18 additions & 1 deletion test/test_cache.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import concurrent.futures
import threading
import time

import cachetools
import pytest
Expand Down Expand Up @@ -129,10 +131,12 @@ class ExampleWithMetaclass(metaclass=LruCacheMetaclass):
cacheMaxSize = 4

def __init__(self, arg):
pass
if isinstance(arg, (int, float)):
time.sleep(arg)

@pytest.mark.singular
def testCachesInfo(self):
cachesClear()
large_image.cache_util.cache._tileCache = None
large_image.cache_util.cache._tileLock = None
assert cachesInfo()['test']['used'] == 0
Expand All @@ -149,8 +153,21 @@ def testCachesInfo(self):
# memcached shows an items record as well
assert 'items' in cachesInfo()['tileCache']

@pytest.mark.singular
def testCachesKeyLock(self):
cachesClear()
assert cachesInfo()['test']['used'] == 0
starttime = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
executor.map(self.ExampleWithMetaclass, [3, 3, 2])
endtime = time.time()
# This really should be close to 3
assert endtime - starttime < 6
assert cachesInfo()['test']['used'] == 2

@pytest.mark.singular
def testCachesClear(self):
cachesClear()
large_image.cache_util.cache._tileCache = None
large_image.cache_util.cache._tileLock = None
config.setConfig('cache_backend', 'python')
Expand Down

0 comments on commit 18576d9

Please sign in to comment.