TypeScriptWorkerPool.java
package org.egothor.methodatlas.discovery.typescript;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.egothor.methodatlas.util.WorkerCircuitBreaker;
import org.egothor.methodatlas.util.WorkerPool;
/**
* Manages a pool of long-lived Node.js worker processes for TypeScript file
* scanning.
*
* <p>
* The generic lifecycle (lazy on-demand creation, the idle-worker queue,
* circuit-breaker-tracked restarts, and shutdown-hook teardown) is provided by
* {@link WorkerPool}. This subclass supplies only the Node.js-specific worker
* creation and termination and a strongly-typed {@link #scan} entry point.
* The per-request creation context is the scan root {@link Path}, which is
* forwarded to {@link TypeScriptWorker#start(Path)} for filesystem sandboxing
* when a fresh worker is started.
* </p>
*
* @see WorkerPool
* @see TypeScriptWorker
*/
final class TypeScriptWorkerPool extends WorkerPool<TypeScriptWorker, Path> {
private static final Logger LOG = Logger.getLogger(TypeScriptWorkerPool.class.getName());
private final Path bundlePath;
private final NodeEnvironment nodeEnv;
private final long workerTimeoutMillis;
/**
* Creates a worker pool. No workers are started at construction time.
*
* @param bundlePath path to the verified bundle JS file
* @param nodeEnv Node.js environment information
* @param poolSize maximum number of concurrent workers; must be positive
* @param workerTimeoutMillis per-request timeout in milliseconds
* @param circuitBreaker restart-limit tracker shared with this pool
*/
/* default */ TypeScriptWorkerPool(Path bundlePath, NodeEnvironment nodeEnv,
int poolSize, long workerTimeoutMillis, WorkerCircuitBreaker circuitBreaker) {
super("TypeScript", "ts-worker-pool-shutdown", poolSize, circuitBreaker);
this.bundlePath = bundlePath;
this.nodeEnv = nodeEnv;
this.workerTimeoutMillis = workerTimeoutMillis;
}
@Override
protected TypeScriptWorker createWorker(int index, Path allowedRoot) throws IOException {
TypeScriptWorker worker = new TypeScriptWorker(bundlePath, nodeEnv, workerTimeoutMillis, index);
worker.start(allowedRoot);
return worker;
}
@Override
protected void kill(TypeScriptWorker worker, String reason) {
worker.kill(reason);
}
/**
* Scans one TypeScript file by delegating to a pooled worker.
*
* @param filePath absolute path of the file to scan
* @param functionNames test-function call names
* @param allowedRoot scan root for permission sandboxing; forwarded to a
* newly started worker; may be {@code null}
* @return list of discovered method descriptors; empty when the circuit is
* open or a non-recoverable error occurs
* @throws IOException if borrowing a worker fails with a hard I/O error
*/
/* default */ List<TypeScriptWorker.MethodDescriptor> scan(
Path filePath, List<String> functionNames, Path allowedRoot) throws IOException {
if (isCircuitOpen()) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.warning("TypeScript worker pool circuit breaker is open — skipping " + filePath);
}
return List.of();
}
TypeScriptWorker worker = borrow(allowedRoot);
if (worker == null) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.warning("No TypeScript worker available within " + borrowTimeoutMillis()
+ " ms — skipping " + filePath);
}
return List.of();
}
try {
List<TypeScriptWorker.MethodDescriptor> result =
worker.scan(filePath, functionNames);
returnWorker(worker);
return result;
} catch (TypeScriptWorker.WorkerException e) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING,
"TypeScript worker error scanning " + filePath + " — "
+ "killing and replacing worker: " + e.getMessage(), e);
}
worker.kill("scan error: " + e.getMessage());
replaceWorker(allowedRoot);
return List.of();
}
}
}