Skip to content

Commit

Permalink
CSCFAIRMETA-1080: Qvain write lock (#889)
Browse files Browse the repository at this point in the history
* Reload dataset from Metax when moving to editor from datasets table
* Reset Qvain store when entering datasets table
* Fix inconsistent onMouseLeave behavior in tooltipHoverOnSave
* Add noreply arg to CatalogRecordCache
* Return success bool instead of new value when updating cache
* Add add, gets, cas methods to Cache
* Simplify Cache method names
* Update ipdb to fix failing travis
* Remove readonly from Qvain.Projects store, use one from Qvain store instead
* Fix usage of readonly in projects
* Fix bug in displaying actor card that has no buttons
  • Loading branch information
tahme authored Sep 30, 2021
1 parent 2d20630 commit f6a6ddf
Show file tree
Hide file tree
Showing 39 changed files with 1,073 additions and 116 deletions.
3 changes: 3 additions & 0 deletions etsin_finder/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def add_restful_resources(app):
QvainDataset,
QvainDatasets,
QvainDatasetFiles,
QvainDatasetLock,
FileCharacteristics,
)

Expand All @@ -88,6 +89,7 @@ def add_restful_resources(app):
api.add_resource(QvainDatasets, '/api/qvain/datasets')
api.add_resource(QvainDataset, '/api/qvain/datasets/<id:cr_id>')
api.add_resource(QvainDatasetFiles, '/api/qvain/datasets/<id:cr_id>/files')
api.add_resource(QvainDatasetLock, '/api/qvain/datasets/<id:cr_id>/lock')

# Qvain API RPC endpoints
api.add_resource(QvainDatasetChangeCumulativeState, '/api/rpc/datasets/change_cumulative_state')
Expand Down Expand Up @@ -194,6 +196,7 @@ def create_app(testing=None):
app.mail = Mail(app)
app.cr_cache = CatalogRecordCache(app)
app.cr_permission_cache = CatalogRecordCache(app, ttl=60, prefix='cr_permission_')
app.cr_lock_cache = CatalogRecordCache(app, ttl=60, prefix='cr_lock_', noreply=False)
app.rems_cache = RemsCache(app)
app.url_map.converters['id'] = IdentifierConverter

Expand Down
2 changes: 0 additions & 2 deletions etsin_finder/auth/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

"""Functionalities related to authorization and what users are allowed to see."""

from flask import current_app

