apache/pulsar · error · IOException

Class ${factoryClass} does not implement interface ${interfa

Error message

Class ${factoryClass} does not implement interface ${interfaceName}

What it means

OffloaderUtils.getOffloaderFactory() reflectively instantiates the configured offloaderFactoryClass on a NarClassLoader and verifies it implements LedgerOffloaderFactory. If the instantiated object does not implement that interface, an IOException is thrown. This is a plugin contract violation — the class loads but is the wrong type.

Source

Thrown at managed-ledger/src/main/java/org/apache/bookkeeper/mledger/offload/OffloaderUtils.java:80

        OffloaderDefinition conf = ObjectMapperFactory.getYamlMapper().getObjectMapper()
            .readValue(configStr, OffloaderDefinition.class);
        if (StringUtils.isEmpty(conf.getOffloaderFactoryClass())) {
            throw new IOException(
                String.format("The '%s' offloader does not provide an offloader factory implementation",
                    conf.getName()));
        }

        try {
            // Try to load offloader factory class and check it implements Offloader interface
            Class<?> factoryClass = ncl.loadClass(conf.getOffloaderFactoryClass());
            CompletableFuture<LedgerOffloaderFactory<?>> loadFuture = new CompletableFuture<>();
            Thread loadingThread = new Thread(() -> {
                Thread.currentThread().setContextClassLoader(ncl);
                try {
                    Object offloader = factoryClass.getDeclaredConstructor().newInstance();
                    if (!(offloader instanceof LedgerOffloaderFactory)) {
                        throw new IOException("Class " + conf.getOffloaderFactoryClass() + " does not implement "
                                + "interface " + LedgerOffloaderFactory.class.getName());
                    }
                    loadFuture.complete((LedgerOffloaderFactory<?>) offloader);
                } catch (Throwable t) {
                    loadFuture.completeExceptionally(t);
                }
            }, "load-factory-" + factoryClass);
            try {
                loadingThread.start();
                return Pair.of(ncl, loadFuture.get());
            } finally {
                loadingThread.join();
            }
        } catch (Throwable t) {
            rethrowIOException(t);
        }
        return null;
    }

View on GitHub (pinned to 820761864e)

Solutions

  1. Set offloaderFactoryClass to a class that implements LedgerOffloaderFactory (not LedgerOffloader)
  2. Rebuild the offloader NAR against the broker's managed-ledger API version so interfaces come from the parent classloader only
  3. Do not bundle broker/manged-ledger classes inside the NAR — mark them as provided so instanceof resolves against the broker's classes
  4. Check the configured class name for typos (driver class vs factory class)

Example fix

// before
offloaderFactoryClass: org.example.MyS3Offloader  // implements LedgerOffloader
// after
offloaderFactoryClass: org.example.MyS3OffloaderFactory // implements LedgerOffloaderFactory
Defensive patterns

Strategy: validation

Validate before calling

// verify the configured class implements the factory SPI under the plugin classloader
Class<?> c = ncl.loadClass(conf.getOffloaderFactoryClass());
if (!LedgerOffloaderFactory.class.isAssignableFrom(c)) {
    throw new IllegalStateException(conf.getOffloaderFactoryClass() + " is not a LedgerOffloaderFactory");
}

Type guard

static boolean isOffloaderFactory(Class<?> c) {
    return LedgerOffloaderFactory.class.isAssignableFrom(c);
}

Try / catch

try {
    OffloaderUtils.getOffloaderFactory(ncl, conf, nrConf);
} catch (IOException e) {
    log.error("Offloader factory class wrong type or classloader mismatch", e);
}

Prevention

When it happens

Trigger: Pointing offloaderFactoryClass at a class that implements LedgerOffloader (the driver) rather than LedgerOffloaderFactory; classloader split where the plugin's LedgerOffloaderFactory is loaded by a different classloader than the broker's, making instanceof fail; typos in configuration pointing at the wrong class.

Common situations: Custom offloaders migrated incompletely from the pre-factory API; packaging the offloader classes both in the NAR and the broker classpath causing duplicate-class/classloader mismatch; misconfigured conf pointing at the driver class instead of the factory class.

Related errors


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