apache/pulsar · error · IOException

Unable to delete ${file}

Error message

Unable to delete ${file}

What it means

FileUtils.deleteFile(File, recurse) deletes a file or directory tree; after recursively deleting children it calls the retrying internal deleteFile(file, 5), and if that still fails it throws IOException 'Unable to delete <abs path>'. Deletion typically fails because something still holds the file open (unlink returns EBUSY/EPERM) or the parent directory lacks write permission.

Source

Thrown at pulsar-common/src/main/java/org/apache/pulsar/common/nar/FileUtils.java:191

     *
     * @param files to delete
     * @param recurse will recurse
     * @throws IOException if issues deleting files
     */
    public static void deleteFiles(final Collection<File> files, final boolean recurse) throws IOException {
        for (final File file : files) {
            FileUtils.deleteFile(file, recurse);
        }
    }

    public static void deleteFile(final File file, final boolean recurse) throws IOException {
        final File[] list = file.listFiles();
        if (file.isDirectory() && recurse && list != null) {
            FileUtils.deleteFiles(Arrays.asList(list), recurse);
        }
        //now delete the file itself regardless of whether it is plain file or a directory
        if (!FileUtils.deleteFile(file, 5)) {
            throw new IOException("Unable to delete " + file.getAbsolutePath());
        }
    }

    public static void sleepQuietly(final long millis) {
        try {
            Thread.sleep(millis);
        } catch (final InterruptedException ex) {
            /* do nothing */
        }
    }

    public static boolean mayBeANarArchive(File jarFile) {
        try (ZipFile zipFile = new ZipFile(jarFile);) {
            ZipEntry entry = zipFile.getEntry("META-INF/bundled-dependencies");
            if (entry == null || !entry.isDirectory()) {
                log.info().attr("jarFile", jarFile)
                        .log("Jar file does not contain META-INF/bundled-dependencies,"
                                + " it is not a NAR file");

View on GitHub (pinned to 820761864e)

Solutions

  1. Find and stop the process holding the file open (lsof | grep <path> on Linux; handle.exe on Windows).
  2. Ensure the parent directory is writable (chmod/chown).
  3. Pass recurse=true for non-empty directories.
  4. Retry after the holder releases the lock — the internal code already retries 5 times.

Example fix

// before
FileUtils.deleteFile(new File("/tmp/nar/my.nar-unpacked")); // non-empty dir
// after
FileUtils.deleteFile(new File("/tmp/nar/my.nar-unpacked"), true); // recursive
Defensive patterns

Strategy: retry

Validate before calling

if (file.exists() && !file.getParentFile().canWrite()) {
    throw new IllegalStateException("Parent not writable, delete will fail: " + file.getParent());
}

Try / catch

try {
    FileUtils.deleteFile(file, true);
} catch (IOException e) {
    if (e.getMessage().startsWith("Unable to delete")) {
        // commonly a file lock: schedule retry after holders release
        log.warn("Delete failed for {}; likely locked by another process", file, e);
        return;
    }
    throw e;
}

Prevention

When it happens

Trigger: deleteFile called on a path that is locked/open by another process, is a non-empty directory without recurse=true, or whose parent directory is not writable by the process user.

Common situations: Windows file locks (antivirus/indexers holding NAR jars), a running JVM keeping an unpacked jar mapped, deleting NAR unpack dirs while the broker still has them loaded, or root-owned leftovers.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/117e80b6b866904e. Report an issue: GitHub.