Skip to content

Progress reporting: read partclone's snapshot, do not consume it - #184

Merged
mastacontrola merged 1 commit into
masterfrom
fix/statusreporter-polling
Sep 18, 2026
Merged

mastacontrola merged 1 commit into
masterfrom
fix/statusreporter-polling

Conversation

@mastacontrola

@mastacontrola mastacontrola commented Sep 18, 2026

Copy link
Copy Markdown
Member

fog.statusreporter treated /tmp/status.fog as a stream. It is a snapshot: FOG's own partclone patch rewrites the file whole — fopen(..., "w") — on every UI tick. Three defects came out of that one misreading, and none of them is visible from the imaging screen.

It truncated the file after every read. partclone rewrites it anyway, so the truncation bought nothing — and a read landing between the truncation and the next tick found the file empty and posted an empty status, which service/progress.php discards in silence (it requires six @-separated fields and says nothing when it does not get them). It also meant nothing else could ever read that file, because this consumed it.

It posted every three seconds whether or not the value had changed. Each post costs the server a host lookup, a task load, an image load and a save, multiplied by every client in a multicast. See the measurement below for how much of that is actually redundant — less than this originally claimed.

[[ -z $mac ]] && continue jumped past the usleep at the bottom of the loop, so a task reaching it span at 100% CPU for its whole duration. funcs.sh recovers the MAC now (FOGProject/fogproject#1767) so nothing reaches it, which is why the shape is pinned rather than left to be rediscovered.

What changes

Read without truncating. Send only a line carrying the six fields the server needs. Post only when the value has changed, with a keepalive so a client that has gone quiet and one that has stalled stay distinguishable in the server's access log — that last part is not hypothetical, forum topic 18247 is a deploy whose whole diagnosis is a 48-second hole between its final progress post and its reboot.

Polling and posting are deliberately separate rates. partclone is asked for a tick a second (-f 1), so reading once a second means the value that goes out is current; posting keeps the old three-second floor, so the server's worst case is what it has always been.

Transport now matches reportToServer(): --max-time, so a wedged connection cannot stall the loop with nothing on screen, and --data-urlencode per field. -Lks is unchanged — -k trusts the certificate a default FOG install presents, and that is a decision about every call in FOS rather than about this one.

Compatibility

The wire format is untouched. FOS is not branched — one init runs against 1.5 and 1.6 alike — and nothing here needs a server that understands anything new.

Verification

tests/checks/status-reporting.sh runs the shipped loop for real against a sandbox fixture, with the timing constants rewritten so the assertions take seconds. Every case was confirmed red against the defect it covers, reintroduced one at a time:

Mutant Case that went red
truncation restored 3. the producer's file is not truncated by the read
changed-value guard removed 4b. an unchanged value is not re-posted
continue restored 8b. an empty MAC does not spin the loop (329 cpu ticks), 9a

The spin is measured from /proc rather than ps -o time=: the loop burns its time in the tail it forks, so ps reports 00:00:00 for a process eating a core, and the assertion passed on the very defect it exists to catch until that was fixed.

Full suite: 23 passed, 0 failed.

Measured on a booted init

Built x64 (build.sh -nfa x64), confirmed the built image carries the change by extracting
bin/fog.statusreporter back out of it, deployed it to a 1.6 lab server and ran a real deploy:
win11-fostest, Single Disk - Resizable + Everything — the same image configuration as forum
topic 18247 — onto an empty 80 GB disk. The client fetched the new init (31,520,852 bytes, the new
build's exact size).

Task reached Complete, pct 100, Post_Stage3.php 200
Fields the server parsed mid-run percent 46.93, bpm 10.44, timeElapsed 00:01:53, timeRemaining 00:02:07, dataCopied 19.594 GiB, dataTotal 40.762 GiB — all six, so the payload is understood and not merely accepted
Run 09:16:30 boot.php → 09:21:57 Post_Stage3, 84 progress posts
Gaps 3s x74, 4s x5, 5s x3, 9s x1

The claim above was too strong, and this run is why. During an active partclone restore the
value genuinely changes every second (-f 1), so the 3-second floor means the new code posts at
the same rate the old one did. 84 posts against roughly 97 for the old code over the same window
is about a 13% reduction, not the large one implied. The saving is real but it lives entirely in
the phases where nothing is moving: one 9-second gap where a partition boundary stalled the value,
and 26 seconds of silence between the last progress post and Post_Stage3 — the post-restore
work — where the old code would have sent about eight identical updates.

That 26-second window is worth looking at twice. It is the same window that, in forum topic 18247,
is a 48-second hole with a reboot at the end of it. The keepalive is 30 seconds and this run did
not reach it, which is the behaviour intended: quiet because there was nothing to say, and the
server's next word from that client was the completion.

The correctness fixes — no truncation of the producer's file, no empty or malformed payloads, a
request timeout, no sleep-skipping continue — are not rate questions and are unaffected by this.

Everything above was also re-verified green after the lab run.

🤖 Generated with Claude Code

fog.statusreporter treated /tmp/status.fog as a stream. It is a snapshot:
FOG's own partclone patch rewrites the file whole, fopen(..., "w"), on
every UI tick. Three defects came out of that one misreading, and none of
them is visible from the imaging screen.

It truncated the file after every read. partclone rewrites it anyway, so
the truncation bought nothing -- and a read landing between the truncation
and the next tick found the file EMPTY and posted an empty status, which
service/progress.php discards in silence because it requires six
@-separated fields and says nothing when it does not get them. It also
meant nothing else could ever read that file, because this consumed it.

It posted every three seconds whether or not the value had changed. Each
post costs the server a host lookup, a task load, an image load and a
save, multiplied by every client in a multicast, to re-send a value the
server already held.

And `[[ -z $mac ]] && continue` jumped PAST the usleep at the bottom of
the loop, so a task reaching it span at 100% CPU for its whole duration.
funcs.sh recovers the MAC now (fogproject#1767) so nothing reaches it,
which is why the shape is pinned rather than left to be rediscovered.

Now: read without truncating, send only a line carrying the six fields
the server needs, and post only when the value has changed -- with a
keepalive so a client that has gone quiet and one that has stalled stay
distinguishable in the server's access log. That last part is not
hypothetical; forum topic 18247 is a deploy whose diagnosis is a
48-second hole between its final progress post and its reboot.

Polling and posting are deliberately separate rates. partclone is asked
for a tick a second (-f 1), so reading once a second means the value that
goes out is current; posting keeps the old three-second floor, so the
server's worst case is what it has always been.

Transport now matches reportToServer(): --max-time, so a wedged
connection cannot stall the loop with nothing on screen, and
--data-urlencode per field. -Lks is unchanged; -k trusts the certificate
a default FOG install presents, and that is a decision about every call
in FOS rather than about this one.

The wire format is untouched, so this is safe against every server
version in the field. FOS is not branched -- one init runs against 1.5
and 1.6 alike -- and nothing here needs a server that understands
anything new.

tests/checks/status-reporting.sh runs the shipped loop for real against a
sandbox fixture, with the timing constants rewritten so the assertions
take seconds. Each case was confirmed to fail against the defect it
covers, reintroduced one at a time. The spin is measured from /proc
rather than `ps -o time=`: the loop burns its time in the tail it forks,
so ps reports 00:00:00 for a process eating a core, and the assertion
passed on the very defect it exists to catch until that was fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mastacontrola
mastacontrola merged commit be07982 into master Sep 18, 2026
1 check passed
@mastacontrola
mastacontrola deleted the fix/statusreporter-polling branch September 18, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant