Skip to content

Cache libvirt domain state - #819

Merged
marmarek merged 11 commits into
QubesOS:mainfrom
ben-grande:cache-running
Sep 14, 2026
Merged

marmarek merged 11 commits into
QubesOS:mainfrom
ben-grande:cache-running

Conversation

@ben-grande

@ben-grande ben-grande commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

The state changes rarely, but querying it can take a considerable time, that is blocking, when looping through the state of multiple domains.

For: QubesOS/qubes-issues#10569
For: QubesOS/qubes-issues#9902


Didn't run openqa locally, only experiment some things:

  • start
  • shutdown
  • kill
  • pause
  • unpause
  • checking Domains Widget
  • restart qubesd
  • qubed-query -e dom0 admin.vm.CurrentState QUBE

TODO:

@ben-grande

Copy link
Copy Markdown
Contributor Author

PipelineRetryFailed

@ben-grande

Copy link
Copy Markdown
Contributor Author

Didn't run openqa locally, only experiment some things, start, shutdown, kill, checking Domains Widget.

@ben-grande

Copy link
Copy Markdown
Contributor Author

PipelineRetryFailed

@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 35.02304% with 141 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.67%. Comparing base (109bc0b) to head (4656652).
⚠️ Report is 30 commits behind head on main.

Files with missing lines Patch % Lines
qubes/vm/qubesvm.py 34.88% 140 Missing ⚠️
qubes/app.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #819      +/-   ##
==========================================
- Coverage   70.72%   70.67%   -0.05%     
==========================================
  Files          61       61              
  Lines       14315    14401      +86     
==========================================
+ Hits        10124    10178      +54     
- Misses       4191     4223      +32     
Flag Coverage Δ
unittests 70.67% <35.02%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ben-grande
ben-grande force-pushed the cache-running branch 2 times, most recently from 5eff83f to aa2e823 Compare June 1, 2026 21:26
@ben-grande

Copy link
Copy Markdown
Contributor Author

PipelineRetryFailed

1 similar comment
@ben-grande

Copy link
Copy Markdown
Contributor Author

PipelineRetryFailed

@ben-grande
ben-grande marked this pull request as draft June 2, 2026 07:55
@ben-grande

Copy link
Copy Markdown
Contributor Author

Marked as draft as there still is some caching issues for the _power_state.

@marmarek

marmarek commented Jun 2, 2026

Copy link
Copy Markdown
Member

If necessary (or simpler), it's IMO okay to use cache only for is_running() (that is used frequently), but still do active check for get_power_state(). It could be also a safety valve for cases when cache are not reliable (like during startup or shutdown).

@ben-grande

Copy link
Copy Markdown
Contributor Author

If necessary (or simpler), it's IMO okay to use cache only for is_running() (that is used frequently), but still do active check for get_power_state(). It could be also a safety valve for cases when cache are not reliable (like during startup or shutdown).

Experimenting with cache a bit more and it's getting better, but if I don't get it to a hundred percent, will not cache the power state.

@ben-grande

ben-grande commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

From my tests, it seems to be working. My nemesis, OpenQA, should try me. Do you know a subset of tests that are interesting before joining this PR in the full run?

Looking at this recent test:

  • system_tests_suspend (soft-failed)
  • system_tests_usbproxy
  • system_tests_devices
  • system_tests_gui_tools
  • system_tests_guivm_gui_interactive

Line to use after this PR is not draft anymore:

openQArun TEST=system_tests_suspend,system_tests_usbproxy,system_tests_devices,system_tests_gui_tools,system_tests_guivm_gui_interactive

@marmarek

marmarek commented Jun 2, 2026

Copy link
Copy Markdown
Member

I'd include also at least one of the guivm tests

@ben-grande
ben-grande force-pushed the cache-running branch 4 times, most recently from 9538d21 to 6011ca3 Compare June 2, 2026 21:30
@ben-grande

Copy link
Copy Markdown
Contributor Author

PipelineRetryFailed

@ben-grande

Copy link
Copy Markdown
Contributor Author

Last CI failed on Fedora 43 to due to:

======================================================================
ERROR: qubes.tests.api_admin/TC_00_VMs/test_150_pool_info
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib64/python3.14/contextlib.py", line 85, in inner
    return func(*args, **kwds)
  File "/home/gitlab-runner/builds/QubesOS/qubes-core-admin/qubes/tests/api_admin.py", line 76, in setUp
    super().setUp()
    ~~~~~~~~~~~~~^^
  File "/home/gitlab-runner/builds/QubesOS/qubes-core-admin/qubes/tests/__init__.py", line 516, in setUp
    self.loop = asyncio.get_event_loop()
                ~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/lib64/python3.14/asyncio/events.py", line 715, in get_event_loop
    raise RuntimeError('There is no current event loop in thread %r.'
                       % threading.current_thread().name)
RuntimeError: There is no current event loop in thread 'MainThread'.

So it finally reached this repo.

@ben-grande

Copy link
Copy Markdown
Contributor Author

PipelineRetryFailed

@ben-grande

Copy link
Copy Markdown
Contributor Author

openQArun TEST=system_tests_suspend,system_tests_usbproxy,system_tests_devices,system_tests_gui_tools,system_tests_guivm_gui_interactive

@ben-grande
ben-grande marked this pull request as ready for review July 28, 2026 14:19
@ben-grande
ben-grande force-pushed the cache-running branch 2 times, most recently from 01ed08c to d9da451 Compare July 29, 2026 09:53
@ben-grande

Copy link
Copy Markdown
Contributor Author

