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
- Set offloaderFactoryClass to a class that implements LedgerOffloaderFactory (not LedgerOffloader)
- Rebuild the offloader NAR against the broker's managed-ledger API version so interfaces come from the parent classloader only
- Do not bundle broker/manged-ledger classes inside the NAR — mark them as provided so instanceof resolves against the broker's classes
- 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
- Point offloaderFactoryClass at the *Factory* class, not the driver
- Never bundle managed-ledger/broker classes inside the NAR (mark them provided)
- Rebuild plugins against the broker's API version on upgrades
- Test plugin loading in a canary broker before fleet-wide rollout
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
- The '${offloaderName}' offloader does not provide an offload
- Additional servlet instance of type ${className} doesn't imp
- Error offloading: ${lastError}
- there should not be multi ledger in a block %s %s
- Cursor %s mark-delete position %s is ahead of the last posit
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/eef2eda114697a08.
Report an issue: GitHub.