diff --git a/AGENTS.md b/AGENTS.md index 3afdb09..090b56a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,7 +200,7 @@ authority; `decisions.md` records the team's rulings; `foundation.md` and `stage-contract.md` are the two model pages everything else builds on, and `foundation.md` governs where an older page still describes the retired run model; each pipeline stage has its own page (`reference`, -`difference`, `load`, `maintain`, `crossmatch`, `statistics`, +`difference`, `intake`, `load`, `maintain`, `crossmatch`, `statistics`, `finalize`, `alerts`, `prune`, `export`); `photometry.md` is not a stage page, since the rebuild carries no photometry stage, and holds the forced-photometry note instead; `tool.md`, `loop.md` and diff --git a/system/alerts.md b/system/alerts.md index 5ee8eb9..ee2720a 100644 --- a/system/alerts.md +++ b/system/alerts.md @@ -311,6 +311,11 @@ position within it and within the container. ## Kafka +The foundation publishes packets to Kafka with its own verb, `rapidpipe +publish`, after the packet stage; the [foundation](foundation) page +states it under Publication to Kafka. What follows is the retired +stage's record. + `[publish] kafka` defaults to `false`. Setting it `true` exits 64, "Kafka publication is not enabled in this build": the same refusal `dev`'s own pipeline path takes, since `dev` never constructs a Kafka diff --git a/system/decisions.md b/system/decisions.md index 7f4ed16..268efa6 100644 --- a/system/decisions.md +++ b/system/decisions.md @@ -9,6 +9,26 @@ page where attribution matters. ## Rulings +Ben's rulings of 2026-10-21 evening on the instance type and the plan +in force. The [foundation](foundation) page states the design they +shaped, and the [stage contract](stage-contract) page the execution +record. + +(decision-instance-type)= +**The instance type is recorded and no family is pinned**: every job +records the instance type it ran on beside its image digest, and the +compute environment is not pinned to one instance family. +(Ben, 2026-10-21 20:41 PDT) + +(decision-hlwas-direction)= +**The exercised HLWAS pipeline is the plan in force**: the direction +question is set aside; the plan is a heavily exercised pipeline from +ASDF intake through Kafka publication, with the data entirely within +SMDC and nothing imported from another centre, starting with the SOC +HLWAS simulation without source injection, and iterating and ramping +once it runs end to end. The test is that the chain has the right +shape and functions on simple data. (Ben, 2026-10-21 20:50 PDT) + Ben's rulings of 2026-10-21 on the night through the chain. The [foundation](foundation) page states the design they shaped, and the [stage contract](stage-contract) page the retry and the PSF input. diff --git a/system/foundation.md b/system/foundation.md index 4636142..09693f9 100644 --- a/system/foundation.md +++ b/system/foundation.md @@ -6,14 +6,18 @@ This page states how RAPID records what it made and which of it is official, and the narrow chain of stages built on that record, from stored images to alert packets. It is the design the `rebuild` branch of the pipeline repository implements: `database/schema.sql`, the -package `rapidpipe.foundation`, the stages `import`, `reference`, -`difference`, `associate` and `packet`, the reader -`scripts/read_packets.py` and the queries in `database/queries.sql`. +package `rapidpipe.foundation`, the stages `intake`, `import`, +`reference`, `difference`, `associate` and `packet`, the loop, the +Kafka publication, the reader `scripts/read_packets.py`, the run +validation `scripts/validate_run.py` and the queries in +`database/queries.sql`. The [stage contract](stage-contract) page states what a stage is under it, and the [decisions](decisions) page records the rulings it rests on. The chain has run end to end on SMDC's database and AWS Batch over -one field's whole night, 69 epochs; the last sections give the -procedure, the checks the night met and what it measured. +one field's whole night, 69 epochs, and then from SOC L2 ASDF files to +the Kafka topic over 426 epochs of a simulated wide-area survey; the +last sections give the procedure, the checks both met and what they +measured. ## In plain terms @@ -52,7 +56,7 @@ from the run's body. A job is one execution of one stage under a run. The `jobs` row holds the stage, its scope, its state (`launched`, `running`, `published` or `failed`), the image digest it ran under (none for a job run from a -checkout), the settings hash, the attempt whose outputs were published, +checkout) and beside it the instance type, the settings hash, the attempt whose outputs were published, its manifest, the launched, started and ended times, the exit code, and the reason an automatic binding was refused, if one was. Local and Batch executions both get a job row. A killed execution is a job row @@ -331,9 +335,14 @@ and `dev`'s per-object forced-photometry store. | `rapidpipe job local --run --stage --scope JSON [--inputs JSON] [--expects JSON] [--excludes JSON]` | records a job and runs it in this process, then publishes | | `rapidpipe job run --job ` | runs an existing job's next attempt, then publishes; stops at once if the job is published | | `rapidpipe job submit ... --job-definition ` | records a job and submits one Batch job whose command is `job run` | +| `rapidpipe publish --run [--settings FILE]` | produces the run's official, unstamped packet sets to the Kafka topic and stamps each (Publication to Kafka, below) | +| `rapidpipe loop --run --filters --scas --pointings --job-definition` | carries a selection one stage forward per pass until it drains ([loop](loop)) | -A refusal exits 1 with the binding named; a missing row or unreadable -file exits 64. +A refusal exits 1 with the binding named. The exception is a refusal +because the job's run is not live: the job is published and its rows +are an unbound candidate, which is what a run that is not live is for, +so the job exits 0 and the refusal is on its row and on standard +error. A missing row or unreadable file exits 64. ### Where outputs live @@ -370,8 +379,13 @@ a published job and otherwise takes the next attempt. Batch places a retry within seconds, with no backoff, so the three attempts ride out a database outage only as long as the connection's own retries inside each attempt, about 8 s each. A job can still be resubmitted by hand, -the same Batch request again. Running the job records the job definition's image digest on -the job row, and its printed result carries the peak resident memory +the same Batch request again. Running the job records the job +definition's image digest on the job row and beside it the instance +type, read from the instance's metadata service when the attempt +starts (none when the service does not answer, as on a developer's +machine), so a re-execution can be compared with the instance it ran +on ({ref}`ruling `). The compute environment is +not pinned to one instance family. The job's printed result carries the peak resident memory of the stage's process and of its largest child, which is how a toolkit binary's memory is seen; Batch itself reports no memory figure. @@ -392,6 +406,7 @@ image. Each simplification keeps the shape of the full stage. | Stage | Reads (named in `job_inputs`) | Writes (declared outputs) | Left out until breadth | |---|---|---|---| +| `intake` | one SOC L2 `_cal.asdf` file under the run's source root; the science PSF of its filter and SCA | the gzipped 16-HDU FITS converted as `dev` converts it; `exposures` and `l2files`, with limiting magnitude, overlapping fields and sky indices computed | other filters, for want of their PSFs | | `import` | stored products for one field: L2 files, difference images, PSFs, the reference and its mosaic catalog, the Gaia copy; the cap on sources per image | copies of the image files; `exposures`, `l2files`, `diffimages`, `psfs`, `refimages`, `refimcatalogs`, `catalogs` and `sources` (the brightest N per difference image by flux over its error), with limiting magnitude, overlapping fields and sky indices computed; the source system's ids kept; provenance marked unknown | other fields and epochs; recovering provenance | | `reference` | the field's `l2files` rows for one filter and SCA | the awaicgen mosaic with its coverage and uncertainty maps and SExtractor catalog; `refimages`, `refimimages` (one row per coadded L2 row) and `refimcatalogs` | `dev`'s later reference changes (infobits, status, the 9100-pixel mosaic); the Photutils catalog | | `difference` | one epoch's `l2files` row, a `refimages` row with its mosaic catalog, and one `psfs` row of each kind, `science` and `reference`; the cap on sources | the ZOGY difference image and the files `packet` reads beside it, SFFT and naive as `dev` runs them; `diffimages` and the `sources` set, mapped exactly as the import maps `dev`'s | sumrat, real/bogus and the extra SFFT catalog columns; `dev`'s loader prefilter | @@ -418,6 +433,15 @@ A PSF's `psf` scope declares its kind; the stage reads the file's before any manifest if it names the other kind. A file with no keyword imports under the declared kind, and the manifest says so. +### intake + +The [intake](intake) page states `intake`: its scope (one file of a +dataset), the science PSF it names, the conversion ported from `dev`, +the `exposures` and `l2files` rows it declares with the `expid` packed +from the file's name, and its settings. Where `import` copies products +another system made, `intake` makes the L2 product from the raw file, +so a run that starts from intake owns its data from the first stage. + ### reference and difference The [reference](reference) page states `reference`: its scope (field, @@ -490,6 +514,42 @@ stated size with the stated fill; that every forced value equals the precedes its trigger; and that every index row decodes to its record and names the job's recorded inputs. +### Publication to Kafka + +`rapidpipe publish --run ` takes every packet set of the run that is +official and not yet stamped, and produces one message per packet to the +topic. The key is the packet set's job and the packet's source id; the +value is a one-record Avro container, the set's container header with +the packet's own block, so each message decodes on its own and carries +its schema. There is no schema registry. The producer waits for every +acknowledgement from all in-sync replicas, then stamps the set's job +with the time (`jobs.kafka_published_at`). A producer error leaves the +set unstamped and the verb exits 70. A crash between the last +acknowledgement and the stamp offers the set again on the next call, so +a consumer may see a packet twice; replay is allowed and there is no +exactly-once delivery. A set that loses its binding before it is +stamped is not offered. + +The cluster's bootstrap servers, the topic and the largest request the +producer may send are named in the settings file's `[kafka]` table as +the deployment's parameter names, never their values, and the +connection authenticates with the caller's own cloud credentials. The +transport is not part of a run's recipe: `publish` reads the table from +the shipped settings file or the one `--settings` names, not from the +run's body. + +`scripts/consume_packets.py --run ` is the consumer check. It reads +every partition from its earliest offset to the end offset at its +start, with no consumer group; keeps the messages whose key names one +of the run's stamped sets; and requires each to decode as one record +valid against its index row's schema, to equal the outbox bytes for its +key, and every stamped packet to be seen at least once. Zero kept is a +failure. + +Today the producer runs where the loop runs, on a host, after the +packet stage has published. Producing from the packet stage's own Batch +job, as its last act, is not decided (Not decided here, below). + ### Settings of the thin run The run's body is `rapidpipe/settings/foundation-thin.toml`, one table @@ -505,8 +565,10 @@ them); `[difference]` adds `max_sources` (500) with the import's meaning, and `[difference.statistics] clip_correction_seed` (0) makes the stage's output the same on the same inputs. The paths in those tables are the pipeline image's; a run outside the image overrides them in its own body. Data locations are -never settings: the import's source directory is its job's scope and -the outputs root is the environment's. +never in the shipped file: the import's source directory is its job's +scope, the intake's source root is the run's own `[intake] +source_root`, given in the settings overlay at `run create`, and the +outputs root is the environment's. ## The queries @@ -556,7 +618,7 @@ minutes. The same observations on synthetic rows run in CI under The SMDC database `rapid` holds the foundation in its `public` schema, and the pipeline repository's `docs/foundation-smdc-step.md` is the -procedure, one command and then its check per step. It has six parts. +procedure, one command and then its check per step. It has seven parts. **The schema.** `public` is dropped and recreated from `schema.sql` in one transaction, through the pooler, as the database owner. Everything @@ -662,6 +724,55 @@ campaign official and exercises the operations on it. pooler*: the second blocked on the row lock, then refused naming the binding, and nothing was half-applied. +**The wide-area survey.** The seventh part takes the chain from raw +SOC L2 files to the Kafka topic, with every byte inside SMDC and +nothing imported from another centre. Its data are STScI's SOC +simulation r00342 of the High Latitude Wide Area Survey, read from a +public bucket: 54 pointings on a grid one SCA wide, revisited to within +a few arcseconds in every pass, with a Gaia-fitted WCS and no +time-variable sources, so every difference is a null test. It uses F146 +only, the one filter with PSFs on SMDC. + +- *Fixes first.* The instance type went onto the job row, a refusal + because the run is not live stopped being a Batch failure, the + download cache is removed when the process exits, and the reference + stage's seed is pinned as the difference stage's is. One proof job on + each of two Intel sizes reproduced its epoch's earlier Intel rows bit + for bit; the one earlier AMD execution of the same epoch differs from + all of them in a few thousand rows, by at most half an arcsecond and a + few percent in flux. +- *The endpoint before breadth.* The GBTDS night's official packets + were produced to the topic from the fleet host and read back by the + consumer check, every one byte-equal to the outbox; a second publish + produced nothing. +- *A live run beside the GBTDS one.* The HLWAS dataset has its own run, + live for its survey and dataset while the GBTDS run stays live for + its own, as the one-live-run rule allows. Its 36 F146 PSFs, one of + each kind per SCA, were imported under the dataset, and a Gaia DR3 + copy covering the whole footprint was fetched from the ESA archive + and imported under its own catalog name, since the `catalogs` key has + no dataset and the GBTDS copy holds the plain name. +- *One pointing.* The six F146 epochs of one SCA's pointing went through + intake, reference, difference, associate and packet on Batch, then to + the topic, with the validation checks now a script run over the whole + run. +- *The loop.* `rapidpipe loop` took a second pointing from ASDF to the + topic in 18 passes with no hand on it. +- *The ramp.* The loop's selection widened to every F146 pointing on + SCA 8, 312 more epochs on 52 more fields, drained in 15 passes and + 42.8 minutes with no failed job; then to all 18 SCAs of the first + pointing, 102 more epochs, drained in 18 passes and 34.6 minutes. The + run ended with 426 epochs on 70 fields, 70 references, 70 per-field + chains and 55,436 packets on the topic, every job published and bound. +- *A field shared by two SCAs.* The pointing grid's step is one SCA + width, so SCA 9 of one pointing lands on the field SCA 8 of the next + pointing covers. The reference key has no SCA, so SCA 9's epochs were + differenced against SCA 8's reference, each naming its own SCA's + reference PSF, and were appended to that field's chain after SCA 8's + epochs although each was taken earlier. Nothing failed and the chain + check held, since it checks integrity and not order; the question is + open (Not decided here, below). + ## Validation A night is one coherent account when it meets ten checks. Each is a @@ -709,6 +820,18 @@ official, five sets deep; 7 to 10 held on the campaign. Check 9 held by the rule's own refusal: after a later group has moved one of its keys, an undo refuses and an explicit group restores. +Checks 1 to 6 are now one script, `scripts/validate_run.py --run `, +each a function scoped to one run's jobs. Two are restated for that +scope: the chain check covers every object set of the run that is bound +or is the last of its chain, and the forced check requires that a +packet set's row for an (object, image) pair a recorded predecessor set +also holds is that predecessor's measurement. Checks 7 to 10 are +operations with no read-only form over a run, checked as they run. The +script passed on both GBTDS runs, and on the wide-area run after its +first pointing, after its second, after the SCA 8 ramp and after the +18-SCA ramp. The reader was clean on every packet set and the consumer +check on every packet each time. + ## Measurements Measured 2026-10-21 on SMDC, field 4658155, SCA 8, filter W146, every @@ -769,9 +892,54 @@ minutes over one night; the difference stage dominates until the window is long. Shortening either is the later task the latency ruling names. -**Not measured, for want of data on the host that ran it**: a second -field; a night's fan-out across 18 chips; the matcher and the -difference stage at the plan's density; a thirty-day window of real +### The wide-area ramp + +Measured 2026-10-22 on SMDC over the F146 ramp on SCA 8, 312 epochs on +52 fields, all driven by the loop. The compute environment chose +r6i.8xlarge for nearly every job, because one job definition reserves +the difference stage's 24 GiB for every stage. + +| Stage | Jobs | Queue wait, min / median / max | In container, min / median / max | Peak memory | Output, median per job | +|---|---|---|---|---|---| +| `intake` | 312 | 54 / 144 / 158 s | 17.1 / 20.8 / 24.6 s | 910 MiB | 195 MB | +| `reference`, 6 frames | 52 | 5.0 / 6.5 / 9.2 s | 77 / 98 / 102 s | 2,831 MiB | 596 MB | +| `difference` | 312 | 7 / 154 / 228 s | 206 / 246 / 327 s | 16,125 MiB | 2.55 GB | +| `associate` | 312 | 4.6 / 11.5 / 191 s | 2.4 / 3.5 / 11.8 s | 238 MiB | a few KB | +| `packet` | 312 | 3.0 / 48 / 189 s | 13.0 / 30.2 / 60.5 s | 867 MiB | 26 MB | + +- **The drain.** First intake submitted to last packet produced, 42.3 + minutes. The stages' spans, first launched to last ended: intake 3.5 + minutes, reference 1.9, difference 9.0, associate 23.8, packet 29.7. + At most 290 containers ran at once, the differences. +- **The chains.** Each field's six associates took 20 minutes end to + end for about 21 seconds of containers: a chain advances one link per + loop pass ([loop](loop)). +- **One intake on Batch**: fetch of the 204 MB file 0.8 s, conversion + 2.7 s, gzip 5.5 s, limiting magnitude 3.5 s, rows 1.0 s, upload and + publish 1.5 s. +- **Kafka.** 43,725 packets in 324 sets, 8.43 GB of Avro, at about 4.3 + s and 31 packets a second per set; the largest message is 1.5% of the + request limit, and every message repeats the 35 KB container header. +- **The checks.** `validate_run.py` took 20.5 minutes over the 324 + epochs, the reader two minutes eight sets at a time, and the consumer + check 32 minutes at 20 GB of memory. +- **The database** is 365 MB after the SCA 8 ramp: `sources` 133 MB, + `forced` 49 MB, `objects` 48 MB. After the 18-SCA ramp it is 396 MB + and `official` holds 3,523 rows. +- **Storage.** The run's prefix holds 1.2 TB after both ramps; the + difference stage's working files are 89% of it, 42 uncompressed FITS + files per epoch. +- **The 18-SCA ramp**: 102 epochs, first intake to last packet 34.0 + minutes, at most 99 containers at once, 11,711 packets, median queue + waits 86 s for intake and 98 s for difference. +- **The null test.** Per epoch the difference stage found 798 to 1,207 + detections before the cap of 500, median 954, with 53 to 119 above + S/N 20. They sit at and around stars, the residual a PSF made for + another simulation leaves; none is variability. SCAs 4 and 5 leave + three to four times the bright residuals of the other SCAs. + +**Not measured, for want of data on the host that ran it**: the matcher +and the difference stage at the plan's density; a thirty-day window of real cadence; completeness against the injected sources; and the rubrat, NED and NED-LVS matches, whose model and catalog copies are in a bucket this account cannot read. sumrat needs no new data, but no stage @@ -795,14 +963,33 @@ computes it yet. - Whether an undo after a later group has moved one of its keys should have a verb. Today it refuses, which is the safety the design asks for, and an explicit group restores. -- A published job of a run that is not live, launched with `expects`, - has its binding refused, exits 1, and Batch reports it failed though - its rows are published. Launching without `expects`, or exiting 0 - when the refusal is the run's liveness, would make the two agree. - The difference stage's rows differ in some columns between CPU - families on the same inputs, by unverified cause. Recording the - instance type beside the image digest would make a re-execution - comparable. + families on the same inputs, by unverified cause: two Intel sizes + agree bit for bit and one AMD execution does not. The instance type + is now on every job row, so a comparison can be filtered; whether a + run should hold to one family is open, and today it does not. +- Whether a reference is per field and filter, as its key is today, or + per field, filter and SCA; and what a field's chain does with an + epoch that arrives after later ones. An SCA landing on a field + another SCA referenced is differenced against that stack and joins + its chain out of order. +- A reference disjoint from the epochs it is subtracted from. Today a + field's reference coadds the same epochs that are then differenced, + which suits a null test and would suppress a real transient. +- The `catalogs` key, (name, version), is the one key without the + dataset. A second footprint's Gaia copy takes its own name today; + the key gaining the dataset, as every other key has, would make it + one product per dataset. +- Producing packets to Kafka from the packet stage's own Batch job, as + its last act, in place of a host process after the fact. It needs a + grant on the Batch job role and a path from the compute environment + to the brokers, neither tested. +- PSFs for the other filters. Only F146's exist on SMDC, so only F146 + has been run. +- **Source injection is the next scientific step.** The chain has the + right shape and functions on a static simulated sky; whether it + finds what is there is the next question, asked by injecting sources + into the same data, before more breadth. - Packets after a replacement stay as published with the old detections in their history; whether a loop should ever rebuild them is open. A campaign reruns from the replaced epoch. diff --git a/system/index.md b/system/index.md index b61ad84..1e8015f 100644 --- a/system/index.md +++ b/system/index.md @@ -17,22 +17,23 @@ repository's history. The rebuild starts from this page. | Document | Status | Scope | |---|---|---| | [`specification.md`](specification) | DRAFT | Purpose and outcome; the pipelines and their stages; the stage contract; runs, the three output states, promotion, attempts and deletion; tools; the three repositories and their boundary; releases; the manifest edges; constraints; sequencing; what is not yet decided | -| [`decisions.md`](decisions) | DRAFT | The team's rulings, one dated line each with its author: Ben's rulings of 2026-10-20 and 2026-10-21 on the foundation, its SMDC step and the night through the chain (latency later, the PSF kind, the read role, the Batch retry), his rulings of 2026-09-27, his rulings of 2026-10-06 on weekly maintenance on SMDC, earlier rulings, and the rulings carried from the build pending team review | -| [`foundation.md`](foundation) | DRAFT | The foundation and the thin chain: runs, jobs, recorded inputs, scientific keys, official bindings and change groups, publish-last; the PSF key's kind; the decisions with their reasons; the verbs, the `file://` and `s3://` prefixes and a job on Batch with its retry rule; the thin chain, with the `import`, `associate` and `packet` stages and the reader; the eight queries and the team's spatial shapes; the procedure proof; the SMDC procedure and the night run on it (cutover, live loop, campaign, one group making it official, a mid-night replacement, undo); the ten validation checks a night meets and their outcomes; the night's measurements; what is not yet decided | -| [`stage-contract.md`](stage-contract) | DRAFT | The `rapidpipe` package and its dependency direction; a stage as `run(job)` and its `JobContext`; settings from the run's body; the manifest; inputs told apart by key, the PSF kind among them; running a job locally, again or on Batch, with its image digest and peak memory; exit codes and the Batch retry of exit 75; `admit` and `finalize`, still on the older contract | +| [`decisions.md`](decisions) | DRAFT | The team's rulings, one dated line each with its author: Ben's rulings of 2026-10-21 evening (the instance type recorded with no family pinned; the exercised HLWAS pipeline as the plan in force), his rulings of 2026-10-20 and 2026-10-21 on the foundation, its SMDC step and the night through the chain (latency later, the PSF kind, the read role, the Batch retry), his rulings of 2026-09-27, his rulings of 2026-10-06 on weekly maintenance on SMDC, earlier rulings, and the rulings carried from the build pending team review | +| [`foundation.md`](foundation) | DRAFT | The foundation and the thin chain: runs, jobs, recorded inputs, scientific keys, official bindings and change groups, publish-last; the PSF key's kind; the decisions with their reasons; the verbs, the `file://` and `s3://` prefixes and a job on Batch with its retry rule and instance type; the thin chain, with the `intake`, `import`, `associate` and `packet` stages, the reader and the publication to Kafka with its consumer check; the eight queries and the team's spatial shapes; the procedure proof; the SMDC procedure, the night run on it (cutover, live loop, campaign, one group making it official, a mid-night replacement, undo) and the HLWAS run from SOC L2 files to the topic (the live run beside the GBTDS one, one pointing, the loop, the ramp to 426 epochs); the ten validation checks, the script that runs six of them and their outcomes; the night's and the ramp's measurements; what is not yet decided, source injection as the next step among it | +| [`stage-contract.md`](stage-contract) | DRAFT | The `rapidpipe` package and its dependency direction; a stage as `run(job)` and its `JobContext`; settings from the run's body; the manifest; inputs told apart by key, the PSF kind among them; running a job locally, again or on Batch, with its image digest, instance type and peak memory; exit codes, a refusal because the run is not live exiting 0, and the Batch retry of exit 75; `admit` and `finalize`, still on the older contract | | [`reference.md`](reference) | DRAFT | The `reference` stage on `run(job)`: its scope and `l2files` inputs, the awaicgen coadd and SExtractor catalog in `dev`'s order, the header stamp, the three declared tables and the drawn `rfid`, its settings, and what it measured | | [`difference.md`](difference) | DRAFT | The `difference` stage on `run(job)`: its scope and four inputs with the two PSF kinds, the ZOGY, SFFT and naive steps in `dev`'s order, the declared `diffimages` row and source set with the drawn `pid`, one differencer published, its settings and determinism, and what it measured against `dev` | +| [`intake.md`](intake) | DRAFT | The `intake` stage on `run(job)`: one SOC L2 ASDF file under the run's source root, the conversion ported from `dev`, the declared `exposures` and `l2files` rows with the packed `expid` and the shared exposures key, its settings, and what the ramp measured | | [`load.md`](load) | DRAFT | The `load` stage as ported from `dev`: its inputs, the child tables, what lands in `sources`, the source-set result set, its settings and exit codes; the `psf` registration block | | [`maintain.md`](maintain) | DRAFT | The `maintain` stage: the once-per-date CLUSTER and ANALYZE of a `sources` child table moved out of `load`, its `detector-date` unit, its manifest and exit codes | | [`crossmatch.md`](crossmatch) | DRAFT | The `crossmatch` stage as ported from `dev`: one field's two passes, the catalog a pass reads (base plus delta), the per-field tables and their set-scoped keys, what lands in `astroobjects` and `merges`, the association set, its settings and exit codes | | [`statistics.md`](statistics) | DRAFT | The `statistics` stage as ported from `dev`: per-object position and flux statistics over an association set's base-plus-delta membership, what lands in `astroobjectsmeta`, the statistics-set result set, its settings and exit codes | | [`finalize.md`](finalize) | DRAFT | The `finalize` stage as ported from `dev`'s post-processing pipeline: the chain order, the manifest it republishes as new instances, the header stamp, its settings and exit codes | -| [`alerts.md`](alerts) | DRAFT | The `alerts` stage as ported from `dev`'s alert production: the input-set manifest, what it reads from the source, association and statistics sets, the container and the alert outbox, its registration, settings and exit codes | +| [`alerts.md`](alerts) | DRAFT | The retired `alerts` stage as ported from `dev`'s alert production: the input-set manifest, what it reads from the source, association and statistics sets, the container and the alert outbox, its registration, settings and exit codes; its Kafka section points at the foundation's publication | | [`prune.md`](prune) | DRAFT | The `prune` stage: the not-best merge exclusion ported from `dev`'s `pruneNotBestMerges`, its `pruned-set` result set and `prunedmerges` rows, the run model's own-run exclusion clause, its settings and exit codes | | [`photometry.md`](photometry) | DRAFT | Forced photometry: the alert-driven measurement inside the `packet` stage, and `dev`'s standalone `forcedPhotometryForField.py`, not ported | | [`export.md`](export) | DRAFT | The `export` stage as ported from `dev`'s `generateSourceHATSCatalog.py`: the named source sets it reads, the CSV dump and the `hats-import` build, the `catalog-export` bundle and registration, its settings and exit codes; the light-curve catalog is not ported | | [`tool.md`](tool) | DRAFT | The command-line tool `rapidpipe`, every subcommand and its flags, the operations mapped from the specification's Tools sentence onto subcommands, the input-set composer, the tool's own exit codes, personal submission from a workstation, and where it runs | -| [`loop.md`](loop) | DRAFT | The processing-date loop: the operations-registry trigger, the venue and spec it runs from, the production run and stage chain per date, base-catalog binding across dates, promotion, its own records, and release binding | +| [`loop.md`](loop) | DRAFT | The loop, `rapidpipe loop`: the selection as arguments, one pass's order from intake to Kafka, pending, failed and drained, the per-field chains, its exit codes and what it measured; it replaces the run model's processing-date loop | | [`operations.md`](operations) | DRAFT | Operations proposals the team has not yet ruled on: what they assume about the mission, three shapes of the run model and the recommended named stream of batch runs, replacement scope and the chain switch, batch cadence against the latency requirement, a proposed production check policy, reference eligibility and selection, staged inputs of a production run, capacity lanes | | [`observability.md`](observability) | DRAFT | The `rapidpipe` log line and where it goes, the record of an attempt with and without CloudWatch, monitoring with `rapidpipe` alone, queue, execution and orchestration time per Batch job, profiling a stage, and today's job-duration baseline against a 30-minute target | @@ -45,6 +46,7 @@ foundation stage-contract reference difference +intake load maintain crossmatch diff --git a/system/intake.md b/system/intake.md new file mode 100644 index 0000000..8c889d4 --- /dev/null +++ b/system/intake.md @@ -0,0 +1,131 @@ +# The intake stage + +**Status: DRAFT** + +The `intake` stage turns one SOC L2 file, a calibrated Roman image in +ASDF, into the L2 product the rest of the chain reads: a gzipped FITS +file in `dev`'s layout, one `l2files` row and one `exposures` row. It is +where a run's data enter the record, so a run that starts from intake +owns every byte from the raw file to the packet. The +[foundation](foundation) page states the records it works within, and +the [stage contract](stage-contract) page what a stage is. + +The stage is `rapidpipe/stages/intake.py` on the pipeline repository's +`rebuild` branch. Its conversion, `rapidpipe/science/intake.py`, is +ported from `dev`'s `pipeline/ingestL2Files.py` with `dev`'s logic +unchanged. + +## Scope and inputs + +A job's scope is one file of one dataset: `{"dataset": ..., "file": +}`, where the stem is the SOC file name without its +`_cal.asdf` suffix. The file itself is read from the run's source +root, `[intake] source_root`, a URL prefix given in the run's settings +overlay at `run create` and never in the shipped settings. A public +source root is read anonymously when `[intake] unsigned` is true; a +signed read is not tried after an anonymous one fails. + +The job names one input by key: the `psfs` set holding the science PSF +of the file's filter and SCA, used for the limiting magnitude. The +`_wcs.asdf` sidecar SOC delivers beside each file is not read, since +the calibrated file carries the same gWCS, fit results and metadata. + +## What it runs + +1. Fetch `_cal.asdf` through `prefix.get`. +2. Parse the stem's program, plan, pass, segment, observation, visit, + exposure, SCA and filter, and check them against the file's own + observation metadata and detector. A contradiction fails the job + before any manifest. +3. Convert with `dev`'s `build_fits_file`: a 16-HDU FITS whose primary + header holds every scalar of the ASDF metadata as a `HIERARCH` card; + the science array in DN with a degree-5 SIP distortion fitted to the + gWCS, every coefficient to fifth order present; the other arrays + scaled by their exposure-time exponent, half-precision arrays + widened; and the tables as binary tables. The uncompressed file is + made in a temporary directory and only the gzipped one is kept. +4. Gzip at `[intake] gzip_level`. +5. Measure the limiting magnitude with the science PSF, as `import` + does. +6. Derive the image centre, corners, tessellation field, overlapping + fields and HEALPix indexes, as `import` does. +7. Write the rows and the manifest. + +The fitted SIP reproduces the gWCS to well under the fit's own 0.1 +pixel bound: on a real file, to below 1e-5 arcsec at the corners and +the centre. The difference stage reformats the science image itself, +so intake does not. + +## Declared outputs + +| Output | Key | Notes | +|---|---|---| +| the gzipped FITS | | under the attempt's prefix; the `l2files` row's `filename` is its URL | +| `l2files` | dataset, expid, sca, fid | the file's footprint, field, overlapping fields, HEALPix indexes, WCS columns and limiting magnitude | +| `exposures` | dataset, expid | one per exposure | + +The manifest records the file's provenance: its observation +identifiers, romancal's version and the summary of its Gaia WCS fit +(status, geometry, matches, rms and mean absolute error, rotation), +with the conversion's bytes and step times. + +**The `expid`.** `dev` draws an `expid` from a sequence keyed by the +observation date. In the foundation the key is the producer's, and a +SOC exposure's own identifier, 23 digits, fits no `integer` column, so +intake packs the identifiers that vary within a program and plan: +`pass * 10**6 + observation * 10**4 + visit * 10**2 + exposure`, each of +observation, visit and exposure below 100. The 18 SCAs of one exposure +share it, as they share `dev`'s one `Exposures` row. + +**The shared `exposures` key.** Because the 18 SCAs share an +`exposures` key, only one job may expect it with no open binding. The +loop has the first intake of an exposure expect it and every other +intake of that exposure leave it out of its expects. Every intake still +writes the row, so 17 of each exposure's 18 rows are unbound +candidates. + +The `SWVERS` keyword is `job `: the job names its run, and the run +its code revision. + +## Settings + +The run's `[intake]` table, with `dev`'s hard-coded values as keys: + +| Key | Value | `dev` | +|---|---|---| +| `source_root` | the run's overlay only | | +| `unsigned` | false | | +| `sip_degree` | 5 | `SIPDISTORTIONDEGREE`'s default | +| `sip_max_pix_error` | 0.1 | the `to_fits_sip` argument | +| `gzip_level` | 1 | `L2GZIPLEVEL`'s default | +| `program_id` | -1 | the `SOCSIMS` program id | +| `[intake.nominal_zptmag]` | `dev`'s fallback zeropoints | used only when the file has no photometry metadata | +| `limmag_nsigma`, `limmag_clip_sigma` | 5.0, 3.0 | as `[import]` has them | + +## What it measured + +On one 204 MB F146 file, in a Batch container: the anonymous fetch +took 0.8 s, the conversion 2.7 s to a 421 MB FITS, gzip at level 1 5.5 +s to 195 MB, the limiting magnitude 3.5 s, the rows 1.0 s, upload and +publish 1.5 s; the stage peaked at 906 MiB. On a host whose working +directory is network storage, conversion and gzip took about 4 s each. + +Over the F146 ramp on SCA 8, 312 intakes ran in 17.1 to 24.6 s each, +median 20.8 s, at 910 MiB, writing 60.9 GB; their median queue wait was +144 s, because the compute environment scales up from nothing and every +job reserves the difference stage's 24 GiB. The 426 intakes of the run +all published on their first attempt. + +## Not decided here + +- Gzip is single-threaded and is the stage's largest step on Batch; a + parallel compressor, or no compression, since the difference stage + unpacks the file, would roughly halve the stage. +- Intake peaks at under a twentieth of what its job definition + reserves. A smaller reservation for intake would pack many more per + instance, and several files per job would cut the queue wait that + dominates it. +- Whether an intake that does not expect the `exposures` key should + write the row at all. +- The `l2files` row and the PSF lookup are built by both `import` and + `intake`; one helper would hold them once. diff --git a/system/loop.md b/system/loop.md index 5b60a96..8d39a06 100644 --- a/system/loop.md +++ b/system/loop.md @@ -1,411 +1,159 @@ -# The processing-date loop +# The loop **Status: DRAFT** -Parts of this page describe the run model that the [foundation](foundation) -page replaced: units, attempts, custody, promotion, checks and releases. -Where the two differ, the foundation page governs. +`rapidpipe loop` carries a selection of a live run's data from the +source root to the Kafka topic. Each pass admits what is new, moves +every ready epoch one stage forward, and ends by publishing what became +official. It is one verb run under a timeout from a host, not a service +and not a scheduler of its own. The [foundation](foundation) page +states the records it reads and writes, and the +[intake](intake) page the stage it starts with. -`rapidpipe loop run` runs regular operations from one scheduled trigger; -it is not a continuously running service. The -[specification](specification)'s Sequencing item 7 states the requirement. +This page replaces the processing-date loop of the retired run model, +with its operations-registry trigger, loop spec, per-date runs and +promotion. Where an older page still points at that loop's discovery, +classification or commands, this page governs. ## In plain terms -A spec names a schedule, a release and processing dates. The loop creates -one production run per date and walks it through admission, differencing, -loading, crossmatch, statistics, pruning and alerts, in order. Each -field's crossmatch uses the previous date's association set as its base, -so objects accumulate across dates as they do in `dev`'s date-by-date -operation. Once every unit completes, the run is promoted under the -spec's check policy. A refusal is recorded without failing the date, and -the loop moves to the next date. A separate table records one row per -schedule and date. +The loop looks at the run's source root and at the database, decides +what can run now, submits it to Batch without waiting, and goes to +sleep. Two minutes later it looks again. A file nobody has taken in +gets an intake job; a field with all its images taken in gets a +reference; an image with a reference gets a difference; and each +field's association and packets advance one epoch at a time, oldest +first. At the end of every pass, whatever packet sets became official +go to the topic. When a pass finds nothing new, nothing running and +nothing to submit, the selection is drained and the loop exits. -## The scheduler +## The selection -The trigger is the `processing-date-loop` row in `rapid_systems`' -operations registry, `cloudformation/operations.tsv`. The registry is -the estate's single scheduling mechanism: it already drives `pin-sweep` -and other recurring fleet operations through an SSM Maintenance Window -and an Automation runbook per row. The loop row has trigger -`window:at()`, target `Name=rapid-admin`, runbook -`rapid-op-processing-date-loop-runbook`, concurrency 1, error cutoff 1, -owner `rapid-alerts`, freshness `P7D` and freshness actions `quiet`. -The runbook's shell step sets `executionTimeout` to 21600 seconds, -about two control dates at roughly 4.5 hours each, so SSM kills a stalled -run before it runs past the window. - -One Maintenance Window firing runs the whole loop over every date the -spec names. The loop walks the dates. It has no EventBridge rule or -Batch driver job of its own: the registry's `event:` trigger class is -reactive only, never an orchestrator, and a separate driver would -duplicate the registry's scheduling of unattended fleet operations. - -The proof row uses a one-shot `at()`, the registry's deploy-phase shape -that `postgres-swap` uses as an inert placeholder. After the proof, the -row keeps a far-future `at()` instead of a daily `cron()`. A daily -trigger over the control inputs would re-process the same image into -the trial database every day; there are no real deliveries yet to -advance against. The steady-state cadence will be set once they exist -(Not decided here, below). - -## Venue - -The operation runs as root under SSM on `rapid-admin`, from a checkout -of the release tag. Every registry row already runs on `rapid-admin`. -The workstation idle-stops fifteen minutes after its last session, -which would interrupt a loop polling Batch for hours. - -`rapid-admin-instance-role` already holds `batch:SubmitJob`/`DescribeJobs` -on the `rapid-*` definitions and reads the rebuild database secret. -Input-set writes for its own runs are granted through a new parameter, -`LoopLauncherRoleNames`, rather than the [tool](tool) page's -`WorkstationSubmitterRoleNames`. The role already carries 22 of the 25 -policies an instance role can hold, so the loop receives only -`ScratchSubmitter`, without the workstation role's fuller set. - -Like every other registry row, the runbook embeds its operation script, -`op-processing-date-loop.sh`. The script reads the spec's S3 location -from the SSM parameter `/rapid/rebuild/loop/spec` and fetches it. It -clones `rapid` at the named tag into `/var/lib/rapid/loop/rapid-` -and refuses if `git describe --tags --exact-match` in that checkout -does not equal the tag. It then exports the launcher's environment -(the pooler, rebuild database, queue and job-definition names, no -secrets) and runs `rapidpipe.cli.main loop run --spec `, teeing -the log to `/var/log/rapid/processing-date-loop.log`. Ben's sentence -for it: the scheduled operation reads the loop spec named in one SSM -parameter, checks out the release the spec names, and runs the loop -over the spec's dates. - -## Release binding - -The spec names the release; the operation checks out that tag and each -date's run records it. The run's code and the image its units execute -therefore use the same tag, never a branch head. The release is the -first tag cut at a rebuild head carrying the loop's code and migration, -allocated by `next_tag` from the laptop using the -[foundation](foundation) page's recipe. - -## The loop spec - -The spec is a TOML document read from S3 or a local path: - -```toml -[loop] -schedule = "processing-date-loop" -release = "rebuild-v0.3" -kind = "production" -owner = "loop/processing-date-loop" -lane = "loop" -check_policy = "rebuild-trial@1" -max_attempts = 3 -inbox = "s3:///rapidpipe-firstrun/ops4/inbox/" -difference_template = "control/20260923/inputs" -admit_settings = "control/20260923/settings/admit-socsim.toml" -difference_settings = "control/20260923/settings/difference-gain1-imgnoise.toml" +``` +rapidpipe loop --run --filters F146 --scas 8 --pointings all \ + --job-definition [--max-intake 100] [--interval 120] [--once] ``` -This is `ops4-stream`, at -`s3:///rapidpipe-firstrun/ops4/loop/ops4-stream.toml`, -release the newly cut tag, policy `rebuild-trial@1`. - -A spec needs `inbox` or `[[dates]]`; otherwise `loop run` refuses it -with `LoopSpecError`, exit 64. The explicit `[[dates]]` form remains -available for proofs and backfills selected by `loop run --date D`: -one entry per processing date, with one `[[dates.detector_images]]` per -delivery carrying its own `admit_settings`, `difference_template` and -`difference_settings`. - -For explicit dates, `loop run` processes every date whose `loop_dates` -row is absent or `open`, in spec order. Inbox discovery and batch order -are described under Discovery and batches; retries and exit codes are -under Concurrency and recovery. A later batch's crossmatch binds the -earlier batch's association set for its field, just as a later date's -does (Base catalog, below). - -## Discovery and batches - -With `inbox` set, each firing discovers deliveries at that location -before processing anything listed under `[[dates]]`. - -### Inbox layout - -Deliveries land at `///manifest.json`. -The date directory states the processing date. The mission's interface -for that is undefined ([operations](operations), "What this page assumes -about the mission"), and the socsim fixture has only one exposure, so -its FITS observation date cannot separate proof dates. Stating the date -in the staging layout is the stream's smallest assumption. -`` is the `-scaNN` prefix `admit` already reads; -its manifest gives the detector unit id. Inbox keys that do not match -`^/(\d{4}-\d{2}-\d{2})/([^/]+)/manifest\.json$` are ignored and -counted in the firing's log line. - -Discovered manifests carry no stage inputs. Instead, `[loop]` supplies -stream-level inputs for every discovered delivery: `difference_template` -is required when `inbox` is set; `admit_settings` and -`difference_settings` are optional. One template for all deliveries is -a proof-scale assumption. Resolving a reference per field for a real -stream remains open on the [operations](operations) page. - -### What a firing does - -Under the schedule's advisory lock, `loop run` on an inbox spec: - -1. Resumes the schedule's `open` and reopenable `failed` rows in - `(processing_date, batch)` order, reading deliveries from - `loop_deliveries` rather than the spec. A non-reopenable `failed` - row stops it. -2. Discovers new manifests (Classification, below) and creates every - new batch, oldest processing date first, freezing membership as - each batch is created. -3. Processes the new batches in that order. - -If nothing is resumed or discovered, the firing prints `schedule -: nothing to discover` and exits 0 without writing a row, run or -object. `--date D` selects only `[[dates]]` entries. A spec with -`[[dates]]` and no `inbox` keeps its existing behaviour: it bypasses -discovery because a person has named the deliveries. - -### Classification - -Discovery lists the inbox and keeps manifest keys not yet recorded in -`loop_deliveries` for the schedule. A recorded location is never read -again, whatever its state. In key order, each new manifest is read and -classified against the schedule's admission history: rows already -`batched` plus whatever this firing has classified before it. The -delivered image's identity is `(exposure, detector, -version)`; a checksum is the primary file's `sha256`. - -| Manifest | Outcome | -|---|---| -| Not a delivery manifest, or missing a key field or checksum | Quarantined, reason `malformed` | -| Same exposure, detector, version and checksum as a batched delivery | Refused, reason `identical re-delivery` | -| Same exposure, detector and version as a batched delivery, different checksum | Quarantined, reason `checksum conflict` | -| Same exposure and detector, a newer version, while an earlier version is batched | Deferred, reason `corrected delivery awaits a correction run` | -| None of the above | Batched | - -Refused, quarantined and deferred manifests create no batch or run. -Each classification still creates one `loop_deliveries` row, which -`loop show` prints after the schedule's batch rows. For an inbox spec, -`loop plan` performs and prints the same classification without writing. - -### One transaction per batch - -Discovery commits in two stages under the same lock. First, it inserts -and commits all refused, quarantined and deferred manifests in one -transaction, before any batch exists. Even a firing with nothing -admissible leaves a full record. - -Then, for each new date, oldest first, the loop creates a run and inserts -its `loop_dates` row and `batched` `loop_deliveries` rows. The date row -is `open`; its batch number is one past the date's highest existing -batch, or 1 if none exists. The loop commits before moving to the next -date, freezing the batch's membership. A manifest arriving afterward -waits for the next firing. Processing begins only after all of the -firing's batches have committed. - -A crash after a batch commits leaves its `loop_dates` row `open`; the -next firing resumes it from the database against the same run. A crash -before commit leaves nothing behind, so the next firing rediscovers -the unrecorded manifests. - -## Per date - -Each processing date becomes one production run through the CLI's -`run create --release` path. Owner and lane come from the spec. Purpose -is `"processing date (schedule )"`, `input_selection_ref` is the -spec's location, `check_policy_ref` is its policy, and -`max_attempts_per_unit` is its attempt limit. `selected_stages` holds -the loop's chain: admit, register, difference, finalize, register, load, -maintain, crossmatch, statistics, prune, alerts. The chain has two -`register` steps, not three: `finalize`'s contract never registers the -raw difference instance, and promotion refuses two candidates for the -same kind and logical key. - -For each detector image, the tool's stages run in order: -admit(delivery) → register → difference (input set composed from the -template) → finalize → register(finalize's output) → load(finalize's -output). Next comes `maintain`, with a `detector-date` unit id of -`/SCA` derived from the load attempt's child-table name. -For each distinct `field` in that table, the chain continues: -crossmatch (input set containing the load manifest's source set and -the base association set, below) → statistics (crossmatch's output) → -prune (crossmatch's output). - -After each field's prune, the loop reads the selected prune attempt's -manifest. It must name exactly one pruned set whose base is the -association set that date's crossmatch produced for the field; -otherwise the date fails. Each detector image's alerts input set names -the source set, followed, for each field it touches, by the association -set, its statistics set and that pruned set, in that order (the -[alerts](alerts) page's `pruned-set` binding). The date record gains -`pruned_sets {field: instance}` (Records, below). Submission and polling -use `submit_unit` and `reconcile`, as `run start` does for one unit at -a time. - -## Base catalog - -A field's base catalog comes from the newest earlier `loop_dates` row -in the same schedule that is `complete` and has an association set for -that field. Rows are ordered by `(processing_date, batch)`; the bound -instance is the association set produced by the selected crossmatch -attempt for the field's unit in that row's run. If no earlier complete -row has one, the field has no base, as on a schedule's first date or -for a field first seen on a later date. A later batch of the same date -extends the earlier batch's association sets. - -The search skips failed and open dates and steps back to the most -recent complete row, so a date's failure does not stall base-catalog -binding for later dates. Promotion is not required: association sets -bind by instance, independently of current custody. `prune`'s not-best -exclusion keeps an inferior instance out of the chain a later date -reads. Each field's record stores `base_promoted`, whether the date -supplying its base had been promoted at bind time. - -Field discovery and base selection both obey the [foundation](foundation) -page's cross-run reading rule. Field discovery reads a source set only -after the rule passes; an unreadable own source set fails the date. -A candidate base that fails the rule (another run's scratch set, or -one from an unselected attempt) is skipped. The search continues to -the next earlier complete row, just as it steps past failed or open -dates. Each skip is recorded in `bases_skipped`: per field, the run, -processing date, instance and reason skipped. - -Crossmatch and alerts input-set manifests are written under -`/runs//inputs///`, the layout used by -the [tool](tool) page's composer. This implements the loop's own -field-level input selection. The tool page's question of which -reference to use among several eligible ones remains open (below). - -A schedule's first date has no base. If field association sets for that -sky are already current, the [foundation](foundation) page's association-set -ancestor rule refuses scheduler promotion until a chain switch to the -earlier sets exists: the before instance must be reachable from the -new set through `base`. A proof or new stream over the same sky -therefore continues the existing schedule with a new inbox prefix and -new exposures, rather than starting a new schedule name. - -## Promotion - -When every unit in a date's run is complete, the loop resolves the -check policy from the spec, then the run, then the default. It checks -the run's candidates and records each result as `scheduler`. -`promote_run` is called only for a team-approved policy with -`auto_promote` true, the same gate used by `run start` ([foundation](foundation), -"Automatic promotion"). No shipped policy permits this. - -Every scheduled date so far has used `rebuild-trial@1`, which leaves -the run a candidate and records -`promotion = candidate; promotion is a person's (policy )` in -`loop_dates`. A refusal under a policy that permits automatic promotion -is recorded as `refused: ` without failing the date. -`finish_run` follows either way, and the date exits 0 regardless of -which text was recorded. - -A candidate date still supplies the next date's base by instance; the -next date's field entry `base_promoted` (Base catalog, above) records `false`. -A person promotes candidates with `rapidpipe run promote`, in date -order. The [foundation](foundation) page's ancestor rule refuses promotion of a -later date's association set while the earlier date's run it depends -on remains a candidate. - -## Concurrency and recovery - -`loop run` holds one PostgreSQL advisory lock for the spec's schedule -throughout its run. A second launcher for the same schedule, such as -a stray retry or overlapping Maintenance Window, exits 75 without -touching any date. - -Within the lock, dates are processed in spec order. A `complete` row is -skipped; an `open` row is resumed (The loop spec, above), skipping -complete units, attaching to running attempts and re-attempting ready -units. A `failed` row is skipped by default. - -For a failed date whose run has a failed or cancelled unit, -`loop run --retry-failed` seeds a replacement run using the [foundation](foundation) -page's `run create --seed --only-failed` recovery. The date row -points to the new run and retains the old id in `record.previous_runs`. -The loop walks the date again, re-running only the seed's non-complete -units. Only a failed date with no failed or cancelled unit resumes the -same run, through the reopen described below. - -`reconcile` marks an attempt `lost` when its Batch job is gone. The -unit then follows the [stage contract](stage-contract) page's retry-safe -recovery: recover a completed-but-unconfirmed write without duplicating -it, or discard an incomplete write and try again within the run's -attempt allowance. A unit with no attempts left fails, failing the date. - -An unreadable input set fails the date with its reason: an absent or -invalid manifest, or a producer run that is deleting or deleted. This -can happen before any unit exists, leaving `--retry-failed` no failed -or cancelled unit to seed. A later `loop run` reopens a failed row -whose run has no failed or cancelled unit and resumes the same run. -The row returns to `open`, its failure moves to -`record.previous_failures`, and the reopen time is appended to -`record.reopened`; `loop plan` shows `action=reopen`. A failed unit -requires `--retry-failed`'s seeded path. A finished run's row is never -reopened because a finished run accepts no new units. - -The date's move to `complete` and its run's `finish_run` commit in one -transaction. A crash cannot leave either a completed date row for an -unfinished run or a finished run without its completed row. - -`loop run` exits 0 when every processed date reaches `complete`, 1 on -the first failed date without starting a later date, 64 for bad -arguments or a refused spec (including a parse failure), and 75 for a -timeout or held lock. The [tool](tool) page gives the full vocabulary. - -## Records - -`loop_dates` holds one row per schedule, date and batch, keyed on -`(schedule, processing_date, batch)`. Migration -`20260926-01-loop-batches.sql` added `batch` and `kind`: a date's first -batch is 1 and each later batch takes the next integer; `kind` is -`batch` or `switch` (the [operations](operations) page's chain switch). -Existing rows kept batch 1 and kind `batch`. - -The table was introduced by `20260924-11-loop-dates.sql`, with schedule, -processing date, run, state (`open`, `complete`, `failed`), started and -ended timestamps, promotion id and a `record` JSON column. It is granted -to `rapid_rebuild_pipeline` under the same guard as the rebuild's other -grants. - -`record` carries the spec's location and release, each unit's selected -attempt and job id, the field list and the base sets each field bound. -Per field it also carries `base_promoted` (Base catalog, above), -`bases_skipped` (bases the reading rule refused before finding an -eligible one) and `pruned_sets` (the pruned-set instance the field's -`prune` wrote and alerts read). It records the alert container's -instance and location, and the promotion id or refusal reason. Inbox -batches add `record.batch` and `record.deliveries`, the batch's -`loop_deliveries` locations. - -`loop_deliveries` (same migration) holds one row per -classified manifest. Its key, `(schedule, location)`, prevents a second -read. Columns record schedule, location, processing date, exposure, -detector, version, checksum, the delivery instance and unit it produced, -state (`batched`, `refused`, `quarantined` or `deferred`), the reason -for any state other than `batched`, batch and discovery time. An index -on `(schedule, exposure, detector, version)` supports classification -lookup. `loop show ` prints the schedule's `loop_dates` rows, -then its `loop_deliveries` rows. - -## Commands - -The full command surface for `loop run`, `loop plan` and `loop show`, -including every flag, is on the [tool](tool) page. +The selection is three arguments: filters, SCAs, and pointings, either +`all` or a list of `VVV_EEEE`, a visit and an exposure. A pointing is +the same (visit, exposure) pair in every pass of a filter; the +observation number is not part of it, since it can change between +passes of one filter. The selection is the operator's instruction for +this invocation and is recorded in every pass's summary. It is not part +of the run's recipe: widening it changes nothing in the run's settings, +and the same run can be drained one pointing, one SCA or one footprint +at a time. + +The loop is for the live run of a survey and dataset. The run's +settings name its source root, and the run holds exactly one open Gaia +catalog binding, which every packet job names. The job definition is an +argument because the run records a code revision, not a job +definition. The loop changes no stage code, so it may run from a newer +checkout than the run's revision while the jobs run the run's image. + +## One pass + +A pass reads the source root's listing and one snapshot of the run's +tables, decides every submission from them in a pure function, submits +each through `job submit`'s path without waiting, and publishes. Its +order: + +1. **intake**: one job per listed file the selection admits that no + published intake of the dataset and no intake job of this run has + taken, at most `--max-intake` per pass. Each names its filter's and + SCA's science PSF. The first intake of an exposure expects the + shared `exposures` key; the others leave it out. +2. **reference**: one job per field, filter and SCA with at least + `[reference.selection] min_frames` official L2 rows, no official + reference and none pending or failed, and only while no intake of + the run is pending, so a reference coadds every epoch the drain + admits rather than the first two to land. +3. **difference**: one job per official L2 row whose field has an + official reference and whose key has no official difference and no + difference job of the run pending or failed. +4. **associate**: per field, one job for the earliest official source + set, by `mjdobs`, that no associate of the run has named, with the + field's open object set as its predecessor. It waits while an + associate of the field is pending, while an earlier selected L2 row + of the field lacks its source set, and while any intake of the run + is pending, since an intake's field is known only once it publishes. +5. **packet**: per field, one job for the earliest associated epoch + without a packet set, once every earlier epoch's packet set is + official, because a packet set reuses its predecessors' forced rows. + It names the reference, the run's Gaia catalog, the science PSF, the + object set, the epoch's source set, the field's earlier packet sets, + and the field's difference images and L2 rows within the stage's + forced window. +6. **Kafka**: `publish` over the run's official, unstamped packet sets + ([foundation](foundation), Publication to Kafka). + +Fields advance independently, so one pass can carry many fields at +different depths, and a packet job for epoch *n* goes out beside the +associate for epoch *n + 1*. + +**Pending and failed.** A job is pending while it is `launched` or +`running`. A failed job is reported and never resubmitted: its key +counts as taken until a person reruns it with `job run --job `, and +the loop then carries on from it. + +**Drained.** A pass is drained when it found no new file, has nothing +pending and submitted nothing. The verb then exits 0. It exits 0 after +one pass with `--once`, 2 after a pass that reports a failed job, and +64 when the run has no single Gaia catalog. Each pass prints one JSON +line: submitted and pending per stage, failed, new, admitted, listed, +packets and sets produced, drained, its time and the selection. The +next pass starts `--interval` seconds after the last one started, or at +once if the pass ran longer. + +## The per-field chain + +A field's object sets form one chain, in `mjdobs` order, one +`associate` per epoch, each naming the last as its predecessor; its +packet sets form a second chain behind it. The loop keeps each chain +chronological by holding a field's association while any earlier epoch +of the field is still upstream. One case it cannot hold: when two SCAs +cover one field, as adjacent pointings do on a grid one SCA wide, the +second SCA's epochs share the first SCA's reference, since the +reference key has no SCA, and join the chain after the epochs already +in it even when they are earlier in time. That is open +([foundation](foundation), Not decided here). + +## What it measured + +On SMDC, 2026-10-22, over the HLWAS simulation's F146 data: + +| Selection | Epochs | Passes | Loop start to drained | Jobs | Failed | +|---|---|---|---|---|---| +| one pointing, SCA 8 | 6 | 18 | 34.7 min | 25 | 0 | +| every pointing, SCA 8 | 312 | 15 | 42.8 min | 1,300 | 0 | +| one pointing, all 18 SCAs | 102 | 18 | 34.6 min | 424 | 0 | + +- **An idle pass** takes 29 to 35 seconds, about 30 of them the listing + of the whole source root, 272,160 keys in 273 pages. +- **Submission** takes 0.17 to 0.3 s a job, two short transactions and + one Batch request; 200 intakes went out in 34 s. +- **A pass that publishes** about 52 packet sets takes 251 to 274 s, + about 4.3 s a set, more than twice the interval. +- **A chain's span is the pass interval times its depth**: a depth-6 + chain took 20 minutes for about 21 seconds of associate containers, + and the same across 52 fields at once. ## Not decided here -- The mission's own inbox layout and manifest fields, once the interface - is defined; the layout above is a proof-scale assumption. -- A per-field difference template or reference selection: the stream - applies one template to every delivery today. -- Parallel dates: today's loop runs dates one after another. -- Lanes or resource profiles dedicated to the loop, beyond the one the - spec names. -- The steady-state cadence, once real deliveries exist to advance - against. -- Publishing the alerts a date's run produces. +- Where the loop runs on a schedule, and at what cadence, once real + deliveries exist. Today a person starts it under a timeout on a host. +- A chain that advances faster than one link per pass, by passing again + as soon as chain work ends or by running `associate` and `packet` in + one Batch job. At GBTDS depth this is the loop's latency. +- Listing only the selection's prefixes instead of the whole source + root, which would cut an idle pass to a few seconds. +- Producing every set of a pass under one connection and one flush, + which would remove most of the publish step's time. +- A Batch job that dies without `job run` recording it stays `running` + in `jobs`, so the loop never drains and runs to its timeout. Recording + the Batch job id on the row would let a pass see it. Not yet seen. +- A job marked `failed` between Batch's retries of exit 75 makes the + loop exit 2 though the retry may succeed. Not yet seen. +- The mission's own delivery layout and manifest, once the interface is + defined; today the source root is a directory of SOC files. +- Lanes or resource profiles dedicated to the loop. diff --git a/system/stage-contract.md b/system/stage-contract.md index 98fff81..ab16e24 100644 --- a/system/stage-contract.md +++ b/system/stage-contract.md @@ -125,7 +125,9 @@ way of running a stage goes through it: `run_job` reads the job and stops at once, touching no file, if it is already published. Otherwise it claims the next attempt, marks the job `running` with its start time and, from `RAPID_IMAGE_DIGEST`, the image -digest the job definition pins (none from a checkout), closes its +digest the job definition pins (none from a checkout) and beside it the +instance type from the instance's metadata service (none when it does +not answer), closes its connection, and calls the stage's `run(job)`. Under an `s3://` outputs root the stage writes into a local working directory, which `put` uploads to the attempt's prefix, manifest last, before publication. @@ -141,8 +143,8 @@ definition. | Code | Meaning | |---|---| -| 0 | The job was published, or was already published | -| 1 | The job was published, but its automatic binding was refused; its rows are an unbound candidate | +| 0 | The job was published, or was already published; or it was published and its automatic binding was refused only because its run is not live, the refusal on its row and on standard error | +| 1 | The job was published, but its automatic binding was refused for another reason, such as an expected binding no longer open; its rows are an unbound candidate | | 64 | Usage: bad arguments, no outputs root, or a stage with no `run(job)` | | 70 | The stage or the publication raised; the job is marked `failed` | | 75 | The database could not be reached after the connection's own retries: a temporary dependency failure | @@ -184,8 +186,9 @@ This contract replaces the run model's stage invocation: the checks before a stage reads, `register` writing rows from manifests, and stages writing catalog rows themselves. The run model's database stages (`register`, `load`, `maintain`, `crossmatch`, `statistics`, -`prune`, `alerts`, `export`) were retired with its schema; `import`, -`associate` and `packet` replace the ones the thin chain needs. +`prune`, `alerts`, `export`) were retired with its schema; `intake`, +`import`, `associate` and `packet` replace the ones the thin chain +needs. ## Not decided here