This is an automated email from the ASF dual-hosted git repository.

SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/main by this push:
     new bf0a1e8fc2 [CELEBORN-2346] Add RequestSlots failure metrics
bf0a1e8fc2 is described below

commit bf0a1e8fc281d622fdf176076706d4dc4a52d80e
Author: Chao Sun <[email protected]>
AuthorDate: Wed Jun 3 17:38:19 2026 +0800

    [CELEBORN-2346] Add RequestSlots failure metrics
    
    ### What changes were proposed in this pull request?
    
    Export a new master `RequestSlotsFailed` counter with a bounded `status` 
label for `SLOT_NOT_AVAILABLE` and `WORKER_EXCLUDED`.
    
    This patch also makes metric-specific labels override configured 
`celeborn.metrics.extraLabels`, while preserving reserved `role` and `instance` 
labels. Configured extra labels are snapshotted when a metrics source is 
constructed so labeled metric keys remain stable.
    
    ### Why are the changes needed?
    
    `RequestSlots` placement failures directly affect shuffle registration, but 
existing master metrics do not expose whether applications are receiving these 
failure responses. The new counter provides a direct monitoring signal.
    
    Without the label precedence change, an extra label such as `status=prod` 
shadows the metric-specific failure reason and collapses both series.
    
    ### Does this PR resolve a correctness bug?
    
    - [ ] Yes
    
    ### Does this PR introduce _any_ user-facing change?
    
    - [x] Yes
    
    ### How was this patch tested?
    
    - `JAVA_HOME=$(brew --prefix openjdk17) dev/reformat`
    - `JAVA_HOME=$(brew --prefix openjdk17) build/mvn --no-transfer-progress 
-DskipTests 
-DprotocPluginExecutable=/tmp/protoc-gen-grpc-java-noop-proto3-optional -pl 
master -am install`
    - `JAVA_HOME=$(brew --prefix openjdk17) build/mvn --no-transfer-progress 
-DprotocPluginExecutable=/tmp/protoc-gen-grpc-java-noop-proto3-optional 
-Dsuites=org.apache.celeborn.common.metrics.source.CelebornSourceSuite -pl 
common test-compile scalatest:test`
    - `JAVA_HOME=$(brew --prefix openjdk17) build/mvn --no-transfer-progress 
-DprotocPluginExecutable=/tmp/protoc-gen-grpc-java-noop-proto3-optional 
-Dsuites=org.apache.celeborn.service.deploy.master.MasterSourceSuite,org.apache.celeborn.service.deploy.master.MasterSuite
 -pl master test-compile scalatest:test`
    
    The local `protocPluginExecutable` override is an ARM workstation 
workaround for the downloaded `protoc-gen-grpc-java` artifact.
    
    Closes #3714 from sunchao/CELEBORN-2346-request-slots-failure-metrics.
    
    Authored-by: Chao Sun <[email protected]>
    Signed-off-by: Nicholas Jiang <[email protected]>
---
 assets/grafana/celeborn-dashboard.json             | 95 +++++++++++++++++++++-
 .../common/metrics/source/AbstractSource.scala     | 24 ++++--
 .../metrics/source/CelebornSourceSuite.scala       | 18 ++++
 docs/monitoring.md                                 |  1 +
 .../celeborn/service/deploy/master/Master.scala    |  2 +
 .../service/deploy/master/MasterSource.scala       | 16 ++++
 .../service/deploy/master/MasterSourceSuite.scala  | 43 ++++++++++
 7 files changed, 191 insertions(+), 8 deletions(-)

diff --git a/assets/grafana/celeborn-dashboard.json 
b/assets/grafana/celeborn-dashboard.json
index 65a984d677..fbbc1a05b5 100644
--- a/assets/grafana/celeborn-dashboard.json
+++ b/assets/grafana/celeborn-dashboard.json
@@ -2100,6 +2100,99 @@
           ],
           "title": "metrics_RatisApplyCompletedIndexDiff_Value",
           "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "description": "Number of failed RequestSlots responses over the 
