Edit on GitHub

starplast.jobs

Background jobs, and a record of them that outlives the job.

Everything slow in this application -- rebuilding an embedding, a hyperparameter walk, a chat reply -- used to run with no visible sign except the window going unresponsive, which is indistinguishable from a crash. A job here is a named unit of work with a state you can look at after it has finished, because "what did that run do" is asked more often after the fact than during.

Jobs are kept after they finish rather than discarded. A failed job that vanishes takes its traceback with it, and the user is left knowing only that something did not work.

@dataclass
class Job:

One unit of background work, and its state after it has finished.

Kept after completion rather than discarded: a failed job that vanishes takes its traceback with it, leaving the user knowing only that something did not work.

Job( id: int, name: str, state: str = 'pending', note: str = '', progress: int = -1, result: Any = None, error: str = '', traceback: str = '', exception: Optional[BaseException] = None, _cancel: bool = False)
id: int
name: str
state: str = 'pending'
note: str = ''
progress: int = -1
result: Any = None
error: str = ''
traceback: str = ''
exception: Optional[BaseException] = None
active: bool
def cancel(self) -> None:

Ask the job to stop. Cooperative: the callable has to check should_stop.

There is no forcible kill. Terminating a thread mid-write leaves half-written caches, and this application writes caches.

cancelled: bool
class JobRunner(PyQt6.QtCore.QObject):

Runs jobs on a thread pool and reports on them.

busy_changed exists so a caller can drive one indicator without counting jobs itself -- it fires only on the transitions, not on every job, so an indicator does not flicker when one job ends as another begins.

JobRunner(parent=None, max_threads: int = 2)
def started(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def progress(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def finished(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def busy_changed(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

jobs: dict[int, Job]
def submit(self, fn: Callable, name: str) -> Job:

Start a job on the pool and return it, so a caller can watch or cancel it.

busy: bool
def active(self) -> list[Job]:

Jobs that have not finished.

def cancel_all(self) -> None:

Ask every unfinished job to stop.

def wait(self, ms: int = 5000) -> bool:

Block until the pool drains. For shutdown and for tests, not for the GUI thread.