sftwnd opened a new issue, #1314:
URL: https://github.com/apache/curator/issues/1314

   ### Summary
   
   Since 5.9.0, `PersistentWatcher.start()` casts the client to 
`org.apache.curator.framework.imps.CuratorFrameworkBase`:
   
   ```java
   // 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 in `start()` and, after #1293, also in 
`close()`.
   - 5.8.0 and earlier are not affected.
   
   ### Background
   
   The cast was introduced by #1262, the fix for #1259:
   
   - `NamespaceFacade.getCuratorListenable()` throws 
`UnsupportedOperationException`.
   - `PersistentWatcher` needs the `CuratorEventType.CLOSING` event 
(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`:
   
   - `CuratorFramework` is 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 of 
`CuratorFrameworkBase`, which is in the `imps` package. The `CuratorFramework` 
interface 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. `CuratorFrameworkBase` has 
package-private abstract methods (`getNamespaceImpl()`, 
`getFailedDeleteManager()`, `processBackgroundOperation(...)`, 
`queueOperation(...)` and others), and `DelegatingCuratorFramework` is 
package-private.
   
   ### Use case
   
   We wrap the client in a delegating `CuratorFramework`:
   
   - It holds a client created by `CuratorFrameworkFactory` and 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 `CuratorFramework` keep 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
   
   ```java
   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 `CuratorFramework` 
interface;
   - 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`:
   
   ```diff
   --- 
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 `master` source checked as well)
   - ZooKeeper 3.9.6
   - OpenJDK 25.0.4
   - spring-integration-zookeeper 6.5.10
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to