last 1 hour, broken down by status.",
+          "fieldConfig": {
+            "defaults": {
+              "color": {
+                "mode": "palette-classic"
+              },
+              "custom": {
+                "axisCenteredZero": false,
+                "axisColorMode": "text",
+                "axisLabel": "",
+                "axisPlacement": "auto",
+                "barAlignment": 0,
+                "drawStyle": "line",
+                "fillOpacity": 0,
+                "gradientMode": "none",
+                "hideFrom": {
+                  "legend": false,
+                  "tooltip": false,
+                  "viz": false
+                },
+                "lineInterpolation": "linear",
+                "lineWidth": 1,
+                "pointSize": 5,
+                "scaleDistribution": {
+                  "type": "linear"
+                },
+                "showPoints": "auto",
+                "spanNulls": false,
+                "stacking": {
+                  "group": "A",
+                  "mode": "none"
+                },
+                "thresholdsStyle": {
+                  "mode": "off"
+                }
+              },
+              "mappings": [],
+              "thresholds": {
+                "mode": "absolute",
+                "steps": [
+                  {
+                    "color": "green"
+                  },
+                  {
+                    "color": "red",
+                    "value": 1
+                  }
+                ]
+              }
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 0,
+            "y": 196
+          },
+          "id": 272,
+          "options": {
+            "legend": {
+              "calcs": [],
+              "displayMode": "list",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "maxHeight": 600,
+              "mode": "single",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "datasource": {
+                "type": "prometheus",
+                "uid": "${DS_PROMETHEUS}"
+              },
+              "editorMode": "code",
+              "expr": 
"increase(metrics_RequestSlotsFailed_Count{role=\"Master\", 
instance=~\"${instance}\"}[1h])",
+              "legendFormat": "{{statusCode}} ${baseLegend}",
+              "range": true,
+              "refId": "A"
+            }
+          ],
+          "title": "metrics_RequestSlotsFailed_increase_1h",
+          "type": "timeseries"
         }
       ],
       "title": "Master",
@@ -18027,4 +18120,4 @@
   "uid": "U_qgru_7z",
   "version": 1,
   "weekStart": ""
-}
\ No newline at end of file
+}
diff --git 
a/common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala
 
b/common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala
index 6034dd9ca4..e7b71e5ec2 100644
--- 
a/common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala
+++ 
b/common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala
@@ -68,6 +68,7 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
     
ThreadUtils.newDaemonSingleThreadScheduledExecutor("worker-metrics-cleaner")
 
   val roleLabel: (String, String) = "role" -> role
+  val metricsExtraLabels: Map[String, String] = conf.metricsExtraLabels
   val instanceLabel: Map[String, String] = role match {
     case Role.MASTER =>
       Map("instance" -> s"${Utils.localHostName(conf)}:${conf.masterHttpPort}")
@@ -75,7 +76,7 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
       Map("instance" -> s"${Utils.localHostName(conf)}:${conf.workerHttpPort}")
     case _ => Map.empty
   }
-  val staticLabels: Map[String, String] = conf.metricsExtraLabels + roleLabel 
++ instanceLabel
+  val staticLabels: Map[String, String] = labelsWithCustomizedLabels(Map.empty)
   val staticLabelsString: String = MetricLabels.labelString(staticLabels)
 
   val applicationLabel = "applicationId"
@@ -110,7 +111,9 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
     // filter out non-number type gauges
     if (gauge.getValue.isInstanceOf[Number]) {
       val metricNameWithLabel = metricNameWithCustomizedLabels(name, labels)
-      namedGauges.putIfAbsent(metricNameWithLabel, NamedGauge(name, gauge, 
labels ++ staticLabels))
+      namedGauges.putIfAbsent(
+        metricNameWithLabel,
+        NamedGauge(name, gauge, labelsWithCustomizedLabels(labels)))
       metricRegistry.synchronized({
         if (!metricRegistry.getMetrics.containsKey(metricNameWithLabel)) {
           metricRegistry.register(metricNameWithLabel, gauge)
@@ -146,7 +149,7 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
       meter: Meter): Unit = {
     namedMeters.putIfAbsent(
       metricNameWithCustomizedLabels(name, labels),
-      NamedMeter(name, meter, labels ++ staticLabels))
+      NamedMeter(name, meter, labelsWithCustomizedLabels(labels)))
   }
 
   def addMeter(
@@ -177,7 +180,7 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
         val namedTimer = NamedTimer(
           name,
           metricRegistry.timer(metricNameWithLabel, timerSupplier),
-          labels ++ staticLabels)
+          labelsWithCustomizedLabels(labels))
         val values = JavaUtils.newConcurrentHashMap[String, Long]()
         (namedTimer, values)
       })
