Skip to content

✨ Add daemon version drift warning#7318

Merged
agoscinski merged 1 commit into
aiidateam:mainfrom
agoscinski:feat/verdi-status-warning
Apr 29, 2026
Merged

✨ Add daemon version drift warning#7318
agoscinski merged 1 commit into
aiidateam:mainfrom
agoscinski:feat/verdi-status-warning

Conversation

@agoscinski
Copy link
Copy Markdown
Collaborator

@agoscinski agoscinski commented Apr 14, 2026

Record installed package versions when the daemon starts or restarts in a file, and warn in verdi status and verdi daemon status if current versions differ from those the daemon was started with. This helps catch subtle bugs caused by upgrading aiida-core or plugins without restarting the daemon.

After playing with different possibilities I only used the information that is available in direct_url.json that follows the specification from https://packaging.python.org/en/latest/specifications/direct-url-data-structure/ so the approach is robust. In contrast pip freeze does for example retrieves more information by for example in case of a editable mode install checking the local directory for the git hash. This allows to also detect local changes in the editable installation but would introduce extra logic that tries to retrieve the HEAD hash from the directory. I don't think this feature is worth the complexity.

In the examples below you see that I added a fish-like compression of the directory ~/c/a/w/f/new-checkout I am not sure if this is the best way, but including the whole directory is quite verbose and I kind of like the way fish does it.

Note that there can be still a change in the aiida-core dependencies and this will not be tracked. To detect this it would require a comparison of the complete dependency tree which would make verdi status way too slow.

Note that by using ep.dist.version local changes in aiida.__version__ are not detected for an editable installation (only when newly pip installed). I think that also is consistent with the philosophy that we do not track local changes in editable mode.

Below the agent summary, its quite comprehensive I would say.


Description

This PR adds daemon package-state tracking and surfaces a warning in
both verdi status and verdi daemon status when the currently
installed AiiDA packages no longer match the package state the daemon
was started with.

The goal is to make daemon/environment drift visible before it turns
into confusing runtime behavior after:

  • upgrading aiida-core
  • upgrading or uninstalling an AiiDA plugin
  • switching editable checkouts

The warning compares the package snapshot recorded at daemon startup
against the current package snapshot.

What is tracked:

  • installed package version
  • VCS commit hash when it is present in package metadata
  • editable install source path for local editable installs

For editable installs, this PR intentionally compares normalized source
paths, not the checkout's current git HEAD. Editable checkouts can
change without a commit change, and package metadata for local
editables does not reliably expose VCS commit information.

Examples

Version or editable-path mismatch:

$ verdi status
✔ version:     AiiDA v2.8.0.post1
✔ config:      /path/to/.aiida
✔ profile:     presto-2
✔ storage:     SqliteDosStorage[/path/to/repository]: open,
✔ broker:      RabbitMQ ...
⏺ daemon:      Daemon is running with PID 13669
               The daemon is running with mismatching package state. Restart the daemon with `verdi daemon restart`.
               Changed packages: aiida-core (2.8.0.post0 @ ~/c/a/w/f/old-checkout -> 2.8.0.post0 @ ~/c/a/w/f/new-checkout)

Plugin removed after daemon start:

$ verdi status
✔ version:     AiiDA v2.8.0.post1
✔ config:      /path/to/.aiida
✔ profile:     presto-2
✔ storage:     SqliteDosStorage[/path/to/repository]: open,
✔ broker:      RabbitMQ ...
⏺ daemon:      Daemon is running with PID 13669
               The daemon is running with mismatching package state. Restart the daemon with `verdi daemon restart`.
               Removed packages: aiida-quantumespresso (4.12.1)

Plugin added after daemon start:

$ verdi status
...
⏺ daemon:      Daemon is running with PID 12345
               The daemon is running with mismatching package state. Restart the daemon with `verdi daemon restart`.
               Added packages: aiida-plugin (1.2.3)

The same mismatch is also surfaced through verdi daemon status:

$ verdi daemon status
Profile: presto-2
Daemon is running as PID 13669 since 2026-04-22 08:30:30
Active workers [1]:
  PID    MEM %    CPU %  started
-----  -------  -------  -------------------
13670     0.42        0  2026-04-22 08:30:30
Log file: /path/to/aiida-presto-2.log
The daemon is running with mismatching package state. Restart the daemon with `verdi daemon restart`.
Changed packages: aiida-core (2.8.0.post0 @ ~/c/a/w/f/old-checkout -> 2.8.0.post0 @ ~/c/a/w/f/new-checkout)
Use `verdi daemon [incr | decr] [num]` to increase / decrease the number of workers

