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

  1. Call start() exactly once per LocalRunner instance
  2. Create a new LocalRunner instance instead of restarting the old one
  3. Ensure close()/stop is invoked before any further lifecycle use, or guard with your own started flag
  4. 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

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


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/55d13468ad6b1e58. Report an issue: GitHub.