{"record":{"id":"eef2eda114697a08","repo":"apache/pulsar","slug":"class-factoryclass-does-not-implement-interface","errorCode":null,"errorMessage":"Class ${factoryClass} does not implement interface ${interfaceName}","messagePattern":"Class (.+?) does not implement interface (.+?)","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"managed-ledger/src/main/java/org/apache/bookkeeper/mledger/offload/OffloaderUtils.java","lineNumber":80,"sourceCode":"\n        OffloaderDefinition conf = ObjectMapperFactory.getYamlMapper().getObjectMapper()\n            .readValue(configStr, OffloaderDefinition.class);\n        if (StringUtils.isEmpty(conf.getOffloaderFactoryClass())) {\n            throw new IOException(\n                String.format(\"The '%s' offloader does not provide an offloader factory implementation\",\n                    conf.getName()));\n        }\n\n        try {\n            // Try to load offloader factory class and check it implements Offloader interface\n            Class<?> factoryClass = ncl.loadClass(conf.getOffloaderFactoryClass());\n            CompletableFuture<LedgerOffloaderFactory<?>> loadFuture = new CompletableFuture<>();\n            Thread loadingThread = new Thread(() -> {\n                Thread.currentThread().setContextClassLoader(ncl);\n                try {\n                    Object offloader = factoryClass.getDeclaredConstructor().newInstance();\n                    if (!(offloader instanceof LedgerOffloaderFactory)) {\n                        throw new IOException(\"Class \" + conf.getOffloaderFactoryClass() + \" does not implement \"\n                                + \"interface \" + LedgerOffloaderFactory.class.getName());\n                    }\n                    loadFuture.complete((LedgerOffloaderFactory<?>) offloader);\n                } catch (Throwable t) {\n                    loadFuture.completeExceptionally(t);\n                }\n            }, \"load-factory-\" + factoryClass);\n            try {\n                loadingThread.start();\n                return Pair.of(ncl, loadFuture.get());\n            } finally {\n                loadingThread.join();\n            }\n        } catch (Throwable t) {\n            rethrowIOException(t);\n        }\n        return null;\n    }","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/managed-ledger/src/main/java/org/apache/bookkeeper/mledger/offload/OffloaderUtils.java#L62-L98","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"// before\noffloaderFactoryClass: org.example.MyS3Offloader  // implements LedgerOffloader\n// after\noffloaderFactoryClass: org.example.MyS3OffloaderFactory // implements LedgerOffloaderFactory","handlingStrategy":"validation","validationCode":"// verify the configured class implements the factory SPI under the plugin classloader\nClass<?> c = ncl.loadClass(conf.getOffloaderFactoryClass());\nif (!LedgerOffloaderFactory.class.isAssignableFrom(c)) {\n    throw new IllegalStateException(conf.getOffloaderFactoryClass() + \" is not a LedgerOffloaderFactory\");\n}","typeGuard":"static boolean isOffloaderFactory(Class<?> c) {\n    return LedgerOffloaderFactory.class.isAssignableFrom(c);\n}","tryCatchPattern":"try {\n    OffloaderUtils.getOffloaderFactory(ncl, conf, nrConf);\n} catch (IOException e) {\n    log.error(\"Offloader factory class wrong type or classloader mismatch\", e);\n}","preventionTips":["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"],"tags":["offload","plugin","classloader","tiered-storage","spi"],"backgroundTag":"class-does-not-implement-interface","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}