Skip to content

Commit 80b707a

Browse files
Brian Balsamobcail
authored andcommitted
Memory cache off-by-one error (#396)
* Fixes an off by one in the handling of the in memory cache, which also caused it to blow up if cache size == 0 * Fixed InfoCache getter bypassing size restrictions of the RAM cache and actually remedied the issues with the RAM cache size == 0 case, as I thought I had in the previous commit. Added tests for these cases. * Minor effeciency improvement - only read from disk once in the case where none of the info is cached in RAM. Still does two disk reads in all cases where RAM cache size > 0 * per conversation with @bcail in #396 - Avoiding writing to disk on every get()
1 parent dff373b commit 80b707a

File tree

2 files changed

+73
-21
lines changed

2 files changed

+73
-21
lines changed

loris/img_info.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,7 @@ def get(self, request):
372372
info_and_lastmod = (info, lastmod)
373373
logger.debug('Info for %s read from file system', request)
374374
# into mem:
375-
self._dict[request.url] = info_and_lastmod
376-
375+
self.__setitem__(request, info, _to_fs=False)
377376
return info_and_lastmod
378377

379378
def has_key(self, request):
@@ -389,31 +388,37 @@ def __getitem__(self, request):
389388
else:
390389
return info_lastmod
391390

392-
def __setitem__(self, request, info):
393-
# to fs
394-
logger.debug('request passed to __setitem__: %s', request)
391+
def __setitem__(self, request, info, _to_fs=True):
395392
info_fp = self._get_info_fp(request)
396-
dp = os.path.dirname(info_fp)
397-
mkdir_p(dp)
398-
logger.debug('Created %s', dp)
393+
if _to_fs:
394+
# to fs
395+
logger.debug('request passed to __setitem__: %s', request)
396+
dp = os.path.dirname(info_fp)
397+
mkdir_p(dp)
398+
logger.debug('Created %s', dp)
399399

400-
with open(info_fp, 'w') as f:
401-
f.write(info.to_full_info_json())
402-
logger.debug('Created %s', info_fp)
400+
with open(info_fp, 'w') as f:
401+
f.write(info.to_full_info_json())
402+
logger.debug('Created %s', info_fp)
403403

404404

405-
if info.color_profile_bytes:
406-
icc_fp = self._get_color_profile_fp(request)
407-
with open(icc_fp, 'wb') as f:
408-
f.write(info.color_profile_bytes)
409-
logger.debug('Created %s', icc_fp)
405+
if info.color_profile_bytes:
406+
icc_fp = self._get_color_profile_fp(request)
407+
with open(icc_fp, 'wb') as f:
408+
f.write(info.color_profile_bytes)
409+
logger.debug('Created %s', icc_fp)
410410

411411
# into mem
412-
lastmod = datetime.utcfromtimestamp(os.path.getmtime(info_fp))
413-
with self._lock:
414-
while len(self._dict) >= self.size:
415-
self._dict.popitem(last=False)
416-
self._dict[request.url] = (info,lastmod)
412+
# The info file cache on disk must already exist before
413+
# this is called - it's where the mtime gets drawn from.
414+
# aka, nothing outside of this class should be using
415+
# to_fs=False
416+
if self.size > 0:
417+
lastmod = datetime.utcfromtimestamp(os.path.getmtime(info_fp))
418+
with self._lock:
419+
self._dict[request.url] = (info,lastmod)
420+
while len(self._dict) > self.size:
421+
self._dict.popitem(last=False)
417422

418423
def __delitem__(self, request):
419424
with self._lock:

tests/img_info_t.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from os import path
88
import json
99
import tempfile
10+
from datetime import datetime
1011

1112
try:
1213
from urllib.parse import unquote
@@ -505,6 +506,29 @@ def test_info_goes_to_http_fs_cache(self):
505506
)
506507
self.assertTrue(path.exists(expected_path))
507508

509+
def test_just_ram_cache_update(self):
510+
# Cache size of one, so it's easy to manipulate
511+
cache = img_info.InfoCache(root=self.SRC_IMAGE_CACHE, size=1)
512+
self.app.info_cache = cache
513+
# First request
514+
request_uri = '/%s/%s' % (self.test_jp2_color_id,'info.json')
515+
resp = self.client.get(request_uri)
516+
expected_path = path.join(
517+
self.app.info_cache.http_root,
518+
unquote(self.test_jp2_color_id),
519+
'info.json'
520+
)
521+
fs_first_time = datetime.utcfromtimestamp(os.path.getmtime(expected_path))
522+
# Push this entry out of the RAM cache with another
523+
push_request_uri = '/%s/%s' % (self.test_jp2_gray_id,'info.json')
524+
resp = self.client.get(push_request_uri)
525+
# Request the first file again
526+
# It should now exist on disk, but not in RAM, so it shouldn't
527+
# have been rewritten by the second get.
528+
resp = self.client.get(request_uri)
529+
fs_second_time = datetime.utcfromtimestamp(os.path.getmtime(expected_path))
530+
self.assertTrue(fs_first_time == fs_second_time)
531+
508532
def test_can_delete_items_from_infocache(self):
509533
cache, req = self._cache_with_request()
510534
del cache[req]
@@ -513,6 +537,29 @@ def test_empty_cache_has_zero_size(self):
513537
cache = img_info.InfoCache(root=self.SRC_IMAGE_CACHE)
514538
assert len(cache) == 0
515539

540+
def test_cache_limit(self):
541+
cache = img_info.InfoCache(root=self.SRC_IMAGE_CACHE, size=2)
542+
self.app.info_cache = cache
543+
request_uris = [
544+
'/%s/%s' % (self.test_jp2_color_id,'info.json'),
545+
'/%s/%s' % (self.test_jpeg_id,'info.json'),
546+
'/%s/%s' % (self.test_png_id,'info.json'),
547+
'/%s/%s' % (self.test_jp2_gray_id,'info.json')
548+
]
549+
for x in request_uris:
550+
resp = self.client.get(x)
551+
552+
# Check we only cache two
553+
assert len(self.app.info_cache) == 2
554+
555+
def test_no_cache(self):
556+
cache = img_info.InfoCache(root=self.SRC_IMAGE_CACHE, size=0)
557+
self.app.info_cache = cache
558+
request_uri = '/%s/%s' % (self.test_jp2_color_id,'info.json')
559+
resp = self.client.get(request_uri)
560+
561+
assert len(self.app.info_cache) == 0
562+
516563
def test_deleting_cache_item_removes_color_profile_fp(self):
517564
# First assemble the cache
518565
cache, req = self._cache_with_request()

0 commit comments

Comments
 (0)