Implementation Notes

  • add a daemon version-info file in the profile daemon filepaths
  • write the package snapshot on daemon start/restart
  • remove the snapshot on daemon stop
  • read VCS commit hashes from direct_url.json / distribution origin
    metadata when available
  • record normalized editable paths for local editable installs
  • validate the stored snapshot format before using it
  • share package-drift formatting between verdi status and
    verdi daemon status
  • keep verdi status output to a single daemon: line block

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 83.11111% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.15%. Comparing base (6625039) to head (300080b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/aiida/engine/daemon/client.py 84.50% 20 Missing ⚠️
src/aiida/cmdline/commands/cmd_status.py 56.25% 14 Missing ⚠️
src/aiida/cmdline/commands/cmd_daemon.py 66.67% 3 Missing ⚠️
src/aiida/cmdline/utils/daemon.py 98.19% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7318      +/-   ##
==========================================
+ Coverage   80.08%   80.15%   +0.07%     
==========================================
  Files         576      577       +1     
  Lines       45280    45475     +195     
==========================================
+ Hits        36259    36445     +186     
- Misses       9021     9030       +9     

☔ View full report in Codecov by Sentry.
📢 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.

@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from eb8eafe to c78afcb Compare April 17, 2026 14:40
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 17, 2026
Record installed package versions when the daemon starts
or restarts, and warn in  if current versions differ from those the
daemon was started with. This helps catch subtle bugs
caused by upgrading aiida-core or plugins without
restarting the daemon.

Apply pre-commit formatting fixes during the replay so the
commit passes the repository linting workflow on its own.
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 17, 2026
Read VCS commit identifiers from importlib distribution
metadata instead of calling git in the source checkout.

This keeps the daemon version snapshot based on installation
metadata and still distinguishes packages installed from
specific VCS commits. Editable local installs now fall back
to the package version alone, which matches the intended
scope of the feature.

Apply pre-commit formatting fixes during the replay so the
commit passes the repository linting workflow on its own.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from c78afcb to bd2cb3e Compare April 17, 2026 15:17
@GeigerJ2 GeigerJ2 self-requested a review April 22, 2026 04:44
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 22, 2026
Record the package snapshot used when the daemon starts and
show a `verdi status` warning when the current environment
no longer matches it. This covers VCS installs via package
metadata and editable installs via normalized source paths.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from bd2cb3e to bff9297 Compare April 22, 2026 09:35
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 22, 2026
Record the package snapshot used when the daemon starts and
show a `verdi status` warning when the current environment
no longer matches it. This covers VCS installs via package
metadata and editable installs via normalized source paths.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from bff9297 to c2908db Compare April 22, 2026 09:59
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 22, 2026
Record the package snapshot used when the daemon starts and
show a `verdi status` warning when the current environment
no longer matches it. This covers VCS installs via package
metadata and editable installs via normalized source paths.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from c2908db to c185fc7 Compare April 22, 2026 10:00
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 23, 2026
Record the package snapshot used when the daemon starts and
show a `verdi status` warning when the current environment
no longer matches it. This covers VCS installs via package
metadata and editable installs via normalized source paths.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from c185fc7 to ab0f6bf Compare April 23, 2026 06:09
@agoscinski agoscinski marked this pull request as ready for review April 23, 2026 06:09
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 23, 2026
Record the package snapshot used when the daemon starts and
show a `verdi status` warning when the current environment
no longer matches it. This covers VCS installs via package
metadata and editable installs via normalized source paths.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from ab0f6bf to 81d4cd8 Compare April 23, 2026 06:09
Copy link
Copy Markdown
Collaborator

@GeigerJ2 GeigerJ2 left a comment

Choose a reason for hiding this comment

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

Just adding my pending comments here for now, will go again to #7319.

Comment thread src/aiida/engine/daemon/client.py Outdated
Comment thread src/aiida/engine/daemon/client.py
Copy link
Copy Markdown
Collaborator

@GeigerJ2 GeigerJ2 left a comment

Choose a reason for hiding this comment

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

OK, posting my pending comments already, as I need to sign off for today. Will continue tomorrow.

Comment thread src/aiida/engine/daemon/client.py Outdated
Comment thread src/aiida/cmdline/utils/daemon.py Outdated
Comment thread src/aiida/engine/daemon/client.py Outdated
Comment thread src/aiida/engine/daemon/client.py Outdated
return self._config.filepaths(self.profile)['daemon']['pid']

@property
def daemon_version_file(self) -> str:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Naming: the key in filepaths is version_info, the file extension is .version, and this property is daemon_version_file. Three terms for the same thing. Pick one — I'd suggest package_snapshot everywhere, since this records package state, not "the AiiDA version".

Comment on lines +606 to +633
@staticmethod
def get_package_version_snapshot() -> PackageVersionSnapshot:
"""Return version information for installed packages that provide ``aiida.*`` entry points.

For packages installed from a git repository through a VCS URL, the
version string includes the recorded commit hash from the distribution
metadata. Editable installs from a local path record their normalized
source path.
"""
from aiida.plugins.entry_point import ENTRY_POINT_GROUP_PREFIX, eps

versions: PackageVersionSnapshot = {}
for ep in eps():
if ep.group.startswith(ENTRY_POINT_GROUP_PREFIX) and ep.dist:
if ep.dist.name not in versions:
version_info: PackageVersionInfo = {'version': ep.dist.version}
direct_url_data = _get_dist_direct_url_data(ep.dist)
commit = _get_dist_commit_hash(ep.dist, direct_url_data)
editable_path = _get_dist_editable_path(ep.dist, direct_url_data)

if commit:
version_info['version'] = f'{ep.dist.version}+{commit[:8]}'

if editable_path:
version_info['editable_path'] = editable_path

versions[ep.dist.name] = version_info
return versions
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two structural concerns:

  1. Misplaced as a method on DaemonClient. This @staticmethod does not touch any instance state — it inspects the local Python environment by walking eps() and reading direct_url.json from each dist. Nothing about it is daemon-y; the daemon is just one consumer that happens to persist the snapshot to a file. The mixed call sites prove the awkwardness: client.get_package_version_snapshot() in _write_version_file vs DaemonClient.get_package_version_snapshot() in cmd_status.py:180 / cmd_daemon.py:158. Best home is aiida.plugins.entry_point as a module-level function (e.g. get_aiida_package_versions()) — that module already owns eps(), ENTRY_POINT_GROUP_PREFIX, and the surrounding entry-point introspection. The same applies to _validate_package_version_snapshot (:636) and the _get_dist_* helpers (:66-149) — none of them touch DaemonClient state. The PackageVersionInfo TypedDict and PackageVersionSnapshot alias move there too, all still private. The only things that should stay on DaemonClient are the two instance methods that own the daemon-specific file path: _write_version_file and get_daemon_version_info — both just call the new module-level functions.

  2. Commit hash folded into the version string. version_info['version'] = f'{ep.dist.version}+{commit[:8]}' (line 625-628) packs two semantically different things into one string. For a VCS install we store {'version': '2.8.0.post0+abcdef12'} instead of {'version': '2.8.0.post0', 'commit': 'abcdef12'}. Few downsides:

    • Hard to read. format_package_state_change_lines does a string-equality compare on version, so commit-only drift shows up as Changed packages: aiida-core (2.8.0.post0+abc12345 -> 2.8.0.post0+def67890). The user has to mentally parse the + suffix to realise the version didn't actually move — only the commit did. (Same readability issue you get (at least I get... ^^) from staring at pip install git+...@<sha> URLs.) With a separate commit field the formatter could say Changed packages: aiida-core (commit abc12345 -> def67890).
    • Hijacks PEP 440 local-version syntax. PEP 440 allows +local, so it's not technically invalid, but a future reader sees 2.8.0.post0+abc12345 and can't tell whether abc12345 is a commit, a build number, a vendor tag, etc. A typed commit: str field is self-documenting.
    • Loses filterability. A future "warn only on actual version bumps, ignore commit-only drift" check would have to parse the + suffix back out. With separate fields it's just daemon['version'] != current['version'].

    Suggested shape:

    version_info: PackageVersionInfo = {'version': ep.dist.version}
    if commit:
        version_info['commit'] = commit[:8]

    plus a commit: NotRequired[str] field on PackageVersionInfo, and format_package_version_info becomes aware of it the same way it currently is of editable_path.

Comment thread src/aiida/engine/daemon/client.py Outdated
Comment thread src/aiida/cmdline/utils/daemon.py Outdated
Comment thread src/aiida/cmdline/commands/cmd_status.py
Comment thread src/aiida/cmdline/commands/cmd_daemon.py Outdated
Comment thread tests/cmdline/commands/test_status.py Outdated
Comment thread tests/cmdline/commands/test_status.py Outdated
Comment thread tests/cmdline/commands/test_daemon.py Outdated
Comment thread src/aiida/engine/daemon/client.py Outdated
Comment thread src/aiida/engine/daemon/client.py Outdated
Comment thread src/aiida/engine/daemon/client.py Outdated
Comment thread src/aiida/engine/daemon/client.py Outdated
@GeigerJ2
Copy link
Copy Markdown
Collaborator

GeigerJ2 commented Apr 29, 2026

Sorry for the very pedantic review here @agoscinski, that's the character I gave my review agent :D

All the comment I posted were flagged during the review. I already filtered / adapted where I think it makes sense. Your call if you agree, or if they are too nitpicky for the PR. But, why not post them, if I have them already :)