from etsin_finder.auth import authentication
from etsin_finder.services import cr_service
from etsin_finder.services.cr_service import (
Expand Down
185 changes: 158 additions & 27 deletions etsin_finder/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from etsin_finder.app_config import get_memcached_config
from etsin_finder.utils.utils import FlaskService
from etsin_finder.log import log


class BaseCache(FlaskService):
Expand All @@ -21,6 +22,7 @@ def __init__(self, app):
"""Init cache."""
super().__init__(app)

self.app = app
memcached_config = get_memcached_config(app)

if memcached_config:
Expand All @@ -34,7 +36,7 @@ def __init__(self, app):
elif not self.is_testing:
app.logger.error("Unable to initialize Cache due to missing config")

def do_update(self, key, value, ttl):
def do_add(self, key, value, ttl, noreply=None):
"""Update cache.
Update cache with new key and specific time-to-live.
Expand All @@ -43,19 +45,42 @@ def do_update(self, key, value, ttl):
key (str): The key to update.
value (str): The value to update the key with.
ttl (int): Number of seconds until the item is expired from the cache.
noreply (bool): Set True or False to override default noreply value for request.
Returns:
str: The value.
bool: True if successful, otherwise False
"""
success = False
try:
self.cache.set(key, value, expire=ttl)
success = bool(self.cache.add(key, value, expire=ttl, noreply=noreply))
except Exception as e:
from etsin_finder.log import log
self.app.logger.warning("Add to cache failed")
self.app.logger.warning(e)
return success

log.warning("Insert to cache failed")
log.warning(e)
return value
def do_update(self, key, value, ttl, noreply=None):
"""Update cache.
Update cache with new key and specific time-to-live.
Args:
key (str): The key to update.
value (str): The value to update the key with.
ttl (int): Number of seconds until the item is expired from the cache.
noreply (bool): Set True or False to override default noreply value for request.
Returns:
bool: True if successful, otherwise False
"""
success = False
try:
success = self.cache.set(key, value, expire=ttl, noreply=noreply)
except Exception as e:
self.app.logger.warning("Insert to cache failed")
self.app.logger.warning(e)
return success

def do_get(self, key):
"""Try to fetch entry from cache.
Expand All @@ -70,63 +95,112 @@ def do_get(self, key):
try:
return self.cache.get(key, None)
except Exception as e:
from etsin_finder.log import log

log.debug("Get from cache failed")
log.debug(e)
self.app.logger.debug("Get from cache failed")
self.app.logger.debug(e)
return None

def do_delete(self, key):
def do_delete(self, key, noreply=None):
"""Try to delete entry from cache.
Args:
key (str): The key to fetch.
noreply (bool): Set True or False to override default noreply value for request.
Returns:
bool: True if successful, otherwise False
"""
success = False
try:
success = bool(self.cache.delete(key, noreply=noreply))
except Exception as e:
self.app.logger.debug("Delete from cache failed")
self.app.logger.debug(e)
return success

def do_gets(self, key):
"""Try to fetch entry from cache for Check-And-Set.
Args:
key (str): The key to fetch.
Returns tuple:
str: The value for the key, or default if the key wasn’t found.
token: CAS token
"""
try:
return self.cache.delete(key, None)
return self.cache.gets(key)
except Exception as e:
from etsin_finder.log import log
self.app.logger.debug("Gets from cache failed")
self.app.logger.debug(e)
return None, None

log.debug("Delete from cache failed")
log.debug(e)
return None
def do_cas(self, key, value, token, ttl, noreply=None):
"""Update cache only if it has not changed after gets
Update cache with new key and specific time-to-live.
Args:
key (str): The key to update.
value (str): The value to update the key with.
token (str): Token from do_gets.
ttl (int): Number of seconds until the item is expired from the cache.
noreply (bool): Set True or False to override default noreply value for request.
Returns:
bool: True if successful, otherwise False
"""
success = False
try:
success = bool(
self.cache.cas(key, value, token, expire=ttl, noreply=noreply)
)
except Exception as e:
self.app.logger.warning("Insert to cache failed")
self.app.logger.warning(e)
return success


class CatalogRecordCache(BaseCache):
"""Cache that stores data related to a specific catalog record."""

def __init__(self, app, ttl=1200, prefix="cr_"):
def __init__(self, app, ttl=1200, noreply=None, prefix="cr_"):
"""Init cache.
Args:
app: App instance
ttl: How long to store value, in seconds
prefix: Cache key prefix, should be unique for each cache
noreply: Set noreply for all cache requests, None for defaults
"""
super().__init__(app)
self.CACHE_ITEM_TTL = ttl
self.CACHE_KEY_PREFIX = prefix
self.CACHE_NO_REPLY = noreply

def update_cache(self, cr_id, data):
def update(self, cr_id, data):
"""Update catalog record cache with catalog record json.
Args:
cr_id (str): Catalog record identifier.
data: Data to save.
Returns:
data: Updated data
success (bool)
"""
if cr_id and data:
return self.do_update(self._get_cache_key(cr_id), data, self.CACHE_ITEM_TTL)
return self.do_update(
self._get_cache_key(cr_id),
data,
self.CACHE_ITEM_TTL,
noreply=self.CACHE_NO_REPLY,
)
return data

def get_from_cache(self, cr_id):
def get(self, cr_id):
"""Get data from catalog record cache.
Args:
Expand All @@ -135,19 +209,76 @@ def get_from_cache(self, cr_id):
"""
return self.do_get(self._get_cache_key(cr_id))

def delete_from_cache(self, cr_id):
def delete(self, cr_id):
"""Delete data from catalog record cache.
Args:
cr_id (str): Catalog record identifier.
Returns:
data: Saved data
success (bool)
"""
return self.do_delete(self._get_cache_key(cr_id), noreply=self.CACHE_NO_REPLY)

def gets(self, cr_id):
"""Get value from cache.
Args:
cr_id (str): Catalog record identifier.
data: Data to save.
Returns:
data: Value from cache.
token: CAS token.
"""
if cr_id:
return self.do_gets(self._get_cache_key(cr_id))
return (None, None)

def add(self, cr_id, data):
"""Add value to cache.
Args:
cr_id (str): Catalog record identifier.
data: Data to save.
Returns:
success (bool)
"""
if cr_id:
return self.do_add(
self._get_cache_key(cr_id),
data,
self.CACHE_ITEM_TTL,
noreply=self.CACHE_NO_REPLY,
)
return False

def cas(self, cr_id, data, token):
"""Update catalog record cache with catalog record json.
Args:
cr_id (str): Catalog record identifier.
data: Data to save.
Returns:
success (bool)
"""
return self.do_delete(self._get_cache_key(cr_id))
return self.do_cas(
self._get_cache_key(cr_id),
data,
token,
self.CACHE_ITEM_TTL,
noreply=self.CACHE_NO_REPLY,
)

def _get_cache_key(self, cr_id):
if not cr_id:
log.warning("missing cr_id: {cr_id}")
return f"{self.CACHE_KEY_PREFIX}{cr_id}"


Expand All @@ -156,7 +287,7 @@ class RemsCache(BaseCache):

CACHE_ITEM_TTL = 300

def update_cache(self, cr_id, user_id, permission=False):
def update(self, cr_id, user_id, permission=False):
"""Update cache with user entitlement for a specific catalog record.
Args:
Expand All @@ -174,7 +305,7 @@ def update_cache(self, cr_id, user_id, permission=False):
)
return permission

