Regression in 5.9.0: CuratorCache fails with ClassCastException when the client is not a CuratorFrameworkBase
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
Research direction
Read PersistentWatcher in curator-recipes, especially start() and close(), and the existing TestPersistentWatcher.testNamespacedWatching test. Add a regression test using a delegating CuratorFramework that reproduces the CuratorCache.start() failure, then verify it passes alongside the namespaced case. Done means both wrapper and namespaced clients work without a ClassCastException or UnsupportedOperationException.
Written by the indexing model from the issue text.
Description
Summary
Since 5.9.0, PersistentWatcher.start() casts the client to org.apache.curator.framework.imps.CuratorFrameworkBase:
// This could be a namespaced facade which does not support getCuratorListenable.
((CuratorFrameworkBase) client).client().getCuratorListenable().addListener(...);
CuratorCache is built on PersistentWatcher. As a result, CuratorCache.start() throws ClassCastException whenever the CuratorFramework passed to it is not a subclass of CuratorFrameworkBase. A typical case is a decorator that implements the public CuratorFramework interface and delegates to a client created by CuratorFrameworkFactory. The same code works with 5.8.0.
Affected versions
- 5.9.0: the cast is in
PersistentWatcher.start(). - Current
master: the same cast is instart()and, after #1293, also inclose(). - 5.8.0 and earlier are not affected.
Background
The cast was introduced by #1262, the fix for #1259:
NamespaceFacade.getCuratorListenable()throwsUnsupportedOperationException.PersistentWatcherneeds theCuratorEventType.CLOSINGevent (CURATOR-729, #520).- To get it, #1262 added
CuratorFrameworkBase.client()and reaches the underlying client through it.
That fixes namespaced instances, but it assumes that every CuratorFramework is a CuratorFrameworkBase:
CuratorFrameworkis the public interface that applications and libraries program against. The requirement "An instance of CuratorFramework MUST BE an instance of CuratorFrameworkBase" is stated only in the Javadoc ofCuratorFrameworkBase, which is in theimpspackage. TheCuratorFrameworkinterface itself does not mention it.- Before 5.9.0 no recipe relied on it, so wrappers of the public interface worked with all recipes.
- Third-party code cannot meet the requirement.
CuratorFrameworkBasehas package-private abstract methods (getNamespaceImpl(),getFailedDeleteManager(),processBackgroundOperation(...),queueOperation(...)and others), andDelegatingCuratorFrameworkis package-private.
Use case
We wrap the client in a delegating CuratorFramework:
- It holds a client created by
CuratorFrameworkFactoryand forwards every call to it. - It can replace that client at runtime: it closes the old instance and builds a new one.
- Components that received the
CuratorFrameworkkeep the same reference throughout.
After the upgrade to 5.9.0, every component that uses CuratorCache on top of this wrapper fails at startup. In our case this includes Spring Integration's ZookeeperMetadataStore (spring-integration-zookeeper 6.5.x), which uses CuratorCache internally.
Any other decorator of the interface is affected in the same way, for example:
- metrics or tracing wrappers;
java.lang.reflect.Proxy-based test doubles.
Steps to reproduce
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.retry.RetryOneTime;
import org.apache.curator.test.TestingServer;
import org.junit.jupiter.api.Test;
public class CuratorCacheDelegatingClientTest {
@Test
public void testCuratorCacheWithDelegatingClient() throws Exception {
try (TestingServer server = new TestingServer();
CuratorFramework real =
CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(100))) {
// A decorator that implements the public CuratorFramework interface by delegating every call
CuratorFramework wrapper = (CuratorFramework) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class<?>[] {CuratorFramework.class}, (proxy, method, args) -> {
try {
return method.invoke(real, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
});
wrapper.start();
try (CuratorCache cache = CuratorCache.build(wrapper, "/test")) {
cache.start(); // 5.8.0: OK, 5.9.0: ClassCastException
}
}
}
}
Actual result (5.9.0)
java.lang.ClassCastException: class jdk.proxy2.$Proxy9 cannot be cast to class org.apache.curator.framework.imps.CuratorFrameworkBase (jdk.proxy2.$Proxy9 is in module jdk.proxy2 of loader 'app'; org.apache.curator.framework.imps.CuratorFrameworkBase is in unnamed module of loader 'app')
at org.apache.curator.framework.recipes.watch.PersistentWatcher.start(PersistentWatcher.java:85)
at org.apache.curator.framework.recipes.cache.CuratorCacheImpl.start(CuratorCacheImpl.java:94)
at CuratorCacheDelegatingClientTest.testCuratorCacheWithDelegatingClient(CuratorCacheDelegatingClientTest.java:27)
Expected result
CuratorCache works with any CuratorFramework implementation, as it did in 5.8.0.
If implementations that do not extend CuratorFrameworkBase are intentionally unsupported, then:
- the restriction should be documented on the public
CuratorFrameworkinterface; - a violation should fail with a descriptive error instead of a
ClassCastException.
Suggested fix
Use the underlying client only when the instance is a CuratorFrameworkBase, which keeps the #1259 fix for namespaced facades. Otherwise fall back to the public API, as 5.8.0 did. Against current master:
--- a/curator-recipes/src/main/java/org/apache/curator/framework/recipes/watch/PersistentWatcher.java
+++ b/curator-recipes/src/main/java/org/apache/curator/framework/recipes/watch/PersistentWatcher.java
@@ -87,11 +87,18 @@
public void start() {
Preconditions.checkState(state.compareAndSet(State.LATENT, State.STARTED), "Already started");
client.getConnectionStateListenable().addListener(connectionStateListener);
- // This could be a namespaced facade which does not support getCuratorListenable.
- ((CuratorFrameworkBase) client).client().getCuratorListenable().addListener(clientCloseListener);
+ curatorListenable().addListener(clientCloseListener);
reset();
}
+ private Listenable<CuratorListener> curatorListenable() {
+ // A namespaced facade does not support getCuratorListenable(), so use the underlying client (#1259).
+ // Other CuratorFramework implementations are handled through the public API.
+ return client instanceof CuratorFrameworkBase
+ ? ((CuratorFrameworkBase) client).client().getCuratorListenable()
+ : client.getCuratorListenable();
+ }
+
/**
* Remove the watcher
*/
@@ -99,7 +106,7 @@
public void close() {
if (state.compareAndSet(State.STARTED, State.CLOSED)) {
listeners.clear();
- ((CuratorFrameworkBase) client).client().getCuratorListenable().removeListener(clientCloseListener);
+ curatorListenable().removeListener(clientCloseListener);
client.getConnectionStateListenable().removeListener(connectionStateListener);
try {
client.watchers().remove(watcher).guaranteed().inBackground().forPath(basePath);
We checked the change locally with two tests: the delegating client above, and a namespaced client (client.usingNamespace("ns"), the #1259 case):
| Delegating client | Namespaced client (#1259) | |
|---|---|---|
| 5.8.0 | passes | UnsupportedOperationException |
| 5.9.0 | ClassCastException |
passes |
5.9.0 with the same change in start() |
passes | passes |
master PersistentWatcher with the patch above, on 5.9.0 jars |
passes | passes |
TestPersistentWatcher.testNamespacedWatching already covers the namespaced case. A test like the one above could be added next to it as a regression test for the delegating case.
As a longer-term option, the underlying client could be exposed through the public API, for example a method on CuratorFramework that facades override. Then recipes would not need to cast to classes from the imps package at all.
Environment
- Apache Curator 5.9.0 (current
mastersource checked as well) - ZooKeeper 3.9.6
- OpenJDK 25.0.4
- spring-integration-zookeeper 6.5.10
- Dominant language
- Java
- Stars
- 3.2k
- Forks
- 1.2k
- Avg merge
- 4h 35m
- Merged PRs (30d)
- 1
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from apache/curator
-
Optimize storage for single-node CuratorCachePossibly taken @waterWang claimed this 59 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 68/100
-
TreeCache.iterator() throws NoSuchElementException after a parent node's last child is deletedPossibly taken @apm1467 claimed this 61 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 55/100
-
Add Jackson PolymorphicTypeValidatorPossibly taken @pjfanning claimed this 11 days ago. Open
Difficulty 4/5 3-5 days Newbie friendliness 52/100
-
Upgrade to Jackson 3Open
Difficulty 5/5 Over a week Newbie friendliness 35/100
-
CuratorCache initialization overhead with many empty ZNodesPossibly taken @2748109647 claimed this 5 days ago. Open
Difficulty 5/5 Over a week Newbie friendliness 25/100
Similar issues
-
staticAssetsPath follows symlinks outside the directory on WindowsPossibly taken @ybelMekk claimed this today. Openbug security findings
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
navikt/mock-oauth2-server#1132 ·
Maintainers usually reply within 1 day
-
agent:working follow-up
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Sanne/incus-spawn#1683 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
checkstyle/checkstyle#22210 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
google/error-prone#6239 · 1 comment ·
Maintainers usually reply within 1 day
-
[Failing Test]: BigqueryClient.queryUnflattened fails BigQuery ITs on a transient 503Possibly taken @mxtymoshyk claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
Maintainers usually reply within 1 day