Skip to content

Commit

Permalink
Common temporary location manager for profiling product
Browse files Browse the repository at this point in the history
  • Loading branch information
jbachorik committed Nov 18, 2024
1 parent f3f2b15 commit d2479b5
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
import static com.datadog.profiling.controller.ProfilingSupport.*;
import static com.datadog.profiling.controller.ProfilingSupport.isObjectCountParallelized;
import static datadog.trace.api.Platform.isJavaVersionAtLeast;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_DEBUG_CLEANUP_REPO;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_DEBUG_CLEANUP_REPO_DEFAULT;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_HEAP_HISTOGRAM_ENABLED;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_HEAP_HISTOGRAM_ENABLED_DEFAULT;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_HEAP_HISTOGRAM_MODE;
Expand All @@ -33,6 +31,7 @@
import com.datadog.profiling.controller.ConfigurationException;
import com.datadog.profiling.controller.Controller;
import com.datadog.profiling.controller.ControllerContext;
import com.datadog.profiling.controller.TempLocationManager;
import com.datadog.profiling.controller.jfr.JFRAccess;
import com.datadog.profiling.controller.jfr.JfpUtils;
import com.datadog.profiling.controller.openjdk.events.AvailableProcessorCoresEvent;
Expand All @@ -42,19 +41,13 @@
import datadog.trace.bootstrap.config.provider.ConfigProvider;
import datadog.trace.bootstrap.instrumentation.jfr.backpressure.BackpressureProfiling;
import datadog.trace.bootstrap.instrumentation.jfr.exceptions.ExceptionProfiling;
import datadog.trace.util.PidHelper;
import de.thetaphi.forbiddenapis.SuppressForbidden;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -92,14 +85,8 @@ public OpenJdkController(final ConfigProvider configProvider)
// configure the JFR stackdepth before we try to load any JFR classes
int requestedStackDepth = getConfiguredStackDepth(configProvider);
this.jfrStackDepthApplied = JFRAccess.instance().setStackDepth(requestedStackDepth);
boolean shouldCleanupJfrRepository =
configProvider.getBoolean(
PROFILING_DEBUG_CLEANUP_REPO, PROFILING_DEBUG_CLEANUP_REPO_DEFAULT);
String jfrRepositoryBase = null;
if (shouldCleanupJfrRepository) {
jfrRepositoryBase = getJfrRepositoryBase(configProvider);
JFRAccess.instance().setBaseLocation(jfrRepositoryBase + "/pid_" + PidHelper.getPid());
}
String jfrRepositoryBase = getJfrRepositoryBase(configProvider);
JFRAccess.instance().setBaseLocation(jfrRepositoryBase);
// Make sure we can load JFR classes before declaring that we have successfully created
// factory and can use it.
Class.forName("jdk.jfr.Recording");
Expand All @@ -112,10 +99,6 @@ public OpenJdkController(final ConfigProvider configProvider)
Map<String, String> recordingSettings;

try {
if (shouldCleanupJfrRepository) {
cleanupJfrRepositories(Paths.get(jfrRepositoryBase));
}

recordingSettings =
JfpUtils.readNamedJfpResource(
ultraMinimal ? JfpUtils.SAFEPOINTS_JFP : JfpUtils.DEFAULT_JFP);
Expand Down Expand Up @@ -270,21 +253,27 @@ && isEventEnabled(recordingSettings, "jdk.NativeMethodSample")) {
}

private static String getJfrRepositoryBase(ConfigProvider configProvider) {
return configProvider.getString(
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE,
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE_DEFAULT);
}

