Skip to content

flush

flush #

The watcher that writes open steps to their scratch's index, at most ten times a second.

Opening or closing a step wakes the flusher; advancing one only marks it dirty, so a loop advancing thousands of times costs the index one write per interval, not per iteration. A closed step is deleted at the next flush; a process that dies first is reclaimed through its liveness lock like any other owner of index rows.

INTERVAL_SECONDS = 0.1 module-attribute #

The longest an advance waits before the index sees it.

flusher = Flusher() module-attribute #

The one flusher of this process.

Flusher #

Writes the steps that report to a scratch into its steps table.

Source code in capturegraph-lib/capturegraph/recipes/progress/flush.py
class Flusher:
    """Writes the steps that report to a scratch into its ``steps`` table."""

    def __init__(self) -> None:
        """A flusher whose thread starts with the first step that has a scratch."""
        self._lock = threading.Lock()
        self._writing = threading.Lock()
        self._wake = threading.Event()
        self._closed: list[Step] = []
        self._thread: threading.Thread | None = None

    def opened(self, step: Step) -> None:
        """Wake the flusher so the index sees ``step`` open promptly."""
        if step.scratch is None:
            return
        with self._lock:
            if self._thread is None:
                self._thread = threading.Thread(target=self._run, name="cg-progress", daemon=True)
                self._thread.start()
        self._wake.set()

    def closed(self, step: Step) -> None:
        """Queue ``step``'s row for deletion and wake the flusher."""
        if step.scratch is None:
            return
        with self._lock:
            self._closed.append(step)
        self._wake.set()

    def flush(self) -> None:
        """Write every dirty open step and delete every closed one, now.

        One flush at a time: reading a step settles its dirty mark, so a caller
        arriving mid-flush would otherwise find nothing to write and return
        before the writer committed.
        """
        with self._writing:
            with self._lock:
                closed, self._closed = self._closed, []
            by_scratch: defaultdict[Scratch, tuple[list[Step], list[Step]]] = defaultdict(
                lambda: ([], [])
            )
            for step in registry.live():
                if step.scratch is not None and step.dirty:
                    by_scratch[step.scratch][0].append(step)
            for step in closed:
                if step.scratch is not None:
                    by_scratch[step.scratch][1].append(step)
            for scratch, (dirty, gone) in by_scratch.items():
                if scratch.closed:
                    continue
                try:
                    with scratch.database.transaction() as db:
                        for step in dirty:
                            steps.upsert(db, _row(step, scratch))
                        for step in gone:
                            steps.delete(db, step.id)
                except Exception:
                    logger.exception(f"progress: could not write steps to {scratch}")

    def _run(self) -> None:
        while True:
            self.flush()
            busy = any(step.scratch is not None for step in registry.live())
            self._wake.wait(INTERVAL_SECONDS if busy else None)
            self._wake.clear()

__init__() #

A flusher whose thread starts with the first step that has a scratch.

Source code in capturegraph-lib/capturegraph/recipes/progress/flush.py
def __init__(self) -> None:
    """A flusher whose thread starts with the first step that has a scratch."""
    self._lock = threading.Lock()
    self._writing = threading.Lock()
    self._wake = threading.Event()
    self._closed: list[Step] = []
    self._thread: threading.Thread | None = None

closed(step) #

Queue step's row for deletion and wake the flusher.

Source code in capturegraph-lib/capturegraph/recipes/progress/flush.py
def closed(self, step: Step) -> None:
    """Queue ``step``'s row for deletion and wake the flusher."""
    if step.scratch is None:
        return
    with self._lock:
        self._closed.append(step)
    self._wake.set()

flush() #

Write every dirty open step and delete every closed one, now.

One flush at a time: reading a step settles its dirty mark, so a caller arriving mid-flush would otherwise find nothing to write and return before the writer committed.

Source code in capturegraph-lib/capturegraph/recipes/progress/flush.py
def flush(self) -> None:
    """Write every dirty open step and delete every closed one, now.

    One flush at a time: reading a step settles its dirty mark, so a caller
    arriving mid-flush would otherwise find nothing to write and return
    before the writer committed.
    """
    with self._writing:
        with self._lock:
            closed, self._closed = self._closed, []
        by_scratch: defaultdict[Scratch, tuple[list[Step], list[Step]]] = defaultdict(
            lambda: ([], [])
        )
        for step in registry.live():
            if step.scratch is not None and step.dirty:
                by_scratch[step.scratch][0].append(step)
        for step in closed:
            if step.scratch is not None:
                by_scratch[step.scratch][1].append(step)
        for scratch, (dirty, gone) in by_scratch.items():
            if scratch.closed:
                continue
            try:
                with scratch.database.transaction() as db:
                    for step in dirty:
                        steps.upsert(db, _row(step, scratch))
                    for step in gone:
                        steps.delete(db, step.id)
            except Exception:
                logger.exception(f"progress: could not write steps to {scratch}")

opened(step) #

Wake the flusher so the index sees step open promptly.

Source code in capturegraph-lib/capturegraph/recipes/progress/flush.py
def opened(self, step: Step) -> None:
    """Wake the flusher so the index sees ``step`` open promptly."""
    if step.scratch is None:
        return
    with self._lock:
        if self._thread is None:
            self._thread = threading.Thread(target=self._run, name="cg-progress", daemon=True)
            self._thread.start()
    self._wake.set()