From 301d24e88189f9b7703a64ac8c71b8dedd8fc3dd Mon Sep 17 00:00:00 2001 From: architjainjain Date: Mon, 1 Jun 2026 15:21:15 +0530 Subject: [PATCH 1/2] HIVE-27126: queue level resource stats for YARN RM. --- .../logs/BeelineInPlaceUpdateStream.java | 5 + .../hadoop/hive/common/log/InPlaceUpdate.java | 7 + .../hive/common/log/ProgressMonitor.java | 7 + .../org/apache/hadoop/hive/conf/HiveConf.java | 12 + .../hive/common/log/TestInPlaceUpdate.java | 192 ++++++ .../hadoop/hive/ql/exec/tez/TezSession.java | 2 + .../ql/exec/tez/TezSessionPoolSession.java | 6 + .../hive/ql/exec/tez/TezSessionState.java | 44 ++ .../exec/tez/monitoring/RenderStrategy.java | 116 ++-- .../ql/exec/tez/monitoring/TezJobMonitor.java | 118 +++- .../tez/monitoring/TezProgressMonitor.java | 77 +++ .../yarnqueue/NoOpQueueMetricsCollector.java | 51 ++ .../yarnqueue/QueueMetricsCache.java | 144 +++++ .../yarnqueue/QueueMetricsCollector.java | 48 ++ .../yarnqueue/QueueMetricsRefreshPool.java | 200 ++++++ .../yarnqueue/QueueMetricsSnapshot.java | 134 ++++ .../yarnqueue/QueueMetricsState.java | 287 +++++++++ .../yarnqueue/YarnQueueMetricsCollector.java | 204 ++++++ .../hadoop/hive/ql/session/SessionState.java | 5 + .../hive/ql/exec/tez/TestTezSessionState.java | 122 ++++ .../TestTezJobMonitorQueueMetrics.java | 293 +++++++++ .../TestTezProgressMonitorQueueMetrics.java | 462 ++++++++++++++ .../TestNoOpQueueMetricsCollector.java | 147 +++++ .../yarnqueue/TestQueueMetricsCache.java | 340 ++++++++++ .../TestQueueMetricsRefreshPool.java | 364 +++++++++++ .../yarnqueue/TestQueueMetricsState.java | 322 ++++++++++ .../TestYarnQueueMetricsCollector.java | 587 ++++++++++++++++++ service-rpc/if/TCLIService.thrift | 1 + .../rpc/thrift/TProgressUpdateResp.java | 77 ++- .../hive/service/cli/JobProgressUpdate.java | 10 +- .../service/cli/thrift/ThriftCLIService.java | 17 +- .../hive/service/server/HiveServer2.java | 68 +- .../TestHiveServer2QueueMetricsPoolInit.java | 249 ++++++++ 33 files changed, 4663 insertions(+), 55 deletions(-) create mode 100644 common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java create mode 100644 ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java create mode 100644 ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java create mode 100644 ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java create mode 100644 ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java create mode 100644 ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java create mode 100644 ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java create mode 100644 ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java create mode 100644 ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java create mode 100644 ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java create mode 100644 ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java create mode 100644 ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java create mode 100644 ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java create mode 100644 ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java create mode 100644 ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java create mode 100644 service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java diff --git a/beeline/src/java/org/apache/hive/beeline/logs/BeelineInPlaceUpdateStream.java b/beeline/src/java/org/apache/hive/beeline/logs/BeelineInPlaceUpdateStream.java index abf06ff4994f..91e7d0a90fe1 100644 --- a/beeline/src/java/org/apache/hive/beeline/logs/BeelineInPlaceUpdateStream.java +++ b/beeline/src/java/org/apache/hive/beeline/logs/BeelineInPlaceUpdateStream.java @@ -97,5 +97,10 @@ public String executionStatus() { public double progressedPercentage() { return response.getProgressedPercentage(); } + + @Override + public String queueMetrics() { + return response.isSetQueueMetrics() ? response.getQueueMetrics() : ""; + } } } diff --git a/common/src/java/org/apache/hadoop/hive/common/log/InPlaceUpdate.java b/common/src/java/org/apache/hadoop/hive/common/log/InPlaceUpdate.java index 042379aaf9e1..37368dcee469 100644 --- a/common/src/java/org/apache/hadoop/hive/common/log/InPlaceUpdate.java +++ b/common/src/java/org/apache/hadoop/hive/common/log/InPlaceUpdate.java @@ -176,6 +176,13 @@ public void render(ProgressMonitor monitor) { reprintLine(SEPARATOR); reprintLineWithColorAsBold(footer, Ansi.Color.RED); reprintLine(SEPARATOR); + + // Display queue metrics if available (may be multi-line: queue name + metrics) + String queueMetrics = monitor.queueMetrics(); + if (queueMetrics != null && !queueMetrics.isEmpty()) { + reprintMultiLine(queueMetrics); + reprintLine(SEPARATOR); + } } diff --git a/common/src/java/org/apache/hadoop/hive/common/log/ProgressMonitor.java b/common/src/java/org/apache/hadoop/hive/common/log/ProgressMonitor.java index c315896aa4cc..c8463499e660 100644 --- a/common/src/java/org/apache/hadoop/hive/common/log/ProgressMonitor.java +++ b/common/src/java/org/apache/hadoop/hive/common/log/ProgressMonitor.java @@ -53,6 +53,11 @@ public String executionStatus() { public double progressedPercentage() { return 0; } + + @Override + public String queueMetrics() { + return ""; + } }; List headers(); @@ -66,4 +71,6 @@ public double progressedPercentage() { String executionStatus(); double progressedPercentage(); + + String queueMetrics(); } diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java index 301adc2c895d..762b60a4dec1 100644 --- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java +++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java @@ -3941,6 +3941,18 @@ public static enum ConfVars { HIVE_SERVER2_TEZ_QUEUE_ACCESS_CHECK("hive.server2.tez.queue.access.check", false, "Whether to check user access to explicitly specified YARN queues. " + "yarn.resourcemanager.webapp.address must be configured to use this."), + HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL("hive.tez.queue.metrics.refresh.interval", "0s", + new TimeValidator(TimeUnit.SECONDS), + "Interval for refreshing YARN queue resource metrics during Tez query execution. " + + "When set to a positive value (e.g. 10s), displays real-time memory, vCore, capacity " + + "and application metrics for the YARN queue being used. " + + "Set to 0 or negative to disable. Minimum effective value is 1 second."), + HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS("hive.server2.tez.queue.metrics.refresh.threads", 4, + "Number of threads in the scheduled thread pool for refreshing YARN queue metrics. " + + "This pool is used by HiveServer2 to periodically collect queue resource information from YARN RM. " + + "The pool is only created when execution engine is 'tez'. Whether to actually use it for metrics " + + "collection is controlled per-session via hive.tez.queue.metrics.refresh.interval. " + + "In non-Tez environments (e.g., MR, Spark, local), the pool is not initialized."), HIVE_SERVER2_TEZ_SESSION_LIFETIME("hive.server2.tez.session.lifetime", "162h", new TimeValidator(TimeUnit.HOURS), "The lifetime of the Tez sessions launched by HS2 when default sessions are enabled.\n" + diff --git a/common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java b/common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java new file mode 100644 index 000000000000..4d34075fd79d --- /dev/null +++ b/common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java @@ -0,0 +1,192 @@ +/* + * 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.hadoop.hive.common.log; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for InPlaceUpdate + *

+ * We capture stdout via a ByteArrayOutputStream and inspect the rendered output. + * These tests verify the rendering layer integration between ProgressMonitor and + * InPlaceUpdate, particularly focusing on separator line positioning when queue + * metrics are displayed. + */ +public class TestInPlaceUpdate { + + /** + * Minimal ProgressMonitor stub — returns empty headers/rows/footer. + * Queue metrics can be customized per test. + */ + private static ProgressMonitor makeMonitor(String queueMetrics) { + return new ProgressMonitor() { + @Override + public List headers() { + return Arrays.asList("VERTICES", "MODE", "STATUS", "TOTAL", "COMPLETED", "RUNNING", "PENDING", "FAILED", "KILLED"); + } + + @Override + public List> rows() { + return Collections.emptyList(); + } + + @Override + public String footerSummary() { + return "VERTICES: 00/00"; + } + + @Override + public long startTime() { + return System.currentTimeMillis(); + } + + @Override + public double progressedPercentage() { + return 0.0; + } + + @Override + public String executionStatus() { + return "RUNNING"; + } + + @Override + public String queueMetrics() { + return queueMetrics; + } + }; + } + + /** + * Expected separator: 94 dashes (matches MIN_TERMINAL_WIDTH). + */ + private static final String SEPARATOR = + new String(new char[InPlaceUpdate.MIN_TERMINAL_WIDTH]).replace("\0", "-"); + + + /** + * Test #1: When queueMetrics() returns a non-empty string, InPlaceUpdate.render() must + * print a separator line immediately after the metrics block — so total separators + * = 4 (VERTICES table) + 1 (after queue metrics) = 5. + * + * This is the MOST CRITICAL test - verifies line 183 of InPlaceUpdate.java. + */ + @Test + public void testSeparatorPrintedAfterQueueMetrics() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + InPlaceUpdate inPlace = new InPlaceUpdate(ps); + + // Updated to new 4-line format (no staleness in line 1) + String metrics = """ + QUEUE: default + MEMORY: 2.0/8.0 GB (25.00% used) | VCORES: 4/16 (25.00% used) + CAPACITY: 60.00% (allocated) | 25.00% (used) + APPS: 1 running, 0 pending | CONTAINERS: 2 allocated, 0 pending"""; + + inPlace.render(makeMonitor(metrics)); + ps.flush(); + + String output = baos.toString(); + + // The metrics content should appear + assertTrue("Output should contain QUEUE: line", output.contains("QUEUE: default")); + assertTrue("Output should contain MEMORY: line", output.contains("MEMORY: 2.0/8.0 GB")); + assertTrue("Output should contain CAPACITY: line", output.contains("CAPACITY:")); + assertTrue("Output should contain APPS: line", output.contains("APPS:")); + + // The separator must appear AFTER the queue metrics block in the rendered output + // Find the last line of queue metrics (APPS: line) + int appsIdx = output.indexOf("APPS:"); + assertTrue("APPS: line should be found in output", appsIdx > 0); + + // Separator should appear after APPS line + int separatorIdx = output.indexOf(SEPARATOR, appsIdx); + assertTrue("Separator must appear after APPS: line (separatorIdx=" + separatorIdx + + ", appsIdx=" + appsIdx + ")", + separatorIdx > appsIdx); + + // Total separators = 4 (VERTICES table) + 1 (after queue metrics) = 5 + int count = StringUtils.countMatches(output, SEPARATOR); + assertEquals("With queue metrics, total separators should be 5 (4 VERTICES + 1 after metrics)", + 5, count); + } + + + /** + * Test #2: When queueMetrics() returns an empty string, InPlaceUpdate.render() must NOT + * print an extra separator — so total remains 4 (the VERTICES table separators only). + */ + @Test + public void testNoExtraSeparatorWhenQueueMetricsEmpty() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + InPlaceUpdate inPlace = new InPlaceUpdate(ps); + + inPlace.render(makeMonitor("")); + ps.flush(); + + String output = baos.toString(); + + // VERTICES table renders 4 separators (before-header, after-header, before-footer, after-footer) + // With empty queueMetrics there should be exactly 4, not 5. + int count = StringUtils.countMatches(output, SEPARATOR); + assertEquals("With empty queue metrics, only 4 VERTICES-table separators should appear", + 4, count); + } + + + /** + * Test #3: When queueMetrics() returns null, behaviour should be identical to empty. + */ + @Test + public void testNoExtraSeparatorWhenQueueMetricsNull() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + InPlaceUpdate inPlace = new InPlaceUpdate(ps); + + inPlace.render(makeMonitor(null)); + ps.flush(); + + String output = baos.toString(); + + int count = StringUtils.countMatches(output, SEPARATOR); + assertEquals("With null queue metrics, only 4 VERTICES-table separators should appear", + 4, count); + } + + /** + * Test #4: Verify separator constant length matches MIN_TERMINAL_WIDTH. + */ + @Test + public void testSeparatorLengthEqualsMinTerminalWidth() { + assertTrue("Separator should consist only of dashes with length = MIN_TERMINAL_WIDTH", + SEPARATOR.matches("-{" + InPlaceUpdate.MIN_TERMINAL_WIDTH + "}")); + } +} + diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSession.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSession.java index be587236c0b3..1dca30f8b25a 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSession.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSession.java @@ -34,6 +34,7 @@ import org.apache.hadoop.hive.ql.session.SessionState.LogHelper; import org.apache.hadoop.hive.ql.wm.WmContext; import org.apache.hadoop.yarn.api.records.LocalResource; +import org.apache.hadoop.yarn.client.api.YarnClient; import org.apache.tez.client.TezClient; import org.apache.tez.dag.api.DAG; import org.apache.tez.dag.api.TezException; @@ -90,6 +91,7 @@ public String toString() { HiveConf getConf(); TezClient getTezClient(); DAGClient submitDAG(DAG dag) throws TezException, IOException; + YarnClient getYarnClient(); boolean isOpen(); boolean isOpening(); boolean getDoAsEnabled(); diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionPoolSession.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionPoolSession.java index d0c74176f4ea..4147202f2a3c 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionPoolSession.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionPoolSession.java @@ -34,6 +34,7 @@ import org.apache.hadoop.hive.ql.wm.WmContext; import org.apache.hadoop.hive.registry.impl.TezAmInstance; import org.apache.hadoop.yarn.api.records.LocalResource; +import org.apache.hadoop.yarn.client.api.YarnClient; import org.apache.tez.client.TezClient; import org.apache.tez.dag.api.DAG; import org.apache.tez.dag.api.TezException; @@ -340,6 +341,11 @@ public TezClient getTezClient() { return baseSession.getTezClient(); } + @Override + public YarnClient getYarnClient() { + return baseSession.getYarnClient(); + } + @Override public DAGClient submitDAG(DAG dag) throws TezException, IOException { return baseSession.submitDAG(dag); diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionState.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionState.java index e025afdfd69a..e8e665544b2c 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionState.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/TezSessionState.java @@ -74,6 +74,7 @@ import org.apache.hadoop.security.token.Token; import org.apache.hadoop.yarn.api.records.LocalResource; import org.apache.hadoop.yarn.api.records.LocalResourceType; +import org.apache.hadoop.yarn.client.api.YarnClient; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.tez.client.TezClient; import org.apache.tez.common.TezUtils; @@ -120,6 +121,7 @@ public class TezSessionState implements TezSession { Path tezScratchDir; protected LocalResource appJarLr; private TezClient session; + private YarnClient yarnClient; private Future sessionFuture; /** Console used for user feedback during async session opening. */ private LogHelper console; @@ -753,6 +755,17 @@ public void close(boolean keepDagFilesDir) throws Exception { closeClient(asyncSession); } } + + // Stop YarnClient if it was initialized + if (yarnClient != null) { + try { + LOG.info("Stopping YarnClient for session: {}", sessionId); + yarnClient.stop(); + yarnClient = null; + } catch (Exception e) { + LOG.warn("Error stopping YarnClient for session {}: {}", sessionId, e.getMessage()); + } + } } finally { try { cleanupScratchDir(); @@ -823,6 +836,37 @@ public TezClient getTezClient() { return session; } + @Override + public YarnClient getYarnClient() { + // Lazy initialization: only create YarnClient when queue metrics are enabled + if (yarnClient == null && session != null) { + long refreshInterval = HiveConf.getTimeVar(conf, + ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, TimeUnit.MILLISECONDS); + + // Only initialize if metrics are enabled (interval > 0) + if (refreshInterval > 0) { + synchronized (this) { + // Double-check locking pattern + if (yarnClient == null) { + try { + yarnClient = YarnClient.createYarnClient(); + yarnClient.init(conf); + yarnClient.start(); + LOG.info("YarnClient lazily initialized for session: {} (queue metrics enabled with interval: {}ms)", + sessionId, refreshInterval); + } catch (Exception e) { + LOG.warn("Failed to initialize YarnClient for metrics collection", e); + yarnClient = null; + } + } + } + } else { + LOG.debug("YarnClient not initialized: queue metrics disabled (interval: {}ms)", refreshInterval); + } + } + return yarnClient; + } + @Override public DAGClient submitDAG(DAG dag) throws TezException, IOException { return getTezClient().submitDAG(dag); diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/RenderStrategy.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/RenderStrategy.java index 495165fc9467..655904f8b141 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/RenderStrategy.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/RenderStrategy.java @@ -64,8 +64,9 @@ private abstract static class BaseUpdateFunction implements UpdateFunction { @Override public void update(DAGStatus status, Map vertexProgressMap) { - renderProgress(monitor.progressMonitor(status, vertexProgressMap)); - String report = getReport(vertexProgressMap); + ProgressMonitor progressMonitor = monitor.progressMonitor(status, vertexProgressMap); + renderProgress(progressMonitor); + String report = getReport(vertexProgressMap, progressMonitor); if (showReport(report)) { renderReport(report); lastReport = report; @@ -84,58 +85,85 @@ private boolean showReport(String report) { Map 1: 0(+1)/1 Reducer 2: 0/1 Map 1: 1/1 Reducer 2: 0(+1)/1 Map 1: 1/1 Reducer 2: 1/1 + When YARN queue metrics are available, they are appended after the vertex progress, e.g.: + Map 1: 1/1 Reducer 2: 1/1 QUEUE: default | MEMORY: 1.5/5.4 GB (27.78% used) | + VCORES: 3/7 (42.86% used) | CAPACITY: 46.30% (used), 60.00% (allocated) | + APPS: 1 running, 0 pending | CONTAINERS: 4 allocated, 0 pending */ - private String getReport(Map progressMap) { + private String getReport(Map progressMap, ProgressMonitor progressMonitor) { StringWriter reportBuffer = new StringWriter(); SortedSet keys = new TreeSet<>(progressMap.keySet()); for (String s : keys) { Progress progress = progressMap.get(s); - final int complete = progress.getSucceededTaskCount(); - final int total = progress.getTotalTaskCount(); - final int running = progress.getRunningTaskCount(); - final int failed = progress.getFailedTaskAttemptCount(); - if (total <= 0) { - reportBuffer.append(String.format("%s: -/-\t", s)); + appendVertexProgress(reportBuffer, s, progress); + } + + appendQueueMetrics(reportBuffer, progressMonitor); + + return reportBuffer.toString(); + } + + private void appendVertexProgress(StringWriter reportBuffer, String vertexName, Progress progress) { + final int complete = progress.getSucceededTaskCount(); + final int total = progress.getTotalTaskCount(); + final int running = progress.getRunningTaskCount(); + final int failed = progress.getFailedTaskAttemptCount(); + + if (total <= 0) { + reportBuffer.append(String.format("%s: -/-\t", vertexName)); + return; + } + + handlePerfLogging(vertexName, complete, total, running, failed); + appendProgressString(reportBuffer, vertexName, complete, total, running, failed); + } + + private void handlePerfLogging(String vertexName, int complete, int total, int running, int failed) { + String vertexKey = PerfLogger.TEZ_RUN_VERTEX + vertexName; + + if (complete == total) { + // We may have missed the start of the vertex due to the 3 seconds interval + if (!perfLogger.startTimeHasMethod(vertexKey)) { + perfLogger.perfLogBegin(TezJobMonitor.CLASS_NAME, vertexKey); + } + if (!perfLogger.endTimeHasMethod(vertexKey)) { + perfLogger.perfLogEnd(TezJobMonitor.CLASS_NAME, vertexKey); + } + } else if (complete < total && (complete > 0 || running > 0 || failed > 0) + && !perfLogger.startTimeHasMethod(vertexKey)) { + // Vertex is started, but not complete - begin logging + perfLogger.perfLogBegin(TezJobMonitor.CLASS_NAME, vertexKey); + } + } + + private void appendProgressString(StringWriter reportBuffer, String vertexName, + int complete, int total, int running, int failed) { + if (complete < total && (complete > 0 || running > 0 || failed > 0)) { + // Vertex is started, but not complete + if (failed > 0) { + reportBuffer.append(String.format("%s: %d(+%d,-%d)/%d\t", vertexName, complete, running, failed, total)); + } else { + reportBuffer.append(String.format("%s: %d(+%d)/%d\t", vertexName, complete, running, total)); + } + } else { + // Vertex is waiting for input/slots or complete + if (failed > 0) { + reportBuffer.append(String.format("%s: %d(-%d)/%d\t", vertexName, complete, failed, total)); } else { - if (complete == total) { - /* - * We may have missed the start of the vertex due to the 3 seconds interval - */ - if (!perfLogger.startTimeHasMethod(PerfLogger.TEZ_RUN_VERTEX + s)) { - perfLogger.perfLogBegin(TezJobMonitor.CLASS_NAME, PerfLogger.TEZ_RUN_VERTEX + s); - } - - if (!perfLogger.endTimeHasMethod(PerfLogger.TEZ_RUN_VERTEX + s)) { - perfLogger.perfLogEnd(TezJobMonitor.CLASS_NAME, PerfLogger.TEZ_RUN_VERTEX + s); - } - } - if (complete < total && (complete > 0 || running > 0 || failed > 0)) { - - if (!perfLogger.startTimeHasMethod(PerfLogger.TEZ_RUN_VERTEX + s)) { - perfLogger.perfLogBegin(TezJobMonitor.CLASS_NAME, PerfLogger.TEZ_RUN_VERTEX + s); - } - - /* vertex is started, but not complete */ - if (failed > 0) { - reportBuffer.append( - String.format("%s: %d(+%d,-%d)/%d\t", s, complete, running, failed, total)); - } else { - reportBuffer.append(String.format("%s: %d(+%d)/%d\t", s, complete, running, total)); - } - } else { - /* vertex is waiting for input/slots or complete */ - if (failed > 0) { - /* tasks finished but some failed */ - reportBuffer.append(String.format("%s: %d(-%d)/%d\t", s, complete, failed, total)); - } else { - reportBuffer.append(String.format("%s: %d/%d\t", s, complete, total)); - } - } + reportBuffer.append(String.format("%s: %d/%d\t", vertexName, complete, total)); } } + } - return reportBuffer.toString(); + private void appendQueueMetrics(StringWriter reportBuffer, ProgressMonitor progressMonitor) { + if (progressMonitor != null) { + String queueMetrics = progressMonitor.queueMetrics(); + if (queueMetrics != null && !queueMetrics.isEmpty()) { + // Replace newlines with spaces so the whole report stays on a single log line + reportBuffer.append("\t").append(queueMetrics.replace("\n", " | ")); + } + } } abstract void renderProgress(ProgressMonitor progressMonitor); diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java index 23c003a58244..8a0acee2421a 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java @@ -18,6 +18,11 @@ */ package org.apache.hadoop.hive.ql.exec.tez.monitoring; +import org.apache.hadoop.hive.ql.exec.tez.TezSession; +import org.apache.hadoop.hive.ql.exec.tez.Utils; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.NoOpQueueMetricsCollector; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsCollector; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.YarnQueueMetricsCollector; import static org.apache.tez.dag.api.client.DAGStatus.State.RUNNING; @@ -40,9 +45,8 @@ import org.apache.hadoop.hive.conf.HiveConf.ConfVars; import org.apache.hadoop.hive.ql.Context; import org.apache.hadoop.hive.ql.exec.Utilities; -import org.apache.hadoop.hive.ql.exec.tez.TezSession; import org.apache.hadoop.hive.ql.exec.tez.TezSessionPoolManager; -import org.apache.hadoop.hive.ql.exec.tez.Utils; +import org.apache.hadoop.yarn.client.api.YarnClient; import org.apache.hadoop.hive.ql.log.PerfLogger; import org.apache.hadoop.hive.ql.plan.BaseWork; import org.apache.hadoop.hive.ql.session.SessionState; @@ -78,6 +82,7 @@ public class TezJobMonitor { private static final int MAX_CHECK_INTERVAL = 1000; private static final int MAX_RETRY_INTERVAL = 2500; private static final int MAX_RETRY_FAILURES = (MAX_RETRY_INTERVAL / MAX_CHECK_INTERVAL) + 1; + private static final long MIN_QUEUE_METRICS_REFRESH_INTERVAL_MS = 1000L; private final TezSession session; private final PerfLogger perfLogger; @@ -120,6 +125,7 @@ public static void initShutdownHook() { private final RenderStrategy.UpdateFunction updateFunction; // compile time tez counters private final TezCounters counters; + private final QueueMetricsCollector metricsCollector; public TezJobMonitor(TezSession session, List topSortedWorks, final DAGClient dagClient, HiveConf conf, DAG dag, Context ctx, final TezCounters counters, PerfLogger perfLogger) { @@ -135,6 +141,9 @@ public TezJobMonitor(TezSession session, List topSortedWorks, final DA this.counters = counters; this.shouldCollectSummaryString = conf.getBoolVar(HiveConf.ConfVars.HIVE_QUERY_HISTORY_ENABLED) && conf.getBoolVar(ConfVars.HIVE_QUERY_HISTORY_EXEC_SUMMARY_ENABLED); + + // Initialize YARN queue metrics collector if enabled + this.metricsCollector = initializeMetricsCollector(); } private RenderStrategy.UpdateFunction updateFunction() { @@ -145,6 +154,99 @@ private RenderStrategy.UpdateFunction updateFunction() { : new RenderStrategy.LogToFileFunction(this, perfLogger); } + /** + * Initializes the YARN queue metrics collector based on configuration. + * + *

This method implements the Null Object pattern - it always returns a non-null collector. + * When metrics collection is disabled or initialization fails, a no-op collector is returned. + * + *

Metrics collection requires: + *

+ * + *

The refresh interval is validated against {@value #MIN_QUEUE_METRICS_REFRESH_INTERVAL_MS}ms minimum. + * Thread pool management is delegated to {@link QueueMetricsRefreshPool}. + * + * @return {@link YarnQueueMetricsCollector} if enabled, otherwise {@link NoOpQueueMetricsCollector#INSTANCE}. + * Never returns null. + * @see #getValidatedRefreshInterval() + * @see #getValidatedQueueName() + */ + private QueueMetricsCollector initializeMetricsCollector() { + + try { + // Get and validate refresh interval + long refreshInterval = getValidatedRefreshInterval(); + if (refreshInterval <= 0) { + return NoOpQueueMetricsCollector.INSTANCE; + } + + // Get YarnClient from session + YarnClient yarnClient = session.getYarnClient(); + if (yarnClient == null) { + LOG.warn("YarnClient not available, skipping queue metrics collection"); + return NoOpQueueMetricsCollector.INSTANCE; + } + + // Get queue name, default to "default" if not specified + String queueName = getValidatedQueueName(); + + // Get query ID from DAG name + String queryId = dag.getName(); + + LOG.info("Initializing YARN queue metrics collector for queue: {}, refresh interval: {}ms", + queueName, refreshInterval); + + // Pool sizing (topology computation) is delegated to QueueMetricsRefreshPool. + return new YarnQueueMetricsCollector(yarnClient, queueName, refreshInterval, queryId, hiveConf); + } catch (Exception e) { + LOG.warn("Unable to initialize YARN queue metrics collector", e); + return NoOpQueueMetricsCollector.INSTANCE; + } + } + + /** + * Retrieves and validates the queue metrics refresh interval from configuration. + * + * @return validated refresh interval in milliseconds, or -1 if disabled + */ + private long getValidatedRefreshInterval() { + // Get refresh interval — controls whether the feature is enabled. + // interval <= 0 means disabled (default is 0s = disabled). + long refreshInterval = HiveConf.getTimeVar(hiveConf, + ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, TimeUnit.MILLISECONDS); + + if (refreshInterval <= 0) { + LOG.debug("Queue metrics collection disabled (refresh interval: {}ms)", refreshInterval); + return -1; + } + + // Validate minimum refresh interval (at least 1 second) + if (refreshInterval < MIN_QUEUE_METRICS_REFRESH_INTERVAL_MS) { + LOG.warn("Queue metrics refresh interval {}ms is less than minimum {}ms, using {}ms", + refreshInterval, MIN_QUEUE_METRICS_REFRESH_INTERVAL_MS, MIN_QUEUE_METRICS_REFRESH_INTERVAL_MS); + refreshInterval = MIN_QUEUE_METRICS_REFRESH_INTERVAL_MS; + } + + return refreshInterval; + } + + /** + * Retrieves and validates the queue name from the session. + * + * @return validated queue name, defaults to "default" if not specified + */ + private String getValidatedQueueName() { + String queueName = session.getQueueName(); + if (queueName == null || queueName.trim().isEmpty()) { + queueName = "default"; + LOG.info("Queue name not specified. For metrics monitoring, using 'default' as queue name"); + } + return queueName; + } + private boolean isProfilingEnabled() { return HiveConf.getBoolVar(hiveConf, HiveConf.ConfVars.TEZ_EXEC_SUMMARY) || Utilities.isPerfOrAboveLogging(hiveConf); @@ -159,7 +261,6 @@ public int monitorExecution() { DAGStatus status = null; Map vertexProgressMap = null; - long monitorStartTime = System.currentTimeMillis(); synchronized (shutdownList) { shutdownList.add(dagClient); @@ -313,6 +414,15 @@ public int monitorExecution() { synchronized (shutdownList) { shutdownList.remove(dagClient); } + + // Shutdown metrics collector (no-op if disabled) + try { + metricsCollector.shutdown(); + LOG.debug("Shut down metrics collector for queue: {}", metricsCollector.getQueueName()); + } catch (Exception e) { + LOG.warn("Error shutting down queue metrics collector", e); + } + break; } } @@ -531,7 +641,7 @@ public String getDiagnostics() { ProgressMonitor progressMonitor(DAGStatus status, Map progressMap) { try { return new TezProgressMonitor(dagClient, status, topSortedWorks, progressMap, console, - executionStartTime); + executionStartTime, metricsCollector); } catch (IOException | TezException e) { console.printInfo("Getting Progress Information: " + e.getMessage() + " stack trace: " + ExceptionUtils.getStackTrace(e)); diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java index 21af184f0779..aeba1ac375f9 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java @@ -18,7 +18,14 @@ */ package org.apache.hadoop.hive.ql.exec.tez.monitoring; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.NoOpQueueMetricsCollector; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsCollector; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsSnapshot; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hive.common.log.InPlaceUpdate; import org.apache.hadoop.hive.common.log.ProgressMonitor; + import org.apache.hadoop.hive.ql.plan.BaseWork; import org.apache.hadoop.hive.ql.session.SessionState; import org.apache.tez.dag.api.TezException; @@ -41,10 +48,12 @@ public class TezProgressMonitor implements ProgressMonitor { private static final int COLUMN_1_WIDTH = 16; + private static final String QUEUE_UNAVAILABLE_MSG = "QUEUE: unavailable"; private final List topSortedWork; private final SessionState.LogHelper console; private final long executionStartTime; private final DAGStatus status; + private final QueueMetricsCollector metricsCollector; Map vertexStatusMap = new HashMap<>(); Map progressCountsMap = new HashMap<>(); @@ -55,10 +64,19 @@ public class TezProgressMonitor implements ProgressMonitor { TezProgressMonitor(DAGClient dagClient, DAGStatus status, List topSortedWork, Map progressMap, SessionState.LogHelper console, long executionStartTime) throws IOException, TezException { + this(dagClient, status, topSortedWork, progressMap, console, executionStartTime, + NoOpQueueMetricsCollector.INSTANCE); + } + + TezProgressMonitor(DAGClient dagClient, DAGStatus status, List topSortedWork, + Map progressMap, SessionState.LogHelper console, long executionStartTime, + QueueMetricsCollector metricsCollector) + throws IOException, TezException { this.status = status; this.topSortedWork = topSortedWork; this.console = console; this.executionStartTime = executionStartTime; + this.metricsCollector = metricsCollector != null ? metricsCollector : NoOpQueueMetricsCollector.INSTANCE; for (Map.Entry entry : progressMap.entrySet()) { String vertexName = entry.getKey(); progressCountsMap.put(vertexName, new VertexProgress(entry.getValue(), status.getState())); @@ -328,6 +346,65 @@ public int hashCode() { } } + @Override + public String queueMetrics() { + + try { + // If metrics collection is disabled, return empty string (no output) + if (!metricsCollector.isEnabled()) { + return ""; + } + + QueueMetricsSnapshot snapshot = metricsCollector.getLatestSnapshot(); + if (snapshot == null) { + // Enabled but unavailable (error case: RM unreachable, queue not found, etc.) + return QUEUE_UNAVAILABLE_MSG; + } + + // Truncate queue name if too long (leave room for "QUEUE: " prefix) + String displayQueueName = metricsCollector.getQueueName(); + int maxQueueNameLength = InPlaceUpdate.MIN_TERMINAL_WIDTH - "QUEUE: ".length(); + if (displayQueueName.length() > maxQueueNameLength) { + displayQueueName = StringUtils.abbreviate(displayQueueName, maxQueueNameLength); + } + + // Line 1: Queue name + String lineQueueHeader = "QUEUE: " + displayQueueName; + + // Line 2: Memory + VCores (resource usage) + String lineQueueResources = String.format( + "MEMORY: %.1f/%.1f GB (%s used) | VCORES: %d/%d (%s used)", + snapshot.getMemoryUsedGB(), + snapshot.getMemoryTotalGB(), + snapshot.getMemoryPercentage(), + snapshot.getVCoresUsed(), + snapshot.getVCoresTotal(), + snapshot.getVCoresPercentage() + ); + + // Line 3: Capacity (current usage and allocated capacity) + String lineCapacity = String.format( + "CAPACITY: %.2f%% (used), %.2f%% (allocated)", + snapshot.getCurrentCapacityPercentage(), + snapshot.getCapacityPercentage() + ); + + // Line 4: Apps and Containers + String lineAppsAndContainers = String.format( + "APPS: %d running, %d pending | CONTAINERS: %d allocated, %d pending", + snapshot.getRunningApps(), + snapshot.getPendingApps(), + snapshot.getAllocatedContainers(), + snapshot.getPendingContainers() + ); + + return lineQueueHeader + "\n" + lineQueueResources + "\n" + lineCapacity + "\n" + lineAppsAndContainers; + } catch (Exception e) { + console.printInfo("Error formatting queue metrics: " + e.getMessage()); + return QUEUE_UNAVAILABLE_MSG; + } + } + public DAGStatus getStatus() { return status; } diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java new file mode 100644 index 000000000000..01ee53b3da51 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java @@ -0,0 +1,51 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; +/** + * Null Object implementation of {@link QueueMetricsCollector}. + * Used when queue metrics collection is disabled. Provides safe no-op behavior + * so that callers don't need null checks. + * + * Thread-safe singleton following the Null Object pattern. + */ +public final class NoOpQueueMetricsCollector implements QueueMetricsCollector { + /** Singleton instance - thread-safe via static initialization. */ + public static final NoOpQueueMetricsCollector INSTANCE = new NoOpQueueMetricsCollector(); + // Private constructor prevents instantiation + private NoOpQueueMetricsCollector() { + } + @Override + public QueueMetricsSnapshot getLatestSnapshot() { + // No metrics available when collection is disabled + return null; + } + @Override + public String getQueueName() { + // Return empty string instead of null to avoid NPEs + return ""; + } + @Override + public void shutdown() { + // No-op: nothing to shut down + } + @Override + public boolean isEnabled() { + // Metrics collection is disabled for no-op collector + return false; + } +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java new file mode 100644 index 000000000000..5b1198be94dc --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java @@ -0,0 +1,144 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * JVM-wide cache mapping queue names to their {@link QueueMetricsState}. + * Responsible only for entry lifecycle: get, put, placeholder creation, and expiry. + * All per-queue logic lives in {@link QueueMetricsState}. + *

+ * Entries expire after {@value #CACHE_EXPIRE_AFTER_ACCESS_MINUTES} minutes of inactivity + * (no reads or writes). While a queue has active sessions, both the background refresh task + * (via put) and TezProgressMonitor (via get) continuously reset the access timer — the entry + * lives as long as the queue is in use. Once all sessions finish and the refresh task is + * cancelled, no more reads or writes occur and the entry auto-expires after 60 minutes. + * This provides a natural grace period for brief query gaps on the same queue. + */ +public final class QueueMetricsCache { + private static final Logger LOG = LoggerFactory.getLogger(QueueMetricsCache.class); + + private static final QueueMetricsCache INSTANCE = new QueueMetricsCache(); + + // Entries auto-expire after 60 minutes of no reads or writes. + // Active queues: reset continuously by get() (TezProgressMonitor polling) and put() (RM refresh). + // Idle queues: task cancelled + job done → no reads or writes → expires after 60 minutes. + // 60 minutes safely exceeds any realistic refresh interval or progress polling interval. + private static final long CACHE_EXPIRE_AFTER_ACCESS_MINUTES = 60; + + private final Cache cache; + + private QueueMetricsCache() { + this.cache = CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterAccess(CACHE_EXPIRE_AFTER_ACCESS_MINUTES, TimeUnit.MINUTES) + .build(); + + LOG.info("QueueMetricsCache initialized: max=1000, expireAfterAccess={}min", + CACHE_EXPIRE_AFTER_ACCESS_MINUTES); + } + + public static QueueMetricsCache getInstance() { + return INSTANCE; + } + + /** + * Returns the {@link QueueMetricsState} for the given queue, or null if not present. + * Guava resets the expireAfterAccess timer on every call. + */ + public QueueMetricsState get(String queueName) { + if (queueName == null) { + return null; + } + return cache.getIfPresent(queueName); + } + + /** + * Updates the snapshot on an existing entry after a successful RM fetch, or creates + * a new entry if none exists. expireAfterAccess timer resets on this call. + */ + public void put(String queueName, QueueMetricsSnapshot snapshot, + long refreshIntervalMs) { + if (queueName == null || snapshot == null) { + return; + } + QueueMetricsState existing = cache.getIfPresent(queueName); + if (existing == null) { + cache.put(queueName, new QueueMetricsState(snapshot, refreshIntervalMs)); + LOG.debug("Created state entry for queue: {}", queueName); + } else { + existing.applySnapshot(snapshot, refreshIntervalMs); + } + } + + /** + * Atomically creates a placeholder {@link QueueMetricsState} with no snapshot. + * Uses putIfAbsent so concurrent sessions racing to create the first entry are safe: + * exactly one placeholder wins and all others get the same entry back. + * Returns the authoritative state (the one actually in the cache). + */ + public QueueMetricsState putPlaceholder(String queueName, long refreshIntervalMs) { + if (queueName == null) { + return null; + } + // lastWriteTime=0 (epoch) inside QueueMetricsState signals "never written" — + // ensures the first refresh fires immediately (age = now - 0 always exceeds any interval). + QueueMetricsState newState = new QueueMetricsState(null, refreshIntervalMs); + QueueMetricsState existing = cache.asMap().putIfAbsent(queueName, newState); + if (existing != null) { + LOG.debug("State for queue: {} already created by concurrent session, using existing", queueName); + return existing; + } + LOG.debug("Created placeholder state for queue: {}", queueName); + return newState; + } + + /** Returns the number of queues currently tracked in the cache. */ + public int getActiveQueueCount() { + return (int) cache.size(); + } + + /** + * Invalidates all entries. Called on JVM shutdown. + */ + public void shutdown() { + try { + cache.invalidateAll(); + LOG.info("QueueMetricsCache shutdown complete"); + } catch (Exception e) { + LOG.warn("Error during cache shutdown", e); + } + } + + /** + * Resets cache for test isolation. NEVER call in production code. + */ + @VisibleForTesting + public static void resetForTesting() { + INSTANCE.cache.invalidateAll(); + LOG.debug("QueueMetricsCache reset for testing"); + } +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java new file mode 100644 index 000000000000..334fdc67373c --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java @@ -0,0 +1,48 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; +/** + * Interface for collecting YARN queue metrics. + * Implementations include the active collector ({@link YarnQueueMetricsCollector}) + * and a no-op variant ({@link NoOpQueueMetricsCollector}) following the Null Object pattern. + */ +public interface QueueMetricsCollector { + /** + * Returns the latest queue metrics snapshot. + * + * @return snapshot of queue metrics, or null if not available + */ + QueueMetricsSnapshot getLatestSnapshot(); + /** + * Returns the name of the queue being monitored. + * + * @return queue name + */ + String getQueueName(); + /** + * Shuts down the metrics collector and releases resources. + * Safe to call multiple times. + */ + void shutdown(); + /** + * Returns whether queue metrics collection is enabled for this collector. + * + * @return true if metrics collection is active, false if disabled (no-op collector) + */ + boolean isEnabled(); +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java new file mode 100644 index 000000000000..666be60865eb --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java @@ -0,0 +1,200 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Singleton manager for the JVM-wide refresh executor pool used by queue metrics collection. + * Provides a shared {@link ScheduledExecutorService} that fires periodic YARN RM refresh tasks + * across all queries in the HiveServer2 process. + *

+ * Initialized during HiveServer2 startup via {@link #init(int)} when Tez session pool is set up. + * Pool size is configured via {@code hive.server2.tez.queue.metrics.refresh.threads} (default: 4). + *

+ * All {@link YarnQueueMetricsCollector} instances share this single pool, ensuring efficient + * resource usage and preventing thread explosion when many queries run concurrently. + *

+ * Thread-safe singleton implementation using double-check locking pattern. + */ +public final class QueueMetricsRefreshPool { + private static final Logger LOG = LoggerFactory.getLogger(QueueMetricsRefreshPool.class); + + private static final int DEFAULT_THREAD_COUNT = 4; + public static final int JITTER_PERCENT = 10; + + private static final AtomicReference INSTANCE = new AtomicReference<>(null); + private static final Object INIT_LOCK = new Object(); + + private final ScheduledExecutorService refreshPool; + + + /** + * Initializes the singleton pool with the specified thread count. + * Must be called during HiveServer2 startup. Subsequent calls are ignored. + * + * @param threadCount number of threads for the refresh pool + */ + public static void init(int threadCount) { + if (INSTANCE.get() != null) { + LOG.debug("QueueMetricsRefreshPool already initialized, ignoring init call"); + return; + } + synchronized (INIT_LOCK) { + if (INSTANCE.get() == null) { + INSTANCE.set(new QueueMetricsRefreshPool(threadCount)); + } + } + } + + /** + * Returns the singleton instance. Must be called after {@link #init(int)}. + * For tests or non-HS2 environments, lazily initializes with default thread count. + * + * @return the singleton pool instance + */ + public static QueueMetricsRefreshPool getInstance() { + QueueMetricsRefreshPool local = INSTANCE.get(); + if (local != null) { + return local; + } + // Lazy init for tests/non-HS2 with default thread count + synchronized (INIT_LOCK) { + local = INSTANCE.get(); + if (local == null) { + LOG.warn("QueueMetricsRefreshPool not initialized via init(), using default thread count: {}", + DEFAULT_THREAD_COUNT); + local = new QueueMetricsRefreshPool(DEFAULT_THREAD_COUNT); + INSTANCE.set(local); + } + return local; + } + } + + + private QueueMetricsRefreshPool(int threadCount) { + this.refreshPool = Executors.newScheduledThreadPool(threadCount, + new ThreadFactoryBuilder() + .setNameFormat("queue-metrics-refresh-%d") + .setDaemon(true) + .build()); + LOG.info("QueueMetricsRefreshPool initialized with {} threads", threadCount); + } + + + /** + * Schedules a periodic refresh task. initialDelay=0 so the first fetch runs immediately. + * + * @param task the refresh task to schedule + * @param intervalMs the interval in milliseconds between task executions + * @return a ScheduledFuture representing the scheduled task + */ + public ScheduledFuture scheduleRefreshTask(Runnable task, long intervalMs) { + return refreshPool.scheduleWithFixedDelay(task, 0, intervalMs, TimeUnit.MILLISECONDS); + } + + /** + * Shuts down the refresh pool gracefully, waiting up to 10 seconds for in-flight tasks + * to complete before forcing termination. Must be called during HiveServer2 shutdown, + * after all Tez sessions have been stopped. + */ + public static void shutdown() { + synchronized (INIT_LOCK) { + QueueMetricsRefreshPool current = INSTANCE.get(); + if (current == null) { + return; + } + LOG.info("Shutting down QueueMetricsRefreshPool"); + try { + current.refreshPool.shutdown(); + if (!current.refreshPool.awaitTermination(10, TimeUnit.SECONDS)) { + LOG.warn("QueueMetricsRefreshPool did not terminate gracefully, forcing shutdown"); + current.refreshPool.shutdownNow(); + if (!current.refreshPool.awaitTermination(5, TimeUnit.SECONDS)) { + LOG.error("QueueMetricsRefreshPool did not terminate after forced shutdown"); + } + } + LOG.info("QueueMetricsRefreshPool shutdown complete"); + } catch (InterruptedException e) { + LOG.warn("Interrupted during QueueMetricsRefreshPool shutdown", e); + current.refreshPool.shutdownNow(); + Thread.currentThread().interrupt(); + } finally { + INSTANCE.set(null); + } + } + } + + /** + * Calculates deterministic hash-based jitter for a queue name to prevent thundering herd. + * Jitter range: 0 to (intervalMs * JITTER_PERCENT / 100) + * + * @param queueName the queue name to hash + * @param intervalMs the refresh interval in milliseconds + * @return jitter value in milliseconds (0 to intervalMs * 10%) + */ + public static long calculateJitter(String queueName, long intervalMs) { + long jitterWindow = intervalMs * JITTER_PERCENT / 100; + // Guard against zero or negative jitterWindow to prevent ArithmeticException + if (jitterWindow <= 0) { + return 0; + } + // Use bitwise AND to ensure non-negative hash (handles Integer.MIN_VALUE edge case) + int nonNegativeHash = queueName.hashCode() & 0x7fffffff; + return nonNegativeHash % jitterWindow; + } + // ───────────────────────────────────────────────────────── + // Test Support + // ───────────────────────────────────────────────────────── + + /** + * Returns the current singleton instance without lazy initialization. + * Used for testing to verify if the pool was initialized via init(). + * NEVER call in production code - use {@link #getInstance()} instead. + * + * @return the current instance, or null if not initialized + */ + @VisibleForTesting + public static QueueMetricsRefreshPool getInstanceForTesting() { + return INSTANCE.get(); + } + + /** + * Resets the singleton for test isolation. NEVER call in production code. + */ + @VisibleForTesting + public static void resetForTesting() { + synchronized (INIT_LOCK) { + QueueMetricsRefreshPool current = INSTANCE.get(); + if (current != null) { + current.refreshPool.shutdownNow(); + INSTANCE.set(null); + } + } + } +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java new file mode 100644 index 000000000000..2da57ca0760b --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java @@ -0,0 +1,134 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.apache.hadoop.yarn.api.records.QueueInfo; +import org.apache.hadoop.yarn.api.records.QueueStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Immutable snapshot of YARN queue resource metrics at a point in time. + * Created by {@link YarnQueueMetricsCollector} after each successful YARN RM fetch, + * stored in {@link QueueMetricsState}, and read by TezProgressMonitor for display. + */ +public final class QueueMetricsSnapshot { + private static final Logger LOG = LoggerFactory.getLogger(QueueMetricsSnapshot.class); + + private final float memoryUsedGB; + private final float memoryTotalGB; + private final int vCoresUsed; + private final int vCoresTotal; + private final float capacityPercentage; + private final float currentCapacityPercentage; + private final int runningApps; + private final int pendingApps; + private final int allocatedContainers; + private final int pendingContainers; + private final long collectionTimestamp; + + public QueueMetricsSnapshot(QueueInfo queueInfo) { + if (queueInfo == null) { + throw new IllegalArgumentException("QueueInfo cannot be null"); + } + this.collectionTimestamp = System.currentTimeMillis(); + QueueStatistics stats = queueInfo.getQueueStatistics(); + if (stats != null) { + this.memoryUsedGB = stats.getAllocatedMemoryMB() / 1024.0f; + this.memoryTotalGB = (stats.getAllocatedMemoryMB() + stats.getAvailableMemoryMB()) / 1024.0f; + this.vCoresUsed = (int) stats.getAllocatedVCores(); + this.vCoresTotal = (int) (stats.getAllocatedVCores() + stats.getAvailableVCores()); + this.runningApps = (int) stats.getNumAppsRunning(); + this.pendingApps = (int) stats.getNumAppsPending(); + this.allocatedContainers = (int) stats.getAllocatedContainers(); + this.pendingContainers = (int) stats.getPendingContainers(); + } else { + LOG.debug("QueueStatistics is null for queue, using zero values"); + this.memoryUsedGB = 0; + this.memoryTotalGB = 0; + this.vCoresUsed = 0; + this.vCoresTotal = 0; + this.runningApps = 0; + this.pendingApps = 0; + this.allocatedContainers = 0; + this.pendingContainers = 0; + } + this.capacityPercentage = queueInfo.getCapacity() * 100; + this.currentCapacityPercentage = queueInfo.getCurrentCapacity() * 100; + } + + public float getMemoryUsedGB() { + return memoryUsedGB; + } + + public float getMemoryTotalGB() { + return memoryTotalGB; + } + + public int getVCoresUsed() { + return vCoresUsed; + } + + public int getVCoresTotal() { + return vCoresTotal; + } + + public float getCapacityPercentage() { + return capacityPercentage; + } + + public float getCurrentCapacityPercentage() { + return currentCapacityPercentage; + } + + public int getRunningApps() { + return runningApps; + } + + public int getPendingApps() { + return pendingApps; + } + + public int getAllocatedContainers() { + return allocatedContainers; + } + + public int getPendingContainers() { + return pendingContainers; + } + + public long getCollectionTimestamp() { + return collectionTimestamp; + } + + public String getMemoryPercentage() { + if (memoryTotalGB > 0) { + return String.format("%.2f%%", (memoryUsedGB / memoryTotalGB) * 100); + } + return "N/A"; + } + + public String getVCoresPercentage() { + if (vCoresTotal > 0) { + return String.format("%.2f%%", ((float) vCoresUsed / vCoresTotal) * 100); + } + return "N/A"; + } +} + diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java new file mode 100644 index 000000000000..e291e0f0db35 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java @@ -0,0 +1,287 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.OptionalLong; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Holds all runtime state for YARN queue metrics collection on a single queue. + * One instance exists per active queue name in the JVM, stored in {@link QueueMetricsCache}. + *

+ * Owns all per-queue logic: session interval registration, refresh task scheduling, + * thundering herd prevention, and circuit breaker. All fields are private — callers + * interact only through methods. + *

+ * Ownership model: + *

+ */ +public class QueueMetricsState { + private static final Logger LOG = LoggerFactory.getLogger(QueueMetricsState.class); + + private static final int MAX_CONSECUTIVE_FAILURES = 5; + private static final int CIRCUIT_BREAKER_PROBE_INTERVAL = 10; + + // Metrics data (written by refresh thread, read by TezProgressMonitor) + private final AtomicReference snapshot; + private volatile long lastWriteTime; + + // Session interval tracking (lock-free atomics) + private final AtomicLong minRefreshIntervalMs; + private final ConcurrentHashMap intervalCounts = new ConcurrentHashMap<>(); + private final AtomicInteger activeSessionCount = new AtomicInteger(0); + + // Refresh task (owned by ensureTaskScheduled under synchronized(this)) + private final AtomicReference> refreshTask = new AtomicReference<>(null); + private final AtomicLong taskCurrentRefreshIntervalMs; + + // Thundering herd guard + private final AtomicBoolean isRefreshing = new AtomicBoolean(false); + + // Circuit breaker + private final AtomicInteger consecutiveFailures = new AtomicInteger(0); + private final AtomicInteger circuitBreakerSkipCount = new AtomicInteger(0); + + QueueMetricsState(QueueMetricsSnapshot snapshot, long refreshIntervalMs) { + this.snapshot = new AtomicReference<>(snapshot); + this.lastWriteTime = 0L; // epoch = "never written" — ensures first fetch fires immediately + this.minRefreshIntervalMs = new AtomicLong(refreshIntervalMs); + this.taskCurrentRefreshIntervalMs = new AtomicLong(refreshIntervalMs); + } + + /** + * Returns the latest snapshot, or null if not yet fetched. + */ + public QueueMetricsSnapshot getSnapshot() { + return snapshot.get(); + } + + /** + * Returns ms since last successful RM write. Large value on first call (lastWriteTime=0). + */ + public long getAgeMs() { + return System.currentTimeMillis() - lastWriteTime; + } + + /** + * Returns the minimum refresh interval across all active sessions. + */ + public long getMinRefreshIntervalMs() { + return minRefreshIntervalMs.get(); + } + + /** Updates snapshot and lastWriteTime after a successful RM fetch. */ + public void applySnapshot(QueueMetricsSnapshot newSnapshot, + long refreshIntervalMs) { + this.snapshot.set(newSnapshot); + this.lastWriteTime = System.currentTimeMillis(); + minRefreshIntervalMs.updateAndGet(current -> Math.min(current, refreshIntervalMs)); + } + + + /** + * Registers this session's interval. Returns true if rescheduling may be needed + * (no task running, or this session lowered the minimum interval). + * Thread-safe: compute() is atomic per-key; getAndAccumulate returns previous value + * so only the thread that actually lowered the minimum triggers rescheduling. + */ + public boolean registerInterval(long refreshIntervalMs) { + intervalCounts.compute(refreshIntervalMs, (k, existing) -> { + if (existing == null) { + return new AtomicInteger(1); + } + existing.incrementAndGet(); + return existing; + }); + long prevMin = minRefreshIntervalMs.getAndAccumulate(refreshIntervalMs, Math::min); + int count = activeSessionCount.incrementAndGet(); + LOG.debug("Session registered at {}ms, activeCount={}", refreshIntervalMs, count); + return refreshTask.get() == null || refreshIntervalMs < prevMin; + } + + /** + * Deregisters this session's interval. Returns true if the task interval may need + * to change (this thread removed the last session at or below the current task interval). + * Thread-safe: compute() atomically decrements and conditionally removes the bucket. + */ + public boolean deregisterInterval(long refreshIntervalMs) { + boolean[] thisBucketRemoved = {false}; + intervalCounts.compute(refreshIntervalMs, (k, existing) -> { + if (existing == null) { + return null; // already removed by concurrent deregister + } + if (existing.decrementAndGet() <= 0) { + thisBucketRemoved[0] = true; + return null; // atomically removes the key + } + return existing; + }); + int remaining = activeSessionCount.updateAndGet(c -> Math.max(0, c - 1)); + LOG.debug("Session deregistered at {}ms, activeCount={}", refreshIntervalMs, remaining); + + if (!thisBucketRemoved[0]) { + return false; // other sessions still at this interval — task unchanged + } + long currentTaskInterval = taskCurrentRefreshIntervalMs.get(); + if (refreshIntervalMs > currentTaskInterval) { + return false; // our interval was slower than the task — removing it changes nothing + } + // Recompute new minimum from remaining buckets. + // orElse(MAX_VALUE): no sessions left — safe sentinel that won't corrupt concurrent registers. + long newMin = intervalCounts.keySet().stream() + .mapToLong(Long::longValue).min().orElse(Long.MAX_VALUE); + // updateAndGet(Math.min): don't overwrite a lower value a concurrent register may have set. + minRefreshIntervalMs.updateAndGet(current -> Math.min(current, newMin)); + return newMin != currentTaskInterval; + } + + + /** + * Ensures the shared refresh task fires at the minimum interval derived from + * active sessions. Cancels and reschedules only when the interval actually changed. + * Serialized under {@code synchronized(this)} — the single scheduling authority for + * this queue. Re-reads intervalCounts inside the lock for authoritative state. + * + * @param poolManager pool that owns the scheduled executor + * @param refreshTask the refresh runnable bound to the calling collector + * @param queueName used for logging only + */ + public void ensureTaskScheduled(QueueMetricsRefreshPool poolManager, + Runnable refreshTask, String queueName) { + synchronized (this) { + OptionalLong minOptional = intervalCounts.keySet().stream() + .mapToLong(Long::longValue).min(); + ScheduledFuture currentTask = this.refreshTask.get(); + + if (minOptional.isEmpty()) { + if (currentTask != null) { + currentTask.cancel(false); + this.refreshTask.set(null); + LOG.info("Cancelled refresh task for queue: {} — no active sessions remaining", queueName); + } + return; + } + + long desiredInterval = minOptional.getAsLong(); + long currentInterval = taskCurrentRefreshIntervalMs.get(); + + if (currentTask == null) { + scheduleTask(poolManager, refreshTask, desiredInterval, queueName); + } else if (currentInterval != desiredInterval) { + currentTask.cancel(false); + this.refreshTask.set(null); + scheduleTask(poolManager, refreshTask, desiredInterval, queueName); + LOG.info("Rescheduled refresh task for queue: {} from {}ms to {}ms", + queueName, currentInterval, desiredInterval); + } else { + LOG.debug("Refresh task for queue: {} already at correct interval {}ms", + queueName, currentInterval); + } + } + } + + private void scheduleTask(QueueMetricsRefreshPool poolManager, Runnable task, + long intervalMs, String queueName) { + long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + long intervalWithJitter = intervalMs + jitter; + ScheduledFuture newTask = poolManager.scheduleRefreshTask(task, intervalWithJitter); + refreshTask.set(newTask); + taskCurrentRefreshIntervalMs.set(intervalMs); + LOG.info("Scheduled refresh task for queue: {} at {}ms interval (base: {}ms, jitter: +{}ms)", + queueName, intervalWithJitter, intervalMs, jitter); + } + + /** + * Returns true if this thread successfully claimed the refresh lock. + */ + public boolean tryStartRefresh() { + return isRefreshing.compareAndSet(false, true); + } + + /** + * Releases the refresh lock. Always call in a finally block after tryStartRefresh(). + */ + public void finishRefresh() { + isRefreshing.set(false); + } + + + /** + * Returns true if the circuit breaker is active and this tick should be skipped. + * Allows one probe attempt every {@value #CIRCUIT_BREAKER_PROBE_INTERVAL} ticks. + */ + public boolean shouldSkipDueToCircuitBreaker(String queueName) { + if (consecutiveFailures.get() < MAX_CONSECUTIVE_FAILURES) { + return false; + } + int skipCount = circuitBreakerSkipCount.incrementAndGet(); + if (skipCount % CIRCUIT_BREAKER_PROBE_INTERVAL == 0) { + LOG.debug("Circuit breaker active for queue: {}, probe attempt (tick {})", queueName, skipCount); + return false; + } + LOG.debug("Circuit breaker active for queue: {}, skipping (tick {})", queueName, skipCount); + return true; + } + + /** + * Records a refresh failure and activates the circuit breaker after threshold. + */ + public void recordRefreshFailure(String queueName, String reason) { + int failures = consecutiveFailures.incrementAndGet(); + if (failures < MAX_CONSECUTIVE_FAILURES) { + LOG.warn("Failed to refresh queue metrics for queue: {} (failure {} of {}): {}", + queueName, failures, MAX_CONSECUTIVE_FAILURES, reason); + } else if (failures == MAX_CONSECUTIVE_FAILURES) { + LOG.warn("Queue metrics collection failing repeatedly for queue: {} ({} consecutive failures). " + + "Circuit breaker activated — probing every {} ticks.", + queueName, failures, CIRCUIT_BREAKER_PROBE_INTERVAL); + } else { + LOG.debug("Queue metrics refresh still failing for queue: {} (failure {}): {}", + queueName, failures, reason); + } + } + + /** + * Resets circuit breaker state after a successful RM fetch. + */ + public void recordRefreshSuccess(String queueName) { + if (consecutiveFailures.get() > 0) { + LOG.info("Queue metrics collection recovered for queue: {} after {} failures", + queueName, consecutiveFailures.get()); + consecutiveFailures.set(0); + circuitBreakerSkipCount.set(0); + } + } +} + diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java new file mode 100644 index 000000000000..09bbed74c912 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java @@ -0,0 +1,204 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.yarn.api.records.QueueInfo; +import org.apache.hadoop.yarn.client.api.YarnClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Collects YARN queue resource metrics using a shared cache to reduce ResourceManager load. + * Coordinates with other collectors via QueueMetricsCache to prevent duplicate RM calls. + * + * Executor pool management (sizing, lifecycle, JMX) is fully delegated to + * {@link QueueMetricsRefreshPool}. This class focuses solely on per-query/per-queue + * metrics logic: session registration, refresh scheduling, and cache coordination. + */ +public class YarnQueueMetricsCollector implements QueueMetricsCollector { + private static final Logger LOG = LoggerFactory.getLogger(YarnQueueMetricsCollector.class); + + private final YarnClient yarnClient; + private final String queueName; + private final long refreshIntervalMs; + private final String queryId; + + + /** + * Creates a collector for the given queue and query. Non-blocking: metrics are + * fetched asynchronously; the first progress update may show no metrics, subsequent + * updates will once the first fetch completes (within one refreshIntervalMs). + * + * @param yarnClient Live YarnClient from the Tez session + * @param queueName YARN queue this query runs on + * @param refreshIntervalMs How often to poll YARN RM (ms) + * @param queryId DAG name for logging + * @param hiveConf Unused (kept for API compatibility) + */ + public YarnQueueMetricsCollector(YarnClient yarnClient, String queueName, long refreshIntervalMs, String queryId, + HiveConf hiveConf) { + if (yarnClient == null) { + throw new IllegalArgumentException("YarnClient cannot be null"); + } + if (queueName == null) { + throw new IllegalArgumentException("Queue name cannot be null"); + } + if (refreshIntervalMs <= 0) { + throw new IllegalArgumentException("refreshIntervalMs must be > 0, got: " + refreshIntervalMs); + } + + this.yarnClient = yarnClient; + this.queueName = queueName; + this.refreshIntervalMs = refreshIntervalMs; + this.queryId = queryId; + + + // Register session and start background refresh scheduling. + initializeSession(); + + LOG.info("Started queue metrics collector for queue: {}, refresh interval: {}ms, query: {}", queueName, + refreshIntervalMs, queryId); + } + + + /** + * Startup sequence: get or create the cache entry for {@code queueName}, + * register this session's interval, then schedule the refresh task if needed. + * Concurrent safety: {@link QueueMetricsCache#putPlaceholder} uses + * {@code putIfAbsent} — two threads seeing null both get back the same entry. + */ + private void initializeSession() { + QueueMetricsCache cache = QueueMetricsCache.getInstance(); + QueueMetricsState state = cache.get(queueName); + if (state == null) { + state = cache.putPlaceholder(queueName, refreshIntervalMs); + } + if (state.registerInterval(refreshIntervalMs)) { + state.ensureTaskScheduled(QueueMetricsRefreshPool.getInstance(), this::refreshMetrics, queueName); + } + } + + + private void refreshMetrics() { + try { + QueueMetricsState state = QueueMetricsCache.getInstance().get(queueName); + if (state == null) { + return; + } + if (state.getAgeMs() < state.getMinRefreshIntervalMs()) { + LOG.debug("Cache entry for queue: {} is fresh, skipping refresh", queueName); + return; + } + if (state.shouldSkipDueToCircuitBreaker(queueName)) { + return; + } + if (!state.tryStartRefresh()) { + LOG.debug("Another collector is refreshing queue: {}, skipping", queueName); + return; + } + performRefresh(state); + } catch (Exception e) { + LOG.error("Unexpected error in refresh task for queue: {}", queueName, e); + } + } + + /** + * Performs the actual refresh operation: fetches metrics from RM and updates cache. + * Handles refresh lock via try-finally to ensure cleanup even on failure. + * + * @param state the queue metrics state holding the refresh lock + */ + private void performRefresh(QueueMetricsState state) { + try { + QueueMetricsSnapshot snapshot = fetchFromRM(); + if (snapshot != null) { + QueueMetricsCache.getInstance().put(queueName, snapshot, refreshIntervalMs); + state.recordRefreshSuccess(queueName); + LOG.debug("Refreshed queue metrics for queue: {}", queueName); + } else { + LOG.warn("YARN RM returned no QueueInfo for queue: {}. Check queue name configuration.", + queueName); + } + } catch (Exception e) { + state.recordRefreshFailure(queueName, e.getMessage()); + } finally { + state.finishRefresh(); + } + } + + private QueueMetricsSnapshot fetchFromRM() { + try { + QueueInfo queueInfo = yarnClient.getQueueInfo(queueName); + return queueInfo != null ? new QueueMetricsSnapshot(queueInfo) : null; + } catch (Exception e) { + LOG.debug("Failed to fetch queue info from RM for queue: {}: {}", queueName, e.getMessage()); + throw new RuntimeException("RM fetch failed", e); + } + } + + + /** + * Returns the latest snapshot from cache (non-blocking). Null if not yet available. + */ + @Override + public QueueMetricsSnapshot getLatestSnapshot() { + QueueMetricsState state = QueueMetricsCache.getInstance().get(queueName); + return state != null ? state.getSnapshot() : null; + } + + /** + * Returns the queue name being monitored. + */ + @Override + public String getQueueName() { + return queueName; + } + + /** + * Called when the query finishes. Deregisters this collector's refresh interval from + * the shared cache state. If other collectors are still monitoring this queue, the + * refresh task continues at the minimum interval among remaining collectors. + *

+ * Does NOT invalidate the cache entry - it remains available for other queries + * and automatically expires after 60 minutes of inactivity. + */ + @Override + public void shutdown() { + QueueMetricsState state = QueueMetricsCache.getInstance().get(queueName); + if (state == null) { + LOG.info("Cache entry already cleared for queue: {} on shutdown of query: {}", queueName, queryId); + return; + } + LOG.info("Query finished for queue: {}, query: {}", queueName, queryId); + if (state.deregisterInterval(refreshIntervalMs)) { + state.ensureTaskScheduled(QueueMetricsRefreshPool.getInstance(), this::refreshMetrics, queueName); + } + } + + /** + * Returns true indicating metrics collection is enabled. + */ + @Override + public boolean isEnabled() { + return true; + } +} + diff --git a/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java b/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java index 3e7185d88b40..021a7f63e9b6 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/session/SessionState.java @@ -2252,6 +2252,11 @@ public String executionStatus() { public double progressedPercentage() { return percentage; } + + @Override + public String queueMetrics() { + return ""; + } }; } diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java index 4be18c545875..d357f25c4171 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java @@ -24,15 +24,19 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.apache.hadoop.hive.conf.HiveConf; import org.apache.hadoop.hive.conf.HiveConfForTest; import org.apache.hadoop.hive.ql.session.SessionState; import org.apache.hadoop.yarn.api.records.LocalResource; +import org.apache.hadoop.yarn.client.api.YarnClient; +import org.apache.tez.client.TezClient; import org.apache.tez.dag.api.TezException; import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -135,4 +139,122 @@ void openInternalUnsafe(boolean isAsync, SessionState.LogHelper console) { sessionStateForTest.open(resources); } + + /** + * Tests that YarnClient is NOT initialized when queue metrics are disabled (default: interval=0). + * This ensures zero overhead when the feature is disabled. + */ + @Test + public void testYarnClientNotInitializedWhenMetricsDisabled() { + SessionState ss = createSessionState(); + HiveConf hiveConf = ss.getConf(); + + // Default config: queue metrics disabled (interval = 0) + Assert.assertEquals("Default interval should be 0 (disabled)", + 0, HiveConf.getTimeVar(hiveConf, HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, TimeUnit.MILLISECONDS)); + + TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); + + // Mock a TezClient and set it + TezClient mockTezClient = Mockito.mock(TezClient.class); + sessionState.setTezClient(mockTezClient); + + // getYarnClient() should return null when metrics disabled + YarnClient yarnClient = sessionState.getYarnClient(); + Assert.assertNull("YarnClient should not be initialized when queue metrics are disabled", yarnClient); + } + + /** + * Tests that YarnClient IS lazily initialized when queue metrics are enabled. + * This ensures the client is created only when needed. + */ + @Test + public void testYarnClientLazilyInitializedWhenMetricsEnabled() { + SessionState ss = createSessionState(); + HiveConf hiveConf = ss.getConf(); + + // Enable queue metrics with a positive interval + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + + TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); + + // Mock a TezClient and set it + TezClient mockTezClient = Mockito.mock(TezClient.class); + sessionState.setTezClient(mockTezClient); + + // First call to getYarnClient() should initialize it + YarnClient yarnClient = sessionState.getYarnClient(); + Assert.assertNotNull("YarnClient should be initialized when queue metrics are enabled", yarnClient); + + // Second call should return the same instance + YarnClient yarnClient2 = sessionState.getYarnClient(); + Assert.assertSame("Should return the same YarnClient instance", yarnClient, yarnClient2); + } + + /** + * Tests that YarnClient is not initialized when TezClient is null, + * even if queue metrics are enabled. + */ + @Test + public void testYarnClientNotInitializedWhenTezClientNull() { + SessionState ss = createSessionState(); + HiveConf hiveConf = ss.getConf(); + + // Enable queue metrics + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + + TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); + + // Don't set TezClient (session is null) + + // getYarnClient() should return null when TezClient is not set + YarnClient yarnClient = sessionState.getYarnClient(); + Assert.assertNull("YarnClient should not be initialized when TezClient is null", yarnClient); + } + + /** + * Tests the thread-safety of lazy YarnClient initialization with concurrent calls. + */ + @Test + public void testYarnClientLazyInitializationThreadSafety() throws InterruptedException { + SessionState ss = createSessionState(); + HiveConf hiveConf = ss.getConf(); + + // Enable queue metrics + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + + TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); + TezClient mockTezClient = Mockito.mock(TezClient.class); + sessionState.setTezClient(mockTezClient); + + // Create multiple threads that call getYarnClient() concurrently + final int threadCount = 10; + Thread[] threads = new Thread[threadCount]; + YarnClient[] clients = new YarnClient[threadCount]; + + for (int i = 0; i < threadCount; i++) { + final int index = i; + threads[i] = new Thread(() -> { + clients[index] = sessionState.getYarnClient(); + }); + } + + // Start all threads + for (Thread thread : threads) { + thread.start(); + } + + // Wait for all threads to complete + for (Thread thread : threads) { + thread.join(); + } + + // All threads should get the same YarnClient instance + YarnClient firstClient = clients[0]; + Assert.assertNotNull("YarnClient should be initialized", firstClient); + + for (int i = 1; i < threadCount; i++) { + Assert.assertSame("All threads should get the same YarnClient instance", firstClient, clients[i]); + } + } } \ No newline at end of file diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java new file mode 100644 index 000000000000..16bd27cd69d6 --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java @@ -0,0 +1,293 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring; + +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.NoOpQueueMetricsCollector; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsCollector; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.YarnQueueMetricsCollector; + +import java.lang.reflect.Field; + +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.conf.HiveConfForTest; +import org.apache.hadoop.hive.ql.Context; +import org.apache.hadoop.hive.ql.exec.tez.TezSession; +import org.apache.hadoop.hive.ql.log.PerfLogger; +import org.apache.hadoop.hive.ql.plan.BaseWork; +import org.apache.hadoop.hive.ql.session.SessionState; +import org.apache.hadoop.yarn.client.api.YarnClient; +import org.apache.tez.common.counters.TezCounters; +import org.apache.tez.dag.api.DAG; +import org.apache.tez.dag.api.client.DAGClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test cases for TezJobMonitor queue metrics initialization. + */ +public class TestTezJobMonitorQueueMetrics { + + @Mock + private TezSession mockSession; + @Mock + private DAGClient mockDagClient; + @Mock + private DAG mockDag; + @Mock + private Context mockContext; + @Mock + private PerfLogger mockPerfLogger; + @Mock + private YarnClient mockYarnClient; + @Mock + private TezCounters mockCounters; + + private HiveConf hiveConf; + private List topSortedWorks; + private SessionState sessionState; + private AutoCloseable mockCloseable; + + @Before + public void setUp() { + mockCloseable = MockitoAnnotations.openMocks(this); + hiveConf = new HiveConfForTest(TestTezJobMonitorQueueMetrics.class); + hiveConf.set("hive.security.authorization.manager", + "org.apache.hadoop.hive.ql.security.authorization.plugin.sqlstd.SQLStdConfOnlyAuthorizerFactory"); + sessionState = SessionState.start(hiveConf); + topSortedWorks = new ArrayList<>(); + when(mockDag.getName()).thenReturn("test-dag-1"); + } + + @After + public void tearDown() throws Exception { + if (mockCloseable != null) { + mockCloseable.close(); + } + if (sessionState != null) { + sessionState.close(); + } + } + + @Test + public void testMetricsCollectorDisabledByDefault() throws Exception { + when(mockSession.getYarnClient()).thenReturn(null); // YarnClient should not be initialized + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor should be created", monitor); + // When metrics are disabled (interval=0), getYarnClient() is never called because + // the check happens before attempting to retrieve the YarnClient + verify(mockSession, never()).getYarnClient(); + verify(mockYarnClient, never()).getQueueInfo(anyString()); + } + + @Test + public void testMetricsCollectorEnabledWithInterval() { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor should be created", monitor); + verify(mockSession, atLeastOnce()).getYarnClient(); + } + + @Test + public void testMetricsCollectorDisabledWithZeroInterval() throws Exception { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 0, TimeUnit.SECONDS); + + when(mockSession.getYarnClient()).thenReturn(null); // YarnClient should not be initialized + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor should be created", monitor); + // When metrics are disabled (interval=0), getYarnClient() is never called + verify(mockSession, never()).getYarnClient(); + verify(mockYarnClient, never()).getQueueInfo(anyString()); + } + + @Test + public void testMetricsCollectorDisabledWithNegativeInterval() throws Exception { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, -1, TimeUnit.SECONDS); + + when(mockSession.getYarnClient()).thenReturn(null); // YarnClient should not be initialized + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor should be created", monitor); + // When metrics are disabled (interval<0), getYarnClient() is never called + verify(mockSession, never()).getYarnClient(); + verify(mockYarnClient, never()).getQueueInfo(anyString()); + } + + @Test + public void testMetricsCollectorWithSmallInterval() { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 500, TimeUnit.MILLISECONDS); + + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor should be created with adjusted interval", monitor); + } + + @Test + public void testMetricsCollectorWithCustomQueue() { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 15, TimeUnit.SECONDS); + + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn("production.analytics"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + verify(mockSession, atLeastOnce()).getQueueName(); + assertNotNull("Monitor should be created with custom queue", monitor); + } + + /** + * Metrics enabled with a null YarnClient: monitor must still be created and must + * reach the YarnClient gate (verify getYarnClient called), but must NOT call + * getQueueName (nothing to resolve without a client). + */ + @Test + public void testMetricsCollectorWithNullYarnClient() { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + when(mockSession.getYarnClient()).thenReturn(null); + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor must be created when YarnClient is null", monitor); + verify(mockSession, atLeastOnce()).getYarnClient(); + } + + /** + * Metrics enabled with a null queue name: monitor must be created and the code + * must reach both the YarnClient and queue-name gates. + */ + @Test + public void testMetricsCollectorWithNullQueueName() { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn(null); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor must be created when queue name is null", monitor); + verify(mockSession, atLeastOnce()).getYarnClient(); + verify(mockSession, atLeastOnce()).getQueueName(); + } + + /** + * Metrics enabled with a blank (whitespace-only) queue name: monitor must be + * created and both client and queue-name gates must be reached. + */ + @Test + public void testMetricsCollectorWithBlankQueueName() { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn(" "); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + assertNotNull("Monitor must be created when queue name is blank", monitor); + verify(mockSession, atLeastOnce()).getYarnClient(); + verify(mockSession, atLeastOnce()).getQueueName(); + } + + @Test + public void testMetricsCollectorTypeWhenEnabled() throws Exception { + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + // Use reflection to access metricsCollector field and verify type + Field collectorField = TezJobMonitor.class.getDeclaredField("metricsCollector"); + collectorField.setAccessible(true); + QueueMetricsCollector collector = (QueueMetricsCollector) collectorField.get(monitor); + + assertTrue("Should return YarnQueueMetricsCollector when enabled", + collector instanceof YarnQueueMetricsCollector); + assertTrue("Collector should report as enabled", collector.isEnabled()); + } + + @Test + public void testMetricsCollectorTypeWhenDisabled() throws Exception { + // Default config has interval = 0 (disabled) + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn("default"); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + // Use reflection to access metricsCollector field and verify type + Field collectorField = TezJobMonitor.class.getDeclaredField("metricsCollector"); + collectorField.setAccessible(true); + QueueMetricsCollector collector = (QueueMetricsCollector) collectorField.get(monitor); + + assertTrue("Should return NoOpQueueMetricsCollector when disabled", + collector instanceof NoOpQueueMetricsCollector); + assertFalse("Collector should report as disabled", collector.isEnabled()); + } +} diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java new file mode 100644 index 000000000000..313d5349981c --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java @@ -0,0 +1,462 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring; + +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsCollector; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsSnapshot; +import org.apache.hadoop.hive.ql.plan.BaseWork; +import org.apache.hadoop.hive.ql.session.SessionState; +import org.apache.tez.dag.api.client.DAGClient; +import org.apache.tez.dag.api.client.DAGStatus; +import org.apache.tez.dag.api.client.Progress; +import org.apache.hadoop.hive.common.log.InPlaceUpdate; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +/** + * Test cases for TezProgressMonitor queue metrics functionality. + */ +public class TestTezProgressMonitorQueueMetrics { + + @Mock + private DAGClient mockDagClient; + + @Mock + private DAGStatus mockDagStatus; + + @Mock + private QueueMetricsCollector mockMetricsCollector; + + @Mock + private QueueMetricsSnapshot mockSnapshot; + + @Mock + private SessionState.LogHelper mockConsole; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + // Default: metrics are enabled (can be overridden in individual tests) + when(mockMetricsCollector.isEnabled()).thenReturn(true); + } + + @Test + public void testQueueMetricsWithNullCollector() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), null); + + String result = monitor.queueMetrics(); + + assertEquals("Should return empty string when collector is null", "", result); + } + + @Test + public void testQueueMetricsUnavailableWhenSnapshotNull() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.isEnabled()).thenReturn(true); // Enabled but snapshot unavailable + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(null); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + + assertEquals("Should return 'unavailable' when enabled but snapshot is null", + "QUEUE: unavailable", result); + } + + @Test + public void testQueueMetricsDisabled() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.isEnabled()).thenReturn(false); // Metrics disabled (0s interval) + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(null); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + + assertEquals("Should return empty string when metrics are disabled", + "", result); + } + + @Test + public void testQueueMetricsFormatting() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + // Setup snapshot with known values + long now = System.currentTimeMillis(); + when(mockSnapshot.getMemoryUsedGB()).thenReturn(8.5f); + when(mockSnapshot.getMemoryTotalGB()).thenReturn(16.0f); + when(mockSnapshot.getMemoryPercentage()).thenReturn("53.12%"); + when(mockSnapshot.getVCoresUsed()).thenReturn(100); + when(mockSnapshot.getVCoresTotal()).thenReturn(200); + when(mockSnapshot.getVCoresPercentage()).thenReturn("50.00%"); + when(mockSnapshot.getCapacityPercentage()).thenReturn(60.0f); + when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(25.0f); + when(mockSnapshot.getRunningApps()).thenReturn(5); + when(mockSnapshot.getPendingApps()).thenReturn(2); + when(mockSnapshot.getAllocatedContainers()).thenReturn(12); + when(mockSnapshot.getPendingContainers()).thenReturn(10); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 5000); // 5 seconds ago + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + when(mockMetricsCollector.getQueueName()).thenReturn("default"); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + + // Verify 4-line format (no staleness) + String[] lines = result.split("\n"); + assertEquals("Should have 4 lines", 4, lines.length); + + // Line 1: Queue name (no staleness) + assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: default")); + assertFalse("Line 1 should NOT contain staleness", lines[0].contains("ago")); + + // Line 2: Memory + VCores with "used" label + assertTrue("Line 2 should contain memory info", lines[1].contains("MEMORY: 8.5/16.0 GB")); + assertTrue("Line 2 should contain 'used' label", lines[1].contains("53.12% used")); + assertTrue("Line 2 should contain vCores info", lines[1].contains("VCORES: 100/200")); + assertTrue("Line 2 should contain vCores 'used' label", lines[1].contains("50.00% used")); + + // Line 3: Capacity with (used) and (allocated) labels + assertTrue("Line 3 should contain capacity used", lines[2].contains("CAPACITY: 25.00% (used)")); + assertTrue("Line 3 should contain capacity allocated", lines[2].contains("60.00% (allocated)")); + + // Line 4: Apps and Containers + assertTrue("Line 4 should contain running apps", lines[3].contains("APPS: 5 running")); + assertTrue("Line 4 should contain pending apps", lines[3].contains("2 pending")); + assertTrue("Line 4 should contain allocated containers", lines[3].contains("CONTAINERS: 12 allocated")); + assertTrue("Line 4 should contain pending containers", lines[3].contains("10 pending")); + + } + + @Test + public void testQueueMetricsStalenessBeyond60Seconds() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + long now = System.currentTimeMillis(); + when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); + when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); + when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getVCoresUsed()).thenReturn(10); + when(mockSnapshot.getVCoresTotal()).thenReturn(100); + when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); + when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(10.0f); + when(mockSnapshot.getRunningApps()).thenReturn(1); + when(mockSnapshot.getPendingApps()).thenReturn(0); + when(mockSnapshot.getAllocatedContainers()).thenReturn(2); + when(mockSnapshot.getPendingContainers()).thenReturn(0); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 120000); // 120 seconds ago + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + when(mockMetricsCollector.getQueueName()).thenReturn("default"); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + + String[] lines = result.split("\n"); + assertEquals("Should have 4 lines", 4, lines.length); + // Staleness is removed in new format, so line 1 should only have queue name + assertFalse("Line 1 should NOT show staleness (removed from new format)", lines[0].contains("ago")); + assertTrue("Line 1 should contain QUEUE: default", lines[0].contains("QUEUE: default")); + } + + @Test + public void testQueueNameTruncation() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + long now = System.currentTimeMillis(); + when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); + when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); + when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getVCoresUsed()).thenReturn(10); + when(mockSnapshot.getVCoresTotal()).thenReturn(100); + when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); + when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(10.0f); + when(mockSnapshot.getRunningApps()).thenReturn(1); + when(mockSnapshot.getPendingApps()).thenReturn(0); + when(mockSnapshot.getAllocatedContainers()).thenReturn(2); + when(mockSnapshot.getPendingContainers()).thenReturn(0); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 1000); + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + // Very long queue name + when(mockMetricsCollector.getQueueName()).thenReturn( + "root.production.analytics.data-engineering.team-alpha.project-beta"); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + + String[] lines = result.split("\n"); + assertEquals("Should have 4 lines", 4, lines.length); + + // Line 1 should not exceed separator width (94 chars) - without staleness, more room for queue name + assertTrue("Line 1 should not exceed 94 characters", lines[0].length() <= 94); + + // When the full queue name would cause line 1 overflow, it should be truncated from start with "..." + if (lines[0].contains("...")) { + // Queue name was truncated + assertFalse("Full long queue name should not appear", + lines[0].contains("root.production.analytics.data-engineering.team-alpha.project-beta")); + assertTrue("Truncated queue name should contain ...", lines[0].contains("...")); + } + + // Line 1 should NOT contain staleness (removed in new format) + assertFalse("Line 1 should NOT contain staleness", lines[0].contains("ago")); + + // Line 2 should contain resource info + assertTrue("Line 2 should contain MEMORY", lines[1].contains("MEMORY:")); + + // Line 3 should contain capacity + assertTrue("Line 3 should contain CAPACITY", lines[2].contains("CAPACITY:")); + + } + + @Test + public void testQueueMetricsWithZeroPercentages() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + long now = System.currentTimeMillis(); + when(mockSnapshot.getMemoryUsedGB()).thenReturn(0.0f); + when(mockSnapshot.getMemoryTotalGB()).thenReturn(0.0f); + when(mockSnapshot.getMemoryPercentage()).thenReturn("N/A"); + when(mockSnapshot.getVCoresUsed()).thenReturn(0); + when(mockSnapshot.getVCoresTotal()).thenReturn(0); + when(mockSnapshot.getVCoresPercentage()).thenReturn("N/A"); + when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); + when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(0.0f); + when(mockSnapshot.getRunningApps()).thenReturn(0); + when(mockSnapshot.getPendingApps()).thenReturn(0); + when(mockSnapshot.getAllocatedContainers()).thenReturn(0); + when(mockSnapshot.getPendingContainers()).thenReturn(0); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now); + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + when(mockMetricsCollector.getQueueName()).thenReturn("empty"); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + + String[] lines = result.split("\n"); + assertEquals("Should have 4 lines", 4, lines.length); + // Line 1: queue name (no staleness) + assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: empty")); + assertFalse("Line 1 should NOT contain staleness", lines[0].contains("ago")); + // Line 2: memory + vcores with N/A + assertTrue("Line 2 should contain N/A for memory percentage", lines[1].contains("N/A")); + assertTrue("Line 2 should handle zero values", lines[1].contains("0.0/0.0 GB")); + // Line 3: capacity + assertTrue("Line 3 should contain capacity", lines[2].contains("CAPACITY:")); + // Line 4: apps and containers + assertTrue("Line 4 should contain APPS:", lines[3].contains("APPS:")); + assertTrue("Line 4 should contain CONTAINERS:", lines[3].contains("CONTAINERS:")); + } + + @Test + public void testQueueMetricsExceptionHandling() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenThrow( + new RuntimeException("Unexpected error")); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + + // Should gracefully handle exceptions and return unavailable + assertEquals("Should return unavailable on exception", + "QUEUE: unavailable", result); + } + + @Test + public void testStalenessAtExactly60Seconds() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + long now = System.currentTimeMillis(); + when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); + when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); + when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getVCoresUsed()).thenReturn(10); + when(mockSnapshot.getVCoresTotal()).thenReturn(100); + when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); + when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(25.0f); + when(mockSnapshot.getRunningApps()).thenReturn(1); + when(mockSnapshot.getPendingApps()).thenReturn(0); + when(mockSnapshot.getAllocatedContainers()).thenReturn(2); + when(mockSnapshot.getPendingContainers()).thenReturn(0); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 60000L); // exactly 60s ago + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + when(mockMetricsCollector.getQueueName()).thenReturn("default"); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + String[] lines = result.split("\n"); + + assertEquals("Should have 4 lines", 4, lines.length); + // Staleness is removed in new format + assertFalse("Should NOT show staleness (removed in new format)", lines[0].contains("ago")); + assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: default")); + } + + @Test + public void testStalenessAtZeroSeconds() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + long now = System.currentTimeMillis(); + when(mockSnapshot.getMemoryUsedGB()).thenReturn(2.0f); + when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); + when(mockSnapshot.getMemoryPercentage()).thenReturn("20.00%"); + when(mockSnapshot.getVCoresUsed()).thenReturn(5); + when(mockSnapshot.getVCoresTotal()).thenReturn(50); + when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getCapacityPercentage()).thenReturn(60.0f); + when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(30.0f); + when(mockSnapshot.getRunningApps()).thenReturn(2); + when(mockSnapshot.getPendingApps()).thenReturn(0); + when(mockSnapshot.getAllocatedContainers()).thenReturn(4); + when(mockSnapshot.getPendingContainers()).thenReturn(0); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now); // right now + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + when(mockMetricsCollector.getQueueName()).thenReturn("default"); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + String[] lines = result.split("\n"); + + assertEquals("Should have 4 lines", 4, lines.length); + // Staleness is removed in new format + assertFalse("Should NOT show staleness (removed in new format)", lines[0].contains("ago")); + assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: default")); + } + + @Test + public void testQueueNameExactlyAtMaxLength() throws Exception { + List works = new ArrayList<>(); + Map progressMap = new HashMap<>(); + + long now = System.currentTimeMillis(); + when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); + when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); + when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getVCoresUsed()).thenReturn(1); + when(mockSnapshot.getVCoresTotal()).thenReturn(10); + when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); + when(mockSnapshot.getCapacityPercentage()).thenReturn(40.0f); + when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(20.0f); + when(mockSnapshot.getRunningApps()).thenReturn(0); + when(mockSnapshot.getPendingApps()).thenReturn(0); + when(mockSnapshot.getAllocatedContainers()).thenReturn(0); + when(mockSnapshot.getPendingContainers()).thenReturn(0); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 2000); // not used since staleness removed + + when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + + // Build a queue name that exactly fills the allowed space: + // Line 1 budget: MIN_TERMINAL_WIDTH (94) - "QUEUE: ".length(7) = 87 (no staleness) + int maxLen = InPlaceUpdate.MIN_TERMINAL_WIDTH - "QUEUE: ".length(); + String exactName = "q".repeat(maxLen); // exactly maxLen characters + when(mockMetricsCollector.getQueueName()).thenReturn(exactName); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, works, progressMap, mockConsole, + System.currentTimeMillis(), mockMetricsCollector); + + String result = monitor.queueMetrics(); + String[] lines = result.split("\n"); + + assertEquals("Should have 4 lines", 4, lines.length); + // Exactly at max length — should NOT be truncated + assertFalse("Queue name at exact max length should not be truncated", + lines[0].contains("...")); + assertTrue("Full queue name should appear", lines[0].contains(exactName)); + assertTrue("Line 1 should still be within terminal width", + lines[0].length() <= InPlaceUpdate.MIN_TERMINAL_WIDTH); + // No staleness in new format + assertFalse("Should NOT show staleness", lines[0].contains("ago")); + } +} + diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java new file mode 100644 index 000000000000..525b5ce278b9 --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java @@ -0,0 +1,147 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +/** + * Test cases for NoOpQueueMetricsCollector (Null Object pattern implementation). + */ +public class TestNoOpQueueMetricsCollector { + + @Test + public void testInstanceIsSingleton() { + NoOpQueueMetricsCollector instance1 = NoOpQueueMetricsCollector.INSTANCE; + NoOpQueueMetricsCollector instance2 = NoOpQueueMetricsCollector.INSTANCE; + + assertNotNull("Instance should not be null", instance1); + assertSame("INSTANCE should return same reference", instance1, instance2); + } + + @Test + public void testIsEnabledReturnsFalse() { + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + assertFalse("isEnabled should return false", collector.isEnabled()); + } + + @Test + public void testGetLatestSnapshotReturnsNull() { + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + assertNull("getLatestSnapshot should return null", collector.getLatestSnapshot()); + } + + @Test + public void testGetQueueNameReturnsEmptyString() { + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + String queueName = collector.getQueueName(); + assertNotNull("Queue name should not be null", queueName); + assertEquals("Queue name should be empty string", "", queueName); + } + + @Test + public void testShutdownIsNoOp() { + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + // Should not throw exception + collector.shutdown(); + // Collector still usable after shutdown - confirms it is truly a no-op + assertFalse("Collector should remain disabled after shutdown", collector.isEnabled()); + assertNull("Snapshot should remain null after shutdown", collector.getLatestSnapshot()); + } + + @Test + public void testShutdownIsIdempotent() { + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + // Multiple calls should all be safe no-ops + collector.shutdown(); + collector.shutdown(); + collector.shutdown(); + + // Verify instance still works after shutdown calls + assertFalse("isEnabled should still return false", collector.isEnabled()); + assertNull("getLatestSnapshot should still return null", collector.getLatestSnapshot()); + } + + + @Test + public void testNullObjectPatternAllowsSafePolymorphism() { + // NoOp collector can be used wherever QueueMetricsCollector is expected + // without null checks + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + // Simulate typical usage pattern + if (collector.isEnabled()) { + // This branch should never execute + fail("NoOp collector should never report as enabled"); + } + + // Safe to call getLatestSnapshot without null check on collector + // (though snapshot itself will be null) + assertNull("Snapshot should be null", collector.getLatestSnapshot()); + + // Safe to get queue name without null check + assertNotNull("Queue name should not be null", collector.getQueueName()); + + // Safe to call shutdown without null check + collector.shutdown(); // No exception + } + + @Test + public void testCanBeUsedInPlaceOfNullCollector() { + // Common pattern: use NoOp instead of null to avoid null checks + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + // This would NPE if collector was null + String queueName = collector.getQueueName(); + assertNotNull("Should not throw NPE", queueName); + + boolean enabled = collector.isEnabled(); + assertFalse("Should safely return false", enabled); + } + + @Test + public void testToStringDoesNotThrow() { + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + String str = collector.toString(); + assertNotNull("toString should not return null", str); + } + + @Test + public void testHashCodeIsConsistent() { + NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; + + int hash1 = collector.hashCode(); + int hash2 = collector.hashCode(); + + assertEquals("hashCode should be consistent", hash1, hash2); + } +} + diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java new file mode 100644 index 000000000000..b6e2ccbca823 --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java @@ -0,0 +1,340 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.apache.hadoop.yarn.api.records.QueueInfo; +import org.apache.hadoop.yarn.api.records.QueueStatistics; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Test cases for QueueMetricsCache. + */ +public class TestQueueMetricsCache { + + @Mock + private QueueInfo mockQueueInfo; + + @Mock + private QueueStatistics mockQueueStats; + + private AutoCloseable closeable; + private QueueMetricsCache cache; + + @Before + public void setUp() { + closeable = MockitoAnnotations.openMocks(this); + cache = QueueMetricsCache.getInstance(); + // Note: We can't fully clear the cache between tests since it's a singleton, + // but we use unique queue names per test to avoid interference + } + + @After + public void tearDown() throws Exception { + if (closeable != null) { + closeable.close(); + } + } + + @Test + public void testSingletonInstanceConsistency() { + QueueMetricsCache instance1 = QueueMetricsCache.getInstance(); + QueueMetricsCache instance2 = QueueMetricsCache.getInstance(); + + assertNotNull("Instance should not be null", instance1); + assertSame("getInstance should return same instance", instance1, instance2); + } + + @Test + public void testGetReturnsNullForNonExistentQueue() { + String nonExistentQueue = "test-nonexistent-" + System.nanoTime(); + QueueMetricsState state = cache.get(nonExistentQueue); + + assertNull("Should return null for non-existent queue", state); + } + + @Test + public void testGetReturnsNullForNullQueueName() { + QueueMetricsState state = cache.get(null); + + assertNull("Should return null for null queue name", state); + } + + @Test + public void testPutCreatesNewEntry() { + setupMockQueueInfo(); + String queueName = "test-new-entry-" + System.nanoTime(); + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + + // Verify queue doesn't exist yet + assertNull("Queue should not exist initially", cache.get(queueName)); + + // Put creates new entry + cache.put(queueName, snapshot, 5000L); + + QueueMetricsState state = cache.get(queueName); + assertNotNull("Queue state should exist after put", state); + assertNotNull("Snapshot should be available", state.getSnapshot()); + assertEquals("Memory used should match", 1.0f, state.getSnapshot().getMemoryUsedGB(), 0.01f); + } + + @Test + public void testPutUpdatesExistingEntry() { + setupMockQueueInfo(); + String queueName = "test-update-entry-" + System.nanoTime(); + + // Create initial entry + QueueMetricsSnapshot snapshot1 = new QueueMetricsSnapshot(mockQueueInfo); + cache.put(queueName, snapshot1, 5000L); + + // Update with new snapshot + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(4096L); + QueueMetricsSnapshot snapshot2 = new QueueMetricsSnapshot(mockQueueInfo); + cache.put(queueName, snapshot2, 3000L); + + QueueMetricsState state = cache.get(queueName); + assertNotNull("State should exist", state); + assertEquals("Memory should be updated", 4.0f, state.getSnapshot().getMemoryUsedGB(), 0.01f); + } + + @Test + public void testPutWithNullQueueNameIsNoOp() { + setupMockQueueInfo(); + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + + // Should not throw exception + cache.put(null, snapshot, 5000L); + // Confirms null queue name was silently ignored - no entry created + assertNull("Null queue name should not create a cache entry", cache.get(null)); + } + + @Test + public void testPutWithNullSnapshotIsNoOp() { + String queueName = "test-null-snapshot-" + System.nanoTime(); + + // Should not throw exception + cache.put(queueName, null, 5000L); + + // Queue should not be created + assertNull("Queue should not exist after put with null snapshot", cache.get(queueName)); + } + + @Test + public void testPutPlaceholderCreatesEmptyEntry() { + String queueName = "test-placeholder-" + System.nanoTime(); + + QueueMetricsState state = cache.putPlaceholder(queueName, 10000L); + + assertNotNull("Placeholder state should be created", state); + assertNull("Snapshot should be null initially", state.getSnapshot()); + assertEquals("Min interval should match", 10000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testPutPlaceholderWithNullQueueName() { + QueueMetricsState state = cache.putPlaceholder(null, 5000L); + + assertNull("Should return null for null queue name", state); + } + + @Test + public void testConcurrentPutPlaceholderRaces() throws Exception { + String queueName = "test-concurrent-placeholder-" + System.nanoTime(); + int threadCount = 10; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(threadCount); + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + ConcurrentHashMap results = new ConcurrentHashMap<>(); + + // Launch threads that all try to create placeholder simultaneously + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + executor.submit(() -> { + try { + startLatch.await(); // Wait for signal to start + QueueMetricsState state = cache.putPlaceholder(queueName, 5000L + threadId * 100); + results.put(threadId, state); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneLatch.countDown(); + } + }); + } + + // Start all threads at once + startLatch.countDown(); + + // Wait for completion + assertTrue("Threads should complete", doneLatch.await(5, TimeUnit.SECONDS)); + executor.shutdown(); + + // Verify all threads got the same state instance (putIfAbsent semantics) + QueueMetricsState firstState = results.get(0); + assertNotNull("First state should exist", firstState); + + for (int i = 1; i < threadCount; i++) { + assertSame("All threads should get same state instance", firstState, results.get(i)); + } + + // Verify only one entry exists in cache + QueueMetricsState cachedState = cache.get(queueName); + assertSame("Cached state should match returned state", firstState, cachedState); + } + + @Test + public void testGetActiveQueueCount() { + int initialCount = cache.getActiveQueueCount(); + String queueName1 = "test-count-1-" + System.nanoTime(); + String queueName2 = "test-count-2-" + System.nanoTime(); + + cache.putPlaceholder(queueName1, 5000L); + assertEquals("Count should increase by 1", initialCount + 1, cache.getActiveQueueCount()); + + cache.putPlaceholder(queueName2, 5000L); + assertEquals("Count should increase by 2", initialCount + 2, cache.getActiveQueueCount()); + } + + + @Test + public void testShutdownDoesNotThrow() { + // Should handle gracefully even if called multiple times + cache.shutdown(); + cache.shutdown(); // Second call should also be safe + // After shutdown all entries should be cleared + assertEquals("Cache should be empty after shutdown", 0, cache.getActiveQueueCount()); + } + + @Test + public void testPutPlaceholderThenPutUpdatesSnapshot() { + setupMockQueueInfo(); + String queueName = "test-placeholder-update-" + System.nanoTime(); + + // Create placeholder first + QueueMetricsState state1 = cache.putPlaceholder(queueName, 10000L); + assertNull("Snapshot should be null initially", state1.getSnapshot()); + + // Now put actual snapshot + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + cache.put(queueName, snapshot, 5000L); + + // Get updated state + QueueMetricsState state2 = cache.get(queueName); + assertSame("Should be same state instance", state1, state2); + assertNotNull("Snapshot should now be populated", state2.getSnapshot()); + assertEquals("Memory should match", 1.0f, state2.getSnapshot().getMemoryUsedGB(), 0.01f); + } + + @Test + public void testConcurrentPutAndGetNoDeadlock() throws Exception { + setupMockQueueInfo(); + String queueName = "test-concurrent-ops-" + System.nanoTime(); + int iterationsPerThread = 100; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(3); + ExecutorService executor = Executors.newFixedThreadPool(3); + AtomicInteger successCount = new AtomicInteger(0); + + // Writer thread 1: putPlaceholder + executor.submit(() -> { + try { + startLatch.await(); + for (int i = 0; i < iterationsPerThread; i++) { + cache.putPlaceholder(queueName, 5000L); + } + successCount.incrementAndGet(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + doneLatch.countDown(); + } + }); + + // Writer thread 2: put + executor.submit(() -> { + try { + startLatch.await(); + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + for (int i = 0; i < iterationsPerThread; i++) { + cache.put(queueName, snapshot, 3000L); + } + successCount.incrementAndGet(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + doneLatch.countDown(); + } + }); + + // Reader thread: get + executor.submit(() -> { + try { + startLatch.await(); + for (int i = 0; i < iterationsPerThread; i++) { + cache.get(queueName); + } + successCount.incrementAndGet(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + doneLatch.countDown(); + } + }); + + startLatch.countDown(); + assertTrue("All threads should complete without deadlock", doneLatch.await(10, TimeUnit.SECONDS)); + assertEquals("All threads should succeed", 3, successCount.get()); + + executor.shutdown(); + } + + // Helper method to setup mock QueueInfo with standard values + private void setupMockQueueInfo() { + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(1024L); + when(mockQueueStats.getAvailableMemoryMB()).thenReturn(1024L); + when(mockQueueStats.getAllocatedVCores()).thenReturn(4L); + when(mockQueueStats.getAvailableVCores()).thenReturn(4L); + when(mockQueueStats.getNumAppsRunning()).thenReturn(1L); + when(mockQueueStats.getNumAppsPending()).thenReturn(0L); + when(mockQueueStats.getAllocatedContainers()).thenReturn(2L); + when(mockQueueStats.getPendingContainers()).thenReturn(0L); + when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); + when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.25f); + } +} + diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java new file mode 100644 index 000000000000..00e7b943027c --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java @@ -0,0 +1,364 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.junit.After; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Test cases for QueueMetricsRefreshPool. + */ +public class TestQueueMetricsRefreshPool { + + @After + public void tearDown() { + // Reset singleton after each test for isolation + QueueMetricsRefreshPool.resetForTesting(); + } + + @Test + public void testInitWithCustomThreadCount() { + QueueMetricsRefreshPool.init(8); + QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); + + assertNotNull("Pool should be initialized", pool); + } + + @Test + public void testInitCalledTwiceIgnoresSecondCall() { + QueueMetricsRefreshPool.init(4); + QueueMetricsRefreshPool pool1 = QueueMetricsRefreshPool.getInstance(); + + QueueMetricsRefreshPool.init(10); // Second init should be ignored + QueueMetricsRefreshPool pool2 = QueueMetricsRefreshPool.getInstance(); + + assertSame("Should return same instance", pool1, pool2); + } + + @Test + public void testGetInstanceWithoutInitUsesDefaultThreadCount() { + // Don't call init(), directly call getInstance() + QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); + + assertNotNull("Pool should be lazily initialized", pool); + } + + @Test + public void testGetInstanceReturnsSameInstance() { + QueueMetricsRefreshPool pool1 = QueueMetricsRefreshPool.getInstance(); + QueueMetricsRefreshPool pool2 = QueueMetricsRefreshPool.getInstance(); + + assertSame("getInstance should return same singleton", pool1, pool2); + } + + @Test + public void testCalculateJitterIsDeterministic() { + String queueName = "test-queue"; + long intervalMs = 10000L; + + long jitter1 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + long jitter2 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + + assertEquals("Same queue name should produce same jitter", jitter1, jitter2); + } + + @Test + public void testCalculateJitterIsWithinRange() { + String queueName = "production-queue"; + long intervalMs = 10000L; + long expectedMaxJitter = intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100; + + long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + + assertTrue("Jitter should be >= 0", jitter >= 0); + assertTrue("Jitter should be <= 10% of interval (1000ms)", jitter <= expectedMaxJitter); + } + + @Test + public void testCalculateJitterDifferentQueuesProduceDifferentValues() { + long intervalMs = 10000L; + String queue1 = "queue-alpha"; + String queue2 = "queue-beta"; + + long jitter1 = QueueMetricsRefreshPool.calculateJitter(queue1, intervalMs); + long jitter2 = QueueMetricsRefreshPool.calculateJitter(queue2, intervalMs); + + // While theoretically they could be equal, hash collisions are rare enough + // that this test is reliable in practice + assertNotEquals("Different queues should produce different jitter values", jitter1, jitter2); + } + + @Test + public void testCalculateJitterWithDifferentIntervals() { + String queueName = "test-queue"; + + long jitter5s = QueueMetricsRefreshPool.calculateJitter(queueName, 5000L); + long jitter10s = QueueMetricsRefreshPool.calculateJitter(queueName, 10000L); + + assertTrue("Jitter for 5s should be <= 500ms", jitter5s <= 500L); + assertTrue("Jitter for 10s should be <= 1000ms", jitter10s <= 1000L); + } + + @Test + public void testScheduleRefreshTaskExecutesTask() throws Exception { + QueueMetricsRefreshPool.init(2); + QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); + + CountDownLatch latch = new CountDownLatch(2); + AtomicInteger executionCount = new AtomicInteger(0); + + Runnable task = () -> { + executionCount.incrementAndGet(); + latch.countDown(); + }; + + ScheduledFuture future = pool.scheduleRefreshTask(task, 50L); + + assertNotNull("Scheduled future should not be null", future); + assertTrue("Task should execute at least twice", latch.await(500, TimeUnit.MILLISECONDS)); + assertTrue("Execution count should be >= 2", executionCount.get() >= 2); + + future.cancel(false); + } + + @Test + public void testScheduleRefreshTaskWithInitialDelayZero() throws Exception { + QueueMetricsRefreshPool.init(1); + QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); + + CountDownLatch firstExecutionLatch = new CountDownLatch(1); + long startTime = System.currentTimeMillis(); + + Runnable task = firstExecutionLatch::countDown; + + ScheduledFuturefuture = pool.scheduleRefreshTask(task, 100L); + + assertTrue("First execution should happen quickly", firstExecutionLatch.await(200, TimeUnit.MILLISECONDS)); + long firstExecutionTime = System.currentTimeMillis() - startTime; + assertTrue("Initial delay should be ~0 (< 200ms)", firstExecutionTime < 200); + + future.cancel(false); + } + + + @Test + public void testResetForTestingShutdownsAndNullsInstance() { + QueueMetricsRefreshPool.init(4); + QueueMetricsRefreshPool pool1 = QueueMetricsRefreshPool.getInstance(); + assertNotNull("Pool should exist", pool1); + + QueueMetricsRefreshPool.resetForTesting(); + + // After reset, getInstance should create a new instance + QueueMetricsRefreshPool pool2 = QueueMetricsRefreshPool.getInstance(); + assertNotNull("New pool should be created", pool2); + assertNotSame("Should be different instance after reset", pool1, pool2); + } + + @Test + public void testResetForTestingIsIdempotent() { + QueueMetricsRefreshPool.resetForTesting(); + QueueMetricsRefreshPool.resetForTesting(); // Second call should not throw + QueueMetricsRefreshPool.resetForTesting(); // Third call should not throw + + // Should still be able to get instance + QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); + assertNotNull("Pool should be available after multiple resets", pool); + } + + @Test + public void testScheduleMultipleTasksConcurrently() throws Exception { + QueueMetricsRefreshPool.init(4); + QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); + + int taskCount = 5; + CountDownLatch latch = new CountDownLatch(taskCount * 2); // Each task should run at least twice + AtomicInteger[] counters = new AtomicInteger[taskCount]; + ScheduledFuture[] futures = new ScheduledFuture[taskCount]; + + for (int i = 0; i < taskCount; i++) { + counters[i] = new AtomicInteger(0); + final int taskId = i; + futures[i] = pool.scheduleRefreshTask(() -> { + counters[taskId].incrementAndGet(); + latch.countDown(); + }, 50L); + } + + assertTrue("All tasks should execute multiple times", latch.await(1, TimeUnit.SECONDS)); + + // Cancel all tasks + for (ScheduledFuture future : futures) { + future.cancel(false); + } + + // Verify all tasks executed at least once + for (int i = 0; i < taskCount; i++) { + assertTrue("Task " + i + " should have executed", counters[i].get() >= 2); + } + } + + @Test + public void testJitterPreventsSynchronization() { + // Test that jitter would prevent thundering herd + String[] queues = {"q1", "q2", "q3", "q4", "q5"}; + long intervalMs = 10000L; + + long[] jitters = new long[queues.length]; + for (int i = 0; i < queues.length; i++) { + jitters[i] = QueueMetricsRefreshPool.calculateJitter(queues[i], intervalMs); + } + + // Check that not all jitters are the same (spreading effect) + boolean hasDifferentJitter = false; + for (int i = 1; i < jitters.length; i++) { + if (jitters[i] != jitters[0]) { + hasDifferentJitter = true; + break; + } + } + + assertTrue("Jitter should vary across different queue names", hasDifferentJitter); + } + + @Test + public void testCalculateJitterWithZeroInterval() { + // Test edge case where jitterWindow becomes 0 (very small interval) + String queueName = "test-queue"; + long smallInterval = 5L; // 5ms interval -> jitterWindow = 0 (5 * 10 / 100 = 0) + + long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, smallInterval); + + assertEquals("Jitter should be 0 when jitterWindow is 0", 0, jitter); + } + + @Test + public void testCalculateJitterWithNegativeInterval() { + // Test edge case with negative interval + String queueName = "test-queue"; + long negativeInterval = -1000L; + + long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, negativeInterval); + + assertEquals("Jitter should be 0 when intervalMs is negative", 0, jitter); + } + + @Test + public void testCalculateJitterWithHashCodeIntMinValue() { + // Test edge case where queue name generates Integer.MIN_VALUE hash + // We need to find a string that produces Integer.MIN_VALUE hashCode + // "polygenelubricants" is known to produce Integer.MIN_VALUE hashCode + String specialQueue = "polygenelubricants"; + long intervalMs = 10000L; + + // Verify the queue name indeed produces Integer.MIN_VALUE + assertEquals("Test string should produce Integer.MIN_VALUE", Integer.MIN_VALUE, specialQueue.hashCode()); + + long jitter = QueueMetricsRefreshPool.calculateJitter(specialQueue, intervalMs); + + // Verify jitter is non-negative and within bounds + assertTrue("Jitter should be >= 0 even for Integer.MIN_VALUE hashCode", jitter >= 0); + assertTrue("Jitter should be <= 10% of interval", jitter < intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100); + } + + @Test + public void testCalculateJitterAlwaysNonNegative() { + // Test with various queue names to ensure jitter is always non-negative + String[] testQueues = { + "queue-1", "queue-2", "production", "default", + "polygenelubricants", // Integer.MIN_VALUE + "test-queue-alpha", "test-queue-beta" + }; + long intervalMs = 10000L; + + for (String queueName : testQueues) { + long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + assertTrue("Jitter for queue '" + queueName + "' should be >= 0, but was: " + jitter, + jitter >= 0); + assertTrue("Jitter for queue '" + queueName + "' should be < " + (intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100), + jitter < intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100); + } + } + + @Test + public void testShutdownNullsInstance() { + QueueMetricsRefreshPool.init(2); + assertNotNull("Pool should be initialized before shutdown", QueueMetricsRefreshPool.getInstance()); + + QueueMetricsRefreshPool.shutdown(); + + // After shutdown the singleton is cleared; getInstance() must create a fresh pool + QueueMetricsRefreshPool newPool = QueueMetricsRefreshPool.getInstance(); + assertNotNull("A new pool should be lazily created after shutdown", newPool); + } + + @Test + public void testShutdownIsIdempotent() { + QueueMetricsRefreshPool.init(2); + + // Calling shutdown multiple times must not throw + QueueMetricsRefreshPool.shutdown(); + QueueMetricsRefreshPool.shutdown(); + QueueMetricsRefreshPool.shutdown(); + + // Pool must still be obtainable afterwards + assertNotNull("Pool should be available after multiple shutdowns", + QueueMetricsRefreshPool.getInstance()); + } + + @Test + public void testShutdownStopsScheduledTasks() throws Exception { + QueueMetricsRefreshPool.init(2); + QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); + + AtomicInteger counter = new AtomicInteger(0); + // Wait for at least two executions so the task is clearly running before we shut down + CountDownLatch twoExecutions = new CountDownLatch(2); + + pool.scheduleRefreshTask(() -> { + counter.incrementAndGet(); + twoExecutions.countDown(); + }, 50L); + + assertTrue("Task should execute at least twice before shutdown", + twoExecutions.await(2, TimeUnit.SECONDS)); + int countAtShutdown = counter.get(); + + // shutdown() blocks until all threads have terminated (awaitTermination), + // so when it returns no further executions can happen. + QueueMetricsRefreshPool.shutdown(); + + int countAfterShutdown = counter.get(); + assertTrue("No new task executions should occur after shutdown", + countAfterShutdown <= countAtShutdown + 1); + } +} + diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java new file mode 100644 index 000000000000..2bc5c3a5d08e --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java @@ -0,0 +1,322 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.apache.hadoop.yarn.api.records.QueueInfo; +import org.apache.hadoop.yarn.api.records.QueueStatistics; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Unit tests for QueueMetricsState - tests state management logic in isolation. + * Tests interval registration, circuit breaker, refresh locking, and other state logic. + */ +public class TestQueueMetricsState { + + @Mock + private QueueInfo mockQueueInfo; + + @Mock + private QueueStatistics mockQueueStats; + + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + setupMockQueueInfo(); + } + + private void setupMockQueueInfo() { + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(1024L); + when(mockQueueStats.getAvailableMemoryMB()).thenReturn(1024L); + when(mockQueueStats.getAllocatedVCores()).thenReturn(4L); + when(mockQueueStats.getAvailableVCores()).thenReturn(4L); + when(mockQueueStats.getNumAppsRunning()).thenReturn(1L); + when(mockQueueStats.getNumAppsPending()).thenReturn(0L); + when(mockQueueStats.getAllocatedContainers()).thenReturn(2L); + when(mockQueueStats.getPendingContainers()).thenReturn(0L); + when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); + when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.25f); + } + + @Test + public void testConstructorWithNullSnapshot() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + assertNull("Snapshot should be null when constructed with null", state.getSnapshot()); + assertEquals("Min interval should be set", 5000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testConstructorWithSnapshot() { + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + QueueMetricsState state = new QueueMetricsState(snapshot, 10000L); + + assertNotNull("Snapshot should not be null", state.getSnapshot()); + assertEquals("Min interval should be set", 10000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testGetAgeMsReturnsLargeValueInitially() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + long age = state.getAgeMs(); + + // Age should be very large when lastWriteTime = 0 (epoch) + assertTrue("Age should be > 1 year in ms", age > 365L * 24 * 60 * 60 * 1000); + } + + @Test + public void testApplySnapshotUpdatesSnapshot() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + assertNull("Initial snapshot should be null", state.getSnapshot()); + + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + state.applySnapshot(snapshot, 5000L); + + assertNotNull("Snapshot should be updated", state.getSnapshot()); + assertEquals("Memory should match", 1.0f, state.getSnapshot().getMemoryUsedGB(), 0.01f); + } + + @Test + public void testApplySnapshotReducesAgeMs() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + long initialAge = state.getAgeMs(); + + // Spin-wait up to 200ms to ensure time has passed so the age comparison is meaningful + long deadline = System.currentTimeMillis() + 200; + while (state.getAgeMs() <= initialAge && System.currentTimeMillis() < deadline) { + Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop + } + + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + state.applySnapshot(snapshot, 5000L); + + long newAge = state.getAgeMs(); + assertTrue("Age should be much smaller after apply", newAge < initialAge); + assertTrue("Age should be recent (< 1s)", newAge < 1000); + } + + @Test + public void testApplySnapshotUpdatesMinRefreshInterval() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + assertEquals("Initial min interval", 10000L, state.getMinRefreshIntervalMs()); + + // Apply snapshot with smaller interval + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + state.applySnapshot(snapshot, 3000L); + + assertEquals("Min interval should be reduced", 3000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testApplySnapshotDoesNotIncreaseMinInterval() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + // Apply snapshot with larger interval + QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + state.applySnapshot(snapshot, 10000L); + + // Min interval should stay at smaller value + assertEquals("Min interval should not increase", 5000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testRegisterIntervalReturnsTrueWhenNoTaskExists() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + + boolean shouldSchedule = state.registerInterval(5000L); + + assertTrue("Should return true when no task exists", shouldSchedule); + } + + @Test + public void testRegisterIntervalReturnsTrueWhenLoweringMinimum() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + + // First registration - creates task + state.registerInterval(10000L); + + // Second registration with faster interval + boolean shouldSchedule = state.registerInterval(5000L); + + assertTrue("Should return true when lowering minimum", shouldSchedule); + assertEquals("Min interval should be updated", 5000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testRegisterIntervalReturnsTrueWhenTaskIsNull() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + // Register faster interval first (task is null, should return true) + boolean shouldSchedule1 = state.registerInterval(5000L); + assertTrue("Should return true when task is null", shouldSchedule1); + + // Register slower interval (task still null, should still return true) + boolean shouldSchedule2 = state.registerInterval(10000L); + assertTrue("Should return true when task is null even with slower interval", shouldSchedule2); + + assertEquals("Min interval should stay at faster value", 5000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testDeregisterIntervalReturnsFalseWhenOtherSessionsRemain() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + // Register two sessions at same interval + state.registerInterval(5000L); + state.registerInterval(5000L); + + // Deregister one session + boolean shouldReschedule = state.deregisterInterval(5000L); + + assertFalse("Should return false when other sessions remain at this interval", shouldReschedule); + } + + @Test + public void testDeregisterIntervalRemovesBucket() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + + // Register fast and slow sessions + state.registerInterval(2000L); // Fast + state.registerInterval(10000L); // Slow + + // After registration, min should be 2000 + assertEquals("Min interval should be 2000ms", 2000L, state.getMinRefreshIntervalMs()); + + // Deregister fast session - removes the 2000ms bucket + state.deregisterInterval(2000L); + + // The minRefreshIntervalMs field uses Math.min logic (line 217) so it won't increase + // back to 10000. This is by design - the field tracks historical minimum, not current. + // The actual task rescheduling logic in ensureTaskScheduled() recomputes from intervalCounts. + assertEquals("Min interval field remains at historical min (by design)", + 2000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testTryStartRefreshPreventsRace() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + boolean first = state.tryStartRefresh(); + boolean second = state.tryStartRefresh(); + + assertTrue("First call should succeed", first); + assertFalse("Second call should fail (already refreshing)", second); + } + + @Test + public void testFinishRefreshReleasesLock() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + state.tryStartRefresh(); + state.finishRefresh(); + + boolean canRefresh = state.tryStartRefresh(); + assertTrue("Should be able to refresh after finish", canRefresh); + } + + @Test + public void testRecordRefreshSuccessResetsCircuitBreaker() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + // Simulate failures + for (int i = 0; i < 6; i++) { + state.recordRefreshFailure("test-queue", "failure " + i); + } + + // Circuit breaker should be active + assertTrue("Circuit breaker should block", + state.shouldSkipDueToCircuitBreaker("test-queue")); + + // Record success + state.recordRefreshSuccess("test-queue"); + + // Circuit breaker should be reset + assertFalse("Circuit breaker should be reset after success", + state.shouldSkipDueToCircuitBreaker("test-queue")); + } + + @Test + public void testRecordRefreshFailureActivatesCircuitBreaker() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + // First 4 failures should not activate circuit breaker + for (int i = 0; i < 4; i++) { + state.recordRefreshFailure("test-queue", "failure " + i); + assertFalse("Circuit breaker should not activate yet at failure " + i, + state.shouldSkipDueToCircuitBreaker("test-queue")); + } + + // 5th failure activates circuit breaker + state.recordRefreshFailure("test-queue", "failure 5"); + assertTrue("Circuit breaker should activate after 5 failures", + state.shouldSkipDueToCircuitBreaker("test-queue")); + } + + @Test + public void testCircuitBreakerAllowsProbeEvery10Ticks() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + // Activate circuit breaker + for (int i = 0; i < 5; i++) { + state.recordRefreshFailure("test-queue", "failure " + i); + } + + // First few ticks should be blocked + int blockedCount = 0; + int allowedCount = 0; + + for (int tick = 1; tick <= 20; tick++) { + if (state.shouldSkipDueToCircuitBreaker("test-queue")) { + blockedCount++; + } else { + allowedCount++; + } + } + + // Should allow approximately 2 probes in 20 ticks (ticks 10 and 20) + assertTrue("Should have some blocked ticks", blockedCount > 10); + assertTrue("Should have some allowed probes", allowedCount >= 1 && allowedCount <= 3); + } + + @Test + public void testShouldSkipDueToCircuitBreakerReturnsFalseWhenHealthy() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + // No failures recorded + assertFalse("Circuit breaker should not skip when healthy", + state.shouldSkipDueToCircuitBreaker("test-queue")); + } +} + + + + + diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java new file mode 100644 index 000000000000..8f59f5ec114d --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java @@ -0,0 +1,587 @@ +/* + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; + +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.yarn.api.records.QueueInfo; +import org.apache.hadoop.yarn.api.records.QueueStatistics; +import org.apache.hadoop.yarn.client.api.YarnClient; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockingDetails; +import static org.mockito.Mockito.when; + +/** + * Test cases for YarnQueueMetricsCollector. + */ +public class TestYarnQueueMetricsCollector { + + @Mock + private YarnClient mockYarnClient; + + @Mock + private QueueInfo mockQueueInfo; + + @Mock + private QueueStatistics mockQueueStats; + + private AutoCloseable closeable; + private HiveConf testConf; + + private static final long WAIT_TIMEOUT_MS = 5000; + + @Before + public void setUp() { + closeable = MockitoAnnotations.openMocks(this); + testConf = new HiveConf(); + // Reset the pool manager singleton and cache so each test starts with a clean state. + QueueMetricsRefreshPool.resetForTesting(); + QueueMetricsCache.resetForTesting(); + } + + @After + public void tearDown() throws Exception { + if (closeable != null) { + closeable.close(); + } + QueueMetricsRefreshPool.resetForTesting(); + QueueMetricsCache.resetForTesting(); + } + + /** + * Helper to create a collector in tests using a default HiveConf (min pool sizes). + */ + private YarnQueueMetricsCollector newCollector(YarnClient yarnClient, String queueName, + long refreshIntervalMs, String queryId) { + return new YarnQueueMetricsCollector(yarnClient, queueName, refreshIntervalMs, queryId, testConf); + } + + /** + * Waits for a snapshot to be available (non-null). + */ + private QueueMetricsSnapshot waitForSnapshot( + YarnQueueMetricsCollector collector, long timeoutMs) { + long startTime = System.currentTimeMillis(); + QueueMetricsSnapshot snapshot; + while ((snapshot = collector.getLatestSnapshot()) == null) { + if (System.currentTimeMillis() - startTime > timeoutMs) { + fail("Snapshot not available after " + timeoutMs + "ms"); + } + Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop + } + return snapshot; + } + + /** + * Waits for a specific number of invocations with timeout. + */ + private void waitForInvocationCount(Object mock, int minCount, long timeoutMs) { + long startTime = System.currentTimeMillis(); + while (mockingDetails(mock).getInvocations().size() < minCount) { + if (System.currentTimeMillis() - startTime > timeoutMs) { + return; + } + Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop + } + } + + /** + * Helper method that configures mock objects with standard happy-path values. + */ + private void setupHappyPathMocks() throws Exception { + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(1024L); + when(mockQueueStats.getAvailableMemoryMB()).thenReturn(1024L); + when(mockQueueStats.getAllocatedVCores()).thenReturn(4L); + when(mockQueueStats.getAvailableVCores()).thenReturn(4L); + when(mockQueueStats.getNumAppsRunning()).thenReturn(1L); + when(mockQueueStats.getNumAppsPending()).thenReturn(0L); + when(mockQueueStats.getAllocatedContainers()).thenReturn(2L); + when(mockQueueStats.getPendingContainers()).thenReturn(0L); + when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); + when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.25f); + when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithNullYarnClient() { + new YarnQueueMetricsCollector(null, "default", 1000, "query-1", testConf); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithNullQueueName() { + new YarnQueueMetricsCollector(mockYarnClient, null, 1000, "query-1", testConf); + } + + @Test + public void testSuccessfulMetricsCollection() throws Exception { + setupHappyPathMocks(); + when(mockYarnClient.getQueueInfo("default")).thenReturn(mockQueueInfo); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "test-query-1"); + try { + QueueMetricsSnapshot snapshot = waitForSnapshot(collector, WAIT_TIMEOUT_MS); + + assertNotNull("Snapshot should not be null", snapshot); + assertEquals("Memory used should be 1GB", 1.0f, snapshot.getMemoryUsedGB(), 0.1f); + assertEquals("Memory total should be 2GB (1+1)", 2.0f, snapshot.getMemoryTotalGB(), 0.1f); + assertEquals("VCores used should be 4", 4, snapshot.getVCoresUsed()); + assertEquals("VCores total should be 8 (4+4)", 8, snapshot.getVCoresTotal()); + assertEquals("Running apps should be 1", 1, snapshot.getRunningApps()); + assertEquals("Pending apps should be 0", 0, snapshot.getPendingApps()); + assertEquals("Allocated containers should be 2", 2, snapshot.getAllocatedContainers()); + assertEquals("Pending containers should be 0", 0, snapshot.getPendingContainers()); + assertEquals("Capacity should be 50%", 50.0f, snapshot.getCapacityPercentage(), 0.1f); + assertEquals("Current capacity should be 25%", 25.0f, snapshot.getCurrentCapacityPercentage(), 0.1f); + assertEquals("Memory percentage", "50.00%", snapshot.getMemoryPercentage()); + assertEquals("VCores percentage", "50.00%", snapshot.getVCoresPercentage()); + } finally { + collector.shutdown(); + } + } + + @Test + public void testMetricsCollectionWithNullQueueInfo() throws Exception { + when(mockYarnClient.getQueueInfo("nonexistent")).thenReturn(null); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "nonexistent", 10000, "test-query-2"); + try { + assertNull("Snapshot should be null for nonexistent queue", collector.getLatestSnapshot()); + } finally { + collector.shutdown(); + } + } + + @Test + public void testMetricsCollectionWithNullQueueStatistics() throws Exception { + when(mockQueueInfo.getQueueStatistics()).thenReturn(null); + when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.0f); + when(mockYarnClient.getQueueInfo("default")).thenReturn(mockQueueInfo); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "test-query-3"); + try { + QueueMetricsSnapshot snapshot = waitForSnapshot(collector, WAIT_TIMEOUT_MS); + assertNotNull("Snapshot should not be null", snapshot); + assertEquals("Memory used should be 0", 0.0f, snapshot.getMemoryUsedGB(), 0.01f); + assertEquals("Memory total should be 0", 0.0f, snapshot.getMemoryTotalGB(), 0.01f); + assertEquals("VCores used should be 0", 0, snapshot.getVCoresUsed()); + assertEquals("VCores total should be 0", 0, snapshot.getVCoresTotal()); + assertEquals("Capacity should still be 50%", 50.0f, snapshot.getCapacityPercentage(), 0.1f); + assertEquals("Current capacity should be 0%", 0.0f, snapshot.getCurrentCapacityPercentage(), 0.1f); + } finally { + collector.shutdown(); + } + } + + @Test + public void testPercentageCalculationWithZeroTotal() { + // Setup with zero totals + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(0L); + when(mockQueueStats.getAvailableMemoryMB()).thenReturn(0L); + when(mockQueueStats.getAllocatedVCores()).thenReturn(0L); + when(mockQueueStats.getAvailableVCores()).thenReturn(0L); + when(mockQueueStats.getNumAppsRunning()).thenReturn(0L); + when(mockQueueStats.getNumAppsPending()).thenReturn(0L); + when(mockQueueStats.getAllocatedContainers()).thenReturn(0L); + when(mockQueueStats.getPendingContainers()).thenReturn(0L); + when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); + when(mockQueueInfo.getCapacity()).thenReturn(0.0f); + when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.0f); + + QueueMetricsSnapshot snapshot = + new QueueMetricsSnapshot(mockQueueInfo); + + // Should return "N/A" for percentages when total is zero + assertEquals("Memory percentage should be N/A", "N/A", snapshot.getMemoryPercentage()); + assertEquals("VCores percentage should be N/A", "N/A", snapshot.getVCoresPercentage()); + } + + @Test + public void testShutdownIdempotency() throws Exception { + when(mockYarnClient.getQueueInfo("default")).thenReturn(mockQueueInfo); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "test-query-4"); + collector.shutdown(); + collector.shutdown(); // second call must be safe + assertTrue("Multiple shutdowns should be safe", true); + } + + @Test + public void testExceptionDuringCollection() throws Exception { + when(mockYarnClient.getQueueInfo("default")) + .thenThrow(new RuntimeException("RM unavailable")); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "test-query-5"); + try { + assertNull("Snapshot should be null after exception", collector.getLatestSnapshot()); + } finally { + collector.shutdown(); + } + } + + @Test + public void testQueueNameRetrieval() throws Exception { + when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); + when(mockQueueInfo.getQueueStatistics()).thenReturn(null); + when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "production", 10000, "test-query-6"); + try { + assertEquals("Queue name should match", "production", collector.getQueueName()); + } finally { + collector.shutdown(); + } + } + + @Test + public void testMemoryAndVCoreCalculation() { + // Test with specific values to verify calculation + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(5120L); // 5GB used + when(mockQueueStats.getAvailableMemoryMB()).thenReturn(15360L); // 15GB available + when(mockQueueStats.getAllocatedVCores()).thenReturn(50L); + when(mockQueueStats.getAvailableVCores()).thenReturn(150L); + when(mockQueueStats.getNumAppsRunning()).thenReturn(3L); + when(mockQueueStats.getNumAppsPending()).thenReturn(2L); + when(mockQueueStats.getAllocatedContainers()).thenReturn(10L); + when(mockQueueStats.getPendingContainers()).thenReturn(7L); + when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); + when(mockQueueInfo.getCapacity()).thenReturn(0.2f); // 20% + when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.05f); // 5% + + QueueMetricsSnapshot snapshot = + new QueueMetricsSnapshot(mockQueueInfo); + + // Total = Used + Available + assertEquals("Memory used", 5.0f, snapshot.getMemoryUsedGB(), 0.01f); + assertEquals("Memory total", 20.0f, snapshot.getMemoryTotalGB(), 0.01f); // 5+15 + assertEquals("Memory percentage", "25.00%", snapshot.getMemoryPercentage()); // 5/20 + + assertEquals("VCores used", 50, snapshot.getVCoresUsed()); + assertEquals("VCores total", 200, snapshot.getVCoresTotal()); // 50+150 + assertEquals("VCores percentage", "25.00%", snapshot.getVCoresPercentage()); // 50/200 + + assertEquals("Running apps", 3, snapshot.getRunningApps()); + assertEquals("Pending apps", 2, snapshot.getPendingApps()); + assertEquals("Allocated containers", 10, snapshot.getAllocatedContainers()); + assertEquals("Pending containers", 7, snapshot.getPendingContainers()); + assertEquals("Capacity", 20.0f, snapshot.getCapacityPercentage(), 0.01f); + assertEquals("Current capacity", 5.0f, snapshot.getCurrentCapacityPercentage(), 0.01f); + } + + @Test(expected = IllegalArgumentException.class) + public void testQueueMetricsSnapshotWithNullQueueInfo() { + new QueueMetricsSnapshot(null); + } + + // ------------------------------------------------------------------------- + // Tests for Issue #1: Jitter on initial delay (Thundering Herd prevention) + // ------------------------------------------------------------------------- + // Note: Jitter is implicitly tested by all tests that successfully create collectors. + // Explicit jitter distribution testing would require reflection to access private + // scheduling details, which is fragile and not worth the maintenance cost. + + @Test + public void testExecutorCleanupOnInitializationFailure() throws Exception { + when(mockYarnClient.getQueueInfo(anyString())) + .thenThrow(new RuntimeException("Simulated RM failure during init")); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "init-fail-query"); + try { + assertNull("Snapshot should be null after init failure", collector.getLatestSnapshot()); + } finally { + collector.shutdown(); + } + } + + @Test + public void testCircuitBreakerActivatesAfterMaxFailures() throws Exception { + when(mockYarnClient.getQueueInfo(anyString())) + .thenThrow(new RuntimeException("YARN RM unavailable")); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 50, "circuit-breaker-query-1"); + try { + waitForInvocationCount(mockYarnClient, 6, 1000); + assertNull("Snapshot should be null when circuit breaker active", collector.getLatestSnapshot()); + int callCount = mockingDetails(mockYarnClient).getInvocations().size(); + assertTrue("Circuit breaker should reduce calls (got " + callCount + ")", callCount < 12); + } finally { + collector.shutdown(); + } + } + + @Test + public void testCircuitBreakerResetsOnSuccess() throws Exception { + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(4096L); + when(mockQueueStats.getAvailableMemoryMB()).thenReturn(4096L); + when(mockQueueStats.getAllocatedVCores()).thenReturn(50L); + when(mockQueueStats.getAvailableVCores()).thenReturn(50L); + when(mockQueueStats.getNumAppsRunning()).thenReturn(2L); + when(mockQueueStats.getNumAppsPending()).thenReturn(1L); + when(mockQueueStats.getAllocatedContainers()).thenReturn(5L); + when(mockQueueStats.getPendingContainers()).thenReturn(5L); + when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); + when(mockQueueInfo.getCapacity()).thenReturn(0.3f); + when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.2f); + when(mockYarnClient.getQueueInfo(anyString())) + .thenThrow(new RuntimeException("Temporary RM failure")) + .thenThrow(new RuntimeException("Temporary RM failure")) + .thenThrow(new RuntimeException("Temporary RM failure")) + .thenThrow(new RuntimeException("Temporary RM failure")) + .thenThrow(new RuntimeException("Temporary RM failure")) + .thenReturn(mockQueueInfo); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 30, "circuit-breaker-recovery-query"); + try { + waitForInvocationCount(mockYarnClient, 3, 200); + assertNull("Snapshot should be null after circuit breaker activates", collector.getLatestSnapshot()); + QueueMetricsSnapshot snapshot = waitForSnapshot(collector, 2000); + assertNotNull("Snapshot should be populated after circuit breaker recovery", snapshot); + assertEquals("Memory used should be 4GB", 4.0f, snapshot.getMemoryUsedGB(), 0.1f); + } finally { + collector.shutdown(); + } + } + + + @Test + public void testNullQueueInfoDoesNotTriggerCircuitBreaker() throws Exception { + when(mockYarnClient.getQueueInfo(anyString())).thenReturn(null); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "nonexistent-queue", 50, "null-queueinfo-query"); + try { + waitForInvocationCount(mockYarnClient, 8, 800); + assertNull("Snapshot should remain null for null QueueInfo", collector.getLatestSnapshot()); + int callCount = mockingDetails(mockYarnClient).getInvocations().size(); + assertTrue("Null QueueInfo should NOT trigger circuit breaker (got " + callCount + " calls)", + callCount >= 8); + } finally { + collector.shutdown(); + } + } + + @Test + public void testSnapshotCollectionTimestampIsRecent() throws Exception { + setupHappyPathMocks(); + long beforeCreate = System.currentTimeMillis(); + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "timestamp-test"); + try { + QueueMetricsSnapshot snapshot = waitForSnapshot(collector, WAIT_TIMEOUT_MS); + long afterCollect = System.currentTimeMillis(); + assertNotNull("Snapshot should not be null", snapshot); + assertTrue("Timestamp should be >= creation time", snapshot.getCollectionTimestamp() >= beforeCreate); + assertTrue("Timestamp should be <= current time", snapshot.getCollectionTimestamp() <= afterCollect); + assertTrue("Timestamp should not be zero", snapshot.getCollectionTimestamp() > 0); + } finally { + collector.shutdown(); + } + } + + @Test + public void testRefreshIntervalRespected() throws Exception { + setupHappyPathMocks(); + when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(2048L); + when(mockQueueStats.getAvailableMemoryMB()).thenReturn(2048L); + when(mockQueueStats.getAllocatedVCores()).thenReturn(8L); + when(mockQueueStats.getAvailableVCores()).thenReturn(8L); + when(mockQueueStats.getNumAppsRunning()).thenReturn(2L); + when(mockQueueInfo.getCapacity()).thenReturn(0.6f); + + long intervalMs = 100; + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", intervalMs, "refresh-interval-test"); + try { + waitForSnapshot(collector, WAIT_TIMEOUT_MS); + int callsAfterFirst = mockingDetails(mockYarnClient).getInvocations().size(); + long toleranceMs = intervalMs + (long) (intervalMs * 0.2) + 300; + waitForInvocationCount(mockYarnClient, callsAfterFirst + 1, toleranceMs); + int callsAfterWait = mockingDetails(mockYarnClient).getInvocations().size(); + assertTrue("At least one refresh should have occurred within interval + tolerance", + callsAfterWait > callsAfterFirst); + } finally { + collector.shutdown(); + } + } + + @Test + public void testZeroRefreshIntervalIsRejected() throws Exception { + when(mockQueueInfo.getQueueStatistics()).thenReturn(null); + when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); + + assertThrows(IllegalArgumentException.class, () -> + new YarnQueueMetricsCollector(mockYarnClient, "default", 0, "zero-interval-test", testConf)); + } + + @Test + public void testNegativeRefreshIntervalIsRejected() throws Exception { + when(mockQueueInfo.getQueueStatistics()).thenReturn(null); + when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); + + assertThrows(IllegalArgumentException.class, () -> + new YarnQueueMetricsCollector(mockYarnClient, "default", -1000, "negative-interval-test", testConf)); + } + + @Test + public void testJitterCalculationRange() { + long intervalMs = 2000; + long maxJitter = intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100; // 200ms + + // Test multiple queue names to ensure jitter is in range + String[] queues = {"default", "production", "batch", "analytics", "q" + "x".repeat(50)}; + for (String queueName : queues) { + long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + assertTrue("Jitter should be >= 0 for " + queueName, jitter >= 0); + assertTrue("Jitter should be < maxJitter (" + maxJitter + "ms) for " + queueName, + jitter < maxJitter); + } + } + + @Test + public void testJitterIsDeterministic() { + long intervalMs = 5000; + String queueName = "production-analytics"; + + long jitter1 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + long jitter2 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + long jitter3 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); + + assertEquals("Jitter should be deterministic (same queue → same jitter)", jitter1, jitter2); + assertEquals("Jitter should be deterministic across multiple calls", jitter2, jitter3); + } + + @Test + public void testMultipleSessionsShareCacheState() throws Exception { + setupHappyPathMocks(); + + // Create two collectors for the same queue + YarnQueueMetricsCollector collector1 = newCollector(mockYarnClient, "default", 5000, "query-1"); + YarnQueueMetricsCollector collector2 = newCollector(mockYarnClient, "default", 5000, "query-2"); + + try { + // Wait for first snapshot + QueueMetricsSnapshot snapshot1 = waitForSnapshot(collector1, WAIT_TIMEOUT_MS); + + // Second collector should get same snapshot from cache (not null) + QueueMetricsSnapshot snapshot2 = collector2.getLatestSnapshot(); + + assertNotNull("Second collector should get cached snapshot", snapshot2); + assertEquals("Both collectors should see same memory value", + snapshot1.getMemoryUsedGB(), snapshot2.getMemoryUsedGB(), 0.01f); + } finally { + collector1.shutdown(); + collector2.shutdown(); + } + } + + @Test + public void testDynamicReschedulingOnIntervalChange() throws Exception { + setupHappyPathMocks(); + + // Start with slow collector (10s) + YarnQueueMetricsCollector slowCollector = newCollector(mockYarnClient, "default", 10000, "slow-query"); + // Wait for first snapshot to confirm slow collector has stabilized + waitForSnapshot(slowCollector, WAIT_TIMEOUT_MS); + + // Add fast collector (1s) - should trigger rescheduling to 1s + YarnQueueMetricsCollector fastCollector = newCollector(mockYarnClient, "default", 1000, "fast-query"); + + try { + // Verify both collectors see updates (implies task running at faster interval) + QueueMetricsSnapshot snapshot = waitForSnapshot(fastCollector, WAIT_TIMEOUT_MS); + assertNotNull("Fast collector should get snapshot quickly", snapshot); + + // Shutdown fast collector - should reschedule back to slow interval + fastCollector.shutdown(); + // Wait up to 500ms for rescheduling to complete + waitForInvocationCount(mockYarnClient, mockingDetails(mockYarnClient).getInvocations().size(), 500); + + // Verify slow collector still works + assertNotNull("Slow collector should continue after fast shutdown", + slowCollector.getLatestSnapshot()); + } finally { + slowCollector.shutdown(); + } + } + + @Test + public void testCircuitBreakerProbeEvery10Ticks() throws Exception { + // Mock to always fail + when(mockYarnClient.getQueueInfo(anyString())) + .thenThrow(new RuntimeException("RM always failing")); + + YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 50, "probe-test"); + + try { + // Wait for circuit breaker to activate (5 failures) + waitForInvocationCount(mockYarnClient, 6, 1000); + int callsAfterActivation = mockingDetails(mockYarnClient).getInvocations().size(); + + // Wait for next ~12 ticks at 50ms interval — poll until invocation count stabilizes + waitForInvocationCount(mockYarnClient, callsAfterActivation + 2, 800); + int callsAfterWait = mockingDetails(mockYarnClient).getInvocations().size(); + + // Should have ~1 probe attempt in 10 ticks + int probeAttempts = callsAfterWait - callsAfterActivation; + assertTrue("Circuit breaker should allow ~1 probe per 10 ticks, got " + probeAttempts, + probeAttempts >= 0 && probeAttempts <= 2); + } finally { + collector.shutdown(); + } + } + + @Test + public void testTaskCancelsWhenAllSessionsDeregister() throws Exception { + setupHappyPathMocks(); + + YarnQueueMetricsCollector collector1 = newCollector(mockYarnClient, "default", 2000, "query-1"); + YarnQueueMetricsCollector collector2 = newCollector(mockYarnClient, "default", 2000, "query-2"); + + // Wait for initial refresh + waitForSnapshot(collector1, WAIT_TIMEOUT_MS); + int callsWithBoth = mockingDetails(mockYarnClient).getInvocations().size(); + + // Shutdown both collectors + collector1.shutdown(); + collector2.shutdown(); + + // Wait and verify no more RM calls after shutdown (task cancelled) + // Spin-wait up to 3 seconds checking that call count has stabilized after both shutdowns + int callsAfterShutdown; + long deadline = System.currentTimeMillis() + 3000; + do { + callsAfterShutdown = mockingDetails(mockYarnClient).getInvocations().size(); + Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop + } while (callsAfterShutdown > callsWithBoth && System.currentTimeMillis() < deadline); + + assertEquals("No more RM calls should occur after all sessions deregister", + callsWithBoth, callsAfterShutdown); + } +} diff --git a/service-rpc/if/TCLIService.thrift b/service-rpc/if/TCLIService.thrift index ee58cd9979d2..cbca32b6e068 100644 --- a/service-rpc/if/TCLIService.thrift +++ b/service-rpc/if/TCLIService.thrift @@ -1289,6 +1289,7 @@ struct TProgressUpdateResp { 4: required TJobExecutionStatus status 5: required string footerSummary 6: required i64 startTime + 7: optional string queueMetrics } struct TGetQueryIdReq { diff --git a/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java b/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java index 8b2f6a21629e..ef787a15d080 100644 --- a/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java +++ b/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java @@ -17,6 +17,7 @@ private static final org.apache.thrift.protocol.TField STATUS_FIELD_DESC = new org.apache.thrift.protocol.TField("status", org.apache.thrift.protocol.TType.I32, (short)4); private static final org.apache.thrift.protocol.TField FOOTER_SUMMARY_FIELD_DESC = new org.apache.thrift.protocol.TField("footerSummary", org.apache.thrift.protocol.TType.STRING, (short)5); private static final org.apache.thrift.protocol.TField START_TIME_FIELD_DESC = new org.apache.thrift.protocol.TField("startTime", org.apache.thrift.protocol.TType.I64, (short)6); + private static final org.apache.thrift.protocol.TField QUEUE_METRICS_FIELD_DESC = new org.apache.thrift.protocol.TField("queueMetrics", org.apache.thrift.protocol.TType.STRING, (short)7); private static final org.apache.thrift.scheme.SchemeFactory STANDARD_SCHEME_FACTORY = new TProgressUpdateRespStandardSchemeFactory(); private static final org.apache.thrift.scheme.SchemeFactory TUPLE_SCHEME_FACTORY = new TProgressUpdateRespTupleSchemeFactory(); @@ -27,6 +28,7 @@ private @org.apache.thrift.annotation.Nullable TJobExecutionStatus status; // required private @org.apache.thrift.annotation.Nullable java.lang.String footerSummary; // required private long startTime; // required + private @org.apache.thrift.annotation.Nullable java.lang.String queueMetrics; // optional /** The set of fields this struct contains, along with convenience methods for finding and manipulating them. */ public enum _Fields implements org.apache.thrift.TFieldIdEnum { @@ -39,7 +41,8 @@ public enum _Fields implements org.apache.thrift.TFieldIdEnum { */ STATUS((short)4, "status"), FOOTER_SUMMARY((short)5, "footerSummary"), - START_TIME((short)6, "startTime"); + START_TIME((short)6, "startTime"), + QUEUE_METRICS((short)7, "queueMetrics"); private static final java.util.Map byName = new java.util.HashMap(); @@ -67,6 +70,8 @@ public static _Fields findByThriftId(int fieldId) { return FOOTER_SUMMARY; case 6: // START_TIME return START_TIME; + case 7: // QUEUE_METRICS + return QUEUE_METRICS; default: return null; } @@ -129,6 +134,8 @@ public java.lang.String getFieldName() { new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); tmpMap.put(_Fields.START_TIME, new org.apache.thrift.meta_data.FieldMetaData("startTime", org.apache.thrift.TFieldRequirementType.REQUIRED, new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.I64))); + tmpMap.put(_Fields.QUEUE_METRICS, new org.apache.thrift.meta_data.FieldMetaData("queueMetrics", org.apache.thrift.TFieldRequirementType.OPTIONAL, + new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); metaDataMap = java.util.Collections.unmodifiableMap(tmpMap); org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(TProgressUpdateResp.class, metaDataMap); } @@ -152,7 +159,6 @@ public TProgressUpdateResp( this.status = status; this.footerSummary = footerSummary; this.startTime = startTime; - setStartTimeIsSet(true); } /** @@ -180,6 +186,9 @@ public TProgressUpdateResp(TProgressUpdateResp other) { this.footerSummary = other.footerSummary; } this.startTime = other.startTime; + if (other.isSetQueueMetrics()) { + this.queueMetrics = other.queueMetrics; + } } public TProgressUpdateResp deepCopy() { @@ -196,6 +205,7 @@ public void clear() { this.footerSummary = null; setStartTimeIsSet(false); this.startTime = 0; + this.queueMetrics = null; } public int getHeaderNamesSize() { @@ -378,6 +388,30 @@ public void setStartTimeIsSet(boolean value) { __isset_bitfield = org.apache.thrift.EncodingUtils.setBit(__isset_bitfield, __STARTTIME_ISSET_ID, value); } + @org.apache.thrift.annotation.Nullable + public java.lang.String getQueueMetrics() { + return this.queueMetrics; + } + + public void setQueueMetrics(@org.apache.thrift.annotation.Nullable java.lang.String queueMetrics) { + this.queueMetrics = queueMetrics; + } + + public void unsetQueueMetrics() { + this.queueMetrics = null; + } + + /** Returns true if field queueMetrics is set (has been assigned a value) and false otherwise */ + public boolean isSetQueueMetrics() { + return this.queueMetrics != null; + } + + public void setQueueMetricsIsSet(boolean value) { + if (!value) { + this.queueMetrics = null; + } + } + public void setFieldValue(_Fields field, @org.apache.thrift.annotation.Nullable java.lang.Object value) { switch (field) { case HEADER_NAMES: @@ -428,6 +462,14 @@ public void setFieldValue(_Fields field, @org.apache.thrift.annotation.Nullable } break; + case QUEUE_METRICS: + if (value == null) { + unsetQueueMetrics(); + } else { + setQueueMetrics((java.lang.String)value); + } + break; + } } @@ -452,6 +494,9 @@ public java.lang.Object getFieldValue(_Fields field) { case START_TIME: return getStartTime(); + case QUEUE_METRICS: + return getQueueMetrics(); + } throw new java.lang.IllegalStateException(); } @@ -475,6 +520,8 @@ public boolean isSet(_Fields field) { return isSetFooterSummary(); case START_TIME: return isSetStartTime(); + case QUEUE_METRICS: + return isSetQueueMetrics(); } throw new java.lang.IllegalStateException(); } @@ -546,6 +593,15 @@ public boolean equals(TProgressUpdateResp that) { return false; } + boolean this_present_queueMetrics = true && this.isSetQueueMetrics(); + boolean that_present_queueMetrics = true && that.isSetQueueMetrics(); + if (this_present_queueMetrics || that_present_queueMetrics) { + if (!(this_present_queueMetrics && that_present_queueMetrics)) + return false; + if (!this.queueMetrics.equals(that.queueMetrics)) + return false; + } + return true; } @@ -573,6 +629,10 @@ public int hashCode() { hashCode = hashCode * 8191 + org.apache.thrift.TBaseHelper.hashCode(startTime); + hashCode = hashCode * 8191 + ((isSetQueueMetrics()) ? 131071 : 524287); + if (isSetQueueMetrics()) + hashCode = hashCode * 8191 + queueMetrics.hashCode(); + return hashCode; } @@ -851,6 +911,14 @@ public void read(org.apache.thrift.protocol.TProtocol iprot, TProgressUpdateResp org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); } break; + case 7: // QUEUE_METRICS + if (schemeField.type == org.apache.thrift.protocol.TType.STRING) { + struct.queueMetrics = iprot.readString(); + struct.setQueueMetricsIsSet(true); + } else { + org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); + } + break; default: org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); } @@ -911,6 +979,11 @@ public void write(org.apache.thrift.protocol.TProtocol oprot, TProgressUpdateRes oprot.writeFieldBegin(START_TIME_FIELD_DESC); oprot.writeI64(struct.startTime); oprot.writeFieldEnd(); + if (struct.queueMetrics != null) { + oprot.writeFieldBegin(QUEUE_METRICS_FIELD_DESC); + oprot.writeString(struct.queueMetrics); + oprot.writeFieldEnd(); + } oprot.writeFieldStop(); oprot.writeStructEnd(); } diff --git a/service/src/java/org/apache/hive/service/cli/JobProgressUpdate.java b/service/src/java/org/apache/hive/service/cli/JobProgressUpdate.java index 095f58ab8117..a50135e0f967 100644 --- a/service/src/java/org/apache/hive/service/cli/JobProgressUpdate.java +++ b/service/src/java/org/apache/hive/service/cli/JobProgressUpdate.java @@ -29,21 +29,23 @@ public class JobProgressUpdate { private final List headers; private final List> rows; public final String status; + private final String queueMetrics; JobProgressUpdate(ProgressMonitor monitor) { this(monitor.headers(), monitor.rows(), monitor.footerSummary(), monitor.progressedPercentage(), - monitor.startTime(), monitor.executionStatus()); + monitor.startTime(), monitor.executionStatus(), monitor.queueMetrics()); } private JobProgressUpdate(List headers, List> rows, String footerSummary, - double progressedPercentage, long startTimeMillis, String status) { + double progressedPercentage, long startTimeMillis, String status, String queueMetrics) { this.progressedPercentage = progressedPercentage; this.footerSummary = footerSummary; this.startTimeMillis = startTimeMillis; this.headers = headers; this.rows = rows; this.status = status; + this.queueMetrics = queueMetrics; } public List headers() { @@ -53,4 +55,8 @@ public List headers() { public List> rows() { return rows; } + + public String queueMetrics() { + return queueMetrics; + } } diff --git a/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java b/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java index a901edecc0f9..37cdb29ff4e3 100644 --- a/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java +++ b/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java @@ -814,14 +814,27 @@ public TGetOperationStatusResp GetOperationStatus(TGetOperationStatusReq req) th } TJobExecutionStatus executionStatus = mapper.forStatus(progressUpdate.status); - resp.setProgressUpdateResponse(new TProgressUpdateResp( + TProgressUpdateResp tProgressUpdateResp = new TProgressUpdateResp( progressUpdate.headers(), progressUpdate.rows(), progressUpdate.progressedPercentage, executionStatus, progressUpdate.footerSummary, progressUpdate.startTimeMillis - )); + ); + // HIVE-27126: Workaround for Thrift code generation bug. + // When queueMetrics field was added and Thrift code regenerated, the generated constructor + // accepts startTimeMillis parameter but FAILS to call setStartTimeIsSet(true). + // Without the isset flag, Thrift serialization treats startTime as unset, causing: + // 1) Clients receive incomplete/invalid progress updates + // 2) Thrift validation may fail or skip the field entirely + // Solution: Explicitly call setStartTime() after construction to set the isset flag. + // This ensures proper serialization and backward compatibility with Thrift clients. + tProgressUpdateResp.setStartTime(progressUpdate.startTimeMillis); + if (progressUpdate.queueMetrics() != null && !progressUpdate.queueMetrics().isEmpty()) { + tProgressUpdateResp.setQueueMetrics(progressUpdate.queueMetrics()); + } + resp.setProgressUpdateResponse(tProgressUpdateResp); if (opException != null) { resp.setSqlState(opException.getSQLState()); resp.setErrorCode(opException.getErrorCode()); diff --git a/service/src/java/org/apache/hive/service/server/HiveServer2.java b/service/src/java/org/apache/hive/service/server/HiveServer2.java index 2a9bf995f3ed..7c8541e878e1 100644 --- a/service/src/java/org/apache/hive/service/server/HiveServer2.java +++ b/service/src/java/org/apache/hive/service/server/HiveServer2.java @@ -78,6 +78,8 @@ import org.apache.hadoop.hive.ql.cache.results.QueryResultsCache; import org.apache.hadoop.hive.ql.exec.tez.TezSessionPoolManager; import org.apache.hadoop.hive.ql.exec.tez.WorkloadManager; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsCache; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsRefreshPool; import org.apache.hadoop.hive.ql.metadata.Hive; import org.apache.hadoop.hive.ql.metadata.HiveException; import org.apache.hadoop.hive.ql.metadata.HiveMaterializedViewsRegistry; @@ -935,8 +937,11 @@ private void initAndStartTezSessionPoolManager(final WMFullResourcePlan resource // will be invoked anyway in TezTask. Doing it early to initialize triggers for non-pool tez session. LOG.info("Initializing tez session pool manager. Active resource plan: {}", resourcePlan == null || resourcePlan.getPlan() == null ? "null" : resourcePlan.getPlan().getName()); - tezSessionPoolManager = TezSessionPoolManager.getInstance(); HiveConf hiveConf = getHiveConf(); + // Initialize the queue metrics refresh pool for collecting YARN queue metrics during query execution. + // Pool is shared across all queries and sized by hive.server2.tez.queue.metrics.refresh.threads config. + initializeQueueMetricsPool(hiveConf); + tezSessionPoolManager = TezSessionPoolManager.getInstance(); if (hiveConf.getBoolVar(ConfVars.HIVE_SERVER2_TEZ_INITIALIZE_DEFAULT_SESSIONS)) { tezSessionPoolManager.setupPool(hiveConf); } else { @@ -967,6 +972,66 @@ private void initAndStartWorkloadManager(final WMFullResourcePlan resourcePlan) } } + /** + * Initializes the shared JVM-wide queue metrics refresh pool. + *

+ * This pool provides background threads for periodic YARN queue metrics collection across + * all Tez sessions. The pool is shared by all queries in this HiveServer2 process to prevent + * thread explosion when many queries run concurrently. + *

+ * Thread count is configured via {@code hive.server2.tez.queue.metrics.refresh.threads}. + *

+ * The pool is only initialized when execution engine is "tez". Whether to actually collect + * queue metrics is controlled per-session by {@code hive.tez.queue.metrics.refresh.interval}, + * which is checked when creating metrics collectors for each query. + *

+ * In non-Tez environments (MR, Spark, local), the pool is not created, avoiding unnecessary + * thread allocation. + *

+ * Failures are non-fatal — logged as warnings so the server can start without queue metrics. + */ + private void initializeQueueMetricsPool(HiveConf hiveConf) { + try { + // Only initialize for Tez execution engine + String executionEngine = hiveConf.getVar(ConfVars.HIVE_EXECUTION_ENGINE); + if (!"tez".equalsIgnoreCase(executionEngine)) { + LOG.info("Skipping queue metrics refresh pool initialization: execution engine is '{}' (not Tez)", + executionEngine); + return; + } + + // Initialize the pool with configured thread count + // Whether to use it is decided per-session based on hive.tez.queue.metrics.refresh.interval + int refreshThreads = hiveConf.getIntVar(ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS); + QueueMetricsRefreshPool.init(refreshThreads); + LOG.info("Queue metrics refresh pool initialized with {} threads for Tez execution engine", refreshThreads); + } catch (Exception e) { + LOG.warn("Failed to initialize queue metrics refresh pool", e); + } + } + + /** + * Shuts down queue metrics infrastructure to prevent resource leaks on HiveServer2 stop. + * Must be called after {@link #stopOrDisconnectTezSessions()} so that no in-flight query + * can submit new refresh tasks while the pool is draining. + *

+ * Shutdown order: + *

    + *
  1. {@link QueueMetricsRefreshPool#shutdown()} — stops background refresh threads
  2. + *
  3. {@link QueueMetricsCache#shutdown()} — invalidates all cached queue-metric entries
  4. + *
+ */ + private void shutdownQueueMetricsInfrastructure() { + try { + LOG.info("Shutting down queue metrics infrastructure"); + QueueMetricsRefreshPool.shutdown(); + QueueMetricsCache.getInstance().shutdown(); + LOG.info("Queue metrics infrastructure shutdown complete"); + } catch (Exception e) { + LOG.warn("Error during queue metrics infrastructure shutdown", e); + } + } + private void closeAndDisallowHiveSessions() { LOG.info("Closing all open hive sessions."); if (cliService == null) { @@ -1118,6 +1183,7 @@ public synchronized void stop() { } stopOrDisconnectTezSessions(); + shutdownQueueMetricsInfrastructure(); if (zKClientForPrivSync != null) { zKClientForPrivSync.close(); diff --git a/service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java b/service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java new file mode 100644 index 000000000000..b27869a53b96 --- /dev/null +++ b/service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java @@ -0,0 +1,249 @@ +/* + * 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.hive.service.server; + +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsRefreshPool; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Method; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * Tests for conditional initialization of QueueMetricsRefreshPool in HiveServer2. + * + * Verifies that the pool is only initialized when execution engine is "tez", + * and skipped for other engines (MR, Spark, local). + * + * Uses {@link QueueMetricsRefreshPool#getInstanceForTesting()} to verify initialization + * state without triggering lazy initialization. + */ +public class TestHiveServer2QueueMetricsPoolInit { + + @Before + public void setUp() { + // Reset the pool before each test + QueueMetricsRefreshPool.resetForTesting(); + } + + @After + public void tearDown() { + // Clean up after each test + QueueMetricsRefreshPool.resetForTesting(); + } + + /** + * Test that pool IS initialized when execution engine is "tez". + * This is a POSITIVE test case. + */ + @Test + public void testPoolInitializedForTezEngine() throws Exception { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "tez"); + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); + + // Verify pool is not initialized before the call + assertNull("Pool should not be initialized before init call", + QueueMetricsRefreshPool.getInstanceForTesting()); + + HiveServer2 hs2 = new HiveServer2(); + invokeInitializeQueueMetricsPool(hs2, conf); + + // Verify pool WAS initialized (POSITIVE case) + assertNotNull("Pool SHOULD be initialized for Tez engine", + QueueMetricsRefreshPool.getInstanceForTesting()); + } + + /** + * Test that pool IS initialized case-insensitively for "Tez", "TEZ", etc. + * This is a POSITIVE test case. + */ + @Test + public void testPoolInitializedForTezEngineCaseInsensitive() throws Exception { + String[] tezVariants = {"tez", "Tez", "TEZ", "tEz"}; + + for (String variant : tezVariants) { + // Reset between iterations + QueueMetricsRefreshPool.resetForTesting(); + + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, variant); + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); + + // Verify not initialized before + assertNull("Pool should not be initialized before init call for: " + variant, + QueueMetricsRefreshPool.getInstanceForTesting()); + + HiveServer2 hs2 = new HiveServer2(); + invokeInitializeQueueMetricsPool(hs2, conf); + + // Verify WAS initialized (POSITIVE case) + assertNotNull("Pool SHOULD be initialized for Tez variant: " + variant, + QueueMetricsRefreshPool.getInstanceForTesting()); + } + } + + /** + * Test that pool is NOT initialized when execution engine is "mr". + * This is a NEGATIVE test case - verifying the pool remains null. + */ + @Test + public void testPoolNotInitializedForMrEngine() throws Exception { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "mr"); + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); + + // Verify pool is not initialized before + assertNull("Pool should not be initialized before init call", + QueueMetricsRefreshPool.getInstanceForTesting()); + + HiveServer2 hs2 = new HiveServer2(); + invokeInitializeQueueMetricsPool(hs2, conf); + + // Verify pool is STILL not initialized (NEGATIVE case) + assertNull("Pool should NOT be initialized for MR engine", + QueueMetricsRefreshPool.getInstanceForTesting()); + } + + /** + * Test that pool is NOT initialized when execution engine is "spark". + * This is a NEGATIVE test case. + */ + @Test + public void testPoolNotInitializedForSparkEngine() throws Exception { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "spark"); + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); + + // Verify not initialized before + assertNull("Pool should not be initialized before init call", + QueueMetricsRefreshPool.getInstanceForTesting()); + + HiveServer2 hs2 = new HiveServer2(); + invokeInitializeQueueMetricsPool(hs2, conf); + + // Verify STILL not initialized (NEGATIVE case) + assertNull("Pool should NOT be initialized for Spark engine", + QueueMetricsRefreshPool.getInstanceForTesting()); + } + + /** + * Test that pool is NOT initialized when execution engine is empty. + * This is a NEGATIVE test case. + */ + @Test + public void testPoolNotInitializedForEmptyEngine() throws Exception { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, ""); + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); + + // Verify not initialized before + assertNull("Pool should not be initialized before init call", + QueueMetricsRefreshPool.getInstanceForTesting()); + + HiveServer2 hs2 = new HiveServer2(); + invokeInitializeQueueMetricsPool(hs2, conf); + + // Verify STILL not initialized (NEGATIVE case) + assertNull("Pool should NOT be initialized for empty engine", + QueueMetricsRefreshPool.getInstanceForTesting()); + } + + /** + * Test that pool is NOT initialized when execution engine is "local". + * This is a NEGATIVE test case. + */ + @Test + public void testPoolNotInitializedForLocalEngine() throws Exception { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "local"); + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); + + // Verify not initialized before + assertNull("Pool should not be initialized before init call", + QueueMetricsRefreshPool.getInstanceForTesting()); + + HiveServer2 hs2 = new HiveServer2(); + invokeInitializeQueueMetricsPool(hs2, conf); + + // Verify STILL not initialized (NEGATIVE case) + assertNull("Pool should NOT be initialized for local engine", + QueueMetricsRefreshPool.getInstanceForTesting()); + } + + /** + * Test that pool initialization respects configured thread count. + * This is a POSITIVE test with different configuration. + */ + @Test + public void testPoolInitializedWithConfiguredThreadCount() throws Exception { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "tez"); + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 8); + + // Verify not initialized before + assertNull("Pool should not be initialized before init call", + QueueMetricsRefreshPool.getInstanceForTesting()); + + HiveServer2 hs2 = new HiveServer2(); + invokeInitializeQueueMetricsPool(hs2, conf); + + // Verify WAS initialized (POSITIVE case) + assertNotNull("Pool SHOULD be initialized with custom thread count", + QueueMetricsRefreshPool.getInstanceForTesting()); + } + + /** + * Test that pool initialization handles exceptions gracefully and doesn't fail server startup. + * Even with invalid config, the method should not throw exceptions to the caller. + */ + @Test + public void testPoolInitializationFailureIsNonFatal() throws Exception { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "tez"); + // Set a potentially problematic value + conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, -1); + + HiveServer2 hs2 = new HiveServer2(); + + // Should not throw exception - errors are caught and logged + try { + invokeInitializeQueueMetricsPool(hs2, conf); + // Test passes if we get here without exception + } catch (Exception e) { + fail("Pool initialization should not throw exceptions to caller, got: " + e.getMessage()); + } + } + + /** + * Helper method to invoke the private initializeQueueMetricsPool method via reflection. + */ + private void invokeInitializeQueueMetricsPool(HiveServer2 hs2, HiveConf conf) throws Exception { + Method method = HiveServer2.class.getDeclaredMethod("initializeQueueMetricsPool", HiveConf.class); + method.setAccessible(true); + method.invoke(hs2, conf); + } +} + + From 24b37b6350c918cfaa1861c33f9fbbb029cc4b06 Mon Sep 17 00:00:00 2001 From: architjainjain Date: Tue, 4 Aug 2026 18:38:15 +0530 Subject: [PATCH 2/2] HIVE-27126: review comments --- .../org/apache/hadoop/hive/conf/HiveConf.java | 5 +- .../hive/common/log/TestInPlaceUpdate.java | 50 +-- iceberg/pom.xml | 6 - pom.xml | 6 + ql/pom.xml | 5 + .../ql/exec/tez/monitoring/TezJobMonitor.java | 19 +- .../tez/monitoring/TezProgressMonitor.java | 6 - .../yarnqueue/NoOpQueueMetricsCollector.java | 21 +- .../yarnqueue/QueueMetricsCache.java | 78 ++-- .../yarnqueue/QueueMetricsCollector.java | 26 +- .../yarnqueue/QueueMetricsRefreshPool.java | 110 +++--- .../yarnqueue/QueueMetricsSnapshot.java | 11 +- .../yarnqueue/QueueMetricsState.java | 223 ++++++----- .../yarnqueue/YarnQueueMetricsCollector.java | 53 +-- .../hive/ql/exec/tez/TestTezSessionState.java | 98 +++-- .../TestTezJobMonitorQueueMetrics.java | 186 ++++------ .../monitoring/TestTezProgressMonitor.java | 3 +- .../TestTezProgressMonitorQueueMetrics.java | 348 ++++------------- .../TestNoOpQueueMetricsCollector.java | 93 +---- .../yarnqueue/TestQueueMetricsCache.java | 118 +++--- .../TestQueueMetricsRefreshPool.java | 172 ++------- .../yarnqueue/TestQueueMetricsState.java | 350 +++++++++++++----- .../TestYarnQueueMetricsCollector.java | 276 ++++++-------- .../gen/thrift/gen-cpp/TCLIService_types.cpp | 25 ++ .../gen/thrift/gen-cpp/TCLIService_types.h | 16 +- .../rpc/thrift/TProgressUpdateResp.java | 47 ++- .../thrift/gen-php/TProgressUpdateResp.php | 24 ++ .../gen/thrift/gen-py/TCLIService/ttypes.py | 14 +- .../thrift/gen-rb/t_c_l_i_service_types.rb | 4 +- .../service/cli/thrift/ThriftCLIService.java | 9 - .../hive/service/server/HiveServer2.java | 28 +- .../TestHiveServer2QueueMetricsPoolInit.java | 249 ------------- 32 files changed, 1076 insertions(+), 1603 deletions(-) delete mode 100644 service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java index 762b60a4dec1..e173d2f0ac24 100644 --- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java +++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java @@ -3949,10 +3949,7 @@ public static enum ConfVars { "Set to 0 or negative to disable. Minimum effective value is 1 second."), HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS("hive.server2.tez.queue.metrics.refresh.threads", 4, "Number of threads in the scheduled thread pool for refreshing YARN queue metrics. " + - "This pool is used by HiveServer2 to periodically collect queue resource information from YARN RM. " + - "The pool is only created when execution engine is 'tez'. Whether to actually use it for metrics " + - "collection is controlled per-session via hive.tez.queue.metrics.refresh.interval. " + - "In non-Tez environments (e.g., MR, Spark, local), the pool is not initialized."), + "This pool is used by HiveServer2 to periodically collect queue resource information from YARN RM."), HIVE_SERVER2_TEZ_SESSION_LIFETIME("hive.server2.tez.session.lifetime", "162h", new TimeValidator(TimeUnit.HOURS), "The lifetime of the Tez sessions launched by HS2 when default sessions are enabled.\n" + diff --git a/common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java b/common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java index 4d34075fd79d..31270798d50d 100644 --- a/common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java +++ b/common/src/test/org/apache/hadoop/hive/common/log/TestInPlaceUpdate.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.common.log; @@ -34,8 +35,7 @@ *

* We capture stdout via a ByteArrayOutputStream and inspect the rendered output. * These tests verify the rendering layer integration between ProgressMonitor and - * InPlaceUpdate, particularly focusing on separator line positioning when queue - * metrics are displayed. + * InPlaceUpdate. */ public class TestInPlaceUpdate { @@ -47,7 +47,15 @@ private static ProgressMonitor makeMonitor(String queueMetrics) { return new ProgressMonitor() { @Override public List headers() { - return Arrays.asList("VERTICES", "MODE", "STATUS", "TOTAL", "COMPLETED", "RUNNING", "PENDING", "FAILED", "KILLED"); + return Arrays.asList("VERTICES", + "MODE", + "STATUS", + "TOTAL", + "COMPLETED", + "RUNNING", + "PENDING", + "FAILED", + "KILLED"); } @Override @@ -94,13 +102,12 @@ public String queueMetrics() { * print a separator line immediately after the metrics block — so total separators * = 4 (VERTICES table) + 1 (after queue metrics) = 5. * - * This is the MOST CRITICAL test - verifies line 183 of InPlaceUpdate.java. + * This is the MOST CRITICAL test - verifies the separator is printed after the metrics block. */ @Test public void testSeparatorPrintedAfterQueueMetrics() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); - InPlaceUpdate inPlace = new InPlaceUpdate(ps); // Updated to new 4-line format (no staleness in line 1) String metrics = """ @@ -109,7 +116,7 @@ public void testSeparatorPrintedAfterQueueMetrics() { CAPACITY: 60.00% (allocated) | 25.00% (used) APPS: 1 running, 0 pending | CONTAINERS: 2 allocated, 0 pending"""; - inPlace.render(makeMonitor(metrics)); + new InPlaceUpdate(ps).render(makeMonitor(metrics)); ps.flush(); String output = baos.toString(); @@ -126,15 +133,12 @@ public void testSeparatorPrintedAfterQueueMetrics() { assertTrue("APPS: line should be found in output", appsIdx > 0); // Separator should appear after APPS line - int separatorIdx = output.indexOf(SEPARATOR, appsIdx); - assertTrue("Separator must appear after APPS: line (separatorIdx=" + separatorIdx - + ", appsIdx=" + appsIdx + ")", - separatorIdx > appsIdx); + assertTrue("Separator must appear after APPS: line", + output.indexOf(SEPARATOR, appsIdx) > appsIdx); // Total separators = 4 (VERTICES table) + 1 (after queue metrics) = 5 - int count = StringUtils.countMatches(output, SEPARATOR); assertEquals("With queue metrics, total separators should be 5 (4 VERTICES + 1 after metrics)", - 5, count); + 5, StringUtils.countMatches(output, SEPARATOR)); } @@ -146,18 +150,16 @@ public void testSeparatorPrintedAfterQueueMetrics() { public void testNoExtraSeparatorWhenQueueMetricsEmpty() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); - InPlaceUpdate inPlace = new InPlaceUpdate(ps); - inPlace.render(makeMonitor("")); + new InPlaceUpdate(ps).render(makeMonitor("")); ps.flush(); String output = baos.toString(); // VERTICES table renders 4 separators (before-header, after-header, before-footer, after-footer) // With empty queueMetrics there should be exactly 4, not 5. - int count = StringUtils.countMatches(output, SEPARATOR); assertEquals("With empty queue metrics, only 4 VERTICES-table separators should appear", - 4, count); + 4, StringUtils.countMatches(output, SEPARATOR)); } @@ -168,16 +170,14 @@ public void testNoExtraSeparatorWhenQueueMetricsEmpty() { public void testNoExtraSeparatorWhenQueueMetricsNull() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); - InPlaceUpdate inPlace = new InPlaceUpdate(ps); - inPlace.render(makeMonitor(null)); + new InPlaceUpdate(ps).render(makeMonitor(null)); ps.flush(); String output = baos.toString(); - int count = StringUtils.countMatches(output, SEPARATOR); assertEquals("With null queue metrics, only 4 VERTICES-table separators should appear", - 4, count); + 4, StringUtils.countMatches(output, SEPARATOR)); } /** diff --git a/iceberg/pom.xml b/iceberg/pom.xml index 47aec7baf2ad..f337e0d5dcc6 100644 --- a/iceberg/pom.xml +++ b/iceberg/pom.xml @@ -37,7 +37,6 @@ 2.44.4 2.29.2 3.27.3 - 4.2.1 2.10.0 false 2.9.3 @@ -211,11 +210,6 @@ assertj-core ${assertj.version} - - org.awaitility - awaitility - ${awaitility.version} - com.esotericsoftware kryo-shaded diff --git a/pom.xml b/pom.xml index 6bd479402508..5e4262dbaa84 100644 --- a/pom.xml +++ b/pom.xml @@ -115,6 +115,7 @@ 4.9.3 2.0.0-M24 1.12.0 + 4.2.1 1.84 1.33.0 6.0.5 @@ -545,6 +546,11 @@ ST4 ${ST4.version} + + org.awaitility + awaitility + ${awaitility.version} + org.apache.commons commons-compress diff --git a/ql/pom.xml b/ql/pom.xml index 244b51d9e807..4bbb8f5ea07b 100644 --- a/ql/pom.xml +++ b/ql/pom.xml @@ -810,6 +810,11 @@ ${mockito-inline.version} test + + org.awaitility + awaitility + test + com.google.guava guava-testlib diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java index 8a0acee2421a..de569990d2c1 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezJobMonitor.java @@ -156,8 +156,6 @@ private RenderStrategy.UpdateFunction updateFunction() { /** * Initializes the YARN queue metrics collector based on configuration. - * - *

This method implements the Null Object pattern - it always returns a non-null collector. * When metrics collection is disabled or initialization fails, a no-op collector is returned. * *

Metrics collection requires: @@ -193,14 +191,13 @@ private QueueMetricsCollector initializeMetricsCollector() { // Get queue name, default to "default" if not specified String queueName = getValidatedQueueName(); - // Get query ID from DAG name - String queryId = dag.getName(); + // Use the DAG name as the query identifier for logging + String dagName = dag.getName(); LOG.info("Initializing YARN queue metrics collector for queue: {}, refresh interval: {}ms", queueName, refreshInterval); - // Pool sizing (topology computation) is delegated to QueueMetricsRefreshPool. - return new YarnQueueMetricsCollector(yarnClient, queueName, refreshInterval, queryId, hiveConf); + return new YarnQueueMetricsCollector(yarnClient, queueName, refreshInterval, dagName); } catch (Exception e) { LOG.warn("Unable to initialize YARN queue metrics collector", e); return NoOpQueueMetricsCollector.INSTANCE; @@ -415,13 +412,9 @@ public int monitorExecution() { shutdownList.remove(dagClient); } - // Shutdown metrics collector (no-op if disabled) - try { - metricsCollector.shutdown(); - LOG.debug("Shut down metrics collector for queue: {}", metricsCollector.getQueueName()); - } catch (Exception e) { - LOG.warn("Error shutting down queue metrics collector", e); - } + // Close metrics collector (no-op if disabled) + metricsCollector.close(); + LOG.debug("Closed metrics collector for queue: {}", metricsCollector.getQueueName()); break; } diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java index aeba1ac375f9..6e8a6ec6f881 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/TezProgressMonitor.java @@ -61,12 +61,6 @@ public class TezProgressMonitor implements ProgressMonitor { * Try to get most the data required from dagClient in the constructor itself so that even after * the tez job has finished this object can be used for later use.s */ - TezProgressMonitor(DAGClient dagClient, DAGStatus status, List topSortedWork, - Map progressMap, SessionState.LogHelper console, long executionStartTime) - throws IOException, TezException { - this(dagClient, status, topSortedWork, progressMap, console, executionStartTime, - NoOpQueueMetricsCollector.INSTANCE); - } TezProgressMonitor(DAGClient dagClient, DAGStatus status, List topSortedWork, Map progressMap, SessionState.LogHelper console, long executionStartTime, diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java index 01ee53b3da51..5fa43e80463a 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/NoOpQueueMetricsCollector.java @@ -9,40 +9,43 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; /** * Null Object implementation of {@link QueueMetricsCollector}. * Used when queue metrics collection is disabled. Provides safe no-op behavior * so that callers don't need null checks. - * - * Thread-safe singleton following the Null Object pattern. */ public final class NoOpQueueMetricsCollector implements QueueMetricsCollector { - /** Singleton instance - thread-safe via static initialization. */ + public static final NoOpQueueMetricsCollector INSTANCE = new NoOpQueueMetricsCollector(); - // Private constructor prevents instantiation + private NoOpQueueMetricsCollector() { } + @Override public QueueMetricsSnapshot getLatestSnapshot() { // No metrics available when collection is disabled return null; } + @Override public String getQueueName() { // Return empty string instead of null to avoid NPEs return ""; } + @Override public void shutdown() { // No-op: nothing to shut down } + @Override public boolean isEnabled() { // Metrics collection is disabled for no-op collector diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java index 5b1198be94dc..b1861fae8979 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCache.java @@ -9,16 +9,16 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; -import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; @@ -35,7 +35,7 @@ * (no reads or writes). While a queue has active sessions, both the background refresh task * (via put) and TezProgressMonitor (via get) continuously reset the access timer — the entry * lives as long as the queue is in use. Once all sessions finish and the refresh task is - * cancelled, no more reads or writes occur and the entry auto-expires after 60 minutes. + * cancelled, no more reads or writes occur and the entry auto-expires after 5 minutes. * This provides a natural grace period for brief query gaps on the same queue. */ public final class QueueMetricsCache { @@ -43,22 +43,23 @@ public final class QueueMetricsCache { private static final QueueMetricsCache INSTANCE = new QueueMetricsCache(); - // Entries auto-expire after 60 minutes of no reads or writes. + private static final int CACHE_MAXIMUM_SIZE = 1000; + + // Entries auto-expire after 5 minutes of no reads or writes. // Active queues: reset continuously by get() (TezProgressMonitor polling) and put() (RM refresh). - // Idle queues: task cancelled + job done → no reads or writes → expires after 60 minutes. - // 60 minutes safely exceeds any realistic refresh interval or progress polling interval. - private static final long CACHE_EXPIRE_AFTER_ACCESS_MINUTES = 60; + // Idle queues: task cancelled + job done → no reads or writes → expires after 5 minutes. + private static final long CACHE_EXPIRE_AFTER_ACCESS_MINUTES = 5; private final Cache cache; private QueueMetricsCache() { this.cache = CacheBuilder.newBuilder() - .maximumSize(1000) + .maximumSize(CACHE_MAXIMUM_SIZE) .expireAfterAccess(CACHE_EXPIRE_AFTER_ACCESS_MINUTES, TimeUnit.MINUTES) .build(); - LOG.info("QueueMetricsCache initialized: max=1000, expireAfterAccess={}min", - CACHE_EXPIRE_AFTER_ACCESS_MINUTES); + LOG.info("QueueMetricsCache initialized: max={}, expireAfterAccess={}min", + CACHE_MAXIMUM_SIZE, CACHE_EXPIRE_AFTER_ACCESS_MINUTES); } public static QueueMetricsCache getInstance() { @@ -78,42 +79,38 @@ public QueueMetricsState get(String queueName) { /** * Updates the snapshot on an existing entry after a successful RM fetch, or creates - * a new entry if none exists. expireAfterAccess timer resets on this call. + * a new entry if none exists. Uses atomic compute() to avoid race conditions. + * expireAfterAccess timer resets on this call. */ public void put(String queueName, QueueMetricsSnapshot snapshot, long refreshIntervalMs) { if (queueName == null || snapshot == null) { return; } - QueueMetricsState existing = cache.getIfPresent(queueName); - if (existing == null) { - cache.put(queueName, new QueueMetricsState(snapshot, refreshIntervalMs)); - LOG.debug("Created state entry for queue: {}", queueName); - } else { - existing.applySnapshot(snapshot, refreshIntervalMs); - } + cache.asMap().compute(queueName, (k, existing) -> { + if (existing == null) { + LOG.debug("Created state entry for queue: {}", k); + return new QueueMetricsState(snapshot, refreshIntervalMs); + } + existing.applySnapshot(snapshot); + return existing; + }); } /** - * Atomically creates a placeholder {@link QueueMetricsState} with no snapshot. - * Uses putIfAbsent so concurrent sessions racing to create the first entry are safe: - * exactly one placeholder wins and all others get the same entry back. + * Atomically gets or creates a {@link QueueMetricsState} for the given queue. + * Uses computeIfAbsent so concurrent sessions racing to create the first entry are safe: + * exactly one instance is created and all callers get the same entry back. * Returns the authoritative state (the one actually in the cache). */ - public QueueMetricsState putPlaceholder(String queueName, long refreshIntervalMs) { + public QueueMetricsState getOrCreate(String queueName, long refreshIntervalMs) { if (queueName == null) { return null; } - // lastWriteTime=0 (epoch) inside QueueMetricsState signals "never written" — - // ensures the first refresh fires immediately (age = now - 0 always exceeds any interval). - QueueMetricsState newState = new QueueMetricsState(null, refreshIntervalMs); - QueueMetricsState existing = cache.asMap().putIfAbsent(queueName, newState); - if (existing != null) { - LOG.debug("State for queue: {} already created by concurrent session, using existing", queueName); - return existing; - } - LOG.debug("Created placeholder state for queue: {}", queueName); - return newState; + return cache.asMap().computeIfAbsent(queueName, k -> { + LOG.debug("Created state for queue: {}", k); + return new QueueMetricsState(null, refreshIntervalMs); + }); } /** Returns the number of queues currently tracked in the cache. */ @@ -132,13 +129,4 @@ public void shutdown() { LOG.warn("Error during cache shutdown", e); } } - - /** - * Resets cache for test isolation. NEVER call in production code. - */ - @VisibleForTesting - public static void resetForTesting() { - INSTANCE.cache.invalidateAll(); - LOG.debug("QueueMetricsCache reset for testing"); - } } diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java index 334fdc67373c..268438791a91 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsCollector.java @@ -9,19 +9,22 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; /** * Interface for collecting YARN queue metrics. * Implementations include the active collector ({@link YarnQueueMetricsCollector}) * and a no-op variant ({@link NoOpQueueMetricsCollector}) following the Null Object pattern. + *

+ * Implements {@link AutoCloseable} to support try-with-resources and ensure proper cleanup. */ -public interface QueueMetricsCollector { +public interface QueueMetricsCollector extends AutoCloseable { /** * Returns the latest queue metrics snapshot. * @@ -37,8 +40,19 @@ public interface QueueMetricsCollector { /** * Shuts down the metrics collector and releases resources. * Safe to call multiple times. + *

+ * This method is called by {@link #close()} to support the AutoCloseable pattern. */ void shutdown(); + /** + * Closes the collector and releases resources. + * Implements {@link AutoCloseable#close()} by delegating to {@link #shutdown()}. + * Does not throw checked exceptions. + */ + @Override + default void close() { + shutdown(); + } /** * Returns whether queue metrics collection is enabled for this collector. * diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java index 666be60865eb..2cf7b2d08d45 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsRefreshPool.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; @@ -26,6 +27,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -49,11 +51,9 @@ public final class QueueMetricsRefreshPool { public static final int JITTER_PERCENT = 10; private static final AtomicReference INSTANCE = new AtomicReference<>(null); - private static final Object INIT_LOCK = new Object(); private final ScheduledExecutorService refreshPool; - /** * Initializes the singleton pool with the specified thread count. * Must be called during HiveServer2 startup. Subsequent calls are ignored. @@ -65,10 +65,10 @@ public static void init(int threadCount) { LOG.debug("QueueMetricsRefreshPool already initialized, ignoring init call"); return; } - synchronized (INIT_LOCK) { - if (INSTANCE.get() == null) { - INSTANCE.set(new QueueMetricsRefreshPool(threadCount)); - } + QueueMetricsRefreshPool newInstance = new QueueMetricsRefreshPool(threadCount); + if (!INSTANCE.compareAndSet(null, newInstance)) { + // Another thread won the race, shut down our instance + newInstance.refreshPool.shutdownNow(); } } @@ -79,24 +79,22 @@ public static void init(int threadCount) { * @return the singleton pool instance */ public static QueueMetricsRefreshPool getInstance() { - QueueMetricsRefreshPool local = INSTANCE.get(); - if (local != null) { - return local; + QueueMetricsRefreshPool current = INSTANCE.get(); + if (current != null) { + return current; } // Lazy init for tests/non-HS2 with default thread count - synchronized (INIT_LOCK) { - local = INSTANCE.get(); - if (local == null) { - LOG.warn("QueueMetricsRefreshPool not initialized via init(), using default thread count: {}", - DEFAULT_THREAD_COUNT); - local = new QueueMetricsRefreshPool(DEFAULT_THREAD_COUNT); - INSTANCE.set(local); - } - return local; + LOG.warn("QueueMetricsRefreshPool not initialized via init(), using default thread count: {}", + DEFAULT_THREAD_COUNT); + QueueMetricsRefreshPool newInstance = new QueueMetricsRefreshPool(DEFAULT_THREAD_COUNT); + if (INSTANCE.compareAndSet(null, newInstance)) { + return newInstance; } + // Another thread won the race, shut down our instance and return the winner + newInstance.refreshPool.shutdownNow(); + return INSTANCE.get(); } - private QueueMetricsRefreshPool(int threadCount) { this.refreshPool = Executors.newScheduledThreadPool(threadCount, new ThreadFactoryBuilder() @@ -106,7 +104,6 @@ private QueueMetricsRefreshPool(int threadCount) { LOG.info("QueueMetricsRefreshPool initialized with {} threads", threadCount); } - /** * Schedules a periodic refresh task. initialDelay=0 so the first fetch runs immediately. * @@ -124,29 +121,25 @@ public ScheduledFuture scheduleRefreshTask(Runnable task, long intervalMs) { * after all Tez sessions have been stopped. */ public static void shutdown() { - synchronized (INIT_LOCK) { - QueueMetricsRefreshPool current = INSTANCE.get(); - if (current == null) { - return; - } - LOG.info("Shutting down QueueMetricsRefreshPool"); - try { - current.refreshPool.shutdown(); - if (!current.refreshPool.awaitTermination(10, TimeUnit.SECONDS)) { - LOG.warn("QueueMetricsRefreshPool did not terminate gracefully, forcing shutdown"); - current.refreshPool.shutdownNow(); - if (!current.refreshPool.awaitTermination(5, TimeUnit.SECONDS)) { - LOG.error("QueueMetricsRefreshPool did not terminate after forced shutdown"); - } - } - LOG.info("QueueMetricsRefreshPool shutdown complete"); - } catch (InterruptedException e) { - LOG.warn("Interrupted during QueueMetricsRefreshPool shutdown", e); + QueueMetricsRefreshPool current = INSTANCE.getAndSet(null); + if (current == null) { + return; + } + LOG.info("Shutting down QueueMetricsRefreshPool"); + try { + current.refreshPool.shutdown(); + if (!current.refreshPool.awaitTermination(10, TimeUnit.SECONDS)) { + LOG.warn("QueueMetricsRefreshPool did not terminate gracefully, forcing shutdown"); current.refreshPool.shutdownNow(); - Thread.currentThread().interrupt(); - } finally { - INSTANCE.set(null); + if (!current.refreshPool.awaitTermination(5, TimeUnit.SECONDS)) { + LOG.error("QueueMetricsRefreshPool did not terminate after forced shutdown"); + } } + LOG.info("QueueMetricsRefreshPool shutdown complete"); + } catch (InterruptedException e) { + LOG.warn("Interrupted during QueueMetricsRefreshPool shutdown", e); + current.refreshPool.shutdownNow(); + Thread.currentThread().interrupt(); } } @@ -172,29 +165,20 @@ public static long calculateJitter(String queueName, long intervalMs) { // Test Support // ───────────────────────────────────────────────────────── + /** - * Returns the current singleton instance without lazy initialization. - * Used for testing to verify if the pool was initialized via init(). - * NEVER call in production code - use {@link #getInstance()} instead. + * Returns true if the pool has been initialized (either via init() or lazy getInstance()). + * Does not trigger lazy initialization. Used for testing. * - * @return the current instance, or null if not initialized + * @return true if initialized, false otherwise */ @VisibleForTesting - public static QueueMetricsRefreshPool getInstanceForTesting() { - return INSTANCE.get(); + public static boolean isInitialized() { + return INSTANCE.get() != null; } - /** - * Resets the singleton for test isolation. NEVER call in production code. - */ @VisibleForTesting - public static void resetForTesting() { - synchronized (INIT_LOCK) { - QueueMetricsRefreshPool current = INSTANCE.get(); - if (current != null) { - current.refreshPool.shutdownNow(); - INSTANCE.set(null); - } - } + public int getThreadCount() { + return ((ScheduledThreadPoolExecutor) refreshPool).getCorePoolSize(); } } diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java index 2da57ca0760b..e812a5bb89cc 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsSnapshot.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java index e291e0f0db35..b252151728c0 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/QueueMetricsState.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; @@ -21,9 +22,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.OptionalLong; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -39,11 +40,17 @@ *

* Ownership model: *

    - *
  • {@code intervalCounts}, {@code minRefreshIntervalMs}, {@code activeSessionCount} - * — owned by {@link #registerInterval}/{@link #deregisterInterval} via lock-free atomics
  • + *
  • {@code sessionIntervals} — a per-queue min-heap with one entry per active session. + * {@code peek()} = O(1) minimum. Owned by + * {@link #registerInterval}/{@link #deregisterInterval} via {@link PriorityBlockingQueue} + * thread-safe operations.
  • + *
  • {@code minRefreshIntervalMs} — volatile hint written only inside + * {@link #ensureTaskScheduled} (under {@code synchronized}); read lock-free by the + * refresh tick for the per-tick freshness check in + * {@code YarnQueueMetricsCollector.refreshMetrics()}.
  • *
  • {@code refreshTask}, {@code taskCurrentRefreshIntervalMs} * — owned by {@link #ensureTaskScheduled} under {@code synchronized(this)}
  • - *
  • {@code snapshot}, {@code lastWriteTime} — written by the refresh thread, read by + *
  • {@code snapshot}, {@code lastWriteNanos} — written by the refresh thread, read by * TezProgressMonitor; {@code volatile} for visibility without synchronization
  • *
*/ @@ -54,13 +61,18 @@ public class QueueMetricsState { private static final int CIRCUIT_BREAKER_PROBE_INTERVAL = 10; // Metrics data (written by refresh thread, read by TezProgressMonitor) - private final AtomicReference snapshot; - private volatile long lastWriteTime; + private volatile QueueMetricsSnapshot snapshot; + private volatile long lastWriteNanos; // System.nanoTime() at last successful write; 0 = never written - // Session interval tracking (lock-free atomics) - private final AtomicLong minRefreshIntervalMs; - private final ConcurrentHashMap intervalCounts = new ConcurrentHashMap<>(); - private final AtomicInteger activeSessionCount = new AtomicInteger(0); + // One entry per active session on this queue. PriorityBlockingQueue keeps the minimum at the + // head (peek() = O(1)). Duplicates are naturally supported — no count bookkeeping needed. + // Scoped to a single queue; n = concurrent queries on that queue (typically tens at most). + private final PriorityBlockingQueue sessionIntervals = new PriorityBlockingQueue<>(); + + // Volatile hint: the minimum interval currently in effect for this queue. + // Written only inside ensureTaskScheduled (synchronized) so it always reflects the + // authoritative scheduled interval. Read lock-free by refreshMetrics() every tick. + private volatile long minRefreshIntervalMs; // Refresh task (owned by ensureTaskScheduled under synchronized(this)) private final AtomicReference> refreshTask = new AtomicReference<>(null); @@ -74,140 +86,125 @@ public class QueueMetricsState { private final AtomicInteger circuitBreakerSkipCount = new AtomicInteger(0); QueueMetricsState(QueueMetricsSnapshot snapshot, long refreshIntervalMs) { - this.snapshot = new AtomicReference<>(snapshot); - this.lastWriteTime = 0L; // epoch = "never written" — ensures first fetch fires immediately - this.minRefreshIntervalMs = new AtomicLong(refreshIntervalMs); + this.snapshot = snapshot; + this.lastWriteNanos = 0L; // 0 = "never written" — ensures first fetch fires immediately + this.minRefreshIntervalMs = refreshIntervalMs; this.taskCurrentRefreshIntervalMs = new AtomicLong(refreshIntervalMs); } /** - * Returns the latest snapshot, or null if not yet fetched. + * Returns ms since last successful RM write using a monotonic clock. + * Large value on first call (lastWriteNanos=0). */ - public QueueMetricsSnapshot getSnapshot() { - return snapshot.get(); + public long getAgeMs() { + if (lastWriteNanos == 0L) { + return Long.MAX_VALUE; + } + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - lastWriteNanos); } /** - * Returns ms since last successful RM write. Large value on first call (lastWriteTime=0). + * Returns the latest snapshot, or null if not yet fetched. */ - public long getAgeMs() { - return System.currentTimeMillis() - lastWriteTime; + public QueueMetricsSnapshot getSnapshot() { + return snapshot; } /** - * Returns the minimum refresh interval across all active sessions. + * Returns the minimum refresh interval across all active sessions on this queue. + * Volatile read — O(1), safe to call from the refresh tick without locking. */ public long getMinRefreshIntervalMs() { - return minRefreshIntervalMs.get(); + return minRefreshIntervalMs; } - /** Updates snapshot and lastWriteTime after a successful RM fetch. */ - public void applySnapshot(QueueMetricsSnapshot newSnapshot, - long refreshIntervalMs) { - this.snapshot.set(newSnapshot); - this.lastWriteTime = System.currentTimeMillis(); - minRefreshIntervalMs.updateAndGet(current -> Math.min(current, refreshIntervalMs)); + /** + * Updates snapshot and lastWriteNanos after a successful RM fetch. + * Does not touch minRefreshIntervalMs — that field is owned by ensureTaskScheduled. + */ + public void applySnapshot(QueueMetricsSnapshot newSnapshot) { + this.snapshot = newSnapshot; + this.lastWriteNanos = System.nanoTime(); } - /** - * Registers this session's interval. Returns true if rescheduling may be needed - * (no task running, or this session lowered the minimum interval). - * Thread-safe: compute() is atomic per-key; getAndAccumulate returns previous value - * so only the thread that actually lowered the minimum triggers rescheduling. + * Registers this session's refresh interval by adding one entry to the min-heap. + * Returns true if rescheduling may be needed — i.e. no task is running yet, or this + * session is strictly faster than the previous minimum and may have lowered it. + *

+ * Thread-safe: {@link PriorityBlockingQueue#offer} and {@link PriorityBlockingQueue#peek} + * are individually thread-safe. The peek-before-offer pattern is not atomic; a concurrent + * offer between the two may cause an extra {@code ensureTaskScheduled} call, but that + * method is idempotent under its lock and will simply do nothing in that case. */ public boolean registerInterval(long refreshIntervalMs) { - intervalCounts.compute(refreshIntervalMs, (k, existing) -> { - if (existing == null) { - return new AtomicInteger(1); - } - existing.incrementAndGet(); - return existing; - }); - long prevMin = minRefreshIntervalMs.getAndAccumulate(refreshIntervalMs, Math::min); - int count = activeSessionCount.incrementAndGet(); - LOG.debug("Session registered at {}ms, activeCount={}", refreshIntervalMs, count); - return refreshTask.get() == null || refreshIntervalMs < prevMin; + Long prevMin = sessionIntervals.peek(); // capture before offer to detect minimum change + sessionIntervals.offer(refreshIntervalMs); + LOG.debug("Session registered at {}ms, totalSessions={}", refreshIntervalMs, sessionIntervals.size()); + // Reschedule only if: no task running yet (first session for this queue), + // OR this session is strictly faster than the previous minimum (lowered it). + return refreshTask.get() == null || (prevMin != null && refreshIntervalMs < prevMin); } /** - * Deregisters this session's interval. Returns true if the task interval may need - * to change (this thread removed the last session at or below the current task interval). - * Thread-safe: compute() atomically decrements and conditionally removes the bucket. + * Deregisters this session's refresh interval by removing one occurrence from the min-heap. + * Returns true if rescheduling may be needed — i.e. the removed interval was at or faster + * than the current task speed, meaning the task may need to slow down or stop. + *

+ * Thread-safe: {@link PriorityBlockingQueue#remove(Object)} is thread-safe. + * O(n) scan of the heap — negligible since n = sessions on a single queue (typically tens). */ public boolean deregisterInterval(long refreshIntervalMs) { - boolean[] thisBucketRemoved = {false}; - intervalCounts.compute(refreshIntervalMs, (k, existing) -> { - if (existing == null) { - return null; // already removed by concurrent deregister - } - if (existing.decrementAndGet() <= 0) { - thisBucketRemoved[0] = true; - return null; // atomically removes the key - } - return existing; - }); - int remaining = activeSessionCount.updateAndGet(c -> Math.max(0, c - 1)); - LOG.debug("Session deregistered at {}ms, activeCount={}", refreshIntervalMs, remaining); - - if (!thisBucketRemoved[0]) { - return false; // other sessions still at this interval — task unchanged - } - long currentTaskInterval = taskCurrentRefreshIntervalMs.get(); - if (refreshIntervalMs > currentTaskInterval) { - return false; // our interval was slower than the task — removing it changes nothing - } - // Recompute new minimum from remaining buckets. - // orElse(MAX_VALUE): no sessions left — safe sentinel that won't corrupt concurrent registers. - long newMin = intervalCounts.keySet().stream() - .mapToLong(Long::longValue).min().orElse(Long.MAX_VALUE); - // updateAndGet(Math.min): don't overwrite a lower value a concurrent register may have set. - minRefreshIntervalMs.updateAndGet(current -> Math.min(current, newMin)); - return newMin != currentTaskInterval; + sessionIntervals.remove(refreshIntervalMs); + LOG.debug("Session deregistered at {}ms, totalSessions={}", refreshIntervalMs, sessionIntervals.size()); + // Reschedule only if the removed session was running at or faster than the current task speed. + // If it was slower, removing it cannot require the task to change — skip the synchronized call. + return refreshIntervalMs <= taskCurrentRefreshIntervalMs.get(); } - /** - * Ensures the shared refresh task fires at the minimum interval derived from - * active sessions. Cancels and reschedules only when the interval actually changed. - * Serialized under {@code synchronized(this)} — the single scheduling authority for - * this queue. Re-reads intervalCounts inside the lock for authoritative state. + * Ensures the shared refresh task fires at the minimum interval across active sessions. + * Cancels and reschedules only when the interval actually changed. + * Serialized under {@code synchronized} — the single scheduling authority for this queue. + * Uses {@code sessionIntervals.peek()} for the authoritative O(1) minimum. + * Updates the volatile {@code minRefreshIntervalMs} hint so refresh ticks can read the + * current minimum lock-free. * * @param poolManager pool that owns the scheduled executor * @param refreshTask the refresh runnable bound to the calling collector * @param queueName used for logging only */ - public void ensureTaskScheduled(QueueMetricsRefreshPool poolManager, - Runnable refreshTask, String queueName) { - synchronized (this) { - OptionalLong minOptional = intervalCounts.keySet().stream() - .mapToLong(Long::longValue).min(); - ScheduledFuture currentTask = this.refreshTask.get(); - - if (minOptional.isEmpty()) { - if (currentTask != null) { - currentTask.cancel(false); - this.refreshTask.set(null); - LOG.info("Cancelled refresh task for queue: {} — no active sessions remaining", queueName); - } - return; - } - - long desiredInterval = minOptional.getAsLong(); - long currentInterval = taskCurrentRefreshIntervalMs.get(); + public synchronized void ensureTaskScheduled(QueueMetricsRefreshPool poolManager, + Runnable refreshTask, String queueName) { + ScheduledFuture currentTask = this.refreshTask.get(); - if (currentTask == null) { - scheduleTask(poolManager, refreshTask, desiredInterval, queueName); - } else if (currentInterval != desiredInterval) { + if (sessionIntervals.isEmpty()) { + minRefreshIntervalMs = 0L; // 0 = no active sessions; freshness check (age < min) is always false + if (currentTask != null) { currentTask.cancel(false); this.refreshTask.set(null); - scheduleTask(poolManager, refreshTask, desiredInterval, queueName); - LOG.info("Rescheduled refresh task for queue: {} from {}ms to {}ms", - queueName, currentInterval, desiredInterval); - } else { - LOG.debug("Refresh task for queue: {} already at correct interval {}ms", - queueName, currentInterval); + LOG.info("Cancelled refresh task for queue: {} — no active sessions remaining", queueName); } + return; + } + + long desiredInterval = sessionIntervals.peek(); // O(1) — min-heap head + long currentInterval = taskCurrentRefreshIntervalMs.get(); + + // Update volatile hint so refreshMetrics() reads the correct minimum lock-free every tick + minRefreshIntervalMs = desiredInterval; + + if (currentTask == null) { + scheduleTask(poolManager, refreshTask, desiredInterval, queueName); + } else if (currentInterval != desiredInterval) { + currentTask.cancel(false); + this.refreshTask.set(null); + scheduleTask(poolManager, refreshTask, desiredInterval, queueName); + LOG.info("Rescheduled refresh task for queue: {} from {}ms to {}ms", + queueName, currentInterval, desiredInterval); + } else { + LOG.debug("Refresh task for queue: {} already at correct interval {}ms", + queueName, currentInterval); } } @@ -236,7 +233,6 @@ public void finishRefresh() { isRefreshing.set(false); } - /** * Returns true if the circuit breaker is active and this tick should be skipped. * Allows one probe attempt every {@value #CIRCUIT_BREAKER_PROBE_INTERVAL} ticks. @@ -284,4 +280,3 @@ public void recordRefreshSuccess(String queueName) { } } } - diff --git a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java index 09bbed74c912..5d244af4dd79 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/YarnQueueMetricsCollector.java @@ -9,21 +9,22 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; -import org.apache.hadoop.hive.conf.HiveConf; import org.apache.hadoop.yarn.api.records.QueueInfo; import org.apache.hadoop.yarn.client.api.YarnClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Objects; /** * Collects YARN queue resource metrics using a shared cache to reduce ResourceManager load. @@ -39,8 +40,7 @@ public class YarnQueueMetricsCollector implements QueueMetricsCollector { private final YarnClient yarnClient; private final String queueName; private final long refreshIntervalMs; - private final String queryId; - + private final String dagName; /** * Creates a collector for the given queue and query. Non-blocking: metrics are @@ -50,53 +50,41 @@ public class YarnQueueMetricsCollector implements QueueMetricsCollector { * @param yarnClient Live YarnClient from the Tez session * @param queueName YARN queue this query runs on * @param refreshIntervalMs How often to poll YARN RM (ms) - * @param queryId DAG name for logging - * @param hiveConf Unused (kept for API compatibility) + * @param dagName Tez DAG name used to identify the query for logging purposes. */ - public YarnQueueMetricsCollector(YarnClient yarnClient, String queueName, long refreshIntervalMs, String queryId, - HiveConf hiveConf) { - if (yarnClient == null) { - throw new IllegalArgumentException("YarnClient cannot be null"); - } - if (queueName == null) { - throw new IllegalArgumentException("Queue name cannot be null"); - } + public YarnQueueMetricsCollector(YarnClient yarnClient, String queueName, long refreshIntervalMs, String dagName) { + this.yarnClient = Objects.requireNonNull(yarnClient, "YarnClient cannot be null"); + this.queueName = Objects.requireNonNull(queueName, "Queue name cannot be null"); if (refreshIntervalMs <= 0) { throw new IllegalArgumentException("refreshIntervalMs must be > 0, got: " + refreshIntervalMs); } - this.yarnClient = yarnClient; - this.queueName = queueName; this.refreshIntervalMs = refreshIntervalMs; - this.queryId = queryId; - + this.dagName = dagName; // Register session and start background refresh scheduling. initializeSession(); LOG.info("Started queue metrics collector for queue: {}, refresh interval: {}ms, query: {}", queueName, - refreshIntervalMs, queryId); + refreshIntervalMs, dagName); } - /** * Startup sequence: get or create the cache entry for {@code queueName}, * register this session's interval, then schedule the refresh task if needed. - * Concurrent safety: {@link QueueMetricsCache#putPlaceholder} uses - * {@code putIfAbsent} — two threads seeing null both get back the same entry. + * Concurrent safety: {@link QueueMetricsCache#getOrCreate} uses + * {@code computeIfAbsent} — concurrent sessions always get the same entry back. */ private void initializeSession() { - QueueMetricsCache cache = QueueMetricsCache.getInstance(); - QueueMetricsState state = cache.get(queueName); + QueueMetricsState state = QueueMetricsCache.getInstance().getOrCreate(queueName, refreshIntervalMs); if (state == null) { - state = cache.putPlaceholder(queueName, refreshIntervalMs); + throw new IllegalStateException("Failed to create QueueMetricsState for queue: " + queueName); } if (state.registerInterval(refreshIntervalMs)) { state.ensureTaskScheduled(QueueMetricsRefreshPool.getInstance(), this::refreshMetrics, queueName); } } - private void refreshMetrics() { try { QueueMetricsState state = QueueMetricsCache.getInstance().get(queueName); @@ -154,7 +142,6 @@ private QueueMetricsSnapshot fetchFromRM() { } } - /** * Returns the latest snapshot from cache (non-blocking). Null if not yet available. */ @@ -184,10 +171,10 @@ public String getQueueName() { public void shutdown() { QueueMetricsState state = QueueMetricsCache.getInstance().get(queueName); if (state == null) { - LOG.info("Cache entry already cleared for queue: {} on shutdown of query: {}", queueName, queryId); + LOG.info("Cache entry already cleared for queue: {} on shutdown of query: {}", queueName, dagName); return; } - LOG.info("Query finished for queue: {}, query: {}", queueName, queryId); + LOG.info("Query finished for queue: {}, query: {}", queueName, dagName); if (state.deregisterInterval(refreshIntervalMs)) { state.ensureTaskScheduled(QueueMetricsRefreshPool.getInstance(), this::refreshMetrics, queueName); } diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java index d357f25c4171..562cdcb8b25d 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/TestTezSessionState.java @@ -23,9 +23,15 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; import org.apache.hadoop.hive.conf.HiveConf; import org.apache.hadoop.hive.conf.HiveConfForTest; @@ -36,10 +42,13 @@ import org.apache.tez.dag.api.TezException; import org.junit.Assert; import org.junit.Test; -import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + public class TestTezSessionState { private static final Logger LOG = LoggerFactory.getLogger(TestTezSessionState.class.getName()); @@ -151,17 +160,16 @@ public void testYarnClientNotInitializedWhenMetricsDisabled() { // Default config: queue metrics disabled (interval = 0) Assert.assertEquals("Default interval should be 0 (disabled)", - 0, HiveConf.getTimeVar(hiveConf, HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, TimeUnit.MILLISECONDS)); + 0, HiveConf.getTimeVar(hiveConf, HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, + TimeUnit.MILLISECONDS)); TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); - // Mock a TezClient and set it - TezClient mockTezClient = Mockito.mock(TezClient.class); - sessionState.setTezClient(mockTezClient); - + sessionState.setTezClient(mock(TezClient.class)); + // getYarnClient() should return null when metrics disabled - YarnClient yarnClient = sessionState.getYarnClient(); - Assert.assertNull("YarnClient should not be initialized when queue metrics are disabled", yarnClient); + Assert.assertNull("YarnClient should not be initialized when queue metrics are disabled", + sessionState.getYarnClient()); } /** @@ -178,17 +186,14 @@ public void testYarnClientLazilyInitializedWhenMetricsEnabled() { TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); - // Mock a TezClient and set it - TezClient mockTezClient = Mockito.mock(TezClient.class); - sessionState.setTezClient(mockTezClient); - + sessionState.setTezClient(mock(TezClient.class)); + // First call to getYarnClient() should initialize it YarnClient yarnClient = sessionState.getYarnClient(); Assert.assertNotNull("YarnClient should be initialized when queue metrics are enabled", yarnClient); // Second call should return the same instance - YarnClient yarnClient2 = sessionState.getYarnClient(); - Assert.assertSame("Should return the same YarnClient instance", yarnClient, yarnClient2); + Assert.assertSame("Should return the same YarnClient instance", yarnClient, sessionState.getYarnClient()); } /** @@ -206,55 +211,48 @@ public void testYarnClientNotInitializedWhenTezClientNull() { TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); // Don't set TezClient (session is null) - + // getYarnClient() should return null when TezClient is not set - YarnClient yarnClient = sessionState.getYarnClient(); - Assert.assertNull("YarnClient should not be initialized when TezClient is null", yarnClient); + Assert.assertNull("YarnClient should not be initialized when TezClient is null", + sessionState.getYarnClient()); } /** * Tests the thread-safety of lazy YarnClient initialization with concurrent calls. */ @Test - public void testYarnClientLazyInitializationThreadSafety() throws InterruptedException { + public void testYarnClientLazyInitializationThreadSafety() throws Exception { SessionState ss = createSessionState(); HiveConf hiveConf = ss.getConf(); - + // Enable queue metrics hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); TezSessionState sessionState = new TezSessionState(ss.getSessionId(), hiveConf); - TezClient mockTezClient = Mockito.mock(TezClient.class); - sessionState.setTezClient(mockTezClient); - - // Create multiple threads that call getYarnClient() concurrently - final int threadCount = 10; - Thread[] threads = new Thread[threadCount]; - YarnClient[] clients = new YarnClient[threadCount]; - - for (int i = 0; i < threadCount; i++) { - final int index = i; - threads[i] = new Thread(() -> { - clients[index] = sessionState.getYarnClient(); - }); - } - - // Start all threads - for (Thread thread : threads) { - thread.start(); - } - - // Wait for all threads to complete - for (Thread thread : threads) { - thread.join(); - } - - // All threads should get the same YarnClient instance - YarnClient firstClient = clients[0]; - Assert.assertNotNull("YarnClient should be initialized", firstClient); - - for (int i = 1; i < threadCount; i++) { - Assert.assertSame("All threads should get the same YarnClient instance", firstClient, clients[i]); + sessionState.setTezClient(mock(TezClient.class)); + + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch start = new CountDownLatch(1); + + try { + List> futures = IntStream.range(0, threadCount) + .mapToObj(i -> executor.submit(() -> { + start.await(); + return sessionState.getYarnClient(); + })) + .toList(); + + start.countDown(); + + YarnClient firstClient = futures.getFirst().get(); + assertNotNull("YarnClient should be initialized", firstClient); + + for (Future future : futures) { + assertSame("All threads should get the same YarnClient instance", firstClient, future.get()); + } + } finally { + executor.shutdownNow(); } } } \ No newline at end of file diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java index 16bd27cd69d6..edbd6bd1af4b 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezJobMonitorQueueMetrics.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring; @@ -38,15 +39,16 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.atLeastOnce; @@ -57,6 +59,7 @@ /** * Test cases for TezJobMonitor queue metrics initialization. */ +@RunWith(MockitoJUnitRunner.class) public class TestTezJobMonitorQueueMetrics { @Mock @@ -77,11 +80,9 @@ public class TestTezJobMonitorQueueMetrics { private HiveConf hiveConf; private List topSortedWorks; private SessionState sessionState; - private AutoCloseable mockCloseable; @Before public void setUp() { - mockCloseable = MockitoAnnotations.openMocks(this); hiveConf = new HiveConfForTest(TestTezJobMonitorQueueMetrics.class); hiveConf.set("hive.security.authorization.manager", "org.apache.hadoop.hive.ql.security.authorization.plugin.sqlstd.SQLStdConfOnlyAuthorizerFactory"); @@ -92,9 +93,6 @@ public void setUp() { @After public void tearDown() throws Exception { - if (mockCloseable != null) { - mockCloseable.close(); - } if (sessionState != null) { sessionState.close(); } @@ -102,14 +100,10 @@ public void tearDown() throws Exception { @Test public void testMetricsCollectorDisabledByDefault() throws Exception { - when(mockSession.getYarnClient()).thenReturn(null); // YarnClient should not be initialized - when(mockSession.getQueueName()).thenReturn("default"); - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); - assertNotNull("Monitor should be created", monitor); // When metrics are disabled (interval=0), getYarnClient() is never called because // the check happens before attempting to retrieve the YarnClient verify(mockSession, never()).getYarnClient(); @@ -123,11 +117,9 @@ public void testMetricsCollectorEnabledWithInterval() { when(mockSession.getYarnClient()).thenReturn(mockYarnClient); when(mockSession.getQueueName()).thenReturn("default"); - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); - assertNotNull("Monitor should be created", monitor); verify(mockSession, atLeastOnce()).getYarnClient(); } @@ -135,14 +127,9 @@ public void testMetricsCollectorEnabledWithInterval() { public void testMetricsCollectorDisabledWithZeroInterval() throws Exception { hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 0, TimeUnit.SECONDS); - when(mockSession.getYarnClient()).thenReturn(null); // YarnClient should not be initialized - when(mockSession.getQueueName()).thenReturn("default"); - - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); - assertNotNull("Monitor should be created", monitor); // When metrics are disabled (interval=0), getYarnClient() is never called verify(mockSession, never()).getYarnClient(); verify(mockYarnClient, never()).getQueueInfo(anyString()); @@ -152,32 +139,14 @@ public void testMetricsCollectorDisabledWithZeroInterval() throws Exception { public void testMetricsCollectorDisabledWithNegativeInterval() throws Exception { hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, -1, TimeUnit.SECONDS); - when(mockSession.getYarnClient()).thenReturn(null); // YarnClient should not be initialized - when(mockSession.getQueueName()).thenReturn("default"); - - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); - assertNotNull("Monitor should be created", monitor); // When metrics are disabled (interval<0), getYarnClient() is never called verify(mockSession, never()).getYarnClient(); verify(mockYarnClient, never()).getQueueInfo(anyString()); } - @Test - public void testMetricsCollectorWithSmallInterval() { - hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 500, TimeUnit.MILLISECONDS); - - when(mockSession.getYarnClient()).thenReturn(mockYarnClient); - when(mockSession.getQueueName()).thenReturn("default"); - - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); - - assertNotNull("Monitor should be created with adjusted interval", monitor); - } @Test public void testMetricsCollectorWithCustomQueue() { @@ -186,108 +155,79 @@ public void testMetricsCollectorWithCustomQueue() { when(mockSession.getYarnClient()).thenReturn(mockYarnClient); when(mockSession.getQueueName()).thenReturn("production.analytics"); - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); verify(mockSession, atLeastOnce()).getQueueName(); - assertNotNull("Monitor should be created with custom queue", monitor); } - /** - * Metrics enabled with a null YarnClient: monitor must still be created and must - * reach the YarnClient gate (verify getYarnClient called), but must NOT call - * getQueueName (nothing to resolve without a client). - */ - @Test - public void testMetricsCollectorWithNullYarnClient() { - hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); - when(mockSession.getYarnClient()).thenReturn(null); - when(mockSession.getQueueName()).thenReturn("default"); - - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); - - assertNotNull("Monitor must be created when YarnClient is null", monitor); - verify(mockSession, atLeastOnce()).getYarnClient(); + private QueueMetricsCollector getCollector(TezJobMonitor monitor) throws Exception { + Field collectorField = TezJobMonitor.class.getDeclaredField("metricsCollector"); + collectorField.setAccessible(true); + return (QueueMetricsCollector) collectorField.get(monitor); } /** - * Metrics enabled with a null queue name: monitor must be created and the code - * must reach both the YarnClient and queue-name gates. + * When metrics are enabled but YarnClient is null, the monitor should fall back + * to NoOpQueueMetricsCollector rather than throwing or attempting collection. */ @Test - public void testMetricsCollectorWithNullQueueName() { + public void testMetricsCollectorWithNullYarnClient() throws Exception { hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); - when(mockSession.getYarnClient()).thenReturn(mockYarnClient); - when(mockSession.getQueueName()).thenReturn(null); + when(mockSession.getYarnClient()).thenReturn(null); TezJobMonitor monitor = new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, mockPerfLogger); - assertNotNull("Monitor must be created when queue name is null", monitor); - verify(mockSession, atLeastOnce()).getYarnClient(); - verify(mockSession, atLeastOnce()).getQueueName(); + QueueMetricsCollector collector = getCollector(monitor); + assertEquals("Should fall back to NoOpQueueMetricsCollector when YarnClient is null", + NoOpQueueMetricsCollector.class, collector.getClass()); + assertFalse("Collector should be disabled when YarnClient is null", collector.isEnabled()); + assertEquals("Queue name should be empty for NoOp collector", "", collector.getQueueName()); } /** - * Metrics enabled with a blank (whitespace-only) queue name: monitor must be - * created and both client and queue-name gates must be reached. + * When metrics are enabled but queue name is null or blank, the monitor should fall back + * to the default queue name rather than failing. + * Tests null, blank (" "), and explicitly "default" queue names. */ @Test - public void testMetricsCollectorWithBlankQueueName() { - hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); - when(mockSession.getYarnClient()).thenReturn(mockYarnClient); - when(mockSession.getQueueName()).thenReturn(" "); - - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); - - assertNotNull("Monitor must be created when queue name is blank", monitor); - verify(mockSession, atLeastOnce()).getYarnClient(); - verify(mockSession, atLeastOnce()).getQueueName(); - } - - @Test - public void testMetricsCollectorTypeWhenEnabled() throws Exception { - hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); - when(mockSession.getYarnClient()).thenReturn(mockYarnClient); - when(mockSession.getQueueName()).thenReturn("default"); - - TezJobMonitor monitor = - new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, - mockPerfLogger); - - // Use reflection to access metricsCollector field and verify type - Field collectorField = TezJobMonitor.class.getDeclaredField("metricsCollector"); - collectorField.setAccessible(true); - QueueMetricsCollector collector = (QueueMetricsCollector) collectorField.get(monitor); - - assertTrue("Should return YarnQueueMetricsCollector when enabled", - collector instanceof YarnQueueMetricsCollector); - assertTrue("Collector should report as enabled", collector.isEnabled()); + public void testMetricsCollectorWithDefaultQueueFallback() throws Exception { + String[] queueNames = {null, " ", "default"}; + String[] descriptions = {"null", "blank", "explicit default"}; + + for (int i = 0; i < queueNames.length; i++) { + String queueName = queueNames[i]; + String desc = descriptions[i]; + + hiveConf.setTimeVar(HiveConf.ConfVars.HIVE_TEZ_QUEUE_METRICS_REFRESH_INTERVAL, 10, TimeUnit.SECONDS); + when(mockSession.getYarnClient()).thenReturn(mockYarnClient); + when(mockSession.getQueueName()).thenReturn(queueName); + + TezJobMonitor monitor = + new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, + mockPerfLogger); + + QueueMetricsCollector collector = getCollector(monitor); + assertEquals("Should use YarnQueueMetricsCollector with default queue when queue name is " + desc, + YarnQueueMetricsCollector.class, collector.getClass()); + assertTrue("Collector should be enabled when queue name is " + desc, collector.isEnabled()); + assertEquals("Queue name should default to 'default' when " + desc, "default", collector.getQueueName()); + } } @Test public void testMetricsCollectorTypeWhenDisabled() throws Exception { // Default config has interval = 0 (disabled) - when(mockSession.getYarnClient()).thenReturn(mockYarnClient); - when(mockSession.getQueueName()).thenReturn("default"); - TezJobMonitor monitor = new TezJobMonitor(mockSession, topSortedWorks, mockDagClient, hiveConf, mockDag, mockContext, mockCounters, mockPerfLogger); - // Use reflection to access metricsCollector field and verify type - Field collectorField = TezJobMonitor.class.getDeclaredField("metricsCollector"); - collectorField.setAccessible(true); - QueueMetricsCollector collector = (QueueMetricsCollector) collectorField.get(monitor); - - assertTrue("Should return NoOpQueueMetricsCollector when disabled", - collector instanceof NoOpQueueMetricsCollector); - assertFalse("Collector should report as disabled", collector.isEnabled()); + QueueMetricsCollector collector = getCollector(monitor); + assertEquals("Should use NoOpQueueMetricsCollector when disabled", + NoOpQueueMetricsCollector.class, collector.getClass()); + assertFalse("Collector should be disabled", collector.isEnabled()); + assertEquals("Queue name should be empty for NoOp collector", "", collector.getQueueName()); } } diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitor.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitor.java index ca38b9f78846..eabc4e601b6c 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitor.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitor.java @@ -19,6 +19,7 @@ package org.apache.hadoop.hive.ql.exec.tez.monitoring; import org.apache.hadoop.hive.ql.plan.BaseWork; +import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.NoOpQueueMetricsCollector; import org.apache.hadoop.hive.ql.session.SessionState; import org.apache.tez.dag.api.TezException; import org.apache.tez.dag.api.client.DAGClient; @@ -92,7 +93,7 @@ public void setupInternalStateOnObjectCreation() throws IOException, TezExceptio TezProgressMonitor monitor = new TezProgressMonitor(dagClient, dagStatus, new ArrayList(), progressMap(), console, - Long.MAX_VALUE); + Long.MAX_VALUE, NoOpQueueMetricsCollector.INSTANCE); verify(dagClient).getVertexStatus(eq(MAPPER), isNull()); verify(dagClient).getVertexStatus(eq(REDUCER), isNull()); diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java index 313d5349981c..ad518594f7ec 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/TestTezProgressMonitorQueueMetrics.java @@ -9,126 +9,117 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring; import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsCollector; import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsSnapshot; -import org.apache.hadoop.hive.ql.plan.BaseWork; import org.apache.hadoop.hive.ql.session.SessionState; import org.apache.tez.dag.api.client.DAGClient; import org.apache.tez.dag.api.client.DAGStatus; -import org.apache.tez.dag.api.client.Progress; import org.apache.hadoop.hive.common.log.InPlaceUpdate; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; -import java.util.Map; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; /** * Test cases for TezProgressMonitor queue metrics functionality. */ +@RunWith(MockitoJUnitRunner.class) public class TestTezProgressMonitorQueueMetrics { @Mock private DAGClient mockDagClient; - @Mock private DAGStatus mockDagStatus; - @Mock private QueueMetricsCollector mockMetricsCollector; - @Mock private QueueMetricsSnapshot mockSnapshot; - @Mock private SessionState.LogHelper mockConsole; @Before public void setUp() { - MockitoAnnotations.openMocks(this); - // Default: metrics are enabled (can be overridden in individual tests) - when(mockMetricsCollector.isEnabled()).thenReturn(true); + // Default: metrics are enabled and snapshot uses standard baseline values. + // Lenient because not every test exercises all snapshot fields. + lenient().when(mockMetricsCollector.isEnabled()).thenReturn(true); + lenient().when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); + lenient().when(mockMetricsCollector.getQueueName()).thenReturn("default"); + lenient().when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); + + long now = System.currentTimeMillis(); + lenient().when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); + lenient().when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); + lenient().when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); + lenient().when(mockSnapshot.getVCoresUsed()).thenReturn(10); + lenient().when(mockSnapshot.getVCoresTotal()).thenReturn(100); + lenient().when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); + lenient().when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); + lenient().when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(10.0f); + lenient().when(mockSnapshot.getRunningApps()).thenReturn(1); + lenient().when(mockSnapshot.getPendingApps()).thenReturn(0); + lenient().when(mockSnapshot.getAllocatedContainers()).thenReturn(2); + lenient().when(mockSnapshot.getPendingContainers()).thenReturn(0); + lenient().when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 5000); } @Test public void testQueueMetricsWithNullCollector() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), null); - String result = monitor.queueMetrics(); - - assertEquals("Should return empty string when collector is null", "", result); + assertEquals("Should return empty string when collector is null", "", monitor.queueMetrics()); } @Test public void testQueueMetricsUnavailableWhenSnapshotNull() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.isEnabled()).thenReturn(true); // Enabled but snapshot unavailable when(mockMetricsCollector.getLatestSnapshot()).thenReturn(null); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), mockMetricsCollector); - String result = monitor.queueMetrics(); - assertEquals("Should return 'unavailable' when enabled but snapshot is null", - "QUEUE: unavailable", result); + "QUEUE: unavailable", monitor.queueMetrics()); } @Test public void testQueueMetricsDisabled() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.isEnabled()).thenReturn(false); // Metrics disabled (0s interval) - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(null); + when(mockMetricsCollector.isEnabled()).thenReturn(false); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), mockMetricsCollector); - String result = monitor.queueMetrics(); - - assertEquals("Should return empty string when metrics are disabled", - "", result); + assertEquals("Should return empty string when metrics are disabled", "", monitor.queueMetrics()); } @Test public void testQueueMetricsFormatting() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - // Setup snapshot with known values - long now = System.currentTimeMillis(); when(mockSnapshot.getMemoryUsedGB()).thenReturn(8.5f); when(mockSnapshot.getMemoryTotalGB()).thenReturn(16.0f); when(mockSnapshot.getMemoryPercentage()).thenReturn("53.12%"); @@ -141,321 +132,138 @@ public void testQueueMetricsFormatting() throws Exception { when(mockSnapshot.getPendingApps()).thenReturn(2); when(mockSnapshot.getAllocatedContainers()).thenReturn(12); when(mockSnapshot.getPendingContainers()).thenReturn(10); - when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 5000); // 5 seconds ago - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); - when(mockMetricsCollector.getQueueName()).thenReturn("default"); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), mockMetricsCollector); - String result = monitor.queueMetrics(); - - // Verify 4-line format (no staleness) - String[] lines = result.split("\n"); + String[] lines = monitor.queueMetrics().split("\n"); assertEquals("Should have 4 lines", 4, lines.length); - - // Line 1: Queue name (no staleness) assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: default")); assertFalse("Line 1 should NOT contain staleness", lines[0].contains("ago")); - - // Line 2: Memory + VCores with "used" label assertTrue("Line 2 should contain memory info", lines[1].contains("MEMORY: 8.5/16.0 GB")); assertTrue("Line 2 should contain 'used' label", lines[1].contains("53.12% used")); assertTrue("Line 2 should contain vCores info", lines[1].contains("VCORES: 100/200")); assertTrue("Line 2 should contain vCores 'used' label", lines[1].contains("50.00% used")); - - // Line 3: Capacity with (used) and (allocated) labels assertTrue("Line 3 should contain capacity used", lines[2].contains("CAPACITY: 25.00% (used)")); assertTrue("Line 3 should contain capacity allocated", lines[2].contains("60.00% (allocated)")); - - // Line 4: Apps and Containers assertTrue("Line 4 should contain running apps", lines[3].contains("APPS: 5 running")); assertTrue("Line 4 should contain pending apps", lines[3].contains("2 pending")); assertTrue("Line 4 should contain allocated containers", lines[3].contains("CONTAINERS: 12 allocated")); assertTrue("Line 4 should contain pending containers", lines[3].contains("10 pending")); - } + /** + * Tests that staleness is NOT shown in the new format regardless of age. + * Tests multiple staleness scenarios: fresh (0s), at boundary (60s), and stale (90s). + */ @Test - public void testQueueMetricsStalenessBeyond60Seconds() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - - long now = System.currentTimeMillis(); - when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); - when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); - when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getVCoresUsed()).thenReturn(10); - when(mockSnapshot.getVCoresTotal()).thenReturn(100); - when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); - when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(10.0f); - when(mockSnapshot.getRunningApps()).thenReturn(1); - when(mockSnapshot.getPendingApps()).thenReturn(0); - when(mockSnapshot.getAllocatedContainers()).thenReturn(2); - when(mockSnapshot.getPendingContainers()).thenReturn(0); - when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 120000); // 120 seconds ago - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); - when(mockMetricsCollector.getQueueName()).thenReturn("default"); - - TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, - System.currentTimeMillis(), mockMetricsCollector); - - String result = monitor.queueMetrics(); - - String[] lines = result.split("\n"); - assertEquals("Should have 4 lines", 4, lines.length); - // Staleness is removed in new format, so line 1 should only have queue name - assertFalse("Line 1 should NOT show staleness (removed from new format)", lines[0].contains("ago")); - assertTrue("Line 1 should contain QUEUE: default", lines[0].contains("QUEUE: default")); + public void testQueueMetricsStalenessNotShown() throws Exception { + long[] snapshotAgesSeconds = {0L, 60L, 90L}; + String[] descriptions = {"fresh (0s)", "at boundary (60s)", "stale (90s)"}; + + for (int i = 0; i < snapshotAgesSeconds.length; i++) { + long ageSeconds = snapshotAgesSeconds[i]; + String desc = descriptions[i]; + + // Update snapshot timestamp for this iteration + long now = System.currentTimeMillis(); + when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - (ageSeconds * 1000)); + + TezProgressMonitor monitor = new TezProgressMonitor( + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, + now, mockMetricsCollector); + + String[] lines = monitor.queueMetrics().split("\n"); + assertEquals("Should have 4 lines for " + desc, 4, lines.length); + assertFalse("Line 1 should NOT show staleness (removed from new format) for " + desc, + lines[0].contains("ago")); + assertTrue("Line 1 should contain QUEUE: default for " + desc, lines[0].contains("QUEUE: default")); + } } @Test public void testQueueNameTruncation() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - long now = System.currentTimeMillis(); - when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); - when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); - when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getVCoresUsed()).thenReturn(10); - when(mockSnapshot.getVCoresTotal()).thenReturn(100); - when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); - when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(10.0f); - when(mockSnapshot.getRunningApps()).thenReturn(1); - when(mockSnapshot.getPendingApps()).thenReturn(0); - when(mockSnapshot.getAllocatedContainers()).thenReturn(2); - when(mockSnapshot.getPendingContainers()).thenReturn(0); - when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 1000); - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); - // Very long queue name when(mockMetricsCollector.getQueueName()).thenReturn( "root.production.analytics.data-engineering.team-alpha.project-beta"); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), mockMetricsCollector); - String result = monitor.queueMetrics(); - - String[] lines = result.split("\n"); + String[] lines = monitor.queueMetrics().split("\n"); assertEquals("Should have 4 lines", 4, lines.length); - - // Line 1 should not exceed separator width (94 chars) - without staleness, more room for queue name assertTrue("Line 1 should not exceed 94 characters", lines[0].length() <= 94); - - // When the full queue name would cause line 1 overflow, it should be truncated from start with "..." if (lines[0].contains("...")) { - // Queue name was truncated assertFalse("Full long queue name should not appear", lines[0].contains("root.production.analytics.data-engineering.team-alpha.project-beta")); - assertTrue("Truncated queue name should contain ...", lines[0].contains("...")); } - - // Line 1 should NOT contain staleness (removed in new format) assertFalse("Line 1 should NOT contain staleness", lines[0].contains("ago")); - - // Line 2 should contain resource info assertTrue("Line 2 should contain MEMORY", lines[1].contains("MEMORY:")); - - // Line 3 should contain capacity assertTrue("Line 3 should contain CAPACITY", lines[2].contains("CAPACITY:")); - } @Test public void testQueueMetricsWithZeroPercentages() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - long now = System.currentTimeMillis(); + when(mockMetricsCollector.getQueueName()).thenReturn("empty"); when(mockSnapshot.getMemoryUsedGB()).thenReturn(0.0f); when(mockSnapshot.getMemoryTotalGB()).thenReturn(0.0f); when(mockSnapshot.getMemoryPercentage()).thenReturn("N/A"); when(mockSnapshot.getVCoresUsed()).thenReturn(0); when(mockSnapshot.getVCoresTotal()).thenReturn(0); when(mockSnapshot.getVCoresPercentage()).thenReturn("N/A"); - when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(0.0f); when(mockSnapshot.getRunningApps()).thenReturn(0); - when(mockSnapshot.getPendingApps()).thenReturn(0); when(mockSnapshot.getAllocatedContainers()).thenReturn(0); - when(mockSnapshot.getPendingContainers()).thenReturn(0); - when(mockSnapshot.getCollectionTimestamp()).thenReturn(now); - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); - when(mockMetricsCollector.getQueueName()).thenReturn("empty"); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), mockMetricsCollector); - String result = monitor.queueMetrics(); - - String[] lines = result.split("\n"); + String[] lines = monitor.queueMetrics().split("\n"); assertEquals("Should have 4 lines", 4, lines.length); - // Line 1: queue name (no staleness) assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: empty")); assertFalse("Line 1 should NOT contain staleness", lines[0].contains("ago")); - // Line 2: memory + vcores with N/A assertTrue("Line 2 should contain N/A for memory percentage", lines[1].contains("N/A")); assertTrue("Line 2 should handle zero values", lines[1].contains("0.0/0.0 GB")); - // Line 3: capacity assertTrue("Line 3 should contain capacity", lines[2].contains("CAPACITY:")); - // Line 4: apps and containers assertTrue("Line 4 should contain APPS:", lines[3].contains("APPS:")); assertTrue("Line 4 should contain CONTAINERS:", lines[3].contains("CONTAINERS:")); } @Test public void testQueueMetricsExceptionHandling() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenThrow( - new RuntimeException("Unexpected error")); + when(mockMetricsCollector.getLatestSnapshot()).thenThrow(new RuntimeException("Unexpected error")); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), mockMetricsCollector); - String result = monitor.queueMetrics(); - - // Should gracefully handle exceptions and return unavailable assertEquals("Should return unavailable on exception", - "QUEUE: unavailable", result); + "QUEUE: unavailable", monitor.queueMetrics()); } - @Test - public void testStalenessAtExactly60Seconds() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - - long now = System.currentTimeMillis(); - when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); - when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); - when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getVCoresUsed()).thenReturn(10); - when(mockSnapshot.getVCoresTotal()).thenReturn(100); - when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getCapacityPercentage()).thenReturn(50.0f); - when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(25.0f); - when(mockSnapshot.getRunningApps()).thenReturn(1); - when(mockSnapshot.getPendingApps()).thenReturn(0); - when(mockSnapshot.getAllocatedContainers()).thenReturn(2); - when(mockSnapshot.getPendingContainers()).thenReturn(0); - when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 60000L); // exactly 60s ago - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); - when(mockMetricsCollector.getQueueName()).thenReturn("default"); - - TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, - System.currentTimeMillis(), mockMetricsCollector); - - String result = monitor.queueMetrics(); - String[] lines = result.split("\n"); - - assertEquals("Should have 4 lines", 4, lines.length); - // Staleness is removed in new format - assertFalse("Should NOT show staleness (removed in new format)", lines[0].contains("ago")); - assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: default")); - } - - @Test - public void testStalenessAtZeroSeconds() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - - long now = System.currentTimeMillis(); - when(mockSnapshot.getMemoryUsedGB()).thenReturn(2.0f); - when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); - when(mockSnapshot.getMemoryPercentage()).thenReturn("20.00%"); - when(mockSnapshot.getVCoresUsed()).thenReturn(5); - when(mockSnapshot.getVCoresTotal()).thenReturn(50); - when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getCapacityPercentage()).thenReturn(60.0f); - when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(30.0f); - when(mockSnapshot.getRunningApps()).thenReturn(2); - when(mockSnapshot.getPendingApps()).thenReturn(0); - when(mockSnapshot.getAllocatedContainers()).thenReturn(4); - when(mockSnapshot.getPendingContainers()).thenReturn(0); - when(mockSnapshot.getCollectionTimestamp()).thenReturn(now); // right now - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); - when(mockMetricsCollector.getQueueName()).thenReturn("default"); - - TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, - System.currentTimeMillis(), mockMetricsCollector); - - String result = monitor.queueMetrics(); - String[] lines = result.split("\n"); - - assertEquals("Should have 4 lines", 4, lines.length); - // Staleness is removed in new format - assertFalse("Should NOT show staleness (removed in new format)", lines[0].contains("ago")); - assertTrue("Line 1 should contain queue name", lines[0].contains("QUEUE: default")); - } @Test public void testQueueNameExactlyAtMaxLength() throws Exception { - List works = new ArrayList<>(); - Map progressMap = new HashMap<>(); - long now = System.currentTimeMillis(); - when(mockSnapshot.getMemoryUsedGB()).thenReturn(1.0f); - when(mockSnapshot.getMemoryTotalGB()).thenReturn(10.0f); - when(mockSnapshot.getMemoryPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getVCoresUsed()).thenReturn(1); - when(mockSnapshot.getVCoresTotal()).thenReturn(10); - when(mockSnapshot.getVCoresPercentage()).thenReturn("10.00%"); - when(mockSnapshot.getCapacityPercentage()).thenReturn(40.0f); - when(mockSnapshot.getCurrentCapacityPercentage()).thenReturn(20.0f); - when(mockSnapshot.getRunningApps()).thenReturn(0); - when(mockSnapshot.getPendingApps()).thenReturn(0); - when(mockSnapshot.getAllocatedContainers()).thenReturn(0); - when(mockSnapshot.getPendingContainers()).thenReturn(0); - when(mockSnapshot.getCollectionTimestamp()).thenReturn(now - 2000); // not used since staleness removed - - when(mockDagStatus.getState()).thenReturn(DAGStatus.State.RUNNING); - when(mockMetricsCollector.getLatestSnapshot()).thenReturn(mockSnapshot); - - // Build a queue name that exactly fills the allowed space: - // Line 1 budget: MIN_TERMINAL_WIDTH (94) - "QUEUE: ".length(7) = 87 (no staleness) int maxLen = InPlaceUpdate.MIN_TERMINAL_WIDTH - "QUEUE: ".length(); - String exactName = "q".repeat(maxLen); // exactly maxLen characters + String exactName = "q".repeat(maxLen); when(mockMetricsCollector.getQueueName()).thenReturn(exactName); TezProgressMonitor monitor = new TezProgressMonitor( - mockDagClient, mockDagStatus, works, progressMap, mockConsole, + mockDagClient, mockDagStatus, new ArrayList<>(), new HashMap<>(), mockConsole, System.currentTimeMillis(), mockMetricsCollector); - String result = monitor.queueMetrics(); - String[] lines = result.split("\n"); - + String[] lines = monitor.queueMetrics().split("\n"); assertEquals("Should have 4 lines", 4, lines.length); - // Exactly at max length — should NOT be truncated - assertFalse("Queue name at exact max length should not be truncated", - lines[0].contains("...")); + assertFalse("Queue name at exact max length should not be truncated", lines[0].contains("...")); assertTrue("Full queue name should appear", lines[0].contains(exactName)); assertTrue("Line 1 should still be within terminal width", lines[0].length() <= InPlaceUpdate.MIN_TERMINAL_WIDTH); - // No staleness in new format assertFalse("Should NOT show staleness", lines[0].contains("ago")); } } diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java index 525b5ce278b9..ac49c8c2ba4c 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestNoOpQueueMetricsCollector.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; @@ -22,46 +23,26 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.fail; /** * Test cases for NoOpQueueMetricsCollector (Null Object pattern implementation). */ public class TestNoOpQueueMetricsCollector { - @Test - public void testInstanceIsSingleton() { - NoOpQueueMetricsCollector instance1 = NoOpQueueMetricsCollector.INSTANCE; - NoOpQueueMetricsCollector instance2 = NoOpQueueMetricsCollector.INSTANCE; - - assertNotNull("Instance should not be null", instance1); - assertSame("INSTANCE should return same reference", instance1, instance2); - } - @Test public void testIsEnabledReturnsFalse() { - NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; - - assertFalse("isEnabled should return false", collector.isEnabled()); + assertFalse("isEnabled should return false", NoOpQueueMetricsCollector.INSTANCE.isEnabled()); } @Test public void testGetLatestSnapshotReturnsNull() { - NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; - - assertNull("getLatestSnapshot should return null", collector.getLatestSnapshot()); + assertNull("getLatestSnapshot should return null", NoOpQueueMetricsCollector.INSTANCE.getLatestSnapshot()); } @Test public void testGetQueueNameReturnsEmptyString() { - NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; - - String queueName = collector.getQueueName(); - assertNotNull("Queue name should not be null", queueName); - assertEquals("Queue name should be empty string", "", queueName); + assertEquals("Queue name should be empty string", "", NoOpQueueMetricsCollector.INSTANCE.getQueueName()); } @Test @@ -84,64 +65,8 @@ public void testShutdownIsIdempotent() { collector.shutdown(); collector.shutdown(); - // Verify instance still works after shutdown calls assertFalse("isEnabled should still return false", collector.isEnabled()); assertNull("getLatestSnapshot should still return null", collector.getLatestSnapshot()); } - - - @Test - public void testNullObjectPatternAllowsSafePolymorphism() { - // NoOp collector can be used wherever QueueMetricsCollector is expected - // without null checks - NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; - - // Simulate typical usage pattern - if (collector.isEnabled()) { - // This branch should never execute - fail("NoOp collector should never report as enabled"); - } - - // Safe to call getLatestSnapshot without null check on collector - // (though snapshot itself will be null) - assertNull("Snapshot should be null", collector.getLatestSnapshot()); - - // Safe to get queue name without null check - assertNotNull("Queue name should not be null", collector.getQueueName()); - - // Safe to call shutdown without null check - collector.shutdown(); // No exception - } - - @Test - public void testCanBeUsedInPlaceOfNullCollector() { - // Common pattern: use NoOp instead of null to avoid null checks - NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; - - // This would NPE if collector was null - String queueName = collector.getQueueName(); - assertNotNull("Should not throw NPE", queueName); - - boolean enabled = collector.isEnabled(); - assertFalse("Should safely return false", enabled); - } - - @Test - public void testToStringDoesNotThrow() { - NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; - - String str = collector.toString(); - assertNotNull("toString should not return null", str); - } - - @Test - public void testHashCodeIsConsistent() { - NoOpQueueMetricsCollector collector = NoOpQueueMetricsCollector.INSTANCE; - - int hash1 = collector.hashCode(); - int hash2 = collector.hashCode(); - - assertEquals("hashCode should be consistent", hash1, hash2); - } } diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java index b6e2ccbca823..31098b2d4542 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsCache.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; @@ -23,8 +24,11 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -43,100 +47,83 @@ /** * Test cases for QueueMetricsCache. */ +@RunWith(MockitoJUnitRunner.class) public class TestQueueMetricsCache { + private static final Logger LOG = LoggerFactory.getLogger(TestQueueMetricsCache.class); + @Mock private QueueInfo mockQueueInfo; - @Mock private QueueStatistics mockQueueStats; - private AutoCloseable closeable; private QueueMetricsCache cache; @Before public void setUp() { - closeable = MockitoAnnotations.openMocks(this); cache = QueueMetricsCache.getInstance(); - // Note: We can't fully clear the cache between tests since it's a singleton, - // but we use unique queue names per test to avoid interference + cache.shutdown(); // clear any state from previous tests + setupMockQueueInfo(); } @After - public void tearDown() throws Exception { - if (closeable != null) { - closeable.close(); - } + public void tearDown() { + cache.shutdown(); } @Test public void testSingletonInstanceConsistency() { - QueueMetricsCache instance1 = QueueMetricsCache.getInstance(); - QueueMetricsCache instance2 = QueueMetricsCache.getInstance(); - - assertNotNull("Instance should not be null", instance1); - assertSame("getInstance should return same instance", instance1, instance2); + assertNotNull("Instance should not be null", QueueMetricsCache.getInstance()); + assertSame("getInstance should return same instance", + QueueMetricsCache.getInstance(), QueueMetricsCache.getInstance()); } @Test public void testGetReturnsNullForNonExistentQueue() { - String nonExistentQueue = "test-nonexistent-" + System.nanoTime(); - QueueMetricsState state = cache.get(nonExistentQueue); - - assertNull("Should return null for non-existent queue", state); + assertNull("Should return null for non-existent queue", cache.get("test-nonexistent")); } @Test public void testGetReturnsNullForNullQueueName() { - QueueMetricsState state = cache.get(null); - - assertNull("Should return null for null queue name", state); + assertNull("Should return null for null queue name", cache.get(null)); } @Test public void testPutCreatesNewEntry() { - setupMockQueueInfo(); - String queueName = "test-new-entry-" + System.nanoTime(); - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); + String queueName = "test-new-entry"; // Verify queue doesn't exist yet assertNull("Queue should not exist initially", cache.get(queueName)); // Put creates new entry - cache.put(queueName, snapshot, 5000L); + cache.put(queueName, new QueueMetricsSnapshot(mockQueueInfo), 5000L); QueueMetricsState state = cache.get(queueName); assertNotNull("Queue state should exist after put", state); assertNotNull("Snapshot should be available", state.getSnapshot()); - assertEquals("Memory used should match", 1.0f, state.getSnapshot().getMemoryUsedGB(), 0.01f); + assertEquals("Memory used should match", 1.0f, state.getSnapshot().getMemoryUsedGB(), 0.001f); } @Test public void testPutUpdatesExistingEntry() { - setupMockQueueInfo(); - String queueName = "test-update-entry-" + System.nanoTime(); + String queueName = "test-update-entry"; // Create initial entry - QueueMetricsSnapshot snapshot1 = new QueueMetricsSnapshot(mockQueueInfo); - cache.put(queueName, snapshot1, 5000L); + cache.put(queueName, new QueueMetricsSnapshot(mockQueueInfo), 5000L); // Update with new snapshot when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(4096L); - QueueMetricsSnapshot snapshot2 = new QueueMetricsSnapshot(mockQueueInfo); - cache.put(queueName, snapshot2, 3000L); + cache.put(queueName, new QueueMetricsSnapshot(mockQueueInfo), 3000L); QueueMetricsState state = cache.get(queueName); assertNotNull("State should exist", state); - assertEquals("Memory should be updated", 4.0f, state.getSnapshot().getMemoryUsedGB(), 0.01f); + assertEquals("Memory should be updated", 4.0f, state.getSnapshot().getMemoryUsedGB(), 0.001f); } @Test public void testPutWithNullQueueNameIsNoOp() { - setupMockQueueInfo(); - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); - // Should not throw exception - cache.put(null, snapshot, 5000L); + cache.put(null, new QueueMetricsSnapshot(mockQueueInfo), 5000L); // Confirms null queue name was silently ignored - no entry created assertNull("Null queue name should not create a cache entry", cache.get(null)); } @@ -153,10 +140,10 @@ public void testPutWithNullSnapshotIsNoOp() { } @Test - public void testPutPlaceholderCreatesEmptyEntry() { + public void testGetOrCreateCreatesEmptyEntry() { String queueName = "test-placeholder-" + System.nanoTime(); - QueueMetricsState state = cache.putPlaceholder(queueName, 10000L); + QueueMetricsState state = cache.getOrCreate(queueName, 10000L); assertNotNull("Placeholder state should be created", state); assertNull("Snapshot should be null initially", state.getSnapshot()); @@ -164,8 +151,8 @@ public void testPutPlaceholderCreatesEmptyEntry() { } @Test - public void testPutPlaceholderWithNullQueueName() { - QueueMetricsState state = cache.putPlaceholder(null, 5000L); + public void testGetOrCreateWithNullQueueName() { + QueueMetricsState state = cache.getOrCreate(null, 5000L); assertNull("Should return null for null queue name", state); } @@ -186,7 +173,7 @@ public void testConcurrentPutPlaceholderRaces() throws Exception { executor.submit(() -> { try { startLatch.await(); // Wait for signal to start - QueueMetricsState state = cache.putPlaceholder(queueName, 5000L + threadId * 100); + QueueMetricsState state = cache.getOrCreate(queueName, 5000L + threadId * 100); results.put(threadId, state); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -218,15 +205,14 @@ public void testConcurrentPutPlaceholderRaces() throws Exception { @Test public void testGetActiveQueueCount() { - int initialCount = cache.getActiveQueueCount(); - String queueName1 = "test-count-1-" + System.nanoTime(); - String queueName2 = "test-count-2-" + System.nanoTime(); + String queueName1 = "test-count-1"; + String queueName2 = "test-count-2"; - cache.putPlaceholder(queueName1, 5000L); - assertEquals("Count should increase by 1", initialCount + 1, cache.getActiveQueueCount()); + cache.getOrCreate(queueName1, 5000L); + assertEquals("Count should be 1", 1, cache.getActiveQueueCount()); - cache.putPlaceholder(queueName2, 5000L); - assertEquals("Count should increase by 2", initialCount + 2, cache.getActiveQueueCount()); + cache.getOrCreate(queueName2, 5000L); + assertEquals("Count should be 2", 2, cache.getActiveQueueCount()); } @@ -240,28 +226,25 @@ public void testShutdownDoesNotThrow() { } @Test - public void testPutPlaceholderThenPutUpdatesSnapshot() { - setupMockQueueInfo(); + public void testGetOrCreateThenPutUpdatesSnapshot() { String queueName = "test-placeholder-update-" + System.nanoTime(); // Create placeholder first - QueueMetricsState state1 = cache.putPlaceholder(queueName, 10000L); + QueueMetricsState state1 = cache.getOrCreate(queueName, 10000L); assertNull("Snapshot should be null initially", state1.getSnapshot()); // Now put actual snapshot - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); - cache.put(queueName, snapshot, 5000L); + cache.put(queueName, new QueueMetricsSnapshot(mockQueueInfo), 5000L); // Get updated state QueueMetricsState state2 = cache.get(queueName); assertSame("Should be same state instance", state1, state2); assertNotNull("Snapshot should now be populated", state2.getSnapshot()); - assertEquals("Memory should match", 1.0f, state2.getSnapshot().getMemoryUsedGB(), 0.01f); + assertEquals("Memory should match", 1.0f, state2.getSnapshot().getMemoryUsedGB(), 0.001f); } @Test public void testConcurrentPutAndGetNoDeadlock() throws Exception { - setupMockQueueInfo(); String queueName = "test-concurrent-ops-" + System.nanoTime(); int iterationsPerThread = 100; CountDownLatch startLatch = new CountDownLatch(1); @@ -269,16 +252,16 @@ public void testConcurrentPutAndGetNoDeadlock() throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); AtomicInteger successCount = new AtomicInteger(0); - // Writer thread 1: putPlaceholder + // Writer thread 1: getOrCreate executor.submit(() -> { try { startLatch.await(); for (int i = 0; i < iterationsPerThread; i++) { - cache.putPlaceholder(queueName, 5000L); + cache.getOrCreate(queueName, 5000L); } successCount.incrementAndGet(); } catch (Exception e) { - e.printStackTrace(); + LOG.error("Error in getOrCreate writer thread", e); } finally { doneLatch.countDown(); } @@ -294,7 +277,7 @@ public void testConcurrentPutAndGetNoDeadlock() throws Exception { } successCount.incrementAndGet(); } catch (Exception e) { - e.printStackTrace(); + LOG.error("Error in put writer thread", e); } finally { doneLatch.countDown(); } @@ -309,7 +292,7 @@ public void testConcurrentPutAndGetNoDeadlock() throws Exception { } successCount.incrementAndGet(); } catch (Exception e) { - e.printStackTrace(); + LOG.error("Error in get reader thread", e); } finally { doneLatch.countDown(); } @@ -337,4 +320,3 @@ private void setupMockQueueInfo() { when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.25f); } } - diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java index 00e7b943027c..a31521b21ceb 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsRefreshPool.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; @@ -21,6 +22,7 @@ import org.junit.After; import org.junit.Test; +import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -29,7 +31,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -40,8 +41,8 @@ public class TestQueueMetricsRefreshPool { @After public void tearDown() { - // Reset singleton after each test for isolation - QueueMetricsRefreshPool.resetForTesting(); + // Shutdown singleton after each test for isolation + QueueMetricsRefreshPool.shutdown(); } @Test @@ -50,6 +51,7 @@ public void testInitWithCustomThreadCount() { QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); assertNotNull("Pool should be initialized", pool); + assertEquals("Thread count should match configured value", 8, pool.getThreadCount()); } @Test @@ -64,11 +66,12 @@ public void testInitCalledTwiceIgnoresSecondCall() { } @Test - public void testGetInstanceWithoutInitUsesDefaultThreadCount() { + public void testGetInstanceWithoutInitLazilyInitializes() { // Don't call init(), directly call getInstance() QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); assertNotNull("Pool should be lazily initialized", pool); + assertEquals("Default thread count should be 4", 4, pool.getThreadCount()); } @Test @@ -84,10 +87,9 @@ public void testCalculateJitterIsDeterministic() { String queueName = "test-queue"; long intervalMs = 10000L; - long jitter1 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); - long jitter2 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); - - assertEquals("Same queue name should produce same jitter", jitter1, jitter2); + assertEquals("Same queue name should produce same jitter", + QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs), + QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs)); } @Test @@ -105,26 +107,22 @@ public void testCalculateJitterIsWithinRange() { @Test public void testCalculateJitterDifferentQueuesProduceDifferentValues() { long intervalMs = 10000L; - String queue1 = "queue-alpha"; - String queue2 = "queue-beta"; - - long jitter1 = QueueMetricsRefreshPool.calculateJitter(queue1, intervalMs); - long jitter2 = QueueMetricsRefreshPool.calculateJitter(queue2, intervalMs); // While theoretically they could be equal, hash collisions are rare enough // that this test is reliable in practice - assertNotEquals("Different queues should produce different jitter values", jitter1, jitter2); + assertNotEquals("Different queues should produce different jitter values", + QueueMetricsRefreshPool.calculateJitter("queue-alpha", intervalMs), + QueueMetricsRefreshPool.calculateJitter("queue-beta", intervalMs)); } @Test public void testCalculateJitterWithDifferentIntervals() { String queueName = "test-queue"; - long jitter5s = QueueMetricsRefreshPool.calculateJitter(queueName, 5000L); - long jitter10s = QueueMetricsRefreshPool.calculateJitter(queueName, 10000L); - - assertTrue("Jitter for 5s should be <= 500ms", jitter5s <= 500L); - assertTrue("Jitter for 10s should be <= 1000ms", jitter10s <= 1000L); + assertTrue("Jitter for 5s should be <= 500ms", + QueueMetricsRefreshPool.calculateJitter(queueName, 5000L) <= 500L); + assertTrue("Jitter for 10s should be <= 1000ms", + QueueMetricsRefreshPool.calculateJitter(queueName, 10000L) <= 1000L); } @Test @@ -133,98 +131,16 @@ public void testScheduleRefreshTaskExecutesTask() throws Exception { QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); CountDownLatch latch = new CountDownLatch(2); - AtomicInteger executionCount = new AtomicInteger(0); - - Runnable task = () -> { - executionCount.incrementAndGet(); - latch.countDown(); - }; - ScheduledFuture future = pool.scheduleRefreshTask(task, 50L); + ScheduledFuture future = pool.scheduleRefreshTask(latch::countDown, 50L); assertNotNull("Scheduled future should not be null", future); assertTrue("Task should execute at least twice", latch.await(500, TimeUnit.MILLISECONDS)); - assertTrue("Execution count should be >= 2", executionCount.get() >= 2); - - future.cancel(false); - } - - @Test - public void testScheduleRefreshTaskWithInitialDelayZero() throws Exception { - QueueMetricsRefreshPool.init(1); - QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); - - CountDownLatch firstExecutionLatch = new CountDownLatch(1); - long startTime = System.currentTimeMillis(); - - Runnable task = firstExecutionLatch::countDown; - - ScheduledFuturefuture = pool.scheduleRefreshTask(task, 100L); - - assertTrue("First execution should happen quickly", firstExecutionLatch.await(200, TimeUnit.MILLISECONDS)); - long firstExecutionTime = System.currentTimeMillis() - startTime; - assertTrue("Initial delay should be ~0 (< 200ms)", firstExecutionTime < 200); future.cancel(false); } - @Test - public void testResetForTestingShutdownsAndNullsInstance() { - QueueMetricsRefreshPool.init(4); - QueueMetricsRefreshPool pool1 = QueueMetricsRefreshPool.getInstance(); - assertNotNull("Pool should exist", pool1); - - QueueMetricsRefreshPool.resetForTesting(); - - // After reset, getInstance should create a new instance - QueueMetricsRefreshPool pool2 = QueueMetricsRefreshPool.getInstance(); - assertNotNull("New pool should be created", pool2); - assertNotSame("Should be different instance after reset", pool1, pool2); - } - - @Test - public void testResetForTestingIsIdempotent() { - QueueMetricsRefreshPool.resetForTesting(); - QueueMetricsRefreshPool.resetForTesting(); // Second call should not throw - QueueMetricsRefreshPool.resetForTesting(); // Third call should not throw - - // Should still be able to get instance - QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); - assertNotNull("Pool should be available after multiple resets", pool); - } - - @Test - public void testScheduleMultipleTasksConcurrently() throws Exception { - QueueMetricsRefreshPool.init(4); - QueueMetricsRefreshPool pool = QueueMetricsRefreshPool.getInstance(); - - int taskCount = 5; - CountDownLatch latch = new CountDownLatch(taskCount * 2); // Each task should run at least twice - AtomicInteger[] counters = new AtomicInteger[taskCount]; - ScheduledFuture[] futures = new ScheduledFuture[taskCount]; - - for (int i = 0; i < taskCount; i++) { - counters[i] = new AtomicInteger(0); - final int taskId = i; - futures[i] = pool.scheduleRefreshTask(() -> { - counters[taskId].incrementAndGet(); - latch.countDown(); - }, 50L); - } - - assertTrue("All tasks should execute multiple times", latch.await(1, TimeUnit.SECONDS)); - - // Cancel all tasks - for (ScheduledFuture future : futures) { - future.cancel(false); - } - - // Verify all tasks executed at least once - for (int i = 0; i < taskCount; i++) { - assertTrue("Task " + i + " should have executed", counters[i].get() >= 2); - } - } @Test public void testJitterPreventsSynchronization() { @@ -238,37 +154,23 @@ public void testJitterPreventsSynchronization() { } // Check that not all jitters are the same (spreading effect) - boolean hasDifferentJitter = false; - for (int i = 1; i < jitters.length; i++) { - if (jitters[i] != jitters[0]) { - hasDifferentJitter = true; - break; - } - } - - assertTrue("Jitter should vary across different queue names", hasDifferentJitter); + assertTrue("Jitter should vary across different queue names", + Arrays.stream(jitters).distinct().count() > 1); } @Test public void testCalculateJitterWithZeroInterval() { // Test edge case where jitterWindow becomes 0 (very small interval) - String queueName = "test-queue"; - long smallInterval = 5L; // 5ms interval -> jitterWindow = 0 (5 * 10 / 100 = 0) - - long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, smallInterval); - - assertEquals("Jitter should be 0 when jitterWindow is 0", 0, jitter); + // 5ms interval -> jitterWindow = 0 (5 * 10 / 100 = 0) + assertEquals("Jitter should be 0 when jitterWindow is 0", + 0, QueueMetricsRefreshPool.calculateJitter("test-queue", 5L)); } @Test public void testCalculateJitterWithNegativeInterval() { // Test edge case with negative interval - String queueName = "test-queue"; - long negativeInterval = -1000L; - - long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, negativeInterval); - - assertEquals("Jitter should be 0 when intervalMs is negative", 0, jitter); + assertEquals("Jitter should be 0 when intervalMs is negative", + 0, QueueMetricsRefreshPool.calculateJitter("test-queue", -1000L)); } @Test @@ -286,7 +188,8 @@ public void testCalculateJitterWithHashCodeIntMinValue() { // Verify jitter is non-negative and within bounds assertTrue("Jitter should be >= 0 even for Integer.MIN_VALUE hashCode", jitter >= 0); - assertTrue("Jitter should be <= 10% of interval", jitter < intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100); + assertTrue("Jitter should be <= 10% of interval", + jitter < intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100); } @Test @@ -294,7 +197,6 @@ public void testCalculateJitterAlwaysNonNegative() { // Test with various queue names to ensure jitter is always non-negative String[] testQueues = { "queue-1", "queue-2", "production", "default", - "polygenelubricants", // Integer.MIN_VALUE "test-queue-alpha", "test-queue-beta" }; long intervalMs = 10000L; @@ -303,8 +205,9 @@ public void testCalculateJitterAlwaysNonNegative() { long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); assertTrue("Jitter for queue '" + queueName + "' should be >= 0, but was: " + jitter, jitter >= 0); - assertTrue("Jitter for queue '" + queueName + "' should be < " + (intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100), - jitter < intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100); + long maxJitter = intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100; + assertTrue("Jitter for queue '" + queueName + "' should be < " + maxJitter, + jitter < maxJitter); } } @@ -356,9 +259,8 @@ public void testShutdownStopsScheduledTasks() throws Exception { // so when it returns no further executions can happen. QueueMetricsRefreshPool.shutdown(); - int countAfterShutdown = counter.get(); - assertTrue("No new task executions should occur after shutdown", - countAfterShutdown <= countAtShutdown + 1); + assertEquals("No new task executions should occur after shutdown", + countAtShutdown, counter.get()); } } diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java index 2bc5c3a5d08e..6f8a5b579f41 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestQueueMetricsState.java @@ -9,11 +9,12 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; @@ -21,33 +22,46 @@ import org.apache.hadoop.yarn.api.records.QueueInfo; import org.apache.hadoop.yarn.api.records.QueueStatistics; import org.junit.Before; +import java.util.concurrent.ScheduledFuture; +import java.time.Duration; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; +import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Unit tests for QueueMetricsState - tests state management logic in isolation. * Tests interval registration, circuit breaker, refresh locking, and other state logic. */ +@RunWith(MockitoJUnitRunner.class) public class TestQueueMetricsState { @Mock private QueueInfo mockQueueInfo; - @Mock private QueueStatistics mockQueueStats; - + @Mock + private QueueMetricsRefreshPool mockPool; + @Mock + private ScheduledFuture mockTask; @Before public void setUp() { - MockitoAnnotations.openMocks(this); + lenient().when(mockPool.scheduleRefreshTask(any(), anyLong())).thenAnswer(inv -> mockTask); setupMockQueueInfo(); } @@ -70,26 +84,25 @@ public void testConstructorWithNullSnapshot() { QueueMetricsState state = new QueueMetricsState(null, 5000L); assertNull("Snapshot should be null when constructed with null", state.getSnapshot()); - assertEquals("Min interval should be set", 5000L, state.getMinRefreshIntervalMs()); + // minRefreshIntervalMs is seeded from the constructor value; updated by ensureTaskScheduled + assertEquals("Min interval should be set to constructor value", 5000L, state.getMinRefreshIntervalMs()); } @Test public void testConstructorWithSnapshot() { - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); - QueueMetricsState state = new QueueMetricsState(snapshot, 10000L); + QueueMetricsState state = new QueueMetricsState(new QueueMetricsSnapshot(mockQueueInfo), 10000L); assertNotNull("Snapshot should not be null", state.getSnapshot()); - assertEquals("Min interval should be set", 10000L, state.getMinRefreshIntervalMs()); + // minRefreshIntervalMs is seeded from the constructor value; updated by ensureTaskScheduled + assertEquals("Min interval should be set to constructor value", 10000L, state.getMinRefreshIntervalMs()); } @Test public void testGetAgeMsReturnsLargeValueInitially() { QueueMetricsState state = new QueueMetricsState(null, 5000L); - long age = state.getAgeMs(); - // Age should be very large when lastWriteTime = 0 (epoch) - assertTrue("Age should be > 1 year in ms", age > 365L * 24 * 60 * 60 * 1000); + assertTrue("Age should be > 1 year in ms", state.getAgeMs() > 365L * 24 * 60 * 60 * 1000); } @Test @@ -97,11 +110,10 @@ public void testApplySnapshotUpdatesSnapshot() { QueueMetricsState state = new QueueMetricsState(null, 10000L); assertNull("Initial snapshot should be null", state.getSnapshot()); - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); - state.applySnapshot(snapshot, 5000L); + state.applySnapshot(new QueueMetricsSnapshot(mockQueueInfo)); assertNotNull("Snapshot should be updated", state.getSnapshot()); - assertEquals("Memory should match", 1.0f, state.getSnapshot().getMemoryUsedGB(), 0.01f); + assertEquals("Memory should match", 1.0f, state.getSnapshot().getMemoryUsedGB(), 0.001f); } @Test @@ -109,80 +121,81 @@ public void testApplySnapshotReducesAgeMs() { QueueMetricsState state = new QueueMetricsState(null, 5000L); long initialAge = state.getAgeMs(); - // Spin-wait up to 200ms to ensure time has passed so the age comparison is meaningful - long deadline = System.currentTimeMillis() + 200; - while (state.getAgeMs() <= initialAge && System.currentTimeMillis() < deadline) { - Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop + // Wait up to 200ms for the clock to advance so the age comparison is meaningful. + // Tolerate timeout: on a heavily-loaded runner the age may already have advanced + // beyond initialAge on the first read, or may take slightly longer — either way + // the assertions below verify the actual invariant. + try { + await().atMost(Duration.ofMillis(200)) + .pollInterval(Duration.ofMillis(10)) + .until(() -> state.getAgeMs() > initialAge); + } catch (org.awaitility.core.ConditionTimeoutException ignored) { + // Intentional: assertions below cover the invariant. } - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); - state.applySnapshot(snapshot, 5000L); + state.applySnapshot(new QueueMetricsSnapshot(mockQueueInfo)); - long newAge = state.getAgeMs(); - assertTrue("Age should be much smaller after apply", newAge < initialAge); - assertTrue("Age should be recent (< 1s)", newAge < 1000); + assertTrue("Age should be much smaller after apply", state.getAgeMs() < initialAge); + assertTrue("Age should be recent (< 1s)", state.getAgeMs() < 1000); } @Test - public void testApplySnapshotUpdatesMinRefreshInterval() { + public void testRegisterIntervalReturnsTrueWhenNoTaskExists() { QueueMetricsState state = new QueueMetricsState(null, 10000L); - assertEquals("Initial min interval", 10000L, state.getMinRefreshIntervalMs()); - // Apply snapshot with smaller interval - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); - state.applySnapshot(snapshot, 3000L); - - assertEquals("Min interval should be reduced", 3000L, state.getMinRefreshIntervalMs()); + assertTrue("Should return true when no task exists", state.registerInterval(5000L)); } @Test - public void testApplySnapshotDoesNotIncreaseMinInterval() { - QueueMetricsState state = new QueueMetricsState(null, 5000L); + public void testRegisterIntervalReturnsTrueWhenLoweringMinimum() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); - // Apply snapshot with larger interval - QueueMetricsSnapshot snapshot = new QueueMetricsSnapshot(mockQueueInfo); - state.applySnapshot(snapshot, 10000L); + // First registration — task is null so returns true + state.registerInterval(10000L); - // Min interval should stay at smaller value - assertEquals("Min interval should not increase", 5000L, state.getMinRefreshIntervalMs()); + // Second registration with strictly faster interval — lowered the minimum → reschedule needed + assertTrue("Should return true when lowering minimum", state.registerInterval(5000L)); } @Test - public void testRegisterIntervalReturnsTrueWhenNoTaskExists() { - QueueMetricsState state = new QueueMetricsState(null, 10000L); + public void testRegisterIntervalReturnsTrueWhenTaskIsNull() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); - boolean shouldSchedule = state.registerInterval(5000L); + // Register faster interval first (task is null, should return true) + assertTrue("Should return true when task is null", state.registerInterval(5000L)); - assertTrue("Should return true when no task exists", shouldSchedule); + // Register slower interval (task still null — no task was ever scheduled — should return true) + assertTrue("Should return true when task is null even with slower interval", state.registerInterval(10000L)); } @Test - public void testRegisterIntervalReturnsTrueWhenLoweringMinimum() { - QueueMetricsState state = new QueueMetricsState(null, 10000L); - - // First registration - creates task - state.registerInterval(10000L); + public void testRegisterIntervalReturnsFalseWhenSlowerThanExistingMinimum() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); - // Second registration with faster interval - boolean shouldSchedule = state.registerInterval(5000L); + // Register faster session and schedule the task — refreshTask is now non-null + state.registerInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); - assertTrue("Should return true when lowering minimum", shouldSchedule); - assertEquals("Min interval should be updated", 5000L, state.getMinRefreshIntervalMs()); + // Registering a slower interval must NOT trigger rescheduling — minimum is unchanged (5000ms) + assertFalse("Should return false when new interval is slower than existing minimum", + state.registerInterval(10000L)); } @Test - public void testRegisterIntervalReturnsTrueWhenTaskIsNull() { + public void testRegisterIntervalAddsOneEntryPerSession() { QueueMetricsState state = new QueueMetricsState(null, 5000L); - // Register faster interval first (task is null, should return true) - boolean shouldSchedule1 = state.registerInterval(5000L); - assertTrue("Should return true when task is null", shouldSchedule1); - - // Register slower interval (task still null, should still return true) - boolean shouldSchedule2 = state.registerInterval(10000L); - assertTrue("Should return true when task is null even with slower interval", shouldSchedule2); + // Two sessions at the same interval — duplicates must be kept (one entry per session) + state.registerInterval(5000L); + state.registerInterval(5000L); - assertEquals("Min interval should stay at faster value", 5000L, state.getMinRefreshIntervalMs()); + // Deregistering one should still leave the other + state.deregisterInterval(5000L); + // If duplicates were not kept, deregister would have removed the only entry. + // Verify by registering a slower session and checking deregister still returns true + // (5000 <= taskCurrentRefreshIntervalMs initial value) — confirms 5000ms entry still present. + assertTrue("Second session at same interval should still be registered after first deregisters", + state.deregisterInterval(5000L)); } @Test @@ -193,42 +206,200 @@ public void testDeregisterIntervalReturnsFalseWhenOtherSessionsRemain() { state.registerInterval(5000L); state.registerInterval(5000L); - // Deregister one session - boolean shouldReschedule = state.deregisterInterval(5000L); + // Deregister one session — 5000 <= taskCurrentRefreshIntervalMs(5000), returns true + // (the task may need to be re-evaluated, even if it stays at 5000ms) + assertTrue("Should return true since 5000 <= taskCurrentRefreshIntervalMs", + state.deregisterInterval(5000L)); + } + + @Test + public void testDeregisterIntervalSignalsRescheduleWhenAtOrBelowTaskInterval() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); + + // 5000 <= taskCurrentRefreshIntervalMs (5000) → should signal rescheduling + assertTrue("Should return true when removed interval is at or below task interval", + state.deregisterInterval(5000L)); + } - assertFalse("Should return false when other sessions remain at this interval", shouldReschedule); + @Test + public void testDeregisterIntervalNoRescheduleWhenSlowerThanTask() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); + state.registerInterval(10000L); + + // Deregister the slow session (10000 > taskCurrentRefreshIntervalMs 5000) → no reschedule + assertFalse("Should return false when removed interval is slower than task interval", + state.deregisterInterval(10000L)); } @Test - public void testDeregisterIntervalRemovesBucket() { + public void testDeregisterIntervalRemovesInterval() { QueueMetricsState state = new QueueMetricsState(null, 10000L); // Register fast and slow sessions state.registerInterval(2000L); // Fast state.registerInterval(10000L); // Slow - // After registration, min should be 2000 - assertEquals("Min interval should be 2000ms", 2000L, state.getMinRefreshIntervalMs()); + // Deregister fast session — 2000 <= taskCurrentRefreshIntervalMs(10000) → returns true + assertTrue("Should return true when the removed interval was at or below task interval", + state.deregisterInterval(2000L)); + + // Deregister slow session — 10000 <= taskCurrentRefreshIntervalMs(10000) → returns true + assertTrue("Should return true when last session deregisters", + state.deregisterInterval(10000L)); + } - // Deregister fast session - removes the 2000ms bucket - state.deregisterInterval(2000L); + @Test + public void testDeregisterFasterIntervalWhenItIsTheLastSession() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); - // The minRefreshIntervalMs field uses Math.min logic (line 217) so it won't increase - // back to 10000. This is by design - the field tracks historical minimum, not current. - // The actual task rescheduling logic in ensureTaskScheduled() recomputes from intervalCounts. - assertEquals("Min interval field remains at historical min (by design)", - 2000L, state.getMinRefreshIntervalMs()); + // Only one session at 5000ms — deregistering it is the last session. + // 5000 <= taskCurrentRefreshIntervalMs(5000) → should signal rescheduling (task must be cancelled) + assertTrue("Should return true when the only session deregisters", + state.deregisterInterval(5000L)); } @Test - public void testTryStartRefreshPreventsRace() { + public void testDeregisterFasterIntervalWhenSlowerSessionStillRemains() { QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); // faster + state.registerInterval(10000L); // slower + + // Deregister the faster session — 5000 <= taskCurrentRefreshIntervalMs(5000) → true. + // The slower session (10000ms) is still in the heap; the task should be rescheduled slower. + assertTrue("Should return true when faster interval deregisters and slower session remains", + state.deregisterInterval(5000L)); + } + + @Test + public void testDeregisterIntervalNoRescheduleWhenSlowerThanScheduledTask() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + + // Register a fast session and schedule the task at 5000ms + state.registerInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + // taskCurrentRefreshIntervalMs is now 5000 (set by ensureTaskScheduled/scheduleTask) - boolean first = state.tryStartRefresh(); - boolean second = state.tryStartRefresh(); + // Add a slow session — does not affect the task interval + state.registerInterval(10000L); - assertTrue("First call should succeed", first); - assertFalse("Second call should fail (already refreshing)", second); + // Deregister the slow session — 10000 > taskCurrentRefreshIntervalMs(5000) → no reschedule + assertFalse("Should return false when removed interval is slower than the scheduled task interval", + state.deregisterInterval(10000L)); + } + + // ------------------------------------------------------------------------- + // ensureTaskScheduled tests + // ------------------------------------------------------------------------- + + @Test + public void testEnsureTaskScheduledStartsTaskWhenNoneExists() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + state.registerInterval(5000L); + + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + verify(mockPool, times(1)).scheduleRefreshTask(any(), anyLong()); + assertEquals("minRefreshIntervalMs should reflect the registered interval", + 5000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testEnsureTaskScheduledIsIdempotentWhenIntervalUnchanged() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); + + // First call schedules the task + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + // Second call with the same heap minimum — should NOT reschedule + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + verify(mockPool, times(1)).scheduleRefreshTask(any(), anyLong()); + } + + @Test + public void testEnsureTaskScheduledReschedulesWhenFasterSessionAdded() { + QueueMetricsState state = new QueueMetricsState(null, 10000L); + + // Schedule at 10000ms + state.registerInterval(10000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + // Add a faster session and reschedule + state.registerInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + // scheduleRefreshTask should have been called twice — initial + reschedule + verify(mockPool, times(2)).scheduleRefreshTask(any(), anyLong()); + assertEquals("minRefreshIntervalMs should reflect the new faster interval", + 5000L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testEnsureTaskScheduledCancelsTaskWhenNoSessionsRemain() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + // Remove the only session + state.deregisterInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + // Task should be cancelled + verify(mockTask, times(1)).cancel(false); + // No additional scheduleRefreshTask call after cancel + verify(mockPool, times(1)).scheduleRefreshTask(any(), anyLong()); + } + + @Test + public void testEnsureTaskScheduledUpdatesMinRefreshIntervalMsToZeroWhenEmpty() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + state.deregisterInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + assertEquals("minRefreshIntervalMs should be 0 when no sessions remain (0 = no active sessions sentinel)", + 0L, state.getMinRefreshIntervalMs()); + } + + @Test + public void testFreshnessCheckNeverSkipsWhenNoSessionsRemain() { + // When minRefreshIntervalMs = 0 (no sessions), the freshness check + // getAgeMs() < getMinRefreshIntervalMs() must always be false — i.e. the + // refresh is never incorrectly skipped due to the sentinel value. + // This is the key correctness property of using 0 instead of Long.MAX_VALUE. + QueueMetricsState state = new QueueMetricsState(null, 5000L); + state.registerInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + state.deregisterInterval(5000L); + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + // getAgeMs() >= 0 always; 0 < 0 is false — freshness check never skips + assertFalse("Freshness check should not skip when no sessions remain (age >= 0, min = 0)", + state.getAgeMs() < state.getMinRefreshIntervalMs()); + } + + @Test + public void testEnsureTaskScheduledDoesNothingWhenAlreadyEmptyAndNoTask() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + // No sessions registered — sessionIntervals is empty and refreshTask is null + + state.ensureTaskScheduled(mockPool, () -> {}, "test-queue"); + + verify(mockPool, never()).scheduleRefreshTask(any(), anyLong()); + } + + @Test + public void testTryStartRefreshPreventsRace() { + QueueMetricsState state = new QueueMetricsState(null, 5000L); + + assertTrue("First call should succeed", state.tryStartRefresh()); + assertFalse("Second call should return false when refresh is already in progress", state.tryStartRefresh()); } @Test @@ -238,8 +409,7 @@ public void testFinishRefreshReleasesLock() { state.tryStartRefresh(); state.finishRefresh(); - boolean canRefresh = state.tryStartRefresh(); - assertTrue("Should be able to refresh after finish", canRefresh); + assertTrue("Should be able to refresh after finish", state.tryStartRefresh()); } @Test @@ -301,9 +471,10 @@ public void testCircuitBreakerAllowsProbeEvery10Ticks() { } } - // Should allow approximately 2 probes in 20 ticks (ticks 10 and 20) - assertTrue("Should have some blocked ticks", blockedCount > 10); - assertTrue("Should have some allowed probes", allowedCount >= 1 && allowedCount <= 3); + // In 20 ticks, probes are allowed at ticks 10 and 20 (skipCount % 10 == 0) → exactly 2 probes, + // and exactly 18 blocked ticks — these are deterministic, not time-dependent. + assertEquals("Should have exactly 18 blocked ticks", 18, blockedCount); + assertEquals("Should have exactly 2 probe ticks (ticks 10 and 20)", 2, allowedCount); } @Test @@ -315,8 +486,3 @@ public void testShouldSkipDueToCircuitBreakerReturnsFalseWhenHealthy() { state.shouldSkipDueToCircuitBreaker("test-queue")); } } - - - - - diff --git a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java index 8f59f5ec114d..375abc3d67ce 100644 --- a/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java +++ b/ql/src/test/org/apache/hadoop/hive/ql/exec/tez/monitoring/yarnqueue/TestYarnQueueMetricsCollector.java @@ -9,157 +9,153 @@ * * 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. + * 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.hadoop.hive.ql.exec.tez.monitoring.yarnqueue; -import org.apache.hadoop.hive.conf.HiveConf; import org.apache.hadoop.yarn.api.records.QueueInfo; import org.apache.hadoop.yarn.api.records.QueueStatistics; import org.apache.hadoop.yarn.client.api.YarnClient; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; +import org.awaitility.core.ConditionTimeoutException; + +import java.time.Duration; + +import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mockingDetails; import static org.mockito.Mockito.when; /** * Test cases for YarnQueueMetricsCollector. */ +@RunWith(MockitoJUnitRunner.class) public class TestYarnQueueMetricsCollector { @Mock private YarnClient mockYarnClient; - @Mock private QueueInfo mockQueueInfo; - @Mock private QueueStatistics mockQueueStats; - private AutoCloseable closeable; - private HiveConf testConf; - private static final long WAIT_TIMEOUT_MS = 5000; @Before - public void setUp() { - closeable = MockitoAnnotations.openMocks(this); - testConf = new HiveConf(); - // Reset the pool manager singleton and cache so each test starts with a clean state. - QueueMetricsRefreshPool.resetForTesting(); - QueueMetricsCache.resetForTesting(); + public void setUp() throws Exception { + // Shutdown the pool manager singleton and cache so each test starts with a clean state. + QueueMetricsRefreshPool.shutdown(); + QueueMetricsCache.getInstance().shutdown(); + setupHappyPathMocks(); } @After - public void tearDown() throws Exception { - if (closeable != null) { - closeable.close(); - } - QueueMetricsRefreshPool.resetForTesting(); - QueueMetricsCache.resetForTesting(); + public void tearDown() { + QueueMetricsRefreshPool.shutdown(); + QueueMetricsCache.getInstance().shutdown(); } /** - * Helper to create a collector in tests using a default HiveConf (min pool sizes). + * Helper to create a collector in tests. */ private YarnQueueMetricsCollector newCollector(YarnClient yarnClient, String queueName, long refreshIntervalMs, String queryId) { - return new YarnQueueMetricsCollector(yarnClient, queueName, refreshIntervalMs, queryId, testConf); + return new YarnQueueMetricsCollector(yarnClient, queueName, refreshIntervalMs, queryId); } /** - * Waits for a snapshot to be available (non-null). + * Waits for a snapshot to be available (non-null) using Awaitility. */ private QueueMetricsSnapshot waitForSnapshot( YarnQueueMetricsCollector collector, long timeoutMs) { - long startTime = System.currentTimeMillis(); - QueueMetricsSnapshot snapshot; - while ((snapshot = collector.getLatestSnapshot()) == null) { - if (System.currentTimeMillis() - startTime > timeoutMs) { - fail("Snapshot not available after " + timeoutMs + "ms"); - } - Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop - } - return snapshot; + await().atMost(Duration.ofMillis(timeoutMs)) + .pollInterval(Duration.ofMillis(10)) + .until(() -> collector.getLatestSnapshot() != null); + return collector.getLatestSnapshot(); } /** - * Waits for a specific number of invocations with timeout. + * Waits until the mock has been invoked at least {@code minCount} times, or the timeout + * elapses. Returns silently on timeout so callers can assert on the observed count. */ private void waitForInvocationCount(Object mock, int minCount, long timeoutMs) { - long startTime = System.currentTimeMillis(); - while (mockingDetails(mock).getInvocations().size() < minCount) { - if (System.currentTimeMillis() - startTime > timeoutMs) { - return; - } - Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop + try { + await().atMost(Duration.ofMillis(timeoutMs)) + .pollInterval(Duration.ofMillis(10)) + .until(() -> mockingDetails(mock).getInvocations().size() >= minCount); + } catch (ConditionTimeoutException ignored) { + // Intentional: callers assert on the observed count after this returns. } } /** - * Helper method that configures mock objects with standard happy-path values. + * Configures mock objects with standard happy-path values. + * Called from {@code @Before} so all tests start with a consistent baseline. + * Stubs are lenient so tests that don't exercise these mocks don't fail with + * UnnecessaryStubbingException. */ private void setupHappyPathMocks() throws Exception { - when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(1024L); - when(mockQueueStats.getAvailableMemoryMB()).thenReturn(1024L); - when(mockQueueStats.getAllocatedVCores()).thenReturn(4L); - when(mockQueueStats.getAvailableVCores()).thenReturn(4L); - when(mockQueueStats.getNumAppsRunning()).thenReturn(1L); - when(mockQueueStats.getNumAppsPending()).thenReturn(0L); - when(mockQueueStats.getAllocatedContainers()).thenReturn(2L); - when(mockQueueStats.getPendingContainers()).thenReturn(0L); - when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); - when(mockQueueInfo.getCapacity()).thenReturn(0.5f); - when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.25f); - when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); + lenient().when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(1024L); + lenient().when(mockQueueStats.getAvailableMemoryMB()).thenReturn(1024L); + lenient().when(mockQueueStats.getAllocatedVCores()).thenReturn(4L); + lenient().when(mockQueueStats.getAvailableVCores()).thenReturn(4L); + lenient().when(mockQueueStats.getNumAppsRunning()).thenReturn(1L); + lenient().when(mockQueueStats.getNumAppsPending()).thenReturn(0L); + lenient().when(mockQueueStats.getAllocatedContainers()).thenReturn(2L); + lenient().when(mockQueueStats.getPendingContainers()).thenReturn(0L); + lenient().when(mockQueueInfo.getQueueStatistics()).thenReturn(mockQueueStats); + lenient().when(mockQueueInfo.getCapacity()).thenReturn(0.5f); + lenient().when(mockQueueInfo.getCurrentCapacity()).thenReturn(0.25f); + lenient().when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NullPointerException.class) public void testConstructorWithNullYarnClient() { - new YarnQueueMetricsCollector(null, "default", 1000, "query-1", testConf); + new YarnQueueMetricsCollector(null, "default", 1000, "query-1"); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NullPointerException.class) public void testConstructorWithNullQueueName() { - new YarnQueueMetricsCollector(mockYarnClient, null, 1000, "query-1", testConf); + new YarnQueueMetricsCollector(mockYarnClient, null, 1000, "query-1"); } @Test - public void testSuccessfulMetricsCollection() throws Exception { - setupHappyPathMocks(); - when(mockYarnClient.getQueueInfo("default")).thenReturn(mockQueueInfo); + public void testSuccessfulMetricsCollection() { YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "test-query-1"); try { QueueMetricsSnapshot snapshot = waitForSnapshot(collector, WAIT_TIMEOUT_MS); assertNotNull("Snapshot should not be null", snapshot); - assertEquals("Memory used should be 1GB", 1.0f, snapshot.getMemoryUsedGB(), 0.1f); - assertEquals("Memory total should be 2GB (1+1)", 2.0f, snapshot.getMemoryTotalGB(), 0.1f); + assertEquals("Memory used should be 1GB", 1.0f, snapshot.getMemoryUsedGB(), 0.001f); + assertEquals("Memory total should be 2GB (1+1)", 2.0f, snapshot.getMemoryTotalGB(), 0.001f); assertEquals("VCores used should be 4", 4, snapshot.getVCoresUsed()); assertEquals("VCores total should be 8 (4+4)", 8, snapshot.getVCoresTotal()); assertEquals("Running apps should be 1", 1, snapshot.getRunningApps()); assertEquals("Pending apps should be 0", 0, snapshot.getPendingApps()); assertEquals("Allocated containers should be 2", 2, snapshot.getAllocatedContainers()); assertEquals("Pending containers should be 0", 0, snapshot.getPendingContainers()); - assertEquals("Capacity should be 50%", 50.0f, snapshot.getCapacityPercentage(), 0.1f); - assertEquals("Current capacity should be 25%", 25.0f, snapshot.getCurrentCapacityPercentage(), 0.1f); + assertEquals("Capacity should be 50%", 50.0f, snapshot.getCapacityPercentage(), 0.001f); + assertEquals("Current capacity should be 25%", 25.0f, snapshot.getCurrentCapacityPercentage(), 0.001f); assertEquals("Memory percentage", "50.00%", snapshot.getMemoryPercentage()); assertEquals("VCores percentage", "50.00%", snapshot.getVCoresPercentage()); } finally { @@ -190,12 +186,12 @@ public void testMetricsCollectionWithNullQueueStatistics() throws Exception { try { QueueMetricsSnapshot snapshot = waitForSnapshot(collector, WAIT_TIMEOUT_MS); assertNotNull("Snapshot should not be null", snapshot); - assertEquals("Memory used should be 0", 0.0f, snapshot.getMemoryUsedGB(), 0.01f); - assertEquals("Memory total should be 0", 0.0f, snapshot.getMemoryTotalGB(), 0.01f); + assertEquals("Memory used should be 0", 0.0f, snapshot.getMemoryUsedGB(), 0.001f); + assertEquals("Memory total should be 0", 0.0f, snapshot.getMemoryTotalGB(), 0.001f); assertEquals("VCores used should be 0", 0, snapshot.getVCoresUsed()); assertEquals("VCores total should be 0", 0, snapshot.getVCoresTotal()); - assertEquals("Capacity should still be 50%", 50.0f, snapshot.getCapacityPercentage(), 0.1f); - assertEquals("Current capacity should be 0%", 0.0f, snapshot.getCurrentCapacityPercentage(), 0.1f); + assertEquals("Capacity should still be 50%", 50.0f, snapshot.getCapacityPercentage(), 0.001f); + assertEquals("Current capacity should be 0%", 0.0f, snapshot.getCurrentCapacityPercentage(), 0.001f); } finally { collector.shutdown(); } @@ -280,8 +276,8 @@ public void testMemoryAndVCoreCalculation() { new QueueMetricsSnapshot(mockQueueInfo); // Total = Used + Available - assertEquals("Memory used", 5.0f, snapshot.getMemoryUsedGB(), 0.01f); - assertEquals("Memory total", 20.0f, snapshot.getMemoryTotalGB(), 0.01f); // 5+15 + assertEquals("Memory used", 5.0f, snapshot.getMemoryUsedGB(), 0.001f); + assertEquals("Memory total", 20.0f, snapshot.getMemoryTotalGB(), 0.001f); // 5+15 assertEquals("Memory percentage", "25.00%", snapshot.getMemoryPercentage()); // 5/20 assertEquals("VCores used", 50, snapshot.getVCoresUsed()); @@ -292,8 +288,8 @@ public void testMemoryAndVCoreCalculation() { assertEquals("Pending apps", 2, snapshot.getPendingApps()); assertEquals("Allocated containers", 10, snapshot.getAllocatedContainers()); assertEquals("Pending containers", 7, snapshot.getPendingContainers()); - assertEquals("Capacity", 20.0f, snapshot.getCapacityPercentage(), 0.01f); - assertEquals("Current capacity", 5.0f, snapshot.getCurrentCapacityPercentage(), 0.01f); + assertEquals("Capacity", 20.0f, snapshot.getCapacityPercentage(), 0.001f); + assertEquals("Current capacity", 5.0f, snapshot.getCurrentCapacityPercentage(), 0.001f); } @Test(expected = IllegalArgumentException.class) @@ -301,12 +297,6 @@ public void testQueueMetricsSnapshotWithNullQueueInfo() { new QueueMetricsSnapshot(null); } - // ------------------------------------------------------------------------- - // Tests for Issue #1: Jitter on initial delay (Thundering Herd prevention) - // ------------------------------------------------------------------------- - // Note: Jitter is implicitly tested by all tests that successfully create collectors. - // Explicit jitter distribution testing would require reflection to access private - // scheduling details, which is fragile and not worth the maintenance cost. @Test public void testExecutorCleanupOnInitializationFailure() throws Exception { @@ -328,10 +318,10 @@ public void testCircuitBreakerActivatesAfterMaxFailures() throws Exception { YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 50, "circuit-breaker-query-1"); try { - waitForInvocationCount(mockYarnClient, 6, 1000); + waitForInvocationCount(mockYarnClient, 6, WAIT_TIMEOUT_MS); assertNull("Snapshot should be null when circuit breaker active", collector.getLatestSnapshot()); - int callCount = mockingDetails(mockYarnClient).getInvocations().size(); - assertTrue("Circuit breaker should reduce calls (got " + callCount + ")", callCount < 12); + assertTrue("Circuit breaker should reduce calls", + mockingDetails(mockYarnClient).getInvocations().size() < 12); } finally { collector.shutdown(); } @@ -360,11 +350,12 @@ public void testCircuitBreakerResetsOnSuccess() throws Exception { YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 30, "circuit-breaker-recovery-query"); try { - waitForInvocationCount(mockYarnClient, 3, 200); + waitForInvocationCount(mockYarnClient, 3, WAIT_TIMEOUT_MS); assertNull("Snapshot should be null after circuit breaker activates", collector.getLatestSnapshot()); - QueueMetricsSnapshot snapshot = waitForSnapshot(collector, 2000); + QueueMetricsSnapshot snapshot = waitForSnapshot(collector, WAIT_TIMEOUT_MS); assertNotNull("Snapshot should be populated after circuit breaker recovery", snapshot); - assertEquals("Memory used should be 4GB", 4.0f, snapshot.getMemoryUsedGB(), 0.1f); + assertEquals("Memory used should be 4GB", 4.0f, snapshot.getMemoryUsedGB(), 0.001f); + } finally { collector.shutdown(); } @@ -377,7 +368,7 @@ public void testNullQueueInfoDoesNotTriggerCircuitBreaker() throws Exception { YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "nonexistent-queue", 50, "null-queueinfo-query"); try { - waitForInvocationCount(mockYarnClient, 8, 800); + waitForInvocationCount(mockYarnClient, 8, WAIT_TIMEOUT_MS); assertNull("Snapshot should remain null for null QueueInfo", collector.getLatestSnapshot()); int callCount = mockingDetails(mockYarnClient).getInvocations().size(); assertTrue("Null QueueInfo should NOT trigger circuit breaker (got " + callCount + " calls)", @@ -388,8 +379,7 @@ public void testNullQueueInfoDoesNotTriggerCircuitBreaker() throws Exception { } @Test - public void testSnapshotCollectionTimestampIsRecent() throws Exception { - setupHappyPathMocks(); + public void testSnapshotCollectionTimestampIsRecent() { long beforeCreate = System.currentTimeMillis(); YarnQueueMetricsCollector collector = newCollector(mockYarnClient, "default", 10000, "timestamp-test"); try { @@ -405,8 +395,7 @@ public void testSnapshotCollectionTimestampIsRecent() throws Exception { } @Test - public void testRefreshIntervalRespected() throws Exception { - setupHappyPathMocks(); + public void testRefreshIntervalRespected() { when(mockQueueStats.getAllocatedMemoryMB()).thenReturn(2048L); when(mockQueueStats.getAvailableMemoryMB()).thenReturn(2048L); when(mockQueueStats.getAllocatedVCores()).thenReturn(8L); @@ -419,67 +408,29 @@ public void testRefreshIntervalRespected() throws Exception { try { waitForSnapshot(collector, WAIT_TIMEOUT_MS); int callsAfterFirst = mockingDetails(mockYarnClient).getInvocations().size(); - long toleranceMs = intervalMs + (long) (intervalMs * 0.2) + 300; + long toleranceMs = intervalMs + (long) (intervalMs * 0.2) + 2000; waitForInvocationCount(mockYarnClient, callsAfterFirst + 1, toleranceMs); - int callsAfterWait = mockingDetails(mockYarnClient).getInvocations().size(); assertTrue("At least one refresh should have occurred within interval + tolerance", - callsAfterWait > callsAfterFirst); + mockingDetails(mockYarnClient).getInvocations().size() > callsAfterFirst); } finally { collector.shutdown(); } } @Test - public void testZeroRefreshIntervalIsRejected() throws Exception { - when(mockQueueInfo.getQueueStatistics()).thenReturn(null); - when(mockQueueInfo.getCapacity()).thenReturn(0.5f); - when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); - + public void testZeroRefreshIntervalIsRejected() { assertThrows(IllegalArgumentException.class, () -> - new YarnQueueMetricsCollector(mockYarnClient, "default", 0, "zero-interval-test", testConf)); + new YarnQueueMetricsCollector(mockYarnClient, "default", 0, "zero-interval-test")); } @Test - public void testNegativeRefreshIntervalIsRejected() throws Exception { - when(mockQueueInfo.getQueueStatistics()).thenReturn(null); - when(mockQueueInfo.getCapacity()).thenReturn(0.5f); - when(mockYarnClient.getQueueInfo(anyString())).thenReturn(mockQueueInfo); - + public void testNegativeRefreshIntervalIsRejected() { assertThrows(IllegalArgumentException.class, () -> - new YarnQueueMetricsCollector(mockYarnClient, "default", -1000, "negative-interval-test", testConf)); - } - - @Test - public void testJitterCalculationRange() { - long intervalMs = 2000; - long maxJitter = intervalMs * QueueMetricsRefreshPool.JITTER_PERCENT / 100; // 200ms - - // Test multiple queue names to ensure jitter is in range - String[] queues = {"default", "production", "batch", "analytics", "q" + "x".repeat(50)}; - for (String queueName : queues) { - long jitter = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); - assertTrue("Jitter should be >= 0 for " + queueName, jitter >= 0); - assertTrue("Jitter should be < maxJitter (" + maxJitter + "ms) for " + queueName, - jitter < maxJitter); - } + new YarnQueueMetricsCollector(mockYarnClient, "default", -1000, "negative-interval-test")); } @Test - public void testJitterIsDeterministic() { - long intervalMs = 5000; - String queueName = "production-analytics"; - - long jitter1 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); - long jitter2 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); - long jitter3 = QueueMetricsRefreshPool.calculateJitter(queueName, intervalMs); - - assertEquals("Jitter should be deterministic (same queue → same jitter)", jitter1, jitter2); - assertEquals("Jitter should be deterministic across multiple calls", jitter2, jitter3); - } - - @Test - public void testMultipleSessionsShareCacheState() throws Exception { - setupHappyPathMocks(); + public void testMultipleSessionsShareCacheState() { // Create two collectors for the same queue YarnQueueMetricsCollector collector1 = newCollector(mockYarnClient, "default", 5000, "query-1"); @@ -493,8 +444,9 @@ public void testMultipleSessionsShareCacheState() throws Exception { QueueMetricsSnapshot snapshot2 = collector2.getLatestSnapshot(); assertNotNull("Second collector should get cached snapshot", snapshot2); - assertEquals("Both collectors should see same memory value", - snapshot1.getMemoryUsedGB(), snapshot2.getMemoryUsedGB(), 0.01f); + assertSame("Both collectors should return the exact same cached snapshot instance", + snapshot1, snapshot2); + } finally { collector1.shutdown(); collector2.shutdown(); @@ -502,8 +454,7 @@ public void testMultipleSessionsShareCacheState() throws Exception { } @Test - public void testDynamicReschedulingOnIntervalChange() throws Exception { - setupHappyPathMocks(); + public void testDynamicReschedulingOnIntervalChange() { // Start with slow collector (10s) YarnQueueMetricsCollector slowCollector = newCollector(mockYarnClient, "default", 10000, "slow-query"); @@ -520,8 +471,8 @@ public void testDynamicReschedulingOnIntervalChange() throws Exception { // Shutdown fast collector - should reschedule back to slow interval fastCollector.shutdown(); - // Wait up to 500ms for rescheduling to complete - waitForInvocationCount(mockYarnClient, mockingDetails(mockYarnClient).getInvocations().size(), 500); + // Wait up to WAIT_TIMEOUT_MS for rescheduling to complete + waitForInvocationCount(mockYarnClient, mockingDetails(mockYarnClient).getInvocations().size(), WAIT_TIMEOUT_MS); // Verify slow collector still works assertNotNull("Slow collector should continue after fast shutdown", @@ -541,25 +492,23 @@ public void testCircuitBreakerProbeEvery10Ticks() throws Exception { try { // Wait for circuit breaker to activate (5 failures) - waitForInvocationCount(mockYarnClient, 6, 1000); + waitForInvocationCount(mockYarnClient, 6, WAIT_TIMEOUT_MS); int callsAfterActivation = mockingDetails(mockYarnClient).getInvocations().size(); - // Wait for next ~12 ticks at 50ms interval — poll until invocation count stabilizes - waitForInvocationCount(mockYarnClient, callsAfterActivation + 2, 800); + // Wait for at least one probe attempt to occur past the circuit-breaker activation. + waitForInvocationCount(mockYarnClient, callsAfterActivation + 1, WAIT_TIMEOUT_MS); int callsAfterWait = mockingDetails(mockYarnClient).getInvocations().size(); - // Should have ~1 probe attempt in 10 ticks int probeAttempts = callsAfterWait - callsAfterActivation; - assertTrue("Circuit breaker should allow ~1 probe per 10 ticks, got " + probeAttempts, - probeAttempts >= 0 && probeAttempts <= 2); + assertTrue("Circuit breaker should allow at least one probe past activation, got " + probeAttempts, + probeAttempts > 0); } finally { collector.shutdown(); } } @Test - public void testTaskCancelsWhenAllSessionsDeregister() throws Exception { - setupHappyPathMocks(); + public void testTaskCancelsWhenAllSessionsDeregister() { YarnQueueMetricsCollector collector1 = newCollector(mockYarnClient, "default", 2000, "query-1"); YarnQueueMetricsCollector collector2 = newCollector(mockYarnClient, "default", 2000, "query-2"); @@ -572,16 +521,21 @@ public void testTaskCancelsWhenAllSessionsDeregister() throws Exception { collector1.shutdown(); collector2.shutdown(); - // Wait and verify no more RM calls after shutdown (task cancelled) - // Spin-wait up to 3 seconds checking that call count has stabilized after both shutdowns - int callsAfterShutdown; - long deadline = System.currentTimeMillis() + 3000; - do { - callsAfterShutdown = mockingDetails(mockYarnClient).getInvocations().size(); - Thread.onSpinWait(); // Hint to JVM that this is a spin-wait loop - } while (callsAfterShutdown > callsWithBoth && System.currentTimeMillis() < deadline); + // Wait and verify no more RM calls after shutdown (task cancelled). + // Poll up to WAIT_TIMEOUT_MS; exit early once the observed count is no longer growing past callsWithBoth. + int[] latest = { mockingDetails(mockYarnClient).getInvocations().size() }; + try { + await().atMost(Duration.ofMillis(WAIT_TIMEOUT_MS)) + .pollInterval(Duration.ofMillis(10)) + .until(() -> { + latest[0] = mockingDetails(mockYarnClient).getInvocations().size(); + return latest[0] <= callsWithBoth; + }); + } catch (ConditionTimeoutException ignored) { + // Assertion below will report the mismatch. + } assertEquals("No more RM calls should occur after all sessions deregister", - callsWithBoth, callsAfterShutdown); + callsWithBoth, latest[0]); } } diff --git a/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.cpp b/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.cpp index c8b96da3ed5b..d7611ca49ff7 100644 --- a/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.cpp +++ b/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.cpp @@ -11611,6 +11611,11 @@ void TProgressUpdateResp::__set_footerSummary(const std::string& val) { void TProgressUpdateResp::__set_startTime(const int64_t val) { this->startTime = val; } + +void TProgressUpdateResp::__set_queueMetrics(const std::string& val) { + this->queueMetrics = val; +__isset.queueMetrics = true; +} std::ostream& operator<<(std::ostream& out, const TProgressUpdateResp& obj) { obj.printTo(out); @@ -11731,6 +11736,14 @@ uint32_t TProgressUpdateResp::read(::apache::thrift::protocol::TProtocol* iprot) xfer += iprot->skip(ftype); } break; + case 7: + if (ftype == ::apache::thrift::protocol::T_STRING) { + xfer += iprot->readString(this->queueMetrics); + this->__isset.queueMetrics = true; + } else { + xfer += iprot->skip(ftype); + } + break; default: xfer += iprot->skip(ftype); break; @@ -11808,6 +11821,11 @@ uint32_t TProgressUpdateResp::write(::apache::thrift::protocol::TProtocol* oprot xfer += oprot->writeI64(this->startTime); xfer += oprot->writeFieldEnd(); + if (this->__isset.queueMetrics) { + xfer += oprot->writeFieldBegin("queueMetrics", ::apache::thrift::protocol::T_STRING, 7); + xfer += oprot->writeString(this->queueMetrics); + xfer += oprot->writeFieldEnd(); + } xfer += oprot->writeFieldStop(); xfer += oprot->writeStructEnd(); return xfer; @@ -11821,6 +11839,8 @@ void swap(TProgressUpdateResp &a, TProgressUpdateResp &b) { swap(a.status, b.status); swap(a.footerSummary, b.footerSummary); swap(a.startTime, b.startTime); + swap(a.queueMetrics, b.queueMetrics); + swap(a.__isset, b.__isset); } TProgressUpdateResp::TProgressUpdateResp(const TProgressUpdateResp& other349) { @@ -11830,6 +11850,8 @@ TProgressUpdateResp::TProgressUpdateResp(const TProgressUpdateResp& other349) { status = other349.status; footerSummary = other349.footerSummary; startTime = other349.startTime; + queueMetrics = other349.queueMetrics; + __isset = other349.__isset; } TProgressUpdateResp& TProgressUpdateResp::operator=(const TProgressUpdateResp& other350) { headerNames = other350.headerNames; @@ -11838,6 +11860,8 @@ TProgressUpdateResp& TProgressUpdateResp::operator=(const TProgressUpdateResp& o status = other350.status; footerSummary = other350.footerSummary; startTime = other350.startTime; + queueMetrics = other350.queueMetrics; + __isset = other350.__isset; return *this; } void TProgressUpdateResp::printTo(std::ostream& out) const { @@ -11849,6 +11873,7 @@ void TProgressUpdateResp::printTo(std::ostream& out) const { out << ", " << "status=" << to_string(status); out << ", " << "footerSummary=" << to_string(footerSummary); out << ", " << "startTime=" << to_string(startTime); + out << ", " << "queueMetrics="; (__isset.queueMetrics ? (out << to_string(queueMetrics)) : (out << "")); out << ")"; } diff --git a/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.h b/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.h index b80d355e03a9..c1b6fa3d0ef9 100644 --- a/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.h +++ b/service-rpc/src/gen/thrift/gen-cpp/TCLIService_types.h @@ -4740,6 +4740,10 @@ void swap(TRenewDelegationTokenResp &a, TRenewDelegationTokenResp &b); std::ostream& operator<<(std::ostream& out, const TRenewDelegationTokenResp& obj); +typedef struct _TProgressUpdateResp__isset { + _TProgressUpdateResp__isset() : queueMetrics(false) {} + bool queueMetrics :1; +} _TProgressUpdateResp__isset; class TProgressUpdateResp : public virtual ::apache::thrift::TBase { public: @@ -4750,7 +4754,8 @@ class TProgressUpdateResp : public virtual ::apache::thrift::TBase { : progressedPercentage(0), status(static_cast(0)), footerSummary(), - startTime(0) { + startTime(0), + queueMetrics() { } virtual ~TProgressUpdateResp() noexcept; @@ -4764,6 +4769,9 @@ class TProgressUpdateResp : public virtual ::apache::thrift::TBase { TJobExecutionStatus::type status; std::string footerSummary; int64_t startTime; + std::string queueMetrics; + + _TProgressUpdateResp__isset __isset; void __set_headerNames(const std::vector & val); @@ -4777,6 +4785,8 @@ class TProgressUpdateResp : public virtual ::apache::thrift::TBase { void __set_startTime(const int64_t val); + void __set_queueMetrics(const std::string& val); + bool operator == (const TProgressUpdateResp & rhs) const { if (!(headerNames == rhs.headerNames)) @@ -4791,6 +4801,10 @@ class TProgressUpdateResp : public virtual ::apache::thrift::TBase { return false; if (!(startTime == rhs.startTime)) return false; + if (__isset.queueMetrics != rhs.__isset.queueMetrics) + return false; + else if (__isset.queueMetrics && !(queueMetrics == rhs.queueMetrics)) + return false; return true; } bool operator != (const TProgressUpdateResp &rhs) const { diff --git a/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java b/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java index ef787a15d080..8ba5d1bb406b 100644 --- a/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java +++ b/service-rpc/src/gen/thrift/gen-javabean/org/apache/hive/service/rpc/thrift/TProgressUpdateResp.java @@ -116,6 +116,7 @@ public java.lang.String getFieldName() { private static final int __PROGRESSEDPERCENTAGE_ISSET_ID = 0; private static final int __STARTTIME_ISSET_ID = 1; private byte __isset_bitfield = 0; + private static final _Fields optionals[] = {_Fields.QUEUE_METRICS}; public static final java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> metaDataMap; static { java.util.Map<_Fields, org.apache.thrift.meta_data.FieldMetaData> tmpMap = new java.util.EnumMap<_Fields, org.apache.thrift.meta_data.FieldMetaData>(_Fields.class); @@ -134,7 +135,7 @@ public java.lang.String getFieldName() { new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); tmpMap.put(_Fields.START_TIME, new org.apache.thrift.meta_data.FieldMetaData("startTime", org.apache.thrift.TFieldRequirementType.REQUIRED, new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.I64))); - tmpMap.put(_Fields.QUEUE_METRICS, new org.apache.thrift.meta_data.FieldMetaData("queueMetrics", org.apache.thrift.TFieldRequirementType.OPTIONAL, + tmpMap.put(_Fields.QUEUE_METRICS, new org.apache.thrift.meta_data.FieldMetaData("queueMetrics", org.apache.thrift.TFieldRequirementType.OPTIONAL, new org.apache.thrift.meta_data.FieldValueMetaData(org.apache.thrift.protocol.TType.STRING))); metaDataMap = java.util.Collections.unmodifiableMap(tmpMap); org.apache.thrift.meta_data.FieldMetaData.addStructMetaDataMap(TProgressUpdateResp.class, metaDataMap); @@ -159,6 +160,7 @@ public TProgressUpdateResp( this.status = status; this.footerSummary = footerSummary; this.startTime = startTime; + setStartTimeIsSet(true); } /** @@ -704,6 +706,16 @@ public int compareTo(TProgressUpdateResp other) { return lastComparison; } } + lastComparison = java.lang.Boolean.compare(isSetQueueMetrics(), other.isSetQueueMetrics()); + if (lastComparison != 0) { + return lastComparison; + } + if (isSetQueueMetrics()) { + lastComparison = org.apache.thrift.TBaseHelper.compareTo(this.queueMetrics, other.queueMetrics); + if (lastComparison != 0) { + return lastComparison; + } + } return 0; } @@ -764,6 +776,16 @@ public java.lang.String toString() { sb.append("startTime:"); sb.append(this.startTime); first = false; + if (isSetQueueMetrics()) { + if (!first) sb.append(", "); + sb.append("queueMetrics:"); + if (this.queueMetrics == null) { + sb.append("null"); + } else { + sb.append(this.queueMetrics); + } + first = false; + } sb.append(")"); return sb.toString(); } @@ -915,7 +937,7 @@ public void read(org.apache.thrift.protocol.TProtocol iprot, TProgressUpdateResp if (schemeField.type == org.apache.thrift.protocol.TType.STRING) { struct.queueMetrics = iprot.readString(); struct.setQueueMetricsIsSet(true); - } else { + } else { org.apache.thrift.protocol.TProtocolUtil.skip(iprot, schemeField.type); } break; @@ -980,9 +1002,11 @@ public void write(org.apache.thrift.protocol.TProtocol oprot, TProgressUpdateRes oprot.writeI64(struct.startTime); oprot.writeFieldEnd(); if (struct.queueMetrics != null) { - oprot.writeFieldBegin(QUEUE_METRICS_FIELD_DESC); - oprot.writeString(struct.queueMetrics); - oprot.writeFieldEnd(); + if (struct.isSetQueueMetrics()) { + oprot.writeFieldBegin(QUEUE_METRICS_FIELD_DESC); + oprot.writeString(struct.queueMetrics); + oprot.writeFieldEnd(); + } } oprot.writeFieldStop(); oprot.writeStructEnd(); @@ -1025,6 +1049,14 @@ public void write(org.apache.thrift.protocol.TProtocol prot, TProgressUpdateResp oprot.writeI32(struct.status.getValue()); oprot.writeString(struct.footerSummary); oprot.writeI64(struct.startTime); + java.util.BitSet optionals = new java.util.BitSet(); + if (struct.isSetQueueMetrics()) { + optionals.set(0); + } + oprot.writeBitSet(optionals, 1); + if (struct.isSetQueueMetrics()) { + oprot.writeString(struct.queueMetrics); + } } @Override @@ -1069,6 +1101,11 @@ public void read(org.apache.thrift.protocol.TProtocol prot, TProgressUpdateResp struct.setFooterSummaryIsSet(true); struct.startTime = iprot.readI64(); struct.setStartTimeIsSet(true); + java.util.BitSet incoming = iprot.readBitSet(1); + if (incoming.get(0)) { + struct.queueMetrics = iprot.readString(); + struct.setQueueMetricsIsSet(true); + } } } diff --git a/service-rpc/src/gen/thrift/gen-php/TProgressUpdateResp.php b/service-rpc/src/gen/thrift/gen-php/TProgressUpdateResp.php index 12005c6c5de3..e7008fcafbab 100644 --- a/service-rpc/src/gen/thrift/gen-php/TProgressUpdateResp.php +++ b/service-rpc/src/gen/thrift/gen-php/TProgressUpdateResp.php @@ -62,6 +62,11 @@ class TProgressUpdateResp 'isRequired' => true, 'type' => TType::I64, ), + 7 => array( + 'var' => 'queueMetrics', + 'isRequired' => false, + 'type' => TType::STRING, + ), ); /** @@ -88,6 +93,10 @@ class TProgressUpdateResp * @var int */ public $startTime = null; + /** + * @var string + */ + public $queueMetrics = null; public function __construct($vals = null) { @@ -110,6 +119,9 @@ public function __construct($vals = null) if (isset($vals['startTime'])) { $this->startTime = $vals['startTime']; } + if (isset($vals['queueMetrics'])) { + $this->queueMetrics = $vals['queueMetrics']; + } } } @@ -201,6 +213,13 @@ public function read($input) $xfer += $input->skip($ftype); } break; + case 7: + if ($ftype == TType::STRING) { + $xfer += $input->readString($this->queueMetrics); + } else { + $xfer += $input->skip($ftype); + } + break; default: $xfer += $input->skip($ftype); break; @@ -263,6 +282,11 @@ public function write($output) $xfer += $output->writeI64($this->startTime); $xfer += $output->writeFieldEnd(); } + if ($this->queueMetrics !== null) { + $xfer += $output->writeFieldBegin('queueMetrics', TType::STRING, 7); + $xfer += $output->writeString($this->queueMetrics); + $xfer += $output->writeFieldEnd(); + } $xfer += $output->writeFieldStop(); $xfer += $output->writeStructEnd(); return $xfer; diff --git a/service-rpc/src/gen/thrift/gen-py/TCLIService/ttypes.py b/service-rpc/src/gen/thrift/gen-py/TCLIService/ttypes.py index 8d72578a4970..9bb79b5e2641 100644 --- a/service-rpc/src/gen/thrift/gen-py/TCLIService/ttypes.py +++ b/service-rpc/src/gen/thrift/gen-py/TCLIService/ttypes.py @@ -7105,17 +7105,19 @@ class TProgressUpdateResp(object): - status - footerSummary - startTime + - queueMetrics """ - def __init__(self, headerNames=None, rows=None, progressedPercentage=None, status=None, footerSummary=None, startTime=None,): + def __init__(self, headerNames=None, rows=None, progressedPercentage=None, status=None, footerSummary=None, startTime=None, queueMetrics=None,): self.headerNames = headerNames self.rows = rows self.progressedPercentage = progressedPercentage self.status = status self.footerSummary = footerSummary self.startTime = startTime + self.queueMetrics = queueMetrics def read(self, iprot): if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None: @@ -7171,6 +7173,11 @@ def read(self, iprot): self.startTime = iprot.readI64() else: iprot.skip(ftype) + elif fid == 7: + if ftype == TType.STRING: + self.queueMetrics = iprot.readString().decode('utf-8', errors='replace') if sys.version_info[0] == 2 else iprot.readString() + else: + iprot.skip(ftype) else: iprot.skip(ftype) iprot.readFieldEnd() @@ -7214,6 +7221,10 @@ def write(self, oprot): oprot.writeFieldBegin('startTime', TType.I64, 6) oprot.writeI64(self.startTime) oprot.writeFieldEnd() + if self.queueMetrics is not None: + oprot.writeFieldBegin('queueMetrics', TType.STRING, 7) + oprot.writeString(self.queueMetrics.encode('utf-8') if sys.version_info[0] == 2 else self.queueMetrics) + oprot.writeFieldEnd() oprot.writeFieldStop() oprot.writeStructEnd() @@ -7915,6 +7926,7 @@ def __ne__(self, other): (4, TType.I32, 'status', None, None, ), # 4 (5, TType.STRING, 'footerSummary', 'UTF8', None, ), # 5 (6, TType.I64, 'startTime', None, None, ), # 6 + (7, TType.STRING, 'queueMetrics', 'UTF8', None, ), # 7 ) all_structs.append(TGetQueryIdReq) TGetQueryIdReq.thrift_spec = ( diff --git a/service-rpc/src/gen/thrift/gen-rb/t_c_l_i_service_types.rb b/service-rpc/src/gen/thrift/gen-rb/t_c_l_i_service_types.rb index 787ddcd7728e..6a4e3fbd9715 100644 --- a/service-rpc/src/gen/thrift/gen-rb/t_c_l_i_service_types.rb +++ b/service-rpc/src/gen/thrift/gen-rb/t_c_l_i_service_types.rb @@ -2194,6 +2194,7 @@ class TProgressUpdateResp STATUS = 4 FOOTERSUMMARY = 5 STARTTIME = 6 + QUEUEMETRICS = 7 FIELDS = { HEADERNAMES => {:type => ::Thrift::Types::LIST, :name => 'headerNames', :element => {:type => ::Thrift::Types::STRING}}, @@ -2201,7 +2202,8 @@ class TProgressUpdateResp PROGRESSEDPERCENTAGE => {:type => ::Thrift::Types::DOUBLE, :name => 'progressedPercentage'}, STATUS => {:type => ::Thrift::Types::I32, :name => 'status', :enum_class => ::TJobExecutionStatus}, FOOTERSUMMARY => {:type => ::Thrift::Types::STRING, :name => 'footerSummary'}, - STARTTIME => {:type => ::Thrift::Types::I64, :name => 'startTime'} + STARTTIME => {:type => ::Thrift::Types::I64, :name => 'startTime'}, + QUEUEMETRICS => {:type => ::Thrift::Types::STRING, :name => 'queueMetrics', :optional => true} } def struct_fields; FIELDS; end diff --git a/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java b/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java index 37cdb29ff4e3..0491f8128108 100644 --- a/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java +++ b/service/src/java/org/apache/hive/service/cli/thrift/ThriftCLIService.java @@ -822,15 +822,6 @@ public TGetOperationStatusResp GetOperationStatus(TGetOperationStatusReq req) th progressUpdate.footerSummary, progressUpdate.startTimeMillis ); - // HIVE-27126: Workaround for Thrift code generation bug. - // When queueMetrics field was added and Thrift code regenerated, the generated constructor - // accepts startTimeMillis parameter but FAILS to call setStartTimeIsSet(true). - // Without the isset flag, Thrift serialization treats startTime as unset, causing: - // 1) Clients receive incomplete/invalid progress updates - // 2) Thrift validation may fail or skip the field entirely - // Solution: Explicitly call setStartTime() after construction to set the isset flag. - // This ensures proper serialization and backward compatibility with Thrift clients. - tProgressUpdateResp.setStartTime(progressUpdate.startTimeMillis); if (progressUpdate.queueMetrics() != null && !progressUpdate.queueMetrics().isEmpty()) { tProgressUpdateResp.setQueueMetrics(progressUpdate.queueMetrics()); } diff --git a/service/src/java/org/apache/hive/service/server/HiveServer2.java b/service/src/java/org/apache/hive/service/server/HiveServer2.java index 7c8541e878e1..ec37e139d288 100644 --- a/service/src/java/org/apache/hive/service/server/HiveServer2.java +++ b/service/src/java/org/apache/hive/service/server/HiveServer2.java @@ -938,8 +938,6 @@ private void initAndStartTezSessionPoolManager(final WMFullResourcePlan resource LOG.info("Initializing tez session pool manager. Active resource plan: {}", resourcePlan == null || resourcePlan.getPlan() == null ? "null" : resourcePlan.getPlan().getName()); HiveConf hiveConf = getHiveConf(); - // Initialize the queue metrics refresh pool for collecting YARN queue metrics during query execution. - // Pool is shared across all queries and sized by hive.server2.tez.queue.metrics.refresh.threads config. initializeQueueMetricsPool(hiveConf); tezSessionPoolManager = TezSessionPoolManager.getInstance(); if (hiveConf.getBoolVar(ConfVars.HIVE_SERVER2_TEZ_INITIALIZE_DEFAULT_SESSIONS)) { @@ -976,35 +974,17 @@ private void initAndStartWorkloadManager(final WMFullResourcePlan resourcePlan) * Initializes the shared JVM-wide queue metrics refresh pool. *

* This pool provides background threads for periodic YARN queue metrics collection across - * all Tez sessions. The pool is shared by all queries in this HiveServer2 process to prevent - * thread explosion when many queries run concurrently. - *

- * Thread count is configured via {@code hive.server2.tez.queue.metrics.refresh.threads}. - *

- * The pool is only initialized when execution engine is "tez". Whether to actually collect - * queue metrics is controlled per-session by {@code hive.tez.queue.metrics.refresh.interval}, - * which is checked when creating metrics collectors for each query. - *

- * In non-Tez environments (MR, Spark, local), the pool is not created, avoiding unnecessary - * thread allocation. + * all Tez sessions. Thread count is configured via + * {@code hive.server2.tez.queue.metrics.refresh.threads}. Whether to actually collect metrics + * is controlled per-session by {@code hive.tez.queue.metrics.refresh.interval}. *

* Failures are non-fatal — logged as warnings so the server can start without queue metrics. */ private void initializeQueueMetricsPool(HiveConf hiveConf) { try { - // Only initialize for Tez execution engine - String executionEngine = hiveConf.getVar(ConfVars.HIVE_EXECUTION_ENGINE); - if (!"tez".equalsIgnoreCase(executionEngine)) { - LOG.info("Skipping queue metrics refresh pool initialization: execution engine is '{}' (not Tez)", - executionEngine); - return; - } - - // Initialize the pool with configured thread count - // Whether to use it is decided per-session based on hive.tez.queue.metrics.refresh.interval int refreshThreads = hiveConf.getIntVar(ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS); QueueMetricsRefreshPool.init(refreshThreads); - LOG.info("Queue metrics refresh pool initialized with {} threads for Tez execution engine", refreshThreads); + LOG.info("Queue metrics refresh pool initialized with {} threads", refreshThreads); } catch (Exception e) { LOG.warn("Failed to initialize queue metrics refresh pool", e); } diff --git a/service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java b/service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java deleted file mode 100644 index b27869a53b96..000000000000 --- a/service/src/test/org/apache/hive/service/server/TestHiveServer2QueueMetricsPoolInit.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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.hive.service.server; - -import org.apache.hadoop.hive.conf.HiveConf; -import org.apache.hadoop.hive.ql.exec.tez.monitoring.yarnqueue.QueueMetricsRefreshPool; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.lang.reflect.Method; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; - -/** - * Tests for conditional initialization of QueueMetricsRefreshPool in HiveServer2. - * - * Verifies that the pool is only initialized when execution engine is "tez", - * and skipped for other engines (MR, Spark, local). - * - * Uses {@link QueueMetricsRefreshPool#getInstanceForTesting()} to verify initialization - * state without triggering lazy initialization. - */ -public class TestHiveServer2QueueMetricsPoolInit { - - @Before - public void setUp() { - // Reset the pool before each test - QueueMetricsRefreshPool.resetForTesting(); - } - - @After - public void tearDown() { - // Clean up after each test - QueueMetricsRefreshPool.resetForTesting(); - } - - /** - * Test that pool IS initialized when execution engine is "tez". - * This is a POSITIVE test case. - */ - @Test - public void testPoolInitializedForTezEngine() throws Exception { - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "tez"); - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); - - // Verify pool is not initialized before the call - assertNull("Pool should not be initialized before init call", - QueueMetricsRefreshPool.getInstanceForTesting()); - - HiveServer2 hs2 = new HiveServer2(); - invokeInitializeQueueMetricsPool(hs2, conf); - - // Verify pool WAS initialized (POSITIVE case) - assertNotNull("Pool SHOULD be initialized for Tez engine", - QueueMetricsRefreshPool.getInstanceForTesting()); - } - - /** - * Test that pool IS initialized case-insensitively for "Tez", "TEZ", etc. - * This is a POSITIVE test case. - */ - @Test - public void testPoolInitializedForTezEngineCaseInsensitive() throws Exception { - String[] tezVariants = {"tez", "Tez", "TEZ", "tEz"}; - - for (String variant : tezVariants) { - // Reset between iterations - QueueMetricsRefreshPool.resetForTesting(); - - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, variant); - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); - - // Verify not initialized before - assertNull("Pool should not be initialized before init call for: " + variant, - QueueMetricsRefreshPool.getInstanceForTesting()); - - HiveServer2 hs2 = new HiveServer2(); - invokeInitializeQueueMetricsPool(hs2, conf); - - // Verify WAS initialized (POSITIVE case) - assertNotNull("Pool SHOULD be initialized for Tez variant: " + variant, - QueueMetricsRefreshPool.getInstanceForTesting()); - } - } - - /** - * Test that pool is NOT initialized when execution engine is "mr". - * This is a NEGATIVE test case - verifying the pool remains null. - */ - @Test - public void testPoolNotInitializedForMrEngine() throws Exception { - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "mr"); - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); - - // Verify pool is not initialized before - assertNull("Pool should not be initialized before init call", - QueueMetricsRefreshPool.getInstanceForTesting()); - - HiveServer2 hs2 = new HiveServer2(); - invokeInitializeQueueMetricsPool(hs2, conf); - - // Verify pool is STILL not initialized (NEGATIVE case) - assertNull("Pool should NOT be initialized for MR engine", - QueueMetricsRefreshPool.getInstanceForTesting()); - } - - /** - * Test that pool is NOT initialized when execution engine is "spark". - * This is a NEGATIVE test case. - */ - @Test - public void testPoolNotInitializedForSparkEngine() throws Exception { - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "spark"); - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); - - // Verify not initialized before - assertNull("Pool should not be initialized before init call", - QueueMetricsRefreshPool.getInstanceForTesting()); - - HiveServer2 hs2 = new HiveServer2(); - invokeInitializeQueueMetricsPool(hs2, conf); - - // Verify STILL not initialized (NEGATIVE case) - assertNull("Pool should NOT be initialized for Spark engine", - QueueMetricsRefreshPool.getInstanceForTesting()); - } - - /** - * Test that pool is NOT initialized when execution engine is empty. - * This is a NEGATIVE test case. - */ - @Test - public void testPoolNotInitializedForEmptyEngine() throws Exception { - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, ""); - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); - - // Verify not initialized before - assertNull("Pool should not be initialized before init call", - QueueMetricsRefreshPool.getInstanceForTesting()); - - HiveServer2 hs2 = new HiveServer2(); - invokeInitializeQueueMetricsPool(hs2, conf); - - // Verify STILL not initialized (NEGATIVE case) - assertNull("Pool should NOT be initialized for empty engine", - QueueMetricsRefreshPool.getInstanceForTesting()); - } - - /** - * Test that pool is NOT initialized when execution engine is "local". - * This is a NEGATIVE test case. - */ - @Test - public void testPoolNotInitializedForLocalEngine() throws Exception { - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "local"); - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 4); - - // Verify not initialized before - assertNull("Pool should not be initialized before init call", - QueueMetricsRefreshPool.getInstanceForTesting()); - - HiveServer2 hs2 = new HiveServer2(); - invokeInitializeQueueMetricsPool(hs2, conf); - - // Verify STILL not initialized (NEGATIVE case) - assertNull("Pool should NOT be initialized for local engine", - QueueMetricsRefreshPool.getInstanceForTesting()); - } - - /** - * Test that pool initialization respects configured thread count. - * This is a POSITIVE test with different configuration. - */ - @Test - public void testPoolInitializedWithConfiguredThreadCount() throws Exception { - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "tez"); - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, 8); - - // Verify not initialized before - assertNull("Pool should not be initialized before init call", - QueueMetricsRefreshPool.getInstanceForTesting()); - - HiveServer2 hs2 = new HiveServer2(); - invokeInitializeQueueMetricsPool(hs2, conf); - - // Verify WAS initialized (POSITIVE case) - assertNotNull("Pool SHOULD be initialized with custom thread count", - QueueMetricsRefreshPool.getInstanceForTesting()); - } - - /** - * Test that pool initialization handles exceptions gracefully and doesn't fail server startup. - * Even with invalid config, the method should not throw exceptions to the caller. - */ - @Test - public void testPoolInitializationFailureIsNonFatal() throws Exception { - HiveConf conf = new HiveConf(); - conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "tez"); - // Set a potentially problematic value - conf.setIntVar(HiveConf.ConfVars.HIVE_SERVER2_TEZ_QUEUE_METRICS_REFRESH_THREADS, -1); - - HiveServer2 hs2 = new HiveServer2(); - - // Should not throw exception - errors are caught and logged - try { - invokeInitializeQueueMetricsPool(hs2, conf); - // Test passes if we get here without exception - } catch (Exception e) { - fail("Pool initialization should not throw exceptions to caller, got: " + e.getMessage()); - } - } - - /** - * Helper method to invoke the private initializeQueueMetricsPool method via reflection. - */ - private void invokeInitializeQueueMetricsPool(HiveServer2 hs2, HiveConf conf) throws Exception { - Method method = HiveServer2.class.getDeclaredMethod("initializeQueueMetricsPool", HiveConf.class); - method.setAccessible(true); - method.invoke(hs2, conf); - } -} - -