Talking about nitpicks, here are a few more of them:

  • Asymmetric snapshot validationsrc/aiida/engine/daemon/client.py:669-676 (daemon snapshot is validated; live snapshot is not). EDIT @agoscinski d9108c6
  • if editable_path: treats empty string as missingsrc/aiida/cmdline/utils/daemon.py:54-62 (prefer explicit is not None). EDIT @agoscinski: acc9c9f
  • Newline-strategy refactor in cmd_daemon.py — drop trailing \n from each element and use '\n'.join(lines), mirroring the cleaner pattern already in cmd_status.py:175-191. Inline at src/aiida/cmdline/commands/cmd_daemon.py:150-167. EDIT @agoscinski 91ea19b
  • File-extension / key namingsrc/aiida/manage/configuration/config.py:835 (overlaps with the daemon_version_file naming comment but is a separate anchor). EDIT @agoscinski c5c7e21
  • Strengthen ordering assertion in tests/engine/daemon/test_client.py:265-281 — once the snapshot-after-await fix lands, add a test that patches _await_condition to fail and asserts the snapshot file was not written. EDIT @agoscinski 2b03836 and 07ee16c

That's it. Will wait for the updates, then check again.

agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 29, 2026
Record the package snapshot used when the daemon starts and
show a `verdi status` warning when the current environment
no longer matches it. This covers VCS installs via package
metadata and editable installs via normalized source paths.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch 3 times, most recently from bc1847d to 7f74488 Compare April 29, 2026 17:53
@agoscinski agoscinski changed the title ✨ Add daemon version mismatch warning ✨ Add daemon version drift warning Apr 29, 2026
agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 29, 2026
Record a snapshot of installed package versions when the daemon
starts or restarts, and warn on `verdi status` / `verdi daemon
status` when the current environment no longer matches.

