server/tag-categories: fix default categories

- Don't cache default category in its entirety - cache only its name
- Purge cache on category name changes and default category changes
- Lock records for updates where applicable
This commit is contained in:
rr- 2016-08-27 12:39:59 +02:00
parent 06ab98fa70
commit ef0f74297f
5 changed files with 73 additions and 31 deletions

View file

@ -40,7 +40,8 @@ def get_tag_category(ctx, params):
@routes.put('/tag-category/(?P<category_name>[^/]+)/?') @routes.put('/tag-category/(?P<category_name>[^/]+)/?')
def update_tag_category(ctx, params): def update_tag_category(ctx, params):
category = tag_categories.get_category_by_name(params['category_name']) category = tag_categories.get_category_by_name(
params['category_name'], lock=True)
versions.verify_version(category, ctx) versions.verify_version(category, ctx)
versions.bump_version(category) versions.bump_version(category)
if ctx.has_param('name'): if ctx.has_param('name'):
@ -60,7 +61,8 @@ def update_tag_category(ctx, params):
@routes.delete('/tag-category/(?P<category_name>[^/]+)/?') @routes.delete('/tag-category/(?P<category_name>[^/]+)/?')
def delete_tag_category(ctx, params): def delete_tag_category(ctx, params):
category = tag_categories.get_category_by_name(params['category_name']) category = tag_categories.get_category_by_name(
params['category_name'], lock=True)
versions.verify_version(category, ctx) versions.verify_version(category, ctx)
auth.verify_privilege(ctx.user, 'tag_categories:delete') auth.verify_privilege(ctx.user, 'tag_categories:delete')
tag_categories.delete_category(category) tag_categories.delete_category(category)
@ -73,8 +75,10 @@ def delete_tag_category(ctx, params):
@routes.put('/tag-category/(?P<category_name>[^/]+)/default/?') @routes.put('/tag-category/(?P<category_name>[^/]+)/default/?')
def set_tag_category_as_default(ctx, params): def set_tag_category_as_default(ctx, params):
auth.verify_privilege(ctx.user, 'tag_categories:set_default') auth.verify_privilege(ctx.user, 'tag_categories:set_default')
category = tag_categories.get_category_by_name(params['category_name']) category = tag_categories.get_category_by_name(
params['category_name'], lock=True)
tag_categories.set_default_category(category) tag_categories.set_default_category(category)
ctx.session.flush()
snapshots.modify(category, ctx.user) snapshots.modify(category, ctx.user)
ctx.session.commit() ctx.session.commit()
tags.export_to_json() tags.export_to_json()

View file

@ -55,5 +55,10 @@ def get(key):
return _CACHE.hash[key].value return _CACHE.hash[key].value
def remove(key):
if has(key):
del _CACHE.hash[key]
def put(key, value): def put(key, value):
_CACHE.insert_item(LruCacheItem(key, value)) _CACHE.insert_item(LruCacheItem(key, value))

View file

@ -4,6 +4,9 @@ from szurubooru import config, db, errors
from szurubooru.func import util, cache from szurubooru.func import util, cache
DEFAULT_CATEGORY_NAME_CACHE_KEY = 'default-tag-category'
class TagCategoryNotFoundError(errors.NotFoundError): class TagCategoryNotFoundError(errors.NotFoundError):
pass pass
@ -69,6 +72,7 @@ def update_category_name(category, name):
raise InvalidTagCategoryNameError('Name is too long.') raise InvalidTagCategoryNameError('Name is too long.')
_verify_name_validity(name) _verify_name_validity(name)
category.name = name category.name = name
cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY)
def update_category_color(category, color): def update_category_color(category, color):
@ -82,15 +86,17 @@ def update_category_color(category, color):
category.color = color category.color = color
def try_get_category_by_name(name): def try_get_category_by_name(name, lock=False):
return db.session \ query = db.session \
.query(db.TagCategory) \ .query(db.TagCategory) \
.filter(sqlalchemy.func.lower(db.TagCategory.name) == name.lower()) \ .filter(sqlalchemy.func.lower(db.TagCategory.name) == name.lower())
.one_or_none() if lock:
query = query.with_lockmode('update')
return query.one_or_none()
def get_category_by_name(name): def get_category_by_name(name, lock=False):
category = try_get_category_by_name(name) category = try_get_category_by_name(name, lock)
if not category: if not category:
raise TagCategoryNotFoundError('Tag category %r not found.' % name) raise TagCategoryNotFoundError('Tag category %r not found.' % name)
return category return category
@ -104,38 +110,50 @@ def get_all_categories():
return db.session.query(db.TagCategory).all() return db.session.query(db.TagCategory).all()
def try_get_default_category(): def try_get_default_category(lock=False):
key = 'default-tag-category' query = db.session \
if cache.has(key):
return cache.get(key)
category = db.session \
.query(db.TagCategory) \ .query(db.TagCategory) \
.filter(db.TagCategory.default) \ .filter(db.TagCategory.default)
.first() if lock:
query = query.with_lockmode('update')
category = query.first()
# if for some reason (e.g. as a result of migration) there's no default # if for some reason (e.g. as a result of migration) there's no default
# category, get the first record available. # category, get the first record available.
if not category: if not category:
category = db.session \ query = db.session \
.query(db.TagCategory) \ .query(db.TagCategory) \
.order_by(db.TagCategory.tag_category_id.asc()) \ .order_by(db.TagCategory.tag_category_id.asc())
.first() if lock:
cache.put(key, category) query = query.with_lockmode('update')
category = query.first()
return category return category
def get_default_category(): def get_default_category(lock=False):
category = try_get_default_category() category = try_get_default_category(lock)
if not category: if not category:
raise TagCategoryNotFoundError('No tag category created yet.') raise TagCategoryNotFoundError('No tag category created yet.')
return category return category
def get_default_category_name():
if cache.has(DEFAULT_CATEGORY_NAME_CACHE_KEY):
return cache.get(DEFAULT_CATEGORY_NAME_CACHE_KEY)
default_category = try_get_default_category()
default_category_name = default_category.name if default_category else None
cache.put(DEFAULT_CATEGORY_NAME_CACHE_KEY, default_category_name)
return default_category_name
def set_default_category(category): def set_default_category(category):
assert category assert category
old_category = try_get_default_category() old_category = try_get_default_category(lock=True)
if old_category: if old_category:
db.session.refresh(old_category)
old_category.default = False old_category.default = False
db.session.refresh(category)
category.default = True category.default = True
cache.remove(DEFAULT_CATEGORY_NAME_CACHE_KEY)
def delete_category(category): def delete_category(category):

View file

@ -58,8 +58,7 @@ def _check_name_intersection(names1, names2, case_sensitive):
def sort_tags(tags): def sort_tags(tags):
default_category = tag_categories.try_get_default_category() default_category_name = tag_categories.get_default_category_name()
default_category_name = default_category.name if default_category else None
return sorted( return sorted(
tags, tags,
key=lambda tag: ( key=lambda tag: (
@ -170,7 +169,7 @@ def get_or_create_tags_by_names(names):
names = util.icase_unique(names) names = util.icase_unique(names)
existing_tags = get_tags_by_names(names) existing_tags = get_tags_by_names(names)
new_tags = [] new_tags = []
tag_category_name = tag_categories.get_default_category().name tag_category_name = tag_categories.get_default_category_name()
for name in names: for name in names:
found = False found = False
for existing_tag in existing_tags: for existing_tag in existing_tags:

View file

@ -181,16 +181,32 @@ def test_try_get_default_category_when_default(tag_category_factory):
assert actual_default_category != category1 assert actual_default_category != category1
def test_try_get_default_category_from_cache(tag_category_factory): def test_get_default_category_name(tag_category_factory):
category1 = tag_category_factory()
category2 = tag_category_factory(default=True)
db.session.add_all([category1, category2])
db.session.flush()
assert tag_categories.get_default_category_name() == category2.name
category2.default = False
db.session.flush()
cache.purge()
assert tag_categories.get_default_category_name() == category1.name
db.session.query(db.TagCategory).delete()
cache.purge()
assert tag_categories.get_default_category_name() is None
def test_get_default_category_name_caching(tag_category_factory):
category1 = tag_category_factory() category1 = tag_category_factory()
category2 = tag_category_factory() category2 = tag_category_factory()
db.session.add_all([category1, category2]) db.session.add_all([category1, category2])
db.session.flush() db.session.flush()
tag_categories.try_get_default_category() tag_categories.get_default_category_name()
db.session.query(db.TagCategory).delete() db.session.delete(category1)
assert tag_categories.try_get_default_category() == category1 db.session.flush()
assert tag_categories.get_default_category_name() == category1.name
cache.purge() cache.purge()
assert tag_categories.try_get_default_category() is None assert tag_categories.get_default_category_name() == category2.name
def test_get_default_category(): def test_get_default_category():