@@ -189,7 +192,10 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
     val metricNameWithLabel = metricNameWithCustomizedLabels(name, labels)
     namedCounters.putIfAbsent(
       metricNameWithLabel,
-      NamedCounter(name, metricRegistry.counter(metricNameWithLabel), labels 
++ staticLabels))
+      NamedCounter(
+        name,
+        metricRegistry.counter(metricNameWithLabel),
+        labelsWithCustomizedLabels(labels)))
   }
 
   def addHistogram(name: String): Unit = {
@@ -203,7 +209,7 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
       NamedHistogram(
         name,
         metricRegistry.histogram(name, histogramSupplier),
-        labels ++ staticLabels))
+        labelsWithCustomizedLabels(labels)))
   }
 
   def counters(): List[NamedCounter] = {
@@ -708,9 +714,13 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
     if (labels.isEmpty) {
       metricsName + staticLabelsString
     } else {
-      metricsName + MetricLabels.labelString(labels ++ staticLabels)
+      metricsName + 
MetricLabels.labelString(labelsWithCustomizedLabels(labels))
     }
   }
+
+  protected def labelsWithCustomizedLabels(labels: Map[String, String]): 
Map[String, String] = {
+    metricsExtraLabels ++ labels + roleLabel ++ instanceLabel
+  }
 }
 
 class TimerSupplier(val slidingWindowSize: Int)
diff --git 
a/common/src/test/scala/org/apache/celeborn/common/metrics/source/CelebornSourceSuite.scala
 
b/common/src/test/scala/org/apache/celeborn/common/metrics/source/CelebornSourceSuite.scala
index 0f776f8df0..110d0b1ebe 100644
--- 
a/common/src/test/scala/org/apache/celeborn/common/metrics/source/CelebornSourceSuite.scala
+++ 
b/common/src/test/scala/org/apache/celeborn/common/metrics/source/CelebornSourceSuite.scala
@@ -115,4 +115,22 @@ class CelebornSourceSuite extends CelebornFunSuite {
     createAbstractSourceAndCheck(conf, extraLabels3)
 
   }
+
+  test("test customized labels override extra labels") {
+    val conf = new CelebornConf()
+    conf.set(CelebornConf.METRICS_EXTRA_LABELS.key, 
"user=extra,role=extra,instance=extra")
+    val source = new AbstractSource(conf, Role.MASTER) {
+      override def sourceName: String = "mockSource"
+    }
+    val labels = Map("user" -> "metric")
+    source.addCounter("Counter", labels)
+    source.incCounter("Counter", 1, labels)
+    conf.set(CelebornConf.METRICS_EXTRA_LABELS.key, "user=changed")
+    source.incCounter("Counter", 1, labels)
+
+    val metrics = source.getMetrics
+    val instance = source.instanceLabel("instance")
+    assert(metrics.contains(
+      
s"""metrics_Counter_Count{instance="$instance",role="Master",user="metric"} 
2"""))
+  }
 }
diff --git a/docs/monitoring.md b/docs/monitoring.md
index aa845640a0..5679ef548a 100644
--- a/docs/monitoring.md
+++ b/docs/monitoring.md
@@ -122,6 +122,7 @@ These metrics are exposed by Celeborn master.
     | RatisApplyCompletedIndexDiff | The difference value of 
ApplyCompletedIndex of the master nodes in HA mode.               |
     | PartitionSize                | The size of estimated shuffle partition.  
                                                |
     | OfferSlotsTime               | The time for masters to handle 
`RequestSlots` request when registering shuffle.           |
+    | RequestSlotsFailed           | The count of failed `RequestSlots` 
requests, labeled by the failure status code.           |
 
   - namespace=CPU
 
