Skip to content
41 changes: 34 additions & 7 deletions clients/src/main/java/org/apache/kafka/common/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -736,29 +736,56 @@ public static Properties mkProperties(final Map<String, String> properties) {
/**
* Recursively delete the given file/directory and any subfiles (if any exist)
*
* @param file The root file at which to begin deleting
* @param rootFile The root file at which to begin deleting
*/
public static void delete(final File file) throws IOException {
if (file == null)
public static void delete(final File rootFile) throws IOException {
delete(rootFile, Collections.emptyList());
}

/**
* Recursively delete the subfiles (if any exist) of the passed in root file that are not included
* in the list to keep
*
* @param rootFile The root file at which to begin deleting
* @param filesToKeep The subfiles to keep (note that if a subfile is to be kept, so are all its parent
* files in its pat)h; if empty we would also delete the root file
*/
public static void delete(final File rootFile, final List<File> filesToKeep) throws IOException {
if (rootFile == null)
return;
Files.walkFileTree(file.toPath(), new SimpleFileVisitor<Path>() {
Files.walkFileTree(rootFile.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFileFailed(Path path, IOException exc) throws IOException {
// If the root path did not exist, ignore the error; otherwise throw it.
if (exc instanceof NoSuchFileException && path.toFile().equals(file))
if (exc instanceof NoSuchFileException && path.toFile().equals(rootFile))
return FileVisitResult.TERMINATE;
throw exc;
}

@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
Files.delete(path);
if (!filesToKeep.contains(path.toFile())) {
Files.delete(path);
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException {
Files.delete(path);
// KAFKA-8999: if there's an exception thrown previously already, we should throw it
if (exc != null) {
throw exc;
}

if (rootFile.toPath().equals(path)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit tricky: for the root file, we only consider deleting it if there's no specified skipping sub-files; otherwise we never try to delete since it would doom with DirectoryNotEmpty.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to avoid the unnecessary conversions in this method (rootFile.toPath, path.toFile). We can do some work at the start of the method to improve efficiency.

// only delete the parent directory if there's nothing to keep
if (filesToKeep.isEmpty()) {
Files.delete(path);
}
} else if (!filesToKeep.contains(path.toFile())) {
Files.delete(path);
}

return FileVisitResult.CONTINUE;
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -716,11 +716,11 @@ private KafkaStreams(final InternalTopologyBuilder internalTopologyBuilder,
}
final ProcessorTopology globalTaskTopology = internalTopologyBuilder.buildGlobalStateTopology();
final long cacheSizePerThread = totalCacheSize / (threads.length + (globalTaskTopology == null ? 0 : 1));
final boolean createStateDirectory = taskTopology.hasPersistentLocalStore() ||
final boolean hasPersistentStores = taskTopology.hasPersistentLocalStore() ||
(globalTaskTopology != null && globalTaskTopology.hasPersistentGlobalStore());

try {
stateDirectory = new StateDirectory(config, time, createStateDirectory);
stateDirectory = new StateDirectory(config, time, hasPersistentStores);
} catch (final ProcessorStateException fatal) {
throw new StreamsException(fatal);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.HashMap;
import java.util.regex.Pattern;

Expand All @@ -50,11 +51,12 @@ public class StateDirectory {
static final String LOCK_FILE_NAME = ".lock";
private static final Logger log = LoggerFactory.getLogger(StateDirectory.class);

private final Time time;
private final String appId;
private final File stateDir;
private final boolean createStateDirectory;
private final boolean hasPersistentStores;
private final HashMap<TaskId, FileChannel> channels = new HashMap<>();
private final HashMap<TaskId, LockAndOwner> locks = new HashMap<>();
private final Time time;

private FileChannel globalStateChannel;
private FileLock globalStateLock;
Expand All @@ -72,22 +74,27 @@ private static class LockAndOwner {
/**
* Ensures that the state base directory as well as the application's sub-directory are created.
*
* @param config streams application configuration to read the root state directory path
* @param time system timer used to execute periodic cleanup procedure
* @param hasPersistentStores only when the application's topology does have stores persisted on local file
* system, we would go ahead and auto-create the corresponding application / task / store
* directories whenever necessary; otherwise no directories would be created.
*
* @throws ProcessorStateException if the base state directory or application state directory does not exist
* and could not be created when createStateDirectory is enabled.
* and could not be created when hasPersistentStores is enabled.
*/
public StateDirectory(final StreamsConfig config,
final Time time,
final boolean createStateDirectory) {
public StateDirectory(final StreamsConfig config, final Time time, final boolean hasPersistentStores) {
this.time = time;
this.createStateDirectory = createStateDirectory;
this.hasPersistentStores = hasPersistentStores;
this.appId = config.getString(StreamsConfig.APPLICATION_ID_CONFIG);
final String stateDirName = config.getString(StreamsConfig.STATE_DIR_CONFIG);
final File baseDir = new File(stateDirName);
if (this.createStateDirectory && !baseDir.exists() && !baseDir.mkdirs()) {
if (this.hasPersistentStores && !baseDir.exists() && !baseDir.mkdirs()) {
throw new ProcessorStateException(
String.format("base state directory [%s] doesn't exist and couldn't be created", stateDirName));
}
stateDir = new File(baseDir, config.getString(StreamsConfig.APPLICATION_ID_CONFIG));
if (this.createStateDirectory && !stateDir.exists() && !stateDir.mkdir()) {
stateDir = new File(baseDir, appId);
if (this.hasPersistentStores && !stateDir.exists() && !stateDir.mkdir()) {
throw new ProcessorStateException(
String.format("state directory [%s] doesn't exist and couldn't be created", stateDir.getPath()));
}
Expand All @@ -100,7 +107,7 @@ public StateDirectory(final StreamsConfig config,
*/
public File directoryForTask(final TaskId taskId) {
final File taskDir = new File(stateDir, taskId.toString());
if (createStateDirectory && !taskDir.exists() && !taskDir.mkdir()) {
if (hasPersistentStores && !taskDir.exists() && !taskDir.mkdir()) {
throw new ProcessorStateException(
String.format("task directory [%s] doesn't exist and couldn't be created", taskDir.getPath()));
}
Expand All @@ -113,9 +120,13 @@ public File directoryForTask(final TaskId taskId) {
boolean directoryForTaskIsEmpty(final TaskId taskId) {
final File taskDir = directoryForTask(taskId);

return taskDirEmpty(taskDir);
}

private boolean taskDirEmpty(final File taskDir) {
final File[] storeDirs = taskDir.listFiles(pathname ->
!pathname.getName().equals(LOCK_FILE_NAME) &&
!pathname.getName().equals(CHECKPOINT_FILE_NAME));
!pathname.getName().equals(CHECKPOINT_FILE_NAME));

// if the task is stateless, storeDirs would be null
return storeDirs == null || storeDirs.length == 0;
Expand All @@ -128,7 +139,7 @@ boolean directoryForTaskIsEmpty(final TaskId taskId) {
*/
File globalStateDir() {
final File dir = new File(stateDir, "global");
if (createStateDirectory && !dir.exists() && !dir.mkdir()) {
if (hasPersistentStores && !dir.exists() && !dir.mkdir()) {
throw new ProcessorStateException(
String.format("global state directory [%s] doesn't exist and couldn't be created", dir.getPath()));
}
Expand All @@ -141,12 +152,12 @@ private String logPrefix() {

/**
* Get the lock for the {@link TaskId}s directory if it is available
* @param taskId
* @param taskId task id
* @return true if successful
* @throws IOException
* @throws IOException if the file cannot be created or file handle cannot be grabbed, should be considered as fatal
*/
synchronized boolean lock(final TaskId taskId) throws IOException {
if (!createStateDirectory) {
if (!hasPersistentStores) {
return true;
}

Expand Down Expand Up @@ -190,7 +201,7 @@ synchronized boolean lock(final TaskId taskId) throws IOException {
}

synchronized boolean lockGlobalState() throws IOException {
if (!createStateDirectory) {
if (!hasPersistentStores) {
return true;
}

Expand Down Expand Up @@ -252,18 +263,21 @@ synchronized void unlock(final TaskId taskId) throws IOException {
}

public synchronized void clean() {
// remove task dirs
try {
cleanRemovedTasks(0, true);
} catch (final Exception e) {
// this is already logged within cleanRemovedTasks
throw new StreamsException(e);
}
// remove global dir
try {
if (stateDir.exists()) {
Utils.delete(globalStateDir().getAbsoluteFile());
}
} catch (final IOException e) {
log.error("{} Failed to delete global state directory due to an unexpected exception", logPrefix(), e);
log.error("{} Failed to delete global state directory of {} due to an unexpected exception",
appId, logPrefix(), e);
throw new StreamsException(e);
}
}
Expand All @@ -285,7 +299,7 @@ public synchronized void cleanRemovedTasks(final long cleanupDelayMs) {

private synchronized void cleanRemovedTasks(final long cleanupDelayMs,
final boolean manualUserCall) throws Exception {
final File[] taskDirs = listTaskDirectories();
final File[] taskDirs = listAllTaskDirectories();
if (taskDirs == null || taskDirs.length == 0) {
return; // nothing to do
}
Expand All @@ -294,61 +308,69 @@ private synchronized void cleanRemovedTasks(final long cleanupDelayMs,
final String dirName = taskDir.getName();
final TaskId id = TaskId.parse(dirName);
if (!locks.containsKey(id)) {
Exception exception = null;
try {
if (lock(id)) {
final long now = time.milliseconds();
final long lastModifiedMs = taskDir.lastModified();
if (now > lastModifiedMs + cleanupDelayMs || manualUserCall) {
if (!manualUserCall) {
log.info(
"{} Deleting obsolete state directory {} for task {} as {}ms has elapsed (cleanup delay is {}ms).",
logPrefix(),
dirName,
id,
now - lastModifiedMs,
cleanupDelayMs);
} else {
log.info(
"{} Deleting state directory {} for task {} as user calling cleanup.",
logPrefix(),
dirName,
id);
}
Utils.delete(taskDir);
if (now > lastModifiedMs + cleanupDelayMs) {
log.info("{} Deleting obsolete state directory {} for task {} as {}ms has elapsed (cleanup delay is {}ms).",
logPrefix(), dirName, id, now - lastModifiedMs, cleanupDelayMs);

Utils.delete(taskDir, Collections.singletonList(new File(taskDir, LOCK_FILE_NAME)));
} else if (manualUserCall) {
log.info("{} Deleting state directory {} for task {} as user calling cleanup.",
logPrefix(), dirName, id);

Utils.delete(taskDir, Collections.singletonList(new File(taskDir, LOCK_FILE_NAME)));
}
}
} catch (final OverlappingFileLockException e) {
// locked by another thread
if (manualUserCall) {
log.error("{} Failed to get the state directory lock.", logPrefix(), e);
throw e;
}
} catch (final IOException e) {
log.error("{} Failed to delete the state directory.", logPrefix(), e);
if (manualUserCall) {
throw e;
}
} catch (final OverlappingFileLockException | IOException e) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a minor code clean up.

exception = e;
} finally {
try {
unlock(id);
} catch (final IOException e) {
log.error("{} Failed to release the state directory lock.", logPrefix());

// for manual user call, stream threads are not running so it is safe to delete

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment makes me wonder why we even bother with locking at all for manual calls. If we know there’s no app running, why not just delete the whole state directory and not bother with locks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that too, and tried it out, it's just that based on manualUserCall to decide whether lock / unlock the block, we ended up with either much more code duplicates or clumsy and finer-grained if else condition, neither of which I like. On the other hand, since streams.cleanUp is usually a one-time thing compared with the periodic internal clean I think having two delete calls are okay to same some code duplication here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok!

// the whole directory
if (manualUserCall) {
throw e;
Utils.delete(taskDir);
}
} catch (final IOException e) {
exception = e;
}
}

if (exception != null && manualUserCall) {
log.error("{} Failed to release the state directory lock.", logPrefix());
throw exception;
}
}
}
}

/**
* List all of the task directories that are non-empty
* @return The list of all the non-empty local directories for stream tasks
*/
File[] listNonEmptyTaskDirectories() {
return !stateDir.exists() ? new File[0] :
stateDir.listFiles(pathname -> {
if (!pathname.isDirectory() || !PATH_NAME.matcher(pathname.getName()).matches()) {
return false;
} else {
return !taskDirEmpty(pathname);
}
});
}

/**
* List all of the task directories
* @return The list of all the existing local directories for stream tasks
*/
File[] listTaskDirectories() {
File[] listAllTaskDirectories() {
return !stateDir.exists() ? new File[0] :
stateDir.listFiles(pathname -> pathname.isDirectory() && PATH_NAME.matcher(pathname.getName()).matches());
stateDir.listFiles(pathname -> pathname.isDirectory() && PATH_NAME.matcher(pathname.getName()).matches());
}

private FileChannel getOrCreateFileChannel(final TaskId taskId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ private Set<TaskId> tasksOnLocalStorage() {

final Set<TaskId> locallyStoredTasks = new HashSet<>();

final File[] stateDirs = stateDirectory.listTaskDirectories();
final File[] stateDirs = stateDirectory.listNonEmptyTaskDirectories();
if (stateDirs != null) {
for (final File dir : stateDirs) {
try {
Expand Down
Loading