bneradt commented on code in PR #13616:
URL: https://github.com/apache/trafficserver/pull/13616#discussion_r3993087211


##########
include/tsutil/Metrics.h:
##########
@@ -231,43 +299,80 @@ class Metrics
       return std::make_tuple(name, type, metric->_value.load());
     }
 
+    /** Equality.
+     *
+     * Three way rather than a plain position compare: 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.
+     *
+     * Two positional iterators may hold different snapshots, so exhaustion 
between them is judged
+     * against the earlier bound. Otherwise a walk could pass its own bound 
while a stop iterator
+     * made later was still live: they would never compare equal and @c 
operator++ could not make
+     * progress. The sentinel keeps its own answer, since its bound is 
meaningless.
+     *
+     * @note A snapshot is the sequence: iterators from different ones are no 
more comparable than
+     *   iterators into different containers, and mixing them is unspecified. 
Within one snapshot
+     *   equality is the equivalence relation an input iterator requires. The 
rule above keeps the
+     *   unspecified case terminating rather than hanging.
+     */
     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;
     }
 
   private:
     void next();
+    void advance();
+    void skip_unlisted();
+
+    bool
+    at_end() const
+    {
+      return _end || _it >= _bound;
+    }
 
     const Metrics  &_metrics;
-    Metrics::IdType _it;
+    Metrics::IdType _it{0};
+    /// One past the last slot allocated when this iterator was made. 
Iteration is a snapshot.
+    Metrics::IdType _bound{0};
+    bool            _end{false};
   };
 
   iterator
   begin() const
   {
-    return iterator(*this, 0);
+    return iterator(*this);
   }
 
   iterator
   end() const
   {
-    return iterator(*this, _storage->next_free_id());
+    return iterator(*this, iterator::end_tag{});
   }
 
   iterator
   find(const std::string_view name) const
   {
     auto id = lookup(name);
 
-    if (id == NOT_FOUND) {
+    // An unlisted slot is never visited by iteration, so handing out an 
iterator to one would
+    // produce a bound that a skipping walk steps straight over. Reach it with 
lookup() instead.
+    if (id == NOT_FOUND || !listed(id)) {
       return end();
     } else {
       return iterator(*this, id);

Review Comment:
   [P2] Keep find() from returning a different metric after concurrent unlisting
   
   There is a check/use gap between `listed(id)` above and the positional 
constructor's `skip_unlisted()`. With adjacent listed metrics `a` and `b`, 
`find("a")` can observe `listed(a) == true`, another thread can unlist `a`, and 
the constructor then advances to `b`. The returned iterator is non-end but 
dereferences to a metric whose name does not match the query. I reproduced that 
interleaving by triggering the unlist when the constructor acquires its bound. 
Please make the find path retain the requested position or return end if that 
position is skipped, rather than accepting the next listed metric, and add a 
regression test for this interleaving.



##########
include/tsutil/Metrics.h:
##########
@@ -231,43 +299,80 @@ class Metrics
       return std::make_tuple(name, type, metric->_value.load());
     }
 
+    /** Equality.
+     *
+     * Three way rather than a plain position compare: 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.
+     *
+     * Two positional iterators may hold different snapshots, so exhaustion 
between them is judged
+     * against the earlier bound. Otherwise a walk could pass its own bound 
while a stop iterator
+     * made later was still live: they would never compare equal and @c 
operator++ could not make
+     * progress. The sentinel keeps its own answer, since its bound is 
meaningless.
+     *
+     * @note A snapshot is the sequence: iterators from different ones are no 
more comparable than
+     *   iterators into different containers, and mixing them is unspecified. 
Within one snapshot
+     *   equality is the equivalence relation an input iterator requires. The 
rule above keeps the
+     *   unspecified case terminating rather than hanging.
+     */
     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;

Review Comment:
   [P2] Account for a saved subrange endpoint becoming unlisted
   
   The shared-bound rule does not cover listing changes within the same 
allocation snapshot. Create `a`, `b`, and `c`, obtain `start = m.find("a")` and 
`stop = m.find("b")`, then call `m.unlist("b")` without creating any metrics. A 
walk `for (auto it = start; it != stop; ++it)` skips `b`, visits `c` outside 
the intended range, and passes its end without ever comparing equal to `stop`: 
the saved stop remains below the shared bound while the walking iterator is 
exhausted. This also arises when another thread unlists the endpoint during 
traversal. The current subrange test only unlists interior slots before 
obtaining the iterators. Please either support this endpoint transition or 
explicitly define and enforce the iterator invalidation/synchronization 
requirement for unlisting, with a regression test; snapshotting only the 
allocation bound does not preserve the endpoint.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to