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                )
class CallbackError(builtins.Exception):
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.

class SolveInterrupter:
 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.
pybind_interrupter
def interrupt(self) -> None:
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.

interrupted: bool
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.

def add_trigger_target( self, target: SolveInterrupter) -> None:
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.

def remove_trigger_target( self, target: SolveInterrupter) -> None:
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.

@contextlib.contextmanager
def interruption_callback(self, callback: Callable[[], None]) -> Iterator[None]:
 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.