private static void cleanupJfrRepositories(Path repositoryBase) {
try {
Files.walkFileTree(repositoryBase, new JfrCleanupVisitor(repositoryBase));
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.warn("Unable to cleanup old JFR repositories", e);
} else {
log.warn("Unable to cleanup old JFR repositories");
String legacy =
configProvider.getString(
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE,
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE_DEFAULT);
if (!legacy.equals(ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE_DEFAULT)) {
log.warn(
"The configuration key {} is deprecated. Please use {} instead.",
ProfilingConfig.PROFILING_JFR_REPOSITORY_BASE,
ProfilingConfig.PROFILING_TEMP_DIR);
}
Path repositoryPath = TempLocationManager.getInstance().getTempDir().resolve("jfr");
if (!Files.exists(repositoryPath)) {
try {
Files.createDirectories(repositoryPath);
} catch (IOException e) {
log.error("Failed to create JFR repository directory: {}", repositoryPath, e);
throw new IllegalStateException(
"Failed to create JFR repository directory: " + repositoryPath, e);
}
}
return repositoryPath.toString();
}

int getMaxSize() {
Expand Down Expand Up @@ -331,58 +320,4 @@ private int getConfiguredStackDepth(ConfigProvider configProvider) {
return configProvider.getInteger(
ProfilingConfig.PROFILING_STACKDEPTH, ProfilingConfig.PROFILING_STACKDEPTH_DEFAULT);
}

private static class JfrCleanupVisitor implements FileVisitor<Path> {
private boolean shouldClean = false;

private final Path root;
private final Set<String> pidSet = PidHelper.getJavaPids();

JfrCleanupVisitor(Path root) {
this.root = root;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
if (dir.equals(root)) {
return FileVisitResult.CONTINUE;
}
String fileName = dir.getFileName().toString();
// the JFR repository directories are under <basedir>/pid_<pid>
String pid = fileName.startsWith("pid_") ? fileName.substring(4) : null;
shouldClean |= pid != null && !pidSet.contains(pid);
if (shouldClean) {
log.debug("Cleaning JFR repository under {}", dir);
}
return shouldClean ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().toLowerCase().endsWith(".jfr")) {
Files.delete(file);
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
if (log.isDebugEnabled() && file.toString().toLowerCase().endsWith(".jfr")) {
log.debug("Failed to delete file {}", file, exc);
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (shouldClean) {
Files.delete(dir);
String fileName = dir.getFileName().toString();
// reset the flag only if we are done cleaning the top-level directory
shouldClean = !fileName.startsWith("pid_");
}
return FileVisitResult.CONTINUE;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package com.datadog.profiling.controller;

import datadog.trace.api.config.ProfilingConfig;
import datadog.trace.bootstrap.config.provider.ConfigProvider;
import datadog.trace.util.PidHelper;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A manager class for temporary locations used by the profiling product. The temporary location is
* keyed by the process ID and allows for cleanup of orphaned temporary files on startup by querying
* the list of running Java processes and cleaning up any temporary locations that do not correspond
* to a running Java process. Also, the temporary location is cleaned up on shutdown.
*/
public final class TempLocationManager {
private static final Logger log = LoggerFactory.getLogger(TempLocationManager.class);

private static final class SingletonHolder {
private static final TempLocationManager INSTANCE = new TempLocationManager();
}

private class CleanupVisitor implements FileVisitor<Path> {
private boolean shouldClean = false;

private final Set<String> pidSet = PidHelper.getJavaPids();

private final boolean cleanSelf;

CleanupVisitor(boolean cleanSelf) {
this.cleanSelf = cleanSelf;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
if (dir.equals(baseTempDir)) {
return FileVisitResult.CONTINUE;
}
String fileName = dir.getFileName().toString();
// the JFR repository directories are under <basedir>/pid_<pid>
String pid = fileName.startsWith("pid_") ? fileName.substring(4) : null;
boolean isSelfPid = pid != null && pid.equals(PidHelper.getPid());
shouldClean |= (cleanSelf && isSelfPid) || (!cleanSelf && !pidSet.contains(pid));
if (shouldClean) {
log.debug("Cleaning temporary location {}", dir);
}
return shouldClean ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
if (log.isDebugEnabled()) {
log.debug("Failed to delete file {}", file, exc);
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (shouldClean) {
Files.delete(dir);
String fileName = dir.getFileName().toString();
// reset the flag only if we are done cleaning the top-level directory
shouldClean = !fileName.startsWith("pid_");
}
return FileVisitResult.CONTINUE;
}
}

private final Path baseTempDir;
private final Path tempDir;

private final CompletableFuture<Void> cleanupTask;

/**
* Get the singleton instance of the TempLocationManager. It will run the cleanup task in the
* background.
*
* @return the singleton instance of the TempLocationManager
*/
public static TempLocationManager getInstance() {
return getInstance(false);
}

/**
* Get the singleton instance of the TempLocationManager.
*
* @param waitForCleanup if true, wait for the cleanup task to finish before returning
* @return the singleton instance of the TempLocationManager
*/
static TempLocationManager getInstance(boolean waitForCleanup) {
TempLocationManager instance = SingletonHolder.INSTANCE;
if (waitForCleanup) {
instance.waitForCleanup();
}
return instance;
}

private TempLocationManager() {
this(ConfigProvider.getInstance());
}

TempLocationManager(ConfigProvider configProvider) {
Path configuredTempDir =
Paths.get(
configProvider.getString(
ProfilingConfig.PROFILING_TEMP_DIR, ProfilingConfig.PROFILING_TEMP_DIR_DEFAULT));
if (!Files.exists(configuredTempDir)) {
log.warn(
"Base temp directory, as defined in '"
+ ProfilingConfig.PROFILING_TEMP_DIR
+ "' does not exist: "
+ configuredTempDir);
throw new IllegalStateException(
"Base temp directory, as defined in '"
+ ProfilingConfig.PROFILING_TEMP_DIR
+ "' does not exist: "
+ configuredTempDir);
}

String pid = PidHelper.getPid();

baseTempDir = configuredTempDir.resolve("ddprof");
tempDir = baseTempDir.resolve("pid_" + pid);
cleanupTask = CompletableFuture.runAsync(() -> cleanup(false));

Runtime.getRuntime()
.addShutdownHook(
new Thread(
() -> {
try {
cleanupTask.join();
} finally {
cleanup(true);
}
},
"Temp Location Manager Cleanup"));
}

/**
* Get the temporary directory for the current process.
*
* @return the temporary directory for the current process
*/
public Path getTempDir() {
return getTempDir(true);
}

/**
* Get the temporary directory for the current process.
*
* @param create if true, create the directory if it does not exist
* @return the temporary directory for the current process
* @throws IllegalStateException if the directory could not be created
*/
public Path getTempDir(boolean create) {
if (create && !Files.exists(tempDir)) {
try {
Files.createDirectories(
tempDir,
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
} catch (Exception e) {
log.warn("Failed to create temp directory: {}", tempDir, e);
throw new IllegalStateException("Failed to create temp directory: " + tempDir, e);
}
}
return tempDir;
}

void cleanup(boolean cleanSelf) {
try {
Files.walkFileTree(baseTempDir, new CleanupVisitor(cleanSelf));
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.warn("Unable to cleanup temp location {}", baseTempDir, e);
} else {
log.warn("Unable to cleanup temp location {}", baseTempDir);
}
}
}

private void waitForCleanup() {
cleanupTask.join();
}
}
Loading

0 comments on commit d2479b5

Please sign in to comment.