Source code for solprop.driver.service

# SPDX-FileCopyrightText: 2026 John C. Travers and contributors
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""The service loop: Python's side of an execution session (SEP 47 section 5).

A session (:mod:`solprop.backends.session`) owns the run's numerical state and its
schedule. This loop advances it and services what it reports -- saved planes and their
output transactions, plane observers and progress, snapshot persistence, warnings and
the terminal record -- then acknowledges each batch. It holds no numerical state of its
own: it never rediscovers the next plane, rebuilds a statistic or replays the attempts
to reconstruct a controller.

Ordering follows SEP 47 section 5.3: a plane is committed with its method history and
its execution record in one transaction (``solprop._interrupts.committing``); a
snapshot is serviced after that commit; the plane observer and the reporter follow.
A service failure leaves the batch unacknowledged, so the field it names can still be
exported for the terminal checkpoint, and the primary failure is preserved.
"""

from __future__ import annotations

import dataclasses
import logging
import time
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import cast

import numpy as np

from solprop._host import RunClock, usage
from solprop._interrupts import committing, poll
from solprop._ownership import hold
from solprop.backends.events import (
    EventBatch,
    ExportTicket,
    ObservationKind,
    ObservationReady,
    Outcome,
    Progress,
    ServiceKind,
    ServiceRequired,
    emit_warning,
    join_attempts,
)
from solprop.backends.export import ExportRecipe
from solprop.backends.host.session import HostSession
from solprop.backends.protocol import BackendHistory, HostStepping
from solprop.backends.protocol import StepStats as BackendRows
from solprop.backends.session import RunSession
from solprop.driver.plane_view import PlaneWatch
from solprop.driver.run import (
    SessionFacts,
    _announce,
    _earlier_execution,
    _FailedObservation,
    _history_of,
    _MarchKeeper,
    _method_history,
    _open_run,
    _record_abandoned_step,
    _record_execution,
    _refuse_unread_collar,
    _resume_position,
    _retain,
    _Status,
    retention_at,
)
from solprop.driver.summary import resources
from solprop.exceptions import ResponseBudgetError, RunTerminated, StopRequested
from solprop.fourier import GridKind, spectrum_to_internal
from solprop.log import (
    InterruptibleReporter,
    ProgressReporter,
    StopFlag,
    SummarisingReporter,
)
from solprop.output.hdf5 import HDF5Output
from solprop.output.schema import (
    CheckpointOutput,
    FieldRetention,
    Output,
    PlaneStore,
    RecipeRecord,
    RunMetadata,
    SavedPlane,
    StepHistory,
    StepStats,
    StoredField,
    TerminalCheckpoint,
    _adopted,
)
from solprop.output.snapshots import SnapshotController
from solprop.output.tdse_archive import TDSEArchiveWriter
from solprop.stepper.multistep import Multistep
from solprop.translate.problem import PropagationProblem
from solprop.translate.run_request import RunRequest
from solprop.translate.tdse import TDSEBinding

__all__ = ["MONITOR_RECONSTRUCTED", "ServiceContext", "serve"]

logger = logging.getLogger("solprop.driver.run")

_CONSUMER = ExportRecipe(power=True, optics=False)
"""The field a plane observer, a checkpoint record and an output that is not a plane
store hold: output amplitudes in the internal FFT convention (SEP 47 section 5.4)."""

_STORED = ExportRecipe(power=True, optics=True)
"""The field a plane store keeps: :data:`_CONSUMER` in the optics convention, which the
store's own conversion makes (``PlaneStore.save``)."""