diff --git 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
index 81e0e0076c..6b25552060 100644
--- 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
+++ 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/Master.scala
@@ -947,6 +947,7 @@ private[celeborn] class Master(
 
     if (numAvailableWorkers == 0) {
       logError(s"Offer slots for $shuffleKey failed due to all workers are 
excluded!")
+      masterSource.incRequestSlotsFailed(StatusCode.WORKER_EXCLUDED)
       context.reply(
         RequestSlotsResponse(StatusCode.WORKER_EXCLUDED, new WorkerResource(), 
requestSlots.packed))
       return
@@ -1009,6 +1010,7 @@ private[celeborn] class Master(
     // reply false if offer slots failed
     if (slots == null || slots.isEmpty) {
       logError(s"Offer slots for $numReducers reducers of $shuffleKey failed!")
+      masterSource.incRequestSlotsFailed(StatusCode.SLOT_NOT_AVAILABLE)
       context.reply(RequestSlotsResponse(
         StatusCode.SLOT_NOT_AVAILABLE,
         new WorkerResource(),
diff --git 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/MasterSource.scala
 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/MasterSource.scala
index aa28fd9005..3e22411ae1 100644
--- 
a/master/src/main/scala/org/apache/celeborn/service/deploy/master/MasterSource.scala
+++ 
b/master/src/main/scala/org/apache/celeborn/service/deploy/master/MasterSource.scala
@@ -19,19 +19,31 @@ package org.apache.celeborn.service.deploy.master
 
 import org.apache.celeborn.common.CelebornConf
 import org.apache.celeborn.common.metrics.source.{AbstractSource, Role}
+import org.apache.celeborn.common.protocol.message.StatusCode
 
 class MasterSource(conf: CelebornConf) extends AbstractSource(conf, 
Role.MASTER) {
   override val sourceName = "master"
 
   import MasterSource._
+  RequestSlotsFailureStatuses.foreach { status =>
+    addCounter(REQUEST_SLOTS_FAILED_COUNT, Map(STATUS_CODE_LABEL -> 
status.name()))
+  }
   // add timers
   addTimer(OFFER_SLOTS_TIME)
   addTimer(UPDATE_RESOURCE_CONSUMPTION_TIME)
   // start cleaner
   startCleaner()
+
+  def incRequestSlotsFailed(status: StatusCode): Unit = {
+    if (RequestSlotsFailureStatuses.contains(status)) {
+      incCounter(REQUEST_SLOTS_FAILED_COUNT, 1, Map(STATUS_CODE_LABEL -> 
status.name()))
+    }
+  }
 }
 
 object MasterSource {
+  val STATUS_CODE_LABEL = "statusCode"
+
   val WORKER_COUNT = "WorkerCount"
 
   val LOST_WORKER_COUNT = "LostWorkerCount"
@@ -63,6 +75,7 @@ object MasterSource {
   val ACTIVE_SHUFFLE_FILE_COUNT = "ActiveShuffleFileCount"
 
   val OFFER_SLOTS_TIME = "OfferSlotsTime"
+  val REQUEST_SLOTS_FAILED_COUNT = "RequestSlotsFailed"
 
   val RATIS_APPLY_COMPLETED_INDEX = "RatisApplyCompletedIndex"
 
@@ -71,4 +84,7 @@ object MasterSource {
   val DEVICE_CELEBORN_TOTAL_CAPACITY = "DeviceCelebornTotalBytes"
 
   val UPDATE_RESOURCE_CONSUMPTION_TIME = "UpdateResourceConsumptionTime"
+
+  private val RequestSlotsFailureStatuses =
+    Seq(StatusCode.SLOT_NOT_AVAILABLE, StatusCode.WORKER_EXCLUDED)
 }
diff --git 
a/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSourceSuite.scala
 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSourceSuite.scala
new file mode 100644
index 0000000000..cd591532f8
--- /dev/null
+++ 
b/master/src/test/scala/org/apache/celeborn/service/deploy/master/MasterSourceSuite.scala
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.celeborn.service.deploy.master
+
+import org.apache.celeborn.CelebornFunSuite
+import org.apache.celeborn.common.CelebornConf
+import org.apache.celeborn.common.protocol.message.StatusCode
+
+class MasterSourceSuite extends CelebornFunSuite {
+
+  test("test request slots failed metrics") {
+    val conf = new CelebornConf()
+    conf.set(CelebornConf.METRICS_EXTRA_LABELS.key, "statusCode=prod")
+    val source = new MasterSource(conf)
+
+    source.incRequestSlotsFailed(StatusCode.SLOT_NOT_AVAILABLE)
+    source.incRequestSlotsFailed(StatusCode.WORKER_EXCLUDED)
+    source.incRequestSlotsFailed(StatusCode.SUCCESS)
+
+    val metrics = source.getMetrics
+    val instance = source.instanceLabel("instance")
+    assert(metrics.contains(
+      
s"""metrics_RequestSlotsFailed_Count{instance="$instance",role="Master",statusCode="SLOT_NOT_AVAILABLE"}
 1"""))
+    assert(metrics.contains(
+      
s"""metrics_RequestSlotsFailed_Count{instance="$instance",role="Master",statusCode="WORKER_EXCLUDED"}
 1"""))
+    assert(!metrics.contains("""statusCode="SUCCESS""""))
+  }
+}

Reply via email to