Skip to content

tentacle: osd/PeeringState: add perf counters for PG rebuild times - #69965

Merged
batrick merged 1 commit into
ceph:tentaclefrom
sseshasa:wip-77956-tentacle
Jul 18, 2026
Merged

tentacle: osd/PeeringState: add perf counters for PG rebuild times#69965
batrick merged 1 commit into
ceph:tentaclefrom
sseshasa:wip-77956-tentacle

Conversation

@sseshasa

@sseshasa sseshasa commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

backport tracker: https://tracker.ceph.com/issues/77956


backport of #69578
parent tracker: https://tracker.ceph.com/issues/77493

this backport was staged using ceph-backport.sh version 16.0.0.6848
find the latest version at https://github.com/ceph/ceph/blob/main/src/script/ceph-backport.sh

Track per-OSD PG rebuild duration in prepare_stats_for_publish()
(primary OSD only). Three new counters are added to the
recoverystate_perf collection:

 - pg_rebuild_duration: LONGRUNAVG time counter (sum+count pair);
   This counter internally maintains 'avgcount' that tracks the
   cumulative number of rebuild events.
 - pg_rebuild_max_secs: maximum rebuild duration observed in seconds
 - pg_rebuild_min_secs: minimum rebuild duration observed in seconds

The logic uses a per-PG in-memory latch (rebuild_start_time) to
capture the redundancy failure entry point. When a PG is first observed
to be in a vulnerable state (PG_STATE_DEGRADED,
PG_STATE_UNDERSIZED, or num_objects_misplaced/degraded > 0), it
latches info.stats.last_change as the start time, provided
last_change > last_clean, which ensures only genuine new failures
after the prior clean interval are tracked.

On recovery, the rebuild duration is computed as (now - rebuild_start_time)
and is only recorded if delta num_objects_recovered > 0 or the PG had
confirmed redundancy loss at latch time, filtering out spurious state
transitions. The latch is cleared after each recorded event.

The latch is also cleared in clear_primary_state() so that an interval
change or role transition (primary -> replica) does not carry a stale
start time or baseline recovered count into a future interval.

The new last_degraded is intentionally not used here to retain compatibility
with older Ceph branches where the field doesn't exist and requires encoding
changes. A future simplification can replace the latch with a direct
(last_clean - last_degraded) calculation once last_degraded is consistently
available.

This interim solution is a close approximation of the PG rebuild time.

These counters are scraped per-OSD by ceph-exporter and exposed to
Prometheus, enabling durability score calculations over user-defined
time windows.

Other Changes:
1. Add unit tests to TestPeeringState.cc that exercise the latch logic.
2. Add a standalone integration test to verify that the rebuild perf counters
   are incremented on the primary OSD after a recovery event.

Fixes: https://tracker.ceph.com/issues/77493
Signed-off-by: Sridhar Seshasayee <sridhar.seshasayee@ibm.com>
(cherry picked from commit 4d29f4e)

        qa/standalone/osd/osd-recovery-stats.sh
- Removed tests related to 'last_degraded' field which is not introduced
  in tentacle. The test related to this change is obviously retained.
        src/osd/PeeringState.cc
- Removed an 'if' block related to setting the 'last_degraded' field which
  is not introduced in tentacle.
        src/osd/osd_perf_counters.cc
- Removed a bunch of perf counters definitions not yet available in tentacle.
        src/osd/osd_perf_counters.h
- Removed a bunch a perf counter declarations not yet available in tentacle.
        src/test/osd/TestPeeringState.cc
- Removed the src/test/osd/TestPeeringState.cc file as it's not yet
  intoduced in tentacle.
@sseshasa
sseshasa requested a review from a team as a code owner July 6, 2026 15:13
@sseshasa sseshasa added this to the tentacle milestone Jul 6, 2026
@sseshasa sseshasa added the core label Jul 6, 2026
@github-actions github-actions Bot added the tests label Jul 6, 2026
@sseshasa
sseshasa requested review from ronen-fr and rzarzynski and removed request for a team July 6, 2026 15:14
github-actions[bot]

This comment was marked as outdated.

@github-actions github-actions Bot added the releng-audit-fail Release engineering: failed backport verification audit. label Jul 6, 2026
@sseshasa sseshasa modified the milestones: tentacle, v20.2.3 Jul 8, 2026
@sseshasa

sseshasa commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@ronen-fr @rzarzynski The releng-audit check failed due to the following conflicts and this is documented in the commit's conflict section:

  • The 'last_degraded' field doesn't exist in tentacle and this is intentional. We plan to introduce this from umbrella.
  • The TestPeeringState.cc file doesn't exist in tentacle, but the standalone test is backported and therefore we have test coverage.
  • A bunch of perf counters don't exist in tentacle and are therefore removed.

This summary should help with the review. Please let me know if you have any questions.

@yuriw

yuriw commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

This PR has been added to wip-yuri9-testing-20260707.172137-tentacle.

@batrick batrick added the releng-audit-queue Release engineering: queued for backport verification audit. label Jul 13, 2026
@github-actions github-actions Bot removed releng-audit-queue Release engineering: queued for backport verification audit. releng-audit-fail Release engineering: failed backport verification audit. labels Jul 13, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ceph Release Engineering Audit Report

Commit Parity Visualizer

BACKPORT PR #69965 SOURCE PR SOURCE STATUS
9e59487 osd/PeeringState: add perf counters for PG rebuild times PR #69578 4d29f4e osd/PeeringState: add perf counters for PG rebuild times

Automated Backport Parity Review - Cherry-Pick Conflicts / Deviations

⚠️ WARNING: Cherry-pick conflicts or deviations were found that require component lead review.

A conflict or deviation was detected during the simulation of this backport. The code in this PR does not match a clean cherry-pick of the upstream commits.

This does not necessarily indicate an issue but a maintainer should review.

**Click to expand conflict summaries.**

Deviation in Backport 9e59487 (cherry-pick of 4d29f4e)

Affected File(s)

Range Diff

Click to expand
--- Original (4d29f4e4)
+++ Backport (9e594876)
@@ -46,3 +46,18 @@
 
 Fixes: https://tracker.ceph.com/issues/77493
 Signed-off-by: Sridhar Seshasayee <sridhar.seshasayee@ibm.com>
+(cherry picked from commit 4d29f4e4021081f19a304dea6938b6f4350eb748)
+
+        qa/standalone/osd/osd-recovery-stats.sh
+- Removed tests related to 'last_degraded' field which is not introduced
+  in tentacle. The test related to this change is obviously retained.
+        src/osd/PeeringState.cc
+- Removed an 'if' block related to setting the 'last_degraded' field which
+  is not introduced in tentacle.
+        src/osd/osd_perf_counters.cc
+- Removed a bunch of perf counters definitions not yet available in tentacle.
+        src/osd/osd_perf_counters.h
+- Removed a bunch a perf counter declarations not yet available in tentacle.
+        src/test/osd/TestPeeringState.cc
+- Removed the src/test/osd/TestPeeringState.cc file as it's not yet
+  intoduced in tentacle.

================================================================================
RANGE DIFF
================================================================================

