Skip to content

Commit

Permalink
Cache power state when caching is enabled
Browse files Browse the repository at this point in the history
Power state changes are signaled with events too, so it is possible to
cache it and update/invalidate cache with events.
Additionally, admin.vm.List returns a power state, so the cache can be
populated early. This in particular greatly improves qvm-ls performance -
eliminate admin.vm.CurrentState call at all.

QubesOS/qubes-issues#3293
  • Loading branch information
marmarek committed May 22, 2020
1 parent bfe1a3d commit 79c7392
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 4 deletions.
52 changes: 50 additions & 2 deletions qubesadmin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ def refresh_cache(self, force=False):
props = props.split(' ')
new_vm_list[vm_name] = dict(
[vm_prop.split('=', 1) for vm_prop in props])
# if cache not enabled, drop power state
if not self.app.cache_enabled:
try:
del new_vm_list[vm_name]['state']
except KeyError:
pass

self._vm_list = new_vm_list
for name, vm in list(self._vm_objects.items()):
Expand Down Expand Up @@ -103,9 +109,12 @@ def get_blind(self, item):
# done by 'item not in self' check above, unless blind_mode is
# enabled
klass = None
power_state = None
if self._vm_list and item in self._vm_list:
klass = self._vm_list[item]['class']
self._vm_objects[item] = cls(self.app, item, klass=klass)
power_state = self._vm_list[item].get('state')
self._vm_objects[item] = cls(self.app, item, klass=klass,
power_state=power_state)
return self._vm_objects[item]

def __contains__(self, item):
Expand Down Expand Up @@ -598,7 +607,7 @@ def _invalidate_cache(self, subject, event, name, **kwargs):
:param name: name of the property
:param kwargs: other arguments
:return: none
""" # pylint: disable=unused-argument
""" # pylint: disable=unused-argument
if subject is None:
subject = self

Expand All @@ -608,6 +617,45 @@ def _invalidate_cache(self, subject, event, name, **kwargs):
except KeyError:
pass

def _update_power_state_cache(self, subject, event, **kwargs):
""" Update cached VM power state.
This method is designed to be hooed as an event handler for:
- domain-pre-start
- domain-start
- domain-shutdown
- domain-paused
- domain-unpaused
This is done in :py:class:`qubesadmin.events.EventsDispatcher` class
directly, before calling other handlers.
:param subject: a VM object
:param event: name of the event
:param kwargs: other arguments
:return:
""" # pylint: disable=unused-argument,no-self-use

if not self.app.cache_enabled:
return

if event == 'domain-pre-start':
power_state = 'Transient'
elif event == 'domain-start':
power_state = 'Running'
elif event == 'domain-shutdown':
power_state = 'Halted'
elif event == 'domain-paused':
power_state = 'Paused'
elif event == 'domain-unpaused':
power_state = 'Running'
else:
# unknown power state change, drop cached power state
power_state = None

# pylint: disable=protected-access
subject._power_state_cache = power_state


class QubesLocal(QubesBase):
"""Application object communicating through local socket.
Expand Down
3 changes: 3 additions & 0 deletions qubesadmin/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ def handle(self, subject, event, **kwargs):
if event.startswith('property-set:') or \
event.startswith('property-reset:'):
self.app._invalidate_cache(subject, event, **kwargs)
elif event in ('domain-pre-start', 'domain-start', 'domain-shutdown',
'domain-paused', 'domain-unpaused'):
self.app._update_power_state_cache(subject, event, **kwargs)

handlers = [h_func for h_name, h_func_set in self.handlers.items()
for h_func in h_func_set
Expand Down
27 changes: 27 additions & 0 deletions qubesadmin/tests/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ def test_009_getitem_cache_class(self):
self.fail('VM not found in collection')
self.assertAllCalled()

def test_010_getitem_cache_power_state(self):
self.app.cache_enabled = True
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
b'0\x00test-vm class=AppVM state=Running\n'
try:
vm = self.app.domains['test-vm']
self.assertEqual(vm.name, 'test-vm')
self.assertEqual(vm.klass, 'AppVM')
self.assertEqual(vm.get_power_state(), 'Running')
except KeyError:
self.fail('VM not found in collection')
self.assertAllCalled()

def test_011_getitem_non_cache_power_state(self):
self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
b'0\x00test-vm class=AppVM state=Running\n'
self.app.expected_calls[('test-vm', 'admin.vm.CurrentState', None, None)] = \
b'0\x00power_state=Running mem=1024'
try:
vm = self.app.domains['test-vm']
self.assertEqual(vm.name, 'test-vm')
self.assertEqual(vm.klass, 'AppVM')
self.assertEqual(vm.get_power_state(), 'Running')
except KeyError:
self.fail('VM not found in collection')
self.assertAllCalled()


class TC_10_QubesBase(qubesadmin.tests.QubesTestCase):
def setUp(self):
Expand Down
10 changes: 8 additions & 2 deletions qubesadmin/vm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ class QubesVM(qubesadmin.base.PropertyHolder):

firewall = None

def __init__(self, app, name, klass=None):
def __init__(self, app, name, klass=None, power_state=None):
super(QubesVM, self).__init__(app, 'admin.vm.property.', name)
self._volumes = None
self._klass = klass
self._power_state_cache = power_state
self.log = logging.getLogger(name)
self.tags = qubesadmin.tags.Tags(self)
self.features = qubesadmin.features.Features(self)
Expand Down Expand Up @@ -181,8 +182,13 @@ def get_power_state(self):
'''

if self._power_state_cache is not None:
return self._power_state_cache
try:
return self._get_current_state()['power_state']
power_state = self._get_current_state()['power_state']
if self.app.cache_enabled:
self._power_state_cache = power_state
return power_state
except qubesadmin.exc.QubesDaemonNoResponseError:
return 'NA'

Expand Down

0 comments on commit 79c7392

Please sign in to comment.