elastic/elasticsearch · error · UncheckedIOException

Unable to create reaper JAR output directory {}

Error message

Unable to create reaper JAR output directory {}

What it means

ReaperService.locateReaperJar() throws UncheckedIOException (non-internal mode) when Files.createDirectories(jarPath.getParent()) fails while preparing buildDir/reaper/ to receive the copied reaper.jar. The parent directory is <buildDir>/reaper/; the failure wraps the original IOException with the target path so the filesystem cause (permissions, quota, read-only mount) is identifiable.

Source

Thrown at build-tools/src/main/java/org/elasticsearch/gradle/ReaperService.java:146

        if (getParameters().getInternal()) {
            // when running inside the Elasticsearch build just pull find the jar in the runtime classpath
            URL main = this.getClass().getClassLoader().getResource(REAPER_CLASS);
            String mainPath = main.getFile();
            Matcher matcher = REAPER_JAR_PATH_PATTERN.matcher(mainPath);

            if (matcher.matches()) {
                String path = matcher.group(1);
                return Path.of(OS.<String>conditional().onWindows(() -> path.substring(1)).onUnix(() -> path).supply());
            } else {
                throw new RuntimeException("Unable to locate " + REAPER_CLASS + " on build classpath.");
            }
        } else {
            // copy the reaper jar
            Path jarPath = getParameters().getBuildDir().get().getAsFile().toPath().resolve("reaper").resolve("reaper.jar");
            try {
                Files.createDirectories(jarPath.getParent());
            } catch (IOException e) {
                throw new UncheckedIOException("Unable to create reaper JAR output directory " + jarPath.getParent(), e);
            }

            try (
                OutputStream out = Files.newOutputStream(jarPath);
                InputStream jarInput = this.getClass().getResourceAsStream("/META-INF/reaper.jar");
            ) {
                logger.info("Copying reaper.jar...");
                jarInput.transferTo(out);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }

            return jarPath;
        }
    }

    private void ensureReaperAlive() {
        if (reaperProcess.isAlive() == false) {

View on GitHub (pinned to db6a809a66)

Solutions

  1. Check the wrapped IOException's message for the filesystem reason (Permission denied, No space left on device, Read-only file system).
  2. Ensure buildDir is writable by the Gradle process and not on a read-only mount.
  3. Free disk space or raise quota if the volume is full.
  4. Remove a conflicting non-directory file at <buildDir>/reaper if one exists.
  5. If running in internal mode is appropriate, switch Params.internal to true to bypass the copy entirely.
Defensive patterns

Strategy: validation

Validate before calling

Path reaperDir = buildDir.toPath().resolve("reaper");
if (!Files.isWritable(buildDir.toPath())) {
    throw new IllegalStateException("buildDir is not writable: " + buildDir);
}

Try / catch

try { Files.createDirectories(reaperDir); } catch (IOException e) { throw new UncheckedIOException("Cannot create " + reaperDir, e); }

Prevention

When it happens

Trigger: Non-internal mode (Params.internal = false): the service tries to materialize buildDir/reaper/reaper.jar from the bundled /META-INF/reaper.jar resource, and creating buildDir/reaper/ fails.

Common situations: Read-only build directory (CI with a locked workspace, container with a read-only mount); insufficient permissions on the build dir; disk full or quota exceeded; the buildDir was deleted concurrently; parent path conflicts with an existing file (not a directory).

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/4c99a0f1ad5a2732. Report an issue: GitHub.