niklasf / python-chess

A chess library for Python, with move generation and validation, PGN parsing and writing, Polyglot opening book reading, Gaviota tablebase probing, Syzygy tablebase probing, and UCI/XBoard engine communication
https://python-chess.readthedocs.io/en/latest/
GNU General Public License v3.0
2.45k stars 532 forks source link

AssertionError is thrown if async analysis is cancelled too soon #1116

Open mooskagh opened 4 weeks ago

mooskagh commented 4 weeks ago

I'm writing a website that listens to a pgn feed and runs an analysis of the current position in an asyncio task. When a new position comes, the analysis task is cancelled, and new task started.

If analysis is cancelled too soon (two updates come one after another), python-chess throws an AssertionError exception.

Traceback (most recent call last):
  File "/home/crem/dev/lczero-live/backend/analyzer.py", line 157, in _uci_worker_think
    with await self._engine.analysis(
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/crem/dev/lczero-live/.venv/lib/python3.12/site-packages/chess/engine.py", line 1774, in analysis
    return await self.communicate(UciAnalysisCommand)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/crem/dev/lczero-live/.venv/lib/python3.12/site-packages/chess/engine.py", line 997, in communicate
    self.next_command.set_finished()
  File "/home/crem/dev/lczero-live/.venv/lib/python3.12/site-packages/chess/engine.py", line 1269, in set_finished
    assert self.state in [CommandState.ACTIVE, CommandState.CANCELLING], self.state
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: CommandState.NEW
mooskagh commented 2 weeks ago

Having this awkward lock did help to work around it.
It's ugly because I cannot have overlapping async with, so I acquire/release manually and then release in finally: for the case engine.analysis throws an exception.

await self._uci_cancelation_lock.acquire()
with await self._engine.analysis(board=board) as analysis:
    self._uci_cancelation_lock.release()
    # do the stuff
finally:
    try:
        self._uci_cancelation_lock.release()
    except RuntimeError:
        pass

### somewhere else:
async with self._uci_cancelation_lock:
   task.cancel()