Skip to content

Report what the controller is not being told - #6434

Merged
eriknordmark merged 5 commits into
lf-edge:masterfrom
eriknordmark:deferred-queue-metrics
Sep 8, 2026
Merged

eriknordmark merged 5 commits into
lf-edge:masterfrom
eriknordmark:deferred-queue-metrics

Conversation

@eriknordmark

@eriknordmark eriknordmark commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Description

A device that cannot deliver a report to the controller says nothing about it
today: the controller sees an absence, which is indistinguishable from a device
with nothing to say. This adds that missing account.

Each metrics message now carries the state of the deferred queue -- how many
messages are still waiting for the controller, how long the oldest of them has
been waiting, and how many were given up on since boot, split into the ones the
controller rejected and the ones a later periodic publication supersedes. Only
the two queues carrying controller traffic are counted; the local operator
console queue holds nothing the controller is waiting for.

Alongside it, every controller URL records how its answers came out: accepted,
refused with a status worth offering the payload against again, or rejected
outright. The existing per-URL counters say only whether the controller was
reachable -- an answered 404 counts as a success there -- so a device whose
reports are being turned away looks identical to a healthy one. These three
counters are what separate them.

Reporting three more counters per URL costs bytes in a message that already
runs close to the pubsub size limit, because agents fetching many distinct URLs
-- downloader pulling image blobs -- hold a per-URL entry for each. The
counters are left out of the encoding while zero, which is always the case for
those agents, and the map is now measured before it is published, giving up the
least recently used entries until what remains fits. An oversized publication
is fatal to the publishing agent, so this trades a crash for the loss of the
oldest per-URL detail.

The advertised API capability moves to DEFERRED_QUEUE_METRICS.

PR dependencies

  • Report what a device fails to deliver to the controller eve-api#154 -- MERGED (2026-08-28). Adds deferredQueueMetric,
    the three urlcloudMetric counters and the capability value this PR reports.
    Already satisfied by master, which pins eve-api at
    v0.0.0-20260907084402-08ba01c79328; this PR carries no bump of its own. No
    replace directive anywhere.
  • Bump eve-api so newer fields are not dropped adam#159 -- MERGED (2026-08-29). Not a build dependency, but the
    end-to-end tests here cannot pass without it. Adam re-serializes what a device
    sent using its own copy of the API, and protojson drops what that copy does not
    carry, so against the previous adam every counter added by Removing hard coded count of TSC members #154 read as unset
    however correctly the device reported it -- which presents as an EVE bug.
    Satisfied by the published lfedge/adam:0.0.82, which the third commit here
    selects.

Behaviour also builds on #6304 (merged), which introduced the retriable/rejected
distinction in the deferred queue that these counters report, and the controller
fault injection the tests use.

How to test and validate this PR

Covered by automated tests.

Unit tests, make -C pkg/pillar test: ten new cases across controllerconn
and types covering the queue snapshot (backlog, message age, the two drop
reasons, aggregation across queues), the answer classification, and the size
guard -- a map that fits publishes untouched, one that does not is trimmed
least recently used first until it does, and one that cannot fit at all is
reported rather than published. Each was validated against un-fixed code --
neutering the drop accounting, the message-age inheritance, the age entirely,
the answer classification, the omitted zero counters or the size check makes
the matching test fail on its assertion, and all pass again once restored.

End to end, evetest/tests/controllerfaults, two new cases in
TestControllerFaultsSuite:

  • TestDeferredQueueBacklogReported -- with the controller answering 503 to
    every info message, the metrics must report a growing backlog, the age of the
    oldest held message, and refusals counted against the info endpoint while the
    metrics endpoint keeps counting accepted answers; nothing may be counted as
    given up on, and the queue must drain to empty once the controller accepts
    info again.
  • TestDeferredQueueDropsReported -- with the controller rejecting info
    messages with 404, the drops must be reported as rejections with the time of
    the last one, and none of them counted as superseded.

Both require EVETEST_CONTROLLER_FAULTS=true; they skip without it. Run with:

EVETEST_CONTROLLER_FAULTS=true make evetest NAME=TestControllerFaultsSuite

