Copilot commented on code in PR #3740:
URL: https://github.com/apache/celeborn/pull/3740#discussion_r3592348934


##########
master/src/main/scala/org/apache/celeborn/service/deploy/master/ApplicationMetricsSource.scala:
##########
@@ -0,0 +1,113 @@
+/*
+ * 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 java.util.{Map => JMap}
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+
+import org.apache.celeborn.common.CelebornConf
+import org.apache.celeborn.common.internal.Logging
+import org.apache.celeborn.common.metrics.{ClientMetric, MetricType}
+import org.apache.celeborn.common.metrics.source.{AbstractSource, Role}
+import org.apache.celeborn.common.util.{JavaUtils, Utils}
+
+class ApplicationMetricsSource(conf: CelebornConf)
+  extends AbstractSource(conf, Role.MASTER) with Logging {
+  override val sourceName = "application"
+
+  private val masterClientMetricsEnabled = conf.masterClientMetricsEnabled
+  private val removedAppRetentionMs = 
conf.masterClientMetricsRemovedAppRetentionMs
+
+  // Tracking applications that have been terminated
+  private val removedAppIds =
+    JavaUtils.newConcurrentHashMap[String, java.lang.Long]()
+
+  private val seriesCardinalityWarnThreshold =
+    conf.masterClientMetricsSeriesCardinalityWarnThreshold

Review Comment:
   `warnIfSeriesCardinalityHigh` currently logs a warning every time 
`trackedSeries` exceeds the threshold. However, the config docs and 
`CelebornConf.MASTER_CLIENT_METRICS_SERIES_CARDINALITY_WARN_THRESHOLD` describe 
this as a *one-time* warning; repeated heartbeats could spam logs once the 
threshold is crossed. Track whether the warning has already been emitted (e.g., 
an `AtomicBoolean`) and only log on the first exceedance.



##########
master/src/main/scala/org/apache/celeborn/service/deploy/master/ApplicationMetricsSource.scala:
##########
@@ -0,0 +1,113 @@
+/*
+ * 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 java.util.{Map => JMap}
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+
+import org.apache.celeborn.common.CelebornConf
+import org.apache.celeborn.common.internal.Logging
+import org.apache.celeborn.common.metrics.{ClientMetric, MetricType}
+import org.apache.celeborn.common.metrics.source.{AbstractSource, Role}
+import org.apache.celeborn.common.util.{JavaUtils, Utils}
+
+class ApplicationMetricsSource(conf: CelebornConf)
+  extends AbstractSource(conf, Role.MASTER) with Logging {
+  override val sourceName = "application"
+
+  private val masterClientMetricsEnabled = conf.masterClientMetricsEnabled
+  private val removedAppRetentionMs = 
conf.masterClientMetricsRemovedAppRetentionMs
+
+  // Tracking applications that have been terminated
+  private val removedAppIds =
+    JavaUtils.newConcurrentHashMap[String, java.lang.Long]()
+
+  private val seriesCardinalityWarnThreshold =
+    conf.masterClientMetricsSeriesCardinalityWarnThreshold
+
+  if (masterClientMetricsEnabled) {
+    startRemovedAppCleaner()
+  }
+
+  private def startRemovedAppCleaner(): Unit = {
+    val cleanTask: Runnable = new Runnable {
+      override def run(): Unit = Utils.tryLogNonFatalError {
+        val cutoff = System.currentTimeMillis() - removedAppRetentionMs
+        removedAppIds.entrySet().asScala.foreach { entry =>
+          if (entry.getValue < cutoff) {
+            removedAppIds.remove(entry.getKey, entry.getValue)
+          }
+        }
+      }
+    }
+    metricsCleaner.scheduleWithFixedDelay(
+      cleanTask,
+      removedAppRetentionMs,
+      removedAppRetentionMs,
+      TimeUnit.MILLISECONDS)
+  }
+
+  def updateApplicationMetrics(
+      appId: String,
+      metricLabels: Map[String, String],
+      metrics: JMap[String, ClientMetric]): Unit = {
+    if (!masterClientMetricsEnabled || metricLabels.isEmpty) {
+      return
+    }
+
+    if (removedAppIds.containsKey(appId)) {
+      return
+    }
+
+    metrics.asScala.foreach { case (name, metric) =>
+      metric.metricType match {
+        case MetricType.Gauge =>
+          addOrUpdateGaugeForApp(name, metricLabels, appId, metric.value)
+        case MetricType.Counter =>
+          addOrUpdateCounterForApp(name, metricLabels, appId, metric.value)
+      }
+    }
+
+    if (removedAppIds.containsKey(appId)) {
+      removeAppFromMetrics(appId)
+    }
+
+    warnIfSeriesCardinalityHigh()
+  }
+
+  def removeApplicationMetrics(appId: String): Unit = {
+    if (masterClientMetricsEnabled) {
+      removedAppIds.put(appId, System.currentTimeMillis())
+    }
+    removeAppFromMetrics(appId)
+  }
+
+  private def warnIfSeriesCardinalityHigh(): Unit = {
+    val trackedSeries = gauges().size + counters().size
+    if (trackedSeries > seriesCardinalityWarnThreshold) {
+      logWarning(
+        s"Client metrics are tracking $trackedSeries distinct series, 
exceeding " +
+          s"$seriesCardinalityWarnThreshold. Client metric series are keyed by 
" +
+          s"'${CelebornConf.CLIENT_METRICS_APP_LABELS.key}' and are only 
reclaimed when an " +
+          "application is lost, so high-cardinality labels can grow memory 
without bound. " +
+          "Ensure these labels are low-cardinality (e.g. env/team), not 
per-application values.")
+    }
+  }

Review Comment:
   This warning is intended to be emitted only once when the series cardinality 
first exceeds the threshold (per the config/docs), but the current code will 
warn on every update after crossing the threshold. Gate the log with an 
`AtomicBoolean.compareAndSet(false, true)` so it’s truly one-time.



##########
common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala:
##########
@@ -212,6 +234,47 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
         labelsWithCustomizedLabels(labels)))
   }
 
+  protected def addOrUpdateGaugeForApp(
+      name: String,
+      labels: Map[String, String],
+      appId: String,
+      value: Long): Unit = {
+    val key = metricNameWithCustomizedLabels(name, labels)
+    namedGaugesWithDetails.compute(
+      key,
+      (_, existing) => {
+        val tracked = Option(existing).getOrElse {
+          val holder = new AtomicLong()
+          addGauge(name, labels)(() => holder.get())
+          val namedGauge = namedGauges.get(key).asInstanceOf[NamedGauge[Long]]
+          TrackedGauge(namedGauge, holder, 
ConcurrentHashMap.newKeySet[String]())
+        }
+        tracked.contributingAppIds.add(appId)
+        tracked.handle.set(value)
+        tracked
+      })
+  }
+
+  protected def addOrUpdateCounterForApp(
+      name: String,
+      labels: Map[String, String],
+      appId: String,
+      delta: Long): Unit = {
+    if (delta <= 0) {
+      return
+    }
+    val key = metricNameWithCustomizedLabels(name, labels)
+    namedCountersWithDetails.compute(
+      key,
+      (_, existing) => {
+        val tracked = Option(existing).getOrElse(
+          TrackedCounter(addCounter(name, labels), 
ConcurrentHashMap.newKeySet[String]()))
+        tracked.contributingAppIds.add(appId)
+        tracked.namedCounter.counter.inc(delta)
+        tracked
+      })

Review Comment:
   Same Scala-2.11 issue as above: `ConcurrentHashMap.compute` requires a 
`java.util.function.BiFunction`, but a Scala function literal is passed and 
there is no implicit converter for `BiFunction` in `FunctionConverter`. This 
will break Scala 2.11 compilation.



##########
common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala:
##########
@@ -277,6 +344,45 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
     metricNameWithLabel
   }
 
+  protected def removeAppFromMetrics(appId: String): Unit = {
+    removeAppFromTrackedGauges(appId)
+    removeAppFromTrackedCounters(appId)
+  }
+
+  private def removeAppFromTrackedGauges(appId: String): Unit = {
+    namedGaugesWithDetails.keySet().asScala.toList.foreach { key =>
+      namedGaugesWithDetails.computeIfPresent(
+        key,
+        (_, tracked) => {
+          tracked.contributingAppIds.remove(appId)
+          if (tracked.contributingAppIds.isEmpty) {
+            namedGauges.remove(key)
+            metricRegistry.remove(key)
+            null
+          } else {
+            tracked
+          }
+        })
+    }
+  }
+
+  private def removeAppFromTrackedCounters(appId: String): Unit = {
+    namedCountersWithDetails.keySet().asScala.toList.foreach { key =>
+      namedCountersWithDetails.computeIfPresent(
+        key,
+        (_, tracked) => {
+          tracked.contributingAppIds.remove(appId)
+          if (tracked.contributingAppIds.isEmpty) {
+            namedCounters.remove(key)
+            metricRegistry.remove(key)
+            null
+          } else {
+            tracked
+          }
+        })
+    }

Review Comment:
   Same Scala-2.11 compilation issue here: `computeIfPresent` requires 
`java.util.function.BiFunction`, but a Scala function literal is passed. This 
will break cross-building with Scala 2.11.



##########
common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala:
##########
@@ -277,6 +344,45 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
     metricNameWithLabel
   }
 
+  protected def removeAppFromMetrics(appId: String): Unit = {
+    removeAppFromTrackedGauges(appId)
+    removeAppFromTrackedCounters(appId)
+  }
+
+  private def removeAppFromTrackedGauges(appId: String): Unit = {
+    namedGaugesWithDetails.keySet().asScala.toList.foreach { key =>
+      namedGaugesWithDetails.computeIfPresent(
+        key,
+        (_, tracked) => {
+          tracked.contributingAppIds.remove(appId)
+          if (tracked.contributingAppIds.isEmpty) {
+            namedGauges.remove(key)
+            metricRegistry.remove(key)
+            null
+          } else {
+            tracked
+          }
+        })
+    }

Review Comment:
   `ConcurrentHashMap.computeIfPresent` also requires a 
`java.util.function.BiFunction`. Passing a Scala function literal will not 
compile under Scala 2.11 (no BiFunction converter is defined). Use an explicit 
`BiFunction` here.



##########
common/src/main/scala/org/apache/celeborn/common/metrics/source/AbstractSource.scala:
##########
@@ -212,6 +234,47 @@ abstract class AbstractSource(conf: CelebornConf, role: 
String)
         labelsWithCustomizedLabels(labels)))
   }
 
+  protected def addOrUpdateGaugeForApp(
+      name: String,
+      labels: Map[String, String],
+      appId: String,
+      value: Long): Unit = {
+    val key = metricNameWithCustomizedLabels(name, labels)
+    namedGaugesWithDetails.compute(
+      key,
+      (_, existing) => {
+        val tracked = Option(existing).getOrElse {
+          val holder = new AtomicLong()
+          addGauge(name, labels)(() => holder.get())
+          val namedGauge = namedGauges.get(key).asInstanceOf[NamedGauge[Long]]
+          TrackedGauge(namedGauge, holder, 
ConcurrentHashMap.newKeySet[String]())
+        }
+        tracked.contributingAppIds.add(appId)
+        tracked.handle.set(value)
+        tracked
+      })

Review Comment:
   `ConcurrentHashMap.compute` expects a `java.util.function.BiFunction`. This 
code passes a Scala function literal, but the repo’s Scala-2.11 compatibility 
shims (`FunctionConverter`) only cover `Function` and `Consumer`, not 
`BiFunction`, so this will fail to compile when cross-building against Scala 
2.11. Use an explicit `java.util.function.BiFunction` here (or add a BiFunction 
converter elsewhere).



-- 
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