{"record":{"id":"13bd0a51ce51d541","repo":"apple/pkl","slug":"external-reader-process-has-already-been-closed","errorCode":null,"errorMessage":"External reader process has already been closed.","messagePattern":"External reader process has already been closed\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"pkl-core/src/main/java/org/pkl/core/externalreader/ExternalReaderProcessImpl.java","lineNumber":89,"sourceCode":"            : null;\n  }\n\n  @Override\n  public ExternalModuleResolver getModuleResolver(long evaluatorId)\n      throws ExternalReaderProcessException {\n    return ExternalModuleResolver.of(getTransport(), evaluatorId);\n  }\n\n  @Override\n  public ExternalResourceResolver getResourceResolver(long evaluatorId)\n      throws ExternalReaderProcessException {\n    return ExternalResourceResolver.of(getTransport(), evaluatorId);\n  }\n\n  private MessageTransport getTransport() throws ExternalReaderProcessException {\n    synchronized (lock) {\n      if (closed) {\n        throw new IllegalStateException(\"External reader process has already been closed.\");\n      }\n      if (process != null) {\n        if (!process.isAlive()) {\n          throw new ExternalReaderProcessException(\n              ErrorMessages.create(\"externalReaderAlreadyTerminated\"));\n        }\n\n        assert transport != null;\n        return transport;\n      }\n\n      // This relies on Java/OS behavior around PATH resolution, absolute/relative paths, etc.\n      var command = new ArrayList<String>();\n      command.add(spec.executable());\n      if (spec.arguments() != null) {\n        command.addAll(spec.arguments());\n      }\n","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/apple/pkl/blob/f3efcbfc9b60d30053b0536d664948d7aa1b8673/pkl-core/src/main/java/org/pkl/core/externalreader/ExternalReaderProcessImpl.java#L71-L107","documentation":"ExternalReaderProcessImpl.getTransport hands out the message transport for an external reader subprocess, but only while the reader is still open. If close() was already called, calling getTransport (via getModuleResolver/getResourceResolver/spec accessors) throws this IllegalStateException, signalling a lifecycle violation: use-after-close of the reader process.","triggerScenarios":"Calling getModuleResolver(), getResourceResolver(), getModuleReaderSpec(), getResourceReaderSpec(), or anything else that reaches getTransport() after ExternalReaderProcess.close() has completed.","commonSituations":"Evaluator shutdown racing with module/resource loading, closing readers in a finally block while another thread still resolves imports, double-close followed by reuse.","solutions":["Reorder code so all resolver/spec usage happens before close()","Check your shutdown sequence: close the external reader only after all module and resource resolution is done","Guard each access with an isOpen/closed check in the calling code","Avoid sharing the reader across threads without synchronization on its lifecycle"],"exampleFix":"// before\nresolver.close();\nvar moduleResolver = reader.getModuleResolver(); // throws\n// after\nvar moduleResolver = reader.getModuleResolver();\n// ...use moduleResolver...\nresolver.close();","handlingStrategy":"type-guard","validationCode":"if (externalReader.isClosed()) {\n  throw new IllegalStateException(\"Cannot resolve modules: external reader already closed\");\n}\nvar moduleResolver = externalReader.getModuleResolver();","typeGuard":"boolean readerUsable(ExternalReaderProcess r) {\n  return r != null && !r.isClosed(); // consult the public lifecycle API before use\n}","tryCatchPattern":"try {\n  var resolver = reader.getResourceResolver();\n} catch (IllegalStateException e) {\n  if (e.getMessage().contains(\"already been closed\")) {\n    // recreate the reader or skip work scheduled after shutdown\n  }\n}","preventionTips":["Enforce a strict lifecycle: acquire resolvers first, close the reader last","Use try-with-resources or a shutdown hook ordering that closes readers after evaluation completes","Avoid sharing ExternalReaderProcess across threads without coordination","Never reuse a closed reader; create a new one instead"],"tags":["pkl","lifecycle","state","process"],"backgroundTag":"invalid-state-transition","analyzedSha":"f3efcbfc9b60d30053b0536d664948d7aa1b8673","analyzedAt":"2026-09-08T13:10:45.570Z","contentChangedAt":"2026-09-08T13:10:45.570Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}