@@ -511,11 +511,29 @@ ErrorCode EventReportBackend::OnHeartbeat(const std::string &instance_id,
511511 }
512512 const ReporterSnapshotKey reporter_key{instance_id, host_ip_port};
513513 const auto lifecycle_fence = GetOrCreateLifecycleFence (reporter_key);
514+
515+ // Fast path: a steady HEARTBEAT does not change lifecycle state. Use a
516+ // shared lease so same-generation ADD/DELETE can proceed; it still pins
517+ // NodeInfo and excludes REGISTER/HOST_DOWN writers.
518+ {
519+ std::shared_lock<std::shared_mutex> lifecycle_lock (lifecycle_fence->mutex );
520+ if (!AcceptingReports ()) {
521+ return EC_INSTANCE_NOT_EXIST ;
522+ }
523+ std::unique_lock<std::shared_mutex> nodes_lock (nodes_mutex_);
524+ if (TryPublishSteadyHeartbeatLocked (reporter_key, *lifecycle_fence, system_status, nodes_lock)) {
525+ return EC_OK ;
526+ }
527+ }
528+
529+ // Slow path: HEARTBEAT may create or recover a node and change lifecycle
530+ // state. Drop the shared lease, acquire it exclusively, and revalidate
531+ // because shared_mutex has no atomic lock upgrade.
514532 std::unique_lock<std::shared_mutex> lifecycle_lock (lifecycle_fence->mutex );
515533 if (!AcceptingReports ()) {
516534 return EC_INSTANCE_NOT_EXIST ;
517535 }
518- std::unique_lock<std::shared_mutex> lock (nodes_mutex_);
536+ std::unique_lock<std::shared_mutex> nodes_lock (nodes_mutex_);
519537 auto &host_map = instance_nodes_[instance_id];
520538 auto it = host_map.find (host_ip_port);
521539 if (it == host_map.end ()) {
@@ -563,16 +581,47 @@ ErrorCode EventReportBackend::OnHeartbeat(const std::string &instance_id,
563581 }
564582 lifecycle_fence->generation = node_generation_[instance_id][host_ip_port];
565583 lifecycle_fence->registered = true ;
584+ PublishHeartbeatStatus (info, system_status, nodes_lock);
585+ return EC_OK ;
586+ }
587+
588+ bool EventReportBackend::TryPublishSteadyHeartbeatLocked (const ReporterSnapshotKey &reporter_key,
589+ const LifecycleFence &lifecycle_fence,
590+ const std::map<std::string, std::string> &system_status,
591+ std::unique_lock<std::shared_mutex> &nodes_lock) {
592+ if (!lifecycle_fence.registered ) {
593+ return false ;
594+ }
595+ const auto instance_it = instance_nodes_.find (reporter_key.instance_id );
596+ const auto generation_it = node_generation_.find (reporter_key.instance_id );
597+ if (instance_it == instance_nodes_.end () || generation_it == node_generation_.end ()) {
598+ return false ;
599+ }
600+ const auto node_it = instance_it->second .find (reporter_key.host_ip_port );
601+ const auto host_generation_it = generation_it->second .find (reporter_key.host_ip_port );
602+ if (node_it == instance_it->second .end () || !node_it->second || host_generation_it == generation_it->second .end () ||
603+ lifecycle_fence.generation != host_generation_it->second ||
604+ !node_it->second ->available .load (std::memory_order_relaxed)) {
605+ return false ;
606+ }
607+ auto &info = *node_it->second ;
608+ info.last_heartbeat_ms .store (NowMillis (), std::memory_order_release);
609+ PublishHeartbeatStatus (info, system_status, nodes_lock);
610+ return true ;
611+ }
612+
613+ void EventReportBackend::PublishHeartbeatStatus (NodeInfo &info,
614+ const std::map<std::string, std::string> &system_status,
615+ std::unique_lock<std::shared_mutex> &nodes_lock) {
566616 std::unique_lock<std::mutex> status_lock (info.status_mutex );
567617 const std::map<std::string, std::string> previous_system_status = info.last_system_status ;
568618 info.last_system_status = system_status;
569619 const auto metrics_tags = info.metrics_tags ;
570620
571- // The per-reporter lifecycle writer keeps NodeInfo alive and prevents
572- // HOST_DOWN/REGISTER from crossing gauge publication. The status lock
573- // serializes SetNodeUnavailable's gauge reset. Release the global node
574- // table lock so metric work for one reporter does not block all others.
575- lock.unlock ();
621+ // Lock handoff: status_mutex now serializes this publication with
622+ // SetNodeUnavailable's gauge reset, while the lifecycle lease pins
623+ // NodeInfo. Release the global node-table lock before slower metric work.
624+ nodes_lock.unlock ();
576625 if (metrics_registry_) {
577626 const auto parse_gauge = [](const std::string &value, double &out) {
578627 if (value.empty ()) {
@@ -607,7 +656,6 @@ ErrorCode EventReportBackend::OnHeartbeat(const std::string &instance_id,
607656 }
608657 }
609658 }
610- return EC_OK ;
611659}
612660
613661void EventReportBackend::SetNodeUnavailable (const std::string &instance_id, const std::string &host_ip_port) {
@@ -1126,6 +1174,9 @@ ErrorCode EventReportBackend::AcquireLifecycleMutationLease(const ReporterSnapsh
11261174 if (!lifecycle_fence) {
11271175 return EC_NODE_NOT_REGISTERED ;
11281176 }
1177+ // ADD/DELETE mutate metadata, not reporter lifecycle. This shared lease
1178+ // pins and validates the current registration/generation while the
1179+ // metadata RMW is in progress.
11291180 auto lease = std::make_shared<std::shared_lock<std::shared_mutex>>(lifecycle_fence->mutex , std::try_to_lock);
11301181 if (!lease->owns_lock ()) {
11311182 // Do not wait behind a lifecycle writer while the caller may already
@@ -1151,10 +1202,11 @@ ErrorCode EventReportBackend::CommitSnapshotVersionIfGeneration(const ReporterSn
11511202 return EC_NODE_NOT_REGISTERED ;
11521203 }
11531204 // Commit runs after the metadata RMW has released its shard locks, so it
1154- // can safely wait for a transient HEARTBEAT writer. Using the mutation
1205+ // can safely wait for a transient lifecycle writer. Using the mutation
11551206 // path's try-lock here would turn harmless lock contention into a failed
1156- // snapshot. REGISTER/HOST_DOWN still serialize first and are rejected by
1157- // the generation/registered check below.
1207+ // snapshot. REGISTER/HOST_DOWN and a lifecycle-changing HEARTBEAT still
1208+ // serialize first and are rejected by the generation/registered check
1209+ // below.
11581210 std::shared_lock<std::shared_mutex> lifecycle_lease (lifecycle_fence->mutex );
11591211 if (!AcceptingReports ()) {
11601212 return EC_INSTANCE_NOT_EXIST ;
@@ -1197,9 +1249,9 @@ ErrorCode EventReportBackend::AcquireSnapshotCleanupLease(const ReporterSnapshot
11971249 return EC_MISMATCH ;
11981250 }
11991251 // Cleanup acquires this lease before taking metadata locks. It can block
1200- // behind a short-lived HEARTBEAT/ REGISTER writer without creating the
1201- // lifecycle->metadata / metadata->lifecycle inversion that forces delta
1202- // mutations to use try_lock.
1252+ // behind a short-lived REGISTER or lifecycle-changing HEARTBEAT writer
1253+ // without creating the lifecycle->metadata / metadata->lifecycle
1254+ // inversion that forces delta mutations to use try_lock.
12031255 auto lease = std::make_shared<std::shared_lock<std::shared_mutex>>(lifecycle_fence->mutex );
12041256 if (!AcceptingReports () || !lifecycle_fence->registered || lifecycle_fence->generation != expected_generation) {
12051257 return EC_MISMATCH ;
0 commit comments