This covers version changes, added/removed plugins, editable
install path changes, and VCS installs via commit hash from
distribution metadata.

Implementation details:

- A JSON file (`.package_snapshot`) is written by `DaemonClient`
  on `start_daemon` (after `_await_condition`) and `restart_daemon`
  (gated on `response.status == 'ok'`), and cleaned up on
  `stop_daemon`. The file contains a `PackageVersionSnapshot`
  mapping package names to version and optional editable path.

- `get_package_version_snapshot()` walks all `aiida.*` entry points
  and reads `direct_url.json` metadata (PEP 610) to detect
  editable installs and VCS commit hashes.

- `get_daemon_package_drift_lines()` in `cmdline/utils/daemon.py`
  compares the daemon-recorded snapshot against the live one and
  returns categorized mismatch lines (Changed/Added/Removed).
  Both snapshots are validated before comparison.

- New unit tests in `tests/cmdline/utils/test_daemon.py` pin the
  formatter contract without CLI startup overhead. Integration
  tests in `test_status.py` and `test_daemon.py` verify the
  warning surfaces correctly.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from c5c7e21 to 2fe4fba Compare April 29, 2026 19:33
Record a snapshot of installed package versions when the daemon
starts or restarts, and warn on `verdi status` / `verdi daemon
status` when the current environment no longer matches.

This covers version changes, added/removed plugins, editable
install path changes, and VCS installs via commit hash from
distribution metadata.

Implementation details:

- A JSON file (`.package_snapshot`) is written by `DaemonClient`
  on `start_daemon` (after `_await_condition`) and `restart_daemon`
  (gated on `response.status == 'ok'`), and cleaned up on
  `stop_daemon`. The file contains a `PackageVersionSnapshot`
  mapping package names to version and optional editable path.

- `get_package_version_snapshot()` walks all `aiida.*` entry points
  and reads `direct_url.json` metadata (PEP 610) to detect
  editable installs and VCS commit hashes.

- `get_daemon_package_drift_lines()` in `cmdline/utils/daemon.py`
  compares the daemon-recorded snapshot against the live one and
  returns categorized mismatch lines (Changed/Added/Removed).
  Both snapshots are validated before comparison.

- New unit tests in `tests/cmdline/utils/test_daemon.py` pin the
  formatter contract without CLI startup overhead. Integration
  tests in `test_status.py` and `test_daemon.py` verify the
  warning surfaces correctly.
