Add simple properties caching

Reduce Admin API calls by caching returned values. The cache is not
enabled by default, because it could result in stale values being
returned. It can be enabled by setting 'cache_enabled' to True on
Qubes() object. This is safe in two cases:
 - the application don't care about changed values - like a short-lived
   process that retrieve values once (for example qvm-ls)
 - the application listen for events and invalidate cache when property
   is changed

For the second case, invalidating the cache on appropriate event
(property-set:*, property-reset:*) is done before calling other event
handlers. This is because the event may try to access the property value
(not necessary from the event arguments), so we need to be sure it will
see the new value.

Fixes QubesOS/qubes-issues#5415
This commit is contained in:
Marek Marczykowski-Górecki 2019-12-03 02:59:09 +01:00
parent 5315bbf8f9
commit 218d43a2e0
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
3 changed files with 79 additions and 10 deletions

View File

@ -153,6 +153,8 @@ class QubesBase(qubesadmin.base.PropertyHolder):
log = None
#: do not check for object (VM, label etc) existence before really needed
blind_mode = False
#: cache retrieved properties values
cache_enabled = False
def __init__(self):
super(QubesBase, self).__init__(self, 'admin.property.', 'dom0')
@ -576,6 +578,35 @@ class QubesBase(qubesadmin.base.PropertyHolder):
stdout, stderr = proc.communicate()
return proc, stdout, stderr
def _invalidate_cache(self, subject, event, name, **kwargs):
"""Invalidate cached value of a property.
This method is designed to be hooked as an event handler for:
- property-set:*
- property-del:*
This is done in :py:class:`qubesadmin.events.EventsDispatcher` class
directly, before calling other handlers.
It handles both VM and global properties.
Note: even if the new value is given in the event arguments, it is
ignored because it comes without type information.
:param subject: either VM object or None
:param event: name of the event
:param name: name of the property
:param kwargs: other arguments
:return: none
""" # pylint: disable=unused-argument
if subject is None:
subject = self
try:
# pylint: disable=protected-access
del subject._properties_cache[name]
except KeyError:
pass
class QubesLocal(QubesBase):
"""Application object communicating through local socket.

View File

@ -44,6 +44,7 @@ class PropertyHolder(object):
self._method_dest = method_dest
self._properties = None
self._properties_help = None
self._properties_cache = {}
def qubesd_call(self, dest, method, arg=None, payload=None,
payload_stream=None):
@ -141,16 +142,20 @@ class PropertyHolder(object):
'''
if item.startswith('_'):
raise AttributeError(item)
# cached value
if item in self._properties_cache:
return self._properties_cache[item][0]
# cached properties list
if self._properties is not None and item not in self._properties:
raise AttributeError(item)
property_str = self.qubesd_call(
self._method_dest,
self._method_prefix + 'Get',
item,
None)
(default, _value) = property_str.split(b' ', 1)
assert default.startswith(b'default=')
is_default_str = default.split(b'=')[1]
is_default = is_default_str.decode('ascii') == "True"
assert isinstance(is_default, bool)
is_default, value = self._deserialize_property(property_str)
if self.app.cache_enabled:
self._properties_cache[item] = (is_default, value)
return is_default
def property_get_default(self, item):
@ -192,6 +197,15 @@ class PropertyHolder(object):
def __getattr__(self, item):
if item.startswith('_'):
raise AttributeError(item)
# cached value
if item in self._properties_cache:
value = self._properties_cache[item][1]
if value is AttributeError:
raise AttributeError(item)
return value
# cached properties list
if self._properties is not None and item not in self._properties:
raise AttributeError(item)
try:
property_str = self.qubesd_call(
self._method_dest,
@ -200,8 +214,25 @@ class PropertyHolder(object):
None)
except qubesadmin.exc.QubesDaemonNoResponseError:
raise qubesadmin.exc.QubesPropertyAccessError(item)
(_default, prop_type, value) = property_str.split(b' ', 2)
return self._parse_type_value(prop_type, value)
is_default, value = self._deserialize_property(property_str)
if self.app.cache_enabled:
self._properties_cache[item] = (is_default, value)
if value is AttributeError:
raise AttributeError(item)
return value
def _deserialize_property(self, api_response):
"""
Deserialize property.Get response format
:param api_response: bytes, as retrieved from qubesd
:return: tuple(is_default, value)
"""
(default, prop_type, value) = api_response.split(b' ', 2)
assert default.startswith(b'default=')
is_default_str = default.split(b'=')[1]
is_default = is_default_str.decode('ascii') == "True"
value = self._parse_type_value(prop_type, value)
return is_default, value
def _parse_type_value(self, prop_type, value):
'''
@ -224,11 +255,11 @@ class PropertyHolder(object):
return str(value)
if prop_type == 'bool':
if value == '':
raise AttributeError
return AttributeError
return value == "True"
if prop_type == 'int':
if value == '':
raise AttributeError
return AttributeError
return int(value)
if prop_type == 'vm':
if value == '':

View File

@ -188,7 +188,8 @@ class EventsDispatcher(object):
return some_event_received
def handle(self, subject, event, **kwargs):
'''Call handlers for given event'''
"""Call handlers for given event"""
# pylint: disable=protected-access
if subject:
if event in ['property-set:name']:
self.app.domains.clear_cache()
@ -210,6 +211,12 @@ class EventsDispatcher(object):
.devices[devclass][ident]
except (KeyError, ValueError):
pass
# invalidate cache if needed; call it before other handlers
# as those may want to use cached value
if event.startswith('property-set:') or \
event.startswith('property-reset:'):
self.app._invalidate_cache(subject, event, **kwargs)
handlers = [h_func for h_name, h_func_set in self.handlers.items()
for h_func in h_func_set
if fnmatch.fnmatch(event, h_name)]