|
9 | 9 | """Test engine utilities such as the exponential backoff mechanism."""
|
10 | 10 |
|
11 | 11 | import asyncio
|
| 12 | +import contextlib |
12 | 13 |
|
13 | 14 | import pytest
|
14 | 15 | from aiida import orm
|
15 | 16 | from aiida.engine import calcfunction, workfunction
|
16 | 17 | from aiida.engine.utils import (
|
17 | 18 | InterruptableFuture,
|
18 | 19 | exponential_backoff_retry,
|
| 20 | + get_process_state_change_timestamp, |
19 | 21 | instantiate_process,
|
20 | 22 | interruptable_task,
|
21 | 23 | is_process_function,
|
| 24 | + set_process_state_change_timestamp, |
22 | 25 | )
|
23 | 26 |
|
24 | 27 | ITERATION = 0
|
@@ -225,3 +228,52 @@ async def coro():
|
225 | 228 |
|
226 | 229 | result = await task_fut
|
227 | 230 | assert result == 'NOT ME!!!'
|
| 231 | + |
| 232 | + |
| 233 | +@pytest.mark.parametrize('with_transaction', (True, False)) |
| 234 | +@pytest.mark.parametrize('monkeypatch_process_state_change', (True, False)) |
| 235 | +def test_set_process_state_change_timestamp(manager, with_transaction, monkeypatch_process_state_change, monkeypatch): |
| 236 | + """Test :func:`aiida.engine.utils.set_process_state_change_timestamp`. |
| 237 | +
|
| 238 | + This function is known to except when the ``core.sqlite_dos`` storage plugin is used and multiple processes are run. |
| 239 | + The function is called each time a process changes state and since it is updating the same row in the settings table |
| 240 | + the limitation of SQLite to not allow concurrent writes to the same page causes an exception to be thrown because |
| 241 | + the database is locked. This exception is caught in ``set_process_state_change_timestamp`` and simply is ignored. |
| 242 | + This test makes sure that if this happens, any other state changes, e.g. an extra being set on a node, are not |
| 243 | + accidentally reverted, when the changes are performed in an explicit transaction or not. |
| 244 | + """ |
| 245 | + storage = manager.get_profile_storage() |
| 246 | + |
| 247 | + node = orm.CalculationNode().store() |
| 248 | + extra_key = 'some_key' |
| 249 | + extra_value = 'some value' |
| 250 | + |
| 251 | + # Initialize the process state change timestamp so it is possible to check whether it was changed or not at the |
| 252 | + # end of the test. |
| 253 | + set_process_state_change_timestamp(node) |
| 254 | + current_timestamp = get_process_state_change_timestamp() |
| 255 | + assert current_timestamp is not None |
| 256 | + |
| 257 | + if monkeypatch_process_state_change: |
| 258 | + |
| 259 | + def set_global_variable(*_, **__): |
| 260 | + from sqlalchemy.exc import OperationalError |
| 261 | + |
| 262 | + raise OperationalError('monkey failure', None, '', '') |
| 263 | + |
| 264 | + monkeypatch.setattr(storage, 'set_global_variable', set_global_variable) |
| 265 | + |
| 266 | + transaction_context = storage.transaction if with_transaction else contextlib.nullcontext |
| 267 | + |
| 268 | + with transaction_context(): |
| 269 | + node.base.extras.set(extra_key, extra_value) |
| 270 | + set_process_state_change_timestamp(node) |
| 271 | + |
| 272 | + # The node extra should always have been set, regardless if the process state change excepted |
| 273 | + assert node.base.extras.get(extra_key) == extra_value |
| 274 | + |
| 275 | + # The process state change should have changed if the storage plugin was not monkeypatched to fail |
| 276 | + if monkeypatch_process_state_change: |
| 277 | + assert get_process_state_change_timestamp() == current_timestamp |
| 278 | + else: |
| 279 | + assert get_process_state_change_timestamp() != current_timestamp |
0 commit comments