@agoscinski agoscinski force-pushed the feat/verdi-status-warning branch from 2fe4fba to 300080b Compare April 29, 2026 19:34
@agoscinski
Copy link
Copy Markdown
Collaborator Author

I should have applied all suggestions, even the nitpicky once.

That's it. Will wait for the updates, then check again.

I will merge it now if CI passes (besides flaky tests) because I will anyway not be around to adapt any further review feedback. Its easier for me to make another support branch if the PRs are merged into main (otherwise I have to consider errors that are due to merge conflicts).

@agoscinski
Copy link
Copy Markdown
Collaborator Author

I checket the CI. The errors are replacing crashed worker gw0 and tests fail that are not related to these changes. I am currently trying to fix these in #7349

@agoscinski agoscinski enabled auto-merge (squash) April 29, 2026 20:11
@agoscinski agoscinski disabled auto-merge April 29, 2026 20:12
@agoscinski agoscinski merged commit 0230784 into aiidateam:main Apr 29, 2026
15 of 18 checks passed
@agoscinski agoscinski deleted the feat/verdi-status-warning branch April 29, 2026 20:12
@GeigerJ2
Copy link
Copy Markdown
Collaborator

Just adding the review items I think are sensible, and that didn't get adressed here, for possible follow-up:

PackageVersionInfo / PackageVersionSnapshot in public __all__

These TypedDict types are exported in aiida.engine.__all__ and aiida.engine.daemon.__all__. They're internal data structures for daemon version tracking. Exporting them makes them public API with deprecation guarantees. They're only consumed by cmdline.utils.daemon (which already imports from TYPE_CHECKING). If plugins don't need these types, they should be removed from __all__.

get_package_version_snapshot is a @staticmethod

Makes mocking awkward (tests use staticmethod(lambda: ...)) and the call sites are split between client.get_package_version_snapshot() and DaemonClient.get_package_version_snapshot(). A @classmethod would be more natural.

Commit hash folded into the version string

version_info['version'] = f'{ep.dist.version}+{commit[:8]}' packs two semantically different things into one string. A separate commit field on PackageVersionInfo would be cleaner: easier to read in output, self-documenting, and allows future filtering (e.g. warn only on version bumps, ignore commit-only drift).

Helpers misplaced on DaemonClient

get_package_version_snapshot, _validate_package_version_snapshot, and the _get_dist_* helpers don't touch any instance state. They inspect the local Python environment via eps() and direct_url.json. Better home is aiida.plugins.entry_point as module-level functions, since that module already owns eps() and ENTRY_POINT_GROUP_PREFIX. Only _write_version_file and get_daemon_package_snapshot (which own the daemon-specific file path) should stay on DaemonClient.

Redundant path when only version changed

When only the version differs but the editable path is the same, the path is repeated on both sides:

Changed packages: aiida-core (2.7.0 @ ~/path/to/checkout -> 2.8.0.post0 @ ~/path/to/checkout)

Could show aiida-core (2.7.0 -> 2.8.0.post0 @ ~/path/to/checkout) instead, or omit the path when unchanged.

agoscinski added a commit to agoscinski/aiida-core that referenced this pull request Apr 30, 2026
Record a snapshot of installed package versions when the daemon
starts or restarts, and warn on `verdi status` / `verdi daemon
status` when the current environment no longer matches.

This covers version changes, added/removed plugins, editable
install path changes, and VCS installs via commit hash from
distribution metadata.

Implementation details:

- A JSON file (`.package_snapshot`) is written by `DaemonClient`
  on `start_daemon` (after `_await_condition`) and `restart_daemon`
  (gated on `response.status == 'ok'`), and cleaned up on
  `stop_daemon`. The file contains a `PackageVersionSnapshot`
  mapping package names to version and optional editable path.

- `get_package_version_snapshot()` walks all `aiida.*` entry points
  and reads `direct_url.json` metadata (PEP 610) to detect
  editable installs and VCS commit hashes.

- `get_daemon_package_drift_lines()` in `cmdline/utils/daemon.py`
  compares the daemon-recorded snapshot against the live one and
  returns categorized mismatch lines (Changed/Added/Removed).
  Both snapshots are validated before comparison.

- New unit tests in `tests/cmdline/utils/test_daemon.py` pin the
  formatter contract without CLI startup overhead. Integration
  tests in `test_status.py` and `test_daemon.py` verify the
  warning surfaces correctly.

Reviewed-by: Julian Geiger <julian.geiger@psi.ch>
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.

2 participants