prefect_bootstrap
Idempotent Prefect bootstrapping for the metadata runtimes.
Every process that serves and triggers the file_metadata/scan_metadata
deployments against a Prefect server must first ensure three things: a
tag-based concurrency limit serialising cache writes, the zombie-flow-run
reaper automation (self-hosted Prefect ships neither by default), and a
heartbeat cadence fine enough for that automation to be accurate.
This is shared by the orchestrator (its embedded desktop-app server) and
bitfount.scripts.run_pod (the sidecar server used in Docker), so the policy —
and any future tuning of it — has one source of truth. The heartbeat frequency
and the reaper's detection window are coupled (a window shorter than the
cadence reaps healthy runs), which is exactly why they live in the same module.
What this module cannot ensure is that the reaper ever fires. Its pending
detection window lives in Prefect's automation_bucket table and is swept on
server restart, and it cannot witness a crash that took the server down in the
same instant as the run — which is the normal shape of a laptop reboot or power
cut here, since the server is a child of the same application as the pod. The
client-side complement is bitfount.runtimes.recovery, which each process runs
once at start-up: it judges liveness from the heartbeat stream rather than
waiting to witness a transition, ends the runs that are provably gone, and
resubmits them. The two are not redundant — the reaper handles the common case
promptly while the process is up; recovery handles the case the reaper
structurally cannot see.
Module
Functions
ensure_background_cache_write_concurrency_limit
async def ensure_background_cache_write_concurrency_limit() ‑> None:Idempotently create the background_cache_write concurrency limit.
create_concurrency_limit is itself idempotent (an upsert), so this can
be called on every process startup with no special guard.
ensure_heartbeat_frequency
def ensure_heartbeat_frequency() ‑> None:Pin PREFECT_FLOWS_HEARTBEAT_FREQUENCY before any serve() call.
Liveness detection is only as good as the heartbeat cadence underneath it.
Prefect's default is 180 s (prefect.settings.models.flows), so a process
that leaves it unset emits heartbeats less often than the zombie window
below — and every long-running flow gets reaped mid-flight as a false
positive.
Must be called before the deployments are served, because serve() runs
each flow in a subprocess that inherits this environment. Uses
setdefault, so an explicit operator override still wins.
ensure_scan_chain_automation
async def ensure_scan_chain_automation() ‑> None:Idempotently register — and reconcile — the scan-chain automation.
Replaces the completion-callback chaining this once had, which rebuilt the
scan trigger from an in-memory dict and was therefore silently lost when a
restart mid-run emptied that dict. A server-side automation reacts to the
file_metadata deployment's flow-run Completed event instead, so the chain
survives the restart of whatever submitted the run.
Shared by both processes that serve these deployments, and not optional for
either: scan_metadata_runtime declines to run while a file_metadata run is
in flight (bitfount.runtimes.dedup.is_indexing_in_flight) precisely because
this automation will run it once indexing completes. A server without it
would turn every such skip into scan metadata that never gets collected.
Must be called after the scan deployment is served — its id is resolved here.
Reconciles rather than skips for the same reason ensure_zombie_automation
does: the automation outlives any deploy, so an install upgraded from an older
build holds a spec whose parameters predate only_paths, and skipping would
leave every scoped file_metadata run chaining into an unscoped scan run.
Raises
Exception: Whatever the Prefect client raises. Callers treat a failure here as non-fatal, but they do the logging — the two callers word it differently.
ensure_zombie_automation
async def ensure_zombie_automation() ‑> None:Idempotently register — and reconcile — the zombie-reaper automation.
A killed flow run (e.g. a Windows Update reboot mid-walk, or a Docker
container recycle) would otherwise sit at Running forever — nothing flips
it. This registers a Proactive EventTrigger that fires when a flow run
has gone silent for _zombie_window() and moves it to Crashed. See
_desired_zombie_automation for why the trigger is shaped the way it is.
Unlike create_concurrency_limit, create_automation is not itself
idempotent — calling it twice would create duplicate automations — so this
is guarded by a name lookup (read_automations_by_name).
That guard cannot merely skip when a match is found, though: the automation lives in the Prefect server's DB and outlives any deploy, so a definition registered by an older version would otherwise persist forever and no fix to the spec above would ever reach an existing install. Instead we compare against the desired spec and update in place when it differs, which makes every startup self-healing.
liveness_silence_window
def liveness_silence_window() ‑> datetime.timedelta:How long a flow run may go silent before anything may presume it dead.
The public name for the window the reaper automation uses, exported so that
client-side liveness checks derive "dead" from the same value rather than a
second guess at it. A client that used a shorter window would call a run dead
that the reaper still considers healthy; a longer one would refuse to act on a
run Prefect had already marked Crashed.
This matters because the reaper is not sufficient on its own: its pending
detection window is swept on server restart, so after a machine-wide outage a
dead run is never marked Crashed and a client must reach the same verdict
from the heartbeat stream itself
scan_chain_automation_registered
async def scan_chain_automation_registered(client: PrefectClient) ‑> bool:Whether the scan-chain automation is registered and enabled.
Two decisions elsewhere are only safe because the chain exists — recovery
replaying file_metadata alone and expecting scan to follow, and
scan_metadata_runtime skipping itself while indexing is in flight — and
registration is best-effort at start-up. Both therefore ask rather than
assume: an assumption that silently fails here drops scan metadata until
someone restarts the process.
A disabled automation counts as absent: it will not fire, whatever the row says.
Arguments
client: An openPrefectClient.
Returns
Whether an enabled automation is registered under
SCAN_CHAIN_AUTOMATION_NAME. False if the question cannot be answered
— the callers' fallbacks cost work, and being wrong the other way costs
coverage.
Global variables
BACKGROUND_CACHE_WRITE_TAG- Tag applied tostore_file_metadata/scan-metadata write tasks; the concurrency limit below ensures only one such write runs at a time across all flows, so concurrent SQLite writes can't corrupt the cache DB.
HEARTBEAT_FREQUENCY_SECONDS- This is the fallback default:_zombie_window()sizes the reaper's window from thePREFECT_FLOWS_HEARTBEAT_FREQUENCYenv var, which is also what the served flow-run subprocesses read, so cadence and window always track one runtime value and cannot drift apart. The orchestrator hardcodes this same number (orchestrator.prefect_manager._HEARTBEAT_FREQUENCY_SECONDS) to avoid importing the SDK into its early env-setup path; a test there asserts the two agree. Must stay >= Prefect's ownge=30bound on the setting, else the served subprocesses die with a pydantic ValidationError.
SCAN_CHAIN_AUTOMATION_NAME- Name of the file_metadata -> scan_metadata chain automation; also its idempotency key (seeensure_scan_chain_automation).
ZOMBIE_AUTOMATION_NAME- Name of the zombie-reaper automation; also used as the idempotency key (seeensure_zombie_automation).