apache/pulsar · error · IllegalArgumentException
Pulsar Function local run already started!
Error message
Pulsar Function local run already started!
What it means
LocalRunner.start uses an AtomicBoolean 'running' with compareAndSet to guarantee a single local function run per runner instance. Calling start() on an already-started (or concurrently started) runner throws IllegalArgumentException. It is a lifecycle-guard, not an environment failure.
Source
Thrown at pulsar-functions/localrun/src/main/java/org/apache/pulsar/functions/LocalRunner.java:351
}
private static void closeClassLoaderIfneeded(UserCodeClassLoader userCodeClassLoader) {
if (userCodeClassLoader != null && userCodeClassLoader.isClassLoaderCreated()) {
if (userCodeClassLoader.getClassLoader() instanceof Closeable) {
try {
((Closeable) userCodeClassLoader.getClassLoader()).close();
} catch (IOException e) {
log.warn().exception(e).log("Error closing classloader");
}
}
}
}
public void start(boolean blocking) throws Exception {
List<RuntimeSpawner> local = new LinkedList<>();
synchronized (this) {
if (!running.compareAndSet(false, true)) {
throw new IllegalArgumentException("Pulsar Function local run already started!");
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
FunctionDetails functionDetails = null;
String userCodeFile;
String transformFunctionFile = null;
int parallelism;
if (functionConfig != null) {
FunctionConfigUtils.inferMissingArguments(functionConfig, true);
parallelism = functionConfig.getParallelism();
if (functionConfig.getRuntime() == FunctionConfig.Runtime.JAVA) {
userCodeFile = functionConfig.getJar();
userCodeClassLoader = extractClassLoader(
userCodeFile, ComponentType.FUNCTION, functionConfig.getClassName());
ValidatableFunctionPackage validatableFunctionPackage =
new LoadedFunctionPackage(getCurrentOrUserCodeClassLoader(),
FunctionDefinition.class);
functionDetails = FunctionConfigUtils.convert(
functionConfig,View on GitHub (pinned to 820761864e)
Solutions
- Call start() exactly once per LocalRunner instance
- Create a new LocalRunner instance instead of restarting the old one
- Ensure close()/stop is invoked before any further lifecycle use, or guard with your own started flag
- Synchronize or use a single thread/executor to trigger startup
Example fix
// before
runner.start();
runner.start();
// after
if (runnerStarted.compareAndSet(false, true)) {
runner.start();
} Defensive patterns
Strategy: try-catch
Validate before calling
java.util.concurrent.atomic.AtomicBoolean started = new AtomicBoolean(false);
if (!started.compareAndSet(false, true)) {
throw new IllegalStateException("runner already started");
}
runner.start(true); Try / catch
try {
runner.start(true);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("already started")) {
// ignore: runner is already running
}
} Prevention
- Start each LocalRunner exactly once
- Wrap start in your own compareAndSet guard
- Never share a LocalRunner across threads without synchronization
When it happens
Trigger: Invoking LocalRunner.start() (or start(boolean)) twice on the same LocalRunner instance, or from two threads concurrently; also when calling start after a previous start that never closed the runner.
Common situations: Test scaffolding that starts the runner in both @BeforeEach and the test body; re-running start() after close() failed; sharing a LocalRunner across threads without coordination.
Related errors
- ManagedLedgerFactory is already closed.
- Positions must not be null
- Invalid range ${range}
- ManagedLedger ${name} has already been closed
- Cannot start the service once it was stopped
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/55d13468ad6b1e58.
Report an issue: GitHub.