[docs] @dataclass(frozen=True) class ServiceContext: """What the service loop needs besides the session and the output. Attributes ---------- problem : solprop.translate.problem.PropagationProblem The translated problem. request : solprop.translate.run_request.RunRequest The run's policies. metadata : solprop.output.schema.RunMetadata The run's record. reporter : solprop.log.ProgressReporter Where progress goes. retention : solprop.output.schema.FieldRetention Which planes keep their field. resume_history : bool or None The public method-history retention choice. snapshots : solprop.output.snapshots.SnapshotController or None Snapshot requests, when enabled. tdse_binding : solprop.translate.tdse.TDSEBinding or None A live atomic runtime. tdse_archive : solprop.output.tdse_archive.TDSEArchiveWriter or None A live archive to suspend beside the optical writer. recipe : solprop.output.schema.RecipeRecord or None The recipe the run was built from. start_plane : int The first save plane beyond the starting state. stop : solprop.log.StopFlag or None The run's stop flag, polled between batches as well as inside the session. restoration : collections.abc.Mapping or None, optional What a resumed execution restored of the method history (``Simulation._continue_march``), for its execution record; ``None`` for a fresh run or a caller that says nothing. """ problem: PropagationProblem request: RunRequest metadata: RunMetadata reporter: ProgressReporter retention: FieldRetention resume_history: bool | None snapshots: SnapshotController | None tdse_binding: TDSEBinding | None tdse_archive: TDSEArchiveWriter | None recipe: RecipeRecord | None start_plane: int stop: StopFlag | None = None restoration: Mapping[str, object] | None = None
def _rows(rows: BackendRows) -> StepStats: """Return a session's periodic rows in the form an output handler stores. - ``rows``: the rows, with a leading row axis and no batch axis. """ return StepStats(z=rows.z, step=rows.step, values=dict(rows.values)) MONITOR_RECONSTRUCTED = { "state": "reconstructed", "reason": "persisted_records", "restored": ["cumulative_totals", "quiet_state", "transverse_record"], "restarted": ["check_cadence", "transverse_cap"], } """How a resumed execution's boundary monitor was restored (SEP 47 section 5.5). Today's reconstruction from the persisted records: the cumulative totals, the quiet state and the transverse record are restored, and the check cadence and the transverse cap restart from that restored state; persisting them is not claimed.""" def _restart(base: Mapping[str, object], keeper: _MarchKeeper) -> dict[str, object]: """Return an execution's restart record with the latest retention decision. - ``base``: what the execution restored, fixed when it started. """ retention = keeper.record() return dict(base) if retention is None else {"retention": retention, **base} class _Exports: """One export per pending ticket, shared by every consumer. The output, a plane observer and a terminal checkpoint asking for the same field share one export (SEP 47 section 5.4); a field-free plane fetches nothing unless a consumer asks. A plane store's field is exported straight into its stored form, into the store's own room where it has one; a consumer that wants the internal convention after that is given the exact inverse of the optics conversion (:func:`solprop.fourier.spectrum_to_internal`), with no second export. An output that is not a plane store is given the consumer field itself. - ``kind``: the grid's spectral layout. """ def __init__(self, session: RunSession, kind: GridKind) -> None: """Start with nothing exported.""" self._session = session self._kind: GridKind = kind self._ticket: ExportTicket | None = None self._field: np.ndarray | None = None self._stored: np.ndarray | None = None self._phase: np.ndarray | None = None self._phased = False def _hold(self, ticket: ExportTicket) -> None: """Forget what another ticket exported.""" if self._ticket != ticket: self._ticket = ticket self._field = self._stored = self._phase = None self._phased = False def field(self, ticket: ExportTicket) -> np.ndarray: """Return the consumer field for a ticket, exporting it once. - Returns: Read-only output amplitudes in the internal convention. """ self._hold(ticket) if self._field is None: if self._stored is not None: field = spectrum_to_internal(self._stored, kind=self._kind) else: field = self._session.export(ticket, _CONSUMER) # A new array or a view of a held observation: held, so nothing copies it. hold(field) self._field = field return self._field def stored(self, ticket: ExportTicket, out: np.ndarray | None) -> np.ndarray: """Export the stored form of a ticket's field into `out`. The plane store is a plane's first consumer (:func:`_prepare_plane`), so its export is the ticket's first; any consumer after it is given the consumer field from it (:meth:`field`). - ``out``: the plane store's room (``PlaneStore.field_destination``), or ``None`` for a new array. - Returns: Read-only output amplitudes in the optics convention: `out` when given. """ self._hold(ticket) assert self._field is None and self._stored is None, "a plane's first export" stored = self._session.export(ticket, _STORED, out=out) if out is None: # A new array, this plane's own: held, so nothing copies it. hold(stored) else: # The plane store's room, which the store owns and writes again: frozen # but not held. stored.setflags(write=False) self._stored = stored return stored def release(self) -> None: """Drop the exports once their batch is acknowledged.""" self._ticket = self._field = self._stored = self._phase = None self._phased = False def phase(self, ticket: ExportTicket) -> np.ndarray | None: """Return the noise phase for a ticket, read once. - Returns: the phase, read-only, or ``None`` without a noise model. """ self._hold(ticket) if not self._phased: phase = self._session.noise_phase(ticket) if phase is not None: phase.setflags(write=False) self._phase, self._phased = phase, True return self._phase _Chunk = tuple[BackendHistory | None, BackendRows | None] """One event's attempts and periodic rows, as it delivered them.""" class _Chunks: """The attempts and rows delivered since the last saved plane, in order. A session delivers each attempt once, with the event after it (:mod:`solprop.backends.events`); this is where they are assembled. An observation takes them all as its plane's history and starts afresh; a running checkpoint includes them without consuming them; a terminal checkpoint includes them. """ def __init__(self, step: Callable[[float, float, bool], None]) -> None: """Start with nothing held.""" self._chunks: list[_Chunk] = [] self._step = step def add( self, history: BackendHistory | None, rows: BackendRows | None, *, report: bool = True, ) -> None: """Hold one event's chunk, and report its attempts. Every attempt arrives exactly once, so the reporter hears each once, in order, at the cadence of the session's yields rather than from inside its march. - ``rows``: its periodic rows, without a batch axis. - ``report``: ``False`` for the attempts no event delivered, which a failure's checkpoint holds and no reporter call may lose. """ self._chunks.append((history, rows)) if report and history is not None: for z, dz, accepted in zip( history.z.tolist(), history.dz.tolist(), history.accepted.tolist(), strict=True, ): self._step(z, dz, accepted) def joined(self) -> tuple[BackendHistory, BackendRows]: """Return every chunk held, assembled, without consuming them. - Returns: the attempts and rows since the last saved plane. """ return join_attempts(self._chunks) def take(self) -> tuple[BackendHistory, BackendRows]: """Return every chunk held, assembled, and start afresh. - Returns: the attempts and rows since the last saved plane. """ joined = self.joined() self._chunks = [] return joined def _plane( event: ObservationReady, attempts: tuple[BackendHistory, BackendRows], field: np.ndarray | None, phase: np.ndarray | None, ) -> SavedPlane: """Build the plane an output stores from an observation. - ``attempts``: its history and rows, assembled from the chunks since the previous plane. - ``field``: its field in the internal convention, or ``None``: a field-free plane, or one whose field is exported straight into a plane store. - ``phase``: its noise phase, where it keeps its field and a noise model runs. """ return SavedPlane( z=event.z, field=None if field is None else _adopted(field), stats=dict(event.stats), step_history=_history_of(attempts[0]), step_stats=_rows(attempts[1]), noise_phase=None if phase is None else _adopted(phase), ) def _prepare_plane( event: ObservationReady, output: Output, exports: _Exports, attempts: tuple[BackendHistory, BackendRows], shape: tuple[int, ...], *, keep: bool, ) -> tuple[SavedPlane, StoredField | None]: """Build an observation's plane, its kept field exported once for the output. A plane store that :attr:`~solprop.output.schema.PlaneStore.takes_exports` is given the field exported straight into its stored form, into its own room where it has one (SEP 47 section 5.4), and the plane carries none; any other output is given the consumer field on the plane (:func:`_save_plane`). - ``shape``: ``(n_modes, n_pol, n_omega)`` of the field. - Returns: the plane and, where its field was exported into a plane store, the stored field (``None`` otherwise). """ ticket = event.ticket phase = exports.phase(ticket) if keep else None if keep and isinstance(output, PlaneStore) and output.takes_exports: plane = _plane(event, attempts, None, None) room = output.field_destination(plane, shape) stored = StoredField( exports.stored(ticket, room), None if phase is None else _adopted(phase) ) return plane, stored return _plane(event, attempts, exports.field(ticket) if keep else None, phase), None def _save_plane(output: Output, plane: SavedPlane, stored: StoredField | None) -> None: """Hand a prepared plane to the output (:func:`_prepare_plane`).""" if stored is None: output.save(plane) else: assert isinstance(output, PlaneStore) output.save_exported(plane, stored)
[docs] def serve( # noqa: PLR0912, PLR0915 - the commit sequence of SEP 47 section 5.3 session: RunSession, output: Output, context: ServiceContext, ) -> None: """Advance a session to its end, servicing every event it reports. Any :class:`~solprop.backends.session.RunSession` is served: the reference's :class:`~solprop.backends.host.session.HostSession`, whose driver gives the execution facts and method history, or a backend's own session, through its protocol alone (:class:`~solprop.driver.run.SessionFacts`). Parameters ---------- session : solprop.backends.session.RunSession The started session. output : solprop.output.schema.Output Where the planes go; closed when the loop ends, whether or not it succeeded. context : ServiceContext Everything else the service needs. Raises ------ BaseException The run's primary failure, a stop request or a signal, after its terminal record and any checkpoint are written. """ problem, reporter = context.problem, context.reporter backend = _facts(session) planes = context.request.observation.save_planes started = time.perf_counter() clock = RunClock.start() checkpoint_output = _open_run( output, context.metadata, backend, context.tdse_binding ) if context.recipe is not None and isinstance(output, PlaneStore): with committing(): output.record_recipe(context.recipe) reserve = getattr(output, "reserve", None) if reserve is not None: reserve(planes.size + 1) _retain(output, context.retention, problem.shape) reached, missing_current_save = _resume_position( output, checkpoint_output, problem, context.request ) _refuse_unread_collar(problem, backend) # The request's own monitor: its thresholds; the session owns its state. monitor = context.request.boundary.monitor # Read before this execution's first record replaces the earlier one's. earlier = None if checkpoint_output is None else checkpoint_output.outcome resumed, monitored, unwatched = _earlier_execution(output, checkpoint_output) base: dict[str, object] = {} if context.restoration is not None: base["restoration"] = dict(context.restoration) if resumed and monitor is not None: # The record continues across the resume (SEP 21, ADR-057). session.continue_monitor( { "record": monitored, "plane": missing_current_save or output.n_saved == 0 or float(output["z"][-1]) == reached, "unwatched": unwatched, } ) base["monitor"] = dict(MONITOR_RECONSTRUCTED) _announce( problem, context.metadata, backend, output, reporter, (context.start_plane, reached), request=context.request, boundary_check=None if monitor is None else monitor.check, ) keeper = _MarchKeeper( output, backend, context.resume_history, method=_method_history(context.request), retention=None if earlier is None else retention_at(earlier.execution, reached), ) exports = _Exports(session, context.metadata.grid.kind) shape = tuple(context.problem.shape) current: list[ObservationReady | None] = [None] def _fetch() -> np.ndarray: """Return the pending observation's field, for the plane observer.""" event = current[0] assert event is not None # the observer is shown a pending plane only return exports.field(event.ticket) watch = PlaneWatch(reporter, context.metadata, _fetch, clock.observer) pending: SavedPlane | None = None pending_save_count = output.n_saved pending_ticket: ExportTicket | None = None snapshots = context.snapshots snapshots_started = False terminal_rows: tuple[object, object] = (None, None) chunks = _Chunks(reporter.step) failure: BaseException | None = None adams = isinstance(context.request.integration.method, Multistep) try: if snapshots is not None: snapshots._start() snapshots_started = True _record_execution( checkpoint_output, backend, context.tdse_binding, _Status("running", reached), clock=clock, restart=_restart(base, keeper), ) batch: EventBatch | None = None while True: if batch is not None: # Between batches, before the next propagation: a stop set during the # service is honoured here, while the batch's field can still be # exported for the checkpoint (SEP 41, S17). poll(context.stop) session.acknowledge(batch.receipt) exports.release() current[0] = None batch = None if session.finished: break if snapshots is not None and snapshots._due(): session.request_snapshot() pending = None pending_save_count = output.n_saved pending_ticket = None terminal_rows = (None, None) batch = session.advance() for event in batch.events: if isinstance(event, Progress): chunks.add(event.history, event.step_stats) elif isinstance(event, ObservationReady): chunks.add(event.history, event.step_stats) attempts = chunks.take() if event.kind is ObservationKind.LAUNCH and not ( output.n_saved == 0 or missing_current_save ): # A resumed file already holds the plane at its position. continue current[0] = event pending_ticket = event.ticket # Its attempts, should the plane fail before it holds them. terminal_rows = attempts if event.kind is not ObservationKind.LAUNCH: _warn_coarse(event, planes) keep = context.retention.keeps(output.n_saved) pending, stored = _prepare_plane( event, output, exports, attempts, shape, keep=keep ) pending_save_count = output.n_saved with committing(): _save_plane(output, pending, stored) if event.kind is not ObservationKind.LAUNCH: keeper.keep(pending.z) reached = event.z _record_execution( checkpoint_output, backend, context.tdse_binding, _Status("running", reached), clock=clock, restart=_restart(base, keeper), ) watch.show(pending, output.n_saved - 1, stored) if event.kind is not ObservationKind.LAUNCH: reporter.plane(event.z, event.summary) elif isinstance(event, ServiceRequired): chunks.add(event.history, event.step_stats) if event.kind is ServiceKind.WARNING: # A warning barrier (SEP 47 section 5.5): the filters may # raise it, which fails the run at this committed endpoint, # its field and the attempts since the last plane there for # the checkpoint. pending_ticket = event.ticket terminal_rows = chunks.joined() emit_warning(event) else: # A snapshot, which a session yields only at the request above. assert isinstance(output, HDF5Output) _service_snapshot( event, output, checkpoint_output, backend, keeper, exports, context, chunks.joined(), clock=clock, adams=adams, base=base, ) else: chunks.add(event.history, event.step_stats) if event.outcome is Outcome.COMPLETED: with committing(): _record_execution( checkpoint_output, backend, context.tdse_binding, _Status("complete", reached), clock=clock, restart=_restart(base, keeper), ) else: assert event.error is not None pending_ticket = event.ticket terminal_rows = chunks.joined() raise event.error reporter.finished(time.perf_counter() - started) if isinstance(reporter, SummarisingReporter): reporter.summary(resources(usage(clock))) except BaseException as error: failure = error failed_z = _persist_failure( session, checkpoint_output, context, keeper, exports, pending_ticket, terminal_rows, chunks, base, _FailedObservation( pending, output.n_saved > pending_save_count, error, reached, clock, output.n_saved > 0 and context.retention.keeps(output.n_saved - 1), ), ) if failed_z is not None: reached = failed_z if isinstance(error, RunTerminated | StopRequested): logger.log( logging.INFO if isinstance(error, StopRequested) else logging.WARNING, "%s at z = %.6e m; %s", error, reached, "the run's state there is written, and resume=True continues it" if failed_z is not None else "no state was written there", ) if isinstance(reporter, InterruptibleReporter): reporter.interrupted(reached, error) raise finally: try: output.close() except BaseException as error: if failure is None: raise failure.add_note(f"the optical output could not be closed: {error}") finally: if snapshots is not None and snapshots_started: snapshots._finish()
def _warn_coarse(event: ObservationReady, planes: np.ndarray) -> None: """Log a plane whose propagation did not meet its tolerance. - ``planes``: the requested save planes, m. """ summary = event.summary if summary.met_tolerance: return index = min(event.plane_index, planes.size - 1) logger.warning( "a step was too coarse for the tolerance it is scaled by on the way to the " "save plane at z = %.6e m (plane %d of %d): the largest scaled error norm was " "%.3e against the requested 1.0", float(planes[index]), index + 1, planes.size, summary.max_norm, ) def _service_snapshot( # noqa: PLR0917 - the snapshot transaction's context event: ServiceRequired, output: HDF5Output, checkpoint_output: CheckpointOutput | None, backend: object, keeper: _MarchKeeper, exports: _Exports, context: ServiceContext, attempts: tuple[BackendHistory, BackendRows], *, clock: RunClock, adams: bool, base: Mapping[str, object], ) -> None: """Persist a running checkpoint at an accepted endpoint and copy the snapshot. At a saved plane the plane is the checkpoint, as before; elsewhere the endpoint's field and the attempts and rows since the last plane are written as a running checkpoint without being consumed (SEP 47 section 5.5). - ``attempts``: the attempts and rows since the last plane, the snapshot's own chunk included: the running checkpoint's, which does not consume them. """ snapshots = context.snapshots assert snapshots is not None z = event.z at_plane = output.n_saved > 0 and float(output["z"][-1]) == z checkpoint = None def persist() -> None: """Keep the method history and record the execution beside the snapshot.""" keeper.keep(z, required=adams) _record_execution( checkpoint_output, backend, # pyright: ignore[reportArgumentType] context.tdse_binding, _Status("running", z), clock=clock, checkpoint=checkpoint, restart=_restart(base, keeper), ) # A snapshot at the plane just saved needs no running checkpoint beside it. if not at_plane: assert event.ticket is not None history, rows = attempts phase = exports.phase(event.ticket) checkpoint = TerminalCheckpoint( plane=SavedPlane( z=z, field=_adopted(exports.field(event.ticket)), stats={}, step_history=_history_of(history), step_stats=_rows(rows), noise_phase=None if phase is None else _adopted(phase), ), next_save_index=int( np.searchsorted(context.request.observation.save_planes, z, "right") ), landed=True, ) snapshots._service( output, context.tdse_archive, next_save_index=int( np.searchsorted(context.request.observation.save_planes, z, "right") ), require_history=adams, persist=persist, provenance=context.metadata, z=z, ) def _persist_failure( # noqa: PLR0917 - the failure's whole context session: RunSession, output: CheckpointOutput | None, context: ServiceContext, keeper: _MarchKeeper, exports: _Exports, ticket: ExportTicket | None, rows: tuple[object, object], chunks: _Chunks, base: Mapping[str, object], failure: _FailedObservation, ) -> float | None: """Checkpoint the accepted state without replacing the original exception. - ``output``: handler supporting terminal checkpoints, when available. - ``ticket``: the committed field to checkpoint, or ``None`` when the state is poisoned or the failure arose outside the session. - ``rows``: the attempts and periodic rows since the last saved plane, assembled for the terminal event or the failed plane, or ``None`` for each. - Returns: the checkpointed position, m, when one was written. """ if output is None: return None backend = _facts(session) in_flight = backend.in_flight if failure.pending is None and in_flight is not None: # A driver's step in flight, or a poisoned session's: the latest running # checkpoint, when one follows the last plane, is the state to resume from # (SEP 47 section 5.5). return _record_abandoned_step( backend, output, context.tdse_binding, failure, in_flight, restart=_restart(base, keeper), ) if ticket is None: # The loop's own session is open, and a state left in flight is recorded # above: the committed state always has a ticket. ticket = _fallback_ticket(session) assert ticket is not None problem = context.problem reached = None try: pending = failure.pending field = _adopted(exports.field(ticket)) phase = exports.phase(ticket) if pending is None or pending.z != ticket.z: history, step_rows = rows if history is None and step_rows is None: # A failure outside the session: the chunks since the last plane, a # running checkpoint's included, and what no event delivered, which # the run's reporter does not hear: it is ending, and a reporter that # raised here would lose the checkpoint. chunks.add(*session.undelivered(ticket), report=False) history, step_rows = chunks.joined() pending = SavedPlane( z=ticket.z, field=field, stats={}, step_history=StepHistory.empty() if history is None else _history_of(history), # pyright: ignore[reportArgumentType] step_stats=StepStats.empty() if step_rows is None else _rows(step_rows), # pyright: ignore[reportArgumentType] noise_phase=None if phase is None else _adopted(phase), ) elif pending.field is None: pending = dataclasses.replace( pending, field=field, noise_phase=None if phase is None else _adopted(phase), ) history = pending.step_history ends = (history.z + history.h)[history.accepted] landed = ( not context.request.observation.dense or pending.z == problem.initial.z or bool(np.any(np.isclose(ends, pending.z, rtol=1e-12, atol=0.0))) ) if failure.already_saved: pending = dataclasses.replace( pending, step_history=StepHistory.empty(), step_stats=StepStats.empty() ) reached = pending.z checkpoint = TerminalCheckpoint( plane=pending, next_save_index=int( np.searchsorted( context.request.observation.save_planes, pending.z, side="right" ) ), landed=landed, ) with committing(deliver=False): # The history first, so that the record states what was kept beside the # checkpoint; a failure to keep it still leaves the checkpoint recorded. try: keeper.keep(reached) except Exception as unkept: failure.error.add_note( f"the method history could not be kept beside the terminal " f"checkpoint: {unkept}" ) logger.exception("could not keep the method history") _record_execution( output, backend, context.tdse_binding, _Status( "budget_exhausted" if isinstance(failure.error, ResponseBudgetError) else "failed", reached, f"{type(failure.error).__name__}: {failure.error}", ), clock=failure.clock, checkpoint=checkpoint, restart=_restart(base, keeper), ) except Exception as checkpoint_error: failure.error.add_note( f"terminal checkpoint could not be persisted: {checkpoint_error}" ) logger.exception("could not persist the terminal checkpoint") return reached def _fallback_ticket(session: RunSession) -> ExportTicket | None: """Return a ticket for the committed state after a failure outside the session. A service failure -- an output error, a signal during a commit -- leaves the session's batch pending, so its state is still where the batch left it. When nothing is pending, the session issues a failed terminal batch whose ticket exports the committed state (``HostSession.pending_ticket``). - Returns: a ticket for the committed state, or ``None`` when the session is closed or its state was left in flight. """ # A session that can end itself at its committed state for the checkpoint says # so (`NativeSession.failure_ticket`); the reference session's pending ticket # already does. issue = getattr(session, "failure_ticket", None) if callable(issue): return cast("ExportTicket | None", issue()) return session.pending_ticket() def _facts(session: RunSession) -> HostStepping | SessionFacts: """Return what the service loop's records read as the session's backend. The reference session's driver, or a session's own facts (:class:`~solprop.driver.run.SessionFacts`). """ if isinstance(session, HostSession): return session.driver return SessionFacts(session)