Conversation
d61ab29 to
40a37bf
Compare
metric_aggregate is dynamic and overridable, but the publication decision is made when a group is constructed and a published metric name was never removable. A name published while the setting was 0 therefore kept reporting for the life of the process, leaving per group and per hostname metrics side by side at metric_aggregate 2. AGGREGATE_ONLY now tombstones the per group names it declines to publish. A group is rebuilt on the first connection after its count falls to zero, so the change converges as groups go idle.
ATS metric names separate a qualifier with a dot, as in proxy.process.eventloop.time.max, not an underscore. The aggregate added in apache#13506 has only ever existed on master, so renaming it now costs nothing. Also wait for the reconfigure in the retraction autest: http_config_cb schedules it a second out, so a request made as soon as traffic_ctl returns is still served by the previous configuration.
The requirement for the suppressed-per-group mode was a single metric per hostname, the max, rather than the sums as well. Mode 2 is now that max alone, and mode 3 is the sums and the max, for when the totals are wanted too. Mode 1 is unchanged. The sums are withdrawn the same way the per group metrics are when a mode stops asking for them.
for_each forwards its callable but the header got <utility> only through another include. It compiles today; that is not a property this header controls.
Metrics.h is installed, so dropping the iterator, find(), createSpan() and rename() breaks downstream plugins even though nothing in tree used them. Record the removals and the for_each replacement where upgraders will look.
clang-format only, from merging this file with the one master added.
40a37bf to
3854c19
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Moderate issues remain in the build configuration, metric ownership handling, and retraction test setup.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Updates per-server connection metrics to support runtime aggregate-mode changes and retract obsolete metric names.
Changes:
- Adds aggregate modes and metric retraction.
- Adds unit and end-to-end coverage.
- Updates configuration, documentation, and Metrics API migration notes.
File summaries
| File | Reviewed changes and findings |
|---|---|
tests/gold_tests/origin_connection/per_server_connection_max.test.py |
Adds retraction coverage. Moderate (1 vote): retain ATS, origin, and DNS processes across runs. Nit (1 vote): rename the obsolete AggregateOnlyWithoutHostAggregateTest. Nit (3 votes): update the assertion message from AGGREGATE_ONLY to AGGREGATE_SUM. |
src/records/RecordsConfig.cc |
Allows aggregate values 0–3. No findings. |
src/iocore/net/unit_tests/test_ConnectionTracker.cc |
Tests aggregate modes and transitions. No findings. |
src/iocore/net/ConnectionTracker.cc |
Implements mode-specific publication and unlisting. Moderate (2 votes): avoid unlisting names still owned by live MATCH_HOST metrics in mixed groups. |
src/iocore/net/CMakeLists.txt |
Registers the connection-tracker tests. Moderate (3 votes): remove the duplicate source entry in test_net. |
include/tsutil/Metrics.h |
Adds the direct <utility> dependency. No findings. |
include/iocore/net/ConnectionTracker.h |
Documents aggregate modes and lifecycle. No findings. |
doc/release-notes/upgrading.en.rst |
Documents removed Metrics APIs. No findings. |
doc/admin-guide/monitoring/statistics/core/http-connection.en.rst |
Updates metric documentation. Nit (1 vote): refer to aggregate outputs as metrics, or explicitly identify the counters. |
doc/admin-guide/files/records.yaml.en.rst |
Documents dynamic aggregation behavior. No findings. |
Review details
Suppressed comments (3)
doc/admin-guide/monitoring/statistics/core/http-connection.en.rst:238
current_connectionis documented as a Gauge immediately above, so calling all three aggregate outputs “counters” is inaccurate. Please refer to them as metrics (or name the two counters explicitly).
For a hostname aggregate there are two kinds. The *sums* are those same three counters, each added
across the groups of that hostname which have aggregation enabled, published at
tests/gold_tests/origin_connection/per_server_connection_max.test.py:528
- This test now exercises
AGGREGATE_MAX, but its class and invocation remainAggregateOnlyWithoutHostAggregateTest, referring to the removedAGGREGATE_ONLYmode. Rename it (for example,AggregateMaxWithoutHostAggregateTest) so test output and future references describe the behavior being covered.
metric_aggregate 2 (AGGREGATE_MAX) normally leaves the per group metrics hidden and publishes
tests/gold_tests/origin_connection/per_server_connection_max.test.py:642
- This helper retains only ATS, but the first run also starts
self._serverand the shared DNS. The later test runs issue more curls without starting or retaining those processes, so the origin/DNS are not guaranteed to remain alive and the retraction check can fail before exercising metric publication. Keep both long-lived dependencies inStillRunningAfterfor every run (and call_use_shared_dnson the later runs).
def _curl(self, tr) -> None:
"""Drive one request through the remap rule."""
tr.MakeCurlCommand(f"-v --fail -s -x 127.0.0.1:{self._ts.Variables.port} 'http://retract.origin.com/get'", ts=self._ts)
tr.Processes.Default.ReturnCode = 0
tr.StillRunningAfter = self._ts
- Files reviewed: 10/10 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
bneradt
left a comment
There was a problem hiding this comment.
Reviewed 3854c19. One correctness issue needs addressing before approval: the new sum-retraction branch can withdraw a live MATCH_HOST group's own metrics when a MATCH_BOTH group for the same hostname is constructed, even when both use metric_aggregate=0. I added the concrete sequence and regression-test request to the existing inline discussion: #13666 (comment).
The four-mode publication logic otherwise looks consistent for MATCH_BOTH groups, and the Metrics include and v11 migration documentation follow-ups are addressed. The duplicate test_ConnectionTracker.cc CMake entry is worth removing as already requested, but I am not treating it as a separate correctness blocker.
Validation: source/test review and clean git diff --check. Thirteen CI checks pass; Clang-Analyzer is still pending. I did not run a local build or tests.
The rebase added test_ConnectionTracker.cc to test_net again; master already listed it when it added its own tests to that file.
It said AGGREGATE_ONLY, which no longer exists, so a failure pointed at the wrong configuration. Interpolate the configured value instead.
A derived metric is shared by its sources, so a contributor that goes away cannot unlist it: another source may still be publishing through that name. remove_source drops one source and unlists the name only when the last one goes, and re-adding relists it.
The per hostname sums and max are named per hostname, so every group of that hostname shares them. A group built for a mode that does not publish them was unlisting names another group was still publishing: two mappings to one hostname with different metric_aggregate values, which is what overriding it is for, and the second group hid the first one's aggregate. Worse with mixed match types, where MATCH_HOST's own metric carries the same name as the MATCH_BOTH aggregate. Groups now remove their source instead. The per group names go the same way, though they have a single source, so the derived pass stops recomputing a value into a name that is no longer published.
The per group and per hostname names now behave differently, and the earlier text said the last group rebuilt decides whether the sums are published, which was describing the defect.
There was a problem hiding this comment.
🟡 Changes recommended
Critical metric race and name-collision issues remain, along with requested regression coverage and documentation updates.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
doc/admin-guide/monitoring/statistics/core/http-connection.en.rst:253
- With modes 2 and 3, the max and sums do not necessarily have the same source set: a mode-2 group contributes to
current_connection.maxbut not to the sums. This paragraph therefore documents incorrect behavior for mixed settings; describe the membership of the max and sums separately.
enabled. Mappings that disagree for one hostname therefore produce an aggregate over part of it: the
sums cover a subset of the groups and ``current_connection.max`` takes its maximum over that same
src/iocore/net/ConnectionTracker.cc:527
- The
has_aggregateguard here is specifically needed for aMATCH_HOSTgroup: its*.fqdnper-group names collide with aMATCH_BOTHhostname aggregate, so removing aMATCH_BOTHsource must not unlist a still-live host source. The new unit test only covers theMATCH_PORTno-aggregate fallback; please add a regression case that keeps a host group alive while constructing a both group at mode 0 or 2 and verifies all three shared names remain listed.
} else if (has_aggregate) {
// Stop contributing rather than unlist: every group of this hostname shares these names, so
// one that does not want them must not remove a name another is still publishing.
Metrics::Derived::remove_source(sum_names[0], _count_metric);
Metrics::Derived::remove_source(sum_names[1], _count_total_metric);
Metrics::Derived::remove_source(sum_names[2], _blocked_metric);
src/iocore/net/unit_tests/test_ConnectionTracker.cc:218
- The current transitions cover adding the max and removing/re-adding the sums, but never remove a max source after it has actually been registered:
publish_max == falseis only reached before the max name exists in theNONE -> MAXcase. Add a transition fromAGGREGATE_MAX(orGROUP/SUM) toAGGREGATE_NONEand assert the hostname max disappears while the per-group names are published again.
SECTION("switching to AGGREGATE_MAX retracts already published per group metrics")
{
// The production sequence: run for a while with the per group metrics published, then change
// the setting. Without a retraction the first set of names is published forever.
txn.metric_aggregate = ConnectionTracker::AGGREGATE_NONE;
open_and_close_connection(txn, addr);
REQUIRE(is_published(current_group));
txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX;
open_and_close_connection(txn, addr);
CHECK_FALSE(is_published(current_group));
CHECK_FALSE(is_published(total_group));
CHECK_FALSE(is_published(blocked_group));
CHECK(is_published(host_metric("current_connection.max")));
}
- Files reviewed: 12/12 changed files
- Comments generated: 3
- Review effort level: Lite
| "proxy.process.http.per_server.total_connection." + _metric_name, | ||
| "proxy.process.http.per_server.blocked_connection." + _metric_name, | ||
| }; | ||
| std::string const max_name = "proxy.process.http.per_server.current_connection.max." + _host_metric_name; |
There was a problem hiding this comment.
[P2] Confirmed from the name builders and Derived::add_source: with an empty metric_prefix, MATCH_HOST for max.example.test and MATCH_BOTH for example.test both register proxy.process.http.per_server.current_connection.max.example.test. If the MATCH_HOST source (value 7) registers first, the entry retains SUM; adding two MATCH_BOTH sources with values 2 and 3 produces 12 instead of the expected hostname max of 3. In the reverse construction order it retains MAX and the unrelated host source can inflate that max to 7. Please make the max aggregate namespace distinct from valid per-group hostname names and cover both construction orders. This remains a correctness blocker.
There was a problem hiding this comment.
Reconsidering severity after discussion: the collision is real, but requires mixed MATCH_HOST/MATCH_BOTH configurations and the related hostnames example.test and max.example.test. I have no evidence that this is common, and calling it a merge blocker overstated its practical impact. Treating this as a non-blocking naming follow-up; it does not prevent my approval.
| SECTION("one hostname's groups do not unlist each other's aggregate") | ||
| { | ||
| // metric_aggregate is overridable, so two mappings to one hostname can disagree. Both groups | ||
| // share the hostname's aggregate names, so a group that does not want them must stop | ||
| // contributing rather than unlist a name the other one is still publishing. | ||
| IpEndpoint other; | ||
| REQUIRE(ats_ip_pton("10.9.8.5:443", &other) == 0); | ||
|
|
||
| txn.metric_aggregate = ConnectionTracker::AGGREGATE_SUM; | ||
| open_and_close_connection(txn, addr); | ||
| REQUIRE(is_published(host_metric("current_connection"))); | ||
|
|
||
| txn.metric_aggregate = ConnectionTracker::AGGREGATE_MAX; | ||
| open_and_close_connection(txn, other); | ||
|
|
||
| CHECK(is_published(host_metric("current_connection"))); | ||
| CHECK(is_published(host_metric("current_connection.max"))); | ||
| } |
|
Three comments, all correct. Two were mechanical; the third found a real defect and it is worse than described. Duplicate test source — Stale assertion message — Unlisting a name another group owns — Confirmed, and the mixed match type case is not the only route. The same defect reaches through a configuration that overriding Two So the underlying rule is about ownership rather than about match types. A per group name has exactly one owner and unlisting it is sound. The sums and the max are named per hostname and shared by every group of that hostname, so no single group may unlist them.
Both directions of Verified the new test fails against the old code: restoring the Documentation was describing the defect
|
bneradt
left a comment
There was a problem hiding this comment.
Re-reviewed 2ec417e. The original MATCH_HOST ownership issue is addressed: removing this group's source leaves other contributors listed. The duplicate CMake entry is also fixed.
Not ready for approval yet. I confirmed two remaining P2 issues in the existing inline discussions:
- The max metric rename collides with a valid MATCH_HOST hostname (max.example.test versus the max aggregate for example.test), merging unrelated sources and using whichever SUM/MAX operation registered first: #13666 (comment).
- The new public Derived::remove_source API can unlist a metric after a concurrent add_source has completed. Source membership and listing transitions need coordinated synchronization: #13666 (comment). ConnectionTracker's table lock serializes its own constructor calls, but does not protect independent users of this public API.
The sharing test is useful, but still does not cover the original live MATCH_HOST/MATCH_BOTH scenario; please add that regression alongside the fixes.
Validation: source/test review and clean git diff --check; no local build or test execution. Docs, Format, and RAT pass; the other 11 reported CI checks are still pending.
remove_source unlisted the derived name after releasing metrics_lock, while add_source relists inside create() before taking it. A concurrent pair could therefore register a source and then have the unlist land on top of it, leaving a metric that update_derived keeps recomputing but nothing enumerates. Doing both transitions under the lock, next to the mutation that justifies them, closes that ordering. The in-tree caller was already safe because ConnectionTracker constructors are serialized by the outbound table lock, but the public API should not depend on its callers for this.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical test-lifecycle and moderate metric publication issues block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (6)
doc/admin-guide/files/records.yaml.en.rst:2037
- The same wording says a group is discarded when its connection count reaches zero, but the production path releases the group from
PoolableSession::release_outbound_connection_tracking()when the upstream session closes;TxnState::release()alone does not erase the table entry. As a result, keep-alive sessions can defer retraction beyond transaction completion. Please describe the session close/churn requirement rather than implying that any count-to-zero transition is sufficient.
connection count reaches zero, so a change is picked up the next time that upstream is reopened.
A group that never goes idle keeps whatever was in effect when it was created.
doc/admin-guide/files/records.yaml.en.rst:2099
- The new documentation says per-group names belong to one group, but the already-documented overridable
MATCH_HOST/MATCH_BOTHcombination can merge a MATCH_HOST per-group name with a MATCH_BOTH hostname sum (ConnectionTracker::Group::host_metric_namenotes this ininclude/iocore/net/ConnectionTracker.h:244-247). In that case rebuilding one group only removes its source and the shared name can remain listed while another source uses it. Please qualify this statement so the documented retraction behavior matches the source-aware implementation.
The per group metrics belong to a single group, so raising the value withdraws them as that group
is rebuilt. The sums and the max are named per hostname and shared by its groups, so a group
rebuilt for a value that does not publish them only stops contributing; they are withdrawn once
no group of that hostname publishes them. Mappings that disagree for one hostname therefore
include/iocore/net/ConnectionTracker.h:113
- This describes the re-evaluation trigger as the connection count reaching zero, but transaction-level
TxnState::release()only decrements the count; the group is removed byGroup::release()fromPoolableSession::release_outbound_connection_tracking()when the upstream session closes. With origin keep-alive, a group can therefore survive many drained transactions and retain the old setting. Document the session-lifetime condition here so operators do not expect a reload or transaction churn to retract the metrics.
* A change is applied per group, when that group is next constructed, which happens on the first
* connection after its count last fell to zero. A group that never goes idle keeps whatever was
* in effect when it was created.
src/iocore/net/ConnectionTracker.cc:512
- This aggregate name can collide with a valid
MATCH_HOSTper-group name: a host namedmax.<fqdn>publishescurrent_connection.max.<fqdn>too.Derived::add_sourcethen keeps the first operation for that name, so construction order can either add the unrelated host count to the hostname sum or let it inflate the hostname MAX. Use a namespace or escaping that cannot be produced byGroup::metric_name, and cover both construction orders.
std::string const max_name = "proxy.process.http.per_server.current_connection.max." + _host_metric_name;
src/iocore/net/ConnectionTracker.cc:527
- The PR description says the last group constructed decides whether a hostname's sums remain published when mappings disagree, but this branch only removes the current group's source. If an earlier group still contributes, a mode 0/2 group will not retract the shared sum; the updated records documentation says the opposite (that mappings cannot hide each other's aggregate). Please reconcile the documented contract with the implementation.
// Stop contributing rather than unlist: every group of this hostname shares these names, so
// one that does not want them must not remove a name another is still publishing.
Metrics::Derived::remove_source(sum_names[0], _count_metric);
Metrics::Derived::remove_source(sum_names[1], _count_total_metric);
Metrics::Derived::remove_source(sum_names[2], _blocked_metric);
tests/gold_tests/origin_connection/per_server_connection_max.test.py:604
- This docstring calls the new publication mechanism a “tombstone,” but metrics are not deleted or reclaimed; the API and surrounding documentation call this
unlist. That term is already used for cache slot deletion elsewhere, so it can mislead readers about the lifetime semantics this test is intended to describe.
the store grew a tombstone, a name published while the setting was 0 kept reporting for the
- Files reviewed: 12/12 changed files
- Comments generated: 1
- Review effort level: Lite
bneradt
left a comment
There was a problem hiding this comment.
Re-reviewed c525ced. Approved.
The original shared-source ownership issue is addressed. The latest update also fixes the concurrent add/remove issue by performing the final relist/unlist transitions under the same lock as source membership changes, with a concurrent regression test added.
After reconsidering its practical likelihood, the mixed-match metric-name collision is a non-blocking follow-up, not a reason to hold this PR. I clarified that in the inline discussion.
All 14 reported CI checks pass, and git diff --check is clean. Validation here was source/test review and CI inspection; I did not run a local build or tests.
Uses
ts::Metrics::unlistfrom #13616, now merged, so this is rebased onto it and the diff is its own.Problem
proxy.config.http.per_server.connection.metric_aggregateisRECU_DYNAMICand overridable, but the decision it drives — which per server metric names get published — was made once, in theConnectionTracker::Groupconstructor, and a published metric name could not be withdrawn. So the setting only ever took effect for names created after it changed.Seen in production. A box that ran for a while at
metric_aggregate0 before being switched to 2 reports both shapes, and no reload removes the first set:The metric store hands out ids in allocation order and
traffic_ctlprints them that way, so the dump is a timeline: every<fqdn>.<ip>:<port>name was created before the first aggregate name, with no interleaving. The config change took effect for everything after it; what came before was unretractable.What the modes mean now
The suppressed-per-group mode was specified as a single metric per hostname — the max — not the sums as well. Mode 2 is that, and mode 3 is new for when the totals are wanted too:
AGGREGATE_NONEAGGREGATE_GROUPAGGREGATE_MAXAGGREGATE_SUMMode 1 is unchanged.
AGGREGATE_ONLYis gone; 2 and 3 replace it.The constructor now reduces to three independent decisions — publish the sums, publish the max, publish the per group metrics — each of which either registers a derived source or unlists the name. That reads better than the nested condition it replaces, and it is what makes 3 → 2 withdraw the sums rather than leave them behind.
An out of range value from a plugin is normalised to
AGGREGATE_GROUPonce, at the top of the constructor, rather than being implicit in the conditions.Metric rename
current_connection_maxbecomescurrent_connection.max. ATS separates a qualifier with a dot —proxy.process.eventloop.time.max,.events.max— not an underscore. The metric only exists on master, from #13506, so the rename is free now and would not be after a release carries it.What converges, and when
A change is applied per group, when that group is next constructed, which happens on the first connection after its count last fell to zero.
Group::release()is called fromPoolableSession::release_outbound_connection_tracking(), so it is the upstream session closing that erases the group, not the transaction ending. With origin keep alive on, a pooled session holds a group open and that group keeps whatever setting it was built with; a group that never goes idle never re-evaluates.Two consequences worth knowing rather than discovering:
Both are documented at the enum and in
records.yaml.en.rst.Tests
src/iocore/net/unit_tests/test_ConnectionTracker.ccis new. It drives the production sequence in process: run atAGGREGATE_NONEso the per group names publish, switch, open and close another connection, assert the names are gone and the aggregates are there. Nine sections cover each mode, both switch directions for the sums, and the no-aggregate fallback at 2 and 3.Getting that harness right took a correction worth recording:
TxnState::release()only decrements, so a test using it never erases the group and nothing is re-evaluated. It has to follow the real path —TxnState::drop()into the session, thenGroup::release().per_server_connection_max.test.pygainsAggregateRetractionTest, which drives traffic at 0, asserts the per group name is published so the later assertion cannot pass vacuously, raises the setting withtraffic_ctl, drives traffic again and asserts the withdrawal. It disables origin keep alive so group churn is deterministic, and waits after thetraffic_ctlcall becausehttp_config_cbschedules the reconfigure a second out — without that wait the next request is still served by the previousHttpConfigParams, which looks exactly like a failure to retract.MultiGroupAggregateTestgains anExcludesExpression. Every assertion in that file was aContainsExpression, which is why the original leak went unnoticed; a test that only checks for presence cannot catch a metric that should not be there.Verified the autest fails without the fix: with the
unlistcall disabled, exactly one assertion fails, the retraction one. Four autests pass in a Fedora 44 container on the CI image — the threeper_server*tests andslow_post, which exercises the same constructor throughconnection.maxenforcement.Two follow-ups from #13616
Both raised in review there after it merged, so they land here.
Metrics.husedstd::forwardinfor_eachwhile getting<utility>only through anotherinclude. It compiles today, but that is not a property this header controls, so the include is now
direct.
Metrics.his installed, so removingMetrics::iterator,begin(),end(),find(),createSpan()andrename()breaks downstream plugins even with no in-tree callers. The v11section of
doc/release-notes/upgrading.en.rstnow records the removals and shows thefor_eachreplacement, which is where an upgrader looks.