Skip to content

Commit 615e2c6

Browse files
authored
Merge branch 'aiidateam:main' into fix/scp-glob-escaping
2 parents 4d74294 + 2068a7d commit 615e2c6

30 files changed

Lines changed: 709 additions & 151 deletions

File tree

.docker/aiida-core-base/s6-assets/init/aiida-prepare.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ verdi config set warnings.development_version False
1818
# If the environment variable `SETUP_DEFAULT_AIIDA_PROFILE` is not set, set it to `true`.
1919
if [[ ${SETUP_DEFAULT_AIIDA_PROFILE:-true} == true ]] && ! verdi profile show ${AIIDA_PROFILE_NAME} &> /dev/null; then
2020

21-
# Create AiiDA profile.
22-
verdi presto \
21+
# Create AiiDA profile. Set AIIDA_NO_DAEMON_AUTOSTART because the
22+
# Docker container manages the daemon lifecycle separately via
23+
# s6-overlay (aiida-daemon-start runs after this script and
24+
# run-before-daemon-start).
25+
AIIDA_NO_DAEMON_AUTOSTART=1 verdi presto \
2326
--verbosity info \
2427
--profile-name "${AIIDA_PROFILE_NAME:-default}" \
2528
--email "${AIIDA_USER_EMAIL:-aiida@localhost}" \

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Behavior changes
6+
7+
**`verdi presto`: auto-starts the daemon** ([#7351](https://github.com/aiidateam/aiida-core/pull/7351))
8+
9+
`verdi presto` now starts the daemon automatically after creating the profile, when a broker is configured.
10+
Scripts that previously ran `verdi daemon start` after `verdi presto` continue to work; the daemon-start invocation is idempotent.
11+
312
## v2.8.0 - 2026-03-16
413

514
This release brings important improvements to the `BaseRestartWorkChain`, the engine, stashing, typing coverage, and dependency updates.

docs/source/internals/broker.rst

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ Message types
137137
- worker → broker
138138
- Negative acknowledgment. Broker requeues the task for redelivery.
139139
* - ``TASK_RESPONSE``
140-
- worker → broker → client
141-
- Return the result of a completed task. Broker forwards to original sender.
140+
- broker → client
141+
- Immediate acknowledgment that the task was persisted to the queue
142+
(matches RabbitMQ publisher-confirm semantics).
143+
The worker's eventual result is not forwarded back to the sender.
142144
* - ``RPC``
143145
- client → broker → recipient
144146
- Remote procedure call to a named recipient. Broker routes by subscriber ID.
@@ -173,6 +175,7 @@ The protocol maps AMQP concepts to ZMQ message types:
173175
174176
AMQP concept ZMQ broker equivalent
175177
──────────────────────── ──────────────────────────────────
178+
publisher confirm TASK_RESPONSE (immediate broker ack)
176179
basic.ack TASK_ACK
177180
basic.nack TASK_NACK
178181
consumer with prefetch TASK dispatch to available workers
@@ -209,24 +212,27 @@ Message flow: task submission
209212
│ task_send(task) │ │
210213
│──── TASK ──────────────────────────────────────────▶│
211214
│ │ push to PersistentQueue
215+
│◀──── TASK_RESPONSE (ack) ──────────────────────────│ immediate
212216
│ │ │
213217
│ │◀─── TASK ────────────│ dispatch
214218
│ │ │
215219
│ │ (process runs...) │
216220
│ │ │
217221
│ │──── TASK_ACK ───────▶│ ack: remove from queue
218-
│ │──── TASK_RESPONSE ──▶│
219-
│ │ │
220-
│◀────────────── TASK_RESPONSE (forwarded) ──────────│
221222
│ │ │
222223
223224
Key details:
224225

226+
- The broker sends an **immediate** ``TASK_RESPONSE`` back to the sender as soon as the task
227+
is persisted to the queue on disk. This matches RabbitMQ's publisher-confirm semantics:
228+
the caller's ``Future`` resolves without waiting for a worker to process the task.
229+
Without this, ``task_send`` would block until ``broker.task_timeout`` when no workers are connected
230+
(e.g. during ``verdi process repair``).
225231
- The broker persists the task to disk **before** dispatching it.
226232
If the broker crashes, tasks are recovered from disk on restart.
227233
- Workers delay the ACK until the task Future resolves (see :ref:`deferred ACK <internal_architecture:broker:deferred_ack>` below).
228234
If a worker dies before ACK'ing, the broker detects the disconnect (via ZMTP heartbeats + PING probing) and requeues the task.
229-
- If ``no_reply=True``, no TASK_RESPONSE is sent.
235+
- If ``no_reply=True``, no ``TASK_RESPONSE`` is sent (fire-and-forget, used by ``submit()``).
230236

231237

232238
Task dispatch strategy

docs/source/reference/command_line.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ Below is a list with all available subcommands.
319319
* Create a default user for the profile (email can be configured through the `--email` option)
320320
* Set up the localhost as a `Computer` and configure it
321321
* Set a number of configuration options with sensible defaults
322+
* Start the daemon (unless `--no-broker` is specified)
322323
323324
By default the command creates a profile that uses SQLite for the database. For the
324325
message broker, it automatically checks for RabbitMQ running on localhost. If found, it

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ classifiers = [
2222
'Programming Language :: Python :: 3.11',
2323
'Programming Language :: Python :: 3.12',
2424
'Programming Language :: Python :: 3.13',
25+
'Programming Language :: Python :: 3.14',
2526
'Topic :: Scientific/Engineering'
2627
]
2728
# NOTE: When updating versions of some packages, such as requests or paramiko,

src/aiida/brokers/rabbitmq/defaults.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
from __future__ import annotations
44

5+
import asyncio
6+
import logging
57
import os
68
import typing as t
9+
from contextlib import contextmanager, suppress
710

811
from aiida.common.extendeddicts import AttributeDict
912
from aiida.common.log import AIIDA_LOGGER
@@ -27,6 +30,49 @@
2730
)
2831

2932

33+
@contextmanager
34+
def _suppress_rabbitmq_probe_logging() -> t.Iterator[None]:
35+
"""Suppress expected RabbitMQ probe logs for missing brokers."""
36+
logger_names = ('aiormq.connection', 'aio_pika.robust_connection')
37+
levels = {name: logging.getLogger(name).level for name in logger_names}
38+
39+
try:
40+
for name in logger_names:
41+
logging.getLogger(name).setLevel(logging.CRITICAL + 1)
42+
yield
43+
finally:
44+
for name, level in levels.items():
45+
logging.getLogger(name).setLevel(level)
46+
47+
48+
def _probe_rabbitmq_connection(connection_params: dict[str, t.Any]) -> None:
49+
"""Validate RabbitMQ connection parameters in an isolated event loop."""
50+
from aio_pika.connection import make_url
51+
from aio_pika.robust_connection import RobustConnection
52+
53+
async def connect() -> None:
54+
connection = RobustConnection(
55+
make_url(
56+
host=connection_params['host'],
57+
port=connection_params['port'],
58+
login=connection_params['username'],
59+
password=connection_params['password'],
60+
virtualhost=connection_params['virtual_host'],
61+
ssl=connection_params['protocol'] == 'amqps',
62+
),
63+
loop=asyncio.get_running_loop(),
64+
)
65+
66+
try:
67+
await connection.connect()
68+
finally:
69+
with suppress(Exception):
70+
await connection.close()
71+
72+
with _suppress_rabbitmq_probe_logging():
73+
asyncio.run(connect())
74+
75+
3076
def detect_rabbitmq_config(
3177
protocol: str | None = None,
3278
username: str | None = None,
@@ -38,10 +84,8 @@ def detect_rabbitmq_config(
3884
"""Try to connect to a RabbitMQ server with the default connection parameters.
3985
4086
:raises ConnectionError: If the connection failed with the provided connection parameters
41-
:returns: The connection parameters if the RabbitMQ server was successfully connected to, or ``None`` otherwise.
87+
:returns: The connection parameters with keys prefixed with ``broker_``.
4288
"""
43-
from kiwipy.rmq.threadcomms import connect
44-
4589
connection_params = {
4690
'protocol': protocol or os.getenv('AIIDA_BROKER_PROTOCOL', BROKER_DEFAULTS['protocol']),
4791
'username': username or os.getenv('AIIDA_BROKER_USERNAME', BROKER_DEFAULTS['username']),
@@ -54,9 +98,10 @@ def detect_rabbitmq_config(
5498
LOGGER.info(f'Attempting to connect to RabbitMQ with parameters: {connection_params}')
5599

56100
try:
57-
connect(connection_params=connection_params)
58-
except ConnectionError:
59-
raise ConnectionError(f'Failed to connect with following connection parameters: {connection_params}')
101+
_probe_rabbitmq_connection(connection_params)
102+
except Exception as exception:
103+
msg = f'Failed to connect with following connection parameters: {connection_params}'
104+
raise ConnectionError(msg) from exception
60105

61106
# The profile configuration expects the keys of the broker config to be prefixed with ``broker_``.
62107
return {f'broker_{key}': value for key, value in connection_params.items()}

src/aiida/brokers/zmq/communicator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ def _on_loop() -> None:
205205
if threading.current_thread() is not self._loop_thread:
206206
self._loop_thread.join(timeout=LOOP_JOIN_TIMEOUT)
207207

208+
if self._loop_thread.is_alive():
209+
_LOGGER.warning('ZMQ loop thread did not join in time, terminating ZMQ context')
210+
# Force-terminate the ZMQ context to unblock the poll loop.
211+
# This causes recv_multipart to raise ZMQError, exiting the thread.
212+
if self._context is not None:
213+
self._context.term()
214+
self._context = None
215+
self._loop_thread.join(timeout=LOOP_JOIN_TIMEOUT)
216+
208217
self._loop = None
209218
self._loop_thread = None
210219

src/aiida/brokers/zmq/server.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,7 @@ def __init__(
8585
# rpc_subscribers: identifier -> client_identity (bytes)
8686
self._rpc_subscribers: dict[str, bytes] = {}
8787

88-
# Pending responses: correlation_id -> (client_identity, timestamp)
89-
self._pending_task_responses: dict[str, tuple[bytes, float]] = {}
88+
# Pending RPC responses: correlation_id -> (client_identity, timestamp)
9089
self._pending_rpc_responses: dict[str, tuple[bytes, float]] = {}
9190

9291
# Task-worker assignments: task_id -> worker_identity
@@ -283,6 +282,14 @@ def _handle_task(self, identity: bytes, msg: dict[str, Any]) -> None:
283282
"""Handle incoming task message.
284283
285284
Queue the task and try to dispatch to an available worker.
285+
286+
When the sender expects a reply (``no_reply=False``), an immediate
287+
acknowledgment response is sent back as soon as the task is persisted
288+
to disk. This mirrors RabbitMQ's publisher-confirm behaviour: the
289+
caller learns that the broker accepted the task without having to wait
290+
for a worker to process it. The worker's eventual result (if any) is
291+
*not* forwarded back to the original sender — callers like
292+
``continue_process`` only need the confirmation, not the outcome.
286293
"""
287294
task_id = msg['id']
288295
sender = msg.get('sender', '')
@@ -299,33 +306,27 @@ def _handle_task(self, identity: bytes, msg: dict[str, Any]) -> None:
299306
}
300307
self._task_queue.push(task_id, task_data)
301308

302-
# Track pending response if reply expected
309+
# Send an immediate acknowledgment to the sender so its Future
310+
# resolves without waiting for a worker (matches RabbitMQ semantics).
303311
if not no_reply:
304-
self._pending_task_responses[task_id] = (identity, time.time())
312+
response = {
313+
'type': MessageType.TASK_RESPONSE.value,
314+
'task_id': task_id,
315+
'result': True,
316+
}
317+
self._send_to_client(identity, response)
305318

306319
# Try to dispatch immediately
307320
self._dispatch_pending_tasks()
308321

309322
def _handle_task_response(self, identity: bytes, msg: dict[str, Any]) -> None:
310323
"""Handle task response from worker.
311324
312-
Route the response back to the original task sender.
325+
Since the broker already sends an immediate acknowledgment when a task
326+
is queued, the worker's result response is simply logged and discarded.
313327
"""
314-
task_id = msg.get('task_id')
315-
if not task_id:
316-
_LOGGER.warning('Task response missing task_id')
317-
return
318-
319-
# Find original sender
320-
pending = self._pending_task_responses.pop(task_id, None)
321-
if not pending:
322-
_LOGGER.warning('No pending response for task: %s', task_id)
323-
return
324-
325-
original_sender, _ = pending
326-
327-
# Forward response to original sender
328-
self._send_to_client(original_sender, msg)
328+
task_id = msg.get('task_id', '?')
329+
_LOGGER.debug('Received task response for %s (discarded — sender already acknowledged)', task_id)
329330

330331
def _handle_task_ack(self, identity: bytes, msg: dict[str, Any]) -> None:
331332
"""Handle task acknowledgment from worker."""
@@ -602,7 +603,6 @@ def get_status(self) -> dict[str, Any]:
602603
'task_subscribers': len(self._task_subscribers),
603604
'rpc_subscribers': len(self._rpc_subscribers),
604605
'available_workers': len(self._available_workers),
605-
'pending_task_responses': len(self._pending_task_responses),
606606
'pending_rpc_responses': len(self._pending_rpc_responses),
607607
}
608608

src/aiida/brokers/zmq/service.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,6 @@ def start(self) -> None:
9393
self._write_status(self._server.get_status())
9494

9595
def stop(self) -> None:
96-
if not self._running:
97-
return
98-
9996
self._running = False
10097

10198
if self._server:

src/aiida/cmdline/commands/cmd_presto.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from __future__ import annotations
1212

13+
import os
1314
import pathlib
1415
import re
1516
import typing as t
@@ -183,6 +184,7 @@ def verdi_presto(
183184
* Create a default user for the profile (email can be configured through the `--email` option)
184185
* Set up the localhost as a `Computer` and configure it
185186
* Set a number of configuration options with sensible defaults
187+
* Start the daemon (unless `--no-broker` is specified)
186188
187189
By default the command creates a profile that uses SQLite for the database. For the message broker, it automatically
188190
checks for RabbitMQ running on localhost. If found, it configures RabbitMQ as the broker. Otherwise, it falls back
@@ -289,3 +291,20 @@ def verdi_presto(
289291
computer.set_default_mpiprocs_per_machine(1)
290292

291293
echo.echo_success('Configured the localhost as a computer.')
294+
295+
# `AIIDA_NO_DAEMON_AUTOSTART` is an internal escape hatch for the Docker init script (see
296+
# `.docker/aiida-core-base/s6-assets/init/aiida-prepare.sh`); deliberately undocumented in `--help`.
297+
if broker_backend is not None and os.environ.get('AIIDA_NO_DAEMON_AUTOSTART') != '1':
298+
from aiida.common.exceptions import ConfigurationError
299+
from aiida.engine.daemon.client import DaemonException, get_daemon_client
300+
301+
echo.echo('Starting the daemon... ', nl=False)
302+
try:
303+
client = get_daemon_client(profile.name)
304+
client.start_daemon()
305+
except (DaemonException, ConfigurationError) as exception:
306+
echo.echo('FAILED', fg=echo.COLORS['error'], bold=True)
307+
echo.echo_warning(f'Failed to start the daemon: {exception}')
308+
echo.echo_report('You can start it manually with `verdi daemon start`.')
309+
else:
310+
echo.echo('OK', fg=echo.COLORS['success'], bold=True)

0 commit comments

Comments
 (0)