apple/pkl · error · ExternalReaderProcessException

externalReaderAlreadyTerminated

externalReaderAlreadyTerminated

Error message

externalReaderAlreadyTerminated

What it means

When the external reader process is not closed but the underlying subprocess has already died, getTransport throws ExternalReaderProcessException with code 'externalReaderAlreadyTerminated'. The transport only exists once the process is alive and started, so a dead child means the reader can no longer serve requests.

Source

Thrown at pkl-core/src/main/java/org/pkl/core/externalreader/ExternalReaderProcessImpl.java:93

  public ExternalModuleResolver getModuleResolver(long evaluatorId)
      throws ExternalReaderProcessException {
    return ExternalModuleResolver.of(getTransport(), evaluatorId);
  }

  @Override
  public ExternalResourceResolver getResourceResolver(long evaluatorId)
      throws ExternalReaderProcessException {
    return ExternalResourceResolver.of(getTransport(), evaluatorId);
  }

  private MessageTransport getTransport() throws ExternalReaderProcessException {
    synchronized (lock) {
      if (closed) {
        throw new IllegalStateException("External reader process has already been closed.");
      }
      if (process != null) {
        if (!process.isAlive()) {
          throw new ExternalReaderProcessException(
              ErrorMessages.create("externalReaderAlreadyTerminated"));
        }

        assert transport != null;
        return transport;
      }

      // This relies on Java/OS behavior around PATH resolution, absolute/relative paths, etc.
      var command = new ArrayList<String>();
      command.add(spec.executable());
      if (spec.arguments() != null) {
        command.addAll(spec.arguments());
      }

      var builder = new ProcessBuilder(command);
      var workingDir = spec.workingDir();
      if (workingDir != null) {
        builder.directory(new File(workingDir));

View on GitHub (pinned to f3efcbfc9b)

Solutions

  1. Check the reader process's stderr/exit code to find why it died and fix the root cause
  2. Verify the external reader binary/launcher is present, executable, and compatible
  3. Increase memory limits or fix signals that kill the subprocess
  4. Recreate the external reader process and retry the evaluation once the crash cause is fixed

Example fix

// ensure reader is alive before resolving
if (!processHandle.isAlive()) {
  throw new IllegalStateException("reader crashed; check stderr logs before retrying");
}
var resolvers = ExternalResourceResolver.of(reader.getTransport(), evaluatorId);
Defensive patterns

Strategy: try-catch

Validate before calling

// before using the reader, confirm the child process is alive
if (readerProcessHandle != null && !readerProcessHandle.isAlive()) {
  throw new IllegalStateException("External reader died; check its stderr and exit code");
}

Try / catch

try {
  var transport = reader.getTransport(); // via resolver accessors
} catch (ExternalReaderProcessException e) {
  if (e.getMessage().contains("externalReaderAlreadyTerminated")) {
    // inspect reader stderr/exit code, fix crash cause, then recreate the reader
  }
}

Prevention

When it happens

Trigger: The external reader subprocess crashed or was killed (OOM, signal, non-zero exit) and then the evaluator calls getTransport via getModuleResolver/getResourceResolver or spec accessors.

Common situations: Reader binary path wrong or crashing on startup, system OOM-killer terminating the child, timeout/kill of a hung reader, container shutting down the process.

Related errors


AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08). Data as JSON: /api/errors/daa586f27a796278. Report an issue: GitHub.