This is an automated email from the ASF dual-hosted git repository.

merlimat pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/pulsar.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 474474937d5 [improve][meta] Support tuning Oxia MetadataStoreConfig 
through metadata-store URIs (#26150)
474474937d5 is described below

commit 474474937d5a2f5771e23743251115f25cfeec1a
Author: Qiang Zhao <[email protected]>
AuthorDate: Tue Jul 7 00:31:48 2026 +0800

    [improve][meta] Support tuning Oxia MetadataStoreConfig through 
metadata-store URIs (#26150)
    
    Co-authored-by: void-ptr974 <[email protected]>
---
 conf/bookkeeper.conf                               |   4 +
 conf/broker.conf                                   |   5 +
 .../broker/BookKeeperClientFactoryImplTest.java    |   7 +
 .../common/naming/ServiceConfigurationTest.java    |   6 +-
 .../bookkeeper/AbstractMetadataDriver.java         |  34 ++-
 .../bookkeeper/MetadataStoreConfigQueryParams.java | 113 +++++++++
 .../metadata/bookkeeper/MetadataStoreUrl.java      |  80 ++++++
 .../bookkeeper/AbstractMetadataDriverTest.java     | 277 +++++++++++++++++++++
 8 files changed, 511 insertions(+), 15 deletions(-)

diff --git a/conf/bookkeeper.conf b/conf/bookkeeper.conf
index a482a28cc75..ff672e56ad3 100644
--- a/conf/bookkeeper.conf
+++ b/conf/bookkeeper.conf
@@ -666,6 +666,10 @@ diskCheckInterval=10000
 #  - metadataServiceUri=etcd+hierarchical:http://my-etcd:2379
 #  - metadataServiceUri=metadata-store:zk:my-zk-1:2281/ledgers
 #  - metadataServiceUri=metadata-store:oxia://oxia-server:6648/bookkeeper
+#  - 
metadataServiceUri=metadata-store:oxia://oxia-server:6648/bookkeeper?batchingMaxDelayMillis=10&batchingMaxSizeKb=256&numSerDesThreads=4
+# Supported MetadataStoreConfig query parameters for the "metadata-store:" 
driver are allowReadOnlyOperations,
+# batchingEnabled, batchingMaxDelayMillis, batchingMaxOperations, 
batchingMaxSizeKb, configFilePath, fsyncEnable,
+# numSerDesThreads, and sessionTimeoutMillis. Other query parameters are 
passed through to the metadata store provider.
 # If you use metadata-store configuration, you need to configure following 
items in JVM option:
 # 
-Dbookkeeper.metadata.client.drivers=org.apache.pulsar.metadata.bookkeeper.PulsarMetadataClientDriver
 # 
-Dbookkeeper.metadata.bookie.drivers=org.apache.pulsar.metadata.bookkeeper.PulsarMetadataBookieDriver
diff --git a/conf/broker.conf b/conf/broker.conf
index 066138ef98d..84ccd1d852b 100644
--- a/conf/broker.conf
+++ b/conf/broker.conf
@@ -1097,6 +1097,11 @@ maxConcurrentHttpRequests=1024
 # Examples:
 # - metadata-store:zk:zk1:2181,zk2:2181,zk3:2181/ledgers
 # - metadata-store:oxia://oxia-server:6648/bookkeeper
+# - 
metadata-store:oxia://oxia-server:6648/bookkeeper?batchingMaxDelayMillis=10&batchingMaxSizeKb=256&numSerDesThreads=4
+# When using the "metadata-store:" driver with a separated BookKeeper metadata 
service, supported
+# MetadataStoreConfig query parameters are allowReadOnlyOperations, 
batchingEnabled, batchingMaxDelayMillis,
+# batchingMaxOperations, batchingMaxSizeKb, configFilePath, fsyncEnable, 
numSerDesThreads, and
+# sessionTimeoutMillis. Other query parameters are passed through to the 
metadata store provider.
 # When the value is empty, the broker will default to using broker's 
metadataStoreUrl config appended with "/ledgers"
 bookkeeperMetadataServiceUri=
 
diff --git 
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/BookKeeperClientFactoryImplTest.java
 
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/BookKeeperClientFactoryImplTest.java
index fdf8dea0f04..f3c8acd365f 100644
--- 
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/BookKeeperClientFactoryImplTest.java
+++ 
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/BookKeeperClientFactoryImplTest.java
@@ -278,6 +278,13 @@ public class BookKeeperClientFactoryImplTest {
                         .getMetadataServiceUri(), expectedUri);
 
             }
+            {
+                String uri = 
"metadata-store:oxia://oxia-server:6648/bookkeeper"
+                        + 
"?batchingMaxDelayMillis=10&batchingMaxSizeKb=256&numSerDesThreads=4";
+                conf.setBookkeeperMetadataServiceUri(uri);
+                
assertEquals(factory.createBkClientConfiguration(mock(MetadataStoreExtended.class),
 conf)
+                        .getMetadataServiceUri(), uri);
+            }
         } catch (ConfigurationException e) {
             e.printStackTrace();
             fail("Get metadata service uri should be successful", e);
diff --git 
a/pulsar-broker/src/test/java/org/apache/pulsar/common/naming/ServiceConfigurationTest.java
 
b/pulsar-broker/src/test/java/org/apache/pulsar/common/naming/ServiceConfigurationTest.java
index ed551c8c1bb..e9662e85f8d 100644
--- 
a/pulsar-broker/src/test/java/org/apache/pulsar/common/naming/ServiceConfigurationTest.java
+++ 
b/pulsar-broker/src/test/java/org/apache/pulsar/common/naming/ServiceConfigurationTest.java
@@ -221,16 +221,18 @@ public class ServiceConfigurationTest {
 
     @Test
     public void testBookkeeperMetadataStore() throws Exception {
+        String bookkeeperMetadataServiceUri = 
"metadata-store:oxia://oxia-server:6648/bookkeeper"
+                + 
"?batchingMaxDelayMillis=10&batchingMaxSizeKb=256&numSerDesThreads=4";
         String confFile = "metadataStoreUrl=zk1:2181\n"
                 + "configurationMetadataStoreUrl=zk2:2182\n"
-                + "bookkeeperMetadataServiceUri=xx:other-system\n";
+                + "bookkeeperMetadataServiceUri=" + 
bookkeeperMetadataServiceUri + "\n";
         @Cleanup
         InputStream stream = new ByteArrayInputStream(confFile.getBytes());
         final ServiceConfiguration conf = 
PulsarConfigurationLoader.create(stream, ServiceConfiguration.class);
 
         assertEquals(conf.getMetadataStoreUrl(), "zk1:2181");
         assertEquals(conf.getConfigurationMetadataStoreUrl(), "zk2:2182");
-        assertEquals(conf.getBookkeeperMetadataStoreUrl(), "xx:other-system");
+        assertEquals(conf.getBookkeeperMetadataStoreUrl(), 
bookkeeperMetadataServiceUri);
         assertTrue(conf.isConfigurationStoreSeparated());
         assertTrue(conf.isBookkeeperMetadataStoreSeparated());
     }
diff --git 
a/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/AbstractMetadataDriver.java
 
b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/AbstractMetadataDriver.java
index 435f94b05dc..1ebeecea6e4 100644
--- 
a/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/AbstractMetadataDriver.java
+++ 
b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/AbstractMetadataDriver.java
@@ -32,7 +32,6 @@ import 
org.apache.bookkeeper.meta.LegacyHierarchicalLedgerManagerFactory;
 import org.apache.bookkeeper.meta.exceptions.Code;
 import org.apache.bookkeeper.meta.exceptions.MetadataException;
 import org.apache.bookkeeper.util.BookKeeperConstants;
-import org.apache.pulsar.metadata.api.MetadataStoreConfig;
 import org.apache.pulsar.metadata.api.MetadataStoreException;
 import org.apache.pulsar.metadata.api.extended.MetadataStoreExtended;
 
@@ -105,27 +104,36 @@ public abstract class AbstractMetadataDriver implements 
Closeable {
             this.storeInstanceIsOwned = false;
         } else {
 
-            String url;
+            String metadataServiceUri = null;
+            MetadataStoreUrl metadataStoreUrl;
             try {
-                url = conf.getMetadataServiceUri()
-                        .replaceFirst(METADATA_STORE_SCHEME + ":", "")
-                        .replace(";", ",");
+                metadataServiceUri = conf.getMetadataServiceUri();
+                metadataStoreUrl = MetadataStoreUrl.parse(metadataServiceUri);
             } catch (Exception e) {
-                throw new MetadataException(Code.METADATA_SERVICE_ERROR, e);
+                throw metadataStoreCreationException(metadataServiceUri, null, 
e);
             }
             try {
-                this.store = MetadataStoreExtended.create(url,
-                        MetadataStoreConfig.builder()
-                                .sessionTimeoutMillis(conf.getZkTimeout())
-                                
.metadataStoreName(MetadataStoreConfig.METADATA_STORE)
-                                .build());
+                this.store = 
MetadataStoreExtended.create(metadataStoreUrl.url(),
+                        MetadataStoreConfigQueryParams.createConfig(conf, 
metadataStoreUrl.configParams()));
                 this.storeInstanceIsOwned = true;
-            } catch (MetadataStoreException e) {
-                throw new MetadataException(Code.METADATA_SERVICE_ERROR, e);
+            } catch (IllegalArgumentException | MetadataStoreException e) {
+                throw metadataStoreCreationException(metadataServiceUri, 
metadataStoreUrl.url(), e);
             }
         }
     }
 
+    private static MetadataException metadataStoreCreationException(
+            String metadataServiceUri, String metadataStoreUrl, Exception 
cause) {
+        String message = "Failed to create BookKeeper metadata store";
+        if (metadataServiceUri != null) {
+            message += " from metadataServiceUri '" + metadataServiceUri + "'";
+        }
+        if (metadataStoreUrl != null) {
+            message += " (metadata store URL '" + metadataStoreUrl + "')";
+        }
+        return new MetadataException(Code.METADATA_SERVICE_ERROR, message, 
cause);
+    }
+
     public String getScheme() {
         return METADATA_STORE_SCHEME;
     }
diff --git 
a/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/MetadataStoreConfigQueryParams.java
 
b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/MetadataStoreConfigQueryParams.java
new file mode 100644
index 00000000000..61866232c64
--- /dev/null
+++ 
b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/MetadataStoreConfigQueryParams.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metadata.bookkeeper;
+
+import java.util.Map;
+import java.util.function.BiConsumer;
+import org.apache.bookkeeper.conf.AbstractConfiguration;
+import org.apache.pulsar.metadata.api.MetadataStoreConfig;
+import 
org.apache.pulsar.metadata.api.MetadataStoreConfig.MetadataStoreConfigBuilder;
+
+class MetadataStoreConfigQueryParams {
+    private static final Map<String, BiConsumer<MetadataStoreConfigBuilder, 
String>> PARAM_SETTERS = Map.ofEntries(
+            booleanParam("allowReadOnlyOperations", 
MetadataStoreConfigBuilder::allowReadOnlyOperations),
+            booleanParam("batchingEnabled", 
MetadataStoreConfigBuilder::batchingEnabled),
+            nonNegativeIntParam("batchingMaxDelayMillis", 
MetadataStoreConfigBuilder::batchingMaxDelayMillis),
+            positiveIntParam("batchingMaxOperations", 
MetadataStoreConfigBuilder::batchingMaxOperations),
+            positiveIntParam("batchingMaxSizeKb", 
MetadataStoreConfigBuilder::batchingMaxSizeKb),
+            Map.entry("configFilePath", 
MetadataStoreConfigBuilder::configFilePath),
+            booleanParam("fsyncEnable", 
MetadataStoreConfigBuilder::fsyncEnable),
+            positiveIntParam("numSerDesThreads", 
MetadataStoreConfigBuilder::numSerDesThreads),
+            positiveIntParam("sessionTimeoutMillis", 
MetadataStoreConfigBuilder::sessionTimeoutMillis));
+
+    private MetadataStoreConfigQueryParams() {
+    }
+
+    static boolean contains(String key) {
+        return PARAM_SETTERS.containsKey(key);
+    }
+
+    @SuppressWarnings("rawtypes")
+    static MetadataStoreConfig createConfig(AbstractConfiguration conf, 
Map<String, String> configParams) {
+        MetadataStoreConfigBuilder builder = MetadataStoreConfig.builder()
+                .sessionTimeoutMillis(conf.getZkTimeout())
+                .metadataStoreName(MetadataStoreConfig.METADATA_STORE);
+        configParams.forEach((key, value) -> {
+            BiConsumer<MetadataStoreConfigBuilder, String> setter = 
PARAM_SETTERS.get(key);
+            if (setter == null) {
+                throw new IllegalArgumentException("Unsupported 
MetadataStoreConfig query parameter '" + key
+                        + "'. Supported parameters are " + 
PARAM_SETTERS.keySet());
+            }
+            setter.accept(builder, value);
+        });
+        return builder.build();
+    }
+
+    private static Map.Entry<String, BiConsumer<MetadataStoreConfigBuilder, 
String>> booleanParam(
+            String key, BiConsumer<MetadataStoreConfigBuilder, Boolean> 
setter) {
+        return Map.entry(key, (builder, value) -> setter.accept(builder, 
parseBoolean(key, value)));
+    }
+
+    private static Map.Entry<String, BiConsumer<MetadataStoreConfigBuilder, 
String>> nonNegativeIntParam(
+            String key, BiConsumer<MetadataStoreConfigBuilder, Integer> 
setter) {
+        return Map.entry(key, (builder, value) -> setter.accept(builder, 
parseNonNegativeInt(key, value)));
+    }
+
+    private static Map.Entry<String, BiConsumer<MetadataStoreConfigBuilder, 
String>> positiveIntParam(
+            String key, BiConsumer<MetadataStoreConfigBuilder, Integer> 
setter) {
+        return Map.entry(key, (builder, value) -> setter.accept(builder, 
parsePositiveInt(key, value)));
+    }
+
+    private static boolean parseBoolean(String key, String value) {
+        if ("true".equalsIgnoreCase(value)) {
+            return true;
+        } else if ("false".equalsIgnoreCase(value)) {
+            return false;
+        }
+        throw new IllegalArgumentException("MetadataStoreConfig query 
parameter '" + key
+                + "' must be true or false");
+    }
+
+    private static int parseNonNegativeInt(String key, String value) {
+        int intValue = parseInt(key, value);
+        if (intValue < 0) {
+            throw new IllegalArgumentException("MetadataStoreConfig query 
parameter '" + key
+                    + "' must be greater than or equal to 0");
+        }
+        return intValue;
+    }
+
+    private static int parsePositiveInt(String key, String value) {
+        int intValue = parseInt(key, value);
+        if (intValue <= 0) {
+            throw new IllegalArgumentException("MetadataStoreConfig query 
parameter '" + key
+                    + "' must be greater than 0");
+        }
+        return intValue;
+    }
+
+    private static int parseInt(String key, String value) {
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("MetadataStoreConfig query 
parameter '" + key
+                    + "' must be an integer", e);
+        }
+    }
+}
diff --git 
a/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/MetadataStoreUrl.java
 
