ortools.util.python.solve_interrupter
Python interrupter for solves.
1#!/usr/bin/env python3 2# Copyright 2010-2025 Google LLC 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Python interrupter for solves.""" 16 17from collections.abc import Callable, Iterator 18import contextlib 19from typing import Optional 20 21from absl import logging 22 23from ortools.util.python import pybind_solve_interrupter 24 25 26class CallbackError(Exception): 27 """Exception raised when an interrupter callback fails. 28 29 When using SolveInterrupter.interruption_callback(), this exception is raised 30 when exiting the context manager if the callback failed. The error in the 31 callback is the cause of this exception. 32 """ 33 34 35class SolveInterrupter: 36 """Interrupter used by solvers to know when they should interrupt the solve. 37 38 Once triggered with interrupt(), an interrupter can't be reset. It can be 39 triggered from any thread. 40 41 Thread-safety: APIs on this class are safe to call concurrently from multiple 42 threads. 43 44 Attributes: 45 pybind_interrupter: The pybind wrapper around PySolveInterrupter. 46 """ 47 48 def __init__(self) -> None: 49 self.pybind_interrupter = pybind_solve_interrupter.PySolveInterrupter() 50 51 def interrupt(self) -> None: 52 """Interrupts the solve as soon as possible. 53 54 Once requested the interruption can't be reset. The user should use a new 55 SolveInterrupter for later solves. 56 57 It is safe to call this function multiple times. Only the first call will 58 have visible effects; other calls will be ignored. 59 """ 60 self.pybind_interrupter.interrupt() 61 62 @property 63 def interrupted(self) -> bool: 64 """True if the solve interruption has been requested.""" 65 return self.pybind_interrupter.interrupted 66 67 def add_trigger_target(self, target: "SolveInterrupter") -> None: 68 """Triggers the target when this interrupter is triggered.""" 69 self.pybind_interrupter.add_trigger_target(target.pybind_interrupter) 70 71 def remove_trigger_target(self, target: "SolveInterrupter") -> None: 72 """Removes the target if not null and present, else do nothing.""" 73 self.pybind_interrupter.remove_trigger_target(target.pybind_interrupter) 74 75 @contextlib.contextmanager 76 def interruption_callback(self, callback: Callable[[], None]) -> Iterator[None]: 77 """Returns a context manager that (un)register the provided callback. 78 79 The callback is immediately called if the interrupter has already been 80 triggered or if it is triggered during the registration. This is typically 81 useful for a solver implementation so that it does not have to test 82 `interrupted` to do the same thing it does in the callback. Simply 83 registering the callback is enough. 84 85 The callback function can't make calls to interruption_callback(), and 86 interrupt(). This would result is a deadlock. Reading `interrupted` is fine 87 though. 88 89 Exceptions raised in the callback are raised on exit from the context 90 manager if no other error happens within the context. Else the exception is 91 logged. 92 93 Args: 94 callback: The callback. 95 96 Returns: 97 A context manager. 98 99 Raises: 100 CallbackError: When exiting the context manager if an exception was raised 101 in the callback. 102 """ 103 callback_error: Optional[Exception] = None 104 105 def protetected_callback(): 106 """Calls callback() storing any exception in callback_error.""" 107 nonlocal callback_error 108 try: 109 callback() 110 except Exception as e: # pylint: disable=broad-exception-caught 111 # It is fine to set callback_error without any threading protection as 112 # the SolveInterrupter guarantees it will only call it at most once. 113 callback_error = e 114 115 callback_id = self.pybind_interrupter.add_interruption_callback( 116 protetected_callback 117 ) 118 119 no_exception_in_context = False 120 try: 121 yield 122 no_exception_in_context = True 123 finally: 124 self.pybind_interrupter.remove_interruption_callback(callback_id) 125 # It is fine to access callback_error without threading protection after 126 # remove_interruption_callback() has returned as the SolveInterrupter 127 # guarantees any pending call is done and no future call can happen. 128 if callback_error is not None: 129 if no_exception_in_context: 130 raise CallbackError() from callback_error 131 # We don't want the error in the context to be masked by an error in the 132 # callback. We log it instead. 133 logging.error( 134 "An exception occurred in callback but is masked by another" 135 " exception: %s", 136 repr(callback_error), 137 )
27class CallbackError(Exception): 28 """Exception raised when an interrupter callback fails. 29 30 When using SolveInterrupter.interruption_callback(), this exception is raised 31 when exiting the context manager if the callback failed. The error in the 32 callback is the cause of this exception. 33 """
Exception raised when an interrupter callback fails.
When using SolveInterrupter.interruption_callback(), this exception is raised when exiting the context manager if the callback failed. The error in the callback is the cause of this exception.
36class SolveInterrupter: 37 """Interrupter used by solvers to know when they should interrupt the solve. 38 39 Once triggered with interrupt(), an interrupter can't be reset. It can be 40 triggered from any thread. 41 42 Thread-safety: APIs on this class are safe to call concurrently from multiple 43 threads. 44 45 Attributes: 46 pybind_interrupter: The pybind wrapper around PySolveInterrupter. 47 """ 48 49 def __init__(self) -> None: 50 self.pybind_interrupter = pybind_solve_interrupter.PySolveInterrupter() 51 52 def interrupt(self) -> None: 53 """Interrupts the solve as soon as possible. 54 55 Once requested the interruption can't be reset. The user should use a new 56 SolveInterrupter for later solves. 57 58 It is safe to call this function multiple times. Only the first call will 59 have visible effects; other calls will be ignored. 60 """ 61 self.pybind_interrupter.interrupt() 62 63 @property 64 def interrupted(self) -> bool: 65 """True if the solve interruption has been requested.""" 66 return self.pybind_interrupter.interrupted 67 68 def add_trigger_target(self, target: "SolveInterrupter") -> None: 69 """Triggers the target when this interrupter is triggered.""" 70 self.pybind_interrupter.add_trigger_target(target.pybind_interrupter) 71 72 def remove_trigger_target(self, target: "SolveInterrupter") -> None: 73 """Removes the target if not null and present, else do nothing.""" 74 self.pybind_interrupter.remove_trigger_target(target.pybind_interrupter) 75 76 @contextlib.contextmanager 77 def interruption_callback(self, callback: Callable[[], None]) -> Iterator[None]: 78 """Returns a context manager that (un)register the provided callback. 79 80 The callback is immediately called if the interrupter has already been 81 triggered or if it is triggered during the registration. This is typically 82 useful for a solver implementation so that it does not have to test 83 `interrupted` to do the same thing it does in the callback. Simply 84 registering the callback is enough. 85 86 The callback function can't make calls to interruption_callback(), and 87 interrupt(). This would result is a deadlock. Reading `interrupted` is fine 88 though. 89 90 Exceptions raised in the callback are raised on exit from the context 91 manager if no other error happens within the context. Else the exception is 92 logged. 93 94 Args: 95 callback: The callback. 96 97 Returns: 98 A context manager. 99 100 Raises: 101 CallbackError: When exiting the context manager if an exception was raised 102 in the callback. 103 """ 104 callback_error: Optional[Exception] = None 105 106 def protetected_callback(): 107 """Calls callback() storing any exception in callback_error.""" 108 nonlocal callback_error 109 try: 110 callback() 111 except Exception as e: # pylint: disable=broad-exception-caught 112 # It is fine to set callback_error without any threading protection as 113 # the SolveInterrupter guarantees it will only call it at most once. 114 callback_error = e 115 116 callback_id = self.pybind_interrupter.add_interruption_callback( 117 protetected_callback 118 ) 119 120 no_exception_in_context = False 121 try: 122 yield 123 no_exception_in_context = True 124 finally: 125 self.pybind_interrupter.remove_interruption_callback(callback_id) 126 # It is fine to access callback_error without threading protection after 127 # remove_interruption_callback() has returned as the SolveInterrupter 128 # guarantees any pending call is done and no future call can happen. 129 if callback_error is not None: 130 if no_exception_in_context: 131 raise CallbackError() from callback_error 132 # We don't want the error in the context to be masked by an error in the 133 # callback. We log it instead. 134 logging.error( 135 "An exception occurred in callback but is masked by another" 136 " exception: %s", 137 repr(callback_error), 138 )
Interrupter used by solvers to know when they should interrupt the solve.
Once triggered with interrupt(), an interrupter can't be reset. It can be triggered from any thread.
Thread-safety: APIs on this class are safe to call concurrently from multiple threads.
Attributes:
- pybind_interrupter: The pybind wrapper around PySolveInterrupter.
52 def interrupt(self) -> None: 53 """Interrupts the solve as soon as possible. 54 55 Once requested the interruption can't be reset. The user should use a new 56 SolveInterrupter for later solves. 57 58 It is safe to call this function multiple times. Only the first call will 59 have visible effects; other calls will be ignored. 60 """ 61 self.pybind_interrupter.interrupt()
Interrupts the solve as soon as possible.
Once requested the interruption can't be reset. The user should use a new SolveInterrupter for later solves.
It is safe to call this function multiple times. Only the first call will have visible effects; other calls will be ignored.
63 @property 64 def interrupted(self) -> bool: 65 """True if the solve interruption has been requested.""" 66 return self.pybind_interrupter.interrupted
True if the solve interruption has been requested.
68 def add_trigger_target(self, target: "SolveInterrupter") -> None: 69 """Triggers the target when this interrupter is triggered.""" 70 self.pybind_interrupter.add_trigger_target(target.pybind_interrupter)
Triggers the target when this interrupter is triggered.
72 def remove_trigger_target(self, target: "SolveInterrupter") -> None: 73 """Removes the target if not null and present, else do nothing.""" 74 self.pybind_interrupter.remove_trigger_target(target.pybind_interrupter)
Removes the target if not null and present, else do nothing.
76 @contextlib.contextmanager 77 def interruption_callback(self, callback: Callable[[], None]) -> Iterator[None]: 78 """Returns a context manager that (un)register the provided callback. 79 80 The callback is immediately called if the interrupter has already been 81 triggered or if it is triggered during the registration. This is typically 82 useful for a solver implementation so that it does not have to test 83 `interrupted` to do the same thing it does in the callback. Simply 84 registering the callback is enough. 85 86 The callback function can't make calls to interruption_callback(), and 87 interrupt(). This would result is a deadlock. Reading `interrupted` is fine 88 though. 89 90 Exceptions raised in the callback are raised on exit from the context 91 manager if no other error happens within the context. Else the exception is 92 logged. 93 94 Args: 95 callback: The callback. 96 97 Returns: 98 A context manager. 99 100 Raises: 101 CallbackError: When exiting the context manager if an exception was raised 102 in the callback. 103 """ 104 callback_error: Optional[Exception] = None 105 106 def protetected_callback(): 107 """Calls callback() storing any exception in callback_error.""" 108 nonlocal callback_error 109 try: 110 callback() 111 except Exception as e: # pylint: disable=broad-exception-caught 112 # It is fine to set callback_error without any threading protection as 113 # the SolveInterrupter guarantees it will only call it at most once. 114 callback_error = e 115 116 callback_id = self.pybind_interrupter.add_interruption_callback( 117 protetected_callback 118 ) 119 120 no_exception_in_context = False 121 try: 122 yield 123 no_exception_in_context = True 124 finally: 125 self.pybind_interrupter.remove_interruption_callback(callback_id) 126 # It is fine to access callback_error without threading protection after 127 # remove_interruption_callback() has returned as the SolveInterrupter 128 # guarantees any pending call is done and no future call can happen. 129 if callback_error is not None: 130 if no_exception_in_context: 131 raise CallbackError() from callback_error 132 # We don't want the error in the context to be masked by an error in the 133 # callback. We log it instead. 134 logging.error( 135 "An exception occurred in callback but is masked by another" 136 " exception: %s", 137 repr(callback_error), 138 )
Returns a context manager that (un)register the provided callback.
The callback is immediately called if the interrupter has already been
triggered or if it is triggered during the registration. This is typically
useful for a solver implementation so that it does not have to test
interrupted to do the same thing it does in the callback. Simply
registering the callback is enough.
The callback function can't make calls to interruption_callback(), and
interrupt(). This would result is a deadlock. Reading interrupted is fine
though.
Exceptions raised in the callback are raised on exit from the context manager if no other error happens within the context. Else the exception is logged.
Arguments:
- callback: The callback.
Returns:
A context manager.
Raises:
- CallbackError: When exiting the context manager if an exception was raised in the callback.