Skip to content

Commit 0cc7e9d

Browse files
authored
Improve PowerShell execution and failure backoff for Windows disk metrics collection (#17747)
1 parent bc150ff commit 0cc7e9d

2 files changed

Lines changed: 264 additions & 35 deletions

File tree

‎iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/metricsets/disk/WindowsDiskMetricsManager.java‎

Lines changed: 185 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@
2626
import org.slf4j.LoggerFactory;
2727

2828
import java.io.BufferedReader;
29+
import java.io.File;
2930
import java.io.IOException;
3031
import java.io.InputStreamReader;
3132
import java.nio.charset.Charset;
3233
import java.util.ArrayList;
34+
import java.util.Collections;
3335
import java.util.HashMap;
3436
import java.util.HashSet;
3537
import java.util.List;
3638
import java.util.Map;
3739
import java.util.Set;
40+
import java.util.concurrent.TimeUnit;
3841

3942
/**
4043
* Disk metrics manager for Windows system.
@@ -48,9 +51,13 @@ public class WindowsDiskMetricsManager implements IDiskMetricsManager {
4851

4952
private static final double BYTES_PER_KB = 1024.0;
5053
private static final long UPDATE_SMALLEST_INTERVAL = 10000L;
54+
private static final long POWERSHELL_RETRY_INTERVAL = TimeUnit.MINUTES.toMillis(5);
55+
private static final long FAILURE_LOG_INTERVAL = TimeUnit.MINUTES.toMillis(5);
5156
private static final String POWER_SHELL = "powershell";
5257
private static final String POWER_SHELL_NO_PROFILE = "-NoProfile";
5358
private static final String POWER_SHELL_COMMAND = "-Command";
59+
private static final String WINDOWS_POWER_SHELL_RELATIVE_PATH =
60+
"System32\\WindowsPowerShell\\v1.0\\powershell.exe";
5461
private static final String TOTAL_DISK_INSTANCE = "_Total";
5562
private static final Charset WINDOWS_SHELL_CHARSET = getWindowsShellCharset();
5663
private static final String DISK_QUERY =
@@ -78,10 +85,14 @@ public class WindowsDiskMetricsManager implements IDiskMetricsManager {
7885
+ "$_.IOWriteBytesPerSec) }";
7986

8087
private final String processId;
88+
private final PowerShellExecutor powerShellExecutor;
8189
private final Set<String> diskIdSet = new HashSet<>();
8290

8391
private long lastUpdateTime = 0L;
8492
private long updateInterval = 1L;
93+
private long nextPowerShellRetryTime = 0L;
94+
private long nextFailureLogTime = 0L;
95+
private String lastPowerShellFailure = "";
8596

8697
private final Map<String, Long> lastReadOperationCountForDisk = new HashMap<>();
8798
private final Map<String, Long> lastWriteOperationCountForDisk = new HashMap<>();
@@ -106,7 +117,14 @@ public class WindowsDiskMetricsManager implements IDiskMetricsManager {
106117
private long lastWriteOpsCountForProcess = 0L;
107118

108119
public WindowsDiskMetricsManager() {
109-
processId = String.valueOf(MetricConfigDescriptor.getInstance().getMetricConfig().getPid());
120+
this(
121+
String.valueOf(MetricConfigDescriptor.getInstance().getMetricConfig().getPid()),
122+
new ProcessBuilderPowerShellExecutor(resolvePowerShellExecutable()));
123+
}
124+
125+
WindowsDiskMetricsManager(String processId, PowerShellExecutor powerShellExecutor) {
126+
this.processId = processId;
127+
this.powerShellExecutor = powerShellExecutor;
110128
}
111129

112130
@Override
@@ -447,46 +465,105 @@ private double clampPercentage(double value) {
447465
}
448466

449467
private List<String> executePowerShell(String command) {
450-
List<String> result = new ArrayList<>();
451-
List<String> rawOutput = new ArrayList<>();
452-
Process process = null;
468+
if (System.currentTimeMillis() < nextPowerShellRetryTime) {
469+
return Collections.emptyList();
470+
}
471+
453472
try {
454-
process =
455-
new ProcessBuilder(POWER_SHELL, POWER_SHELL_NO_PROFILE, POWER_SHELL_COMMAND, command)
456-
.redirectErrorStream(true)
457-
.start();
458-
try (BufferedReader reader =
459-
new BufferedReader(
460-
new InputStreamReader(process.getInputStream(), WINDOWS_SHELL_CHARSET))) {
461-
String line;
462-
while ((line = reader.readLine()) != null) {
463-
String trimmedLine = line.trim();
464-
if (!trimmedLine.isEmpty()) {
465-
rawOutput.add(trimmedLine);
466-
}
467-
}
468-
}
469-
int exitCode = process.waitFor();
470-
if (exitCode != 0) {
471-
LOGGER.warn(
472-
MetricsMessages.FAILED_TO_COLLECT_WINDOWS_DISK_METRICS,
473-
exitCode,
474-
command,
475-
String.join(" | ", rawOutput));
476-
} else {
477-
result.addAll(rawOutput);
473+
CommandResult commandResult = powerShellExecutor.execute(command);
474+
List<String> rawOutput =
475+
commandResult.getOutput() == null ? Collections.emptyList() : commandResult.getOutput();
476+
if (commandResult.getExitCode() != 0) {
477+
handlePowerShellFailure(buildPowerShellFailure(commandResult), null, command, rawOutput);
478+
return Collections.emptyList();
478479
}
480+
clearPowerShellFailure();
481+
return rawOutput;
479482
} catch (IOException e) {
480-
LOGGER.warn(MetricsMessages.FAILED_TO_EXECUTE_POWERSHELL, e);
483+
handlePowerShellFailure(MetricsMessages.FAILED_TO_EXECUTE_POWERSHELL, e, command, null);
481484
} catch (InterruptedException e) {
482485
Thread.currentThread().interrupt();
483-
LOGGER.warn(MetricsMessages.INTERRUPTED_COLLECTING_WINDOWS_DISK, e);
484-
} finally {
485-
if (process != null) {
486-
process.destroy();
486+
handlePowerShellFailure(
487+
MetricsMessages.INTERRUPTED_COLLECTING_WINDOWS_DISK, e, command, null);
488+
}
489+
return Collections.emptyList();
490+
}
491+
492+
private String buildPowerShellFailure(CommandResult commandResult) {
493+
if (isAccessDeniedOutput(commandResult.getOutput())) {
494+
return "Access denied while collecting windows disk metrics through PowerShell/CIM";
495+
}
496+
return String.format(
497+
"Failed to collect windows disk metrics, powershell exit code: %s",
498+
commandResult.getExitCode());
499+
}
500+
501+
private boolean isAccessDeniedOutput(List<String> output) {
502+
if (output == null) {
503+
return false;
504+
}
505+
for (String line : output) {
506+
if (line == null) {
507+
continue;
508+
}
509+
String lowerCaseLine = line.toLowerCase();
510+
if (lowerCaseLine.contains("access is denied")
511+
|| lowerCaseLine.contains("access denied")
512+
|| lowerCaseLine.contains("permissiondenied")
513+
|| lowerCaseLine.contains("unauthorized")
514+
|| lowerCaseLine.contains("0x80041003")) {
515+
return true;
487516
}
488517
}
489-
return result;
518+
return false;
519+
}
520+
521+
private void handlePowerShellFailure(
522+
String failureMessage, Exception exception, String command, List<String> output) {
523+
long currentTime = System.currentTimeMillis();
524+
nextPowerShellRetryTime = currentTime + POWERSHELL_RETRY_INTERVAL;
525+
if (shouldLogFailure(currentTime, failureMessage)) {
526+
if (exception == null) {
527+
LOGGER.warn(
528+
"{}. Windows disk metrics will be skipped for {} ms before retrying.",
529+
failureMessage,
530+
POWERSHELL_RETRY_INTERVAL);
531+
} else {
532+
LOGGER.warn(
533+
"{}: {}. Windows disk metrics will be skipped for {} ms before retrying.",
534+
failureMessage,
535+
exception.toString(),
536+
POWERSHELL_RETRY_INTERVAL);
537+
}
538+
LOGGER.debug(
539+
"Failed windows disk metrics powershell command: {}, output: {}",
540+
command,
541+
output == null ? "" : String.join(" | ", output),
542+
exception);
543+
} else {
544+
LOGGER.debug(
545+
"{}. Windows disk metrics collection is still in retry backoff.",
546+
failureMessage,
547+
exception);
548+
}
549+
}
550+
551+
private boolean shouldLogFailure(long currentTime, String failureMessage) {
552+
if (!failureMessage.equals(lastPowerShellFailure) || currentTime >= nextFailureLogTime) {
553+
lastPowerShellFailure = failureMessage;
554+
nextFailureLogTime = currentTime + FAILURE_LOG_INTERVAL;
555+
return true;
556+
}
557+
return false;
558+
}
559+
560+
private void clearPowerShellFailure() {
561+
if (!lastPowerShellFailure.isEmpty() || nextPowerShellRetryTime > 0L) {
562+
LOGGER.info("Recovered windows disk metrics collection through PowerShell/CIM.");
563+
}
564+
lastPowerShellFailure = "";
565+
nextFailureLogTime = 0L;
566+
nextPowerShellRetryTime = 0L;
490567
}
491568

492569
private static Charset getWindowsShellCharset() {
@@ -506,9 +583,82 @@ private static Charset getWindowsShellCharset() {
506583
return Charset.defaultCharset();
507584
}
508585

509-
private void checkUpdate() {
586+
private static String resolvePowerShellExecutable() {
587+
String systemRoot = System.getenv("SystemRoot");
588+
if (systemRoot == null || systemRoot.isEmpty()) {
589+
systemRoot = System.getenv("windir");
590+
}
591+
if (systemRoot != null && !systemRoot.isEmpty()) {
592+
File systemPowerShell = new File(systemRoot, WINDOWS_POWER_SHELL_RELATIVE_PATH);
593+
if (systemPowerShell.isFile()) {
594+
return systemPowerShell.getAbsolutePath();
595+
}
596+
}
597+
return POWER_SHELL;
598+
}
599+
600+
private synchronized void checkUpdate() {
510601
if (System.currentTimeMillis() - lastUpdateTime > UPDATE_SMALLEST_INTERVAL) {
511602
updateInfo();
512603
}
513604
}
605+
606+
interface PowerShellExecutor {
607+
CommandResult execute(String command) throws IOException, InterruptedException;
608+
}
609+
610+
static class CommandResult {
611+
private final int exitCode;
612+
private final List<String> output;
613+
614+
CommandResult(int exitCode, List<String> output) {
615+
this.exitCode = exitCode;
616+
this.output = output;
617+
}
618+
619+
int getExitCode() {
620+
return exitCode;
621+
}
622+
623+
List<String> getOutput() {
624+
return output;
625+
}
626+
}
627+
628+
private static class ProcessBuilderPowerShellExecutor implements PowerShellExecutor {
629+
private final String powerShellExecutable;
630+
631+
private ProcessBuilderPowerShellExecutor(String powerShellExecutable) {
632+
this.powerShellExecutable = powerShellExecutable;
633+
}
634+
635+
@Override
636+
public CommandResult execute(String command) throws IOException, InterruptedException {
637+
List<String> rawOutput = new ArrayList<>();
638+
Process process = null;
639+
try {
640+
process =
641+
new ProcessBuilder(
642+
powerShellExecutable, POWER_SHELL_NO_PROFILE, POWER_SHELL_COMMAND, command)
643+
.redirectErrorStream(true)
644+
.start();
645+
try (BufferedReader reader =
646+
new BufferedReader(
647+
new InputStreamReader(process.getInputStream(), WINDOWS_SHELL_CHARSET))) {
648+
String line;
649+
while ((line = reader.readLine()) != null) {
650+
String trimmedLine = line.trim();
651+
if (!trimmedLine.isEmpty()) {
652+
rawOutput.add(trimmedLine);
653+
}
654+
}
655+
}
656+
return new CommandResult(process.waitFor(), rawOutput);
657+
} finally {
658+
if (process != null) {
659+
process.destroy();
660+
}
661+
}
662+
}
663+
}
514664
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.iotdb.metrics.metricsets.disk;
21+
22+
import org.junit.Test;
23+
24+
import java.io.IOException;
25+
import java.util.Arrays;
26+
import java.util.Set;
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
29+
import static org.junit.Assert.assertEquals;
30+
import static org.junit.Assert.assertTrue;
31+
32+
public class WindowsDiskMetricsManagerTest {
33+
34+
@Test
35+
public void testCollectWindowsDiskMetrics() {
36+
AtomicInteger processQueryCount = new AtomicInteger();
37+
WindowsDiskMetricsManager manager =
38+
new WindowsDiskMetricsManager(
39+
"123",
40+
command -> {
41+
if (command.contains("PhysicalDisk")) {
42+
return new WindowsDiskMetricsManager.CommandResult(
43+
0, Arrays.asList("0 C:\t1\t2\t1024\t4096\t0.001\t0.002\t75\t3"));
44+
}
45+
if (command.contains("PerfProc_Process")) {
46+
processQueryCount.incrementAndGet();
47+
return new WindowsDiskMetricsManager.CommandResult(
48+
0, Arrays.asList("3\t4\t8192\t16384"));
49+
}
50+
return new WindowsDiskMetricsManager.CommandResult(1, Arrays.asList("unexpected"));
51+
});
52+
53+
Set<String> diskIds = manager.getDiskIds();
54+
55+
assertTrue(diskIds.contains("0 C:"));
56+
assertEquals(1, processQueryCount.get());
57+
assertEquals(0.25, manager.getIoUtilsPercentage().get("0 C:"), 0.0001);
58+
assertEquals(3.0, manager.getQueueSizeForDisk().get("0 C:"), 0.0001);
59+
assertEquals(1.0, manager.getAvgReadCostTimeOfEachOpsForDisk().get("0 C:"), 0.0001);
60+
assertEquals(2.0, manager.getAvgWriteCostTimeOfEachOpsForDisk().get("0 C:"), 0.0001);
61+
assertEquals(1024.0, manager.getAvgSizeOfEachReadForDisk().get("0 C:"), 0.0001);
62+
assertEquals(2048.0, manager.getAvgSizeOfEachWriteForDisk().get("0 C:"), 0.0001);
63+
}
64+
65+
@Test
66+
public void testPowerShellFailureSkipsFollowingQueryDuringBackoff() {
67+
AtomicInteger executeCount = new AtomicInteger();
68+
WindowsDiskMetricsManager manager =
69+
new WindowsDiskMetricsManager(
70+
"123",
71+
command -> {
72+
executeCount.incrementAndGet();
73+
throw new IOException("CreateProcess error=5");
74+
});
75+
76+
assertTrue(manager.getDiskIds().isEmpty());
77+
assertEquals(1, executeCount.get());
78+
}
79+
}

0 commit comments

Comments
 (0)