def get_from_cache(self, cr_id, user_id):
def get(self, cr_id, user_id):
"""Get entitlement for a user related to a specific catalog record from cache.
Args:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@ beforeEach(() => {
})
})

const datasetsCalls = observable.array([])

jest.mock('axios')
axios.get = jest.fn((...args) => {
datasetsCalls.push(JSON.parse(JSON.stringify(args)))
return Promise.resolve({
data: datasets,
})
Expand All @@ -41,7 +38,6 @@ describe('Qvain datasets page', () => {
let wrapper

beforeEach(async () => {
datasetsCalls.clear()
stores.QvainDatasets.setDatasetsPerPage(5)
wrapper = mount(
<StoresProvider store={stores}>
Expand All @@ -54,8 +50,7 @@ describe('Qvain datasets page', () => {
</BrowserRouter>
</StoresProvider>
)
// wait until datasets have been fetched
await when(() => datasetsCalls.length > 0)
await when(() => stores.QvainDatasets.datasetGroupsOnPage.length > 0)
wrapper.update()

// show more versions
Expand Down Expand Up @@ -90,7 +85,6 @@ describe('Qvain dataset removal modal', () => {
document.body.appendChild(helper)
ReactModal.setAppElement(helper)

datasetsCalls.clear()
wrapper = mount(
<StoresProvider store={stores}>
<BrowserRouter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ const datasetWithChanges = {

describe('getEnterEditAction', () => {
describe.each([
['IDA', idaDataset, 'published-id', true],
['draft', draftDataset, 'draft-id', true],
['IDA', idaDataset, 'published-id', false],
['draft', draftDataset, 'draft-id', false],
['changed', datasetWithChanges, 'changes-id', false],
])('given %s dataset', (description, dataset, expectedIdentifier, expectCallEditDataset) => {
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('given required props', () => {
},
Env: {
getQvainUrl: jest.fn(),
Flags: { flagEnabled: () => false },
},
}

Expand Down
Loading

0 comments on commit f6a6ddf

Please sign in to comment.