1:  4d29f4e4021 ! 1:  9e5948767fb osd/PeeringState: add perf counters for PG rebuild times
    @@ Commit message
     
         Fixes: https://tracker.ceph.com/issues/77493
         Signed-off-by: Sridhar Seshasayee <sridhar.seshasayee@ibm.com>
    +    (cherry picked from commit 4d29f4e4021081f19a304dea6938b6f4350eb748)
    +
    +            qa/standalone/osd/osd-recovery-stats.sh
    +    - Removed tests related to 'last_degraded' field which is not introduced
    +      in tentacle. The test related to this change is obviously retained.
    +            src/osd/PeeringState.cc
    +    - Removed an 'if' block related to setting the 'last_degraded' field which
    +      is not introduced in tentacle.
    +            src/osd/osd_perf_counters.cc
    +    - Removed a bunch of perf counters definitions not yet available in tentacle.
    +            src/osd/osd_perf_counters.h
    +    - Removed a bunch a perf counter declarations not yet available in tentacle.
    +            src/test/osd/TestPeeringState.cc
    +    - Removed the src/test/osd/TestPeeringState.cc file as it's not yet
    +      intoduced in tentacle.
     
      ## qa/standalone/osd/osd-recovery-stats.sh ##
    -@@ qa/standalone/osd/osd-recovery-stats.sh: function TEST_recovery_last_degraded_undersized() {
    +@@ qa/standalone/osd/osd-recovery-stats.sh: function TEST_recovery_multi() {
          kill_daemons $dir || return 1
      }
      
    @@ src/osd/PeeringState.cc: std::optional<pg_stat_t> PeeringState::prepare_stats_fo
     +      }
     +    }
     +
    -     // check if the PG is vulnerable
    -     if (info.stats.state & (PG_STATE_DEGRADED|PG_STATE_UNDERSIZED)) {
    -       // set last_degraded only if we are entering a new
    +     psdout(15) << "publish_stats_to_osd " << pre_publish.reported_epoch
    + 	       << ":" << pre_publish.reported_seq << dendl;
    +     return std::make_optional(std::move(pre_publish));
     
      ## src/osd/PeeringState.h ##
     @@ src/osd/PeeringState.h: public:
    @@ src/osd/PeeringState.h: public:
     
      ## src/osd/osd_perf_counters.cc ##
     @@ src/osd/osd_perf_counters.cc: PerfCounters *build_recoverystate_perf(CephContext *cct) {
    -   rs_perf.add_u64_counter(rs_update_stats_invalidated, "update_stats_invalidated", "Number of times pg stats received invalidations during stats updates");
    -   rs_perf.add_u64_counter(rs_append_log_stats_invalidated, "append_log_stats_invalidated", "Number of times pg stats received invalidations when appending new log entries");
    -   rs_perf.add_u64_counter(rs_merge_log_stats_invalidated, "merge_log_stats_invalidated", "Number of times pg stats received invalidations during merging of log entries");
    +   rs_perf.add_time_avg(rs_waitupthru_latency, "waitupthru_latency", "Waitupthru recovery state latency");
    +   rs_perf.add_time_avg(rs_notrecovering_latency, "notrecovering_latency", "Notrecovering recovery state latency");
    +   rs_perf.add_u64_counter(rs_stats_invalidated, "stats_invalidated", "Number of times pg stats received invalidations");
     +  rs_perf.add_time_avg(rs_pg_rebuild_duration, "pg_rebuild_duration",
     +    "Average PG rebuild duration on this OSD (primary role only)",
     +    NULL, PerfCountersBuilder::PRIO_USEFUL);
    @@ src/osd/osd_perf_counters.cc: PerfCounters *build_recoverystate_perf(CephContext
     
      ## src/osd/osd_perf_counters.h ##
     @@ src/osd/osd_perf_counters.h: enum {
    -   rs_update_stats_invalidated,
    -   rs_append_log_stats_invalidated,
    -   rs_merge_log_stats_invalidated,
    +   rs_waitupthru_latency,
    +   rs_notrecovering_latency,
    +   rs_stats_invalidated,
     +  rs_pg_rebuild_duration,
     +  rs_pg_rebuild_max_secs,
     +  rs_pg_rebuild_min_secs,
        rs_last,
      };
      
    -
    - ## src/test/osd/TestPeeringState.cc ##
    -@@
    -   * PrimaryLogPG to allow this to be tested.
    -   */
    - 
    -+#include <chrono>
    - #include <memory>
    -+#include <thread>
    - #include <gtest/gtest.h>
    - #include "test/osd/MockConnection.h"
    - #include "test/osd/MockECRecPred.h"
    -@@ src/test/osd/TestPeeringState.cc: protected:
    -     }
    -   }
    - 
    -+  // Helper - call prepare_stats_for_publish on an OSD, discarding the result.
    -+  // Passing nullopt forces the publish branch unconditionally (no last-known
    -+  // stat to compare against), which is what we need to drive the latch logic.
    -+  void call_prepare_stats(int osd)
    -+  {
    -+    get_ps(osd)->prepare_stats_for_publish(
    -+      std::nullopt, object_stat_collection_t());
    -+  }
    -+
    -   // ============================================================================
    -   // GTest - Setup and Teardown
    -   // ============================================================================
    -@@ src/test/osd/TestPeeringState.cc: TEST_F(PeeringStateTest, Issue74218) {
    -   verify_logs();
    - }
    - 
    -+// ============================================================================
    -+// Rebuild Stats Perf Counter Tests
    -+//
    -+// These tests exercise the latch logic in prepare_stats_for_publish() that
    -+// feeds rs_pg_rebuild_duration, rs_pg_rebuild_max_secs, and
    -+// rs_pg_rebuild_min_secs.
    -+// Design notes:
    -+//   - call_prepare_stats(osd) passes nullopt so the publish branch always
    -+//     runs, which is needed to drive the latch even when stats are unchanged.
    -+//   - A 10 ms sleep between the latch call and the clean call ensures
    -+//     rebuild_dur.to_msec() > 0 so the record is committed.
    -+//   - rebuild_secs = (uint64_t)rebuild_dur.sec(), so sub-second rebuilds
    -+//     leave pg_rebuild_max_secs and pg_rebuild_min_secs at 0.  Those gauges
    -+//     are verified only for their mutual ordering (min <= max); absolute
    -+//     values are verified via pg_rebuild_duration.sum which is in nanoseconds.
    -+// ============================================================================
    -+
    -+// One complete failure+recovery cycle does the following:
    -+// - Swaps acting[slot] from old_osd to new_osd.
    -+// - Peers, latches (via call_prepare_stats while degraded).
    -+// - Sleeps sleep_ms to guarantee non-zero rebuild duration.
    -+// - Recovers, verifies clean, calls prepare_stats to commit the record.
    -+//
    -+// Acting set BEFORE call: [..., old_osd, ...] at position slot.
    -+// Acting set AFTER call:  [..., new_osd, ...] at position slot.
    -+// The old_osd PeeringState is left in osd_peeringstate (may go stale).
    -+
    -+// ============================================================================
    -+// Test 1: Primary OSD records all four counters after a recovery event.
    -+// ============================================================================
    -+TEST_F(PeeringStateTest, RebuildStatsLatchAndCount) {
    -+  dout(0) << "== RebuildStatsLatchAndCount ==" << dendl;
    -+  test_create_peering_state();
    -+  test_init();
    -+  test_event_initialize();
    -+  eversion_t v = test_append_log_entry();
    -+  test_peering();
    -+  verify_all_active_clean(v, eversion_t());
    -+
    -+  // Stamp last_clean on the primary so new_failure detection works later.
    -+  call_prepare_stats(acting_primary);
    -+
    -+  PerfCounters *perf = get_listener(acting_primary)->recoverystate_perf;
    -+
    -+  // Introduce a missing replica: swap acting[1] from OSD 1 to OSD 9.
    -+  // OSD 9 starts fresh and needs the log entry recovered to it.
    -+  modify_up_acting(1, 9);
    -+  test_create_peering_state(9, 1);
    -+  test_init(9);
    -+  test_event_initialize(9);
    -+  test_peering();
    -+  // PG is now active+recovering+degraded on the primary.
    -+
    -+  // Latch: first prepare_stats call while vulnerable.
    -+  // The state-change block sets info.stats.last_change = now and
    -+  // rebuild_start_time = last_change inside the latch branch.
    -+  call_prepare_stats(acting_primary);
    -+
    -+  // Sleep so that rebuild_dur.to_msec() > 0 when the record fires.
    -+  std::this_thread::sleep_for(std::chrono::milliseconds(10));
    -+
    -+  // Drive the PG back to active+clean.
    -+  test_begin_peer_recover(9, 1);
    -+  test_on_peer_recover(9, 1, v);
    -+  test_recover_got(9, v);
    -+  test_object_recovered();
    -+  test_event_all_replicas_recovered();
    -+  verify_all_active_clean(v, eversion_t());
    -+
    -+  // Record: prepare_stats while clean fires the else branch, commits record.
    -+  call_prepare_stats(acting_primary);
    -+
    -+  // pg_rebuild_duration: at least one sample with a positive nanosecond sum.
    -+  auto [sum_ns, count] = perf->get_tavg_ns(rs_pg_rebuild_duration);
    -+  EXPECT_GE(count, 1u);
    -+  EXPECT_GT(sum_ns, 0u);
    -+
    -+  // max/min gauges track whole seconds; sub-second rebuilds leave both at 0.
    -+  // The key invariant is that min never exceeds max.
    -+  EXPECT_LE(perf->get(rs_pg_rebuild_min_secs), perf->get(rs_pg_rebuild_max_secs));
    -+}
    -+
    -+// ============================================================================
    -+// Test 2: A replica OSD never records anything, even when the PG is degraded.
    -+// ============================================================================
    -+TEST_F(PeeringStateTest, RebuildStatsReplicaSkips) {
    -+  dout(0) << "== RebuildStatsReplicaSkips ==" << dendl;
    -+  test_create_peering_state();
    -+  test_init();
    -+  test_event_initialize();
    -+  eversion_t v = test_append_log_entry();
    -+  test_peering();
    -+  verify_all_active_clean(v, eversion_t());
    -+
    -+  call_prepare_stats(acting_primary);
    -+
    -+  modify_up_acting(1, 9);
    -+  test_create_peering_state(9, 1);
    -+  test_init(9);
    -+  test_event_initialize(9);
    -+  test_peering();
    -+
    -+  // OSD 9 is the recovering replica. prepare_stats_for_publish() is only
    -+  // called by the primary.
    -+  PerfCounters *replica_perf = get_listener(9)->recoverystate_perf;
    -+
    -+  // Drive the primary through the full latch+record cycle.
    -+  call_prepare_stats(acting_primary);   // latch fires on primary
    -+  std::this_thread::sleep_for(std::chrono::milliseconds(10));
    -+
    -+  test_begin_peer_recover(9, 1);
    -+  test_on_peer_recover(9, 1, v);
    -+  test_recover_got(9, v);
    -+  test_object_recovered();
    -+  test_event_all_replicas_recovered();
    -+  verify_all_active_clean(v, eversion_t());
    -+
    -+  call_prepare_stats(acting_primary);   // record fires on primary
    -+
    -+  // All rebuild counters on the replica must remain at their initial values.
    -+  EXPECT_EQ(replica_perf->get(rs_pg_rebuild_max_secs), 0u);
    -+  EXPECT_EQ(replica_perf->get(rs_pg_rebuild_min_secs), 0u);
    -+  auto [sum_ns, count] =
    -+    replica_perf->get_tavg_ns(rs_pg_rebuild_duration);
    -+  EXPECT_EQ(count, 0u);
    -+  EXPECT_EQ(sum_ns, 0u);
    -+
    -+  // Primary must have recorded the event (sanity-check the other side).
    -+  auto [primary_sum_ns, primary_count] =
    -+    get_listener(acting_primary)->recoverystate_perf->get_tavg_ns(rs_pg_rebuild_duration);
    -+  EXPECT_GE(primary_count, 1u);
    -+}
    -+
    -+// ============================================================================
    -+// Test 3: A second prepare_stats call while still vulnerable does not
    -+// overwrite the already-latched start time or double-record the event.
    -+// ============================================================================
    -+TEST_F(PeeringStateTest, RebuildStatsNoDoubleLatch) {
    -+  dout(0) << "== RebuildStatsNoDoubleLatch ==" << dendl;
    -+  test_create_peering_state();
    -+  test_init();
    -+  test_event_initialize();
    -+  eversion_t v = test_append_log_entry();
    -+  test_peering();
    -+  verify_all_active_clean(v, eversion_t());
    -+
    -+  call_prepare_stats(acting_primary);
    -+
    -+  modify_up_acting(1, 9);
    -+  test_create_peering_state(9, 1);
    -+  test_init(9);
    -+  test_event_initialize(9);
    -+  test_peering();
    -+
    -+  PerfCounters *perf = get_listener(acting_primary)->recoverystate_perf;
    -+
    -+  // First vulnerable call — latch fires (rebuild_start_time set).
    -+  call_prepare_stats(acting_primary);
    -+
    -+  // Second vulnerable call while still degraded. The latch is guarded by
    -+  // rebuild_start_time == utime_t(), which is now false, so the start
    -+  // time is not overwritten and no record is emitted.
    -+  call_prepare_stats(acting_primary);
    -+
    -+  // Nothing recorded yet (PG still degraded): duration avgcount must be 0.
    -+  {
    -+    auto [sum_ns, count] = perf->get_tavg_ns(rs_pg_rebuild_duration);
    -+    EXPECT_EQ(count, 0u);
    -+  }
    -+
    -+  // Now complete the recovery and verify exactly one event is recorded.
    -+  std::this_thread::sleep_for(std::chrono::milliseconds(10));
    -+  test_begin_peer_recover(9, 1);
    -+  test_on_peer_recover(9, 1, v);
    -+  test_recover_got(9, v);
    -+  test_object_recovered();
    -+  test_event_all_replicas_recovered();
    -+  verify_all_active_clean(v, eversion_t());
    -+  call_prepare_stats(acting_primary);
    -+
    -+  {
    -+    auto [sum_ns, count] = perf->get_tavg_ns(rs_pg_rebuild_duration);
    -+    EXPECT_EQ(count, 1u);
    -+  }
    -+}
    -+
    -+// ============================================================================
    -+// Test 4: Two sequential failure+recovery cycles accumulate independently.
    -+//
    -+// Cycle 1: acting[1] = 9   (OSD 1 -> OSD 9)
    -+// Cycle 2: acting[2] = 8   (OSD 2 -> OSD 8, while OSD 9 stays in slot 1)
    -+//
    -+// Using distinct slots avoids having to bring OSD 9 stale mid-test while
    -+// still exercising two independent latch+record sequences on the same primary.
    -+// ============================================================================
    -+TEST_F(PeeringStateTest, RebuildStatsCountAccumulates) {
    -+  dout(0) << "== RebuildStatsCountAccumulates ==" << dendl;
    -+  test_create_peering_state();
    -+  test_init();
    -+  test_event_initialize();
    -+  eversion_t v = test_append_log_entry();
    -+  test_peering();
    -+  verify_all_active_clean(v, eversion_t());
    -+
    -+  PerfCounters *perf = get_listener(acting_primary)->recoverystate_perf;
    -+
    -+  // ---- Cycle 1: replace acting[1] with OSD 9 ----
    -+  call_prepare_stats(acting_primary);     // stamp last_clean
    -+
    -+  modify_up_acting(1, 9);
    -+  test_create_peering_state(9, 1);
    -+  test_init(9);
    -+  test_event_initialize(9);
    -+  test_peering();
    -+  // active+recovering: OSD 9 needs recovery.
    -+
    -+  call_prepare_stats(acting_primary);     // latch fires
    -+  std::this_thread::sleep_for(std::chrono::milliseconds(10));
    -+
    -+  test_begin_peer_recover(9, 1);
    -+  test_on_peer_recover(9, 1, v);
    -+  test_recover_got(9, v);
    -+  test_object_recovered();
    -+  test_event_all_replicas_recovered();
    -+  verify_all_active_clean(v, eversion_t());
    -+  // Flush share_pg_info messages queued by cycle 1's Clean so they are
    -+  // delivered to the current replicas now, not stale during cycle 2.
    -+  dispatch_all();
    -+  call_prepare_stats(acting_primary);     // record fires
    -+
    -+  {
    -+    auto [sum_ns, count] = perf->get_tavg_ns(rs_pg_rebuild_duration);
    -+    EXPECT_EQ(count, 1u);
    -+  }
    -+
    -+  // ---- Cycle 2: replace acting[2] with OSD 8 ----
    -+  // OSD 9 remains in slot 1; OSD 8 joins as a fresh replica at slot 2.
    -+  call_prepare_stats(acting_primary);     // stamp last_clean for cycle 2
    -+
    -+  modify_up_acting(2, 8);
    -+  test_create_peering_state(8, 2);
    -+  test_init(8);
    -+  test_event_initialize(8);
    -+  test_peering();
    -+  // active+recovering: OSD 8 needs the object recovered to it.
    -+
    -+  call_prepare_stats(acting_primary);     // second latch fires
    -+  std::this_thread::sleep_for(std::chrono::milliseconds(10));
    -+
    -+  test_begin_peer_recover(8, 2);
    -+  test_on_peer_recover(8, 2, v);
    -+  test_recover_got(8, v);
    -+  test_object_recovered();
    -+  test_event_all_replicas_recovered();
    -+  verify_all_active_clean(v, eversion_t());
    -+  call_prepare_stats(acting_primary);     // second record fires
    -+
    -+  // Both events must appear in the duration avgcount.
    -+  auto [sum_ns, count] = perf->get_tavg_ns(rs_pg_rebuild_duration);
    -+  EXPECT_EQ(count, 2u);
    -+  EXPECT_GT(sum_ns, 0u);
    -+
    -+  // min <= max invariant must hold across both events.
    -+  EXPECT_LE(perf->get(rs_pg_rebuild_min_secs), perf->get(rs_pg_rebuild_max_secs));
    -+}
    -+
    -+// ============================================================================
    -+// Test 5: Latch is discarded when the OSD loses its primary role mid-rebuild.
    -+//
    -+// If a new interval begins while rebuild_start_time is set and the OSD
    -+// transitions from primary to stray, clear_primary_state() must discard the
    -+// stale latch so no spurious rebuild event is emitted for a recovery that the
    -+// OSD no longer owns.
    -+// ============================================================================
    -+TEST_F(PeeringStateTest, RebuildStatsLatchClearedOnRoleChange) {
    -+  dout(0) << "== RebuildStatsLatchClearedOnRoleChange ==" << dendl;
    -+  test_create_peering_state();
    -+  test_init();
    -+  test_event_initialize();
    -+  eversion_t v = test_append_log_entry();
    -+  test_peering();
    -+  verify_all_active_clean(v, eversion_t());
    -+
    -+  // Record last_clean so the new_failure guard inside the latch is satisfied.
    -+  call_prepare_stats(acting_primary);        // acting_primary = OSD 0
    -+
    -+  // --- Phase 1: degrade the PG while OSD 0 is still primary. ---
    -+  // Replace acting[1] (OSD 1) with OSD 9; OSD 9 needs recovery.
    -+  modify_up_acting(1, 9);
    -+  test_create_peering_state(9, 1);
    -+  test_init(9);
    -+  test_event_initialize(9);
    -+  test_peering();
    -+  // PG is now active+recovering+degraded; OSD 0 remains primary.
    -+
    -+  PerfCounters *perf_osd0 = get_listener(0)->recoverystate_perf;
    -+
    -+  // Latch: first prepare_stats call while vulnerable sets rebuild_start_time
    -+  // on OSD 0.
    -+  call_prepare_stats(acting_primary);
    -+  ASSERT_NE(get_ps(0)->get_rebuild_start_time(), utime_t())
    -+      << "latch must be set before role change";
    -+
    -+  // --- Phase 2: change the primary before recovery completes. ---
    -+  // Swap acting[0] from OSD 0 to OSD 7. OSD 0 leaves the acting set
    -+  // entirely and becomes a stray; OSD 7 takes over as primary.
    -+  modify_up_acting(0, 7);
    -+  acting_primary = 7;
    -+  up_primary = 7;
    -+  test_create_peering_state(7, 0);
    -+  test_init(7);
    -+  test_event_initialize(7);
    -+
    -+  // advance_map on OSD 0: the new acting set excludes OSD 0, so
    -+  // should_restart_peering()->start_peering_interval()->clear_primary_state()
    -+  // resets the three latch variables.
    -+  test_event_advance_map();
    -+
    -+  // --- Phase 3: verify latch is cleared on the old primary. ---
    -+  EXPECT_EQ(get_ps(0)->get_rebuild_start_time(), utime_t());
    -+  EXPECT_EQ(get_ps(0)->get_rebuild_base_recovered(), 0);
    -+  EXPECT_FALSE(get_ps(0)->get_rebuild_had_redundancy_loss());
    -+
    -+  // No rebuild record must have been emitted: the latch was discarded, not
    -+  // fired. A spurious emission here would record a duration spanning a
    -+  // recovery that OSD 0 did not complete.
    -+  auto [sum_ns, count] = perf_osd0->get_tavg_ns(rs_pg_rebuild_duration);
    -+  EXPECT_EQ(count, 0u);
    -+  EXPECT_EQ(sum_ns,  0u);
    -+}
    -+
    - // ============================================================================
    - // Main
    - // ============================================================================

How to proceed:

  • Authors (Genuine Conflicts): If this is a genuine conflict requiring manual resolution, ensure your resolution is correct. You must explain the conflict resolution in the commit message (e.g., leave the standard Git Conflicts: block intact) and include an explanation for changes.
  • Authors (Need Help?): Reach out to the Component Lead for technical guidance on complex code conflicts.
  • Component Leads (Review): Please review the Range Diff(s) above to verify the author's manual conflict resolution is correct for this release branch. If the deviation is intentional, documented, and approved then the component lead or @ceph/ceph-release-manager can bypass this check by commenting /audit override.

Be familiar with the rules and guidelines for writing backports.


🛟 Need Help?

If you need technical help resolving these issues, please consult with the Component Lead. If you need administrative overrides, please see the #ceph-upstream-releases channel on Slack and request a review from the @ceph/ceph-release-manager.

📋 Component Lead / Release Manager

To override the audit failure, apply releng-audit-override label or comment /audit override.


⚠️ Note: Automated audit checks will be suspended on future pushes to prevent comment spam while you work.

When you are ready for a new audit, please remove the releng-audit-fail label or comment /audit retest.

View workflow run

@github-actions github-actions Bot added the releng-audit-fail Release engineering: failed backport verification audit. label Jul 13, 2026
@github-actions github-actions Bot removed the releng-audit-fail Release engineering: failed backport verification audit. label Jul 17, 2026
@github-actions

Copy link
Copy Markdown

Audit Override Applied by @batrick.

View workflow run

@ljflores

Copy link
Copy Markdown
Member

@batrick
batrick merged commit 2c4ae83 into ceph:tentacle Jul 18, 2026
19 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants