Repository navigation
BUG: MonteCarlo returns normally after KeyboardInterrupt #1151
Description
Activity
I reproduced the interrupt behavior while measuring the slow-test coverage at
cb6106a717207dd8fc2dfe1446d80ff75022f21b.Command:
$ pytest tests --runslow -m slow --cov=rocketpy --cov-append --cov-report=During
test_monte_carlo_simulate[True], sending Ctrl-C did not terminate pytest. The Monte Carlo case returned control to the test, was marked as failed, and pytest continued into the next slow case. A second Ctrl-C was handled the same way; I ultimately had to send SIGQUIT to terminate the run.This is consistent with both simulation paths catching
KeyboardInterruptand returning normally after cleanup. It also makes an apparently stalled Monte Carlo run difficult to stop from a test runner or terminal.Environment:
- macOS 26.5.2, arm64
- Python 3.12.6
- RocketPy 1.13.0
- NumPy 2.5.2
- SciPy 1.18.0
- pytest 9.1.1
The intended regression test should assert that cleanup and file persistence finish, then that the original
KeyboardInterruptescapessimulate()in both serial and parallel modes. I will keep the implementation behind #1054 so it does not duplicate or conflict with that process-handling work.Correcting the last paragraph of the report. I wrote that implementation should follow #1054 because that pull request changes the same control flow. It does change it, but it does not close this issue, so the two are independent after all.
What #1054 does with the interrupt
Both handlers are rewritten, and both record the interrupt instead of propagating it:
except KeyboardInterrupt: self._interrupted = True print("Keyboard interrupt received. Files saved.") self.__keep_the_inputs_that_did_not_finish(inputs_json)
except KeyboardInterrupt: _bring_the_fleet_down(started_processes, simulation_error_event) self._interrupted = True
The flag is read in one place,
__check_each_index_was_recorded_once, where it suppresses the missing-simulations check:missing = ( [] if self._interrupted else sorted(set(range(self.number_of_simulations)) - set(inputs)) )
That is deliberate and correct for what #1054 is about: an interrupted run has gaps in its indices and must not be reported as a corrupt one. But
simulate()still runs to its end and returns, so the caller still cannot tell a partial run from a complete one. This issue survives #1054.Still reproducing on develop
At
4263fa95d7fe6f63d9593f01e4ff7a088369e195, serial mode, with__run_single_simulationraisingKeyboardInterrupton the third call where Ctrl-C would:Starting Monte Carlo analysis Iterations completed: 000001 ... Iterations completed: 000002 ... Keyboard interrupt received. Files saved. __terminate_simulation ran asked for : 10 simulations completed before Ctrl-C: 2 inputs, 2 outputs KeyboardInterrupt seen by caller: FalseThe two completed rows are intact in both files, which is the behavior worth keeping. What is missing is the last step:
simulate()returns as though all ten had run.What this changes about the fix
The proposed fix in the report stands, but the ordering note in it does not. Re-raising after cleanup is a few lines in each handler and does not require the parallel-run rewrite, so it can go in before or after #1054 rather than waiting on it. Whichever lands second is a small rebase.
A second defect in the same handler
The serial handler writes
inputs_jsonto the error file, but that name is bound inside the loop body. Ctrl-C during the firstkeep_simulating()call therefore reaches the handler with nothing bound. At the same commit:Starting Monte Carlo analysis Keyboard interrupt received. Files saved. RAISED UnboundLocalError: cannot access local variable 'inputs_json' where it is not associated with a valueSo interrupting early does raise, but it raises the wrong exception from inside the cleanup, after printing that the files were saved.
#1054 already fixes this by binding
inputs_jsonas the first statement in the loop, with a comment naming exactly this case. Whoever picks this issue up should take that ordering along with the re-raise rather than solving it twice.- linked a pull request that will close this issueBUG: re-raise KeyboardInterrupt after an interrupted Monte Carlo run #1177
on Sep 11, 2026
Describe the bug
MonteCarlo.simulate()returns normally after the user interrupts a run.In serial mode,
__run_in_serial()catchesKeyboardInterrupt, prints that files were saved, appends the current input record to the error file, and then returns. In parallel mode,__run_in_parallel()catchesKeyboardInterrupt, signals and joins the workers, but only re-raises non-keyboard exceptions.The public
simulate()method then calls__terminate_simulation()and returns as if the requested number of simulations completed. Callers cannot distinguish a partial run from a successful one without inspecting the output files.To reproduce
Start a serial or parallel Monte Carlo run and interrupt it with Ctrl-C. At
cb6106a717207dd8fc2dfe1446d80ff75022f21b, control returns normally after the cleanup message instead of propagatingKeyboardInterrupt.Expected behavior
RocketPy should preserve completed rows and stop or join active workers, then re-raise the original
KeyboardInterrupt. This lets command-line callers return the conventional interrupted status and lets Python callers handle a partial run explicitly.The saved files should remain usable with the documented
append=Truecontinuation path.Proposed fix
Keep the current cleanup and file-preservation behavior in both execution modes, but re-raise after cleanup. Add serial and parallel regression tests that verify:
__terminate_simulation()does not convert the interrupted run into success;KeyboardInterrupt.The relevant control flow is also being changed by #1054, so implementation should follow that pull request rather than introducing a conflicting parallel-run rewrite.
Environment
cb6106a717207dd8fc2dfe1446d80ff75022f21b