Prevention of reactor pause

Commit d1c0cbd63ab240ca0120434106b66dac91a03079 added support for disabling calls to reactor.pause() However, the commit message fails to explain why.

Why was this needed?

I have an extension, which executes some gcode. For this, it creates a status wrapper. The gcode that the extension executes is a gcode_shell_command, which calls reactor.pause() while waiting for the shell command to complete.

However, since the extension is executing a gcode_macro, the reactor is prohibiting pausing. When the support for disabling calls to reactor.pause() was added, how was the above pattern supposed to be changed?

Long story starts here: Additional devices require i2c_write_noack() by KevinOConnor · Pull Request #7067 · Klipper3d/klipper · GitHub

Short answer, there is a sections of the code where the reactor should not sleep because it is either produces bugs or generates a race condition.
In case of status wrappen, I think it is a race condition. Ref: Verify no pauses in `stats()` and `get_status()` callbacks by KevinOConnor · Pull Request #7094 · Klipper3d/klipper · GitHub

So, about your specific code, well, don’t pause/sleep here, that’s it.
get_status() should just return already available data.

The status itself is usually updated by the timer.

Hope that helps.

So, how is Klipper supposed to execute macros on klippy:connect? If an extension is setup to execute a gcode template on that event, it will trigger the exception.

You can’t execute G-code on klippy:connect. You have to wait for klippy:ready to run macros.

klippy:connect is typically where you would lookup various other parts of the printer, like the toolhead or virtual SDcard.

I am sorry. That was meant to be klipper:ready. However, Printer._connect() emits klipper:ready, which still cause the issue since the klipper:ready handlers are called with the pause counter incremented:

with self.reactor.assert_no_pause():
    for cb in self.event_handlers.get("klippy:ready", []):
         if self.state_message is not message_ready:
              return
         cb()

As mentioned above, the assert_no_pause() mechanism was added because we know that if some callbacks were to pause it could introduce subtle bugs that would be hard to diagnose.

The “ready” callback is an example of this - if one module were to pause then it is possible for a subset of modules to be informed that the printer is in a “ready” state, while another subset of modules has yet to be informed that the printer is “ready”, and then a new command could be executed while the software is “half in ready”. This “half way state” could cause incorrect and confusing behavior.

The typical solution is to create a new task from the ready callback - the creation of a task is itself non-blocking, and the new task is free to pause as it wishes. See klippy/extras/load_cell.py as an example:

    def _handle_ready(self):
        self.printer.get_reactor().register_callback(self._handle_do_ready)

-Kevin

1 Like