PipelineRetryFailed

@marmarek

Copy link
Copy Markdown
Member

Some tests are failing here

Comment thread qubes/vm/qubesvm.py Outdated
Comment on lines +1760 to +1761
if self.__waiter is None:
self.__waiter = asyncio.get_running_loop().create_future()
waiter = self.__waiter
if self._power_state_waiter is None:
self._power_state_waiter = asyncio.get_running_loop().create_future()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local variable (waiter) was important part. Using attribute directly does not guarantee you wait for the same operation you initiated here - it might be something else that happened later (like, somebody calling kill just after shutdown, or maybe start just after kill).

This is the commit that introduced the issue, but the same issue applies to later change_libvirt_state context manager.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local variable (waiter) was important part. Using attribute directly does not guarantee you wait for the same operation you initiated here - it might be something else that happened later (like, somebody calling kill just after shutdown, or maybe start just after kill).

Hm.. right. I think I will change for action specific waiters, so a waiter for shutdown, a wait for suspend etc.

A use case for attribute instead of local variable is https://github.com/QubesOS/qubes-core-admin/pull/876/changes#r3914042842, as I think an attribute might prevent the shutdown from being called twice when there is an awaitable before the libvirt shutdown.

I think the same thing that is being done for shutdown on the other PR, should be done to the other power state actions also, although I understand that the shutdown is more critical.

Comment thread qubes/vm/qubesvm.py Outdated
yield
finally:
if wait:
await asyncio.wait_for(self._lifecycle_waiter, timeout=timeout)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the same event object for different events feels risky. What happens for example if kill() is called while start() is running? It might return early for example when VIR_DOMAIN_EVENT_DEFINED or VIR_DOMAIN_EVENT_RESUMED is delivered.
This would be even more complicated in connection with #867 that relies more on waiting for an operation started by somebody else (like cancel_start() does). But shutdown() already has similar case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's wrong, will fix it.

Comment thread qubes/vm/qubesvm.py
await self.fire_event_async(
"domain-start-failed", reason=str(exc)
)
await self.notify_failed_startup(exc=exc)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will set self._power_state too early, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, seems too early.

Comment thread qubes/vm/qubesvm.py Outdated
finally:
if wait:
await asyncio.wait_for(
self._lifecycle_waiter[event], timeout=timeout

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said in #819 (comment), this should use waiter saved in a local variable, before yield. Otherwise you may hit None here if the event is delivered early (or in a very unlucky situation, even wait for another instance of that event, make it wait too long or even forever).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I understand what you mean by that, thanks for the explanation.

Since there is "resume", and there is a moment when resuming that
domain might still be suspended, or event suspending a domain on
purposed, allow clients to know the domain state.
Useful when debugging to know what's happening. I thought of logging the
pretty name of the detail, but that got really big and I think it's out
of scope from the Qubes OS project and in scope of the python-libvirt
package.
- Keep events related to domain state in the same place
- Shorter exception messages, as it's already prefixed with qube name
- Much easier to read with less body on the if statements
- Will help on a future commit to access private attributes without
  having to disable pylint checks
- The events are related to a domain, not to the larger app instance
There are no other logs emitted on some power state events, such as
shutdown. Seems reasonable to increase the log level so developers can
have at least an idea of what is happening.
These data only changes once when domain is running.
The state changes rarely, but querying it can take a considerable
blocking time, worse when looping through the state of multiple domains.

For: QubesOS/qubes-issues#10569
For: QubesOS/qubes-issues#9902
Intended to allow callbacks to be executed early instead of having to
wait for the next cycle.
On commit 29f3c9b, this repeated
handling was introduced because "libvirt_domain" called
"self._update_libvirt_domain()" if domain was not found by libvirt. This
is no longer the case and we can rely on cached property.
"Transient" state was used by clients to indicate an impermanent state,
while on the server, to indicate that Qrexec should be running but is
not. One of the issues is that this is not a libvirt state,

The state aren't always as transient (short duration) as we think it is,
clients were never never notified of state changes from
"Running/Paused/Suspended" to "Transient", which can happen on "qrexec"
feature change and on qube's qrexec agent stopping.

A replacement for "Transient" state that clients will be satisfied is
"Starting" and "Halting". I judged introducing these new states,
although not libvirt states, as a better replacement because they can be
dealt with events already received by the client
"domain-(pre-)?(start|shutdown)(-failed)?", and don't need to learn a
new argument for "domain-(resumed|unpaused)" to set the state from
"Paused/Suspended" to "Transient".

In case the old "Transient" meaning of qrexec not being ready, is needed
in the future, add it somewhere else, with another more insightful name,
and not as a power state.

Note that "Transient" is a temporary name and it will be replaced by
"Starting" on R5.0.

For: QubesOS/qubes-issues#10966
@marmarek

Copy link
Copy Markdown
Member

openQArun TEST=system_tests_suspend,system_tests_usbproxy,system_tests_devices,system_tests_gui_tools,system_tests_guivm_gui_interactive,system_tests_basic_vm_qrexec_gui

@marmarek

Copy link
Copy Markdown
Member

openQArun TEST=system_tests_suspend,system_tests_usbproxy,system_tests_devices,system_tests_gui_tools,system_tests_guivm_gui_interactive,system_tests_basic_vm_qrexec_gui,system_tests_dispvm

@marmarek
marmarek merged commit 747cff1 into QubesOS:main Sep 14, 2026
3 of 6 checks passed

This branch was previously deployed

1 inactive deployment
qa 46566524 Deployed Sep 13, 2026 by qubesos-qa[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants