4.7 Ingestion
Ingestion is how farm data reaches us: milking robots, herd software, labs and registries produce files at the farm or in a vendor's cloud, and the ingestion components of farm (its ingestion API, vendor pullers and parser workers, the Sources, Files and Sync domains of its interface) turn them into the canonical data it keeps. Ingestion was a system of its own until 2026-10-30; it is one system with the canonical data because the two are written in the same transactions. It must survive the same file arriving twice, a sync cut halfway, a parser with a bug and a farm with a slow line, without ever losing or duplicating a value. This page follows one sync from the farm to the database and explains where each piece of data is kept.
The farm sync client extracts by itself, and nothing else (decided 2026-11-10). Each integration reads a vendor's software on the farm PC (its database, its export, its API) with the extractor of a strategy, on the schedule, with no action from the farmer once it is set up. There is no folder where a person drops files and no manual upload from the client: data a person hands us by hand is not farm software data, and it would bypass the detection of the source and its version.
- The generic folder reader (
moonitoring.generic_raw) exists only in local and test builds of the client, to try the ingestion path without a vendor; stage and prod builds do not contain it. - An integration appears in the client only when a strategy for that software exists; until the first vendor strategies are built, a stage or prod client has nothing to read and says so.
- Integrations are set up in the sync client, by a signed-in person (decided 2026-11-10) who holds
farm.sources.manageon the farm, with a sign-in at most five minutes old; otherwise the client opens the browser again withprompt=login. The person picks the software from the client's catalogue of supported software (only software with a strategy is listed: the client-side adapter mappings that apply to the farm, global or of its Organization, which the client receives with its sync configuration even before it has a feed), fills the set-up form of that software, runs Test connection, which the client does on the computer, and the integration becomes a card of the client. The client then connects the feed for itself (Sources). - What the client needs to read the software stays on the computer (decided 2026-11-10): a database account or a server password is kept in the Windows Credential Manager of the client and never sent to us.
- Cloud vendor integrations are set up in the client too (decided 2026-11-10), such as the milk recording of AIA, which
farmpulls: the same catalogue and the same fresh sign-in. The client creates the feed and sends the credential once withPUT /feeds/{feed_id}/credential; it goes to Secret Manager, is never returned, and the vendor puller offarmuses it. The client keeps no copy. It offers only the cloud vendors for whichfarmruns a puller (an adapter of sidepullermapped to that channel, listed in its sync configuration), and shows the farm's cloud vendor feeds as read-only cards. - The Organization Dashboard and the Console show the integrations and their problems (feeds, runs, integration gaps) and set up nothing. The Dashboard pauses an integration through its schedule, since schedules are set there (confirmed 2026-11-10).
4.7.1 The Path of a Sync¶
- The farm sync client reads the files of each source, recognizes the feed and its version, compresses each file and computes the hash of its uncompressed content.
- It asks the ingestion API of
farmwhich hashes we already hold, and uploads only the others, through short-lived signed URLs, directly to therawbucket. The API never sits in the path of the bytes, so a large upload costs it nothing. - Once the files are uploaded, the client commits the sync: one transaction records the SyncRun, one RawMetadata per new file and one ParseJob per file to parse.
- A relay publishes the jobs to Pub/Sub, with the feed as ordering key: the files of one feed are parsed in order, all the feeds of all farms in parallel.
- A parser worker reads the raw file, compares it with what we already hold, and writes canonical revisions under one WriteCommit. Values it cannot represent are recorded as skips, never approximated.
Vendor pullers, which fetch data from vendor clouds on a schedule, enter the same path at step 3: they store the file in the bucket and commit it like a farm client.
4.7.2 Four Gates Against Duplicates¶
A file or a record can arrive twice for many reasons: a retried upload, a full snapshot that repeats last week, a message delivered twice. Four gates make each repetition harmless:
| Gate | Where | Stops |
|---|---|---|
| 1 · Known hash | Before upload | A file the feed already delivered is neither uploaded, stored nor parsed again |
| 2 · One active job per file | When a job is created | Two parses of the same file running at once |
| 3 · Job ID with a lease | When a worker takes a job | A message delivered twice, or a worker that died halfway and is replaced |
| 4 · Unchanged record | When writing | A record identical to the last revision produces no new revision |
4.7.3 When Something Fails¶
- A failed job is retried with growing delays. After a set number of attempts it moves to
parse.dead, where it waits for a person; the other feeds keep flowing, and only the stuck feed is held. - Failed jobs and runs are listed in the Console. From there a staff member can ask for a re-parse of one file, one run, or one feed over a period, for example after fixing a parser. A re-parse is a new set of jobs on the same queue, through the same gates, so it only writes what changed.
4.7.4 Where Data Is Kept¶
| What | Where | Why there |
|---|---|---|
Metadata of every sync, file and job: SyncRun, RawMetadata, ParseJob, skip counts |
Cloud SQL, farm database |
Queried and joined constantly; small |
| Canonical data: observations, events, periods, with their revisions | Cloud SQL, farm database, partitioned by farm and month |
What the other systems read, at any write number |
| Raw files, compressed | raw bucket |
Written once, read to parse and re-parse; the only way to rebuild canonical data |
| Row-level skip detail, JSONL next to each raw file | raw bucket |
Evidence for every skipped value, too bulky for the database |
| Datasets for models, Parquet | datasets bucket |
Read by training and BigQuery in place |
Raw Files¶
- Compressed by the client before upload, and stored under the hash of their uncompressed content, so the same data compressed differently is still recognized.
- Kept for as long as any canonical record derives from them; they leave the bucket only through an erasure request.
- Bucket:
moonitoring-<env>-farm-raw, EU dual-region (Milan and Belgium). Raw files cannot be produced again, so they are kept in two regions. - Storage class: written in Standard, moved to Archive by a lifecycle rule after 30 days.
The first 30 days cover parsing and the re-parses that follow a parser fix, which come early. After that a raw file is read only by a rare re-parse, and Archive costs about a tenth of Standard to keep. Archive in Google Cloud answers in milliseconds, like Standard; what it adds is a fee for each read and a minimum of 365 days of storage per file, which an immutable file kept for years always meets.
Why not Autoclass
Autoclass moves files between classes by itself, but charges a monthly fee per thousand files and reaches Archive only after a year without access. Our access pattern is known in advance, so a fixed lifecycle rule is simpler and cheaper.
At our scale the class makes little difference: about 1 MB of compressed raw data per farm per day is 36 GB a year at 100 farms, a few dollars either way. The rule matters as the archive grows over the years.
4.7.5 Capacity¶
Cloud Run, Pub/Sub and Cloud Storage scale far beyond our needs; the limit is the Cloud SQL instance. With prod's current instance (1 vCPU, 3.75 GB, highly available), about 100 farms, a million observations a day, fit comfortably, and up to about 250 to 300 when farm syncs are spread over the night by their schedules. This is an estimate from the model, not a measurement.
Not decided yet: how long canonical data stays in Cloud SQL
Canonical data grows by about 1.1 GB per farm per year, and Cloud SQL storage with high availability costs about twenty times Cloud Storage. Moving partitions older than 6 to 12 months to Parquet files in Cloud Storage would divide that line by 5 to 10, but reads "at number N" on old data would then go through BigQuery. A Cloud SQL disk never shrinks, so this must be decided before the database grows.