git maintenance has a failure channel and no liveness channel — two hosts, two mechanisms, one blind spot #962
Open
opened 2026-07-30 19:31:02 +00:00 by mos-dt-0
·
0 comments
No Branch/Tag Specified
main
remediation/state
feat/rm-02-gate-registry
fix/rm-03-queue-guard
fix/rm-01-reproducible-checkout
remediation/mission-setup
fix/hygiene-inert-format-gate
fix/1019-queue-guard-stdin
feat/mos-ste-writing-standard
fix/1007-suite-hermeticity
fix/991-comment-url-scheme-normalise
feat/push-guard-null-case-verification
mos-comms-live
docs/heartbeat-framework-layering-ms-lead
feat/869-c4-version-coupling
feat/869-c2-install-ordering-guard
feat/869-c5-doctor-activation-check
feat/per-agent-gitea-identity
fix/875-belongs-case-insensitive-slug
fix/ci-queue-wait-404-branch-absent
feat/869-c1-activation-probe
feat/869-c3-broker-supervisor
fix/865-tea-cli-comment-invocation
feat/glpi-skills
fix/860-deflake-mutator-lease-gate
fix/850-detect-platform-port-normalization
fix/856-worktree-deps-preflight
fix/835-pr-review-approve-reject-comment-flag
fix/848-truthful-evidence
fix/812-pr-review-comment
fix/849-recovery-runtime-fixture-race
docs/758-ledger-m5-001-sync
feat/834-tc-server-side-doc
feat/833-constrained-recovery-command
feat/827-gate0-probe
governance/gate0-probe3-amendment
fix/795-codex-pr-diff
fix/795-ci-base-jq
fix/795-ci-base-git
feat/791-pr3-fleet-regen
feat/791-pr2-snapshot-restore
fix/807-glpi-206
fix/808-agent-send-false-sender
feat/791-upgrade-config-protection
feat/790-mosaic-yolo-claudex-pr2
feat/790-mosaic-yolo-claudex
feat/758-v1-v2-migrator
fix/766-exact-fleet-comms
test/758-reconciler-lifecycle-gates
docs/771-kbn101-db-role-split
test/758-example-profile-dispositions
feat/758-shared-role-resolution
feat/mos-logical-identity-fencing
feat/769-kbn100-unified-schema
docs/753-kbn010-threat-gate
feat/758-roster-v2-compiler
feat/756-official-discord-plugin
docs/758-fleet-config-management
fix/mos-option2-qualification-format
docs/issue-758-m0
docs/mos-option2-qualification
mos-comms
feat/tess-interaction-agent
fix/tess-docs-format
next
draft/mosaic-platform-prd
fix/installer-provider-gate-and-local-gateway-redis
release/mosaic-cli-0.0.37
feat/framework-constitution-alpha
fix/git-wrapper-repo-detection
fix/woodpecker-wrapper-legacy-mosaic
fix/t-a292e96f-gitea-pr-metadata
fix/gitea-pr-metadata-login-t-a292e96f
fix/t_a292e96f-pr-metadata-gitea
fix/t_3a368a52-gitea-usc-login
fix/bootstrap-hotfix
fix/populate-known-packages-list
fix/idempotent-init
v0.0.39-alpha
mosaic-v0.0.31
fed-v0.2.0-m2
fed-v0.1.0-m1
mosaic-v0.0.29
mosaic-v0.0.28
mosaic-v0.0.27
mosaic-v0.0.26
mosaic-v0.0.25
mosaic-v0.0.24
v0.2.0
v0.1.0
v0.0.8
v0.0.7
v0.0.6
v0.0.5
v0.0.4
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mosaicstack/stack#962
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Git reports that maintenance ran and failed. Nothing reports that maintenance has not run.
Add a liveness check for git maintenance to the fleet health surface, and drive maintenance from a
scheduler rather than from git's automatic triggers.
Two hosts hit unbounded object growth this week by two different mechanisms. Both were invisible for
the same reason, and the observable we all reached for —
.git/gc.log— cannot distinguish healthyfrom dead.
The gap
git gc --autowrites.git/gc.logwhen it runs and fails, then suppresses further automatic runsfor
gc.logExpiry(default 1 day), re-printing the stale error instead of retrying.There is no counterpart on the success path. Git records no queryable "last successful maintenance"
timestamp. Therefore:
.git/gc.logWe read that absent file on five repositories across two hosts and drew opposite conclusions from
it, correct in some cases and wrong in others, based entirely on what else each reader happened to
know. A file that is written only on failure cannot answer a liveness question.
Specimen A — suppression path
Repository on HOST_A reached ~38 GB across 131 pack files and filled the volume. Work stopped; the
message poller went blind (it fail-closed correctly and reported the blindness, which is the only
reason this surfaced as an incident rather than as a silent channel).
Configuration was pure git defaults on both the failing and the healthy host —
gc.auto,gc.autoPackLimit,gc.autoDetach,maintenance.autoall unset. Defaultgc.autoPackLimitis 50,so 131 packs is 2.6x past the threshold that exists to prevent exactly this.
Most probable mechanism: a disk-pressure spiral. Auto-gc triggered at 50 packs, failed with ENOSPC,
wrote
gc.log, self-suppressed for a day, and repeated while packs climbed and the volume filled.Confirmation is pending and may be recoverable. The operator ran
git gc --prune=now, whichdeletes
gc.logon success — erasing the artifact that would have named the cause. However, whilegc.logexists git copies its contents into the stderr of every subsequent porcelain command. Ona host where git runs under a scheduler with journald capture, hundreds to thousands of copies of
the original error survive the file's deletion. That log search is the outstanding confirmation
step.
Specimen B — never-invoked path, no failure record at all
Repository on HOST_B, measured read-only, no locks taken:
Loose count is ~8.5x past the default
gc.autothreshold, and.git/gc.logis absent — so thesuppression trap cannot be the cause here. Same outcome, different path.
Two independent readings confirm maintenance is not completing:
gc.pruneExpiregrace window. Anysuccessful
gcprune would have removed them.prune-packable=6950— loose but reachable objects that a repack folds in. A completed repacktakes this to ~0.
This falsified the benign reading we considered first, that a healthy
gcwas deliberatelydeclining to touch a large population of young unreachable objects. The population is old.
Instrument caveat worth carrying into any check built from this: the recent 141 MB pack is not
evidence that
gcsucceeded. Fetch writes packs too, and a large pack landing beside 15 undisturbedsiblings is a fetch artifact, not a consolidation — a repack would have folded the siblings and
zeroed
prune-packable, and neither happened. A file's mtime tells you that something wrote it,not what wrote it. Newest-pack-mtime is therefore a weak liveness proxy on repositories with
inbound fetch traffic, and any check using it must say so.
What produces the growth
Worth stating because it changes threshold selection: commits produce loose objects; packs mostly
arrive from fetch — each
git fetch/git pullthat transfers anything writes a pack. Pack counttherefore tracks
inbound traffic x time-since-last-consolidation, not commit frequency. Two hostsrunning an identical 2-minute commit cadence sat at 5 packs and 131 packs respectively, because one
host's pulls were mostly empty.
Consequence: a busy repository crosses the pack threshold quickly with no change in agent behaviour,
and a quiet repository can sit under it for weeks with maintenance equally dead. Pack count alone
is a load metric wearing a health metric's clothes.
Also: the default
gc.autovalue of 6700 is compared against an estimate — git samples.git/objects/17/and multiplies by 256 — not against a count. It fires correctly at thesemagnitudes, but it should not be cited as a count.
Proposed
1. Add a liveness leg to the health surface. The check must answer "when did maintenance last
succeed", not "did it fail". Candidate signals, cheapest first — all read-only, no repository lock:
git count-objects -v→ alert onprune-packablesustained above a small bound, and oncountexceeding
gc.auto. These are consequences of absent maintenance and are independent of why.gc.pruneExpire— a nonzero, growing population is the signature ofprune never completing (Specimen B's one-line signature: 42,406 objects past the grace window
with no failure record is what "no liveness channel" looks like from the outside).
.git/gc.logpresence and content — still worth alerting on; it is diagnostic when present.inode wall with bytes to spare, and it fails closed in the same way with less obvious symptoms.
Do not build the check on newest-pack mtime alone, per the caveat above.
2. Drive maintenance from a scheduler instead of auto-triggers. This closes both gaps with one
change:
and wrapper-mediated may never reach those trigger points, which is a live candidate for Specimen
B. A scheduled run does not depend on which commands agents happen to use.
exit status, which is exactly what git does not provide.
Caveat that must ride with this recommendation:
git maintenance register's default task set(prefetch, commit-graph, loose-objects, incremental-repack) does not prune unreachable objects.
On a Specimen-B repository it would not remove the 42,406 objects past the grace window. A periodic
full
gcis still required for reclamation;git maintenancealone addresses consolidation andliveness reporting, not pruning. Additionally its
prefetchtask writes packs, which adds noise toany mtime-based reading — a further reason not to build the check on mtime.
3. Remediation is not preemptive.
git gctakes a repository lock. On shared checkouts withseveral agents active it will fail whatever is in flight, so it is announce-then-run in a quiet
window, never run-then-announce. Neither specimen was remediated on discovery for this reason;
Specimen A was remediated only because the volume was already full.
Related note for anyone reaching for
git gc --prune=now: defaultgcintentionally retainsunreachable objects younger than
gc.pruneExpireso a concurrent writer cannot lose an objectmid-write.
--prune=nowremoves that protection. It is defensible under disk exhaustion; it shouldnot become the habitual form on a shared checkout, and it destroys
gc.logas a side effect.Why this is filed rather than left as a runbook line
A checkable rule belongs in a check, not in prose. Both incidents were found by an unrelated
question at an unrelated moment — one at a disk wall, one because someone else asked a host to run a
free command. Neither was scheduled. A rule that only fires in the post-mortem is not yet a control.