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()
|