Validated on amd64/KVM against an image built from this branch: the whole suite
passes, 5 of 5 subtests, with the two new ones at 111.98s and 112.06s. Not run
on arm64.

Note for whoever adds to this suite next: the subtests share one device, and
each one's setup allows deviceApplyConfigTimeout for the previous one's
application to be reported gone, which does not cover finishing a container
shutdown as well as deleting it. A subtest returning while the application it
stopped is still halting therefore fails the next subtest's setup. The two
new tests wait for the application to be reported halted before returning, as
the two oldest ones already did, and
TestInfoDroppedOnRejectionKeepsDeviceReporting is ordered last because by
design it cannot.

Changelog notes

The controller is now told what a device is holding back or has given up on
delivering to it: the number of undelivered messages, how long the oldest has
been waiting, and how many were dropped because the controller rejected them or
because a later report supersedes them. Per-URL counters additionally
distinguish an accepted answer from a refusal worth retrying and from an
outright rejection.

PR Backports

  • 17.0-stable: No, as this reports a newly added API field.
  • 16.0-stable: No, as this reports a newly added API field.
  • 14.5-stable: No, as this reports a newly added API field.
  • 13.4-stable: No, as this reports a newly added API field.

Checklist

  • I've provided a proper description
  • I've added the proper documentation -- the fields are documented in
    eve-api (Removing hard coded count of TSC members #154); there is no pillar-side doc describing reported metrics.
  • I've tested my PR on amd64 device
  • I've tested my PR on arm64 device -- not tested; the change is
    architecture independent.
  • I've written the test verification instructions
  • I've set the proper labels to this PR -- no backports, so no stable
    label.

@codecov

codecov Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.25%. Comparing base (b61dae6) to head (01031c0).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
pkg/pillar/cmd/zedagent/handlemetrics.go 0.00% 16 Missing ⚠️
pkg/pillar/controllerconn/deferred.go 95.34% 1 Missing and 1 partial ⚠️
pkg/pillar/cmd/zedagent/reportinfo.go 0.00% 1 Missing ⚠️
pkg/pillar/controllerconn/send.go 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6434      +/-   ##
==========================================
+ Coverage   25.87%   26.25%   +0.38%     
==========================================
  Files         513      523      +10     
  Lines       93751    95570    +1819     
==========================================
+ Hits        24257    25095     +838     
- Misses      67620    68414     +794     
- Partials     1874     2061     +187     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@eriknordmark
eriknordmark marked this pull request as ready for review August 31, 2026 13:54
// interfaces): agents like downloader/mgmtproxy can otherwise grow
// URLCounters without bound and exceed the pubsub message size limit.
// The actual count may transiently exceed this; see AgentMetrics.
const MaxURLCounters = 100

@milan-zededa milan-zededa Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to lower this -- with the newly added fields, the combined size of 100 entries (for e.g. app image blobs) could now exceed the 64KB pubsub limit.
It also might make sense to add json:",omitempty" tag to error counters, which are likely to be at zero for most entries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and the margin is tighter than 64KB: sendUpdate base64-encodes the value before measuring it, so the 65535-byte socket message leaves about 48KB for the JSON. The peak is also 115 entries rather than 100, since makeRoomForNewURL only evicts once the count is past MaxURLCounters+urlCountersWiggle.

Measured max entries that still fit, for a downloader-shaped entry (byte counters set, answer counters zero, one interface):

URL length master with the new fields with omitempty
100 144 121 144
128 133 113 133
160 122 105 122
200 111 97 111
256 99 87 99

So the new fields moved the overflow point from ~165-character URLs down to ~120, and a registry blob URL is 126 characters just for https://<registry>/v2/<org>/<repo>/blobs/sha256:<64 hex>.

I took the omitempty suggestion and applied it to all three, DeliveredMsgCount included: they are written only by RecordAnswer, which is called only from controllerconn/send.go, so for downloader and mgmtproxy -- the two that can actually fill URLCounters -- all three stay zero forever. That restores the master column exactly, so I would rather leave MaxURLCounters at 100 than give up the entries. Nothing changes on the wire to the controller either, since the proto3 scalars already skip zeroes.

The second commit goes at what I think is the more durable problem: an entry cap cannot bound a byte budget while the URL keys are unbounded (metricsURL is the full DownloadURL, query string included). AgentMetrics.Publish now checks the encoded size and drops least-recently-used entries until the value fits, and returns an error rather than publishing if it still does not -- today an overflow is a Fatalf in the publishing agent, which is the failure your original commit was chasing.

Happy to lower MaxURLCounters too if you would rather have the margin.

@milan-zededa milan-zededa Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erik It might make sense to backport b5da0e0 to 16.0 and 17.0, where we have a customer seeing this pubsub fatal caused by large url metrics.
As you pointed out, my fix (#6410, #6409) might not be sufficient.

@milan-zededa milan-zededa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but there are merge conflicts to address.

eriknordmark and others added 5 commits September 7, 2026 17:12
Each metrics message now carries the state of the deferred queue: how many
messages are still waiting for the controller, how long the oldest of them
has been waiting, and how many were given up on since boot, split into the
ones the controller rejected and the ones a later periodic publication
supersedes. Only the two queues carrying controller traffic are counted;
the local operator console queue holds nothing the controller is waiting
for. The advertised API capability moves to DEFERRED_QUEUE_METRICS.

Alongside it, every controller URL now records how its answers came out -
accepted, refused with a status worth offering the payload against again,
or rejected outright. The existing per-URL counters say only whether the
controller was reachable, which a device whose reports are being turned
away looks identical to.

Signed-off-by: eriknordmark <erik@zededa.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extends the controller fault suite with two cases which read back what the
device says about its own queue. With the controller answering 503 to every
info message, the metrics have to show a growing count of undelivered
messages, the age of the oldest one, and refusals counted against the info
endpoint while the metrics endpoint keeps counting accepted answers; the
queue then has to drain to empty once the controller accepts info again.
With the controller rejecting info messages instead, the drops have to be
reported as rejections with the time of the last one, and none of them
counted as superseded.

The metrics message does not travel the deferred queue, so a fault on the
info path leaves the device able to report what the fault is doing to it.

Signed-off-by: eriknordmark <erik@zededa.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The controller re-serializes what a device sent using its own copy of the API,
so fields newer than that copy never reach a test. 0.0.82 carries an eve-api
recent enough to parse the deferred queue metrics, which the controller fault
tests assert on.

Signed-off-by: eriknordmark <erik@zededa.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-URL answer counters are written only for URLs carrying controller
or local profile server traffic. The agents which can fill URLCounters to
its cap -- downloader fetching image blobs, mgmtproxy per target -- never
touch them, so on those agents the three counters serialize as zeroes on
every entry and buy nothing.

That is not free. pubsub base64-encodes a published value before measuring
it against its 64KB socket message limit, leaving about 48KB for the JSON,
and a map at its peak occupancy of 115 entries with registry blob URLs as
keys sits close to that ceiling already; the zeroes cost about 10KB of it,
and going over is a fatal in the publishing agent rather than a dropped
message. Omitting them when zero puts a full map back at the size it was
before the counters existed.

Signed-off-by: eriknordmark <erik@zededa.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: eriknordmark <erik@zededa.com>
The URLCounters cap counts entries, but what it defends is the encoded
size of the published map, and the length of the URL keys dominates that.
A run of long URLs -- a registry blob URL, or a datastore URL carrying a
query string -- puts the map over the limit at an entry count the cap
still allows, and pubsub answers an oversized value with a fatal rather
than an error, taking down an agent that was only reporting on itself.

Ask before publishing, and give up the least recently used entries,
counting each as redacted, until what is left fits. If even an empty set
of URL counters does not fit, report that and skip the publication
instead of letting it be fatal.

Signed-off-by: eriknordmark <erik@zededa.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: eriknordmark <erik@zededa.com>
@eriknordmark
eriknordmark merged commit 0e1007f into lf-edge:master Sep 8, 2026
52 of 54 checks passed
@eriknordmark
eriknordmark deleted the deferred-queue-metrics branch September 9, 2026 13:48
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.

2 participants