Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
solve_interrupter.py
Go to the documentation of this file.
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
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 """
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 )
None remove_trigger_target(self, "SolveInterrupter" target)
None add_trigger_target(self, "SolveInterrupter" target)
Iterator[None] interruption_callback(self, Callable[[], None] callback)