Allow a metric to be unlisted - #13616
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new tombstone and iterator code paths need additional defensive validation and invariant enforcement to avoid incorrect behavior or potential out-of-bounds access on manufactured/invalid IDs.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds a “tombstone” mechanism to ts::Metrics so a metric can be withdrawn from publication (hidden from iteration/enum-based consumers) while remaining resolvable by exact name / id and keeping its backing atomic/value.
Changes:
- Extend
ts::Metrics::Storagewith a per-slot flag array and publictombstone()/tombstoned()APIs. - Update
ts::Metricsiteration semantics to skip tombstoned slots and to use a snapshot bound captured at iterator construction. - Add unit tests covering tombstoning behavior across both
ts::Metricsand records lookup, plus documentation updates.
File summaries
| File | Description |
|---|---|
src/tsutil/Metrics.cc |
Implements tombstone flagging and iterator behavior changes (snapshot bound + skip). |
include/tsutil/Metrics.h |
Exposes the tombstone API, adds per-slot flag storage, and updates iterator semantics/contracts. |
src/tsutil/unit_tests/test_Metrics.cc |
Adds coverage for tombstone behavior, iteration skipping, resurrection, and edge cases. |
src/records/unit_tests/test_RecHiddenMetricLookup.cc |
Verifies record lookup behavior with tombstoned metrics (enumeration vs exact lookup). |
doc/developer-guide/internal-libraries/Metrics.en.rst |
Documents the tombstone feature and its interaction with find()/iteration. |
Review details
Suppressed comments (2)
src/tsutil/Metrics.cc:288
Storage::tombstoned()indexes the per-slot flag array withoffsetwithout validating thatoffset < MAX_SIZE(or that the slot is allocated in the current blob). A manufactured/invalidIdTypewith a large offset can trigger out-of-bounds access; it should safely return false for non-allocated/non-sensical IDs.
Metrics::Storage::tombstoned(Metrics::IdType id) const
{
auto [blob_ix, offset] = _splitID(id);
Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get();
if (!blob) {
return false;
}
return (std::get<2>(*blob)[offset].load(MEMORY_ORDER) & TOMBSTONE) != 0;
}
src/tsutil/Metrics.cc:296
- The positional iterator ctor
iterator(const Metrics&, IdType)does not callskip_tombstoned(). That allows external callers to construct an iterator that points at a tombstoned slot (contradicting the intended "iteration never visits marked slots" invariant) and reintroduces the non-terminating range-walk risk if such an iterator is used as a bound.
Metrics::iterator::iterator(const Metrics &m, IdType pos) : _metrics(m), _it(pos), _bound(m._storage->current_id()) {}
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| auto [blob_ix, offset] = _splitID(id); | ||
| Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get(); | ||
|
|
||
| // Only slots that have actually been allocated can be marked. | ||
| if (!blob || (blob_ix == _cur_blob && offset > _cur_off)) { | ||
| return false; | ||
| } |
|
Thanks — all three findings were real, including the two that were filed as suppressed comments. Fixed in 9ed3ee8.
bool
allocated(IdType id) const
{
auto [blob, entry] = _splitID(id);
if (id < 0 || entry >= MAX_SIZE || !_blobs[blob]) {
return false;
}
return blob < _cur_blob || (blob == _cur_blob && entry < _cur_off);
}This is deliberately stricter than the existing I left The positional iterator constructor. You are right that it let a caller rest an iterator on a tombstoned slot, which reintroduces the non-terminating range-walk. Rather than only skipping, the three constructors are now private with New tests covering each case: an offset past the end of a full blob, the next free slot, and a blob index that was never allocated. |
|
Follow-up on a review question: That is in fact safe, but only by coincidence. b4fee22 makes it explicit: static_assert(MAX_BLOBS == METRIC_TYPE_MASK + 1, "a masked blob index must always be a valid _blobs index");Verified it fires: dropping Also added a test for the largest possible id, which exercises the other half — the offset is not masked to the blob size, so it needs the explicit Worth noting for a possible follow-up, out of scope here: |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
|
|
||
| return blob < _cur_blob || (blob == _cur_blob && entry < _cur_off); | ||
| } | ||
| }; | ||
|
|
||
| Metrics(std::shared_ptr<Storage> &str) : _storage(str) {} | ||
|
|
||
| std::shared_ptr<Storage> _storage; | ||
|
|
||
| public: | ||
| // These are sort of factory classes, using the Metrics singleton for all storage etc. |
| auto [blob_ix, offset] = _splitID(it->second); | ||
|
|
||
| if (Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get(); blob != nullptr) { | ||
| std::get<2>(*blob)[offset].fetch_and(static_cast<uint8_t>(~TOMBSTONE), MEMORY_ORDER); | ||
| } | ||
|
|
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
| /** Mark @a id as not enumerated, or clear that mark. | ||
| * | ||
| * A tombstoned metric keeps its slot, its name and its atomic. It is skipped by iteration, so it | ||
| * vanishes from everything that enumerates the store, but it still resolves through @c lookup and | ||
| * its value may still be read and written. Creating the same name again clears the mark and | ||
| * returns the same id. | ||
| * | ||
| * @return @c false if @a id does not name an allocated slot. | ||
| */ | ||
| bool | ||
| tombstone(IdType id, bool set = true) | ||
| { | ||
| return _storage->tombstone(id, set); | ||
| } | ||
|
|
||
| bool | ||
| tombstoned(IdType id) const | ||
| { | ||
| return _storage->tombstoned(id); | ||
| } | ||
|
|
| // Re-creating a name is how a tombstoned metric is resurrected: same slot, same atomic, and | ||
| // whatever value it accumulated while it was hidden. | ||
| auto [blob_ix, offset] = _splitID(it->second); | ||
|
|
||
| if (Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get(); blob != nullptr) { | ||
| std::get<2>(*blob)[offset].fetch_and(static_cast<uint8_t>(~TOMBSTONE), MEMORY_ORDER); | ||
| } |
|
|
||
| // Only this bit, so a flag added later is not clobbered by a tombstone or a resurrect. | ||
| if (set) { | ||
| std::get<2>(*blob)[offset].fetch_or(TOMBSTONE, MEMORY_ORDER); | ||
| } else { | ||
| std::get<2>(*blob)[offset].fetch_and(static_cast<uint8_t>(~TOMBSTONE), MEMORY_ORDER); | ||
| } |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core metrics storage/iteration semantics used broadly across ATS, and needs human validation with full CI results and concurrency/compatibility scrutiny.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
Went through all six. Two were already addressed, three are the same point and are now fixed, and one is incorrect — details below. Fixed in ef97aa6
Not a defect: the negative-offset concern
The Already addressed The The PR description mismatch was real when filed; the description now documents |
There was a problem hiding this comment.
🟡 Changes recommended
The new per-slot atomic flag storage needs explicit initialization to avoid nondeterministic “unlisted” state, and one unit-test section can dereference begin() after making the store empty (undefined behavior).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/tsutil/unit_tests/test_Metrics.cc:740
- This section dereferences *m.begin() after unlisting id 0; if this TEST_CASE path runs before any other metric is created, begin()==end() and dereferencing is undefined. Create a guaranteed-listed metric first so begin() is always safe to dereference.
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
| if (it != _lookups.end()) { | ||
| // Re-creating a name is how an unlisted metric is relisted: same slot, same atomic, and | ||
| // whatever value it accumulated while it was out of the listing. A name in _lookups always | ||
| // names an allocated slot, so this cannot fail. | ||
| set_listed(it->second, true); | ||
|
|
That was true through C++17, but P0883 changed It also does not depend on that change. I would rather not argue that from the standard, so 02d30f9 asserts it instead. The blob growth test already creates Confirmed the assertion is not vacuous. Storing That matters more than the standard argument, because reading uninitialized heap frequently does return zero — fresh pages are zero-filled — so this class of bug hides well and a test that only samples a metric or two would not catch it. I did not add an explicit initialization loop. It would be dead work on every blob, and the real risk is not today's behavior but a future change to something like |
There was a problem hiding this comment.
🟡 Changes recommended
Metrics::iterator can incorrectly treat listed GAUGE metrics as “end” due to type-bit contamination in find() positional iterators when using the new _bound numeric comparison.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
| Metrics::iterator::iterator(const Metrics &m, IdType pos) : _metrics(m), _it(pos), _bound(m._storage->current_id()) | ||
| { | ||
| // Iteration never visits an unlisted slot, so an iterator must not rest on one either: used as a | ||
| // range bound it would be stepped over and never reached. find() resolves that case to end() | ||
| // before it gets here; this keeps the invariant true for any other positional construction. | ||
| skip_unlisted(); | ||
| } |
02d30f9 to
7b19905
Compare
|
This one is a real bug, and mine. Fixed in 7b19905.
It is a regression from the bound comparison I introduced with unlisting — before that, Fixed as suggested, by keeping only the blob and offset: auto [blob, offset] = _metrics._splitID(pos);
_it = _makeId(blob, offset, MetricType::COUNTER);Dereferencing is unaffected: Test added first and watched fail on Separately, I have dropped the commit I added earlier about |
There was a problem hiding this comment.
🟡 Changes recommended
The new allocated()-based checks introduce unsynchronized reads of shared storage state that can be exercised by the new public APIs, creating a C++ data race risk under concurrent metric registration.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
| if (id < 0 || entry >= MAX_SIZE || !_blobs[blob]) { | ||
| return false; | ||
| } | ||
|
|
||
| return blob < _cur_blob || (blob == _cur_blob && entry < _cur_off); |
|
Correct as stated, but deliberately out of scope here.
So this is not a race introduced by unlisting; it is the existing synchronization model of The synchronization of this class is being addressed directly in #13583, "Metrics: close the id lookup race and bounds gaps left by the lock revert". That is the right place for it — it is a property of the whole store, not of this feature, and fixing it in two PRs at once would just produce conflicts. Whichever of the two lands second should extend the fix to cover the other's accessors: if #13583 goes first, |
A metric name, once created, was published for the life of the process. Any metric whose name or publication policy depends on a runtime changeable setting could therefore never retract a name it had already published, so such a setting only ever took effect for names created after the change. Unlisting takes a slot out of the store's listing. It keeps its slot, its name and its atomic, so lookup by name still resolves and creating the name again relists it with its value intact. Iteration skips it, which is what removes it from traffic_ctl, the JSONRPC record lookup and stats_over_http without any of them changing.
55f4c70 to
321a970
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
| auto [blob_ix, offset] = _splitID(id); | ||
| Metrics::NamesAndAtomics *blob = _blobs[blob_ix].get(); |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Each iterator captures its own bound, and exhaustion was judged against that. A subrange whose stop iterator was made later held a larger bound, so the walk could pass its own bound and go on comparing unequal to a stop that was still live, with operator++ unable to make progress. Two find() calls with a metric created between them was enough. Exhaustion between two positional iterators is now judged against the earlier of the two bounds, so such a subrange ends at the earlier snapshot. The sentinel keeps its own answer, since its bound means nothing.
It asserted the store had at least one listed metric left, which depends on what other sections put there. A listed metric of its own says the same thing without that coupling.
565db22 to
1f0f4ca
Compare
|
Five comments, one real bug. Taking them in order of consequence. The subrange across snapshots — real, fixed in e776b65 This is a genuine defect and reachable through the public API. Two auto start = m.find("a"); // captures bound B1
Metrics::Counter::create("z"); // store grows
auto stop = m.find("z"); // bound B2 > B1, and its position is >= B1
for (auto it = start; it != stop; ++it) { ... } // never terminates
Fixed by judging exhaustion between two positional iterators against the earlier of the two bounds, so such a subrange ends at the earlier snapshot. The sentinel keeps its own answer, since its bound is meaningless — folding it into the minimum would make every other iterator compare exhausted immediately. My earlier subrange test missed this because it created both endpoints after all the metrics, so both held the same bound. The Filed twice, and right both times: that assertion did depend on the shared store holding at least one listed metric. In practice slot 0 is always listed so it could not actually fail, but the coupling is real and pointless. The section now creates a listed metric of its own and asserts it is observed. One correction to the suggestion: an anchor placed before the unlisted tail proves the loop ran, not that iteration reached the tail. The "none of the tail names appear" assertion is what covers the tail. The comment says only the former. Structured bindings and The concern is that binding and is clean. The pattern is also already in the tree — two occurrences in this same file before this PR, three in
Correct that Both commits build and pass |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/tsutil/Metrics.cc:1
iterator(const Metrics&, IdType)normalizes_ittoMetricType::COUNTER, butStorage::type()derives the type from the id bits (_extractType(id)). If the iterator id is coerced to COUNTER, iteration andfind()dereference can report the wrong metric type (e.g., GAUGE/COUNTER mismatches) and potentially break any logic that depends on the encoded type. A robust fix is to keep_itas the canonical id (with its real type bits) and compare exhaustion/ordering using a separate positional value (e.g., blob+offset linear index), or to fetch the type for dereference from the slot’s stored id rather than from_it.
/** @file
Exhaustion is a property of an iterator's own snapshot bound, so two taken at different times can compare equal to each other while disagreeing about end. That is not a total equivalence relation, which makes these unfit for a generic algorithm; only same snapshot comparisons, and comparison against end, are meaningful.
|
Two to answer, since the Iterator equality is not an equivalence relation — correct, documented in 03452ff The counterexample is real and I can be precise about it. With bounds The cause is structural rather than a slip in the comparison: exhaustion is a property of each iterator's own snapshot, so any iterator-versus-end test is per-iterator, and no comparison rule over a single type can paper over that. I looked at the sentinel design you suggest. It does fix it properly — iterator-versus-iterator becomes pure position equality, iterator-versus-sentinel carries the end test, and the two are different relations so nothing is required to hold across them. The cost is that We decided against it for now and documented the limitation instead. These are Worth adding: the min-bound rule this replaced a hang, not correct behaviour. Before it, that same subrange did not terminate at all. Many SECTIONs mutating the singleton store — declining The remedy does not do what it is meant to here. Splitting into separate TEST_CASE("Metrics unlisting", "[libtsapi][Metrics]")
{
auto &m = Metrics::instance();
SECTION(...)The body is a reference binding. Every registration happens inside a On volume: the largest fill in this test case is 8 metrics, and it adds a few dozen in total against a store that holds 8M. The pattern of many sections over The one place this was a real problem was an assertion that depended on the store holding at least one listed metric from elsewhere, which is fixed in 1f0f4ca. |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
| iterator(const Metrics &m, end_tag); | ||
|
|
||
| public: | ||
| using iterator_category = std::input_iterator_tag; |
| bool | ||
| operator==(const iterator &o) const | ||
| { | ||
| return _it == o._it && std::addressof(_metrics) == std::addressof(o._metrics); | ||
| } | ||
| if (std::addressof(_metrics) != std::addressof(o._metrics)) { | ||
| return false; | ||
| } | ||
|
|
||
| bool | ||
| operator!=(const iterator &o) const | ||
| { | ||
| return _it != o._it || std::addressof(_metrics) != std::addressof(o._metrics); | ||
| if (_end || o._end) { | ||
| return at_end() == o.at_end(); | ||
| } | ||
|
|
||
| auto const bound = _bound < o._bound ? _bound : o._bound; | ||
| bool const a = _it >= bound, b = o._it >= bound; | ||
|
|
||
| if (a || b) { | ||
| return a && b; | ||
| } | ||
| return _it == o._it; | ||
| } |
| // Static methods to encapsulate access to the atomic's | ||
| class iterator | ||
| { | ||
| friend class Metrics; |
| // Only Metrics hands these out, through begin(), end() and find(). A caller that could name an | ||
| // arbitrary position could name an unlisted one, which iteration must never visit. | ||
| explicit iterator(const Metrics &m); | ||
| iterator(const Metrics &m, IdType pos); | ||
| iterator(const Metrics &m, end_tag); |
The previous note disclaimed the equivalence relation while the type still declared input_iterator_tag, which advertises what it then denied. A snapshot is the sequence: iterators from different ones are no more comparable than iterators into different containers, so mixing them is unspecified rather than broken, and within one snapshot equality is the relation an input iterator requires.
|
Both of these arrived twice; answering once each.
The objection lands on the previous wording rather than on the code: declaring The framing that fits what the code does: a snapshot is the sequence. The min-bound rule then has a narrower job than the old note implied: it makes the out-of-domain case terminate instead of hang. Before it, that comparison did not terminate at all, which is a worse kind of unspecified. On the two suggested remedies: the sentinel design does fix the relation properly and I priced it out — Private iterator constructors are a source break — correct, and now documented Accurate: I am keeping it private. Nothing in tree constructs an iterator directly, and there is no sensible reason to — a caller holding an id wants What was missing was disclosure, so the PR description now has an API change section recording it, for the 11.0.0 release notes alongside the |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
|
Follow-up #13666 opens the consumer of |
Problem
ts::Metrics::Storagehas no removal path.create()allocates a slot and a name and nothing ever undoes either, so a metric name lives for the life of the process.Any code that decides whether to publish a name based on a runtime changeable input therefore makes a permanent commitment the first time it publishes. The decision is latched at first creation and can never be revisited.
The case that surfaced this is the per upstream server connection metrics from #13506.
proxy.config.http.per_server.connection.metric_aggregateisRECU_DYNAMICand overridable, and at value2the per group<fqdn>.<ip>:<port>metrics are supposed to stay hidden while only the per hostname aggregates are published. On a box that ran for a while at0before being switched to2, both shapes are present intraffic_ctl metric match per_server, and no reload can remove the first set. The config change took effect correctly for everything created after it; the names created before it simply cannot be withdrawn.What this does
Lets a metric be taken out of the store's listing.
An unlisted metric:
traffic_ctl metric match, the JSONRPC record lookup andstats_over_httpwith no change in any of those consumers;lookup(), soRecLookupRecord,LogAccessfield resolution andTSStatFindNamekeep working;Derivedaggregate sourcing from it is unaffected;create()on the same name, returning the same id with its accumulated value intact.An unlisted phone number is the analogy: not in the directory, but it still rings if you know it. This is a publication policy, not a lifetime — any
IdTypeorAtomicType *a caller already holds stays valid across an unlist and relist.This PR adds the mechanism only. Nothing in the tree calls it, so every existing metric enumerates exactly as before. The
ConnectionTrackerfix is a follow up.On the naming
This started out called
tombstone, which was wrong twice over. A tombstone elsewhere is a record that something was deleted, and this codebase already uses it that way —CacheShmtombstones a slot to mark it dead and reusable. Nothing is deleted here.hide/publishwould read best in isolation but both words are already load bearing for a different mechanism in this same class: the two separate stores,hidden_instance()andcreateHiddenPtr()versus the published store. An unlisted metric in the published store would have been "published but not published".listedcollides with neither, and says the useful part out loud.Implementation notes
Storage. A parallel
FlagStoragearray in the blob, rather than a member ofNameAndId: anstd::atomicmember would make that tuple neither copyable nor movable, and the slot is written with a tuple assignment. Blobs are already built withmake_unique, which value initializes, so flags start zero with no change toaddBlob(). Reads are lock free at relaxed ordering, matching the rest of the class. Cost is 1 KiB per 1024 slots against a blob that is already about 48 KiB. The singleUNLISTEDbit is set and cleared withfetch_or/fetch_andrather than a whole word store, so a flag added later is not clobbered.Id validation.
Storage::allocated()gates both entry points. It is deliberately stricter than the existingvalid(): the offset is the low 16 bits of an id and so can name a slot pastMAX_SIZEin a blob that is full, and_cur_offis the next free slot, sovalid()accepts one slot that does not exist yet. That second case is not theoretical — a test that marked the free slot caused an unrelated metric created later in the same run to come out invisible, becausecreate()only clears the flag when it finds the name already present, not when it allocates a fresh slot.A
static_assertnow tiesMAX_BLOBSto the mask_splitIDapplies to the blob index. That relationship is what keeps every_blobs[]subscript in this class in range without an explicit check, and nothing previously enforced it.Iterator. The skip loop needs an end bound, and deriving it from
end()per element would take the storage mutex per element. Instead the iterator captures the bound once at construction andend()becomes a pure sentinel that reads no storage — strictly less locking than before, whereend()locked on every construction. Iteration is now explicitly a snapshot taken atbegin(): a metric created mid iteration is never seen rather than sometimes seen, which matches the reasoning already recorded inRecLookupRecordaboutfind()/end()racing with concurrent registration.operator==is three way. Any exhausted iterator equals the end sentinel and equals any other exhausted iterator, since two of them may have skipped a different number of unlisted slots; two live iterators still compare by position, unchanged. The hand writtenoperator!=is removed in favor of the C++20 synthesized one.The three constructors are private to
Metrics. A caller able to name an arbitrary position could name an unlisted one, which iteration must never visit and which does not terminate a range walk.begin(),end()andfind()are the only ways to obtain an iterator, andfind()returnsend()for an unlisted metric — uselookup()to read one.API change
Metrics::iterator's constructors become private,Metricsa friend. The positional constructor,iterator(const Metrics &, IdType), was previously public, so this is a source level break foranything constructing an iterator directly. Nothing in tree does, and there is no reason to: a
caller with an id wants
lookup, not an iterator over one position.Keeping it public is what allowed the two iterator defects review found here — a gauge id compared
past the bound, and a subrange from two snapshots that did not terminate. Both come from being able
to name an arbitrary position. Private constructors are what make "iteration never visits an
unlisted slot, and a snapshot is the sequence" enforceable rather than advisory.
Worth a line in the 11.0.0 release notes alongside the
createSpanandrenameremovals from#13583.
Tests
test_Metrics.cc: skipped by iteration, still resolvable by name and id, relisted bycreate(), unlist and relist by name,begin()skipping an unlisted first slot, an unlisted run at the end of the store, iterating to a bound that is notend()with unlisted slots inside the range,find()yieldingend(), iterator comparison, independence between the published and hidden stores, and four rejected id shapes: an unallocated blob, an offset past the end of a full blob, the next free slot, and the largest possible id.test_RecHiddenMetricLookup.cc: an unlisted metric is not enumerated byRecLookupMatchingRecords, is still found byRecLookupRecord, and returns to enumeration when relisted.Documented in
doc/developer-guide/internal-libraries/Metrics.en.rst.