b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/MetadataStoreUrl.java
new file mode 100644
index 00000000000..b7a52369b8d
--- /dev/null
+++ 
b/pulsar-metadata/src/main/java/org/apache/pulsar/metadata/bookkeeper/MetadataStoreUrl.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metadata.bookkeeper;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+record MetadataStoreUrl(String url, Map<String, String> configParams) {
+    static MetadataStoreUrl parse(String metadataServiceUri) {
+        String metadataStoreUrl = 
removeMetadataStoreScheme(metadataServiceUri);
+        int queryStart = metadataStoreUrl.indexOf('?');
+        if (queryStart < 0) {
+            return new MetadataStoreUrl(metadataStoreUrl.replace(";", ","), 
Map.of());
+        }
+
+        String url = metadataStoreUrl.substring(0, queryStart).replace(";", 
",");
+        String query = metadataStoreUrl.substring(queryStart + 1);
+        if (query.isEmpty()) {
+            return new MetadataStoreUrl(url, Map.of());
+        }
+
+        Map<String, String> configParams = new LinkedHashMap<>();
+        List<String> providerParams = new ArrayList<>();
+        for (String param : query.split("&", -1)) {
+            if (param.isEmpty()) {
+                continue;
+            }
+            int separator = param.indexOf('=');
+            String key = decodeQueryParam(separator > 0 ? param.substring(0, 
separator) : param);
+            if (!MetadataStoreConfigQueryParams.contains(key)) {
+                providerParams.add(param);
+                continue;
+            }
+            if (separator <= 0) {
+                throw new IllegalArgumentException("Invalid 
MetadataStoreConfig query parameter '" + param
+                        + "'. Expected key=value");
+            }
+
+            String value = decodeQueryParam(param.substring(separator + 1));
+            configParams.put(key, value);
+        }
+
+        if (!providerParams.isEmpty()) {
+            url += "?" + String.join("&", providerParams);
+        }
+        return new MetadataStoreUrl(url, Map.copyOf(configParams));
+    }
+
+    private static String removeMetadataStoreScheme(String metadataServiceUri) 
{
+        String prefix = AbstractMetadataDriver.METADATA_STORE_SCHEME + ":";
+        if (metadataServiceUri.startsWith(prefix)) {
+            return metadataServiceUri.substring(prefix.length());
+        }
+        return metadataServiceUri;
+    }
+
+    private static String decodeQueryParam(String value) {
+        return URLDecoder.decode(value, StandardCharsets.UTF_8);
+    }
+}
diff --git 
a/pulsar-metadata/src/test/java/org/apache/pulsar/metadata/bookkeeper/AbstractMetadataDriverTest.java
 
