This page describes the archive layout shipped in v0.1.4. The expiry-based rolling design
discussed under Alternatives is not implemented.
Why the active table is not partitioned
headgate_job is the hot admission table. Enqueue, claim, lease renewal, acknowledgement,
progress, results, uniqueness and fenced writes all use it.
PostgreSQL and MySQL require a partition key to participate in a partitioned table’s unique
keys. Partitioning headgate_job by time or queue would therefore force Headgate to weaken
global job-ID and idempotency guarantees or add the partition key to every identity lookup.
Neither trade-off is acceptable for the admission path.
Headgate instead keeps the hot table unpartitioned and globally unique. After a terminal job’s
ordinary retention expires, an optional archive policy can move its audit body into a separate
cold table.
Why there are many archive tables
Migration 11 creates:headgate_archive_policy, which stores optional archive retention per queue;headgate_job_archive, the logical parent partitioned by store-timeevicted_at_ms;- 84 monthly partitions covering January 2025 through December 2031;
headgate_job_archive_before_2025andheadgate_job_archive_after_2031, which prevent an archive write outside the prepared range from failing.
- workers never need permission to execute DDL;
- no worker races another worker to create a partition;
- PostgreSQL and MySQL install the same deterministic horizon through migrations;
- an archive insert always has a destination, even outside the monthly range.
Configure archive retention
Archive retention is opt-in per queue. Without a policy, the ordinary retention sweep deletes the expired hot row and does not create a cold copy. Rust:Prune a closed month
Rust exposesprune_archive_month("YYYYMM"); Go exposes
PruneArchiveMonth(ctx, "YYYYMM"). The identifier grammar is closed, so caller input cannot
be interpolated as an arbitrary table or partition name.
Pruning refuses the request unless:
- the requested month has ended according to the store clock; and
- every row in that partition satisfies
evicted_at_ms + archive_retention_ms <= store_now.
TRUNCATE TABLE <child> on PostgreSQL or
ALTER TABLE ... TRUNCATE PARTITION on MySQL. A closed month cannot receive a normal
store-time eviction, so a worker cannot race a new row into it after the retention check.
Run pruning through a singleton store duty and alert on a refusal. Do not replace it with an
unbounded DELETE; that would recreate the table bloat and queue-depth-dependent maintenance
this layout exists to avoid.
Current trade-offs
The mixed-retention cost is the most important limitation. Consider an August partition:
When to enable the cold archive
Enable it when terminal jobs must remain available as audit evidence after they leave the hot queue and the retained volume is large enough that bounded monthly removal matters. Leave it disabled when:- ordinary terminal retention already satisfies the audit requirement;
- the installation has low terminal volume;
- another system exports immutable audit events;
- retaining job payloads would create an unnecessary privacy or compliance burden.
Operate the 2031 horizon
The prepared monthly range ends after December 2031. Rows evicted from January 2032 onward remain safe because PostgreSQL routes them toheadgate_job_archive_after_2031 and MySQL uses
the equivalent edge partition. However, that catch-all cannot be passed to the monthly prune
API until a later migration splits it into bounded months.
Before 2032:
- ship an additive migration that creates another monthly horizon;
- split or move any rows already present in the catch-all partition;
- run the PostgreSQL and MySQL archive conformance tests;
- keep the old migration bytes unchanged—published migrations are immutable.
public is the default installation schema. For a schema-isolated installation, replace it
with the explicitly quoted schema passed to the migrator, for example
"billing-jobs".headgate_job_archive_after_2031. Do not depend on the connection’s
search_path; Headgate itself qualifies every internal relation for the same reason.
For MySQL, inspect information_schema.PARTITIONS for the archive table and alert when the
edge partition begins receiving rows.
Expiry-based rolling partitions
A possible future layout would partition the cold archive byexpires_at_ms and provision a
smaller rolling horizon. Records due for deletion in the same month would then share a
partition.
The alternative is not unconditionally simpler. It exchanges catalog noise for recurring DDL,
additional permissions, partition-horizon monitoring and a more complex PostgreSQL/MySQL
maintenance implementation.
Headgate therefore keeps the released eviction-month layout for the v0.1 series. A future
schema revision should adopt expiry-based rolling partitions only if production evidence shows
that mixed retention materially delays pruning. Migration 11 must not be rewritten after
publication; any change must be a tested forward migration.
Boundaries
- Admission, leases, acknowledgements, results, output, progress and tags remain entirely on
headgate_job. - Archive rows are not returned by ordinary job inspection. Any future audit endpoint must be explicit and must keep payloads withheld by default.
- Archive partitioning is an optional retention layer, not Headgate’s dead-letter queue. The
inspectable
archivedjob state remains in the hot lifecycle until ordinary retention expires. - The destructive integration-test branch requires
HG_TEST_ARCHIVE_PRUNE=1and an isolated database. Normal shared-database tests never truncate archive data.