b/pulsar-metadata/src/test/java/org/apache/pulsar/metadata/bookkeeper/AbstractMetadataDriverTest.java
new file mode 100644
index 00000000000..e7832568a14
--- /dev/null
+++ 
b/pulsar-metadata/src/test/java/org/apache/pulsar/metadata/bookkeeper/AbstractMetadataDriverTest.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metadata.bookkeeper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import io.opentelemetry.api.OpenTelemetry;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import org.apache.bookkeeper.conf.AbstractConfiguration;
+import org.apache.bookkeeper.conf.ClientConfiguration;
+import org.apache.bookkeeper.meta.exceptions.MetadataException;
+import org.apache.pulsar.metadata.api.GetResult;
+import org.apache.pulsar.metadata.api.MetadataStore;
+import org.apache.pulsar.metadata.api.MetadataStoreConfig;
+import org.apache.pulsar.metadata.api.MetadataStoreException;
+import org.apache.pulsar.metadata.api.MetadataStoreProvider;
+import org.apache.pulsar.metadata.api.Stat;
+import org.apache.pulsar.metadata.api.extended.CreateOption;
+import org.apache.pulsar.metadata.impl.AbstractMetadataStore;
+import org.apache.pulsar.metadata.impl.MetadataStoreFactoryImpl;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class AbstractMetadataDriverTest {
+    private static final String CAPTURING_METADATA_STORE_URL = 
"config-capture://host:4181/bookkeeper";
+
+    private String originalMetadataStoreProviders;
+
+    @BeforeClass
+    public void addCapturingMetadataStoreProvider() {
+        originalMetadataStoreProviders = 
System.getProperty(MetadataStoreFactoryImpl.METADATASTORE_PROVIDERS_PROPERTY);
+        String providers = CapturingMetadataStoreProvider.class.getName();
+        if (originalMetadataStoreProviders != null && 
!originalMetadataStoreProviders.isBlank()) {
+            providers = originalMetadataStoreProviders + "," + providers;
+        }
+        
System.setProperty(MetadataStoreFactoryImpl.METADATASTORE_PROVIDERS_PROPERTY, 
providers);
+    }
+
+    @AfterClass(alwaysRun = true)
+    public void restoreMetadataStoreProviders() {
+        if (originalMetadataStoreProviders != null) {
+            
System.setProperty(MetadataStoreFactoryImpl.METADATASTORE_PROVIDERS_PROPERTY,
+                    originalMetadataStoreProviders);
+        } else {
+            
System.clearProperty(MetadataStoreFactoryImpl.METADATASTORE_PROVIDERS_PROPERTY);
+        }
+    }
+
+    @Test
+    public void 
testMetadataStoreConfigCanBeConfiguredWithMetadataServiceUriQuery() throws 
Exception {
+        CapturingMetadataStoreProvider.reset();
+        ClientConfiguration conf = new ClientConfiguration();
+        conf.setZkTimeout(12_345);
+        conf.setMetadataServiceUri("metadata-store:" + 
CAPTURING_METADATA_STORE_URL
+                + "?batchingEnabled=false"
+                + "&batchingMaxDelayMillis=10"
+                + "&batchingMaxOperations=11"
+                + "&batchingMaxSizeKb=12"
+                + "&numSerDesThreads=3"
+                + "&sessionTimeoutMillis=40000"
+                + "&allowReadOnlyOperations=true"
+                + "&configFilePath=%2Ftmp%2Foxia-client.conf"
+                + "&fsyncEnable=false");
+
+        createMetadataStore(conf);
+
+        
assertThat(CapturingMetadataStoreProvider.metadataURL).isEqualTo(CAPTURING_METADATA_STORE_URL);
+        MetadataStoreConfig metadataStoreConfig = 
CapturingMetadataStoreProvider.metadataStoreConfig;
+        
assertThat(metadataStoreConfig.getMetadataStoreName()).isEqualTo(MetadataStoreConfig.METADATA_STORE);
+        assertThat(metadataStoreConfig.isBatchingEnabled()).isFalse();
+        
assertThat(metadataStoreConfig.getBatchingMaxDelayMillis()).isEqualTo(10);
+        
assertThat(metadataStoreConfig.getBatchingMaxOperations()).isEqualTo(11);
+        assertThat(metadataStoreConfig.getBatchingMaxSizeKb()).isEqualTo(12);
+        assertThat(metadataStoreConfig.getNumSerDesThreads()).isEqualTo(3);
+        
assertThat(metadataStoreConfig.getSessionTimeoutMillis()).isEqualTo(40_000);
+        assertThat(metadataStoreConfig.isAllowReadOnlyOperations()).isTrue();
+        
assertThat(metadataStoreConfig.getConfigFilePath()).isEqualTo("/tmp/oxia-client.conf");
+        assertThat(metadataStoreConfig.isFsyncEnable()).isFalse();
+    }
+
+    @Test
+    public void testUnknownMetadataServiceUriQueryParamsArePassedToProvider() 
throws Exception {
+        CapturingMetadataStoreProvider.reset();
+        ClientConfiguration conf = new ClientConfiguration();
+        conf.setMetadataServiceUri("metadata-store:" + 
CAPTURING_METADATA_STORE_URL
+                + "?providerParam=1"
+                + "&batchingMaxDelayMillis=10"
+                + "&providerFlag"
+                + "&batchingMaxDelayMillis=20"
+                + "&numSerDesThreads=3");
+
+        createMetadataStore(conf);
+
+        assertThat(CapturingMetadataStoreProvider.metadataURL)
+                .isEqualTo(CAPTURING_METADATA_STORE_URL + 
"?providerParam=1&providerFlag");
+        MetadataStoreConfig metadataStoreConfig = 
CapturingMetadataStoreProvider.metadataStoreConfig;
+        
assertThat(metadataStoreConfig.getBatchingMaxDelayMillis()).isEqualTo(20);
+        assertThat(metadataStoreConfig.getNumSerDesThreads()).isEqualTo(3);
+    }
+
+    @Test
+    public void testMetadataServiceUriWithoutQueryUsesExistingDefaults() 
throws Exception {
+        CapturingMetadataStoreProvider.reset();
+        ClientConfiguration conf = new ClientConfiguration();
+        conf.setZkTimeout(12_345);
+        
conf.setMetadataServiceUri("metadata-store:config-capture://host1:4181;host2:4181/bookkeeper");
+
+        createMetadataStore(conf);
+
+        assertThat(CapturingMetadataStoreProvider.metadataURL)
+                
.isEqualTo("config-capture://host1:4181,host2:4181/bookkeeper");
+        MetadataStoreConfig metadataStoreConfig = 
CapturingMetadataStoreProvider.metadataStoreConfig;
+        
assertThat(metadataStoreConfig.getMetadataStoreName()).isEqualTo(MetadataStoreConfig.METADATA_STORE);
+        
assertThat(metadataStoreConfig.getSessionTimeoutMillis()).isEqualTo(12_345);
+        assertThat(metadataStoreConfig.isBatchingEnabled()).isTrue();
+        
assertThat(metadataStoreConfig.getBatchingMaxDelayMillis()).isEqualTo(5);
+        
assertThat(metadataStoreConfig.getBatchingMaxOperations()).isEqualTo(1_000);
+        assertThat(metadataStoreConfig.getBatchingMaxSizeKb()).isEqualTo(128);
+        assertThat(metadataStoreConfig.getNumSerDesThreads()).isEqualTo(1);
+    }
+
+    @Test(dataProvider = "invalidMetadataStoreConfigQueryParams")
+    public void testInvalidMetadataStoreConfigQueryParamFails(String query, 
String invalidParameter,
+                                                             boolean 
expectBackendUrlInErrorMessage) {
+        assertInvalidConfigQuery(query, invalidParameter, 
expectBackendUrlInErrorMessage);
+    }
+
+    private static void assertInvalidConfigQuery(String query, String 
invalidParameter,
+                                                 boolean 
expectBackendUrlInErrorMessage) {
+        ClientConfiguration conf = new ClientConfiguration();
+        String metadataServiceUri = "metadata-store:" + 
CAPTURING_METADATA_STORE_URL + query;
+        conf.setMetadataServiceUri(metadataServiceUri);
+
+        Throwable error = catchThrowable(() -> createMetadataStore(conf));
+
+        assertThat(error)
+                .isInstanceOf(MetadataException.class)
+                .hasCauseInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("BookKeeper metadata store")
+                .hasMessageContaining("metadataServiceUri")
+                .hasMessageContaining(metadataServiceUri);
+        if (expectBackendUrlInErrorMessage) {
+            
assertThat(error).hasMessageContaining(CAPTURING_METADATA_STORE_URL);
+        }
+        assertThat(error.getCause()).hasMessageContaining(invalidParameter);
+    }
+
+    @DataProvider(name = "invalidMetadataStoreConfigQueryParams")
+    public Object[][] invalidMetadataStoreConfigQueryParams() {
+        return new Object[][] {
+                {"?batchingEnabled=maybe", "batchingEnabled", true},
+                {"?batchingMaxDelayMillis=-1", "batchingMaxDelayMillis", true},
+                {"?batchingMaxOperations=0", "batchingMaxOperations", true},
+                {"?numSerDesThreads=abc", "numSerDesThreads", true},
+                {"?batchingEnabled", "batchingEnabled", false}
+        };
+    }
+
+    @Test
+    public void testMetadataStoreInstancePathIgnoresMetadataServiceUriQuery() 
throws Exception {
+        CapturingMetadataStoreProvider.reset();
+        ClientConfiguration conf = new ClientConfiguration();
+        MetadataStore sharedStore = new CapturingMetadataStore();
+        conf.setProperty(AbstractMetadataDriver.METADATA_STORE_INSTANCE, 
sharedStore);
+        conf.setMetadataServiceUri("metadata-store:" + 
CAPTURING_METADATA_STORE_URL
+                + "?unknown=1&batchingMaxDelayMillis=bad");
+
+        TestMetadataDriver driver = new TestMetadataDriver();
+        try {
+            driver.createMetadataStore(conf);
+
+            assertThat(driver.store()).isSameAs(sharedStore);
+            assertThat(CapturingMetadataStoreProvider.metadataURL).isNull();
+            
assertThat(CapturingMetadataStoreProvider.metadataStoreConfig).isNull();
+        } finally {
+            driver.close();
+        }
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static void createMetadataStore(AbstractConfiguration conf) throws 
MetadataException {
+        TestMetadataDriver driver = new TestMetadataDriver();
+        try {
+            driver.createMetadataStore(conf);
+        } finally {
+            driver.close();
+        }
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static class TestMetadataDriver extends AbstractMetadataDriver {
+        private void createMetadataStore(AbstractConfiguration conf) throws 
MetadataException {
+            this.conf = conf;
+            super.createMetadataStore();
+        }
+
+        private MetadataStore store() {
+            return store;
+        }
+    }
+
+    public static class CapturingMetadataStoreProvider implements 
MetadataStoreProvider {
+        private static volatile String metadataURL;
+        private static volatile MetadataStoreConfig metadataStoreConfig;
+
+        static void reset() {
+            metadataURL = null;
+            metadataStoreConfig = null;
+        }
+
+        @Override
+        public String urlScheme() {
+            return "config-capture";
+        }
+
+        @Override
+        public MetadataStore create(String metadataURL, MetadataStoreConfig 
metadataStoreConfig,
+                                    boolean enableSessionWatcher) throws 
MetadataStoreException {
+            CapturingMetadataStoreProvider.metadataURL = metadataURL;
+            CapturingMetadataStoreProvider.metadataStoreConfig = 
metadataStoreConfig;
+            return new CapturingMetadataStore();
+        }
+    }
+
+    private static class CapturingMetadataStore extends AbstractMetadataStore {
+        private CapturingMetadataStore() {
+            super("config-capture", OpenTelemetry.noop(), null, 1);
+        }
+
+        @Override
+        protected CompletableFuture<Boolean> existsFromStore(String path) {
+            return CompletableFuture.completedFuture(false);
+        }
+
+        @Override
+        public CompletableFuture<List<String>> getChildrenFromStore(String 
path) {
+            return CompletableFuture.completedFuture(List.of());
+        }
+
+        @Override
+        protected CompletableFuture<Optional<GetResult>> storeGet(String path) 
{
+            return CompletableFuture.completedFuture(Optional.empty());
+        }
+
+        @Override
+        protected CompletableFuture<Void> storeDelete(String path, 
Optional<Long> expectedVersion) {
+            return CompletableFuture.completedFuture(null);
+        }
+
+        @Override
+        protected CompletableFuture<Stat> storePut(String path, byte[] data, 
Optional<Long> optExpectedVersion,
+                                                   EnumSet<CreateOption> opts) 
{
+            return CompletableFuture.completedFuture(new Stat(path, 0, 0, 0, 
false, false, false));
+        }
+    }
+}

Reply via email to