You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@accumulo.apache.org by ct...@apache.org on 2022/05/10 21:17:08 UTC
[accumulo] 05/05: Single node prop store (#2569)
This is an automated email from the ASF dual-hosted git repository.
ctubbsii pushed a commit to branch single-node-props
in repository https://gitbox.apache.org/repos/asf/accumulo.git
commit 47de8950844babf3124df430622e7c1365e0d0ea
Author: EdColeman <de...@etcoleman.com>
AuthorDate: Tue May 10 17:13:08 2022 -0400
Single node prop store (#2569)
Convert multiple node property configuration to a single-node encoded store.
This reduces the number of ZooKeeper watchers needed to track property
changes, and sets the groundwork for atomic changes to multiple properties
in the future.
Co-authored-by: Ed Coleman etcoleman <ed...@apache.org>
Co-authored-by: Christopher Tubbs <ct...@apache.org>
---
.../org/apache/accumulo/core/conf/Property.java | 2 +-
.../accumulo/core/metrics/MetricsProducer.java | 43 ++
.../apache/accumulo/core/conf/PropertyTest.java | 9 +
server/base/pom.xml | 8 +
.../org/apache/accumulo/server/ServerContext.java | 32 +-
.../server/conf/NamespaceConfiguration.java | 119 ++--
.../server/conf/RuntimeFixedProperties.java | 106 ++++
.../server/conf/ServerConfigurationFactory.java | 24 +-
.../accumulo/server/conf/SystemConfiguration.java | 66 ++
.../accumulo/server/conf/TableConfiguration.java | 108 +---
.../server/conf/ZooBasedConfiguration.java | 164 +++++
.../server/conf/ZooCachePropertyAccessor.java | 172 ------
.../accumulo/server/conf/ZooConfiguration.java | 149 -----
.../server/conf/codec/VersionedPropCodec.java | 92 ++-
.../server/conf/codec/VersionedPropGzipCodec.java | 2 +-
.../server/conf/codec/VersionedProperties.java | 32 +-
.../server/conf/store/NamespacePropKey.java | 65 ++
.../accumulo/server/conf/store/PropCache.java | 36 ++
.../accumulo/server/conf/store/PropCacheKey.java | 141 +++++
.../server/conf/store/PropChangeListener.java | 43 ++
.../accumulo/server/conf/store/PropStore.java | 117 ++++
.../accumulo/server/conf/store/SystemPropKey.java | 56 ++
.../accumulo/server/conf/store/TablePropKey.java | 59 ++
.../conf/store/impl/PropCacheCaffeineImpl.java | 142 +++++
.../server/conf/store/impl/PropStoreEventTask.java | 110 ++++
.../server/conf/store/impl/PropStoreMetrics.java | 92 +++
.../server/conf/store/impl/PropStoreWatcher.java | 252 ++++++++
.../server/conf/store/impl/ReadyMonitor.java | 143 +++++
.../server/conf/store/impl/ZooPropLoader.java | 157 +++++
.../server/conf/store/impl/ZooPropStore.java | 390 ++++++++++++
.../server/conf/util/ConfigPropertyUpgrader.java | 138 +++++
.../server/conf/util/ConfigTransformer.java | 395 ++++++++++++
.../accumulo/server/conf/util/PropSnapshot.java | 141 +++++
.../accumulo/server/conf/util/TransformToken.java | 209 +++++++
.../server/init/FileSystemInitializer.java | 35 +-
.../apache/accumulo/server/init/Initialize.java | 24 +-
.../accumulo/server/init/ZooKeeperInitializer.java | 82 ++-
.../accumulo/server/tables/TableManager.java | 59 +-
.../accumulo/server/util/NamespacePropUtil.java | 51 +-
.../org/apache/accumulo/server/util/PropUtil.java | 39 ++
.../accumulo/server/util/SystemPropUtil.java | 45 +-
.../apache/accumulo/server/util/TablePropUtil.java | 55 +-
.../apache/accumulo/server/MockServerContext.java | 13 +
.../server/conf/NamespaceConfigurationTest.java | 149 +++--
.../server/conf/RuntimeFixedPropertiesTest.java | 87 +++
.../conf/ServerConfigurationFactoryTest.java | 114 ++--
.../server/conf/SystemConfigurationTest.java | 141 +++++
.../server/conf/TableConfigurationTest.java | 276 +++++++--
.../server/conf/ZooBasedConfigurationTest.java | 347 +++++++++++
.../server/conf/ZooCachePropertyAccessorTest.java | 188 ------
.../server/conf/codec/VersionedPropCodecTest.java | 8 +-
.../conf/codec/VersionedPropEncryptCodec.java | 2 +-
.../conf/codec/VersionedPropEncryptCodecTest.java | 28 +-
.../conf/codec/VersionedPropGzipCodecTest.java | 28 +-
.../server/conf/codec/VersionedPropertiesTest.java | 39 +-
.../server/conf/store/PropCacheKeyTest.java | 125 ++++
.../conf/store/impl/PropCacheCaffeineImplTest.java | 223 +++++++
.../server/conf/store/impl/PropStoreEventTest.java | 318 ++++++++++
.../server/conf/store/impl/ReadyMonitorTest.java | 180 ++++++
.../server/conf/store/impl/ZooPropLoaderTest.java | 665 +++++++++++++++++++++
.../server/conf/store/impl/ZooPropStoreTest.java | 403 +++++++++++++
.../server/conf/util/PropSnapshotTest.java | 133 +++++
.../BaseHostRegexTableLoadBalancerTest.java | 39 +-
.../server/security/UserImpersonationTest.java | 5 +
.../manager/ManagerClientServiceHandler.java | 39 +-
.../manager/tableOps/create/PopulateZookeeper.java | 17 +-
.../create/PopulateZookeeperWithNamespace.java | 15 +-
.../tableImport/ImportPopulateZookeeper.java | 17 +-
.../accumulo/manager/upgrade/Upgrader9to10.java | 80 +--
.../tserver/TabletServerResourceManager.java | 1 -
test/pom.xml | 4 +
.../accumulo/test/TableConfigurationUpdateIT.java | 8 +-
.../accumulo/test/conf/PropStoreConfigIT.java | 67 +++
.../test/conf/store/PropCacheCaffeineImplZkIT.java | 180 ++++++
.../test/conf/store/PropStoreZooKeeperIT.java | 416 +++++++++++++
.../accumulo/test/conf/store/ZooBasedConfigIT.java | 394 ++++++++++++
.../test/conf/util/ConfigTransformerIT.java | 181 ++++++
.../accumulo/test/conf/util/LegacyPropData.java | 390 ++++++++++++
.../accumulo/test/conf/util/TransformTokenIT.java | 161 +++++
.../test/functional/WatchTheWatchCountIT.java | 5 +-
.../apache/accumulo/test/metrics/MetricsIT.java | 4 +-
.../apache/accumulo/test/shell/ShellServerIT.java | 20 +-
.../apache/accumulo/test/start/KeywordStartIT.java | 2 +
.../test/upgrade/ConfigPropertyUpgraderIT.java | 154 +++++
.../upgrade/DeprecatedPropertyUpgrade9to10IT.java | 67 ---
85 files changed, 8584 insertions(+), 1353 deletions(-)
diff --git a/core/src/main/java/org/apache/accumulo/core/conf/Property.java b/core/src/main/java/org/apache/accumulo/core/conf/Property.java
index 9ca2fdc87a..fa0c069531 100644
--- a/core/src/main/java/org/apache/accumulo/core/conf/Property.java
+++ b/core/src/main/java/org/apache/accumulo/core/conf/Property.java
@@ -1716,7 +1716,7 @@ public enum Property {
|| key.startsWith(TABLE_COMPACTION_SELECTOR_OPTS.getKey())));
}
- private static final EnumSet<Property> fixedProperties = EnumSet.of(
+ public static final EnumSet<Property> fixedProperties = EnumSet.of(
// port options
GC_PORT, MANAGER_CLIENTPORT, TSERV_CLIENTPORT,
diff --git a/core/src/main/java/org/apache/accumulo/core/metrics/MetricsProducer.java b/core/src/main/java/org/apache/accumulo/core/metrics/MetricsProducer.java
index 1c242726f9..699ffc7d2b 100644
--- a/core/src/main/java/org/apache/accumulo/core/metrics/MetricsProducer.java
+++ b/core/src/main/java/org/apache/accumulo/core/metrics/MetricsProducer.java
@@ -530,6 +530,42 @@ import io.micrometer.core.instrument.MeterRegistry;
* <td>Distribution Summary</td>
* <td></td>
* </tr>
+ * <!-- ZooKeeper property cache -->
+ * <tr>
+ * <td>N/A</td>
+ * <td>N/A</td>
+ * <td>{@link #METRICS_PROPSTORE_LOAD_TIMER}</td>
+ * <td>Timer</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>N/A</td>
+ * <td>N/A</td>
+ * <td>{@link #METRICS_PROPSTORE_REFRESH_COUNT}</td>
+ * <td>Counter</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>N/A</td>
+ * <td>N/A</td>
+ * <td>{@link #METRICS_PROPSTORE_REFRESH_LOAD_COUNT}</td>
+ * <td>Counter</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>N/A</td>
+ * <td>N/A</td>
+ * <td>{@link #METRICS_PROPSTORE_EVICTION_COUNT}</td>
+ * <td>Counter</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>N/A</td>
+ * <td>N/A</td>
+ * <td>{@link #METRICS_PROPSTORE_ZK_ERROR_COUNT}</td>
+ * <td>Counter</td>
+ * <td></td>
+ * </tr>
* </table>
*
* @since 2.1.0
@@ -617,6 +653,13 @@ public interface MetricsProducer {
String METRICS_UPDATE_WALOG_WRITE = METRICS_UPDATE_PREFIX + "walog.write";
String METRICS_UPDATE_MUTATION_ARRAY_SIZE = METRICS_UPDATE_PREFIX + "mutation.arrays.size";
+ String METRICS_PROPSTORE_PREFIX = "accumulo.prop.store.";
+ String METRICS_PROPSTORE_LOAD_TIMER = METRICS_PROPSTORE_PREFIX + "load";
+ String METRICS_PROPSTORE_REFRESH_COUNT = METRICS_PROPSTORE_PREFIX + "refresh";
+ String METRICS_PROPSTORE_REFRESH_LOAD_COUNT = METRICS_PROPSTORE_PREFIX + "refresh.load";
+ String METRICS_PROPSTORE_EVICTION_COUNT = METRICS_PROPSTORE_PREFIX + "evictions";
+ String METRICS_PROPSTORE_ZK_ERROR_COUNT = METRICS_PROPSTORE_PREFIX + "zookeeper.error";
+
/**
* Build Micrometer Meter objects and register them with the registry
*/
diff --git a/core/src/test/java/org/apache/accumulo/core/conf/PropertyTest.java b/core/src/test/java/org/apache/accumulo/core/conf/PropertyTest.java
index d568bf9104..5f392ce5f7 100644
--- a/core/src/test/java/org/apache/accumulo/core/conf/PropertyTest.java
+++ b/core/src/test/java/org/apache/accumulo/core/conf/PropertyTest.java
@@ -20,6 +20,7 @@ package org.apache.accumulo.core.conf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -186,4 +187,12 @@ public class PropertyTest {
assertFalse(Property.isValidTablePropertyKey("abc.def"));
}
+
+ @Test
+ public void testFixedPropertiesNonNull() {
+ Property.fixedProperties.forEach(p -> {
+ assertNotNull(p.getDefaultValue());
+ assertFalse(p.getDefaultValue().isBlank());
+ });
+ }
}
diff --git a/server/base/pom.xml b/server/base/pom.xml
index 0f3bc8d98f..edb18c01b9 100644
--- a/server/base/pom.xml
+++ b/server/base/pom.xml
@@ -35,6 +35,10 @@
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.github.ben-manes.caffeine</groupId>
+ <artifactId>caffeine</artifactId>
+ </dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
@@ -100,6 +104,10 @@
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper-jute</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
diff --git a/server/base/src/main/java/org/apache/accumulo/server/ServerContext.java b/server/base/src/main/java/org/apache/accumulo/server/ServerContext.java
index 604e95e033..0242f8386f 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/ServerContext.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/ServerContext.java
@@ -59,13 +59,15 @@ import org.apache.accumulo.core.util.AddressUtil;
import org.apache.accumulo.core.util.Pair;
import org.apache.accumulo.core.util.threads.ThreadPools;
import org.apache.accumulo.core.util.threads.Threads;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
import org.apache.accumulo.fate.zookeeper.ZooReader;
import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
import org.apache.accumulo.server.conf.NamespaceConfiguration;
import org.apache.accumulo.server.conf.ServerConfigurationFactory;
+import org.apache.accumulo.server.conf.SystemConfiguration;
import org.apache.accumulo.server.conf.TableConfiguration;
-import org.apache.accumulo.server.conf.ZooConfiguration;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
import org.apache.accumulo.server.fs.VolumeManager;
import org.apache.accumulo.server.metadata.ServerAmpleImpl;
import org.apache.accumulo.server.rpc.SaslServerConnectionParams;
@@ -76,6 +78,8 @@ import org.apache.accumulo.server.security.SecurityUtil;
import org.apache.accumulo.server.security.delegation.AuthenticationTokenSecretManager;
import org.apache.accumulo.server.tables.TableManager;
import org.apache.accumulo.server.tablets.UniqueNameAllocator;
+import org.apache.accumulo.server.util.NamespacePropUtil;
+import org.apache.accumulo.server.util.TablePropUtil;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.zookeeper.KeeperException;
@@ -92,16 +96,19 @@ public class ServerContext extends ClientContext {
private final ServerInfo info;
private final ZooReaderWriter zooReaderWriter;
private final ServerDirs serverDirs;
+ private final PropStore propStore;
// lazily loaded resources, only loaded when needed
private final Supplier<TableManager> tableManager;
private final Supplier<UniqueNameAllocator> nameAllocator;
private final Supplier<ServerConfigurationFactory> serverConfFactory;
- private final Supplier<AccumuloConfiguration> systemConfig;
+ private final Supplier<SystemConfiguration> systemConfig;
private final Supplier<AuthenticationTokenSecretManager> secretManager;
private final Supplier<CryptoService> cryptoService;
private final Supplier<ScheduledThreadPoolExecutor> sharedScheduledThreadPool;
private final Supplier<AuditedSecurityOperation> securityOperation;
+ private final Supplier<TablePropUtil> tablePropUtilSupplier;
+ private final Supplier<NamespacePropUtil> namespacePropUtilSupplier;
public ServerContext(SiteConfiguration siteConfig) {
this(new ServerInfo(siteConfig));
@@ -112,13 +119,12 @@ public class ServerContext extends ClientContext {
this.info = info;
zooReaderWriter = new ZooReaderWriter(info.getSiteConfiguration());
serverDirs = info.getServerDirs();
+ propStore = ZooPropStore.initialize(info.getInstanceID(), zooReaderWriter);
tableManager = memoize(() -> new TableManager(this));
nameAllocator = memoize(() -> new UniqueNameAllocator(this));
serverConfFactory = memoize(() -> new ServerConfigurationFactory(this, getSiteConfiguration()));
- // system configuration uses its own instance of ZooCache
- // this could be useful to keep its update counter independent
- systemConfig = memoize(() -> new ZooConfiguration(this, new ZooCache(getZooReader(), null),
+ systemConfig = memoize(() -> new SystemConfiguration(this, SystemPropKey.of(getInstanceID()),
getSiteConfiguration()));
secretManager = memoize(() -> new AuthenticationTokenSecretManager(getInstanceID(),
getConfiguration().getTimeInMillis(Property.GENERAL_DELEGATION_TOKEN_LIFETIME)));
@@ -129,6 +135,8 @@ public class ServerContext extends ClientContext {
securityOperation =
memoize(() -> new AuditedSecurityOperation(this, SecurityOperation.getAuthorizor(this),
SecurityOperation.getAuthenticator(this), SecurityOperation.getPermHandler(this)));
+ tablePropUtilSupplier = memoize(() -> new TablePropUtil(this));
+ namespacePropUtilSupplier = memoize(() -> new NamespacePropUtil(this));
}
/**
@@ -434,6 +442,10 @@ public class ServerContext extends ClientContext {
return sharedScheduledThreadPool.get();
}
+ public PropStore getPropStore() {
+ return propStore;
+ }
+
@Override
protected long getTransportPoolMaxAgeMillis() {
return getClientTimeoutInMillis();
@@ -443,4 +455,12 @@ public class ServerContext extends ClientContext {
return securityOperation.get();
}
+ public TablePropUtil tablePropUtil() {
+ return tablePropUtilSupplier.get();
+ }
+
+ public NamespacePropUtil namespacePropUtil() {
+ return namespacePropUtilSupplier.get();
+ }
+
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/NamespaceConfiguration.java b/server/base/src/main/java/org/apache/accumulo/server/conf/NamespaceConfiguration.java
index e3eb342332..2d8b19d615 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/NamespaceConfiguration.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/NamespaceConfiguration.java
@@ -19,123 +19,78 @@
package org.apache.accumulo.server.conf;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
-import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.clientImpl.Namespace;
import org.apache.accumulo.core.conf.AccumuloConfiguration;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.data.NamespaceId;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.apache.accumulo.fate.zookeeper.ZooCacheFactory;
import org.apache.accumulo.server.ServerContext;
-import org.apache.accumulo.server.conf.ZooCachePropertyAccessor.PropCacheKey;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-public class NamespaceConfiguration extends AccumuloConfiguration {
+public class NamespaceConfiguration extends ZooBasedConfiguration {
- private static final Map<PropCacheKey,ZooCache> propCaches = new java.util.HashMap<>();
-
- private final AccumuloConfiguration parent;
- private final AtomicReference<ZooCachePropertyAccessor> propCacheAccessor =
- new AtomicReference<>();
- protected NamespaceId namespaceId = null;
+ private static final Logger log = LoggerFactory.getLogger(NamespaceConfiguration.class);
protected ServerContext context;
- private ZooCacheFactory zcf = new ZooCacheFactory();
- private final String path;
- public NamespaceConfiguration(NamespaceId namespaceId, ServerContext context,
+ public NamespaceConfiguration(ServerContext context, NamespaceId namespaceId,
AccumuloConfiguration parent) {
- this.context = context;
- this.parent = parent;
- this.namespaceId = namespaceId;
- this.path = context.getZooKeeperRoot() + Constants.ZNAMESPACES + "/" + namespaceId
- + Constants.ZNAMESPACE_CONF;
- }
-
- void setZooCacheFactory(ZooCacheFactory zcf) {
- this.zcf = zcf;
- }
-
- private ZooCache getZooCache() {
- synchronized (propCaches) {
- PropCacheKey key = new PropCacheKey(context.getInstanceID(), namespaceId.canonical());
- return propCaches.computeIfAbsent(key,
- k -> zcf.getZooCache(context.getZooKeepers(), context.getZooKeepersSessionTimeOut()));
- }
- }
-
- private ZooCachePropertyAccessor getPropCacheAccessor() {
- // updateAndGet below always calls compare and set, so avoid if not null
- ZooCachePropertyAccessor zcpa = propCacheAccessor.get();
- if (zcpa != null)
- return zcpa;
-
- return propCacheAccessor
- .updateAndGet(pca -> pca == null ? new ZooCachePropertyAccessor(getZooCache()) : pca);
- }
-
- private String getPath() {
- return path;
- }
-
- @Override
- public boolean isPropertySet(Property prop) {
- return getPropCacheAccessor().isPropertySet(prop, getPath()) || parent.isPropertySet(prop);
+ super(log, context, NamespacePropKey.of(context, namespaceId), parent);
}
@Override
public String get(Property property) {
+
String key = property.getKey();
- AccumuloConfiguration getParent;
- if (namespaceId.equals(Namespace.ACCUMULO.id()) && isIteratorOrConstraint(key)) {
+
+ var namespaceId = getPropCacheKey().getId();
+ if (namespaceId != null && namespaceId.equals(Namespace.ACCUMULO.id())
+ && isIteratorOrConstraint(key)) {
// ignore iterators from parent if system namespace
- getParent = null;
- } else {
- getParent = parent;
+ return null;
}
- return getPropCacheAccessor().get(property, getPath(), getParent);
+
+ Map<String,String> theseProps = getSnapshot();
+ String value = theseProps.get(key);
+
+ if (value != null) {
+ return value;
+ }
+
+ return getParent().get(key);
}
+ /**
+ * exclude system iterators/constraints from the system namespace so that they don't affect the
+ * metadata or root tables.
+ */
@Override
public void getProperties(Map<String,String> props, Predicate<String> filter) {
Predicate<String> parentFilter = filter;
// exclude system iterators/constraints from the system namespace
// so they don't affect the metadata or root tables.
if (getNamespaceId().equals(Namespace.ACCUMULO.id()))
- parentFilter = key -> isIteratorOrConstraint(key) ? false : filter.test(key);
+ parentFilter = key -> !isIteratorOrConstraint(key) && filter.test(key);
+
+ getParent().getProperties(props, parentFilter);
- getPropCacheAccessor().getProperties(props, getPath(), filter, parent, parentFilter);
+ getSnapshot().entrySet().stream().filter(e -> filter.test(e.getKey()) && e.getValue() != null)
+ .forEach(e -> props.put(e.getKey(), e.getValue()));
}
protected NamespaceId getNamespaceId() {
- return namespaceId;
+ NamespaceId id = (NamespaceId) getPropCacheKey().getId();
+ if (id == null) {
+ throw new IllegalArgumentException(
+ "Invalid request for namespace id on " + getPropCacheKey());
+ }
+ return id;
}
static boolean isIteratorOrConstraint(String key) {
return key.startsWith(Property.TABLE_ITERATOR_PREFIX.getKey())
|| key.startsWith(Property.TABLE_CONSTRAINT_PREFIX.getKey());
}
-
- @Override
- public synchronized void invalidateCache() {
- ZooCachePropertyAccessor pca = propCacheAccessor.get();
-
- if (pca != null) {
- pca.invalidateCache();
- }
- // Else, if the accessor is null, we could lock and double-check
- // to see if it happened to be created so we could invalidate its cache
- // but I don't see much benefit coming from that extra check.
- }
-
- @Override
- public long getUpdateCount() {
- return parent.getUpdateCount() + getPropCacheAccessor().getZooCache().getUpdateCount();
- }
-
- @Override
- public AccumuloConfiguration getParent() {
- return parent;
- }
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/RuntimeFixedProperties.java b/server/base/src/main/java/org/apache/accumulo/server/conf/RuntimeFixedProperties.java
new file mode 100644
index 0000000000..77f6777bbf
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/RuntimeFixedProperties.java
@@ -0,0 +1,106 @@
+/*
+ * 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.accumulo.server.conf;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.core.conf.SiteConfiguration;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Utility class to a manage a fixed set of defined properties (designated in Properties as fixed).
+ * Certain properties are stored for persistence across restarts, they are read during start-up and
+ * remain unchanged for the life of the instance. Any updates to the properties will only be
+ * reflected with a restart.
+ * <p>
+ * Note that there are no guarantees that all services will always have the same values. If a fixed
+ * property value is changed and if all services are not restarted, they would be operating with
+ * different values.
+ */
+public class RuntimeFixedProperties {
+
+ private static final Logger log = LoggerFactory.getLogger(RuntimeFixedProperties.class);
+
+ // combined site, stored or default props.
+ private final Map<String,String> fixed = new HashMap<>();
+
+ // original, stored fixed props.
+ private final Map<String,String> origStored = new HashMap<>();
+
+ public RuntimeFixedProperties(final Map<String,String> storedProps,
+ final SiteConfiguration siteConfig) {
+ requireNonNull(siteConfig, "a site configuration must be provided");
+ requireNonNull(storedProps, "stored property map must be provided");
+ Property.fixedProperties.forEach(prop -> {
+ String key = prop.getKey();
+ String value = storedProps.get(key);
+ // use value in ZooKeeper
+ if (value != null) {
+ origStored.put(key, value);
+ } else {
+ // Not in ZK, use config or default.
+ value = siteConfig.get(key);
+ }
+ fixed.put(key, value);
+ log.trace("fixed property name: {} = {}", key, value);
+ });
+ }
+
+ @Nullable
+ public String get(final Property property) {
+ return fixed.get(property.getKey());
+ }
+
+ @VisibleForTesting
+ Map<String,String> getAll() {
+ return Collections.unmodifiableMap(fixed);
+ }
+
+ /**
+ * Test if a property has changed since this instance was created. If a property has changed, it
+ * will take effect on next restart.
+ *
+ * @param current
+ * a map of the current stored properties.
+ * @return true if changed and restart required, false otherwise
+ */
+ @VisibleForTesting
+ boolean hasChanged(final Map<String,String> current) {
+ for (Property prop : Property.fixedProperties) {
+ var key = prop.getKey();
+ if (current.containsKey(key)) {
+ if (!current.get(key).equals(fixed.get(key))) {
+ return true;
+ }
+ } else if (origStored.containsKey(key)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/ServerConfigurationFactory.java b/server/base/src/main/java/org/apache/accumulo/server/conf/ServerConfigurationFactory.java
index d968af064e..ac7dc1ce51 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/ServerConfigurationFactory.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/ServerConfigurationFactory.java
@@ -29,9 +29,8 @@ import org.apache.accumulo.core.conf.DefaultConfiguration;
import org.apache.accumulo.core.conf.SiteConfiguration;
import org.apache.accumulo.core.data.NamespaceId;
import org.apache.accumulo.core.data.TableId;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.apache.accumulo.fate.zookeeper.ZooCacheFactory;
import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
import com.google.common.base.Suppliers;
@@ -45,33 +44,21 @@ public class ServerConfigurationFactory extends ServerConfiguration {
private final Map<NamespaceId,NamespaceConfiguration> namespaceConfigs =
new ConcurrentHashMap<>();
- private final Supplier<ZooConfiguration> systemConfig;
-
private final ServerContext context;
private final SiteConfiguration siteConfig;
- private ZooCacheFactory zcf = new ZooCacheFactory();
+ private final Supplier<SystemConfiguration> systemConfig;
public ServerConfigurationFactory(ServerContext context, SiteConfiguration siteConfig) {
this.context = context;
this.siteConfig = siteConfig;
- systemConfig = Suppliers.memoize(() -> {
- // Force the creation of a new ZooCache instead of using a shared one.
- // This is done so that the ZooCache will update less often, causing the
- // configuration update count to increment more slowly.
- ZooCache propCache =
- zcf.getNewZooCache(context.getZooKeepers(), context.getZooKeepersSessionTimeOut());
- return new ZooConfiguration(context, propCache, siteConfig);
- });
+ systemConfig = Suppliers.memoize(
+ () -> new SystemConfiguration(context, SystemPropKey.of(context), getSiteConfiguration()));
}
public ServerContext getServerContext() {
return context;
}
- void setZooCacheFactory(ZooCacheFactory zcf) {
- this.zcf = zcf;
- }
-
public SiteConfiguration getSiteConfiguration() {
return siteConfig;
}
@@ -112,8 +99,7 @@ public class ServerConfigurationFactory extends ServerConfiguration {
@Override
public NamespaceConfiguration getNamespaceConfiguration(NamespaceId namespaceId) {
return namespaceConfigs.computeIfAbsent(namespaceId, key -> {
- var conf = new NamespaceConfiguration(namespaceId, context, getSystemConfiguration());
- conf.setZooCacheFactory(zcf);
+ var conf = new NamespaceConfiguration(context, namespaceId, getSystemConfiguration());
ConfigCheckUtil.validate(conf);
return conf;
});
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/SystemConfiguration.java b/server/base/src/main/java/org/apache/accumulo/server/conf/SystemConfiguration.java
new file mode 100644
index 0000000000..661f3fbcae
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/SystemConfiguration.java
@@ -0,0 +1,66 @@
+/*
+ * 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.accumulo.server.conf;
+
+import org.apache.accumulo.core.conf.AccumuloConfiguration;
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SystemConfiguration extends ZooBasedConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(SystemConfiguration.class);
+
+ private final RuntimeFixedProperties runtimeFixedProps;
+
+ public SystemConfiguration(ServerContext context, SystemPropKey propCacheKey,
+ AccumuloConfiguration parent) {
+ super(log, context, propCacheKey, parent);
+ runtimeFixedProps = new RuntimeFixedProperties(getSnapshot(), context.getSiteConfiguration());
+ }
+
+ @Override
+ public String get(Property property) {
+ log.trace("system config get() - property request for {}", property);
+ if (Property.isFixedZooPropertyKey(property))
+ return runtimeFixedProps.get(property);
+
+ String key = property.getKey();
+ String value = null;
+ if (Property.isValidZooPropertyKey(key)) {
+ value = getSnapshot().get(key);
+ }
+
+ if (value == null || !property.getType().isValidFormat(value)) {
+ if (value != null) {
+ log.error("Using parent value for {} due to improperly formatted {}: {}", key,
+ property.getType(), value);
+ }
+ value = getParent().get(property);
+ }
+ return value;
+ }
+
+ @Override
+ public boolean isPropertySet(Property prop) {
+ return runtimeFixedProps.get(prop) != null || super.isPropertySet(prop);
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java b/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java
index 7de0d53bda..14be47a739 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/TableConfiguration.java
@@ -18,19 +18,14 @@
*/
package org.apache.accumulo.server.conf;
-import static java.util.Objects.requireNonNull;
-
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
import java.util.stream.Collectors;
-import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.classloader.ClassLoaderUtil;
import org.apache.accumulo.core.conf.AccumuloConfiguration;
import org.apache.accumulo.core.conf.Property;
@@ -41,21 +36,16 @@ import org.apache.accumulo.core.iteratorsImpl.IteratorConfigUtil;
import org.apache.accumulo.core.spi.common.ServiceEnvironment;
import org.apache.accumulo.core.spi.compaction.CompactionDispatcher;
import org.apache.accumulo.core.spi.scan.ScanDispatcher;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.apache.accumulo.fate.zookeeper.ZooCacheFactory;
import org.apache.accumulo.server.ServerContext;
import org.apache.accumulo.server.ServiceEnvironmentImpl;
-import org.apache.accumulo.server.conf.ZooCachePropertyAccessor.PropCacheKey;
-
-public class TableConfiguration extends AccumuloConfiguration {
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
- private static final Map<PropCacheKey,ZooCache> propCaches = new java.util.HashMap<>();
+public class TableConfiguration extends ZooBasedConfiguration {
- private final AtomicReference<ZooCachePropertyAccessor> propCacheAccessor =
- new AtomicReference<>();
- private final ServerContext context;
- private final NamespaceConfiguration parent;
- private ZooCacheFactory zcf = new ZooCacheFactory();
+ private final static Logger log = LoggerFactory.getLogger(TableConfiguration.class);
private final TableId tableId;
@@ -65,9 +55,8 @@ public class TableConfiguration extends AccumuloConfiguration {
private final Deriver<CompactionDispatcher> compactionDispatchDeriver;
public TableConfiguration(ServerContext context, TableId tableId, NamespaceConfiguration parent) {
- this.context = requireNonNull(context);
- this.tableId = requireNonNull(tableId);
- this.parent = requireNonNull(parent);
+ super(log, context, TablePropKey.of(context, tableId), parent);
+ this.tableId = tableId;
iteratorConfig = new EnumMap<>(IteratorScope.class);
for (IteratorScope scope : IteratorScope.values()) {
@@ -84,78 +73,51 @@ public class TableConfiguration extends AccumuloConfiguration {
newDeriver(conf -> createCompactionDispatcher(conf, context, tableId));
}
- void setZooCacheFactory(ZooCacheFactory zcf) {
- this.zcf = zcf;
- }
-
- private ZooCache getZooCache() {
- synchronized (propCaches) {
- PropCacheKey key = new PropCacheKey(context.getInstanceID(), tableId.canonical());
- ZooCache propCache = propCaches.get(key);
- if (propCache == null) {
- propCache = zcf.getZooCache(context.getZooKeepers(), context.getZooKeepersSessionTimeOut());
- propCaches.put(key, propCache);
- }
- return propCache;
- }
- }
-
- private ZooCachePropertyAccessor getPropCacheAccessor() {
- // updateAndGet below always calls compare and set, so avoid if not null
- ZooCachePropertyAccessor zcpa = propCacheAccessor.get();
- if (zcpa != null) {
- return zcpa;
+ @Override
+ public boolean isPropertySet(Property prop) {
+ if (_isPropertySet(prop)) {
+ return true;
}
- return propCacheAccessor
- .updateAndGet(pca -> pca == null ? new ZooCachePropertyAccessor(getZooCache()) : pca);
+ return getParent().isPropertySet(prop);
}
- private String getPath() {
- return context.getZooKeeperRoot() + Constants.ZTABLES + "/" + tableId + Constants.ZTABLE_CONF;
- }
-
- @Override
- public boolean isPropertySet(Property prop) {
- return getPropCacheAccessor().isPropertySet(prop, getPath()) || parent.isPropertySet(prop);
+ private boolean _isPropertySet(Property property) {
+ Map<String,String> propMap = getSnapshot();
+ return propMap.get(property.getKey()) != null;
}
@Override
public String get(Property property) {
- return getPropCacheAccessor().get(property, getPath(), parent);
+ String value = _get(property);
+ if (value != null) {
+ return value;
+ }
+ AccumuloConfiguration parent = getParent();
+ if (parent != null) {
+ return parent.get(property);
+ }
+ return null;
}
- @Override
- public void getProperties(Map<String,String> props, Predicate<String> filter) {
- getPropCacheAccessor().getProperties(props, getPath(), filter, parent, null);
+ @Nullable
+ private String _get(Property property) {
+ Map<String,String> propMap = getSnapshot();
+ if (propMap == null) {
+ return null;
+ }
+ return propMap.get(property.getKey());
}
public TableId getTableId() {
return tableId;
}
- @Override
- public synchronized void invalidateCache() {
- ZooCachePropertyAccessor pca = propCacheAccessor.get();
-
- if (pca != null) {
- pca.invalidateCache();
- }
- // Else, if the accessor is null, we could lock and double-check
- // to see if it happened to be created so we could invalidate its cache
- // but I don't see much benefit coming from that extra check.
- }
-
@Override
public String toString() {
return this.getClass().getSimpleName();
}
- @Override
- public long getUpdateCount() {
- return parent.getUpdateCount() + getPropCacheAccessor().getZooCache().getUpdateCount();
- }
-
public static class ParsedIteratorConfig {
private final List<IterInfo> tableIters;
private final Map<String,Map<String,String>> tableOpts;
@@ -219,6 +181,7 @@ public class TableConfiguration extends AccumuloConfiguration {
private static CompactionDispatcher createCompactionDispatcher(AccumuloConfiguration conf,
ServerContext context, TableId tableId) {
+
CompactionDispatcher newDispatcher = Property.createTableInstanceFromPropertyName(conf,
Property.TABLE_COMPACTION_DISPATCHER, CompactionDispatcher.class, null);
@@ -255,9 +218,4 @@ public class TableConfiguration extends AccumuloConfiguration {
public CompactionDispatcher getCompactionDispatcher() {
return compactionDispatchDeriver.derive();
}
-
- @Override
- public AccumuloConfiguration getParent() {
- return parent;
- }
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/ZooBasedConfiguration.java b/server/base/src/main/java/org/apache/accumulo/server/conf/ZooBasedConfiguration.java
new file mode 100644
index 0000000000..2a847af26b
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/ZooBasedConfiguration.java
@@ -0,0 +1,164 @@
+/*
+ * 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.accumulo.server.conf;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+import org.apache.accumulo.core.conf.AccumuloConfiguration;
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.util.PropSnapshot;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Instances maintain a local cache of the AccumuloConfiguration hierarchy that will be consistent
+ * with stored properties.
+ * <p>
+ * When calling getProperties - the local copy will be updated if ZooKeeper changes have been
+ * received.
+ * <p>
+ * The getUpdateCount() provides an optimization for clients - the count can be used to detect
+ * changes without reading the properties. When the update count changes, the next getProperties
+ * call will update the local copy and the change count.
+ */
+public class ZooBasedConfiguration extends AccumuloConfiguration {
+
+ protected final Logger log;
+ private final AccumuloConfiguration parent;
+ private final PropCacheKey<?> propCacheKey;
+
+ private final PropSnapshot propSnapshot;
+
+ public ZooBasedConfiguration(Logger log, ServerContext context, PropCacheKey<?> propCacheKey,
+ AccumuloConfiguration parent) {
+ this.log = requireNonNull(log, "a Logger must be supplied");
+ requireNonNull(context, "the context cannot be null");
+ this.propCacheKey = requireNonNull(propCacheKey, "a PropCacheId must be supplied");
+ this.parent = requireNonNull(parent, "An AccumuloConfiguration parent must be supplied");
+
+ propSnapshot = PropSnapshot.create(propCacheKey, context.getPropStore());
+ }
+
+ @VisibleForTesting
+ public void zkChangeEvent(PropCacheKey<?> propCacheKey) {
+ propSnapshot.zkChangeEvent(propCacheKey);
+ }
+
+ public long getDataVersion() {
+ return propSnapshot.getVersionedProperties().getDataVersion();
+ }
+
+ /**
+ * The update count is the sum of the change count of this configuration and the change counts of
+ * the parents. The count is used to detect if any changes occurred in the configuration hierarchy
+ * and if the configuration needs to be recalculated to maintain consistency with values in the
+ * backend store.
+ * <p>
+ * The count is required to be an increasing value.
+ */
+ @Override
+ public long getUpdateCount() {
+ long count = 0;
+ long dataVersion = 0;
+ for (AccumuloConfiguration p = this; p != null; p = p.getParent()) {
+ if (p instanceof ZooBasedConfiguration) {
+ dataVersion = ((ZooBasedConfiguration) p).getDataVersion();
+ } else {
+ dataVersion = p.getUpdateCount();
+ }
+ count += dataVersion;
+ }
+
+ log.trace("update count result for: {} - data version: {} update: {}", propCacheKey,
+ dataVersion, count);
+ return count;
+ }
+
+ @Override
+ public AccumuloConfiguration getParent() {
+ return parent;
+ }
+
+ public PropCacheKey<?> getPropCacheKey() {
+ return propCacheKey;
+ }
+
+ @Override
+ public @Nullable String get(final Property property) {
+ Map<String,String> props = getSnapshot();
+ String value = props.get(property.getKey());
+ if (value != null) {
+ return value;
+ }
+ AccumuloConfiguration parent = getParent();
+ if (parent != null) {
+ return parent.get(property);
+ }
+ return null;
+ }
+
+ @Override
+ public void getProperties(final Map<String,String> props, final Predicate<String> filter) {
+
+ parent.getProperties(props, filter);
+
+ Map<String,String> theseProps = getSnapshot();
+
+ log.trace("getProperties() for: {} filter: {}, have: {}, passed: {}", getPropCacheKey(), filter,
+ theseProps, props);
+
+ for (Map.Entry<String,String> p : theseProps.entrySet()) {
+ if (filter.test(p.getKey()) && p.getValue() != null) {
+ log.trace("passed filter - add to map: {} = {}", p.getKey(), p.getValue());
+ props.put(p.getKey(), p.getValue());
+ }
+ }
+ }
+
+ @Override
+ public boolean isPropertySet(final Property property) {
+
+ Map<String,String> theseProps = getSnapshot();
+
+ if (theseProps.get(property.getKey()) != null) {
+ return true;
+ }
+
+ return getParent().isPropertySet(property);
+
+ }
+
+ public @NonNull Map<String,String> getSnapshot() {
+ return propSnapshot.getVersionedProperties().asMap();
+ }
+
+ @Override
+ public void invalidateCache() {
+ propSnapshot.requireUpdate();
+ }
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/ZooCachePropertyAccessor.java b/server/base/src/main/java/org/apache/accumulo/server/conf/ZooCachePropertyAccessor.java
deleted file mode 100644
index 9e6799b1f4..0000000000
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/ZooCachePropertyAccessor.java
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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.accumulo.server.conf;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Predicate;
-
-import org.apache.accumulo.core.conf.AccumuloConfiguration;
-import org.apache.accumulo.core.conf.Property;
-import org.apache.accumulo.core.data.InstanceId;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A helper object for accessing properties in a {@link ZooCache}.
- */
-public class ZooCachePropertyAccessor {
- private static final Logger log = LoggerFactory.getLogger(ZooCachePropertyAccessor.class);
-
- static class PropCacheKey {
- final InstanceId instanceId;
- final String scope;
-
- PropCacheKey(InstanceId iid, String s) {
- instanceId = iid;
- scope = s;
- }
-
- @Override
- public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
- if (!(other instanceof PropCacheKey)) {
- return false;
- }
- PropCacheKey o = (PropCacheKey) other;
- return (instanceId.equals(o.instanceId) && scope.equals(o.scope));
- }
-
- @Override
- public int hashCode() {
- int c = 17;
- c = (37 * c) + instanceId.hashCode();
- c = (37 * c) + scope.hashCode();
- return c;
- }
- }
-
- private final ZooCache propCache;
-
- /**
- * Creates a new accessor.
- *
- * @param propCache
- * property cache
- */
- ZooCachePropertyAccessor(ZooCache propCache) {
- this.propCache = propCache;
- }
-
- /**
- * Gets the property cache accessed by this object.
- *
- * @return property cache
- */
- ZooCache getZooCache() {
- return propCache;
- }
-
- boolean isPropertySet(Property property, String path) {
- return propCache.get(path + "/" + property.getKey()) != null;
- }
-
- /**
- * Gets a property. If the property is not in ZooKeeper or is present but an invalid format for
- * the property type, the parent configuration is consulted (if provided).
- *
- * @param property
- * property to get
- * @param path
- * ZooKeeper path where properties lie
- * @param parent
- * parent configuration (optional)
- * @return property value, or null if not found
- */
- String get(Property property, String path, AccumuloConfiguration parent) {
- String key = property.getKey();
- String value = get(path + "/" + key);
-
- if (value == null || !property.getType().isValidFormat(value)) {
- if (value != null) {
- log.error("Using default value for {} due to improperly formatted {}: {}", key,
- property.getType(), value);
- }
- if (parent != null) {
- value = parent.get(property);
- }
- }
- return value;
- }
-
- private String get(String path) {
- byte[] v = propCache.get(path);
- if (v != null) {
- return new String(v, UTF_8);
- } else {
- return null;
- }
- }
-
- /**
- * Gets all properties into the given map. Properties are filtered using the given filter.
- * Properties from a parent configuration are also added to the map and filtered, either using a
- * separate filter or, if not specified, the other filter.
- *
- * @param props
- * map to populate with properties
- * @param path
- * ZooKeeper path where properties lie
- * @param filter
- * property filter (required)
- * @param parent
- * parent configuration (required)
- * @param parentFilter
- * separate filter for parent properties (optional)
- */
- void getProperties(Map<String,String> props, String path, Predicate<String> filter,
- AccumuloConfiguration parent, Predicate<String> parentFilter) {
- parent.getProperties(props, parentFilter != null ? parentFilter : filter);
-
- List<String> children = propCache.getChildren(path);
- if (children != null) {
- for (String child : children) {
- if (child != null && filter.test(child)) {
- String value = get(path + "/" + child);
- if (value != null) {
- props.put(child, value);
- }
- }
- }
- }
- }
-
- /**
- * Clears the internal {@link ZooCache}.
- */
- void invalidateCache() {
- propCache.clear();
- }
-
-}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/ZooConfiguration.java b/server/base/src/main/java/org/apache/accumulo/server/conf/ZooConfiguration.java
deleted file mode 100644
index 7d2306b6d7..0000000000
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/ZooConfiguration.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.accumulo.server.conf;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Predicate;
-
-import org.apache.accumulo.core.Constants;
-import org.apache.accumulo.core.conf.AccumuloConfiguration;
-import org.apache.accumulo.core.conf.Property;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.apache.accumulo.fate.zookeeper.ZooReader;
-import org.apache.accumulo.server.ServerContext;
-import org.apache.zookeeper.KeeperException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class ZooConfiguration extends AccumuloConfiguration {
-
- private static final Logger log = LoggerFactory.getLogger(ZooConfiguration.class);
-
- private final ServerContext context;
- private final ZooCache propCache;
- private final AccumuloConfiguration parent;
- private final Map<String,String> fixedProps = Collections.synchronizedMap(new HashMap<>());
- private final String propPathPrefix;
-
- public ZooConfiguration(ServerContext context, ZooCache propCache, AccumuloConfiguration parent) {
- this.context = context;
- this.propCache = propCache;
- this.parent = parent;
- this.propPathPrefix = context.getZooKeeperRoot() + Constants.ZCONFIG;
- }
-
- @Override
- public void invalidateCache() {
- if (propCache != null)
- propCache.clear();
- }
-
- private String _get(Property property) {
- String key = property.getKey();
- String value = null;
-
- if (Property.isValidZooPropertyKey(key)) {
- value = getRaw(key);
- }
-
- if (value == null || !property.getType().isValidFormat(value)) {
- if (value != null)
- log.error("Using parent value for {} due to improperly formatted {}: {}", key,
- property.getType(), value);
- value = parent.get(property);
- }
- return value;
- }
-
- @Override
- public String get(Property property) {
- if (Property.isFixedZooPropertyKey(property)) {
- String val = fixedProps.get(property.getKey());
- if (val != null) {
- return val;
- } else {
- synchronized (fixedProps) {
- val = _get(property);
- fixedProps.put(property.getKey(), val);
- return val;
- }
-
- }
- } else {
- return _get(property);
- }
- }
-
- @Override
- public boolean isPropertySet(Property prop) {
- if (fixedProps.containsKey(prop.getKey())) {
- return true;
- }
- ZooReader zr = context.getZooReaderWriter();
- String zPath = propPathPrefix + "/" + prop.getKey();
- try {
- if (zr.exists(zPath)) {
- return true;
- }
- } catch (KeeperException | InterruptedException e) {
- throw new IllegalStateException(e);
- }
- return parent.isPropertySet(prop);
- }
-
- private String getRaw(String key) {
- String zPath = propPathPrefix + "/" + key;
- byte[] v = propCache.get(zPath);
- String value = null;
- if (v != null)
- value = new String(v, UTF_8);
- return value;
- }
-
- @Override
- public void getProperties(Map<String,String> props, Predicate<String> filter) {
- parent.getProperties(props, filter);
-
- List<String> children = propCache.getChildren(propPathPrefix);
- if (children != null) {
- for (String child : children) {
- if (child != null && filter.test(child)) {
- String value = getRaw(child);
- if (value != null)
- props.put(child, value);
- }
- }
- }
- }
-
- @Override
- public long getUpdateCount() {
- return parent.getUpdateCount() + propCache.getUpdateCount();
- }
-
- @Override
- public AccumuloConfiguration getParent() {
- return parent;
- }
-}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropCodec.java b/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropCodec.java
index 43a2c4f160..96045e3c05 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropCodec.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropCodec.java
@@ -18,7 +18,7 @@
*/
package org.apache.accumulo.server.conf.codec;
-import static org.apache.accumulo.server.conf.codec.VersionedProperties.tsFormatter;
+import static org.apache.accumulo.server.conf.codec.VersionedProperties.TIMESTAMP_FORMATTER;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -28,9 +28,12 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;
+import java.time.format.DateTimeParseException;
import java.util.HashMap;
import java.util.Map;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
/**
* Abstract class to provide encoding / decoding of versioned properties. This class handles the
* serialization of the metadata and subclasses are required to implement
@@ -55,6 +58,10 @@ public abstract class VersionedPropCodec {
this.encodingOpts = encodingOpts;
}
+ public static VersionedPropCodec getDefault() {
+ return VersionedPropGzipCodec.codec(true);
+ }
+
/**
* The general encoding options that apply to all encodings.
*
@@ -77,13 +84,11 @@ public abstract class VersionedPropCodec {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos)) {
- // write encoding metadata
+ // write encoding info
encodingOpts.encode(dos);
- // write version metadata
- DataVersionInfo vMetadata =
- new DataVersionInfo(vProps.getNextVersion(), vProps.getTimestamp());
- vMetadata.write(dos);
+ // write prop metadata
+ dos.writeUTF(TIMESTAMP_FORMATTER.format(vProps.getTimestamp()));
// delegate property encoding to sub-class
encodePayload(bos, vProps, encodingOpts);
@@ -108,7 +113,19 @@ public abstract class VersionedPropCodec {
abstract void encodePayload(final OutputStream out, final VersionedProperties vProps,
final EncodingOptions encodingOpts) throws IOException;
- public VersionedProperties fromBytes(final byte[] bytes) throws IOException {
+ /**
+ *
+ *
+ * @param version
+ * the data version determined by the prop store
+ * @param bytes
+ * an array of bytes created using a PropCodec.
+ * @return the versioned properties.
+ * @throws IOException
+ * if the an error occurs reading from the byte array.
+ */
+ public @NonNull VersionedProperties fromBytes(final int version, final byte[] bytes)
+ throws IOException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
DataInputStream dis = new DataInputStream(bis)) {
@@ -121,11 +138,13 @@ public abstract class VersionedPropCodec {
+ encodingOpts.getEncodingVersion());
}
- DataVersionInfo vMetadata = DataVersionInfo.fromDataStream(dis);
+ var timestamp = TIMESTAMP_FORMATTER.parse(dis.readUTF(), Instant::from);
Map<String,String> props = decodePayload(bis, encodingOpts);
- return new VersionedProperties(vMetadata.getDataVersion(), vMetadata.getTimestamp(), props);
+ return new VersionedProperties(version, timestamp, props);
+ } catch (NullPointerException | DateTimeParseException ex) {
+ throw new IllegalArgumentException("Invalid data cannot decode byte array", ex);
}
}
@@ -152,28 +171,26 @@ public abstract class VersionedPropCodec {
}
/**
- * Extracts the data version from the encoded byte array without fully decoding the payload.
- * Normally the data version should be obtained from a fully decoded instance of the versioned
- * properties.
+ * Extracts the timestamp from the encoded byte array without fully decoding the payload. Normally
+ * the timestamp should be obtained from a fully decoded instance of the versioned properties.
* <p>
* The cost of reading the byte array from the backing store should be considered verses the
* additional cost of decoding - with a goal of reducing data reads from the store preferred.
* Generally reading from the store will be followed by some sort of usage which would require the
- * full decode operation anyway.
+ * full decode operation anyway, so uses of this method should be narrow and limited.
*
* @param bytes
* serialized encoded versioned property byte array.
- * @return the encoding version used to serialize the properties.
+ * @return the timestamp used to serialize the properties.
*/
- public static int getDataVersion(final byte[] bytes) {
+ public static Instant readTimestamp(final byte[] bytes) {
try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
DataInputStream dis = new DataInputStream(bis)) {
- // skip encoding metadata
+ // read encoding metadata according to the options.
EncodingOptions.fromDataStream(dis);
- return DataVersionInfo.fromDataStream(dis).getDataVersion();
- } catch (NullPointerException | IOException ex) {
- throw new IllegalArgumentException(
- "Failed to read data version version from byte array provided", ex);
+ return TIMESTAMP_FORMATTER.parse(dis.readUTF(), Instant::from);
+ } catch (NullPointerException | DateTimeParseException | IOException ex) {
+ throw new IllegalArgumentException("Failed to read timestamp from byte array provided", ex);
}
}
@@ -247,39 +264,4 @@ public abstract class VersionedPropCodec {
dos.flush();
}
- /**
- * Helper class for reading / writing versioned properties metadata.
- */
- static class DataVersionInfo {
- private final int dataVersion;
- private final Instant timestamp;
-
- public DataVersionInfo(final int dataVersion, final Instant timestamp) {
- this.dataVersion = dataVersion;
- this.timestamp = timestamp;
- }
-
- public static DataVersionInfo fromDataStream(final DataInputStream dis) throws IOException {
- try {
- var dataVersion = dis.readInt();
- var timestamp = tsFormatter.parse(dis.readUTF(), Instant::from);
- return new DataVersionInfo(dataVersion, timestamp);
- } catch (Exception ex) {
- throw new IOException("Could not parse data version info", ex);
- }
- }
-
- public int getDataVersion() {
- return dataVersion;
- }
-
- public Instant getTimestamp() {
- return timestamp;
- }
-
- public void write(final DataOutputStream dos) throws IOException {
- dos.writeInt(dataVersion);
- dos.writeUTF(tsFormatter.format(timestamp));
- }
- }
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodec.java b/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodec.java
index 2a2ef0cbd5..ec6cd50820 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodec.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodec.java
@@ -45,7 +45,7 @@ public class VersionedPropGzipCodec extends VersionedPropCodec {
void encodePayload(final OutputStream out, final VersionedProperties vProps,
final EncodingOptions encodingOpts) throws IOException {
- Map<String,String> props = vProps.getProperties();
+ Map<String,String> props = vProps.asMap();
if (getEncodingOpts().isCompressed()) {
// Write the property map to the output stream, compressing the output using GZip
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedProperties.java b/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedProperties.java
index d64254e81f..957f979837 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedProperties.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/codec/VersionedProperties.java
@@ -29,6 +29,8 @@ import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
/**
* Version properties maintain a {@code Map<String,String>}; of property k,v pairs along with
* versioning information metadata.
@@ -46,10 +48,10 @@ import java.util.TreeMap;
*/
public class VersionedProperties {
- public static final DateTimeFormatter tsFormatter =
+ public static final DateTimeFormatter TIMESTAMP_FORMATTER =
DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.from(ZoneOffset.UTC));
// flag value for initialization - on store both the version and next version should be 0.
- private static final int NO_VERSION = -2;
+ private static final int INIT_VERSION = 0;
private final int dataVersion;
private final Instant timestamp;
private final Map<String,String> props;
@@ -69,7 +71,7 @@ public class VersionedProperties {
* been previously validated (if required)
*/
public VersionedProperties(Map<String,String> props) {
- this(NO_VERSION, Instant.now(), props);
+ this(INIT_VERSION, Instant.now(), props);
}
/**
@@ -95,7 +97,7 @@ public class VersionedProperties {
*
* @return An unmodifiable view of the property key, value pairs.
*/
- public Map<String,String> getProperties() {
+ public @NonNull Map<String,String> asMap() {
return props;
}
@@ -108,22 +110,7 @@ public class VersionedProperties {
* @return 0 for initial version, otherwise the data version when the properties were serialized.
*/
public int getDataVersion() {
- return Math.max(dataVersion, 0);
- }
-
- /**
- * Calculates the version that should be stored when serialized. The serialized version, when
- * stored, should match the version that will be assigned. This way, data reading the serialized
- * version can compare the stored version with the node version at any time to detect if the node
- * version has been updated.
- * <p>
- * The initialization of the data version to a negative value allows this value to be calculated
- * correctly for the first serialization. On the first store, the expected version will be 0.
- *
- * @return the next version number that should be serialized, or 0 if this is the initial version.
- */
- public int getNextVersion() {
- return Math.max(dataVersion + 1, 0);
+ return dataVersion;
}
/**
@@ -142,7 +129,7 @@ public class VersionedProperties {
* @return a formatted timestamp string.
*/
public String getTimestampISO() {
- return tsFormatter.format(timestamp);
+ return TIMESTAMP_FORMATTER.format(timestamp);
}
/**
@@ -216,7 +203,8 @@ public class VersionedProperties {
sb.append("dataVersion=").append(dataVersion).append(prettyPrint ? "\n" : ", ");
- sb.append("timeStamp=").append(tsFormatter.format(timestamp)).append(prettyPrint ? "\n" : ", ");
+ sb.append("timeStamp=").append(TIMESTAMP_FORMATTER.format(timestamp))
+ .append(prettyPrint ? "\n" : ", ");
Map<String,String> sorted = new TreeMap<>(props);
sorted.forEach((k, v) -> {
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/NamespacePropKey.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/NamespacePropKey.java
new file mode 100644
index 0000000000..b13fb4d8b8
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/NamespacePropKey.java
@@ -0,0 +1,65 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+import static org.apache.accumulo.core.Constants.ZNAMESPACES;
+import static org.apache.accumulo.core.Constants.ZNAMESPACE_CONF;
+import static org.apache.accumulo.core.Constants.ZTABLE_CONF;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.NamespaceId;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+public class NamespacePropKey extends PropCacheKey<NamespaceId> {
+
+ private NamespacePropKey(final InstanceId instanceId, final String path, final NamespaceId id) {
+ super(instanceId, path, id);
+ }
+
+ private static String getNodeName(final InstanceId instanceId, final NamespaceId id) {
+ return ZooUtil.getRoot(instanceId) + ZNAMESPACES + "/" + id.canonical() + ZTABLE_CONF + "/"
+ + PROP_NODE_NAME;
+ }
+
+ public static NamespacePropKey of(final ServerContext context, final NamespaceId id) {
+ return of(context.getInstanceID(), id);
+ }
+
+ public static NamespacePropKey of(final InstanceId instanceId, final NamespaceId id) {
+ return new NamespacePropKey(instanceId, getNodePath(instanceId, id), id);
+ }
+
+ private static String getNodePath(final InstanceId instanceId, final NamespaceId id) {
+ return ZooUtil.getRoot(instanceId) + ZNAMESPACES + "/" + id.canonical() + ZNAMESPACE_CONF + "/"
+ + PROP_NODE_NAME;
+ }
+
+ @Override
+ public @NonNull String getNodePath() {
+ return getNodeName(instanceId, id);
+ }
+
+ @Override
+ @NonNull
+ public String getBasePath() {
+ return ZooUtil.getRoot(instanceId) + ZNAMESPACES + "/" + id.canonical() + ZNAMESPACE_CONF;
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropCache.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropCache.java
new file mode 100644
index 0000000000..9d50189a17
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropCache.java
@@ -0,0 +1,36 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public interface PropCache {
+
+ @Nullable
+ VersionedProperties get(final PropCacheKey<?> propCacheKey);
+
+ void remove(final PropCacheKey<?> propCacheKey);
+
+ void removeAll();
+
+ @Nullable
+ VersionedProperties getWithoutCaching(PropCacheKey<?> propCacheKey);
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropCacheKey.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropCacheKey.java
new file mode 100644
index 0000000000..171a3f8274
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropCacheKey.java
@@ -0,0 +1,141 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+import static org.apache.accumulo.core.Constants.ZNAMESPACES;
+import static org.apache.accumulo.core.Constants.ZTABLES;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+import org.apache.accumulo.core.data.AbstractId;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.NamespaceId;
+import org.apache.accumulo.core.data.TableId;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides a strongly-typed id for storing properties in ZooKeeper. The path in ZooKeeper is
+ * determined by the instance id and the type (system, namespace and table), with different root
+ * paths.
+ * <p>
+ * Provides utility methods from constructing different id based on type and methods to parse a
+ * ZooKeeper path and return a prop cache id.
+ */
+public abstract class PropCacheKey<ID_TYPE extends AbstractId<ID_TYPE>>
+ implements Comparable<PropCacheKey<ID_TYPE>> {
+
+ public static final String PROP_NODE_NAME = "encoded_props";
+
+ private static final Logger log = LoggerFactory.getLogger(PropCacheKey.class);
+
+ // indices for path.split();
+ public static final int TYPE_TOKEN_POSITION = 3;
+ public static final int IID_TOKEN_POSITION = 2;
+ public static final int ID_TOKEN_POSITION = 4;
+
+ // remove starting slash from constant.
+ public static final String TABLES_NODE_NAME = ZTABLES.substring(1);
+ public static final String NAMESPACE_NODE_NAME = ZNAMESPACES.substring(1);
+
+ protected final InstanceId instanceId;
+ protected final ID_TYPE id;
+
+ private final String path;
+
+ protected PropCacheKey(final InstanceId instanceId, final String path, final ID_TYPE id) {
+ this.instanceId = instanceId;
+ this.path = path;
+ this.id = id;
+ }
+
+ public @NonNull String getPath() {
+ return path;
+ }
+
+ public @NonNull abstract String getBasePath();
+
+ public @NonNull abstract String getNodePath();
+
+ public @NonNull ID_TYPE getId() {
+ return id;
+ }
+
+ /**
+ * Determine the prop cache id from a ZooKeeper path
+ *
+ * @param path
+ * the path
+ * @return the prop cache id
+ */
+ public static @Nullable PropCacheKey<?> fromPath(final String path) {
+ String[] tokens = path.split("/");
+
+ InstanceId instanceId;
+ try {
+ instanceId = InstanceId.of(tokens[IID_TOKEN_POSITION]);
+ } catch (ArrayIndexOutOfBoundsException ex) {
+ log.warn("Path '{}' is an invalid path for a property cache key", path);
+ return null;
+ }
+ if (tokens.length < 1 || !tokens[tokens.length - 1].equals(PROP_NODE_NAME)) {
+ // without tokens or it does not end with PROP_NAME_NAME
+ return null;
+ }
+ if (tokens[TYPE_TOKEN_POSITION].equals(TABLES_NODE_NAME)) {
+ return TablePropKey.of(instanceId, TableId.of(tokens[ID_TOKEN_POSITION]));
+ }
+ if (tokens[TYPE_TOKEN_POSITION].equals(NAMESPACE_NODE_NAME)) {
+ return NamespacePropKey.of(instanceId, NamespaceId.of(tokens[ID_TOKEN_POSITION]));
+ }
+ return SystemPropKey.of(instanceId);
+ }
+
+ @Override
+ public int compareTo(@NonNull PropCacheKey<ID_TYPE> other) {
+ return Comparator.comparing(PropCacheKey<ID_TYPE>::getPath).compare(this, other);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ PropCacheKey<?> that = (PropCacheKey<?>) o;
+ if (getId().getClass() != that.getId().getClass()) {
+ return false;
+ }
+ return path.equals(that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(path);
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + "{" + getId().getClass().getSimpleName() + "="
+ + getId().canonical() + "'}";
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropChangeListener.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropChangeListener.java
new file mode 100644
index 0000000000..4e4b010cf2
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropChangeListener.java
@@ -0,0 +1,43 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+public interface PropChangeListener {
+
+ /**
+ * Signal that a ZooKeeper data change event occurred and that the data has changed.
+ */
+ void zkChangeEvent(final PropCacheKey<?> propCacheKey);
+
+ /**
+ * Signal that a cache change event occurred - cache change events occur on eviction or
+ * invalidation of the cache entry. The underlying data may or may not have changed.
+ */
+ void cacheChangeEvent(final PropCacheKey<?> propCacheKey);
+
+ /**
+ * Signal that the node had been deleted from ZooKeeper.
+ */
+ void deleteEvent(final PropCacheKey<?> propCacheKey);
+
+ /**
+ * A ZooKeeper connection event (session closed, expired...) and that
+ */
+ void connectionEvent();
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropStore.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropStore.java
new file mode 100644
index 0000000000..8315c22c33
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/PropStore.java
@@ -0,0 +1,117 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+public interface PropStore {
+
+ /**
+ * Test that a node for properties exists without throwing a KeeperException.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @return true if the property node exists, false otherwise.
+ * @throws IllegalStateException
+ * if the check fails due to interrupt.
+ */
+ boolean exists(PropCacheKey<?> propCacheKey);
+
+ /**
+ * Create an initial entry for the PropCacheId. If properties already exist, they are not
+ * modified.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @param props
+ * a map of property k,v pairs
+ * @throws IllegalStateException
+ * if the updates fails because of an underlying store exception
+ */
+ void create(PropCacheKey<?> propCacheKey, Map<String,String> props);
+
+ /**
+ *
+ * @param propCacheId
+ * the prop cache key
+ * @return The versioned properties.
+ * @throws IllegalStateException
+ * if the updates fails because of an underlying store exception or if the properties do
+ * not exist for the propCacheId
+ */
+ @NonNull
+ VersionedProperties get(PropCacheKey<?> propCacheId);
+
+ /**
+ * Adds or updates current properties. If the property currently exists it is overwritten,
+ * otherwise it is added.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @param props
+ * a map of property k,v pairs
+ * @throws IllegalStateException
+ * if the values cannot be written or if an underlying store exception occurs.
+ */
+ void putAll(PropCacheKey<?> propCacheKey, Map<String,String> props);
+
+ /**
+ * Delete the store node from the underlying store.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @throws IllegalStateException
+ * if the updates fails because of an underlying store exception
+ */
+ void delete(PropCacheKey<?> propCacheKey);
+
+ /**
+ * Deletes individual properties specified by the set of keys.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @param keys
+ * a set of keys.
+ * @throws IllegalStateException
+ * if the values cannot be deleted or if an underlying store exception occurs.
+ */
+ void removeProperties(PropCacheKey<?> propCacheKey, Collection<String> keys);
+
+ /**
+ * External processes can register for notifications if the properties change. Normally processes
+ * can read from the store and always receive the current snapshot of the latest values. However,
+ * it the process wants to take an active action on change detections, then they can register and
+ * receive notifications.
+ * <p>
+ * Implementation detail - the notification occurs on a separate thread from the underlying store,
+ * but listeners should not perform lengthy operations on the notification to prevent delaying
+ * other listeners from receive timely notification of the changes detected.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @param listener
+ * a listener
+ */
+ void registerAsListener(PropCacheKey<?> propCacheKey, PropChangeListener listener);
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/SystemPropKey.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/SystemPropKey.java
new file mode 100644
index 0000000000..f960f4a242
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/SystemPropKey.java
@@ -0,0 +1,56 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+import static org.apache.accumulo.core.Constants.ZCONFIG;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+public class SystemPropKey extends PropCacheKey<InstanceId> {
+
+ private SystemPropKey(final InstanceId instanceId, final String path) {
+ super(instanceId, path, instanceId);
+ }
+
+ @Override
+ public @NonNull String getNodePath() {
+ return getNodeName(instanceId);
+ }
+
+ @Override
+ public @NonNull String getBasePath() {
+ return ZooUtil.getRoot(instanceId) + ZCONFIG;
+ }
+
+ public static SystemPropKey of(final ServerContext context) {
+ return of(context.getInstanceID());
+ }
+
+ public static SystemPropKey of(final InstanceId instanceId) {
+ return new SystemPropKey(instanceId, getNodeName(instanceId));
+ }
+
+ private static String getNodeName(final InstanceId instanceId) {
+ return ZooUtil.getRoot(instanceId) + ZCONFIG + "/" + PROP_NODE_NAME;
+ }
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/TablePropKey.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/TablePropKey.java
new file mode 100644
index 0000000000..d8ee686ca3
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/TablePropKey.java
@@ -0,0 +1,59 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+import static org.apache.accumulo.core.Constants.ZTABLES;
+import static org.apache.accumulo.core.Constants.ZTABLE_CONF;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+public class TablePropKey extends PropCacheKey<TableId> {
+
+ public static TablePropKey of(final ServerContext context, final TableId tableId) {
+ return of(context.getInstanceID(), tableId);
+ }
+
+ public static TablePropKey of(final InstanceId instanceId, final TableId tableId) {
+ return new TablePropKey(instanceId, getNodePath(instanceId, tableId), tableId);
+ }
+
+ private TablePropKey(final InstanceId instanceId, final String path, final TableId tableId) {
+ super(instanceId, path, tableId);
+ }
+
+ private static String getNodePath(final InstanceId instanceId, final TableId id) {
+ return ZooUtil.getRoot(instanceId) + ZTABLES + "/" + id.canonical() + ZTABLE_CONF + "/"
+ + PROP_NODE_NAME;
+ }
+
+ @Override
+ public @NonNull String getNodePath() {
+ return getNodePath(instanceId, id);
+ }
+
+ @Override
+ public @NonNull String getBasePath() {
+ return ZooUtil.getRoot(instanceId) + ZTABLES + "/" + id.canonical() + ZTABLE_CONF;
+ }
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropCacheCaffeineImpl.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropCacheCaffeineImpl.java
new file mode 100644
index 0000000000..d1664b8fcb
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropCacheCaffeineImpl.java
@@ -0,0 +1,142 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.accumulo.core.util.threads.ThreadPools;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCache;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.benmanes.caffeine.cache.CacheLoader;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.LoadingCache;
+import com.github.benmanes.caffeine.cache.RemovalCause;
+import com.github.benmanes.caffeine.cache.Ticker;
+import com.google.common.annotations.VisibleForTesting;
+
+public class PropCacheCaffeineImpl implements PropCache {
+
+ public static final TimeUnit BASE_TIME_UNITS = TimeUnit.MINUTES;
+ public static final int REFRESH_MIN = 15;
+ public static final int EXPIRE_MIN = 60;
+ private static final Logger log = LoggerFactory.getLogger(PropCacheCaffeineImpl.class);
+ private static final Executor executor = ThreadPools.getServerThreadPools().createThreadPool(1,
+ 20, 60, TimeUnit.SECONDS, "cache-refresh", false);
+
+ private final PropStoreMetrics metrics;
+
+ private final LoadingCache<PropCacheKey<?>,VersionedProperties> cache;
+
+ private PropCacheCaffeineImpl(final CacheLoader<PropCacheKey<?>,VersionedProperties> cacheLoader,
+ final PropStoreMetrics metrics, final Ticker ticker) {
+ this.metrics = metrics;
+ var builder = Caffeine.newBuilder().refreshAfterWrite(REFRESH_MIN, BASE_TIME_UNITS)
+ .expireAfterAccess(EXPIRE_MIN, BASE_TIME_UNITS).evictionListener(this::evictionNotifier)
+ .executor(executor);
+ if (ticker != null) {
+ builder = builder.ticker(ticker);
+ }
+ cache = builder.build(cacheLoader);
+ }
+
+ public PropStoreMetrics getMetrics() {
+ return metrics;
+ }
+
+ void evictionNotifier(PropCacheKey<?> propCacheKey, VersionedProperties value,
+ RemovalCause cause) {
+ log.trace("Evicted: ID: {} was evicted from cache. Reason: {}", propCacheKey, cause);
+ metrics.incrEviction();
+ }
+
+ @Override
+ public @Nullable VersionedProperties get(PropCacheKey<?> propCacheKey) {
+ log.trace("Called get() for {}", propCacheKey);
+ try {
+ return cache.get(propCacheKey);
+ } catch (Exception ex) {
+ log.info("Cache failed to retrieve properties for: " + propCacheKey, ex);
+ metrics.incrZkError();
+ return null;
+ }
+ }
+
+ @Override
+ public void remove(PropCacheKey<?> propCacheKey) {
+ log.trace("clear {} from cache", propCacheKey);
+ cache.invalidate(propCacheKey);
+ }
+
+ @Override
+ public void removeAll() {
+ cache.invalidateAll();
+ }
+
+ /** for testing - force Caffeine housekeeping to run. */
+ @VisibleForTesting
+ void cleanUp() {
+ cache.cleanUp();
+ }
+
+ /**
+ * Retrieve the version properties if present in the cache, otherwise return null. This prevents
+ * caching the properties and should be used when properties will be updated and then committed to
+ * the backend store. The process that is updating the values may not need them for additional
+ * processing so there is no reason to store them in the cache at this time. If they are used, a
+ * normal cache get will load the property into the cache.
+ *
+ * @param propCacheKey
+ * the property id
+ * @return the version properties if cached, otherwise return null.
+ */
+ @Override
+ public @Nullable VersionedProperties getWithoutCaching(PropCacheKey<?> propCacheKey) {
+ return cache.getIfPresent(propCacheKey);
+ }
+
+ public static class Builder {
+
+ private final PropStoreMetrics metrics;
+ private final ZooPropLoader zooPropLoader;
+ private Ticker ticker = null;
+
+ public Builder(final ZooPropLoader zooPropLoader, final PropStoreMetrics metrics) {
+ Objects.requireNonNull(zooPropLoader, "A PropStoreChangeMonitor must be provided");
+ this.zooPropLoader = zooPropLoader;
+ this.metrics = metrics;
+ }
+
+ public PropCacheCaffeineImpl build() {
+ return new PropCacheCaffeineImpl(zooPropLoader, metrics, ticker);
+ }
+
+ public Builder withTicker(final Ticker ticker) {
+ this.ticker = ticker;
+ return this;
+ }
+ }
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreEventTask.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreEventTask.java
new file mode 100644
index 0000000000..c87e4e8807
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreEventTask.java
@@ -0,0 +1,110 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import java.util.Set;
+
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.PropChangeListener;
+
+/**
+ * Provides a simple runnable base task for notifying listeners for PropStore event change
+ * notifications.
+ */
+public abstract class PropStoreEventTask implements Runnable {
+
+ private final PropCacheKey<?> propCacheKey;
+ private final Set<PropChangeListener> listeners;
+
+ /**
+ * Used when the notification is sent to the listeners without specifying a prop cache key.
+ *
+ * @param listeners
+ * the set of listeners.
+ */
+ private PropStoreEventTask(final Set<PropChangeListener> listeners) {
+ this.propCacheKey = null;
+ this.listeners = listeners;
+ }
+
+ /**
+ * Used when listeners for the specified prop cahe key should receive a notification.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @param listeners
+ * the set of listeners
+ */
+ private PropStoreEventTask(final PropCacheKey<?> propCacheKey,
+ final Set<PropChangeListener> listeners) {
+ this.propCacheKey = propCacheKey;
+ this.listeners = listeners;
+ }
+
+ public static class PropStoreZkChangeEventTask extends PropStoreEventTask {
+
+ PropStoreZkChangeEventTask(final PropCacheKey<?> propCacheKey,
+ final Set<PropChangeListener> listeners) {
+ super(propCacheKey, listeners);
+ }
+
+ @Override
+ public void run() {
+ super.listeners.forEach(listener -> listener.zkChangeEvent(super.propCacheKey));
+ }
+ }
+
+ public static class PropStoreCacheChangeEventTask extends PropStoreEventTask {
+
+ PropStoreCacheChangeEventTask(final PropCacheKey<?> propCacheKey,
+ final Set<PropChangeListener> listeners) {
+ super(propCacheKey, listeners);
+ }
+
+ @Override
+ public void run() {
+ super.listeners.forEach(listener -> listener.cacheChangeEvent(super.propCacheKey));
+ }
+ }
+
+ public static class PropStoreDeleteEventTask extends PropStoreEventTask {
+
+ PropStoreDeleteEventTask(final PropCacheKey<?> propCacheKey,
+ final Set<PropChangeListener> listeners) {
+ super(propCacheKey, listeners);
+ }
+
+ @Override
+ public void run() {
+ super.listeners.forEach(listener -> listener.deleteEvent(super.propCacheKey));
+ }
+ }
+
+ public static class PropStoreConnectionEventTask extends PropStoreEventTask {
+
+ PropStoreConnectionEventTask(final Set<PropChangeListener> listeners) {
+ super(listeners);
+ }
+
+ @Override
+ public void run() {
+ super.listeners.forEach(PropChangeListener::connectionEvent);
+ }
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreMetrics.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreMetrics.java
new file mode 100644
index 0000000000..26e030d343
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreMetrics.java
@@ -0,0 +1,92 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.accumulo.core.metrics.MetricsProducer;
+import org.apache.accumulo.core.metrics.MetricsUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+
+public class PropStoreMetrics implements MetricsProducer {
+
+ private static final Logger log = LoggerFactory.getLogger(PropStoreMetrics.class);
+
+ private Timer load;
+ private Counter refresh;
+ private Counter refreshLoad;
+ private Counter eviction;
+ private Counter zkError;
+
+ @Override
+ public void registerMetrics(MeterRegistry registry) {
+
+ load = Timer.builder(METRICS_PROPSTORE_LOAD_TIMER).description("prop store load time")
+ .tags(MetricsUtil.getCommonTags()).register(registry);
+
+ refresh =
+ Counter.builder(METRICS_PROPSTORE_REFRESH_COUNT).description("prop store refresh count")
+ .tags(MetricsUtil.getCommonTags()).register(registry);
+
+ refreshLoad = Counter.builder(METRICS_PROPSTORE_REFRESH_LOAD_COUNT)
+ .description("prop store refresh load count").tags(MetricsUtil.getCommonTags())
+ .register(registry);
+
+ eviction =
+ Counter.builder(METRICS_PROPSTORE_EVICTION_COUNT).description("prop store eviction count")
+ .tags(MetricsUtil.getCommonTags()).register(registry);
+
+ zkError = Counter.builder(METRICS_PROPSTORE_ZK_ERROR_COUNT)
+ .description("prop store ZooKeeper error count").tags(MetricsUtil.getCommonTags())
+ .register(registry);
+
+ }
+
+ public PropStoreMetrics() {
+ log.debug("Creating PropStore metrics");
+ }
+
+ public void addLoadTime(final long value) {
+ log.trace("Load time: {}", value);
+ load.record(Duration.ofMillis(value));
+ log.trace("Load count: {} time:{}", load.count(), load.totalTime(TimeUnit.MILLISECONDS));
+ }
+
+ public void incrRefresh() {
+ refresh.increment();
+ }
+
+ public void incrRefreshLoad() {
+ refreshLoad.increment();
+ }
+
+ public void incrEviction() {
+ eviction.increment();
+ }
+
+ public void incrZkError() {
+ zkError.increment();
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreWatcher.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreWatcher.java
new file mode 100644
index 0000000000..5e8a6e7ab0
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/PropStoreWatcher.java
@@ -0,0 +1,252 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.accumulo.core.util.threads.ThreadPools;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.PropChangeListener;
+import org.apache.zookeeper.WatchedEvent;
+import org.apache.zookeeper.Watcher;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class serves as a translator between ZooKeeper events and converts them to PropStore events.
+ * Using this as an intermediary, the external listeners do not need to set / manage external
+ * ZooKeeper watchers, they can register for PropStore events if they need to take active action on
+ * change detection.
+ * <p>
+ * Users of the PropStore.get() will get properties that match what is stored in ZooKeeper for each
+ * call and do not need to manage any caching. However, the ability to receive active notification
+ * without needed to register / manage ZooKeeper watchers external to the PropStore is provided in
+ * case other code is relying on active notifications.
+ * <p>
+ * The notification occurs on a separate thread from the ZooKeeper notification handling, but
+ * listeners should not perform lengthy operations on the notification thread so that other listener
+ * notifications are not delayed.
+ */
+public class PropStoreWatcher implements Watcher {
+
+ private static final Logger log = LoggerFactory.getLogger(PropStoreWatcher.class);
+
+ private static final ExecutorService executorService =
+ ThreadPools.getServerThreadPools().createFixedThreadPool(2, "zoo_change_update", false);
+
+ private final ReentrantReadWriteLock listenerLock = new ReentrantReadWriteLock();
+ private final ReentrantReadWriteLock.ReadLock listenerReadLock = listenerLock.readLock();
+ private final ReentrantReadWriteLock.WriteLock listenerWriteLock = listenerLock.writeLock();
+
+ // access should be guarded by acquiring the listener read or write lock
+ private final Map<PropCacheKey<?>,Set<PropChangeListener>> listeners = new HashMap<>();
+
+ private final ReadyMonitor zkReadyMonitor;
+
+ public PropStoreWatcher(final ReadyMonitor zkReadyMonitor) {
+ this.zkReadyMonitor = zkReadyMonitor;
+ }
+
+ public void registerListener(final PropCacheKey<?> propCacheKey,
+ final PropChangeListener listener) {
+ listenerWriteLock.lock();
+ try {
+ Set<PropChangeListener> set = listeners.computeIfAbsent(propCacheKey, s -> new HashSet<>());
+ set.add(listener);
+ } finally {
+ listenerWriteLock.unlock();
+ }
+ }
+
+ /**
+ * Process a ZooKeeper event. This method does not reset the watcher. Subscribers are notified of
+ * the change - if they call get to update and respond to the change the watcher will be (re)set
+ * then. This helps clean up watchers by not automatically re-adding the watcher on the event but
+ * only if being used.
+ *
+ * @param event
+ * ZooKeeper event.
+ */
+ @Override
+ public void process(final WatchedEvent event) {
+
+ String path;
+ PropCacheKey<?> propCacheKey;
+ switch (event.getType()) {
+ case NodeDataChanged:
+ path = event.getPath();
+ log.trace("handle change event for path: {}", path);
+ propCacheKey = PropCacheKey.fromPath(path);
+ if (propCacheKey != null) {
+ signalZkChangeEvent(propCacheKey);
+ }
+ break;
+ case NodeDeleted:
+ path = event.getPath();
+ log.trace("handle delete event for path: {}", path);
+ propCacheKey = PropCacheKey.fromPath(path);
+ if (propCacheKey != null) {
+ // notify listeners
+ Set<PropChangeListener> snapshot = getListenerSnapshot(propCacheKey);
+ if (snapshot != null) {
+ executorService
+ .execute(new PropStoreEventTask.PropStoreDeleteEventTask(propCacheKey, snapshot));
+ }
+ listenerCleanup(propCacheKey);
+ }
+ break;
+ case None:
+ Event.KeeperState state = event.getState();
+ switch (state) {
+ // pause - could reconnect
+ case ConnectedReadOnly:
+ case Disconnected:
+ log.debug("ZooKeeper disconnected event received");
+ zkReadyMonitor.clearReady();
+ executorService.execute(
+ new PropStoreEventTask.PropStoreConnectionEventTask(getAllListenersSnapshot()));
+ break;
+
+ // okay
+ case SyncConnected:
+ log.debug("ZooKeeper connected event received");
+ zkReadyMonitor.setReady();
+ break;
+
+ // terminal - never coming back.
+ case Expired:
+ case Closed:
+ log.info("ZooKeeper connection closed event received");
+ zkReadyMonitor.clearReady();
+ zkReadyMonitor.setClosed(); // terminal condition
+ executorService.execute(
+ new PropStoreEventTask.PropStoreConnectionEventTask(getAllListenersSnapshot()));
+ break;
+
+ default:
+ log.trace("ignoring zooKeeper state: {}", state);
+ }
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ /**
+ * Execute a task to notify registered listeners that the propCacheKey node received an event
+ * notification from ZooKeeper and should be updated. The process can be initiated either by a
+ * ZooKeeper notification or a change detected in the cache based on a ZooKeeper event.
+ *
+ * @param propCacheKey
+ * the cache id
+ */
+ public void signalZkChangeEvent(@NonNull final PropCacheKey<?> propCacheKey) {
+ log.trace("signal ZooKeeper change event: {}", propCacheKey);
+ Set<PropChangeListener> snapshot = getListenerSnapshot(propCacheKey);
+ log.trace("Sending change event to: {}", snapshot);
+ if (snapshot != null) {
+ executorService
+ .execute(new PropStoreEventTask.PropStoreZkChangeEventTask(propCacheKey, snapshot));
+ }
+ }
+
+ /**
+ * Execute a task to notify registered listeners that the propCacheKey node change was detected
+ * should be updated.
+ *
+ * @param propCacheKey
+ * the cache id
+ */
+ public void signalCacheChangeEvent(final PropCacheKey<?> propCacheKey) {
+ log.trace("cache change event: {}", propCacheKey);
+ Set<PropChangeListener> snapshot = getListenerSnapshot(propCacheKey);
+ if (snapshot != null) {
+ executorService
+ .execute(new PropStoreEventTask.PropStoreCacheChangeEventTask(propCacheKey, snapshot));
+ }
+ }
+
+ /**
+ * Clean-up the active listeners set when an entry is removed from the cache, remove it from the
+ * active listeners.
+ *
+ * @param propCacheKey
+ * the cache id
+ */
+ public void listenerCleanup(final PropCacheKey<?> propCacheKey) {
+ listenerWriteLock.lock();
+ try {
+ listeners.remove(propCacheKey);
+ } finally {
+ listenerWriteLock.unlock();
+ }
+ }
+
+ /**
+ * Get an immutable snapshot of the listeners for a prop cache id. The set is intended for
+ * notification of changes for a specific prop cache id.
+ *
+ * @param propCacheKey
+ * the prop cache id
+ * @return an immutable copy of listeners.
+ */
+ private Set<PropChangeListener> getListenerSnapshot(final PropCacheKey<?> propCacheKey) {
+
+ Set<PropChangeListener> snapshot = null;
+ listenerReadLock.lock();
+ try {
+ Set<PropChangeListener> set = listeners.get(propCacheKey);
+ if (set != null) {
+ snapshot = Set.copyOf(set);
+ }
+
+ } finally {
+ listenerReadLock.unlock();
+ }
+ return snapshot;
+ }
+
+ /**
+ * Get an immutable snapshot of the all listeners registered for event. The set is intended for
+ * connection event notifications that are not specific to an individual prop cache id.
+ *
+ * @return an immutable copy of all registered listeners.
+ */
+ private Set<PropChangeListener> getAllListenersSnapshot() {
+
+ listenerReadLock.lock();
+ try {
+ return listeners.keySet().stream().flatMap(key -> listeners.get(key).stream())
+ .collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
+ } finally {
+ listenerReadLock.unlock();
+ }
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ReadyMonitor.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ReadyMonitor.java
new file mode 100644
index 0000000000..84fabc72f9
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ReadyMonitor.java
@@ -0,0 +1,143 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import java.util.StringJoiner;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Provides a barrier to block operations until a resource signals that it is ready. This class
+ * encapsulates a wait / notify operation using a lock and condition. Instances are initialized with
+ * the resource unavailable - call {@link #setReady()} when the resource is available
+ */
+public class ReadyMonitor {
+
+ private final String resourceName;
+ private final long timeout;
+
+ // used to preserve memory ordering consistency (volatile semantics)
+ private final AtomicBoolean haveConnection = new AtomicBoolean(false);
+ private final AtomicBoolean isClosed = new AtomicBoolean(false);
+
+ private final ReentrantLock lock = new ReentrantLock();
+ private final Condition readySignal = lock.newCondition();
+
+ /**
+ * Create an instance of a ready monitor.
+ *
+ * @param resourceName
+ * the resource name guarded by this monitor (used by logging)
+ * @param timeout
+ * the max time in milliseconds this will block waiting.
+ */
+ public ReadyMonitor(final String resourceName, final long timeout) {
+ this.resourceName = resourceName;
+ this.timeout = timeout;
+ }
+
+ /**
+ * Non-blocking call to get ready state. It will throw is the ZooKeeper connection has closed
+ * (non-recoverable condition)
+ *
+ * @return true if resource is ready, false otherwise
+ */
+ public boolean test() {
+ if (isClosed.get()) {
+ throw new IllegalStateException("ZooKeeper has closed the connection - cannot continue");
+ }
+ return haveConnection.get();
+ }
+
+ /**
+ * Method blocks until the resource is ready. If the resource does not become ready within the
+ * timeout an IllegalStateException is thrown.
+ *
+ * @throws IllegalStateException
+ * if the resource does not signal ready withing timeout.
+ */
+ public void isReady() {
+
+ if (test()) {
+ return;
+ }
+
+ // loop handles a spurious notification
+ while (!haveConnection.get()) {
+ try {
+ lock.lock();
+ if (!readySignal.await(timeout, TimeUnit.MILLISECONDS)) {
+ var msg = resourceName + " failed to be ready within " + timeout + " ms";
+ throw new IllegalStateException(msg);
+ }
+ // re-signal in case notification missed
+ readySignal.signalAll();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(resourceName + " interrupted while waiting until ready ",
+ ex);
+ } finally {
+ lock.unlock();
+ }
+ }
+ }
+
+ /**
+ * ZooKeeper has closed the connection. This is a terminal state that cannot be recoved from
+ * without creating a new ZooKeeper client.
+ */
+ public void setClosed() {
+ isClosed.set(true);
+ }
+
+ /**
+ * Indicate that the resource is ready.
+ */
+ public void setReady() {
+
+ // already set - noop
+ if (test()) {
+ return;
+ }
+
+ try {
+ lock.lock();
+ haveConnection.set(true);
+ readySignal.signalAll();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Indicate that resource is NOT available and that calls to {@link #isReady()} should block.
+ */
+ public void clearReady() {
+ haveConnection.set(false);
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", ReadyMonitor.class.getSimpleName() + "[", "]")
+ .add("resourceName='" + resourceName + "'").add("timeout=" + timeout)
+ .add("haveConnection=" + haveConnection).toString();
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ZooPropLoader.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ZooPropLoader.java
new file mode 100644
index 0000000000..4706b132bd
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ZooPropLoader.java
@@ -0,0 +1,157 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.benmanes.caffeine.cache.CacheLoader;
+
+public class ZooPropLoader implements CacheLoader<PropCacheKey<?>,VersionedProperties> {
+
+ private static final Logger log = LoggerFactory.getLogger(ZooPropLoader.class);
+
+ private final ZooReaderWriter zrw;
+ private final VersionedPropCodec propCodec;
+ // used to set watcher, does not react to events.
+ private final PropStoreWatcher propStoreWatcher;
+ private final PropStoreMetrics metrics;
+
+ public ZooPropLoader(final ZooReaderWriter zrw, final VersionedPropCodec propCodec,
+ final PropStoreWatcher propStoreWatcher, final PropStoreMetrics metrics) {
+ this.zrw = zrw;
+ this.propCodec = propCodec;
+ this.propStoreWatcher = propStoreWatcher;
+ this.metrics = metrics;
+ }
+
+ @Override
+ public @Nullable VersionedProperties load(PropCacheKey<?> propCacheKey) {
+ try {
+ log.trace("load called for {}", propCacheKey);
+
+ long startNanos = System.nanoTime();
+
+ Stat stat = new Stat();
+ byte[] bytes = zrw.getData(propCacheKey.getPath(), propStoreWatcher, stat);
+ var vProps = propCodec.fromBytes(stat.getVersion(), bytes);
+
+ metrics.addLoadTime(
+ TimeUnit.MILLISECONDS.convert(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS));
+
+ return vProps;
+ } catch (KeeperException.NoNodeException ex) {
+ metrics.incrZkError();
+ log.debug("property node for {} does not exist - it may be being created", propCacheKey);
+ propStoreWatcher.signalZkChangeEvent(propCacheKey);
+ return null;
+ } catch (Exception ex) {
+ metrics.incrZkError();
+ log.info("Failed to load properties for: {} from ZooKeeper, returning null", propCacheKey,
+ ex);
+ propStoreWatcher.signalZkChangeEvent(propCacheKey);
+ return null;
+ }
+ }
+
+ @Override
+ public CompletableFuture<? extends VersionedProperties> asyncLoad(PropCacheKey<?> propCacheKey,
+ Executor executor) throws Exception {
+ log.trace("asyncLoad called for key: {}", propCacheKey);
+ return CacheLoader.super.asyncLoad(propCacheKey, executor);
+ }
+
+ @Override
+ public CompletableFuture<? extends VersionedProperties> asyncReload(PropCacheKey<?> propCacheKey,
+ VersionedProperties oldValue, Executor executor) throws Exception {
+ log.trace("asyncReload called for key: {}", propCacheKey);
+ metrics.incrRefresh();
+
+ return CompletableFuture.supplyAsync(() -> loadIfDifferentVersion(propCacheKey, oldValue),
+ executor);
+ }
+
+ @Override
+ public @Nullable VersionedProperties reload(PropCacheKey<?> propCacheKey,
+ VersionedProperties oldValue) throws Exception {
+ log.trace("reload called for: {}", propCacheKey);
+ metrics.incrRefresh();
+ return loadIfDifferentVersion(propCacheKey, oldValue);
+ }
+
+ /**
+ * First checks that the ZooKeeper data version matches the versioned property's data version,
+ * using a lighter weight ZooKeeper call to get the Stat structure. If the data versions match,
+ * the original version properties are returned without additional ZooKeeper calls. If the
+ * versions do not match, the data is reread from ZooKeeper and returned. If any exceptions occur,
+ * a null value is returned and any cached values should be considered invalid.
+ *
+ * @param propCacheId
+ * the property cache id
+ * @param currentValue
+ * the current versioned properties.
+ * @return versioned properties that match the values stored in ZooKeeper, or null if the
+ * properties cannot be retrieved.
+ */
+ private @Nullable VersionedProperties loadIfDifferentVersion(PropCacheKey<?> propCacheId,
+ VersionedProperties currentValue) {
+ requireNonNull(propCacheId, "propCacheId cannot be null");
+ try {
+ Stat stat = zrw.getStatus(propCacheId.getPath());
+
+ log.trace("Check stat version on reload. Zk: {}, Cache: {}", stat.getVersion(),
+ currentValue.getDataVersion());
+ // no change, cached version is valid.
+ if (stat.getVersion() == currentValue.getDataVersion()) {
+ return currentValue;
+ }
+
+ log.trace("different version in cache for {}, calling loader to get update from ZooKeeper",
+ propCacheId);
+
+ var updatedValue = load(propCacheId);
+
+ metrics.incrRefreshLoad();
+
+ // The cache will be updated - notify external listeners value changed.
+ propStoreWatcher.signalCacheChangeEvent(propCacheId);
+ log.trace("Updated value {}", updatedValue == null ? "null" : updatedValue.print(true));
+ return updatedValue;
+ } catch (RuntimeException | KeeperException | InterruptedException ex) {
+ log.warn("async exception occurred reading properties from ZooKeeper for: {} returning null",
+ propCacheId, ex);
+ metrics.incrZkError();
+ propStoreWatcher.signalZkChangeEvent(propCacheId);
+ return null;
+ }
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ZooPropStore.java b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ZooPropStore.java
new file mode 100644
index 0000000000..7c522e0e2f
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/store/impl/ZooPropStore.java
@@ -0,0 +1,390 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static java.util.Objects.requireNonNullElseGet;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiFunction;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.metrics.MetricsUtil;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCache;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.PropChangeListener;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.util.ConfigTransformer;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.benmanes.caffeine.cache.Ticker;
+import com.google.common.annotations.VisibleForTesting;
+
+public class ZooPropStore implements PropStore, PropChangeListener {
+
+ private final static Logger log = LoggerFactory.getLogger(ZooPropStore.class);
+ private final static VersionedPropCodec codec = VersionedPropCodec.getDefault();
+
+ private final ZooReaderWriter zrw;
+ private final PropStoreWatcher propStoreWatcher;
+ private final PropCache cache;
+ private final PropStoreMetrics cacheMetrics = new PropStoreMetrics();
+ private final ReadyMonitor zkReadyMon;
+
+ /**
+ * Create instance using ZooPropStore.Builder
+ *
+ * @param instanceId
+ * the instance id
+ * @param zrw
+ * a wrapper set of utilities for accessing ZooKeeper.
+ */
+ private ZooPropStore(final InstanceId instanceId, final ZooReaderWriter zrw) {
+ this(instanceId, zrw, null, null, null);
+ }
+
+ /**
+ * For testing create an instance with the optionally pass synthetic clock (Ticker), a
+ * ReadyMonitor and a PropStore watcher allowing them to be mocked. If the optional components are
+ * passed as null an internal instance is created.
+ *
+ * @param instanceId
+ * the instance id
+ * @param zrw
+ * a wrapper set of utilities for accessing ZooKeeper.
+ * @param monitor
+ * a ready monitor. Optional, if null, one is created.
+ * @param watcher
+ * a watcher. Optional, if null, one is created.
+ * @param ticker
+ * a synthetic clock used for testing. Optional, if null, one is created.
+ */
+ ZooPropStore(final InstanceId instanceId, final ZooReaderWriter zrw, final ReadyMonitor monitor,
+ final PropStoreWatcher watcher, final Ticker ticker) {
+
+ this.zrw = zrw;
+
+ this.zkReadyMon = requireNonNullElseGet(monitor,
+ () -> new ReadyMonitor("prop-store", Math.round(zrw.getSessionTimeout() * 1.75)));
+
+ this.propStoreWatcher = requireNonNullElseGet(watcher, () -> new PropStoreWatcher(zkReadyMon));
+
+ ZooPropLoader propLoader = new ZooPropLoader(zrw, codec, this.propStoreWatcher, cacheMetrics);
+
+ if (ticker == null) {
+ this.cache = new PropCacheCaffeineImpl.Builder(propLoader, cacheMetrics).build();
+ } else {
+ this.cache =
+ new PropCacheCaffeineImpl.Builder(propLoader, cacheMetrics).withTicker(ticker).build();
+ }
+
+ MetricsUtil.initializeProducers(cacheMetrics);
+
+ try {
+ var path = ZooUtil.getRoot(instanceId);
+ if (zrw.exists(path, propStoreWatcher)) {
+ log.debug("Have a ZooKeeper connection and found instance node: {}", instanceId);
+ zkReadyMon.setReady();
+ } else {
+ throw new IllegalStateException("Instance may not have been initialized, root node: " + path
+ + " does not exist in ZooKeeper");
+ }
+ } catch (InterruptedException | KeeperException ex) {
+ throw new IllegalStateException("Failed to read root node " + instanceId + " from ZooKeeper",
+ ex);
+ }
+ }
+
+ public static PropStore initialize(@NonNull final InstanceId instanceId,
+ @NonNull final ZooReaderWriter zrw) {
+ return new ZooPropStore(instanceId, zrw);
+ }
+
+ @Override
+ public boolean exists(final PropCacheKey<?> propCacheKey) {
+ try {
+ if (zrw.exists(propCacheKey.getPath())) {
+ return true;
+ }
+ } catch (KeeperException ex) {
+ // ignore Keeper exception on check.
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted testing if node exists", ex);
+ }
+ return false;
+ }
+
+ public PropStoreMetrics getMetrics() {
+ return cacheMetrics;
+ }
+
+ @Override
+ public void create(PropCacheKey<?> propCacheKey, Map<String,String> props) {
+
+ try {
+ VersionedProperties vProps = new VersionedProperties(props);
+ String path = propCacheKey.getPath();
+ zrw.putPersistentData(path, codec.toBytes(vProps), ZooUtil.NodeExistsPolicy.FAIL);
+ } catch (IOException | KeeperException | InterruptedException ex) {
+ throw new IllegalStateException("Failed to serialize properties for " + propCacheKey, ex);
+ }
+ }
+
+ /**
+ * get or create properties from the store. If the property node does not exist in ZooKeeper,
+ * legacy properties exist, they will be converted to the new storage form and naming convention.
+ * The legacy properties are deleted once the new node format is written.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @return The versioned properties.
+ * @throws IllegalStateException
+ * if the updates fails because of an underlying store exception
+ */
+ @Override
+ public @NonNull VersionedProperties get(final PropCacheKey<?> propCacheKey) {
+ checkZkConnection(); // if ZK not connected, block, do not just return a cached value.
+ propStoreWatcher.registerListener(propCacheKey, this);
+
+ var props = cache.get(propCacheKey);
+ if (props != null) {
+ return props;
+ }
+
+ if (propCacheKey instanceof SystemPropKey) {
+ return new ConfigTransformer(zrw, codec, propStoreWatcher).transform(propCacheKey);
+ }
+
+ throw new IllegalStateException(
+ "Invalid request for " + propCacheKey + ", the property node does not exist");
+ }
+
+ /**
+ * Convenience method for utilities that may not have a PropStore read the encoded properties
+ * directly from ZooKeeper. This allows utilities access when there is a ZooKeeper, may there may
+ * not be a full instance running. All exception handling is left to the caller.
+ *
+ * @param propCacheKey
+ * the prop cache key
+ * @param watcher
+ * a prop store watcher that will receive / handle ZooKeeper events.
+ * @param zrw
+ * a ZooReaderWriter
+ * @return the versioned properties or null if the node does not exist.
+ * @throws IOException
+ * if the underlying data from the ZooKeeper node cannot be decoded.
+ * @throws KeeperException
+ * if a ZooKeeper exception occurs
+ * @throws InterruptedException
+ * if the ZooKeeper read was interrupted.
+ */
+ public static @Nullable VersionedProperties readFromZk(final PropCacheKey<?> propCacheKey,
+ final PropStoreWatcher watcher, final ZooReaderWriter zrw)
+ throws IOException, KeeperException, InterruptedException {
+ try {
+ Stat stat = new Stat();
+ byte[] bytes = zrw.getData(propCacheKey.getPath(), watcher, stat);
+ return codec.fromBytes(stat.getVersion(), bytes);
+ } catch (KeeperException.NoNodeException ex) {
+ // ignore no node - allow other exceptions to propagate
+ return null;
+ }
+ }
+
+ /**
+ * Copies all mappings from the specified map and into the existing property values and stores
+ * them into the backend store. New keys are added and keys the may have existed in the current
+ * properties are overwritten.
+ * <p>
+ * If multiple threads attempt to update values concurrently, this method will automatically
+ * retry. If the threads are setting different keys / values the result will be the sum of the
+ * changes. If the concurrent threads are attempting to set a value(s) for the same key(s), the
+ * value(s) will be the set to the values provided by the last thread to complete. The order is
+ * indeterminate.
+ *
+ * @param propCacheKey
+ * the prop cache id
+ * @param props
+ * a map of property k,v pairs
+ * @throws IllegalStateException
+ * if the values cannot be written or if an underlying store exception occurs.
+ */
+ @Override
+ public void putAll(@NonNull PropCacheKey<?> propCacheKey, @NonNull Map<String,String> props) {
+ if (props.isEmpty()) {
+ return; // no props - noop
+ }
+ mutateVersionedProps(propCacheKey, VersionedProperties::addOrUpdate, props);
+ }
+
+ @Override
+ public void removeProperties(@NonNull PropCacheKey<?> propCacheKey,
+ @NonNull Collection<String> keys) {
+ if (keys.isEmpty()) {
+ return; // no keys - noop.
+ }
+ mutateVersionedProps(propCacheKey, VersionedProperties::remove, keys);
+ }
+
+ @Override
+ public void delete(@NonNull PropCacheKey<?> propCacheKey) {
+ Objects.requireNonNull(propCacheKey, "prop store delete() - Must provide propCacheId");
+ try {
+ log.trace("called delete() for: {}", propCacheKey);
+ final String path = propCacheKey.getPath();
+ zrw.delete(path);
+ cache.remove(propCacheKey);
+ } catch (KeeperException | InterruptedException ex) {
+ throw new IllegalStateException("Failed to delete properties for propCacheId " + propCacheKey,
+ ex);
+ }
+ }
+
+ private <T> void mutateVersionedProps(PropCacheKey<?> propCacheKey,
+ BiFunction<VersionedProperties,T,VersionedProperties> action, T changes) {
+
+ log.trace("mutateVersionedProps called for: {}", propCacheKey);
+
+ try {
+
+ VersionedProperties vProps = cache.getWithoutCaching(propCacheKey);
+ if (vProps == null) {
+ vProps = readPropsFromZk(propCacheKey);
+ }
+
+ for (int attempts = 3; attempts > 0; --attempts) {
+
+ VersionedProperties updates = action.apply(vProps, changes);
+
+ if (zrw.overwritePersistentData(propCacheKey.getPath(), codec.toBytes(updates),
+ updates.getDataVersion())) {
+ return;
+ }
+ Thread.sleep(20); // small pause to get thread to yield.
+ // re-read from zookeeper to ensure the latest version.
+ vProps = readPropsFromZk(propCacheKey);
+ }
+ throw new IllegalStateException(
+ "failed to remove properties to zooKeeper for " + propCacheKey, null);
+ } catch (IllegalArgumentException | IOException ex) {
+ throw new IllegalStateException(
+ "Codec failed to decode / encode properties for " + propCacheKey, ex);
+ } catch (InterruptedException | KeeperException ex) {
+ if (ex instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ throw new IllegalStateException(
+ "failed to remove properties to zooKeeper for " + propCacheKey, ex);
+ }
+ }
+
+ @Override
+ public void registerAsListener(PropCacheKey<?> propCacheKey, PropChangeListener listener) {
+ propStoreWatcher.registerListener(propCacheKey, listener);
+ }
+
+ private void checkZkConnection() {
+ if (zkReadyMon.test()) {
+ return;
+ }
+
+ cache.removeAll();
+
+ // not ready block or propagate error if it times out.
+ zkReadyMon.isReady();
+
+ }
+
+ @Override
+ public void zkChangeEvent(PropCacheKey<?> propCacheKey) {
+ log.trace("Received change event from ZooKeeper for: {} removed from cache", propCacheKey);
+ cache.remove(propCacheKey);
+ }
+
+ /**
+ * NOOP for the prop store - the cache value will reflect the updated value on next read. The
+ * change is also sent to external listeners of the need to take action, but for the prop store,
+ * no additional action is required.
+ *
+ * @param propCacheKey
+ * the prop cache id.
+ */
+ @Override
+ public void cacheChangeEvent(PropCacheKey<?> propCacheKey) {
+ log.trace("zkChangeEvent: {}", propCacheKey);
+ }
+
+ @Override
+ public void deleteEvent(PropCacheKey<?> propCacheKey) {
+ log.trace("deleteEvent: {}", propCacheKey);
+ cache.remove(propCacheKey);
+ }
+
+ @Override
+ public void connectionEvent() {
+ log.trace("connectionEvent");
+ cache.removeAll();
+ }
+
+ /** for testing - force Caffeine housekeeping to run. */
+ @VisibleForTesting
+ public void cleanUp() {
+ ((PropCacheCaffeineImpl) cache).cleanUp();
+ }
+
+ /**
+ * Read and decode property node from ZooKeeper.
+ *
+ * @param propCacheKey
+ * the propCacheKey
+ * @return the decoded properties.
+ * @throws KeeperException
+ * if a ZooKeeper exception occurs to allow caller to decide on action.
+ * @throws IOException
+ * if the node data cannot be decoded into properties to allow the caller to decide on
+ * action.
+ * @throws IllegalStateException
+ * if an interrupt occurs. The interrupt status is reasserted and usually best to not
+ * otherwise try to handle the exception.
+ */
+ private VersionedProperties readPropsFromZk(PropCacheKey<?> propCacheKey)
+ throws KeeperException, IOException {
+ try {
+ Stat stat = new Stat();
+ byte[] bytes = zrw.getData(propCacheKey.getPath(), stat);
+ return codec.fromBytes(stat.getVersion(), bytes);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupt received during ZooKeeper read", ex);
+ }
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/util/ConfigPropertyUpgrader.java b/server/base/src/main/java/org/apache/accumulo/server/conf/util/ConfigPropertyUpgrader.java
new file mode 100644
index 0000000000..1888bca17c
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/util/ConfigPropertyUpgrader.java
@@ -0,0 +1,138 @@
+/*
+ * 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.accumulo.server.conf.util;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.NamespaceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.cli.ServerUtilOpts;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.accumulo.server.conf.store.impl.PropStoreWatcher;
+import org.apache.accumulo.server.conf.store.impl.ReadyMonitor;
+import org.apache.accumulo.start.spi.KeywordExecutable;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.auto.service.AutoService;
+
+@AutoService(KeywordExecutable.class)
+public class ConfigPropertyUpgrader implements KeywordExecutable {
+
+ private static final Logger log = LoggerFactory.getLogger(ConfigPropertyUpgrader.class);
+
+ private final static VersionedPropCodec codec = VersionedPropCodec.getDefault();
+
+ public ConfigPropertyUpgrader() {}
+
+ public static void main(String[] args) throws Exception {
+ new ConfigPropertyUpgrader().execute(args);
+ }
+
+ @Override
+ public String keyword() {
+ return "config-upgrade";
+ }
+
+ @Override
+ public String description() {
+ return "converts properties store in ZooKeeper to 2.1 format";
+ }
+
+ @Override
+ public void execute(final String[] args) throws Exception {
+ ConfigPropertyUpgrader.Opts opts = new ConfigPropertyUpgrader.Opts();
+ opts.parseArgs(ConfigPropertyUpgrader.class.getName(), args);
+
+ ServerContext context = opts.getServerContext();
+
+ doUpgrade(context.getInstanceID(), context.getZooReaderWriter());
+ }
+
+ public void doUpgrade(final InstanceId instanceId, final ZooReaderWriter zrw) {
+
+ ReadyMonitor readyMonitor = new ReadyMonitor(ConfigPropertyUpgrader.class.getSimpleName(),
+ zrw.getSessionTimeout() * 2L);
+ PropStoreWatcher nullWatcher = new PropStoreWatcher(readyMonitor);
+
+ ConfigTransformer transformer = new ConfigTransformer(zrw, codec, nullWatcher);
+
+ upgradeSysProps(instanceId, transformer);
+ upgradeNamespaceProps(instanceId, zrw, transformer);
+ upgradeTableProps(instanceId, zrw, transformer);
+ }
+
+ private void upgradeSysProps(final InstanceId instanceId, final ConfigTransformer transformer) {
+ log.info("Upgrade system config properties for {}", instanceId);
+ transformer.transform(SystemPropKey.of(instanceId));
+ }
+
+ private void upgradeNamespaceProps(final InstanceId instanceId, final ZooReaderWriter zrw,
+ final ConfigTransformer transformer) {
+ String zkPathNamespaceBase = ZooUtil.getRoot(instanceId) + Constants.ZNAMESPACES;
+ try {
+ // sort is cosmetic - only improves readability and consistency in logs
+ Set<String> namespaces = new TreeSet<>(zrw.getChildren(zkPathNamespaceBase));
+ for (String namespace : namespaces) {
+ String zkPropBasePath = zkPathNamespaceBase + "/" + namespace + Constants.ZNAMESPACE_CONF;
+ log.info("Upgrading namespace {} base path: {}", namespace, zkPropBasePath);
+ transformer.transform(NamespacePropKey.of(instanceId, NamespaceId.of(namespace)));
+ }
+ } catch (KeeperException ex) {
+ throw new IllegalStateException(
+ "Failed to read namespaces from ZooKeeper for path: " + zkPathNamespaceBase, ex);
+ } catch (InterruptedException ex) {
+ throw new IllegalStateException(
+ "Interrupted reading namespaces from ZooKeeper for path: " + zkPathNamespaceBase, ex);
+ }
+ }
+
+ private void upgradeTableProps(final InstanceId instanceId, final ZooReaderWriter zrw,
+ ConfigTransformer transformer) {
+ String zkPathTableBase = ZooUtil.getRoot(instanceId) + Constants.ZTABLES;
+ try {
+ // sort is cosmetic - only improves readability and consistency in logs
+ Set<String> tables = new TreeSet<>(zrw.getChildren(zkPathTableBase));
+ for (String table : tables) {
+ String zkPropBasePath = zkPathTableBase + "/" + table + Constants.ZNAMESPACE_CONF;
+ log.info("Upgrading table {} base path: {}", table, zkPropBasePath);
+ transformer.transform(TablePropKey.of(instanceId, TableId.of(table)));
+ }
+ } catch (KeeperException ex) {
+ throw new IllegalStateException(
+ "Failed to read tables from ZooKeeper for path: " + zkPathTableBase, ex);
+ } catch (InterruptedException ex) {
+ throw new IllegalStateException(
+ "Interrupted reading tables from ZooKeeper for path: " + zkPathTableBase, ex);
+ }
+ }
+
+ static class Opts extends ServerUtilOpts {}
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/util/ConfigTransformer.java b/server/base/src/main/java/org/apache/accumulo/server/conf/util/ConfigTransformer.java
new file mode 100644
index 0000000000..ccaa15567b
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/util/ConfigTransformer.java
@@ -0,0 +1,395 @@
+/*
+ * 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.accumulo.server.conf.util;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.accumulo.core.conf.DeprecatedPropertyUtil;
+import org.apache.accumulo.core.util.DurationFormat;
+import org.apache.accumulo.fate.util.Retry;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.impl.PropStoreWatcher;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Read legacy properties (pre 2.1) from ZooKeeper and transform them into the single node format.
+ * The encoded properties are stored in ZooKeeper and then the legacy property ZooKeeper nodes are
+ * deleted.
+ */
+public class ConfigTransformer {
+
+ private static final Logger log = LoggerFactory.getLogger(ConfigTransformer.class);
+
+ private final ZooReaderWriter zrw;
+ private final VersionedPropCodec codec;
+ private final PropStoreWatcher propStoreWatcher;
+ private final Retry retry;
+
+ /**
+ * Instantiate a transformer instance.
+ *
+ * @param zrw
+ * a ZooReaderWriter
+ * @param codec
+ * the codec used to encode to the single-node format.
+ * @param propStoreWatcher
+ * the watcher registered to receive future notifications of changes to the encoded
+ * property node.
+ */
+ public ConfigTransformer(final ZooReaderWriter zrw, VersionedPropCodec codec,
+ final PropStoreWatcher propStoreWatcher) {
+ this.zrw = zrw;
+ this.codec = codec;
+ this.propStoreWatcher = propStoreWatcher;
+
+ // default - allow for a conservative max delay of about a minute
+ retry =
+ Retry.builder().maxRetries(15).retryAfter(250, MILLISECONDS).incrementBy(500, MILLISECONDS)
+ .maxWait(5, SECONDS).backOffFactor(1.75).logInterval(3, MINUTES).createRetry();
+
+ }
+
+ public ConfigTransformer(final ZooReaderWriter zrw, VersionedPropCodec codec,
+ final PropStoreWatcher propStoreWatcher, final Retry retry) {
+ this.zrw = zrw;
+ this.codec = codec;
+ this.propStoreWatcher = propStoreWatcher;
+ this.retry = retry;
+ }
+
+ /**
+ * Transform the properties for the provided prop cache key.
+ *
+ * @return the encoded properties.
+ */
+ public VersionedProperties transform(final PropCacheKey<?> propCacheKey) {
+ TransformToken token = TransformToken.createToken(propCacheKey, zrw);
+ return transform(propCacheKey, token);
+ }
+
+ // Allow external (mocked) TransformToken to be used
+ @VisibleForTesting
+ VersionedProperties transform(final PropCacheKey<?> propCacheKey, final TransformToken token) {
+
+ log.info("checking for legacy property upgrade transform for {}", propCacheKey);
+
+ VersionedProperties results;
+ Instant start = Instant.now();
+ try {
+
+ // check for node - just return if it exists.
+ results = ZooPropStore.readFromZk(propCacheKey, propStoreWatcher, zrw);
+ if (results != null) {
+ log.debug(
+ "Found existing node at {}. skipping legacy prop conversion - version: {}, timestamp: {}",
+ propCacheKey, results.getDataVersion(), results.getTimestamp());
+ return results;
+ }
+
+ while (!token.haveTokenOwnership()) {
+ try {
+ retry.useRetry();
+ retry.waitForNextAttempt();
+ // look and return node if created while trying to token.
+ log.trace("own the token - look for existing encoded node at: {}",
+ propCacheKey.getPath());
+ results = ZooPropStore.readFromZk(propCacheKey, propStoreWatcher, zrw);
+ if (results != null) {
+ log.debug(
+ "Found existing node after getting token at {}. skipping legacy prop conversion - version: {}, timestamp: {}",
+ propCacheKey, results.getDataVersion(), results.getTimestamp());
+ return results;
+ }
+ // still does not exist - try again.
+ token.getTokenOwnership();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Failed to hold transform token for " + propCacheKey, ex);
+ } catch (IllegalStateException ex) {
+ throw new IllegalStateException("Failed to hold transform token for " + propCacheKey, ex);
+ }
+ }
+
+ Set<LegacyPropNode> upgradeNodes = readLegacyProps(propCacheKey);
+ if (upgradeNodes == null) {
+ log.info("Found existing node after reading legacy props {}, skipping conversion",
+ propCacheKey);
+ results = ZooPropStore.readFromZk(propCacheKey, propStoreWatcher, zrw);
+ if (results != null) {
+ return results;
+ }
+ }
+
+ upgradeNodes = convertDeprecatedProps(propCacheKey, upgradeNodes);
+
+ results = writeConverted(propCacheKey, upgradeNodes);
+
+ if (results == null) {
+ throw new IllegalStateException("Could not create properties for " + propCacheKey);
+ }
+
+ // validate token still valid before deletion.
+ if (!token.validateToken()) {
+ throw new IllegalStateException(
+ "legacy conversion failed. Lost transform token for " + propCacheKey);
+ }
+
+ int errorCount = deleteLegacyProps(upgradeNodes);
+ log.debug("deleted legacy props - error count: {}", errorCount);
+ log.debug("property transform for {} took {} ms", propCacheKey,
+ new DurationFormat(Duration.between(start, Instant.now()).toMillis(), ""));
+
+ return results;
+
+ } catch (Exception ex) {
+ log.info("Exception on upgrading legacy properties for: " + propCacheKey, ex);
+ } finally {
+ token.releaseToken();
+ }
+ return null;
+ }
+
+ private Set<LegacyPropNode> convertDeprecatedProps(PropCacheKey<?> propCacheKey,
+ Set<LegacyPropNode> upgradeNodes) {
+
+ if (!(propCacheKey instanceof SystemPropKey)) {
+ return upgradeNodes;
+ }
+
+ Set<LegacyPropNode> renamedNodes = new TreeSet<>();
+
+ for (LegacyPropNode original : upgradeNodes) {
+ var finalName = DeprecatedPropertyUtil.getReplacementName(original.getPropName(),
+ (log, replacement) -> log
+ .info("Automatically renaming deprecated property '{}' with its replacement '{}'"
+ + " in ZooKeeper configuration upgrade.", original, replacement));
+ LegacyPropNode renamed = new LegacyPropNode(original.getPath(), finalName, original.getData(),
+ original.getNodeVersion());
+ renamedNodes.add(renamed);
+ }
+ return renamedNodes;
+ }
+
+ private @Nullable Set<LegacyPropNode> readLegacyProps(PropCacheKey<?> propCacheKey) {
+
+ Set<LegacyPropNode> legacyProps = new TreeSet<>();
+
+ // strip leading slash
+ var tokenName = TransformToken.TRANSFORM_TOKEN.substring(1);
+
+ try {
+ var keyBasePath = propCacheKey.getBasePath();
+ List<String> childNames = zrw.getChildren(keyBasePath);
+ for (String propName : childNames) {
+ log.trace("processing ZooKeeper child node: {} for: {}", propName, propCacheKey);
+ if (tokenName.equals(propName)) {
+ continue;
+ }
+ if (PropCacheKey.PROP_NODE_NAME.equals(propName)) {
+ log.debug(
+ "encoded property node exists for {}. Legacy conversion ignoring conversion of this node",
+ propCacheKey);
+ return null;
+ }
+ log.trace("Adding: {} to list for legacy conversion", propName);
+
+ var path = keyBasePath + "/" + propName;
+ Stat stat = new Stat();
+ byte[] bytes = zrw.getData(path, stat);
+
+ try {
+ LegacyPropNode node;
+ if (stat.getDataLength() > 0) {
+ node = new LegacyPropNode(path, propName, new String(bytes, UTF_8), stat.getVersion());
+ } else {
+ node = new LegacyPropNode(path, propName, "", stat.getVersion());
+ }
+ legacyProps.add(node);
+ } catch (IllegalStateException ex) {
+ log.warn("Skipping invalid property at path " + path, ex);
+ }
+ }
+
+ } catch (KeeperException e) {
+ // TODO add handling
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ e.printStackTrace();
+ }
+ return legacyProps;
+ }
+
+ private int deleteLegacyProps(Set<LegacyPropNode> nodes) {
+ int errorCount = 0;
+ for (LegacyPropNode n : nodes) {
+ try {
+ zrw.deleteStrict(n.getPath(), n.getNodeVersion());
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("interrupt received during upgrade node clean-up", ex);
+ } catch (KeeperException ex) {
+ errorCount++;
+ log.info("Failed to delete node during upgrade clean-up", ex);
+ }
+ }
+ return errorCount;
+ }
+
+ private @Nullable VersionedProperties writeConverted(final PropCacheKey<?> propCacheKey,
+ final Set<LegacyPropNode> nodes) {
+ final Map<String,String> props = new HashMap<>();
+ nodes.forEach(node -> props.put(node.getPropName(), node.getData()));
+ VersionedProperties vProps = new VersionedProperties(props);
+ String path = propCacheKey.getPath();
+ try {
+ try {
+ zrw.putPersistentData(path, codec.toBytes(vProps), ZooUtil.NodeExistsPolicy.FAIL);
+ } catch (KeeperException.NodeExistsException ex) {
+ vProps = ZooPropStore.readFromZk(propCacheKey, propStoreWatcher, zrw);
+ }
+ } catch (InterruptedException | IOException | KeeperException ex) {
+ if (ex instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ throw new IllegalStateException(
+ "failed to create node for " + propCacheKey + " on conversion", ex);
+ }
+ if (!validateWrite(propCacheKey, vProps)) {
+ // failed validation
+ return null;
+ }
+
+ return vProps;
+ }
+
+ private boolean validateWrite(final PropCacheKey<?> propCacheKey,
+ final VersionedProperties vProps) {
+ try {
+ Stat stat = zrw.getStatus(propCacheKey.getPath(), propStoreWatcher);
+ if (stat == null) {
+ throw new IllegalStateException(
+ "failed to get stat to validate created node for " + propCacheKey);
+ }
+ return stat.getVersion() == vProps.getDataVersion();
+ } catch (KeeperException ex) {
+ throw new IllegalStateException("failed to validate created node for " + propCacheKey, ex);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("failed to validate created node for " + propCacheKey, ex);
+ }
+ }
+
+ /**
+ * Immutable container for legacy ZooKeeper property node information.
+ */
+ private static class LegacyPropNode implements Comparable<LegacyPropNode> {
+ private final String path;
+ private final String propName;
+ private final String data;
+ private final int nodeVersion;
+
+ /**
+ * An immutable instance of legacy ZooKeeper property node information. It holds the property
+ * and the node stat for later comparison to enable detection of ZooKeeper node changes. If the
+ * legacy property name has been deprecated, the property is renamed and the conversion is noted
+ * in the log.
+ *
+ * @param path
+ * the ZooKeeper path
+ * @param propName
+ * the property name - if deprecated it will be stored as the updated name and the
+ * conversion logged.
+ * @param data
+ * the property value
+ * @param nodeVersion
+ * the ZooKeeper stat data version.
+ */
+ public LegacyPropNode(@NonNull final String path, final String propName, final String data,
+ final int nodeVersion) {
+ this.path = path;
+ this.propName = propName;
+ this.data = data;
+ this.nodeVersion = nodeVersion;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getPropName() {
+ return propName;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public int getNodeVersion() {
+ return nodeVersion;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ LegacyPropNode that = (LegacyPropNode) o;
+ return path.equals(that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(path);
+ }
+
+ @Override
+ public int compareTo(LegacyPropNode other) {
+ return path.compareTo(other.path);
+ }
+ }
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/util/PropSnapshot.java b/server/base/src/main/java/org/apache/accumulo/server/conf/util/PropSnapshot.java
new file mode 100644
index 0000000000..28cc7371c0
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/util/PropSnapshot.java
@@ -0,0 +1,141 @@
+/*
+ * 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.accumulo.server.conf.util;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.PropChangeListener;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class provides a secondary, local cache of properties and is intended to be used to optimize
+ * constructing a configuration hierarchy from the underlying properties stored in ZooKeeper.
+ * <p>
+ * Configurations and especially the derivers use an updateCount to detect configuration changes.
+ * The updateCount is checked frequently and the configuration hierarchy rebuilt when a change is
+ * detected.
+ */
+public class PropSnapshot implements PropChangeListener {
+
+ private static final Logger log = LoggerFactory.getLogger(PropSnapshot.class);
+
+ private final Lock updateLock = new ReentrantLock();
+ private final AtomicBoolean needsUpdate = new AtomicBoolean(true);
+ private final AtomicReference<VersionedProperties> vPropRef = new AtomicReference<>();
+ private final PropCacheKey<?> propCacheKey;
+ private final PropStore propStore;
+
+ public static PropSnapshot create(final PropCacheKey<?> propCacheKey, final PropStore propStore) {
+ var ps = new PropSnapshot(propCacheKey, propStore);
+ propStore.registerAsListener(propCacheKey, ps);
+ return ps;
+ }
+
+ private PropSnapshot(final PropCacheKey<?> propCacheKey, final PropStore propStore) {
+ this.propCacheKey = propCacheKey;
+ this.propStore = propStore;
+ }
+
+ /**
+ * Get the current snapshot - updating if necessary.
+ *
+ * @return the current property snapshot.
+ */
+ public @NonNull VersionedProperties getVersionedProperties() {
+ updateSnapshot();
+ var answer = vPropRef.get();
+ if (answer == null) {
+ throw new IllegalStateException("Invalid state for property snapshot, no value has been set");
+ }
+ return answer;
+ }
+
+ /**
+ * Signal the current snapshot is invalid and needs to be updated on next access.
+ */
+ public void requireUpdate() {
+ updateLock.lock();
+ try {
+ needsUpdate.set(true);
+ } finally {
+ updateLock.unlock();
+ }
+ }
+
+ /**
+ * Update the current snapshot if a refresh is required.
+ *
+ * @throws IllegalStateException
+ * if the properties cannot be retrieved from the underlying store.
+ */
+ private void updateSnapshot() {
+ if (!needsUpdate.get()) {
+ return;
+ }
+ updateLock.lock();
+ try {
+ // check after locked - another thread could have updated while waiting for lock
+ if (!needsUpdate.get()) {
+ return;
+ }
+ var vProps = propStore.get(propCacheKey);
+ vPropRef.set(vProps);
+ needsUpdate.set(false);
+ } finally {
+ updateLock.unlock();
+ }
+ }
+
+ @Override
+ public void zkChangeEvent(final PropCacheKey<?> eventPropKey) {
+ if (propCacheKey.equals(eventPropKey)) {
+ requireUpdate();
+ }
+ }
+
+ @Override
+ public void cacheChangeEvent(final PropCacheKey<?> eventPropKey) {
+ if (propCacheKey.equals(eventPropKey)) {
+ requireUpdate();
+ }
+ }
+
+ @Override
+ public void deleteEvent(final PropCacheKey<?> eventPropKey) {
+ if (propCacheKey.equals(eventPropKey)) {
+ requireUpdate();
+ log.debug("Received property delete event for {}", propCacheKey);
+ }
+ }
+
+ @Override
+ public void connectionEvent() {
+ requireUpdate();
+ log.debug("Received connection event - update properties required");
+ }
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/conf/util/TransformToken.java b/server/base/src/main/java/org/apache/accumulo/server/conf/util/TransformToken.java
new file mode 100644
index 0000000000..943fc8fa7a
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/conf/util/TransformToken.java
@@ -0,0 +1,209 @@
+/*
+ * 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.accumulo.server.conf.util;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.UUID;
+
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides a token used in property conversion. The token is used to limit the number of processes
+ * that try to create a property node and transform the legacy property format to the 2.1 encoded
+ * properties. Processes do not queue for a token (using a sequential node) - processes should look
+ * for the token to exist, and if present wait and then periodically re-check for the property node
+ * to be created by the process that created / has the token.
+ * <p>
+ * Features
+ * <ul>
+ * <li>Uses ephemeral node that will be removed if transform process terminates without
+ * completing</li>
+ * <li>Watcher not necessary - the existence of the node and uuid in data sufficient to detect
+ * changes.</li>
+ * </ul>
+ */
+public class TransformToken {
+ public static final String TRANSFORM_TOKEN = "/transform_token";
+ private static final Logger log = LoggerFactory.getLogger(TransformToken.class);
+ private final TokenUUID tokenUUID = new TokenUUID();
+ private final String path;
+ private final ZooReaderWriter zrw;
+ private boolean haveToken = false;
+
+ private TransformToken(final @NonNull PropCacheKey<?> key, final ZooReaderWriter zrw) {
+ path = key.getBasePath() + TRANSFORM_TOKEN;
+ this.zrw = zrw;
+
+ boolean t = getTokenOwnership();
+ log.trace("created token - token held: {}", t);
+ }
+
+ /**
+ * Create a lock node in ZooKeeper using an ephemeral node. Will not throw and exception except on
+ * an interrupt. If the lock node is created, the returned lock will be locked. If another lock
+ * already exists, the lock is unlocked and the caller can decide to either wait for the resource
+ * to be created by the thread that created the lock, or try calling to {@code lock} to succeed
+ *
+ * @param key
+ * a PropCacheKey that defines the storage location of the created lock and the
+ * associated property nodes.
+ * @param zrw
+ * a ZooReaderWriter
+ * @return an TransformLock instance.
+ * @throws IllegalStateException
+ * is the lock creation fails due to an underlying ZooKeeper exception.
+ */
+ public static TransformToken createToken(final @NonNull PropCacheKey<?> key,
+ final ZooReaderWriter zrw) {
+ return new TransformToken(key, zrw);
+ }
+
+ /**
+ * Create and try to establish ownership (hold the token). Token ownership can be tested with
+ * {@link #haveTokenOwnership() haveTokenOwnership}
+ *
+ * @return true if able to get ownership, false otherwise.
+ */
+ public boolean getTokenOwnership() {
+ if (haveToken) {
+ return true;
+ }
+ try {
+ // existence check should be lighter-weight than failing on NODE_EXISTS exception
+ if (zrw.exists(path)) {
+ return false;
+ }
+ // if this completes this thread has created the lock
+ zrw.putEphemeralData(path, tokenUUID.asBytes());
+ log.trace("wrote property transform token: {} - {}", path, tokenUUID);
+ haveToken = true;
+ return true;
+ } catch (KeeperException ex) {
+ log.debug(
+ "Failed to write transform token for " + path + " another process may have created one",
+ ex);
+ return false;
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted getting transform token", ex);
+ }
+ }
+
+ /**
+ * Return the token ownership status
+ *
+ * @return true if this instance has ownership of the token, false otherwise.
+ */
+ public boolean haveTokenOwnership() {
+ return haveToken;
+ }
+
+ /**
+ * Verify ownership is still valid while holding the token.
+ *
+ * @return true if token is still owned, false otherwise
+ */
+ public boolean validateToken() {
+ try {
+ byte[] readId = zrw.getData(path);
+ log.trace("validate token: read: {} - expected: {}", readId, tokenUUID);
+ return Arrays.equals(readId, tokenUUID.asBytes());
+ } catch (KeeperException ex) {
+ throw new IllegalStateException("Failed to validate token", ex);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while validating token", ex);
+ }
+ }
+
+ /**
+ * If the token was created by this instance, the uuid of this instance and the uuid stored in the
+ * ZooKeeper data will match.
+ */
+ public void releaseToken() {
+ try {
+ if (log.isTraceEnabled()) {
+ log.trace("releaseToken called - {} - exists in ZooKeeper: {}", path, zrw.exists(path));
+ }
+
+ Stat stat = new Stat();
+ byte[] readId = zrw.getData(path, stat);
+ if (!Arrays.equals(readId, tokenUUID.asBytes())) {
+ throw new IllegalStateException(
+ "tried to release a token that was not held by current thread");
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("releaseToken read id: {} - exists: {}", readId, zrw.exists(path));
+ }
+
+ // make sure we are deleting the same node version just checked.
+ zrw.deleteStrict(path, stat.getVersion());
+ } catch (KeeperException ex) {
+ throw new IllegalStateException("Failed to release transform lock for " + path, ex);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted getting transform token", ex);
+ }
+ haveToken = false;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ TransformToken that = (TransformToken) o;
+ return path.equals(that.path) && Arrays.equals(tokenUUID.asBytes(), that.tokenUUID.asBytes());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(path, Arrays.hashCode(tokenUUID.asBytes()));
+ }
+
+ @Override
+ public String toString() {
+ return "TransformLock{ path='" + path + "', locked='" + haveToken + "' id=" + tokenUUID + "'}'";
+ }
+
+ private static class TokenUUID {
+ private final String id = UUID.randomUUID().toString();
+ private final byte[] idBytes = id.getBytes(UTF_8);
+
+ public byte[] asBytes() {
+ return idBytes;
+ }
+
+ @Override
+ public String toString() {
+ return "TransformToken{id='" + id + '}';
+ }
+ }
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/init/FileSystemInitializer.java b/server/base/src/main/java/org/apache/accumulo/server/init/FileSystemInitializer.java
index 5ebc189ac4..3c020415b2 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/init/FileSystemInitializer.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/init/FileSystemInitializer.java
@@ -51,9 +51,9 @@ import org.apache.accumulo.core.spi.fs.VolumeChooserEnvironment;
import org.apache.accumulo.core.util.ColumnFQ;
import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.store.TablePropKey;
import org.apache.accumulo.server.fs.VolumeChooserEnvironmentImpl;
import org.apache.accumulo.server.fs.VolumeManager;
-import org.apache.accumulo.server.util.TablePropUtil;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
@@ -68,13 +68,9 @@ class FileSystemInitializer {
// config only for root table
private final InitialConfiguration initConfig;
- private final ZooReaderWriter zoo;
- private final String zkRoot;
FileSystemInitializer(InitialConfiguration initConfig, ZooReaderWriter zoo, InstanceId uuid) {
this.initConfig = initConfig;
- this.zoo = zoo;
- this.zkRoot = Constants.ZROOT + "/" + uuid;
}
private static class Tablet {
@@ -96,7 +92,7 @@ class FileSystemInitializer {
ServerContext context) throws IOException, InterruptedException, KeeperException {
SiteConfiguration siteConfig = initConfig.getSiteConf();
// initialize initial system tables config in zookeeper
- initSystemTablesConfig();
+ initSystemTablesConfig(context);
Text splitPoint = MetadataSchema.TabletsSection.getRange().getEndKey().getRow();
@@ -160,20 +156,23 @@ class FileSystemInitializer {
}
}
- private void initSystemTablesConfig() throws IOException, InterruptedException, KeeperException {
- setTableProperties(RootTable.ID, initConfig.getRootTableConf());
- setTableProperties(RootTable.ID, initConfig.getRootMetaConf());
- setTableProperties(MetadataTable.ID, initConfig.getRootMetaConf());
- setTableProperties(MetadataTable.ID, initConfig.getMetaTableConf());
- setTableProperties(REPL_TABLE_ID, initConfig.getReplTableConf());
+ private void initSystemTablesConfig(final ServerContext context)
+ throws IOException, InterruptedException, KeeperException {
+ setTableProperties(context, RootTable.ID, initConfig.getRootTableConf());
+ setTableProperties(context, RootTable.ID, initConfig.getRootMetaConf());
+ setTableProperties(context, MetadataTable.ID, initConfig.getRootMetaConf());
+ setTableProperties(context, MetadataTable.ID, initConfig.getMetaTableConf());
+ setTableProperties(context, REPL_TABLE_ID, initConfig.getReplTableConf());
}
- private void setTableProperties(TableId tableId, HashMap<String,String> props)
- throws IOException, InterruptedException, KeeperException {
- for (Map.Entry<String,String> entry : props.entrySet()) {
- if (!TablePropUtil.setTableProperty(zoo, zkRoot, tableId, entry.getKey(), entry.getValue())) {
- throw new IOException("Cannot create per-table property " + entry.getKey());
- }
+ private void setTableProperties(final ServerContext context, TableId tableId,
+ HashMap<String,String> props) {
+ var propStore = context.getPropStore();
+ TablePropKey tablePropKey = TablePropKey.of(context, tableId);
+ if (propStore.exists(tablePropKey)) {
+ propStore.putAll(tablePropKey, props);
+ } else {
+ propStore.create(tablePropKey, props);
}
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/init/Initialize.java b/server/base/src/main/java/org/apache/accumulo/server/init/Initialize.java
index 9a5cdbbc43..0692a5fe71 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/init/Initialize.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/init/Initialize.java
@@ -158,9 +158,12 @@ public class Initialize implements KeywordExecutable {
return false;
}
- InstanceId iid = InstanceId.of(UUID.randomUUID());
+ InstanceId instanceId = InstanceId.of(UUID.randomUUID());
+ ZooKeeperInitializer zki = new ZooKeeperInitializer();
+ zki.initializeConfig(instanceId, zoo);
+
try (ServerContext context =
- ServerContext.initialize(initConfig.getSiteConf(), instanceName, iid)) {
+ ServerContext.initialize(initConfig.getSiteConf(), instanceName, instanceId)) {
var chooserEnv = new VolumeChooserEnvironmentImpl(Scope.INIT, RootTable.ID, null, context);
String rootTabletDirName = RootTable.ROOT_TABLET_DIR_NAME;
String ext = FileOperations.getNewFileExtension(DefaultConfiguration.getInstance());
@@ -168,14 +171,13 @@ public class Initialize implements KeywordExecutable {
fs.choose(chooserEnv, initConfig.getVolumeUris()) + SEPARATOR + TABLE_DIR + SEPARATOR
+ RootTable.ID + SEPARATOR + rootTabletDirName + SEPARATOR + "00000_00000." + ext)
.toString();
-
- ZooKeeperInitializer zki = new ZooKeeperInitializer();
- zki.initialize(zoo, opts.clearInstanceName, iid, instanceNamePath, rootTabletDirName,
+ zki.initialize(context, opts.clearInstanceName, instanceNamePath, rootTabletDirName,
rootTabletFileUri);
- if (!createDirs(fs, iid, initConfig.getVolumeUris())) {
+
+ if (!createDirs(fs, instanceId, initConfig.getVolumeUris())) {
throw new IOException("Problem creating directories on " + fs.getVolumes());
}
- var fileSystemInitializer = new FileSystemInitializer(initConfig, zoo, iid);
+ var fileSystemInitializer = new FileSystemInitializer(initConfig, zoo, instanceId);
var rootVol = fs.choose(chooserEnv, initConfig.getVolumeUris());
var rootPath = new Path(rootVol + SEPARATOR + TABLE_DIR + SEPARATOR + RootTable.ID + SEPARATOR
+ rootTabletDirName);
@@ -249,7 +251,7 @@ public class Initialize implements KeywordExecutable {
}
}
- private static boolean createDirs(VolumeManager fs, InstanceId iid, Set<String> baseDirs) {
+ private static boolean createDirs(VolumeManager fs, InstanceId instanceId, Set<String> baseDirs) {
try {
for (String baseDir : baseDirs) {
fs.mkdirs(
@@ -257,7 +259,7 @@ public class Initialize implements KeywordExecutable {
new FsPermission("700"));
Path iidLocation = new Path(baseDir, Constants.INSTANCE_ID_DIR);
fs.mkdirs(iidLocation);
- fs.createNewFile(new Path(iidLocation, iid.canonical()));
+ fs.createNewFile(new Path(iidLocation, instanceId.canonical()));
log.info("Created directory {}", baseDir);
}
return true;
@@ -416,7 +418,7 @@ public class Initialize implements KeywordExecutable {
Path iidPath = new Path(aBasePath, Constants.INSTANCE_ID_DIR);
Path versionPath = new Path(aBasePath, Constants.VERSION_DIR);
- InstanceId iid = VolumeManager.getInstanceIDFromHdfs(iidPath, hadoopConf);
+ InstanceId instanceId = VolumeManager.getInstanceIDFromHdfs(iidPath, hadoopConf);
for (Pair<Path,Path> replacementVolume : serverDirs.getVolumeReplacements()) {
if (aBasePath.equals(replacementVolume.getFirst())) {
log.error(
@@ -438,7 +440,7 @@ public class Initialize implements KeywordExecutable {
log.error("Problem getting accumulo data version", e);
return false;
}
- return createDirs(fs, iid, uinitializedDirs);
+ return createDirs(fs, instanceId, uinitializedDirs);
}
private static class Opts extends Help {
diff --git a/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java b/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java
index 822f4c4376..5d377cf00a 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/init/ZooKeeperInitializer.java
@@ -21,6 +21,8 @@ package org.apache.accumulo.server.init;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.accumulo.server.init.Initialize.REPL_TABLE_ID;
+import java.io.IOException;
+
import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.clientImpl.Namespace;
import org.apache.accumulo.core.data.InstanceId;
@@ -31,6 +33,10 @@ import org.apache.accumulo.core.metadata.RootTable;
import org.apache.accumulo.core.metadata.schema.RootTabletMetadata;
import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
import org.apache.accumulo.server.log.WalStateManager;
import org.apache.accumulo.server.metadata.RootGcCandidates;
import org.apache.accumulo.server.tables.TableManager;
@@ -39,12 +45,56 @@ import org.apache.zookeeper.ZooDefs;
class ZooKeeperInitializer {
- void initialize(ZooReaderWriter zoo, boolean clearInstanceName, InstanceId iid,
- String instanceNamePath, String rootTabletDirName, String rootTabletFileUri)
+ private final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+ private final byte[] ZERO_CHAR_ARRAY = {'0'};
+
+ /**
+ * The prop store requires that the system config node exists so that it can set watchers. This
+ * methods creates the ZooKeeper nodes /accumulo/INSTANCE_ID/config/encoded_props and sets the
+ * encoded_props to a default, empty node if it does not exist. If any of the paths or the
+ * encoded_props node exist, the are skipped and not modified.
+ *
+ * @param instanceId
+ * the instance id
+ * @param zoo
+ * a ZooReaderWriter
+ */
+ void initializeConfig(final InstanceId instanceId, final ZooReaderWriter zoo) {
+ try {
+
+ zoo.putPersistentData(Constants.ZROOT, new byte[0], ZooUtil.NodeExistsPolicy.SKIP,
+ ZooDefs.Ids.OPEN_ACL_UNSAFE);
+
+ String zkInstanceRoot = Constants.ZROOT + "/" + instanceId;
+ zoo.putPersistentData(zkInstanceRoot, EMPTY_BYTE_ARRAY, ZooUtil.NodeExistsPolicy.SKIP);
+ zoo.putPersistentData(zkInstanceRoot + Constants.ZCONFIG, EMPTY_BYTE_ARRAY,
+ ZooUtil.NodeExistsPolicy.SKIP);
+
+ var sysPropPath = SystemPropKey.of(instanceId).getPath();
+ VersionedProperties vProps = new VersionedProperties();
+ // skip if the encoded props node exists
+ if (zoo.exists(sysPropPath)) {
+ return;
+ }
+ var created = zoo.putPersistentData(sysPropPath,
+ VersionedPropCodec.getDefault().toBytes(vProps), ZooUtil.NodeExistsPolicy.FAIL);
+ if (!created) {
+ throw new IllegalStateException(
+ "Failed to create default system props during initialization at: {}" + sysPropPath);
+ }
+ } catch (IOException | KeeperException | InterruptedException ex) {
+ throw new IllegalStateException("Failed to initialize configuration for prop store", ex);
+ }
+ }
+
+ void initialize(final ServerContext context, final boolean clearInstanceName,
+ final String instanceNamePath, final String rootTabletDirName, final String rootTabletFileUri)
throws KeeperException, InterruptedException {
// setup basic data in zookeeper
- zoo.putPersistentData(Constants.ZROOT, new byte[0], ZooUtil.NodeExistsPolicy.SKIP,
- ZooDefs.Ids.OPEN_ACL_UNSAFE);
+
+ ZooReaderWriter zoo = context.getZooReaderWriter();
+ InstanceId instanceId = context.getInstanceID();
+
zoo.putPersistentData(Constants.ZROOT + Constants.ZINSTANCES, new byte[0],
ZooUtil.NodeExistsPolicy.SKIP, ZooDefs.Ids.OPEN_ACL_UNSAFE);
@@ -52,30 +102,28 @@ class ZooKeeperInitializer {
if (clearInstanceName) {
zoo.recursiveDelete(instanceNamePath, ZooUtil.NodeMissingPolicy.SKIP);
}
- zoo.putPersistentData(instanceNamePath, iid.canonical().getBytes(UTF_8),
+ zoo.putPersistentData(instanceNamePath, instanceId.canonical().getBytes(UTF_8),
ZooUtil.NodeExistsPolicy.FAIL);
- final byte[] EMPTY_BYTE_ARRAY = new byte[0];
- final byte[] ZERO_CHAR_ARRAY = {'0'};
-
// setup the instance
- String zkInstanceRoot = Constants.ZROOT + "/" + iid;
- zoo.putPersistentData(zkInstanceRoot, EMPTY_BYTE_ARRAY, ZooUtil.NodeExistsPolicy.FAIL);
+ String zkInstanceRoot = Constants.ZROOT + "/" + instanceId;
zoo.putPersistentData(zkInstanceRoot + Constants.ZTABLES, Constants.ZTABLES_INITIAL_ID,
ZooUtil.NodeExistsPolicy.FAIL);
zoo.putPersistentData(zkInstanceRoot + Constants.ZNAMESPACES, new byte[0],
ZooUtil.NodeExistsPolicy.FAIL);
- TableManager.prepareNewNamespaceState(zoo, iid, Namespace.DEFAULT.id(),
- Namespace.DEFAULT.name(), ZooUtil.NodeExistsPolicy.FAIL);
- TableManager.prepareNewNamespaceState(zoo, iid, Namespace.ACCUMULO.id(),
+
+ TableManager.prepareNewNamespaceState(context, Namespace.DEFAULT.id(), Namespace.DEFAULT.name(),
+ ZooUtil.NodeExistsPolicy.FAIL);
+ TableManager.prepareNewNamespaceState(context, Namespace.ACCUMULO.id(),
Namespace.ACCUMULO.name(), ZooUtil.NodeExistsPolicy.FAIL);
- TableManager.prepareNewTableState(zoo, iid, RootTable.ID, Namespace.ACCUMULO.id(),
+
+ TableManager.prepareNewTableState(context, RootTable.ID, Namespace.ACCUMULO.id(),
RootTable.NAME, TableState.ONLINE, ZooUtil.NodeExistsPolicy.FAIL);
- TableManager.prepareNewTableState(zoo, iid, MetadataTable.ID, Namespace.ACCUMULO.id(),
+ TableManager.prepareNewTableState(context, MetadataTable.ID, Namespace.ACCUMULO.id(),
MetadataTable.NAME, TableState.ONLINE, ZooUtil.NodeExistsPolicy.FAIL);
@SuppressWarnings("deprecation")
String replicationTableName = org.apache.accumulo.core.replication.ReplicationTable.NAME;
- TableManager.prepareNewTableState(zoo, iid, REPL_TABLE_ID, Namespace.ACCUMULO.id(),
+ TableManager.prepareNewTableState(context, REPL_TABLE_ID, Namespace.ACCUMULO.id(),
replicationTableName, TableState.OFFLINE, ZooUtil.NodeExistsPolicy.FAIL);
zoo.putPersistentData(zkInstanceRoot + Constants.ZTSERVERS, EMPTY_BYTE_ARRAY,
ZooUtil.NodeExistsPolicy.FAIL);
@@ -96,8 +144,6 @@ class ZooKeeperInitializer {
ZooUtil.NodeExistsPolicy.FAIL);
zoo.putPersistentData(zkInstanceRoot + Constants.ZGC_LOCK, EMPTY_BYTE_ARRAY,
ZooUtil.NodeExistsPolicy.FAIL);
- zoo.putPersistentData(zkInstanceRoot + Constants.ZCONFIG, EMPTY_BYTE_ARRAY,
- ZooUtil.NodeExistsPolicy.FAIL);
zoo.putPersistentData(zkInstanceRoot + Constants.ZTABLE_LOCKS, EMPTY_BYTE_ARRAY,
ZooUtil.NodeExistsPolicy.FAIL);
zoo.putPersistentData(zkInstanceRoot + Constants.ZHDFS_RESERVATIONS, EMPTY_BYTE_ARRAY,
diff --git a/server/base/src/main/java/org/apache/accumulo/server/tables/TableManager.java b/server/base/src/main/java/org/apache/accumulo/server/tables/TableManager.java
index 25f95ac3fb..1b532e7cae 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/tables/TableManager.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/tables/TableManager.java
@@ -24,7 +24,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Set;
import org.apache.accumulo.core.Constants;
@@ -40,7 +39,9 @@ import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeExistsPolicy;
import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeMissingPolicy;
import org.apache.accumulo.server.ServerContext;
-import org.apache.accumulo.server.util.TablePropUtil;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.TablePropKey;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
@@ -62,11 +63,11 @@ public class TableManager {
private final String zkRoot;
private final InstanceId instanceID;
private final ZooReaderWriter zoo;
- private ZooCache zooStateCache;
+ private final ZooCache zooStateCache;
- public static void prepareNewNamespaceState(ZooReaderWriter zoo, InstanceId instanceId,
- NamespaceId namespaceId, String namespace, NodeExistsPolicy existsPolicy)
- throws KeeperException, InterruptedException {
+ public static void prepareNewNamespaceState(ZooReaderWriter zoo, final PropStore propStore,
+ InstanceId instanceId, NamespaceId namespaceId, String namespace,
+ NodeExistsPolicy existsPolicy) throws KeeperException, InterruptedException {
log.debug("Creating ZooKeeper entries for new namespace {} (ID: {})", namespace, namespaceId);
String zPath = Constants.ZROOT + "/" + instanceId + Constants.ZNAMESPACES + "/" + namespaceId;
@@ -74,11 +75,24 @@ public class TableManager {
zoo.putPersistentData(zPath + Constants.ZNAMESPACE_NAME, namespace.getBytes(UTF_8),
existsPolicy);
zoo.putPersistentData(zPath + Constants.ZNAMESPACE_CONF, new byte[0], existsPolicy);
+
+ var propKey = NamespacePropKey.of(instanceId, namespaceId);
+ if (!propStore.exists(propKey)) {
+ propStore.create(propKey, Map.of());
+ }
}
- public static void prepareNewTableState(ZooReaderWriter zoo, InstanceId instanceId,
- TableId tableId, NamespaceId namespaceId, String tableName, TableState state,
- NodeExistsPolicy existsPolicy) throws KeeperException, InterruptedException {
+ public static void prepareNewNamespaceState(final ServerContext context, NamespaceId namespaceId,
+ String namespace, NodeExistsPolicy existsPolicy)
+ throws KeeperException, InterruptedException {
+ prepareNewNamespaceState(context.getZooReaderWriter(), context.getPropStore(),
+ context.getInstanceID(), namespaceId, namespace, existsPolicy);
+ }
+
+ public static void prepareNewTableState(ZooReaderWriter zoo, PropStore propStore,
+ InstanceId instanceId, TableId tableId, NamespaceId namespaceId, String tableName,
+ TableState state, NodeExistsPolicy existsPolicy)
+ throws KeeperException, InterruptedException {
// state gets created last
log.debug("Creating ZooKeeper entries for new table {} (ID: {}) in namespace (ID: {})",
tableName, tableId, namespaceId);
@@ -96,6 +110,17 @@ public class TableManager {
zoo.putPersistentData(zTablePath + Constants.ZTABLE_COMPACT_CANCEL_ID, ZERO_BYTE, existsPolicy);
zoo.putPersistentData(zTablePath + Constants.ZTABLE_STATE, state.name().getBytes(UTF_8),
existsPolicy);
+ var propKey = TablePropKey.of(instanceId, tableId);
+ if (!propStore.exists(propKey)) {
+ propStore.create(propKey, Map.of());
+ }
+ }
+
+ public static void prepareNewTableState(final ServerContext context, TableId tableId,
+ NamespaceId namespaceId, String tableName, TableState state, NodeExistsPolicy existsPolicy)
+ throws KeeperException, InterruptedException {
+ prepareNewTableState(context.getZooReaderWriter(), context.getPropStore(),
+ context.getInstanceID(), tableId, namespaceId, tableName, state, existsPolicy);
}
public TableManager(ServerContext context) {
@@ -183,16 +208,16 @@ public class TableManager {
public void addTable(TableId tableId, NamespaceId namespaceId, String tableName)
throws KeeperException, InterruptedException, NamespaceNotFoundException {
- prepareNewTableState(zoo, instanceID, tableId, namespaceId, tableName, TableState.NEW,
- NodeExistsPolicy.OVERWRITE);
+ prepareNewTableState(zoo, context.getPropStore(), instanceID, tableId, namespaceId, tableName,
+ TableState.NEW, NodeExistsPolicy.OVERWRITE);
updateTableStateCache(tableId);
}
public void cloneTable(TableId srcTableId, TableId tableId, String tableName,
NamespaceId namespaceId, Map<String,String> propertiesToSet, Set<String> propertiesToExclude)
throws KeeperException, InterruptedException {
- prepareNewTableState(zoo, instanceID, tableId, namespaceId, tableName, TableState.NEW,
- NodeExistsPolicy.OVERWRITE);
+ prepareNewTableState(zoo, context.getPropStore(), instanceID, tableId, namespaceId, tableName,
+ TableState.NEW, NodeExistsPolicy.OVERWRITE);
String srcTablePath = Constants.ZROOT + "/" + instanceID + Constants.ZTABLES + "/" + srcTableId
+ Constants.ZTABLE_CONF;
@@ -200,12 +225,8 @@ public class TableManager {
+ Constants.ZTABLE_CONF;
zoo.recursiveCopyPersistentOverwrite(srcTablePath, newTablePath);
- for (Entry<String,String> entry : propertiesToSet.entrySet())
- TablePropUtil.setTableProperty(context, tableId, entry.getKey(), entry.getValue());
-
- for (String prop : propertiesToExclude)
- zoo.recursiveDelete(Constants.ZROOT + "/" + instanceID + Constants.ZTABLES + "/" + tableId
- + Constants.ZTABLE_CONF + "/" + prop, NodeMissingPolicy.SKIP);
+ context.tablePropUtil().setProperties(tableId, propertiesToSet);
+ context.tablePropUtil().removeProperties(tableId, propertiesToExclude);
updateTableStateCache(tableId);
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/NamespacePropUtil.java b/server/base/src/main/java/org/apache/accumulo/server/util/NamespacePropUtil.java
index bb809c61c0..f141513126 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/util/NamespacePropUtil.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/NamespacePropUtil.java
@@ -18,45 +18,36 @@
*/
package org.apache.accumulo.server.util;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import java.util.Collection;
+import java.util.Map;
-import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.data.NamespaceId;
-import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
-import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeExistsPolicy;
-import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeMissingPolicy;
import org.apache.accumulo.server.ServerContext;
-import org.apache.zookeeper.KeeperException;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
-public class NamespacePropUtil {
- public static boolean setNamespaceProperty(ServerContext context, NamespaceId namespaceId,
- String property, String value) throws KeeperException, InterruptedException {
- if (!Property.isTablePropertyValid(property, value))
- return false;
+public class NamespacePropUtil extends PropUtil<NamespaceId> {
- ZooReaderWriter zoo = context.getZooReaderWriter();
-
- // create the zk node for per-namespace properties for this namespace if it doesn't already
- // exist
- String zkNamespacePath = getPath(context, namespaceId);
- zoo.putPersistentData(zkNamespacePath, new byte[0], NodeExistsPolicy.SKIP);
-
- // create the zk node for this property and set it's data to the specified value
- String zPath = zkNamespacePath + "/" + property;
- zoo.putPersistentData(zPath, value.getBytes(UTF_8), NodeExistsPolicy.OVERWRITE);
-
- return true;
+ public NamespacePropUtil(ServerContext context) {
+ super(context);
}
- public static void removeNamespaceProperty(ServerContext context, NamespaceId namespaceId,
- String property) throws InterruptedException, KeeperException {
- String zPath = getPath(context, namespaceId) + "/" + property;
- context.getZooReaderWriter().recursiveDelete(zPath, NodeMissingPolicy.SKIP);
+ @Override
+ public void setProperties(NamespaceId namespaceId, Map<String,String> properties) {
+ for (Map.Entry<String,String> prop : properties.entrySet()) {
+ // TODO reconcile with TablePropUtil see https://github.com/apache/accumulo/issues/2633
+ if (!Property.isTablePropertyValid(prop.getKey(), prop.getValue())) {
+ throw new IllegalArgumentException("Invalid table property for namespace: " + namespaceId
+ + " name: " + prop.getKey() + ", value: " + prop.getValue());
+ }
+ }
+ context.getPropStore().putAll(NamespacePropKey.of(context, namespaceId), properties);
}
- private static String getPath(ServerContext context, NamespaceId namespaceId) {
- return context.getZooKeeperRoot() + Constants.ZNAMESPACES + "/" + namespaceId
- + Constants.ZNAMESPACE_CONF;
+ @Override
+ public void removeProperties(NamespaceId namespaceId, Collection<String> propertyNames) {
+ context.getPropStore().removeProperties(NamespacePropKey.of(context, namespaceId),
+ propertyNames);
}
+
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/PropUtil.java b/server/base/src/main/java/org/apache/accumulo/server/util/PropUtil.java
new file mode 100644
index 0000000000..5a30dc5527
--- /dev/null
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/PropUtil.java
@@ -0,0 +1,39 @@
+/*
+ * 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.accumulo.server.util;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.accumulo.core.data.AbstractId;
+import org.apache.accumulo.server.ServerContext;
+
+public abstract class PropUtil<T extends AbstractId<T>> {
+
+ protected final ServerContext context;
+
+ protected PropUtil(ServerContext context) {
+ this.context = context;
+ }
+
+ public abstract void setProperties(final T id, final Map<String,String> props);
+
+ public abstract void removeProperties(final T id, final Collection<String> propertyNames);
+
+}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/SystemPropUtil.java b/server/base/src/main/java/org/apache/accumulo/server/util/SystemPropUtil.java
index 35ba55dcb0..b3208d5519 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/util/SystemPropUtil.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/SystemPropUtil.java
@@ -18,16 +18,14 @@
*/
package org.apache.accumulo.server.util;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import java.util.List;
+import java.util.Map;
-import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.conf.DeprecatedPropertyUtil;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.conf.PropertyType;
-import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeExistsPolicy;
-import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeMissingPolicy;
import org.apache.accumulo.server.ServerContext;
-import org.apache.zookeeper.KeeperException;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,15 +33,13 @@ public class SystemPropUtil {
private static final Logger log = LoggerFactory.getLogger(SystemPropUtil.class);
- public static void setSystemProperty(ServerContext context, String property, String value)
- throws KeeperException, InterruptedException {
+ public static void setSystemProperty(ServerContext context, String property, String value) {
// Retrieve the replacement name for this property, if there is one.
// Do this before we check if the name is a valid zookeeper name.
final var original = property;
- property = DeprecatedPropertyUtil.getReplacementName(property, (log, replacement) -> {
- log.warn("{} was deprecated and will be removed in a future release;"
- + " setting its replacement {} instead", original, replacement);
- });
+ property = DeprecatedPropertyUtil.getReplacementName(property,
+ (log, replacement) -> log.warn("{} was deprecated and will be removed in a future release;"
+ + " setting its replacement {} instead", original, replacement));
if (!Property.isValidZooPropertyKey(property)) {
IllegalArgumentException iae =
@@ -69,26 +65,21 @@ public class SystemPropUtil {
log.debug("Attempted to set zookeeper property. Value is either null or invalid", iae);
throw iae;
}
-
- // create the zk node for this property and set it's data to the specified value
- String zPath = context.getZooKeeperRoot() + Constants.ZCONFIG + "/" + property;
-
- context.getZooReaderWriter().putPersistentData(zPath, value.getBytes(UTF_8),
- NodeExistsPolicy.OVERWRITE);
+ context.getPropStore().putAll(SystemPropKey.of(context), Map.of(property, value));
}
- public static void removeSystemProperty(ServerContext context, String property)
- throws InterruptedException, KeeperException {
- String resolved = DeprecatedPropertyUtil.getReplacementName(property, (log, replacement) -> {
- log.warn("{} was deprecated and will be removed in a future release; assuming user meant"
- + " its replacement {} and will remove that instead", property, replacement);
- });
+ public static void removeSystemProperty(ServerContext context, String property) {
+ String resolved =
+ DeprecatedPropertyUtil
+ .getReplacementName(property,
+ (log, replacement) -> log.warn(
+ "{} was deprecated and will be removed in a future release; assuming user meant"
+ + " its replacement {} and will remove that instead",
+ property, replacement));
removePropWithoutDeprecationWarning(context, resolved);
}
- public static void removePropWithoutDeprecationWarning(ServerContext context, String property)
- throws InterruptedException, KeeperException {
- String zPath = context.getZooKeeperRoot() + Constants.ZCONFIG + "/" + property;
- context.getZooReaderWriter().recursiveDelete(zPath, NodeMissingPolicy.FAIL);
+ public static void removePropWithoutDeprecationWarning(ServerContext context, String property) {
+ context.getPropStore().removeProperties(SystemPropKey.of(context), List.of(property));
}
}
diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/TablePropUtil.java b/server/base/src/main/java/org/apache/accumulo/server/util/TablePropUtil.java
index 8980c6b9b5..b3fb54be3b 100644
--- a/server/base/src/main/java/org/apache/accumulo/server/util/TablePropUtil.java
+++ b/server/base/src/main/java/org/apache/accumulo/server/util/TablePropUtil.java
@@ -18,48 +18,41 @@
*/
package org.apache.accumulo.server.util;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
-import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.data.TableId;
-import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
-import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeExistsPolicy;
-import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeMissingPolicy;
import org.apache.accumulo.server.ServerContext;
-import org.apache.zookeeper.KeeperException;
+import org.apache.accumulo.server.conf.store.TablePropKey;
-public class TablePropUtil {
+public class TablePropUtil extends PropUtil<TableId> {
- public static boolean setTableProperty(ServerContext context, TableId tableId, String property,
- String value) throws KeeperException, InterruptedException {
- return setTableProperty(context.getZooReaderWriter(), context.getZooKeeperRoot(), tableId,
- property, value);
+ public TablePropUtil(ServerContext context) {
+ super(context);
}
- public static boolean setTableProperty(ZooReaderWriter zoo, String zkRoot, TableId tableId,
- String property, String value) throws KeeperException, InterruptedException {
- if (!Property.isTablePropertyValid(property, value))
- return false;
+ /**
+ * Helper method to set provided properties for the provided table.
+ *
+ * @throws IllegalStateException
+ * if an underlying exception (KeeperException, InterruptException) or other failure to
+ * read properties from the cache / backend store
+ */
+ @Override
+ public void setProperties(TableId tableId, Map<String,String> props) {
+ Map<String,String> tempProps = new HashMap<>(props);
+ // TODO reconcile with NamespacePropUtil see https://github.com/apache/accumulo/issues/2633
+ tempProps.entrySet().removeIf(e -> !Property.isTablePropertyValid(e.getKey(), e.getValue()));
+
+ context.getPropStore().putAll(TablePropKey.of(context, tableId), props);
- // create the zk node for per-table properties for this table if it doesn't already exist
- String zkTablePath = getTablePath(zkRoot, tableId);
- zoo.putPersistentData(zkTablePath, new byte[0], NodeExistsPolicy.SKIP);
-
- // create the zk node for this property and set it's data to the specified value
- String zPath = zkTablePath + "/" + property;
- zoo.putPersistentData(zPath, value.getBytes(UTF_8), NodeExistsPolicy.OVERWRITE);
-
- return true;
}
- public static void removeTableProperty(ServerContext context, TableId tableId, String property)
- throws InterruptedException, KeeperException {
- String zPath = getTablePath(context.getZooKeeperRoot(), tableId) + "/" + property;
- context.getZooReaderWriter().recursiveDelete(zPath, NodeMissingPolicy.SKIP);
+ @Override
+ public void removeProperties(TableId tableId, Collection<String> propertyNames) {
+ context.getPropStore().removeProperties(TablePropKey.of(context, tableId), propertyNames);
}
- private static String getTablePath(String zkRoot, TableId tableId) {
- return zkRoot + Constants.ZTABLES + "/" + tableId.canonical() + Constants.ZTABLE_CONF;
- }
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/MockServerContext.java b/server/base/src/test/java/org/apache/accumulo/server/MockServerContext.java
index f798b5e846..a3ab1350b8 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/MockServerContext.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/MockServerContext.java
@@ -18,6 +18,7 @@
*/
package org.apache.accumulo.server;
+import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import java.util.Properties;
@@ -27,6 +28,8 @@ import org.apache.accumulo.core.conf.DefaultConfiguration;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.crypto.CryptoServiceFactory;
import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.server.conf.store.PropStore;
import org.easymock.EasyMock;
/**
@@ -54,4 +57,14 @@ public class MockServerContext {
return sc;
}
+ public static ServerContext getMockContextWithPropStore(final InstanceId instanceID,
+ ZooReaderWriter zrw, PropStore propStore) {
+
+ ServerContext sc = createMock(ServerContext.class);
+ expect(sc.getInstanceID()).andReturn(instanceID).anyTimes();
+ expect(sc.getZooReaderWriter()).andReturn(zrw).anyTimes();
+ expect(sc.getZooKeeperRoot()).andReturn("/accumulo/" + instanceID).anyTimes();
+ expect(sc.getPropStore()).andReturn(propStore).anyTimes();
+ return sc;
+ }
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/NamespaceConfigurationTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/NamespaceConfigurationTest.java
index dcc6286760..c57d4248de 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/NamespaceConfigurationTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/NamespaceConfigurationTest.java
@@ -18,133 +18,158 @@
*/
package org.apache.accumulo.server.conf;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.accumulo.server.MockServerContext.getMockContextWithPropStore;
+import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
-import java.util.List;
+import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import java.util.function.Predicate;
-import org.apache.accumulo.core.Constants;
import org.apache.accumulo.core.clientImpl.Namespace;
import org.apache.accumulo.core.conf.AccumuloConfiguration;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.data.InstanceId;
import org.apache.accumulo.core.data.NamespaceId;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.apache.accumulo.fate.zookeeper.ZooCacheFactory;
-import org.apache.accumulo.fate.zookeeper.ZooUtil;
-import org.apache.accumulo.server.MockServerContext;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class NamespaceConfigurationTest {
private static final NamespaceId NSID = NamespaceId.of("namespace");
- private static final String ZOOKEEPERS = "localhost";
- private static final int ZK_SESSION_TIMEOUT = 120000;
private InstanceId iid;
private ServerContext context;
private AccumuloConfiguration parent;
- private ZooCacheFactory zcf;
- private ZooCache zc;
- private NamespaceConfiguration c;
+ private NamespaceConfiguration nsConfig;
+
+ private PropStore propStore;
@BeforeEach
public void setUp() {
iid = InstanceId.of(UUID.randomUUID());
- context = MockServerContext.getWithZK(iid, ZOOKEEPERS, ZK_SESSION_TIMEOUT);
+ propStore = createMock(ZooPropStore.class);
+
+ propStore.registerAsListener(anyObject(), anyObject());
+ expectLastCall().anyTimes();
+
+ ZooReaderWriter zrw = createMock(ZooReaderWriter.class);
+
+ context = getMockContextWithPropStore(iid, zrw, propStore);
+
parent = createMock(AccumuloConfiguration.class);
- replay(context);
+ reset(propStore);
+
+ var nsCacheKey = NamespacePropKey.of(iid, NSID);
+ expect(propStore.get(eq(nsCacheKey))).andReturn(new VersionedProperties(123, Instant.now(),
+ Map.of(Property.INSTANCE_SECRET.getKey(), "sekrit"))).anyTimes();
+ propStore.registerAsListener(eq(nsCacheKey), anyObject());
+ expectLastCall().anyTimes();
- c = new NamespaceConfiguration(NSID, context, parent);
- zcf = createMock(ZooCacheFactory.class);
- c.setZooCacheFactory(zcf);
+ replay(propStore, context);
- zc = createMock(ZooCache.class);
- expect(zcf.getZooCache(eq(ZOOKEEPERS), eq(ZK_SESSION_TIMEOUT))).andReturn(zc);
- replay(zcf);
+ nsConfig = new NamespaceConfiguration(context, NSID, parent);
}
@Test
public void testGetters() {
- assertEquals(NSID, c.getNamespaceId());
- assertEquals(parent, c.getParent());
+ NamespaceConfiguration nsConfig = new NamespaceConfiguration(context, NSID, parent);
+ assertEquals(NSID, nsConfig.getNamespaceId());
+ assertEquals(parent, nsConfig.getParent());
}
@Test
public void testGet_InZK() {
- Property p = Property.INSTANCE_SECRET;
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZNAMESPACES + "/" + NSID
- + Constants.ZNAMESPACE_CONF + "/" + p.getKey())).andReturn("sekrit".getBytes(UTF_8));
- replay(zc);
- assertEquals("sekrit", c.get(Property.INSTANCE_SECRET));
+ nsConfig = new NamespaceConfiguration(context, NSID, parent);
+
+ assertEquals("sekrit", nsConfig.get(Property.INSTANCE_SECRET));
+
+ verify(propStore);
}
@Test
public void testGet_InParent() {
Property p = Property.INSTANCE_SECRET;
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZNAMESPACES + "/" + NSID
- + Constants.ZNAMESPACE_CONF + "/" + p.getKey())).andReturn(null);
- replay(zc);
- expect(parent.get(p)).andReturn("sekrit");
+
+ expect(parent.get(p.getKey())).andReturn("sekrit");
replay(parent);
- assertEquals("sekrit", c.get(Property.INSTANCE_SECRET));
+
+ nsConfig = new NamespaceConfiguration(context, NSID, parent);
+
+ assertEquals("sekrit", nsConfig.get(Property.INSTANCE_SECRET));
+
+ // TODO Need to check parent and accessor usage
}
@Test
public void testGet_SkipParentIfAccumuloNS() {
- c = new NamespaceConfiguration(Namespace.ACCUMULO.id(), context, parent);
- c.setZooCacheFactory(zcf);
- Property p = Property.INSTANCE_SECRET;
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZNAMESPACES + "/" + Namespace.ACCUMULO.id()
- + Constants.ZNAMESPACE_CONF + "/" + p.getKey())).andReturn(null);
- replay(zc);
- assertNull(c.get(Property.INSTANCE_SECRET));
+ reset(propStore);
+ var nsPropKey = NamespacePropKey.of(iid, Namespace.ACCUMULO.id());
+ expect(propStore.get(eq(nsPropKey))).andReturn(new VersionedProperties(Map.of("a", "b")))
+ .anyTimes();
+ propStore.registerAsListener(eq(nsPropKey), anyObject());
+ expectLastCall().anyTimes();
+ replay(propStore);
+
+ NamespaceConfiguration accumuloNsConfig =
+ new NamespaceConfiguration(context, Namespace.ACCUMULO.id(), parent);
+ assertNull(accumuloNsConfig.get(Property.INSTANCE_SECRET));
}
@Test
public void testGetProperties() {
Predicate<String> all = x -> true;
Map<String,String> props = new java.util.HashMap<>();
+ props.put("dog", "bark");
+ props.put("cat", "meow");
parent.getProperties(props, all);
+
replay(parent);
- List<String> children = new java.util.ArrayList<>();
- children.add("foo");
- children.add("ding");
- expect(zc.getChildren(
- ZooUtil.getRoot(iid) + Constants.ZNAMESPACES + "/" + NSID + Constants.ZNAMESPACE_CONF))
- .andReturn(children);
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZNAMESPACES + "/" + NSID
- + Constants.ZNAMESPACE_CONF + "/foo")).andReturn("bar".getBytes(UTF_8));
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZNAMESPACES + "/" + NSID
- + Constants.ZNAMESPACE_CONF + "/ding")).andReturn("dong".getBytes(UTF_8));
- replay(zc);
- c.getProperties(props, all);
- assertEquals(2, props.size());
+ reset(propStore);
+
+ var nsPropKey = NamespacePropKey.of(iid, NSID);
+ expect(propStore.get(eq(nsPropKey)))
+ .andReturn(
+ new VersionedProperties(123, Instant.now(), Map.of("foo", "bar", "tick", "tock")))
+ .anyTimes();
+ propStore.registerAsListener(eq(nsPropKey), anyObject());
+ expectLastCall().anyTimes();
+
+ replay(propStore);
+
+ nsConfig = new NamespaceConfiguration(context, NSID, parent);
+
+ nsConfig.getProperties(props, all);
+ assertEquals(4, props.size());
assertEquals("bar", props.get("foo"));
- assertEquals("dong", props.get("ding"));
+ assertEquals("tock", props.get("tick"));
+ assertEquals("bark", props.get("dog"));
+ assertEquals("meow", props.get("cat"));
}
@Test
public void testInvalidateCache() {
- // need to do a get so the accessor is created
- Property p = Property.INSTANCE_SECRET;
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZNAMESPACES + "/" + NSID
- + Constants.ZNAMESPACE_CONF + "/" + p.getKey())).andReturn("sekrit".getBytes(UTF_8));
- zc.clear();
- replay(zc);
- c.get(Property.INSTANCE_SECRET);
- c.invalidateCache();
- verify(zc);
+ nsConfig = new NamespaceConfiguration(context, NSID, parent);
+
+ var value = nsConfig.get(Property.INSTANCE_SECRET);
+ assertEquals("sekrit", value);
+
+ nsConfig.invalidateCache();
+ verify(propStore);
}
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/RuntimeFixedPropertiesTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/RuntimeFixedPropertiesTest.java
new file mode 100644
index 0000000000..58ff7fb154
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/RuntimeFixedPropertiesTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.accumulo.server.conf;
+
+import static org.apache.accumulo.core.conf.Property.GC_PORT;
+import static org.apache.accumulo.core.conf.Property.TSERV_NATIVEMAP_ENABLED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.core.conf.SiteConfiguration;
+import org.junit.jupiter.api.Test;
+
+public class RuntimeFixedPropertiesTest {
+
+ @Test
+ public void defaultTest() {
+ var siteConfig = SiteConfiguration.fromEnv().build();
+ Map<String,String> storedProps = new HashMap<>();
+
+ RuntimeFixedProperties fixed = new RuntimeFixedProperties(storedProps, siteConfig);
+
+ assertEquals("true", fixed.get(Property.TSERV_NATIVEMAP_ENABLED));
+ }
+
+ @Test
+ public void storedOverrideTest() {
+ var siteConfig = SiteConfiguration.fromEnv().build();
+ Map<String,String> storedProps = Map.of("tserver.memory.maps.native.enabled", "false");
+
+ RuntimeFixedProperties fixed = new RuntimeFixedProperties(storedProps, siteConfig);
+
+ assertEquals("false", fixed.get(Property.TSERV_NATIVEMAP_ENABLED));
+ }
+
+ @Test
+ public void allTest() {
+ var siteConfig = SiteConfiguration.fromEnv().build();
+ Map<String,String> storedProps = new HashMap<>();
+
+ RuntimeFixedProperties fixed = new RuntimeFixedProperties(storedProps, siteConfig);
+
+ assertEquals(Property.fixedProperties.size(), fixed.getAll().size());
+ }
+
+ @Test
+ public void changedTest() {
+ var siteConfig = SiteConfiguration.fromEnv().build();
+ Map<String,String> storedProps = Map.of(TSERV_NATIVEMAP_ENABLED.getKey(), "false");
+
+ RuntimeFixedProperties fixed = new RuntimeFixedProperties(storedProps, siteConfig);
+
+ // prop removed - changed
+ assertTrue(fixed.hasChanged(Map.of()));
+
+ // prop modified - changed
+ assertTrue(fixed.hasChanged(Map.of(TSERV_NATIVEMAP_ENABLED.getKey(), "true")));
+
+ // prop added - changed
+ assertTrue(fixed
+ .hasChanged(Map.of(TSERV_NATIVEMAP_ENABLED.getKey(), "false", GC_PORT.getKey(), "1234")));
+
+ // same - no change
+ assertFalse(fixed.hasChanged(storedProps));
+
+ }
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/ServerConfigurationFactoryTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/ServerConfigurationFactoryTest.java
index 1a7ad40410..629aa1a51d 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/ServerConfigurationFactoryTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/ServerConfigurationFactoryTest.java
@@ -22,95 +22,103 @@ import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
-import org.apache.accumulo.core.conf.AccumuloConfiguration;
+import java.util.Map;
+import java.util.UUID;
+
import org.apache.accumulo.core.conf.DefaultConfiguration;
import org.apache.accumulo.core.conf.SiteConfiguration;
import org.apache.accumulo.core.data.InstanceId;
import org.apache.accumulo.core.data.NamespaceId;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.apache.accumulo.fate.zookeeper.ZooCacheFactory;
-import org.apache.accumulo.server.MockServerContext;
+import org.apache.accumulo.core.data.TableId;
import org.apache.accumulo.server.ServerContext;
-import org.junit.jupiter.api.AfterAll;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ServerConfigurationFactoryTest {
private static final String ZK_HOST = "localhost";
- private static final int ZK_TIMEOUT = 120000;
- private static final InstanceId IID = InstanceId.of("iid");
-
- // use the same mock ZooCacheFactory and ZooCache for all tests
- private static ZooCacheFactory zcf;
- private static ZooCache zc;
- private static SiteConfiguration siteConfig = SiteConfiguration.empty().build();
-
- @BeforeAll
- public static void setUpClass() {
- zcf = createMock(ZooCacheFactory.class);
- zc = createMock(ZooCache.class);
- expect(zcf.getNewZooCache(eq(ZK_HOST), eq(ZK_TIMEOUT))).andReturn(zc).anyTimes();
- expect(zcf.getZooCache(ZK_HOST, ZK_TIMEOUT)).andReturn(zc).anyTimes();
- replay(zcf);
-
- expect(zc.getChildren(anyObject(String.class))).andReturn(null).anyTimes();
- replay(zc);
- }
-
- @AfterAll
- public static void verifyClassMocks() {
- verify(zcf, zc);
- }
+ private static final int ZK_TIMEOUT = 120_000;
+ private static final InstanceId IID = InstanceId.of(UUID.randomUUID());
+ private static final TableId TID = TableId.of("TABLE");
+ private static final NamespaceId NSID = NamespaceId.of("NAMESPACE");
+ private static final SiteConfiguration siteConfig = SiteConfiguration.empty().build();
+ private PropStore propStore;
private ServerContext context;
private ServerConfigurationFactory scf;
@BeforeEach
public void setUp() {
- context = MockServerContext.getWithZK(IID, ZK_HOST, ZK_TIMEOUT);
- replay(context);
- scf = new ServerConfigurationFactory(context, siteConfig);
- scf.setZooCacheFactory(zcf);
+ propStore = createMock(ZooPropStore.class);
+ expect(propStore.get(eq(SystemPropKey.of(IID)))).andReturn(new VersionedProperties(Map.of()))
+ .anyTimes();
+ expect(propStore.get(eq(TablePropKey.of(IID, TID))))
+ .andReturn(new VersionedProperties(Map.of())).anyTimes();
+ expect(propStore.get(eq(NamespacePropKey.of(IID, NSID))))
+ .andReturn(new VersionedProperties(Map.of())).anyTimes();
+
+ propStore.registerAsListener(anyObject(), anyObject());
+ expectLastCall().anyTimes();
+
+ context = createMock(ServerContext.class);
+ expect(context.getZooKeeperRoot()).andReturn("/accumulo/" + IID).anyTimes();
+ expect(context.getInstanceID()).andReturn(IID).anyTimes();
+ expect(context.getZooKeepers()).andReturn(ZK_HOST).anyTimes();
+ expect(context.getZooKeepersSessionTimeOut()).andReturn(ZK_TIMEOUT).anyTimes();
+ expect(context.getSiteConfiguration()).andReturn(siteConfig).anyTimes();
+ expect(context.tableNodeExists(TID)).andReturn(true).anyTimes();
+ expect(context.getPropStore()).andReturn(propStore).anyTimes();
+
+ scf = new ServerConfigurationFactory(context, siteConfig) {
+ @Override
+ public NamespaceConfiguration getNamespaceConfigurationForTable(TableId tableId) {
+ if (tableId.equals(TID)) {
+ return getNamespaceConfiguration(NSID);
+ }
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ replay(propStore, context);
}
@AfterEach
public void verifyMocks() {
- verify(context);
+ verify(propStore, context);
}
@Test
- public void testGetDefaultConfiguration() {
- DefaultConfiguration c = scf.getDefaultConfiguration();
- assertNotNull(c);
+ public void testGetters() {
+ assertSame(DefaultConfiguration.getInstance(), scf.getDefaultConfiguration());
+ assertSame(siteConfig, scf.getSiteConfiguration());
+ assertNotNull(scf.getSystemConfiguration());
}
@Test
- public void testGetSiteConfiguration() {
- var c = scf.getSiteConfiguration();
- assertNotNull(c);
+ public void testGetNamespaceConfiguration() {
+ NamespaceConfiguration namespaceConfigSingleton = scf.getNamespaceConfiguration(NSID);
+ assertEquals(NSID, namespaceConfigSingleton.getNamespaceId());
+ assertSame(namespaceConfigSingleton, scf.getNamespaceConfiguration(NSID));
}
@Test
- public void testGetConfiguration() {
- AccumuloConfiguration c = scf.getSystemConfiguration();
- assertNotNull(c);
- }
-
- private static final NamespaceId NSID = NamespaceId.of("NAMESPACE");
-
- @Test
- public void testGetNamespaceConfiguration() {
- NamespaceConfiguration c = scf.getNamespaceConfiguration(NSID);
- assertEquals(NSID, c.getNamespaceId());
- assertSame(c, scf.getNamespaceConfiguration(NSID));
+ public void testGetTableConfiguration() {
+ TableConfiguration tableConfigSingleton = scf.getTableConfiguration(TID);
+ assertEquals(TID, tableConfigSingleton.getTableId());
+ assertSame(tableConfigSingleton, scf.getTableConfiguration(TID));
}
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/SystemConfigurationTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/SystemConfigurationTest.java
new file mode 100644
index 0000000000..44ac62deae
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/SystemConfigurationTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.accumulo.server.conf;
+
+import static org.apache.accumulo.core.conf.Property.GC_PORT;
+import static org.apache.accumulo.core.conf.Property.TABLE_BLOOM_ENABLED;
+import static org.apache.accumulo.core.conf.Property.TABLE_BLOOM_SIZE;
+import static org.apache.accumulo.core.conf.Property.TABLE_DURABILITY;
+import static org.apache.accumulo.core.conf.Property.TSERV_CLIENTPORT;
+import static org.apache.accumulo.core.conf.Property.TSERV_SCAN_MAX_OPENFILES;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.accumulo.core.conf.ConfigurationCopy;
+import org.apache.accumulo.core.conf.SiteConfiguration;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class SystemConfigurationTest {
+
+ private InstanceId instanceId;
+
+ private PropStore propStore;
+
+ private SystemConfiguration sysConfig;
+
+ @BeforeEach
+ public void initMocks() {
+ instanceId = InstanceId.of(UUID.randomUUID());
+ ServerContext context = createMock(ServerContext.class);
+ expect(context.getInstanceID()).andReturn(instanceId).anyTimes();
+ propStore = createMock(ZooPropStore.class);
+ expect(context.getPropStore()).andReturn(propStore).anyTimes();
+ var siteConfig = SiteConfiguration.empty().build();
+ expect(context.getSiteConfiguration()).andReturn(siteConfig).anyTimes();
+
+ replay(context); // prop store is read from context.
+
+ // this test is ignoring listeners
+ propStore.registerAsListener(anyObject(), anyObject());
+ expectLastCall().anyTimes();
+
+ var sysPropKey = SystemPropKey.of(instanceId);
+ VersionedProperties sysProps =
+ new VersionedProperties(1, Instant.now(), Map.of(GC_PORT.getKey(), "1234",
+ TSERV_SCAN_MAX_OPENFILES.getKey(), "19", TABLE_BLOOM_ENABLED.getKey(), "true"));
+ expect(propStore.get(eq(sysPropKey))).andReturn(sysProps).times(2);
+ replay(propStore);
+
+ ConfigurationCopy defaultConfig =
+ new ConfigurationCopy(Map.of(TABLE_BLOOM_SIZE.getKey(), TABLE_BLOOM_SIZE.getDefaultValue(),
+ TABLE_DURABILITY.getKey(), TABLE_DURABILITY.getDefaultValue()));
+
+ sysConfig = new SystemConfiguration(context, sysPropKey, defaultConfig);
+ }
+
+ @Test
+ public void testFromDefault() {
+ assertEquals("true", sysConfig.get(TABLE_BLOOM_ENABLED));
+ assertTrue(sysConfig.isPropertySet(TABLE_BLOOM_ENABLED));
+ }
+
+ @Test
+ public void testFromFixed() {
+
+ var sysPropKey = SystemPropKey.of(instanceId);
+
+ assertEquals("9997", sysConfig.get(TSERV_CLIENTPORT)); // default
+ assertEquals("1234", sysConfig.get(GC_PORT)); // fixed sys config
+ assertEquals("19", sysConfig.get(TSERV_SCAN_MAX_OPENFILES)); // fixed sys config
+ assertEquals("true", sysConfig.get(TABLE_BLOOM_ENABLED)); // sys config
+ assertEquals(TABLE_BLOOM_SIZE.getDefaultValue(), sysConfig.get(TABLE_BLOOM_SIZE)); // default
+
+ assertTrue(sysConfig.isPropertySet(TSERV_CLIENTPORT)); // default
+ assertTrue(sysConfig.isPropertySet(GC_PORT)); // fixed sys config
+ assertTrue(sysConfig.isPropertySet(TSERV_SCAN_MAX_OPENFILES)); // fixed sys config
+ assertTrue(sysConfig.isPropertySet(TABLE_BLOOM_ENABLED)); // sys config
+ assertTrue(sysConfig.isPropertySet(TABLE_BLOOM_SIZE)); // default
+
+ reset(propStore);
+
+ VersionedProperties sysUpdateProps = new VersionedProperties(2, Instant.now(),
+ Map.of(GC_PORT.getKey(), "3456", TSERV_SCAN_MAX_OPENFILES.getKey(), "27",
+ TABLE_BLOOM_ENABLED.getKey(), "false", TABLE_BLOOM_SIZE.getKey(), "2048"));
+ expect(propStore.get(eq(sysPropKey))).andReturn(sysUpdateProps).anyTimes();
+ replay(propStore);
+
+ sysConfig.zkChangeEvent(sysPropKey);
+
+ assertEquals("9997", sysConfig.get(TSERV_CLIENTPORT)); // default
+ assertEquals("1234", sysConfig.get(GC_PORT)); // fixed sys config
+ assertEquals("19", sysConfig.get(TSERV_SCAN_MAX_OPENFILES)); // fixed sys config
+ assertEquals("false", sysConfig.get(TABLE_BLOOM_ENABLED)); // sys config
+ assertEquals("2048", sysConfig.get(TABLE_BLOOM_SIZE)); // default
+
+ assertTrue(sysConfig.isPropertySet(TSERV_CLIENTPORT)); // default
+ assertTrue(sysConfig.isPropertySet(GC_PORT)); // fixed sys config
+ assertTrue(sysConfig.isPropertySet(TSERV_SCAN_MAX_OPENFILES)); // fixed sys config
+ assertTrue(sysConfig.isPropertySet(TABLE_BLOOM_ENABLED)); // sys config
+ assertTrue(sysConfig.isPropertySet(TABLE_BLOOM_SIZE)); // default
+
+ }
+
+ @Test
+ public void isPropertySet() {
+ // TODO - add test
+ }
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/TableConfigurationTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/TableConfigurationTest.java
index c92b06e129..448d898a3b 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/TableConfigurationTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/TableConfigurationTest.java
@@ -18,119 +18,263 @@
*/
package org.apache.accumulo.server.conf;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.accumulo.core.conf.Property.INSTANCE_SECRET;
+import static org.apache.accumulo.core.conf.Property.TABLE_BLOOM_ENABLED;
+import static org.apache.accumulo.core.conf.Property.TABLE_BLOOM_SIZE;
+import static org.apache.accumulo.core.conf.Property.TABLE_DURABILITY;
+import static org.apache.accumulo.core.conf.Property.TABLE_FILE_MAX;
+import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
import static org.easymock.EasyMock.verify;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
-import java.util.List;
+import java.time.Instant;
import java.util.Map;
import java.util.UUID;
import java.util.function.Predicate;
-import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.conf.ConfigurationCopy;
import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.core.conf.SiteConfiguration;
import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.NamespaceId;
import org.apache.accumulo.core.data.TableId;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.apache.accumulo.fate.zookeeper.ZooCacheFactory;
-import org.apache.accumulo.fate.zookeeper.ZooUtil;
-import org.apache.accumulo.server.MockServerContext;
+import org.apache.accumulo.core.iterators.IteratorUtil;
import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class TableConfigurationTest {
- private static final TableId TID = TableId.of("table");
- private static final String ZOOKEEPERS = "localhost";
- private static final int ZK_SESSION_TIMEOUT = 120000;
- private InstanceId iid;
- private ServerContext context;
- private NamespaceConfiguration parent;
- private ZooCacheFactory zcf;
- private ZooCache zc;
- private TableConfiguration c;
+ private static final TableId TID = TableId.of("3");
+ private static final NamespaceId NID = NamespaceId.of("2");
+
+ private InstanceId instanceId;
+
+ private PropStore propStore;
+
+ private TableConfiguration tableConfig;
+ private NamespaceConfiguration nsConfig;
@BeforeEach
- public void setUp() {
- iid = InstanceId.of(UUID.randomUUID());
- context = MockServerContext.getWithZK(iid, ZOOKEEPERS, ZK_SESSION_TIMEOUT);
- replay(context);
-
- parent = createMock(NamespaceConfiguration.class);
- c = new TableConfiguration(context, TID, parent);
- zcf = createMock(ZooCacheFactory.class);
- c.setZooCacheFactory(zcf);
-
- zc = createMock(ZooCache.class);
- expect(zcf.getZooCache(eq(ZOOKEEPERS), eq(ZK_SESSION_TIMEOUT))).andReturn(zc);
- replay(zcf);
+ public void initMocks() {
+ instanceId = InstanceId.of(UUID.randomUUID());
+ ServerContext context = createMock(ServerContext.class);
+ expect(context.getInstanceID()).andReturn(instanceId).anyTimes();
+ propStore = createMock(ZooPropStore.class);
+ expect(context.getPropStore()).andReturn(propStore).anyTimes();
+ var siteConfig = SiteConfiguration.empty().build();
+ expect(context.getSiteConfiguration()).andReturn(siteConfig).anyTimes();
+
+ replay(context); // prop store is read from context.
+
+ // this test is ignoring listeners
+ propStore.registerAsListener(anyObject(), anyObject());
+ expectLastCall().anyTimes();
+
+ var sysPropKey = SystemPropKey.of(instanceId);
+ VersionedProperties sysProps =
+ new VersionedProperties(1, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "true"));
+ expect(propStore.get(eq(sysPropKey))).andReturn(sysProps).times(2);
+
+ var nsPropKey = NamespacePropKey.of(instanceId, NID);
+ VersionedProperties nsProps = new VersionedProperties(2, Instant.now(),
+ Map.of(TABLE_FILE_MAX.getKey(), "21", TABLE_BLOOM_ENABLED.getKey(), "false"));
+ expect(propStore.get(eq(nsPropKey))).andReturn(nsProps).once();
+
+ var tablePropKey = TablePropKey.of(instanceId, TID);
+ VersionedProperties tableProps =
+ new VersionedProperties(3, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "true"));
+ expect(propStore.get(eq(tablePropKey))).andReturn(tableProps).once();
+
+ ConfigurationCopy defaultConfig =
+ new ConfigurationCopy(Map.of(TABLE_BLOOM_SIZE.getKey(), TABLE_BLOOM_SIZE.getDefaultValue(),
+ TABLE_DURABILITY.getKey(), TABLE_DURABILITY.getDefaultValue()));
+
+ replay(propStore);
+
+ SystemConfiguration sysConfig = new SystemConfiguration(context, sysPropKey, defaultConfig);
+ NamespaceId nsid = nsPropKey.getId();
+ if (nsid == null) {
+ throw new IllegalStateException("missing test namespaceId");
+ }
+ nsConfig = new NamespaceConfiguration(context, nsid, sysConfig);
+
+ TableId tid = tablePropKey.getId();
+ if (tid == null) {
+ throw new IllegalStateException("missing test tableId");
+ }
+ tableConfig = new TableConfiguration(context, tid, nsConfig);
+
}
@Test
public void testGetters() {
- assertEquals(TID, c.getTableId());
- assertEquals(parent, c.getParent());
+ assertEquals(TID, tableConfig.getTableId());
+ assertEquals(nsConfig, tableConfig.getParent());
}
@Test
public void testGet_InZK() {
Property p = Property.INSTANCE_SECRET;
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZTABLES + "/" + TID + Constants.ZTABLE_CONF + "/"
- + p.getKey())).andReturn("sekrit".getBytes(UTF_8));
- replay(zc);
- assertEquals("sekrit", c.get(Property.INSTANCE_SECRET));
+ reset(propStore);
+
+ var propKey = TablePropKey.of(instanceId, TID);
+ expect(propStore.get(eq(propKey)))
+ .andReturn(new VersionedProperties(37, Instant.now(), Map.of(p.getKey(), "sekrit")))
+ .anyTimes();
+ replay(propStore);
+
+ tableConfig.zkChangeEvent(propKey);
+
+ assertEquals("sekrit", tableConfig.get(p));
+
+ verify(propStore);
}
@Test
public void testGet_InParent() {
- Property p = Property.INSTANCE_SECRET;
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZTABLES + "/" + TID + Constants.ZTABLE_CONF + "/"
- + p.getKey())).andReturn(null);
- replay(zc);
- expect(parent.get(p)).andReturn("sekrit");
- replay(parent);
- assertEquals("sekrit", c.get(Property.INSTANCE_SECRET));
+
+ String expectedPass = "aPassword1";
+
+ reset(propStore);
+ expect(propStore.get(eq(NamespacePropKey.of(instanceId, NID))))
+ .andReturn(new VersionedProperties(13, Instant.now(), Map.of(TABLE_FILE_MAX.getKey(), "123",
+ Property.INSTANCE_SECRET.getKey(), expectedPass)))
+ .anyTimes();
+ expect(propStore.get(eq(TablePropKey.of(instanceId, TID))))
+ .andReturn(new VersionedProperties(Map.of())).anyTimes();
+ replay(propStore);
+
+ nsConfig.zkChangeEvent(NamespacePropKey.of(instanceId, NID));
+
+ assertEquals("123", tableConfig.get(TABLE_FILE_MAX)); // from ns
+ assertEquals("aPassword1", tableConfig.get(INSTANCE_SECRET.getKey())); // from sys
+
+ verify(propStore);
}
@Test
public void testGetProperties() {
Predicate<String> all = x -> true;
+
+ reset(propStore);
+
+ expect(propStore.get(eq(SystemPropKey.of(instanceId))))
+ .andReturn(new VersionedProperties(1, Instant.now(), Map.of()));
+
+ expect(propStore.get(eq(NamespacePropKey.of(instanceId, NID))))
+ .andReturn(new VersionedProperties(2, Instant.now(), Map.of("dog", "bark", "cat", "meow")));
+
+ expect(propStore.get(eq(TablePropKey.of(instanceId, TID))))
+ .andReturn(new VersionedProperties(4, Instant.now(), Map.of("foo", "bar", "tick", "tock")))
+ .anyTimes();
+
+ replay(propStore);
+
Map<String,String> props = new java.util.HashMap<>();
- parent.getProperties(props, all);
- replay(parent);
- List<String> children = new java.util.ArrayList<>();
- children.add("foo");
- children.add("ding");
- expect(zc
- .getChildren(ZooUtil.getRoot(iid) + Constants.ZTABLES + "/" + TID + Constants.ZTABLE_CONF))
- .andReturn(children);
- expect(zc
- .get(ZooUtil.getRoot(iid) + Constants.ZTABLES + "/" + TID + Constants.ZTABLE_CONF + "/foo"))
- .andReturn("bar".getBytes(UTF_8));
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZTABLES + "/" + TID + Constants.ZTABLE_CONF + "/"
- + "ding")).andReturn("dong".getBytes(UTF_8));
- replay(zc);
- c.getProperties(props, all);
- assertEquals(2, props.size());
+
+ tableConfig.zkChangeEvent(TablePropKey.of(instanceId, TID));
+ nsConfig.zkChangeEvent(NamespacePropKey.of(instanceId, NID));
+
+ tableConfig.getProperties(props, all);
+
+ log.info("Props returned: {}", props);
+ assertEquals(7, props.size());
+ assertEquals(TABLE_BLOOM_SIZE.getDefaultValue(), props.get(TABLE_BLOOM_SIZE.getKey())); // system
+ assertEquals(TABLE_DURABILITY.getDefaultValue(), props.get(TABLE_DURABILITY.getKey())); // system
+ assertEquals("true", props.get(TABLE_BLOOM_ENABLED.getKey())); // system
+ assertEquals("bar", props.get("foo")); // table
+ assertEquals("tock", props.get("tick")); // table
+ assertEquals("bark", props.get("dog")); // namespace
+ assertEquals("meow", props.get("cat")); // namespace
+ }
+
+ @Test
+ public void testGetFilteredProperties() {
+ Predicate<String> filter = x -> !x.equals("filter");
+
+ reset(propStore);
+
+ expect(propStore.get(eq(SystemPropKey.of(instanceId))))
+ .andReturn(new VersionedProperties(1, Instant.now(), Map.of()));
+
+ expect(propStore.get(eq(NamespacePropKey.of(instanceId, NID))))
+ .andReturn(new VersionedProperties(2, Instant.now(),
+ Map.of("dog", "bark", "cat", "meow", "filter", "from_parent")));
+
+ expect(propStore.get(eq(TablePropKey.of(instanceId, TID)))).andReturn(new VersionedProperties(4,
+ Instant.now(), Map.of("filter", "not_returned_by_table", "foo", "bar", "tick", "tock")))
+ .anyTimes();
+
+ replay(propStore);
+
+ tableConfig.zkChangeEvent(TablePropKey.of(instanceId, TID));
+ nsConfig.zkChangeEvent(NamespacePropKey.of(instanceId, NID));
+
+ Map<String,String> props = new java.util.HashMap<>();
+
+ tableConfig.getProperties(props, filter);
+
+ log.info("Props returned: {}", props);
+ assertEquals(7, props.size());
+ assertEquals(TABLE_BLOOM_SIZE.getDefaultValue(), props.get(TABLE_BLOOM_SIZE.getKey())); // system
+ assertEquals(TABLE_DURABILITY.getDefaultValue(), props.get(TABLE_DURABILITY.getKey())); // system
+ assertEquals("true", props.get(TABLE_BLOOM_ENABLED.getKey())); // system
assertEquals("bar", props.get("foo"));
- assertEquals("dong", props.get("ding"));
+ assertEquals("tock", props.get("tick"));
+ assertEquals("bark", props.get("dog"));
+ assertEquals("meow", props.get("cat"));
+ assertNull(props.get("filter"));
}
@Test
public void testInvalidateCache() {
// need to do a get so the accessor is created
Property p = Property.INSTANCE_SECRET;
- expect(zc.get(ZooUtil.getRoot(iid) + Constants.ZTABLES + "/" + TID + Constants.ZTABLE_CONF + "/"
- + p.getKey())).andReturn("sekrit".getBytes(UTF_8));
- zc.clear();
- replay(zc);
- c.get(Property.INSTANCE_SECRET);
- c.invalidateCache();
- verify(zc);
+
+ reset(propStore);
+
+ var propKey = TablePropKey.of(instanceId, TID);
+
+ expect(propStore.get(eq(propKey)))
+ .andReturn(new VersionedProperties(23, Instant.now(), Map.of(p.getKey(), "invalid")))
+ .once();
+ expect(propStore.get(eq(propKey)))
+ .andReturn(new VersionedProperties(39, Instant.now(), Map.of(p.getKey(), "sekrit"))).once();
+
+ replay(propStore);
+
+ tableConfig.zkChangeEvent(propKey);
+ assertEquals("invalid", tableConfig.get(Property.INSTANCE_SECRET));
+
+ tableConfig.invalidateCache();
+
+ assertEquals("sekrit", tableConfig.get(Property.INSTANCE_SECRET));
+
+ verify(propStore);
+ }
+
+ private final static Logger log = LoggerFactory.getLogger(TableConfigurationTest.class);
+
+ @Test
+ public void getParsedIteratorConfig() {
+ var iterConfig = tableConfig.getParsedIteratorConfig(IteratorUtil.IteratorScope.scan);
+ assertNotNull(iterConfig);
}
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/ZooBasedConfigurationTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/ZooBasedConfigurationTest.java
new file mode 100644
index 0000000000..851df8c3f2
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/ZooBasedConfigurationTest.java
@@ -0,0 +1,347 @@
+/*
+ * 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.accumulo.server.conf;
+
+import static org.apache.accumulo.core.conf.Property.GC_PORT;
+import static org.apache.accumulo.core.conf.Property.MANAGER_BULK_RETRIES;
+import static org.apache.accumulo.core.conf.Property.TABLE_BLOOM_ENABLED;
+import static org.apache.accumulo.core.conf.Property.TABLE_BLOOM_SIZE;
+import static org.apache.accumulo.core.conf.Property.TABLE_DURABILITY;
+import static org.apache.accumulo.core.conf.Property.TABLE_SPLIT_THRESHOLD;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+import org.apache.accumulo.core.conf.AccumuloConfiguration;
+import org.apache.accumulo.core.conf.ConfigurationCopy;
+import org.apache.accumulo.core.conf.DefaultConfiguration;
+import org.apache.accumulo.core.conf.SiteConfiguration;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.NamespaceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ZooBasedConfigurationTest {
+
+ private final static Logger log = LoggerFactory.getLogger(ZooBasedConfigurationTest.class);
+
+ private InstanceId instanceId;
+ private ServerContext context;
+ private PropStore propStore;
+
+ @BeforeEach
+ public void initMocks() {
+ instanceId = InstanceId.of(UUID.randomUUID());
+ context = createMock(ServerContext.class);
+ expect(context.getInstanceID()).andReturn(instanceId).anyTimes();
+ propStore = createMock(ZooPropStore.class);
+ propStore.registerAsListener(anyObject(), anyObject());
+ expectLastCall().anyTimes();
+ expect(context.getPropStore()).andReturn(propStore).anyTimes();
+ }
+
+ @AfterEach
+ public void verifyMocks() {
+ // verify mocks common to each test
+ verify(context, propStore);
+ }
+
+ @Test
+ public void defaultInitializationTest() {
+ replay(context, propStore);
+ assertNotNull(context.getPropStore());
+ }
+
+ @Test
+ public void defaultSysConfigTest() {
+ var sysKey = SystemPropKey.of(instanceId);
+
+ var siteConfig = SiteConfiguration.empty().build();
+ expect(context.getSiteConfiguration()).andReturn(siteConfig).anyTimes();
+
+ expect(propStore.get(eq(sysKey))).andReturn(new VersionedProperties()).once(); // default empty
+ // sys props
+ replay(context, propStore);
+ assertNotNull(context.getPropStore());
+
+ ZooBasedConfiguration configuration = new SystemConfiguration(context, sysKey, siteConfig);
+ assertNotNull(configuration);
+
+ }
+
+ @Test
+ public void get() {
+ var sysPropKey = SystemPropKey.of(instanceId);
+
+ var siteConfig = SiteConfiguration.empty().build();
+ expect(context.getSiteConfiguration()).andReturn(siteConfig).anyTimes();
+
+ VersionedProperties vProps = new VersionedProperties(3, Instant.now(), Map
+ .of(TABLE_BLOOM_ENABLED.getKey(), "true", TABLE_SPLIT_THRESHOLD.getKey(), "int expected"));
+ expect(propStore.get(eq(sysPropKey))).andReturn(vProps).once();
+
+ replay(context, propStore);
+
+ ZooBasedConfiguration configuration = new SystemConfiguration(context, sysPropKey, siteConfig);
+
+ assertNotNull(configuration);
+ assertEquals("1G", configuration.get(TABLE_SPLIT_THRESHOLD));
+ assertEquals("true", configuration.get(TABLE_BLOOM_ENABLED));
+ }
+
+ @Test
+ public void getPropertiesTest() {
+ var tableId = TableId.of("t1");
+ var tablePropKey = TablePropKey.of(instanceId, tableId);
+
+ VersionedProperties tProps =
+ new VersionedProperties(Map.of(TABLE_BLOOM_ENABLED.getKey(), "true"));
+
+ var siteConfig = SiteConfiguration.empty().build();
+ // expect(context.getSiteConfiguration()).andReturn(siteConfig).anyTimes();
+
+ expect(propStore.get(eq(tablePropKey))).andReturn(tProps).once();
+ NamespaceId nsId = NamespaceId.of("n1");
+
+ VersionedProperties nProps =
+ new VersionedProperties(Map.of(TABLE_SPLIT_THRESHOLD.getKey(), "3G"));
+ expect(propStore.get(eq(NamespacePropKey.of(instanceId, nsId)))).andReturn(nProps).once();
+
+ replay(context, propStore);
+
+ NamespaceConfiguration nsConfig = new NamespaceConfiguration(context, nsId, siteConfig);
+
+ ZooBasedConfiguration zbc = new TableConfiguration(context, tableId, nsConfig);
+ Map<String,String> readProps = zbc.getSnapshot();
+
+ assertNotNull(zbc.getSnapshot());
+ assertEquals("true", readProps.get(TABLE_BLOOM_ENABLED.getKey()));
+ assertEquals("true", zbc.get(TABLE_BLOOM_ENABLED));
+
+ // read a property that is from the namespace config
+ assertEquals("3G", zbc.get(TABLE_SPLIT_THRESHOLD));
+ // TABLE_SPLIT_THRESHOLD
+
+ // read a fixed property from the system config
+ assertEquals("9998", zbc.get(GC_PORT));
+
+ // read a property from the sysconfig
+ assertEquals("3", zbc.get(MANAGER_BULK_RETRIES));
+ }
+
+ @Test
+ public void systemPropTest() {
+ var sysPropKey = SystemPropKey.of(instanceId);
+ VersionedProperties vProps =
+ new VersionedProperties(99, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "true"));
+ expect(propStore.get(eq(sysPropKey))).andReturn(vProps).once();
+
+ replay(propStore, context);
+
+ AccumuloConfiguration defaultConfig = new ConfigurationCopy(DefaultConfiguration.getInstance());
+
+ ZooBasedConfiguration sysConfig =
+ new ZooBasedConfiguration(log, context, sysPropKey, defaultConfig);
+ assertNotNull(sysConfig);
+ assertEquals("true", sysConfig.get(TABLE_BLOOM_ENABLED));
+ }
+
+ @Test
+ public void loadFailBecauseNodeNotExistTest() {
+ var sysPropKey = SystemPropKey.of(instanceId);
+
+ expect(propStore.get(eq(sysPropKey))).andThrow(new IllegalStateException("fake no node"))
+ .once();
+
+ replay(propStore, context);
+
+ var zbc =
+ new ZooBasedConfiguration(log, context, sysPropKey, DefaultConfiguration.getInstance());
+ assertThrows(IllegalStateException.class, () -> zbc.get("anyproperty"));
+ }
+
+ /**
+ * Walk configuration tree sys -> namespace -> table and test properties are retrieved .
+ */
+ @Test
+ public void tablePropTest() {
+ var sysPropKey = SystemPropKey.of(instanceId);
+ VersionedProperties sysProps =
+ new VersionedProperties(1, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "true"));
+ expect(propStore.get(eq(sysPropKey))).andReturn(sysProps).once();
+
+ var nsPropKey = NamespacePropKey.of(instanceId, NamespaceId.of("ns1"));
+ VersionedProperties nsProps =
+ new VersionedProperties(2, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "false"));
+ expect(propStore.get(eq(nsPropKey))).andReturn(nsProps).once();
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("ns1.table1"));
+ VersionedProperties tableProps =
+ new VersionedProperties(3, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "true"));
+ expect(propStore.get(eq(tablePropKey))).andReturn(tableProps).once();
+
+ replay(propStore, context);
+
+ ConfigurationCopy defaultConfig =
+ new ConfigurationCopy(Map.of(TABLE_BLOOM_SIZE.getKey(), TABLE_BLOOM_SIZE.getDefaultValue(),
+ TABLE_DURABILITY.getKey(), TABLE_DURABILITY.getDefaultValue()));
+
+ ZooBasedConfiguration sysConfig =
+ new ZooBasedConfiguration(log, context, sysPropKey, defaultConfig);
+ ZooBasedConfiguration nsConfig = new ZooBasedConfiguration(log, context, nsPropKey, sysConfig);
+ ZooBasedConfiguration tableConfig =
+ new ZooBasedConfiguration(log, context, tablePropKey, nsConfig);
+
+ assertNotNull(tableConfig);
+ assertEquals("true", sysConfig.get(TABLE_BLOOM_ENABLED));
+ assertEquals("false", nsConfig.get(TABLE_BLOOM_ENABLED));
+ assertEquals("true", tableConfig.get(TABLE_BLOOM_ENABLED));
+
+ // get from "default" - root parent config
+ assertEquals(TABLE_BLOOM_SIZE.getDefaultValue(), tableConfig.get(TABLE_BLOOM_SIZE));
+ assertEquals(TABLE_DURABILITY.getDefaultValue(), tableConfig.get(TABLE_DURABILITY.getKey()));
+
+ // test getProperties
+ Map<String,String> props = new HashMap<>();
+ Predicate<String> all = x -> true;
+ tableConfig.getProperties(props, all);
+ assertEquals("true", props.get(TABLE_BLOOM_ENABLED.getKey()));
+ assertEquals(TABLE_BLOOM_SIZE.getDefaultValue(), props.get(TABLE_BLOOM_SIZE.getKey()));
+ assertEquals(TABLE_DURABILITY.getDefaultValue(), props.get(TABLE_DURABILITY.getKey()));
+
+ // test filtered getProperties
+ props = new HashMap<>();
+ Predicate<String> blooms = x -> (x.contains("bloom"));
+ tableConfig.getProperties(props, blooms);
+ assertEquals("true", props.get(TABLE_BLOOM_ENABLED.getKey()));
+ // in parent
+ assertEquals(TABLE_BLOOM_SIZE.getDefaultValue(), props.get(TABLE_BLOOM_SIZE.getKey()));
+ // in parent - but excluded by filter
+ assertNull(props.get(TABLE_DURABILITY.getKey()));
+
+ props = new HashMap<>();
+ Predicate<String> noBlooms = x -> (!x.contains("bloom"));
+ tableConfig.getProperties(props, noBlooms);
+ // in table - excluded by filter
+ assertNull(props.get(TABLE_BLOOM_ENABLED.getKey()));
+ // in parent - excluded by filter
+ assertNull(props.get(TABLE_BLOOM_SIZE.getKey()));
+ // in parent - allowed by filter
+ assertEquals(TABLE_DURABILITY.getDefaultValue(), props.get(TABLE_DURABILITY.getKey()));
+ }
+
+ /**
+ * Walk configuration tree sys -> namespace -> table and test update count is calculated correctly
+ * if sysid node is deleted.
+ */
+ @Test
+ public void updateCountTest() {
+ var sysPropKey = SystemPropKey.of(instanceId);
+ VersionedProperties sysProps = new VersionedProperties(100, Instant.now(), Map.of());
+ expect(propStore.get(eq(sysPropKey))).andReturn(sysProps).once();
+ expect(propStore.get(eq(sysPropKey))).andThrow(new IllegalStateException("fake no node"))
+ .anyTimes();
+ // mock node deleted after event
+
+ var nsPropKey = NamespacePropKey.of(instanceId, NamespaceId.of("ns1"));
+ VersionedProperties nsProps = new VersionedProperties(20, Instant.now(), Map.of());
+ expect(propStore.get(eq(nsPropKey))).andReturn(nsProps).once();
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("ns1.table1"));
+ VersionedProperties tableProps = new VersionedProperties(3, Instant.now(), Map.of());
+ expect(propStore.get(eq(tablePropKey))).andReturn(tableProps).once();
+
+ replay(propStore, context);
+
+ ConfigurationCopy defaultConfig = new ConfigurationCopy(Map.of());
+
+ ZooBasedConfiguration sysConfig =
+ new ZooBasedConfiguration(log, context, sysPropKey, defaultConfig);
+ ZooBasedConfiguration nsConfig = new ZooBasedConfiguration(log, context, nsPropKey, sysConfig);
+ ZooBasedConfiguration tableConfig =
+ new ZooBasedConfiguration(log, context, tablePropKey, nsConfig);
+
+ assertNotNull(tableConfig);
+ assertEquals(0, defaultConfig.getUpdateCount());
+ assertEquals(100, sysConfig.getUpdateCount());
+ assertEquals(120, nsConfig.getUpdateCount());
+ assertEquals(123, tableConfig.getUpdateCount());
+
+ }
+
+ /**
+ * Walk configuration tree sys -> namespace -> table and test update count is calculated correctly
+ * if table node is deleted.
+ */
+ @Test
+ public void updateCountTableTest() {
+ var sysPropKey = SystemPropKey.of(instanceId);
+ VersionedProperties sysProps = new VersionedProperties(100, Instant.now(), Map.of());
+ expect(propStore.get(eq(sysPropKey))).andReturn(sysProps).once();
+
+ var nsPropKey = NamespacePropKey.of(instanceId, NamespaceId.of("ns1"));
+ VersionedProperties nsProps = new VersionedProperties(20, Instant.now(), Map.of());
+ expect(propStore.get(eq(nsPropKey))).andReturn(nsProps).once();
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("ns1.table1"));
+ VersionedProperties tableProps = new VersionedProperties(3, Instant.now(), Map.of());
+ expect(propStore.get(eq(tablePropKey))).andReturn(tableProps).once();
+
+ replay(propStore, context);
+
+ ConfigurationCopy defaultConfig = new ConfigurationCopy(Map.of());
+
+ ZooBasedConfiguration sysConfig =
+ new ZooBasedConfiguration(log, context, sysPropKey, defaultConfig);
+ ZooBasedConfiguration nsConfig = new ZooBasedConfiguration(log, context, nsPropKey, sysConfig);
+ ZooBasedConfiguration tableConfig =
+ new ZooBasedConfiguration(log, context, tablePropKey, nsConfig);
+
+ assertNotNull(tableConfig);
+ assertEquals(0, defaultConfig.getUpdateCount());
+ assertEquals(100, sysConfig.getUpdateCount());
+ assertEquals(120, nsConfig.getUpdateCount());
+ assertEquals(123, tableConfig.getUpdateCount());
+ }
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/ZooCachePropertyAccessorTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/ZooCachePropertyAccessorTest.java
deleted file mode 100644
index e9ef3d0183..0000000000
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/ZooCachePropertyAccessorTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * 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.accumulo.server.conf;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertSame;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Predicate;
-
-import org.apache.accumulo.core.conf.AccumuloConfiguration;
-import org.apache.accumulo.core.conf.Property;
-import org.apache.accumulo.fate.zookeeper.ZooCache;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-public class ZooCachePropertyAccessorTest {
- private static final String PATH = "/root/path/to/props";
- private static final Property PROP = Property.INSTANCE_SECRET;
- private static final String KEY = PROP.getKey();
- private static final String FULL_PATH = PATH + "/" + KEY;
- private static final String VALUE = "value";
- private static final byte[] VALUE_BYTES = VALUE.getBytes(UTF_8);
-
- private ZooCache zc;
- private ZooCachePropertyAccessor a;
-
- @BeforeEach
- public void setUp() {
- zc = createMock(ZooCache.class);
- a = new ZooCachePropertyAccessor(zc);
- }
-
- @Test
- public void testGetter() {
- assertSame(zc, a.getZooCache());
- }
-
- @Test
- public void testGet_Valid() {
- expect(zc.get(FULL_PATH)).andReturn(VALUE_BYTES);
- replay(zc);
- assertEquals(VALUE, a.get(PROP, PATH, null));
- }
-
- @Test
- public void testGet_Parent() {
- AccumuloConfiguration parent = createMock(AccumuloConfiguration.class);
- expect(parent.get(PROP)).andReturn(VALUE);
- replay(parent);
- expect(zc.get(FULL_PATH)).andReturn(null);
- replay(zc);
- assertEquals(VALUE, a.get(PROP, PATH, parent));
- }
-
- @Test
- public void testGet_Parent_Null() {
- AccumuloConfiguration parent = createMock(AccumuloConfiguration.class);
- expect(parent.get(PROP)).andReturn(null);
- replay(parent);
- expect(zc.get(FULL_PATH)).andReturn(null);
- replay(zc);
- assertNull(a.get(PROP, PATH, parent));
- }
-
- @Test
- public void testGet_Null_NoParent() {
- expect(zc.get(FULL_PATH)).andReturn(null);
- replay(zc);
- assertNull(a.get(PROP, PATH, null));
- }
-
- @Test
- public void testGet_InvalidFormat() {
- Property badProp = Property.MANAGER_CLIENTPORT;
- expect(zc.get(PATH + "/" + badProp.getKey())).andReturn(VALUE_BYTES);
- replay(zc);
- AccumuloConfiguration parent = createMock(AccumuloConfiguration.class);
- expect(parent.get(badProp)).andReturn("12345");
- replay(parent);
- assertEquals("12345", a.get(badProp, PATH, parent));
- }
-
- @Test
- public void testGetProperties() {
- Map<String,String> props = new java.util.HashMap<>();
- AccumuloConfiguration parent = createMock(AccumuloConfiguration.class);
- Predicate<String> filter = createMock(Predicate.class);
- parent.getProperties(props, filter);
- replay(parent);
- String child1 = "child1";
- String child2 = "child2";
- List<String> children = new java.util.ArrayList<>();
- children.add(child1);
- children.add(child2);
- expect(zc.getChildren(PATH)).andReturn(children);
- expect(zc.get(PATH + "/" + child1)).andReturn(VALUE_BYTES);
- expect(zc.get(PATH + "/" + child2)).andReturn(null);
- replay(zc);
- expect(filter.test(child1)).andReturn(true);
- expect(filter.test(child2)).andReturn(true);
- replay(filter);
-
- a.getProperties(props, PATH, filter, parent, null);
- assertEquals(1, props.size());
- assertEquals(VALUE, props.get(child1));
- verify(parent);
- }
-
- @Test
- public void testGetProperties_NoChildren() {
- Map<String,String> props = new java.util.HashMap<>();
- AccumuloConfiguration parent = createMock(AccumuloConfiguration.class);
- Predicate<String> filter = createMock(Predicate.class);
- parent.getProperties(props, filter);
- replay(parent);
- expect(zc.getChildren(PATH)).andReturn(null);
- replay(zc);
-
- a.getProperties(props, PATH, filter, parent, null);
- assertEquals(0, props.size());
- }
-
- @Test
- public void testGetProperties_Filter() {
- Map<String,String> props = new java.util.HashMap<>();
- AccumuloConfiguration parent = createMock(AccumuloConfiguration.class);
- Predicate<String> filter = createMock(Predicate.class);
- parent.getProperties(props, filter);
- replay(parent);
- String child1 = "child1";
- List<String> children = new java.util.ArrayList<>();
- children.add(child1);
- expect(zc.getChildren(PATH)).andReturn(children);
- replay(zc);
- expect(filter.test(child1)).andReturn(false);
- replay(filter);
-
- a.getProperties(props, PATH, filter, parent, null);
- assertEquals(0, props.size());
- }
-
- @Test
- public void testGetProperties_ParentFilter() {
- Map<String,String> props = new java.util.HashMap<>();
- AccumuloConfiguration parent = createMock(AccumuloConfiguration.class);
- Predicate<String> filter = createMock(Predicate.class);
- Predicate<String> parentFilter = createMock(Predicate.class);
- parent.getProperties(props, parentFilter);
- replay(parent);
- expect(zc.getChildren(PATH)).andReturn(null);
- replay(zc);
-
- a.getProperties(props, PATH, filter, parent, parentFilter);
- verify(parent);
- }
-
- @Test
- public void testInvalidateCache() {
- zc.clear();
- replay(zc);
- a.invalidateCache();
- verify(zc);
- }
-}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropCodecTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropCodecTest.java
index c8cac363c0..19ca358e73 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropCodecTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropCodecTest.java
@@ -53,18 +53,18 @@ public class VersionedPropCodecTest {
public void getDataVersionBadTimestamp() {
// length so that array reads do not error
byte[] bytes = new byte[100];
- assertThrows(IllegalArgumentException.class, () -> VersionedPropCodec.getDataVersion(bytes));
+ assertThrows(IllegalArgumentException.class, () -> VersionedPropCodec.readTimestamp(bytes));
}
@Test
public void goPath() throws IOException {
int aVersion = 13;
- VersionedProperties vProps =
- new VersionedProperties(aVersion, Instant.now(), Map.of("k1", "v1"));
+ var timestamp = Instant.now();
+ VersionedProperties vProps = new VersionedProperties(aVersion, timestamp, Map.of("k1", "v1"));
VersionedPropCodec codec = VersionedPropGzipCodec.codec(true);
byte[] encodedBytes = codec.toBytes(vProps);
- assertEquals(aVersion + 1, VersionedPropCodec.getDataVersion(encodedBytes));
+ assertEquals(timestamp, VersionedPropCodec.readTimestamp(encodedBytes));
}
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodec.java b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodec.java
index cb95b822f0..9448fc0b36 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodec.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodec.java
@@ -109,7 +109,7 @@ public class VersionedPropEncryptCodec extends VersionedPropCodec {
}
- Map<String,String> props = vProps.getProperties();
+ Map<String,String> props = vProps.asMap();
// encode the property map to an internal byte array.
byte[] bytes;
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodecTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodecTest.java
index 7019ac7c6d..8fdb2e0641 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodecTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropEncryptCodecTest.java
@@ -112,11 +112,12 @@ public class VersionedPropEncryptCodecTest {
log.debug("Encoded: {}", encodedBytes);
- VersionedProperties decodedProps = encoder.fromBytes(encodedBytes);
+ // store would increment version on write
+ VersionedProperties decodedProps = encoder.fromBytes(aVersion + 1, encodedBytes);
log.debug("Decoded: {}", decodedProps.print(true));
- assertEquals(vProps.getProperties(), decodedProps.getProperties());
+ assertEquals(vProps.asMap(), decodedProps.asMap());
// validate that the expected node version matches original version.
assertEquals(aVersion, vProps.getDataVersion());
@@ -125,10 +126,6 @@ public class VersionedPropEncryptCodecTest {
assertEquals(aVersion + 1, decodedProps.getDataVersion());
assertEquals(aVersion + 1, decodedProps.getDataVersion(), "encoded version should be 1 up");
- assertEquals(vProps.getNextVersion(), decodedProps.getDataVersion(),
- "version written should be the source next version");
- assertEquals(aVersion + 2, decodedProps.getNextVersion(),
- "the next version in decoded should be +2");
assertTrue(vProps.getTimestamp().compareTo(Instant.now()) <= 0,
"timestamp should be now or earlier");
@@ -163,11 +160,12 @@ public class VersionedPropEncryptCodecTest {
log.debug("len: {}, bytes: {}", encodedBytes.length, encodedBytes);
- VersionedProperties decodedProps = encoder1.fromBytes(encodedBytes);
+ // store would increment version on write
+ VersionedProperties decodedProps = encoder1.fromBytes(aVersion + 1, encodedBytes);
log.debug("Decoded: {}", decodedProps.print(true));
- assertEquals(vProps.getProperties(), decodedProps.getProperties());
+ assertEquals(vProps.asMap(), decodedProps.asMap());
// validate that the expected node version matches original version.
assertEquals(aVersion, vProps.getDataVersion());
@@ -176,10 +174,6 @@ public class VersionedPropEncryptCodecTest {
assertEquals(aVersion + 1, decodedProps.getDataVersion());
assertEquals(aVersion + 1, decodedProps.getDataVersion(), "encoded version should be 1 up");
- assertEquals(vProps.getNextVersion(), decodedProps.getDataVersion(),
- "version written should be the source next version");
- assertEquals(aVersion + 2, decodedProps.getNextVersion(),
- "the next version in decoded should be +2");
assertTrue(vProps.getTimestamp().compareTo(Instant.now()) <= 0,
"timestamp should be now or earlier");
@@ -210,17 +204,17 @@ public class VersionedPropEncryptCodecTest {
log.debug("Encoded: {}", encodedBytes1);
log.debug("Encoded: {}", encodedBytes2);
- VersionedProperties from2 = codec1.fromBytes(encodedBytes2);
- VersionedProperties from1 = codec2.fromBytes(encodedBytes1);
+ VersionedProperties from2 = codec1.fromBytes(0, encodedBytes2);
+ VersionedProperties from1 = codec2.fromBytes(0, encodedBytes1);
- assertEquals(from1.getProperties(), from2.getProperties());
+ assertEquals(from1.asMap(), from2.asMap());
VersionedPropCodec codec3 = VersionedPropEncryptCodec.codec(false,
new VersionedPropEncryptCodec.GCMCipherParams(pass, salt));
- VersionedProperties from3 = codec3.fromBytes(encodedBytes1);
+ VersionedProperties from3 = codec3.fromBytes(0, encodedBytes1);
assertEquals(from1.getDataVersion(), from3.getDataVersion());
- assertEquals(from1.getProperties(), from3.getProperties());
+ assertEquals(from1.asMap(), from3.asMap());
assertNotEquals(encodedBytes1, encodedBytes2);
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodecTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodecTest.java
index 8a9663db8f..d4089b4cc9 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodecTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropGzipCodecTest.java
@@ -44,17 +44,15 @@ public class VersionedPropGzipCodecTest {
byte[] encodedMapBytes = encoder.toBytes(vProps);
- VersionedProperties decodedProps = encoder.fromBytes(encodedMapBytes);
+ VersionedProperties decodedProps = encoder.fromBytes(0, encodedMapBytes);
- log.debug("Decoded: {}", decodedProps.getProperties());
+ log.debug("Decoded: {}", decodedProps.asMap());
// default - first write version should be 0
assertEquals(0, decodedProps.getDataVersion(), "default - first write version should be 0");
- assertEquals(1, decodedProps.getNextVersion(),
- "default - first write next version should be 1");
assertTrue(vProps.getTimestamp().compareTo(Instant.now()) <= 0,
"timestamp should be now or earlier");
- assertEquals(vProps.getProperties(), decodedProps.getProperties());
+ assertEquals(vProps.asMap(), decodedProps.asMap());
}
@Test
@@ -65,16 +63,14 @@ public class VersionedPropGzipCodecTest {
byte[] encodedMapBytes = codec.toBytes(vProps);
- VersionedProperties decodedProps = codec.fromBytes(encodedMapBytes);
+ VersionedProperties decodedProps = codec.fromBytes(0, encodedMapBytes);
- log.debug("Decoded: {}", decodedProps.getProperties());
+ log.debug("Decoded: {}", decodedProps.asMap());
assertEquals(0, decodedProps.getDataVersion(), "default - first write version should be 0");
- assertEquals(1, decodedProps.getNextVersion(),
- "default - first write next version should be 1");
assertTrue(vProps.getTimestamp().compareTo(Instant.now()) <= 0,
"timestamp should be now or earlier");
- assertEquals(vProps.getProperties(), decodedProps.getProperties());
+ assertEquals(vProps.asMap(), decodedProps.asMap());
}
/**
@@ -90,11 +86,11 @@ public class VersionedPropGzipCodecTest {
VersionedPropCodec codec = VersionedPropGzipCodec.codec(true);
byte[] encodedBytes = codec.toBytes(vProps);
- VersionedProperties decodedProps = codec.fromBytes(encodedBytes);
+ VersionedProperties decodedProps = codec.fromBytes(aVersion + 1, encodedBytes);
log.trace("Decoded: {}", decodedProps.print(true));
- assertEquals(vProps.getProperties(), decodedProps.getProperties());
+ assertEquals(vProps.asMap(), decodedProps.asMap());
// validate that the expected node version matches original version.
assertEquals(aVersion, vProps.getDataVersion());
@@ -103,10 +99,6 @@ public class VersionedPropGzipCodecTest {
assertEquals(aVersion + 1, decodedProps.getDataVersion());
assertEquals(aVersion + 1, decodedProps.getDataVersion(), "encoded version should be 1 up");
- assertEquals(vProps.getNextVersion(), decodedProps.getDataVersion(),
- "version written should be the source next version");
- assertEquals(aVersion + 2, decodedProps.getNextVersion(),
- "the next version in decoded should be +2");
assertTrue(vProps.getTimestamp().compareTo(Instant.now()) <= 0,
"timestamp should be now or earlier");
@@ -122,11 +114,11 @@ public class VersionedPropGzipCodecTest {
VersionedPropCodec codec = VersionedPropGzipCodec.codec(true);
byte[] encodedBytes = codec.toBytes(vProps);
- VersionedProperties decodedProps = codec.fromBytes(encodedBytes);
+ VersionedProperties decodedProps = codec.fromBytes(0, encodedBytes);
log.debug("Decoded: {}", decodedProps.print(true));
- assertEquals(vProps.getProperties(), decodedProps.getProperties());
+ assertEquals(vProps.asMap(), decodedProps.asMap());
}
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropertiesTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropertiesTest.java
index 739616c7e2..e3e52eec96 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropertiesTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/codec/VersionedPropertiesTest.java
@@ -47,7 +47,7 @@ public class VersionedPropertiesTest {
initProps.put("key3", "value3");
VersionedProperties vProps = new VersionedProperties(initProps);
- Map<String,String> propMap = vProps.getProperties();
+ Map<String,String> propMap = vProps.asMap();
assertEquals(initProps.size(), propMap.size());
@@ -65,9 +65,9 @@ public class VersionedPropertiesTest {
VersionedProperties vProps = new VersionedProperties();
assertNotNull(vProps);
- assertEquals(0, vProps.getProperties().size());
- assertNull(vProps.getProperties().get("key1"));
- assertEquals(Collections.emptyMap(), vProps.getProperties());
+ assertEquals(0, vProps.asMap().size());
+ assertNull(vProps.asMap().get("key1"));
+ assertEquals(Collections.emptyMap(), vProps.asMap());
}
@Test
@@ -85,7 +85,7 @@ public class VersionedPropertiesTest {
VersionedProperties vProps = new VersionedProperties(aMap);
- Map<String,String> rMap = vProps.getProperties();
+ Map<String,String> rMap = vProps.asMap();
assertEquals(aMap.size(), rMap.size());
assertThrows(UnsupportedOperationException.class, () -> rMap.put("k3", "v3"));
@@ -98,12 +98,12 @@ public class VersionedPropertiesTest {
VersionedProperties vProps = new VersionedProperties();
vProps = vProps.addOrUpdate("k1", "v1");
- assertEquals("v1", vProps.getProperties().get("k1"));
- assertEquals(1, vProps.getProperties().size());
+ assertEquals("v1", vProps.asMap().get("k1"));
+ assertEquals(1, vProps.asMap().size());
vProps = vProps.addOrUpdate("k1", "v1-2");
- assertEquals("v1-2", vProps.getProperties().get("k1"));
+ assertEquals("v1-2", vProps.asMap().get("k1"));
}
@Test
@@ -115,8 +115,8 @@ public class VersionedPropertiesTest {
VersionedProperties vProps = new VersionedProperties(aMap);
- assertEquals("v1", vProps.getProperties().get("k1"));
- assertEquals(aMap.size(), vProps.getProperties().size());
+ assertEquals("v1", vProps.asMap().get("k1"));
+ assertEquals(aMap.size(), vProps.asMap().size());
Map<String,String> bMap = new HashMap<>();
bMap.put("k1", "v1-1");
@@ -124,10 +124,10 @@ public class VersionedPropertiesTest {
VersionedProperties updated = vProps.addOrUpdate(bMap);
- assertEquals(2, vProps.getProperties().size());
- assertEquals(3, updated.getProperties().size());
+ assertEquals(2, vProps.asMap().size());
+ assertEquals(3, updated.asMap().size());
- assertEquals("v1-1", updated.getProperties().get("k1"));
+ assertEquals("v1-1", updated.asMap().get("k1"));
}
@@ -140,24 +140,21 @@ public class VersionedPropertiesTest {
VersionedProperties vProps = new VersionedProperties(aMap);
- assertEquals("v1", vProps.getProperties().get("k1"));
- assertEquals(aMap.size(), vProps.getProperties().size());
+ assertEquals("v1", vProps.asMap().get("k1"));
+ assertEquals(aMap.size(), vProps.asMap().size());
// remove 1 existing and 1 not present
VersionedProperties vProps2 = vProps.remove(Arrays.asList("k1", "k3"));
- assertEquals(1, vProps2.getProperties().size());
- assertNull(vProps2.getProperties().get("k1"));
- assertEquals("v2", vProps2.getProperties().get("k2"));
+ assertEquals(1, vProps2.asMap().size());
+ assertNull(vProps2.asMap().get("k1"));
+ assertEquals("v2", vProps2.asMap().get("k2"));
}
@Test
public void getInitialDataVersion() {
VersionedProperties vProps = new VersionedProperties();
assertEquals(0, vProps.getDataVersion());
-
- // the initial version for write should be 0
- assertEquals(0, vProps.getNextVersion(), "Initial expected version should be 0");
assertTrue(vProps.getTimestamp().compareTo(Instant.now()) <= 0,
"timestamp should be now or earlier");
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/store/PropCacheKeyTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/store/PropCacheKeyTest.java
new file mode 100644
index 0000000000..39982dc5fb
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/store/PropCacheKeyTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.accumulo.server.conf.store;
+
+import static org.apache.accumulo.core.Constants.ZCONFIG;
+import static org.apache.accumulo.core.Constants.ZNAMESPACES;
+import static org.apache.accumulo.core.Constants.ZTABLES;
+import static org.apache.accumulo.server.conf.store.PropCacheKey.PROP_NODE_NAME;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.UUID;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.NamespaceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.server.ServerContext;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PropCacheKeyTest {
+ private static final Logger log = LoggerFactory.getLogger(PropCacheKeyTest.class);
+
+ private final InstanceId instanceId = InstanceId.of(UUID.randomUUID());
+
+ @Test
+ public void systemType() {
+ var propKey = SystemPropKey.of(instanceId);
+ log.info("name: {}", propKey);
+ assertTrue(propKey.getPath().endsWith(ZCONFIG + "/" + PROP_NODE_NAME));
+ }
+
+ @Test
+ public void systemTypeFromContext() {
+ ServerContext context = createMock(ServerContext.class);
+ expect(context.getInstanceID()).andReturn(instanceId).once();
+ replay(context);
+
+ var propKey = SystemPropKey.of(context);
+ log.info("propKey: {}", propKey);
+ assertTrue(propKey.getPath().endsWith(ZCONFIG + "/" + PROP_NODE_NAME));
+ verify(context);
+ }
+
+ @Test
+ public void namespaceType() {
+ var propKey = NamespacePropKey.of(instanceId, NamespaceId.of("a"));
+ log.info("propKey: {}", propKey);
+ assertTrue(
+ propKey.getPath().endsWith(PROP_NODE_NAME) && propKey.getPath().contains(ZNAMESPACES));
+ log.info("propKey: {}", propKey);
+ }
+
+ @Test
+ public void namespaceTypeFromContext() {
+ ServerContext context = createMock(ServerContext.class);
+ expect(context.getInstanceID()).andReturn(instanceId).once();
+ replay(context);
+
+ var propKey = NamespacePropKey.of(context, NamespaceId.of("a"));
+ assertTrue(
+ propKey.getPath().endsWith(PROP_NODE_NAME) && propKey.getPath().contains(ZNAMESPACES));
+ verify(context);
+ }
+
+ @Test
+ public void tableType() {
+ var propKey = TablePropKey.of(instanceId, TableId.of("a"));
+ log.info("propKey: {}", propKey);
+ assertTrue(propKey.getPath().endsWith(PROP_NODE_NAME) && propKey.getPath().contains(ZTABLES));
+ log.info("propKey: {}", propKey);
+ }
+
+ @Test
+ public void fromPathTest() {
+
+ var iid = "3f9976c6-3bf1-41ab-9751-1b0a9be3551d";
+
+ PropCacheKey<?> t1 =
+ PropCacheKey.fromPath("/accumulo/" + iid + "/tables/t1/conf/encoded_props");
+ assertNotNull(t1);
+ assertEquals(TableId.of("t1"), t1.getId());
+
+ PropCacheKey<?> n1 =
+ PropCacheKey.fromPath("/accumulo/" + iid + "/namespaces/n1/conf/encoded_props");
+ assertNotNull(n1);
+ assertEquals(NamespaceId.of("n1"), n1.getId());
+ assertNotNull(n1.getId());
+
+ PropCacheKey<?> s1 = PropCacheKey.fromPath("/accumulo/" + iid + "/config/encoded_props");
+ assertNotNull(s1);
+ // system config returns instance id as id placeholder
+ assertEquals(iid, s1.getId().canonical());
+ }
+
+ @Test
+ public void getBasePathTest() {
+ assertTrue(SystemPropKey.of(instanceId).getBasePath().endsWith("/config"));
+ assertTrue(
+ NamespacePropKey.of(instanceId, NamespaceId.of("123")).getBasePath().endsWith("/conf"));
+ assertTrue(TablePropKey.of(instanceId, TableId.of("456")).getBasePath().endsWith("/conf"));
+ }
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/PropCacheCaffeineImplTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/PropCacheCaffeineImplTest.java
new file mode 100644
index 0000000000..d6a766e7d3
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/PropCacheCaffeineImplTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static org.apache.accumulo.core.conf.Property.TABLE_BULK_MAX_TABLETS;
+import static org.apache.accumulo.core.conf.Property.TABLE_FILE_BLOCK_SIZE;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.core.metrics.MetricsUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.github.benmanes.caffeine.cache.Ticker;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+public class PropCacheCaffeineImplTest {
+
+ private InstanceId instanceId;
+ private ServerContext context;
+ private PropStoreWatcher propStoreWatcher;
+ private ZooPropLoader zooPropLoader;
+ private TestTicker ticker;
+
+ private TablePropKey tablePropKey;
+ private VersionedProperties vProps;
+
+ private PropCacheCaffeineImpl cache = null;
+
+ @BeforeEach
+ @SuppressFBWarnings(value = "PREDICTABLE_RANDOM",
+ justification = "random used for testing with variable names")
+ public void init() {
+ ticker = new TestTicker();
+ instanceId = InstanceId.of(UUID.randomUUID());
+
+ PropStoreMetrics cacheMetrics = new PropStoreMetrics();
+ MetricsUtil.initializeProducers(cacheMetrics);
+
+ tablePropKey =
+ TablePropKey.of(instanceId, TableId.of("t" + ThreadLocalRandom.current().nextInt(1, 1000)));
+
+ Map<String,String> props =
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "1234", TABLE_FILE_BLOCK_SIZE.getKey(), "512M");
+
+ vProps = new VersionedProperties(props);
+
+ // mocks
+ context = createMock(ServerContext.class);
+ propStoreWatcher = createMock(PropStoreWatcher.class);
+ zooPropLoader = createMock(ZooPropLoader.class);
+
+ expect(context.getInstanceID()).andReturn(instanceId).anyTimes();
+
+ cache =
+ new PropCacheCaffeineImpl.Builder(zooPropLoader, cacheMetrics).withTicker(ticker).build();
+
+ }
+
+ @AfterEach
+ public void verifyMocks() {
+ verify(context, propStoreWatcher, zooPropLoader);
+ }
+
+ @Test
+ public void getTest() {
+ expect(zooPropLoader.load(eq(tablePropKey))).andReturn(vProps).once();
+ replay(context, propStoreWatcher, zooPropLoader);
+ assertNotNull(cache.get(tablePropKey)); // will call load
+ assertNotNull(cache.get(tablePropKey)); // will read from cache
+ }
+
+ /**
+ * Verifies that value is not cached
+ */
+ @SuppressFBWarnings(value = "PREDICTABLE_RANDOM",
+ justification = "random used for testing with variable names")
+ @Test
+ public void getNoCacheTest() {
+ var table2PropKey = TablePropKey.of(instanceId,
+ TableId.of("t2" + ThreadLocalRandom.current().nextInt(1, 1000)));
+
+ expect(zooPropLoader.load(eq(table2PropKey))).andReturn(vProps).once();
+
+ replay(context, propStoreWatcher, zooPropLoader);
+ var props = cache.getWithoutCaching(tablePropKey);
+ assertNull(props);
+
+ assertNotNull(cache.get(table2PropKey)); // load into cache
+ assertNotNull(cache.getWithoutCaching(table2PropKey)); // read from cache - no load call.
+ }
+
+ @Test
+ public void removeTest() {
+ expect(zooPropLoader.load(eq(tablePropKey))).andReturn(vProps).times(2);
+ replay(context, propStoreWatcher, zooPropLoader);
+ assertNotNull(cache.get(tablePropKey)); // will call load
+ cache.remove(tablePropKey);
+ assertNotNull(cache.get(tablePropKey)); // will call load again
+ }
+
+ @SuppressFBWarnings(value = "PREDICTABLE_RANDOM",
+ justification = "random used for testing with variable names")
+ @Test
+ public void removeAllTest() {
+ var table2PropKey = TablePropKey.of(instanceId,
+ TableId.of("t2" + ThreadLocalRandom.current().nextInt(1, 1000)));
+
+ expect(zooPropLoader.load(eq(tablePropKey))).andReturn(vProps).once();
+ expect(zooPropLoader.load(eq(table2PropKey))).andReturn(vProps).once();
+
+ replay(context, propStoreWatcher, zooPropLoader);
+
+ assertNotNull(cache.get(tablePropKey)); // will call load
+ assertNotNull(cache.get(table2PropKey)); // will call load
+
+ cache.removeAll();
+
+ // check that values are not in cache - will not call load
+ assertNull(cache.getWithoutCaching(tablePropKey));
+ assertNull(cache.getWithoutCaching(table2PropKey));
+ }
+
+ VersionedProperties asyncProps() {
+ return vProps;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Test
+ public void refreshTest() throws Exception {
+
+ expect(zooPropLoader.load(eq(tablePropKey))).andReturn(vProps).once();
+
+ CompletableFuture future = CompletableFuture.supplyAsync(this::asyncProps);
+
+ expect(zooPropLoader.asyncReload(eq(tablePropKey), eq(vProps), anyObject())).andReturn(future)
+ .once();
+
+ replay(context, propStoreWatcher, zooPropLoader);
+ assertNotNull(cache.get(tablePropKey)); // will call load and place into cache
+
+ ticker.advance(30, TimeUnit.MINUTES);
+ cache.cleanUp();
+ assertNotNull(cache.get(tablePropKey)); // will async check stat and then reload
+ }
+
+ @SuppressWarnings({"rawtypes"})
+ @Test
+ public void expireTest() {
+ expect(zooPropLoader.load(eq(tablePropKey))).andReturn(vProps).times(2);
+
+ replay(context, propStoreWatcher, zooPropLoader);
+ assertNotNull(cache.get(tablePropKey)); // will call load
+
+ ticker.advance(90, TimeUnit.MINUTES);
+ cache.cleanUp();
+ assertNotNull(cache.get(tablePropKey)); // expired - will call load.
+ }
+
+ @Test
+ public void getExceptionTest() {
+ expect(zooPropLoader.load(eq(tablePropKey)))
+ .andThrow(new IllegalStateException("forced test exception")).once();
+ replay(context, propStoreWatcher, zooPropLoader);
+ assertNull(cache.get(tablePropKey)); // will call load
+ }
+
+ public static class TestTicker implements Ticker {
+
+ private final long startTime;
+ private long elapsed;
+
+ public TestTicker() {
+ startTime = System.nanoTime();
+ elapsed = 0L;
+ }
+
+ public void advance(final long value, final TimeUnit units) {
+ elapsed += TimeUnit.NANOSECONDS.convert(value, units);
+ }
+
+ @Override
+ public long read() {
+ return startTime + elapsed;
+ }
+ }
+
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/PropStoreEventTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/PropStoreEventTest.java
new file mode 100644
index 0000000000..59d28c3c77
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/PropStoreEventTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static org.apache.accumulo.core.conf.Property.TABLE_BULK_MAX_TABLETS;
+import static org.apache.accumulo.core.conf.Property.TABLE_FILE_BLOCK_SIZE;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.newCapture;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.core.metrics.MetricsUtil;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.PropChangeListener;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.zookeeper.WatchedEvent;
+import org.apache.zookeeper.Watcher;
+import org.apache.zookeeper.data.Stat;
+import org.easymock.Capture;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class PropStoreEventTest {
+ private final VersionedPropCodec propCodec = VersionedPropCodec.getDefault();
+ private InstanceId instanceId;
+
+ // mocks
+ private ServerContext context;
+ private ZooReaderWriter zrw;
+ private ReadyMonitor readyMonitor;
+
+ @BeforeEach
+ public void initCommonMocks() throws Exception {
+ instanceId = InstanceId.of(UUID.randomUUID());
+ context = createMock(ServerContext.class);
+ zrw = createMock(ZooReaderWriter.class);
+ expect(context.getZooReaderWriter()).andReturn(zrw).anyTimes();
+ expect(context.getZooKeepersSessionTimeOut()).andReturn(500).anyTimes();
+ expect(context.getInstanceID()).andReturn(instanceId).anyTimes();
+
+ expect(zrw.exists(eq("/accumulo/" + instanceId), anyObject())).andReturn(true).anyTimes();
+
+ readyMonitor = createMock(ReadyMonitor.class);
+ }
+
+ @AfterEach
+ public void verifyMocks() {
+ verify(context, zrw, readyMonitor);
+ }
+
+ @Test
+ public void zkChangeEventTest() throws Exception {
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("a1"));
+
+ PropStoreWatcher watcher = new PropStoreWatcher(readyMonitor);
+
+ WatchedEvent zkEvent = createMock(WatchedEvent.class);
+ expect(zkEvent.getPath()).andReturn(tablePropKey.getPath()).once();
+ expect(zkEvent.getType()).andReturn(Watcher.Event.EventType.NodeDataChanged);
+ readyMonitor.setReady();
+ expectLastCall().once();
+
+ replay(context, zrw, readyMonitor, zkEvent);
+
+ PropStore propStore = new ZooPropStore(instanceId, zrw, readyMonitor, watcher, null);
+ StoreTestListener listener = new StoreTestListener();
+
+ propStore.registerAsListener(tablePropKey, listener);
+
+ watcher.process(zkEvent);
+
+ Thread.sleep(150);
+
+ assertEquals(1, listener.getZkChangeEventCount());
+
+ verify(zkEvent);
+ }
+
+ @Test
+ public void deleteEventTest() throws Exception {
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("a1"));
+
+ PropStoreWatcher watcher = new PropStoreWatcher(readyMonitor);
+
+ WatchedEvent zkEvent = createMock(WatchedEvent.class);
+ expect(zkEvent.getPath()).andReturn(tablePropKey.getPath()).once();
+ expect(zkEvent.getType()).andReturn(Watcher.Event.EventType.NodeDeleted);
+
+ readyMonitor.setReady();
+ expectLastCall().once();
+
+ replay(context, zrw, readyMonitor, zkEvent);
+
+ PropStore propStore = new ZooPropStore(instanceId, zrw, readyMonitor, watcher, null);
+
+ StoreTestListener listener = new StoreTestListener();
+
+ propStore.registerAsListener(tablePropKey, listener);
+
+ watcher.process(zkEvent);
+
+ Thread.sleep(150);
+ assertEquals(1, listener.getDeleteEventCount());
+ verify(zkEvent);
+
+ }
+
+ @Test
+ public void disconnectEventTest() throws Exception {
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("a1"));
+
+ PropStoreWatcher watcher = new PropStoreWatcher(readyMonitor);
+
+ WatchedEvent zkEvent = createMock(WatchedEvent.class);
+ expect(zkEvent.getType()).andReturn(Watcher.Event.EventType.None);
+ expect(zkEvent.getState()).andReturn(Watcher.Event.KeeperState.Disconnected).once();
+
+ readyMonitor.setReady();
+ expectLastCall().once();
+ readyMonitor.clearReady();
+ expectLastCall();
+
+ replay(context, zrw, readyMonitor, zkEvent);
+
+ PropStore propStore = new ZooPropStore(instanceId, zrw, readyMonitor, watcher, null);
+
+ StoreTestListener listener = new StoreTestListener();
+
+ propStore.registerAsListener(tablePropKey, listener);
+
+ watcher.process(zkEvent);
+
+ Thread.sleep(150);
+ assertEquals(1, listener.getConnectionEventCount());
+ verify(zkEvent);
+
+ }
+
+ @Test
+ public void closedEventTest() throws Exception {
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("a1"));
+
+ PropStoreWatcher watcher = new PropStoreWatcher(readyMonitor);
+
+ WatchedEvent zkEvent = createMock(WatchedEvent.class);
+ expect(zkEvent.getType()).andReturn(Watcher.Event.EventType.None);
+ expect(zkEvent.getState()).andReturn(Watcher.Event.KeeperState.Closed).once();
+
+ readyMonitor.setReady();
+ expectLastCall();
+ readyMonitor.clearReady();
+ expectLastCall();
+ readyMonitor.setClosed();
+ expectLastCall();
+
+ replay(context, zrw, readyMonitor, zkEvent);
+
+ PropStore propStore = new ZooPropStore(instanceId, zrw, readyMonitor, watcher, null);
+
+ StoreTestListener listener = new StoreTestListener();
+
+ propStore.registerAsListener(tablePropKey, listener);
+
+ watcher.process(zkEvent);
+
+ Thread.sleep(150);
+ assertEquals(1, listener.getConnectionEventCount());
+ verify(zkEvent);
+ }
+
+ @Test
+ public void cacheChangeEventTest() throws Exception {
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("a1"));
+
+ PropStoreWatcher watcher = new PropStoreWatcher(readyMonitor);
+ readyMonitor.setReady();
+ expectLastCall().once();
+
+ replay(context, zrw, readyMonitor);
+
+ PropStore propStore = new ZooPropStore(instanceId, zrw, readyMonitor, watcher, null);
+
+ StoreTestListener listener = new StoreTestListener();
+
+ propStore.registerAsListener(tablePropKey, listener);
+
+ watcher.signalCacheChangeEvent(tablePropKey);
+
+ Thread.sleep(150);
+ assertEquals(1, listener.getCacheChangeEventCount());
+ }
+
+ @Test
+ public void validateWatcherSetTest() throws Exception {
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("a1"));
+
+ Map<String,String> props1 =
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "1234", TABLE_FILE_BLOCK_SIZE.getKey(), "512M");
+
+ VersionedProperties vProps = new VersionedProperties(11, Instant.now(), props1);
+ Capture<Stat> stat = newCapture();
+
+ // first call loads cache
+ expect(zrw.getData(eq(tablePropKey.getPath()), anyObject(), capture(stat))).andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setVersion(12);
+ stat.setValue(s);
+ return propCodec.toBytes(vProps);
+ }).once();
+
+ PropStoreWatcher watcher = new PropStoreWatcher(readyMonitor);
+
+ replay(context, zrw, readyMonitor);
+
+ PropStoreMetrics metrics = new PropStoreMetrics();
+ MetricsUtil.initializeProducers(metrics);
+
+ ZooPropLoader loader = new ZooPropLoader(zrw, propCodec, watcher, metrics);
+
+ PropCacheCaffeineImpl cache = new PropCacheCaffeineImpl.Builder(loader, metrics).build();
+
+ // load cache
+ var read1 = cache.get(tablePropKey);
+ assertNotNull(read1);
+ assertEquals("1234", read1.asMap().get(TABLE_BULK_MAX_TABLETS.getKey()));
+ assertEquals("512M", read1.asMap().get(TABLE_FILE_BLOCK_SIZE.getKey()));
+
+ watcher.process(
+ new WatchedEvent(Watcher.Event.EventType.NodeDataChanged, null, tablePropKey.getPath()));
+
+ }
+
+ private static class StoreTestListener implements PropChangeListener {
+
+ private int zkChangeEventCount = 0;
+ private int cacheChangeEventCount = 0;
+ private int deleteEventCount = 0;
+ private int connectionEventCount = 0;
+
+ @Override
+ public void zkChangeEvent(PropCacheKey<?> propCacheKey) {
+ zkChangeEventCount++;
+ }
+
+ @Override
+ public void cacheChangeEvent(PropCacheKey<?> propCacheKey) {
+ cacheChangeEventCount++;
+ }
+
+ @Override
+ public void deleteEvent(PropCacheKey<?> propCacheKey) {
+ deleteEventCount++;
+ }
+
+ @Override
+ public void connectionEvent() {
+ connectionEventCount++;
+ }
+
+ public int getZkChangeEventCount() {
+ return zkChangeEventCount;
+ }
+
+ public int getCacheChangeEventCount() {
+ return cacheChangeEventCount;
+ }
+
+ public int getDeleteEventCount() {
+ return deleteEventCount;
+ }
+
+ public int getConnectionEventCount() {
+ return connectionEventCount;
+ }
+ }
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ReadyMonitorTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ReadyMonitorTest.java
new file mode 100644
index 0000000000..e6d941121b
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ReadyMonitorTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+
+import org.apache.accumulo.core.util.threads.ThreadPools;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ReadyMonitorTest {
+
+ private static final Logger log = LoggerFactory.getLogger(ReadyMonitorTest.class);
+
+ private final int numWorkerThreads = 16;
+ private CountDownLatch readyToRunLatch = null;
+ private CountDownLatch completedLatch = null;
+
+ private ThreadPoolExecutor workerPool = null;
+
+ @BeforeEach
+ public void init() {
+
+ readyToRunLatch = new CountDownLatch(numWorkerThreads);
+ completedLatch = new CountDownLatch(numWorkerThreads);
+
+ // these tests wait for workers to signal ready using count down latch.
+ // size pool so some threads are likely to wait on others to complete.
+ int numPoolThreads = numWorkerThreads / 2;
+ workerPool = ThreadPools.getServerThreadPools().createFixedThreadPool(numPoolThreads,
+ "readyMonitor-test-pool", false);
+ }
+
+ @AfterEach
+ public void teardown() {
+ workerPool.shutdownNow();
+ try {
+ boolean terminated = workerPool.awaitTermination(2000, MILLISECONDS);
+ log.trace("Worked pool successfully terminated: {}", terminated);
+ } catch (InterruptedException ex) {
+ // don't care.
+ workerPool.shutdownNow();
+ }
+ }
+
+ @Test
+ public void isReadyST() {
+ ReadyMonitor readyMonitor = new ReadyMonitor("test", SECONDS.toMillis(1));
+ assertFalse(readyMonitor.test());
+
+ readyMonitor.setReady();
+ assertTrue(readyMonitor.test());
+
+ readyMonitor.isReady();
+ }
+
+ @Test
+ public void clearTest() {
+
+ ReadyMonitor readyMonitor = new ReadyMonitor("test", SECONDS.toMillis(1));
+ assertFalse(readyMonitor.test());
+
+ readyMonitor.setReady();
+ assertTrue(readyMonitor.test());
+
+ readyMonitor.clearReady();
+ assertFalse(readyMonitor.test());
+
+ assertThrows(IllegalStateException.class, readyMonitor::isReady);
+ }
+
+ @Test
+ public void notReady() {
+ ReadyMonitor readyMonitor = new ReadyMonitor("test", SECONDS.toMillis(1));
+ assertFalse(readyMonitor.test());
+
+ assertThrows(IllegalStateException.class, readyMonitor::isReady);
+ }
+
+ @Test
+ public void isReadyMultiThread() throws Exception {
+ final long readyTestTimeout = SECONDS.toMillis(20);
+ ReadyMonitor readyMonitor = new ReadyMonitor("test", readyTestTimeout);
+ assertFalse(readyMonitor.test());
+
+ log.debug("start latch - {}", readyToRunLatch.getCount());
+
+ List<Future<Long>> tasks = new ArrayList<>();
+ for (int i = 0; i < numWorkerThreads; i++) {
+ ReadyTask r = new ReadyTask(readyMonitor, readyToRunLatch, completedLatch);
+ tasks.add(workerPool.submit(r));
+ }
+
+ Thread.sleep(1_000);
+
+ var allReady = readyToRunLatch.await(10_000, MILLISECONDS);
+ var readyCount = readyToRunLatch.getCount();
+ log.trace("All ready: {}, have {}", allReady, readyCount);
+
+ assertTrue(readyCount > 1,
+ "failed - all worker tasks did not report ready - have: " + readyCount);
+
+ readyMonitor.setReady();
+
+ var allComplete = completedLatch.await(20_000, MILLISECONDS);
+ assertTrue(allComplete,
+ "failed - all expected tasks did not complete - count=" + completedLatch.getCount());
+
+ tasks.forEach(f -> {
+ try {
+ var timeWaiting = f.get();
+ log.debug("waiting: {}", NANOSECONDS.toSeconds(timeWaiting));
+ assertTrue(timeWaiting < MILLISECONDS.toNanos(readyTestTimeout));
+ } catch (ExecutionException | InterruptedException ex) {
+ log.warn("Task failed", ex);
+ fail("Task failed with exception - " + ex.getMessage());
+ }
+ });
+ }
+
+ private static class ReadyTask implements Callable<Long> {
+
+ private final ReadyMonitor readyMonitor;
+ private final CountDownLatch readyToRunLatch;
+ private final CountDownLatch finishedLatch;
+
+ public ReadyTask(final ReadyMonitor readyMonitor, final CountDownLatch readyToRunLatch,
+ final CountDownLatch finishedLatch) {
+ this.readyMonitor = readyMonitor;
+ this.readyToRunLatch = readyToRunLatch;
+ this.finishedLatch = finishedLatch;
+ }
+
+ @Override
+ public Long call() throws Exception {
+ // signal ready to run
+ readyToRunLatch.countDown();
+ // time waiting for isReady to complete++
+ long start = System.nanoTime();
+ readyMonitor.isReady();
+ finishedLatch.countDown();
+
+ // returning nanoseconds.
+ return System.nanoTime() - start;
+ }
+ }
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ZooPropLoaderTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ZooPropLoaderTest.java
new file mode 100644
index 0000000000..84a809bd1c
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ZooPropLoaderTest.java
@@ -0,0 +1,665 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static org.apache.accumulo.core.conf.Property.GC_PORT;
+import static org.apache.accumulo.core.conf.Property.MANAGER_CLIENTPORT;
+import static org.apache.accumulo.core.conf.Property.TSERV_CLIENTPORT;
+import static org.apache.accumulo.core.conf.Property.TSERV_NATIVEMAP_ENABLED;
+import static org.apache.accumulo.core.conf.Property.TSERV_SCAN_MAX_OPENFILES;
+import static org.apache.accumulo.server.conf.store.impl.PropCacheCaffeineImpl.REFRESH_MIN;
+import static org.easymock.EasyMock.anyLong;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.newCapture;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.easymock.Capture;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ZooPropLoaderTest {
+
+ private static final Logger log = LoggerFactory.getLogger(ZooPropLoaderTest.class);
+
+ private PropCacheCaffeineImplTest.TestTicker ticker;
+ private InstanceId instanceId;
+ private ServerContext context;
+ private PropCacheKey<?> propCacheKey;
+ private VersionedPropCodec propCodec;
+
+ // mocks
+ private PropStoreMetrics cacheMetrics;
+ private PropStoreWatcher propStoreWatcher;
+ private ZooReaderWriter zrw;
+
+ private ZooPropLoader loader;
+
+ @BeforeEach
+ public void initCommonMocks() {
+ ticker = new PropCacheCaffeineImplTest.TestTicker();
+ instanceId = InstanceId.of(UUID.randomUUID());
+
+ propCacheKey = TablePropKey.of(instanceId, TableId.of("1abc"));
+ propCodec = VersionedPropCodec.getDefault();
+
+ // mocks
+ context = createMock(ServerContext.class);
+ expect(context.getInstanceID()).andReturn(instanceId).anyTimes();
+
+ zrw = createMock(ZooReaderWriter.class);
+
+ cacheMetrics = createMock(PropStoreMetrics.class);
+
+ propStoreWatcher = createMock(PropStoreWatcher.class);
+
+ // loader used in tests
+ loader = new ZooPropLoader(zrw, propCodec, propStoreWatcher, cacheMetrics);
+
+ }
+
+ @AfterEach
+ public void verifyCommonMocks() {
+ verify(context, zrw, propStoreWatcher, cacheMetrics);
+ }
+
+ @Test
+ public void loadTest() throws Exception {
+
+ VersionedProperties defaultProps = new VersionedProperties();
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).anyTimes();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(1);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ assertNotNull(loader.load(propCacheKey));
+ }
+
+ // from cache loader
+
+ /**
+ * Verify that first call loads from ZooKeeper, then second call returns from the cache.
+ *
+ * @throws Exception
+ * any exception is a test failure.
+ */
+ @Test
+ public void loadAndCacheTest() throws Exception {
+
+ VersionedProperties defaultProps = new VersionedProperties();
+
+ expect(zrw.getStatus(propCacheKey.getPath())).andThrow(new KeeperException.NoNodeException())
+ .anyTimes();
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).once();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(1);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // load into cache
+ assertNotNull(cache.get(propCacheKey));
+
+ // read cached entry - load count should not change.
+ ticker.advance(1, TimeUnit.MINUTES);
+ assertNotNull(cache.get(propCacheKey));
+ }
+
+ @Test
+ public void getExpireTimeoutTest() {
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+ // TODO check into implementing an explicit test.
+ // fail("Implement test");
+ }
+
+ /**
+ * Verify that an exception on load result in null value and that the exception does not escape
+ * the load call.
+ * <p>
+ * throws Exception any exception is a test failure.
+ */
+ @Test
+ public void loadFailTest() throws Exception {
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), anyObject()))
+ .andThrow(new KeeperException.NoNodeException("force no node exception")).once();
+
+ propStoreWatcher.signalZkChangeEvent(eq(propCacheKey));
+ expectLastCall();
+
+ cacheMetrics.incrZkError();
+ expectLastCall().once();
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ assertNull(cache.get(propCacheKey));
+
+ log.debug("Metrics: {}", cacheMetrics);
+ }
+
+ /**
+ * Validate that cache expiration functions as expected.
+ * <p>
+ * throws Exception any exception is a test failure.
+ */
+ @Test
+ public void expireTest() throws Exception {
+
+ VersionedProperties defaultProps = new VersionedProperties();
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).times(2);
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(2);
+
+ cacheMetrics.incrEviction();
+ expectLastCall().once();
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // load cache
+ assertNotNull(cache.get(propCacheKey));
+
+ ticker.advance(70, TimeUnit.MINUTES);
+ cache.cleanUp();
+
+ assertNotNull(cache.get(propCacheKey));
+
+ }
+
+ /**
+ * Test that a ZooKeeper exception on an async reload task is correctly handed and that the value
+ * is removed from the cache when the refresh fails and the next get.
+ *
+ * @throws Exception
+ * if a test error occurs.
+ */
+ @Test
+ public void reloadExceptionTest() throws Exception {
+
+ final VersionedProperties defaultProps = new VersionedProperties();
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).once();
+
+ Stat stat = new Stat();
+ stat.setVersion(123); // set different version so reload triggered
+ expect(zrw.getStatus(propCacheKey.getPath())).andReturn(stat).once();
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), anyObject()))
+ .andThrow(new KeeperException.NoNodeException("forced no node")).anyTimes();
+
+ propStoreWatcher.signalZkChangeEvent(anyObject());
+ expectLastCall().anyTimes();
+
+ propStoreWatcher.signalCacheChangeEvent(anyObject());
+ expectLastCall().anyTimes();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(1);
+ cacheMetrics.incrRefresh();
+ expectLastCall().times(1);
+ cacheMetrics.incrRefreshLoad();
+ expectLastCall().times(1);
+ cacheMetrics.incrZkError();
+ expectLastCall().times(2);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // prime cache
+ assertNotNull(cache.get(propCacheKey));
+
+ ticker.advance(5, TimeUnit.MINUTES);
+ cache.cleanUp();
+
+ // read cached value
+ assertNotNull(cache.get(propCacheKey));
+
+ // advance so refresh called.
+ ticker.advance(20, TimeUnit.MINUTES);
+ cache.cleanUp();
+
+ assertNotNull(cache.get(propCacheKey));
+
+ // yield so async thread completes.
+ Thread.sleep(250);
+
+ assertNull(cache.get(propCacheKey));
+ }
+
+ @Test
+ public void getWithoutCachingTest() {
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ assertNull(cache.getWithoutCaching(propCacheKey));
+
+ }
+
+ @Test
+ public void removeTest() throws Exception {
+ final var sysPropKey = SystemPropKey.of(instanceId);
+ final var tablePropKey = TablePropKey.of(instanceId, TableId.of("t1"));
+
+ VersionedProperties defaultProps = new VersionedProperties();
+
+ expect(zrw.getData(eq(sysPropKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).once();
+ expect(zrw.getData(eq(tablePropKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).once();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(2);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // load into cache
+ assertNotNull(cache.get(sysPropKey));
+ assertNotNull(cache.get(tablePropKey));
+
+ cache.remove(tablePropKey);
+ cache.cleanUp();
+
+ // verify retrieved from cache without loading.
+ assertNotNull(cache.getWithoutCaching(sysPropKey));
+ assertNull(cache.getWithoutCaching(tablePropKey));
+ }
+
+ @Test
+ public void removeAllTest() throws Exception {
+ final var sysPropKey = SystemPropKey.of(instanceId);
+ final var tablePropKey = TablePropKey.of(instanceId, TableId.of("t1"));
+
+ VersionedProperties defaultProps = new VersionedProperties();
+
+ expect(zrw.getData(eq(sysPropKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).once();
+ expect(zrw.getData(eq(tablePropKey.getPath()), anyObject(), anyObject()))
+ .andReturn(propCodec.toBytes(defaultProps)).once();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(2);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // load into cache
+ assertNotNull(cache.get(sysPropKey));
+ assertNotNull(cache.get(tablePropKey));
+
+ cache.removeAll();
+ cache.cleanUp();
+
+ // verify retrieved from cache without loading.
+ assertNull(cache.getWithoutCaching(sysPropKey));
+ assertNull(cache.getWithoutCaching(tablePropKey));
+ }
+
+ @Test
+ public void getWithoutCachingNotPresentTest() {
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // load into cache
+ assertNull(cache.getWithoutCaching(propCacheKey));
+ }
+
+ @Test
+ public void refreshTest() throws Exception {
+
+ VersionedProperties defaultProps = new VersionedProperties();
+
+ // first call loads cache
+ Capture<Stat> stat = newCapture();
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), capture(stat))).andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setCzxid(1234);
+ s.setVersion(0);
+ stat.setValue(s);
+ return propCodec.toBytes(defaultProps);
+ }).times(1);
+
+ Stat expectedStat = new Stat();
+ expectedStat.setVersion(0);
+ expect(zrw.getStatus(propCacheKey.getPath())).andReturn(expectedStat).times(2);
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(1);
+ cacheMetrics.incrRefresh();
+ expectLastCall().times(2);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // load cache
+ log.debug("received: {}", cache.get(propCacheKey));
+
+ ticker.advance(REFRESH_MIN + 1, TimeUnit.MINUTES);
+
+ assertNotNull(cache.get(propCacheKey));
+
+ ticker.advance(REFRESH_MIN / 2, TimeUnit.MINUTES);
+
+ assertNotNull(cache.get(propCacheKey));
+
+ Thread.sleep(100);
+
+ ticker.advance(REFRESH_MIN + 1, TimeUnit.MINUTES);
+
+ assertNotNull(cache.get(propCacheKey));
+
+ Thread.sleep(100);
+
+ ticker.advance(1, TimeUnit.MINUTES);
+
+ assertNotNull(cache.get(propCacheKey));
+
+ }
+
+ /**
+ * Test that when the refreshAfterWrite period expires that the data version is checked against
+ * stored value - and on mismatch, rereads the values from ZooKeeper.
+ */
+ @Test
+ public void refreshDifferentVersionTest() throws Exception {
+
+ final int initialVersion = 123;
+ Capture<PropStoreWatcher> propStoreWatcherCapture = newCapture();
+
+ Capture<Stat> stat = newCapture();
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), capture(propStoreWatcherCapture), capture(stat)))
+ .andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setVersion(initialVersion + 1);
+ stat.setValue(s);
+ return propCodec.toBytes(new VersionedProperties(initialVersion + 1, Instant.now(),
+ Map.of(Property.TABLE_SPLIT_THRESHOLD.getKey(), "7G")));
+ }).once();
+
+ // make it look like version on ZK has advanced.
+ Stat stat2 = new Stat();
+ stat2.setVersion(initialVersion + 3); // initSysProps 123, on write 124
+ expect(zrw.getStatus(propCacheKey.getPath())).andReturn(stat2).once();
+
+ Capture<Stat> stat3 = newCapture();
+
+ expect(
+ zrw.getData(eq(propCacheKey.getPath()), capture(propStoreWatcherCapture), capture(stat3)))
+ .andAnswer(() -> {
+ Stat s = stat3.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setVersion(initialVersion + 4);
+ stat3.setValue(s);
+ return propCodec.toBytes(new VersionedProperties(initialVersion + 3, Instant.now(),
+ Map.of(Property.TABLE_SPLIT_THRESHOLD.getKey(), "12G")));
+ }).once();
+
+ propStoreWatcher.signalCacheChangeEvent(eq(propCacheKey));
+ expectLastCall();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(2);
+
+ cacheMetrics.incrRefresh();
+ expectLastCall().times(1);
+
+ cacheMetrics.incrRefreshLoad();
+ expectLastCall().times(1);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // prime cache
+ assertNotNull(cache.get(propCacheKey));
+
+ ticker.advance(REFRESH_MIN + 1, TimeUnit.MINUTES);
+ // first call after refresh return original and schedules update
+ var originalProps = cache.get(propCacheKey);
+ assertNotNull(originalProps);
+ assertNotNull(originalProps.asMap().get(Property.TABLE_SPLIT_THRESHOLD.getKey()));
+
+ // allow refresh thread to run
+ Thread.sleep(50);
+
+ // refresh should have loaded updated value;
+ var updatedProps = cache.get(propCacheKey);
+ log.debug("Updated props: {}", updatedProps == null ? "null" : updatedProps.print(true));
+
+ assertNotNull(updatedProps);
+ Thread.sleep(250);
+
+ assertEquals("12G", updatedProps.asMap().get(Property.TABLE_SPLIT_THRESHOLD.getKey()));
+ }
+
+ /**
+ * Test that when the refreshAfterWrite period expires that the data version is checked against
+ * stored value - and on match, returns the current value without rereading the values from
+ * ZooKeeper.
+ *
+ * @throws Exception
+ * any exception is a test failure
+ */
+ @Test
+ public void refreshSameVersionTest() throws Exception {
+
+ final int expectedVersion = 123;
+
+ VersionedProperties mockProps = createMock(VersionedProperties.class);
+ expect(mockProps.getTimestamp()).andReturn(Instant.now()).once();
+ expect(mockProps.asMap()).andReturn(Map.of());
+
+ Capture<Stat> stat = newCapture();
+
+ // first call loads cache
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), capture(stat))).andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setVersion(expectedVersion);
+ stat.setValue(s);
+ return propCodec.toBytes(mockProps);
+ }).times(1);
+
+ Stat stat2 = new Stat();
+ stat2.setCtime(System.currentTimeMillis());
+ stat2.setMtime(System.currentTimeMillis());
+ stat2.setVersion(expectedVersion);
+
+ expect(zrw.getStatus(propCacheKey.getPath())).andReturn(stat2).once();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(1);
+ cacheMetrics.incrRefresh();
+ expectLastCall().times(1);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics, mockProps);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // prime cache
+ cache.get(propCacheKey);
+
+ ticker.advance(30, TimeUnit.MINUTES);
+ cache.cleanUp();
+
+ VersionedProperties vPropsRead = cache.get(propCacheKey);
+
+ assertNotNull(vPropsRead);
+
+ Thread.sleep(250);
+ cache.get(propCacheKey);
+
+ verify(mockProps);
+ }
+
+ /**
+ * reload exception - exception thrown reading Stat to check version.
+ *
+ * @throws Exception
+ * any exception is a test failure.
+ */
+ @Test
+ public void refreshExceptionTest() throws Exception {
+ VersionedProperties defaultProps = new VersionedProperties();
+
+ // first call loads cache
+ Capture<Stat> stat = newCapture();
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), capture(stat))).andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setCzxid(1234);
+ s.setVersion(0);
+ stat.setValue(s);
+ return propCodec.toBytes(defaultProps);
+ }).times(1);
+
+ Stat expectedStat = new Stat();
+ expectedStat.setVersion(0);
+ expect(zrw.getStatus(propCacheKey.getPath()))
+ .andThrow(new KeeperException.NoNodeException("force no node exception")).once();
+
+ propStoreWatcher.signalZkChangeEvent(eq(propCacheKey));
+ expectLastCall().anyTimes();
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(), anyObject()))
+ .andThrow(new KeeperException.NoNodeException("force no node exception")).once();
+
+ cacheMetrics.addLoadTime(anyLong());
+ expectLastCall().times(1);
+ cacheMetrics.incrRefresh();
+ expectLastCall().times(1);
+ cacheMetrics.incrZkError();
+ expectLastCall().times(2);
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(loader, cacheMetrics).withTicker(ticker).build();
+
+ // load cache
+ log.debug("received: {}", cache.get(propCacheKey));
+
+ ticker.advance(REFRESH_MIN + 1, TimeUnit.MINUTES);
+
+ assertNotNull(cache.get(propCacheKey)); // returns current and queues async refresh
+
+ Thread.sleep(150);
+ assertNull(cache.get(propCacheKey)); // on exception, the loader should return null
+
+ }
+
+ @Test
+ public void captureExampleTest() throws Exception {
+
+ Map<String,String> props = new HashMap<>();
+ props.put(TSERV_CLIENTPORT.getKey(), "1234");
+ props.put(TSERV_NATIVEMAP_ENABLED.getKey(), "false");
+ props.put(TSERV_SCAN_MAX_OPENFILES.getKey(), "2345");
+ props.put(MANAGER_CLIENTPORT.getKey(), "3456");
+ props.put(GC_PORT.getKey(), "4567");
+ VersionedProperties vProps = new VersionedProperties(8, Instant.now(), props);
+
+ Capture<String> path = newCapture();
+ Capture<Stat> stat = newCapture();
+
+ expect(zrw.getData(capture(path), anyObject(), capture(stat))).andAnswer(() -> {
+ Stat r = stat.getValue();
+ r.setCzxid(1234);
+ r.setVersion(9);
+ stat.setValue(r);
+ return propCodec.toBytes(vProps);
+ }).anyTimes();
+
+ replay(context, zrw, propStoreWatcher, cacheMetrics);
+
+ Stat statCheck = new Stat();
+ statCheck.setVersion(9);
+
+ }
+
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ZooPropStoreTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ZooPropStoreTest.java
new file mode 100644
index 0000000000..a7dad16ffa
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/store/impl/ZooPropStoreTest.java
@@ -0,0 +1,403 @@
+/*
+ * 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.accumulo.server.conf.store.impl;
+
+import static org.apache.accumulo.core.conf.Property.TABLE_BULK_MAX_TABLETS;
+import static org.apache.accumulo.core.conf.Property.TABLE_FILE_BLOCK_SIZE;
+import static org.apache.accumulo.core.conf.Property.TABLE_SPLIT_THRESHOLD;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.newCapture;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.data.Stat;
+import org.easymock.Capture;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class ZooPropStoreTest {
+
+ private final VersionedPropCodec propCodec = VersionedPropCodec.getDefault();
+ private InstanceId instanceId;
+
+ // mocks
+ private ServerContext context;
+ private ZooReaderWriter zrw;
+
+ @BeforeEach
+ public void init() throws Exception {
+ instanceId = InstanceId.of(UUID.randomUUID());
+ context = createMock(ServerContext.class);
+ zrw = createMock(ZooReaderWriter.class);
+ expect(context.getZooReaderWriter()).andReturn(zrw).anyTimes();
+ expect(zrw.getSessionTimeout()).andReturn(2_000).anyTimes();
+ expect(context.getInstanceID()).andReturn(instanceId).anyTimes();
+
+ expect(zrw.exists(eq("/accumulo/" + instanceId), anyObject())).andReturn(true).anyTimes();
+ }
+
+ @AfterEach
+ public void verifyMock() {
+ verify(context, zrw);
+ }
+
+ @Test
+ public void create() throws Exception {
+
+ var propCacheKey = TablePropKey.of(instanceId, TableId.of("propCacheKey"));
+
+ Capture<byte[]> bytes = newCapture();
+ expect(zrw.putPersistentData(eq(propCacheKey.getPath()), capture(bytes), anyObject()))
+ .andReturn(true).once();
+
+ replay(context, zrw);
+
+ PropStore propStore = ZooPropStore.initialize(instanceId, zrw);
+
+ propStore.create(propCacheKey,
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "1234", TABLE_FILE_BLOCK_SIZE.getKey(), "512M"));
+
+ var decoded = propCodec.fromBytes(0, bytes.getValue());
+ assertNotNull(decoded);
+ assertEquals(0, decoded.getDataVersion());
+ assertEquals("1234", decoded.asMap().get(TABLE_BULK_MAX_TABLETS.getKey()));
+ assertEquals("512M", decoded.asMap().get(TABLE_FILE_BLOCK_SIZE.getKey()));
+ }
+
+ /**
+ * Simple get tests load from ZooKeeper and then retrieved from cache.
+ *
+ * @throws Exception
+ * any exception is a test error.
+ */
+ @Test
+ public void getTest() throws Exception {
+
+ var propCacheKey = TablePropKey.of(instanceId, TableId.of("propCacheKey"));
+
+ var vProps = new VersionedProperties(Map.of(Property.TABLE_BLOOM_ENABLED.getKey(), "true"));
+
+ // expect one ZooKeeper call - subsequent calls should load from cache.
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(PropStoreWatcher.class), anyObject()))
+ .andReturn(VersionedPropCodec.getDefault().toBytes(vProps)).once();
+
+ replay(context, zrw);
+
+ PropStore propStore = ZooPropStore.initialize(instanceId, zrw);
+
+ assertNotNull(propStore.get(propCacheKey)); // first call will fetch from ZooKeeper
+ assertNotNull(propStore.get(propCacheKey)); // next call will fetch from cache.
+
+ var p = propStore.get(propCacheKey);
+
+ assertEquals("true", p.asMap().get(Property.TABLE_BLOOM_ENABLED.getKey()));
+ }
+
+ @Test
+ public void versionTest() throws Exception {
+
+ var propCacheKey = TablePropKey.of(instanceId, TableId.of("table1"));
+ Map<String,String> props =
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "1234", TABLE_FILE_BLOCK_SIZE.getKey(), "512M");
+
+ Capture<PropStoreWatcher> propStoreWatcherCapture = newCapture();
+ Capture<Stat> stat = newCapture();
+
+ // force version mismatch between zk and props.
+ var expectedVersion = 99;
+ expect(zrw.getData(eq(propCacheKey.getPath()), capture(propStoreWatcherCapture), capture(stat)))
+ .andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setVersion(expectedVersion);
+ stat.setValue(s);
+ return propCodec.toBytes(new VersionedProperties(props));
+ }).once();
+
+ replay(context, zrw);
+
+ PropStore propStore = ZooPropStore.initialize(instanceId, zrw);
+ var vProps = propStore.get(propCacheKey);
+ assertNotNull(vProps);
+ assertEquals(expectedVersion, vProps.getDataVersion());
+ }
+
+ /**
+ * Test that putAll will add and also overwrite properties.
+ *
+ * @throws Exception
+ * any exception is a test error.
+ */
+ @Test
+ public void putAllTest() throws Exception {
+
+ var propCacheKey = TablePropKey.of(instanceId, TableId.of("table1"));
+
+ var initialProps = new VersionedProperties(0, Instant.now(),
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "1234", TABLE_FILE_BLOCK_SIZE.getKey(), "512M"));
+
+ // not cached - will load from ZooKeeper
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(Stat.class)))
+ .andReturn(propCodec.toBytes(initialProps)).once();
+
+ Capture<byte[]> bytes = newCapture();
+ expect(zrw.overwritePersistentData(eq(propCacheKey.getPath()), capture(bytes), eq(0)))
+ .andAnswer(() -> {
+ var stored = propCodec.fromBytes(0, bytes.getValue());
+ assertEquals(3, stored.asMap().size());
+ // overwritten
+ assertEquals("4321", stored.asMap().get(TABLE_BULK_MAX_TABLETS.getKey()));
+ // unchanged
+ assertEquals("512M", stored.asMap().get(TABLE_FILE_BLOCK_SIZE.getKey()));
+ // new
+ assertEquals("123M", stored.asMap().get(TABLE_SPLIT_THRESHOLD.getKey()));
+ return true;
+ }).once();
+
+ replay(context, zrw);
+
+ PropStore propStore = ZooPropStore.initialize(instanceId, zrw);
+
+ Map<String,String> updateProps =
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "4321", TABLE_SPLIT_THRESHOLD.getKey(), "123M");
+
+ propStore.putAll(propCacheKey, updateProps);
+
+ verify(zrw);
+ }
+
+ @Test
+ public void removeTest() throws Exception {
+
+ var propCacheKey = TablePropKey.of(instanceId, TableId.of("table1"));
+
+ var initialProps = new VersionedProperties(123, Instant.now(),
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "1234", TABLE_FILE_BLOCK_SIZE.getKey(), "512M"));
+
+ // not cached - will load from ZooKeeper
+ Capture<Stat> stat = newCapture();
+
+ expect(zrw.getData(eq(propCacheKey.getPath()), capture(stat))).andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setVersion(123);
+ stat.setValue(s);
+ return propCodec.toBytes(initialProps);
+ }).once();
+
+ Capture<byte[]> bytes = newCapture();
+ expect(zrw.overwritePersistentData(eq(propCacheKey.getPath()), capture(bytes), eq(123)))
+ .andAnswer(() -> {
+ var stored = propCodec.fromBytes(124, bytes.getValue());
+ assertEquals(1, stored.asMap().size());
+ // deleted
+ assertNull(stored.asMap().get(TABLE_BULK_MAX_TABLETS.getKey()));
+ // unchanged
+ assertEquals("512M", stored.asMap().get(TABLE_FILE_BLOCK_SIZE.getKey()));
+ // never existed
+ assertNull(stored.asMap().get(TABLE_SPLIT_THRESHOLD.getKey()));
+ return true;
+ }).once();
+
+ replay(context, zrw);
+
+ PropStore propStore = ZooPropStore.initialize(instanceId, zrw);
+
+ Set<String> deleteNames =
+ Set.of(TABLE_BULK_MAX_TABLETS.getKey(), TABLE_SPLIT_THRESHOLD.getKey());
+
+ propStore.removeProperties(propCacheKey, deleteNames);
+ verify(zrw);
+ }
+
+ @Test
+ public void removeWithExceptionsTest() throws Exception {
+
+ var propCacheKey = TablePropKey.of(instanceId, TableId.of("table1"));
+
+ // return "bad data"
+ Capture<Stat> stat = newCapture();
+ expect(zrw.getData(eq(propCacheKey.getPath()), capture(stat))).andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setCzxid(1234);
+ s.setVersion(19);
+ stat.setValue(s);
+ return new byte[100];
+ }).once();
+
+ // mock throwing exceptions
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(Stat.class)))
+ .andThrow(new KeeperException.NoNodeException("mock forced no node")).once();
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(Stat.class)))
+ .andThrow(new InterruptedException("mock forced interrupt exception")).once();
+
+ replay(context, zrw);
+
+ PropStore propStore = ZooPropStore.initialize(instanceId, zrw);
+
+ Set<String> deleteNames =
+ Set.of(TABLE_BULK_MAX_TABLETS.getKey(), TABLE_SPLIT_THRESHOLD.getKey());
+
+ // Parse error converted to IllegalStateException
+ assertThrows(IllegalStateException.class,
+ () -> propStore.removeProperties(propCacheKey, deleteNames));
+ // ZK exception converted to IllegalStateException
+ assertThrows(IllegalStateException.class,
+ () -> propStore.removeProperties(propCacheKey, deleteNames));
+ // InterruptException converted to IllegalStateException
+ assertThrows(IllegalStateException.class,
+ () -> propStore.removeProperties(propCacheKey, deleteNames));
+ }
+
+ @Test
+ public void validateWatcherSetTest() throws Exception {
+
+ var tablePropKey = TablePropKey.of(instanceId, TableId.of("table1"));
+ Map<String,String> props =
+ Map.of(TABLE_BULK_MAX_TABLETS.getKey(), "1234", TABLE_FILE_BLOCK_SIZE.getKey(), "512M");
+
+ Capture<PropStoreWatcher> propStoreWatcherCapture = newCapture();
+ Capture<Stat> stat = newCapture();
+
+ // first call loads cache
+ expect(zrw.getData(eq(tablePropKey.getPath()), capture(propStoreWatcherCapture), capture(stat)))
+ .andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setVersion(13);
+ stat.setValue(s);
+ return propCodec.toBytes(new VersionedProperties(12, Instant.now(), props));
+ }).once();
+
+ // watcher change (re)loads cache on next get call.
+ expect(zrw.getData(eq(tablePropKey.getPath()), capture(propStoreWatcherCapture), capture(stat)))
+ .andAnswer(() -> {
+ Stat s = stat.getValue();
+ s.setCtime(System.currentTimeMillis());
+ s.setMtime(System.currentTimeMillis());
+ s.setVersion(14);
+ stat.setValue(s);
+ return propCodec.toBytes(new VersionedProperties(13, Instant.now(), props));
+ }).once();
+
+ replay(context, zrw);
+
+ ReadyMonitor monitor = new TestReadyMonitor("testmon", 2000);
+ PropStoreWatcher watcher = new TestWatcher(monitor);
+
+ ZooPropStore propStore = new ZooPropStore(instanceId, zrw, monitor, watcher, null);
+
+ assertNotNull(propStore.get(tablePropKey));
+
+ // second call should return from cache - not zookeeper.
+ assertNotNull(propStore.get(tablePropKey));
+ PropStoreWatcher capturedMonitor = propStoreWatcherCapture.getValue();
+ capturedMonitor.signalZkChangeEvent(tablePropKey);
+ Thread.sleep(150);
+
+ assertNotNull(propStore.get(tablePropKey));
+
+ }
+
+ private static class TestReadyMonitor extends ReadyMonitor {
+
+ /**
+ * Create an instance of a ready monitor.
+ *
+ * @param resourceName
+ * the resource name guarded by this monitor (used by logging)
+ * @param timeout
+ * the max time in milliseconds this will block waiting.
+ */
+ public TestReadyMonitor(String resourceName, long timeout) {
+ super(resourceName, timeout);
+ setReady();
+ }
+ }
+
+ private static class TestWatcher extends PropStoreWatcher {
+ public TestWatcher(ReadyMonitor zkReadyMonitor) {
+ super(zkReadyMonitor);
+ }
+ }
+
+ /**
+ * Verify that a node is created when it does not exist.
+ */
+ // @Test
+ public void getNoNodeTest() {
+ // TODO - implementation requires mocking transform locking
+ }
+
+ @Test
+ public void deleteTest() throws Exception {
+
+ var propCacheKey = TablePropKey.of(instanceId, TableId.of("propCacheKey"));
+
+ var vProps = new VersionedProperties(Map.of(Property.TABLE_BLOOM_ENABLED.getKey(), "true"));
+
+ // expect first call to load cache.
+ expect(zrw.getData(eq(propCacheKey.getPath()), anyObject(PropStoreWatcher.class), anyObject()))
+ .andReturn(VersionedPropCodec.getDefault().toBytes(vProps)).once();
+
+ zrw.delete(eq(propCacheKey.getPath()));
+ expectLastCall().once();
+
+ replay(context, zrw);
+
+ PropStore propStore = ZooPropStore.initialize(instanceId, zrw);
+
+ assertNotNull(propStore.get(propCacheKey)); // first call will fetch from ZooKeeper
+ assertNotNull(propStore.get(propCacheKey)); // next call will fetch from cache.
+ var p = propStore.get(propCacheKey);
+
+ assertEquals("true", p.asMap().get(Property.TABLE_BLOOM_ENABLED.getKey()));
+
+ propStore.delete(propCacheKey);
+ Thread.sleep(50);
+ }
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/conf/util/PropSnapshotTest.java b/server/base/src/test/java/org/apache/accumulo/server/conf/util/PropSnapshotTest.java
new file mode 100644
index 0000000000..8017a38901
--- /dev/null
+++ b/server/base/src/test/java/org/apache/accumulo/server/conf/util/PropSnapshotTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.accumulo.server.conf.util;
+
+import static org.apache.accumulo.core.conf.Property.TABLE_BLOOM_ENABLED;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.SystemPropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class PropSnapshotTest {
+
+ private InstanceId instanceId;
+ private PropStore propStore;
+
+ @BeforeEach
+ public void init() {
+ instanceId = InstanceId.of(UUID.randomUUID());
+ propStore = createMock(ZooPropStore.class);
+ propStore.registerAsListener(anyObject(), anyObject());
+ expectLastCall().anyTimes();
+ }
+
+ @AfterEach
+ public void verifyMocks() {
+ verify(propStore);
+ }
+
+ @Test
+ public void getTest() {
+ // init props
+ expect(propStore.get(eq(SystemPropKey.of(instanceId))))
+ .andReturn(new VersionedProperties(123, Instant.now(), Map.of("k1", "v1", "k2", "v2")))
+ .once();
+ // after update
+ expect(propStore.get(eq(SystemPropKey.of(instanceId))))
+ .andReturn(new VersionedProperties(124, Instant.now(), Map.of("k3", "v3"))).once();
+
+ replay(propStore);
+ PropSnapshot snapshot = PropSnapshot.create(SystemPropKey.of(instanceId), propStore);
+
+ assertEquals("v1", snapshot.getVersionedProperties().asMap().get("k1"));
+ assertEquals("v2", snapshot.getVersionedProperties().asMap().get("k2"));
+ assertNull(snapshot.getVersionedProperties().asMap().get("k3"));
+
+ snapshot.requireUpdate();
+
+ assertEquals("v3", snapshot.getVersionedProperties().asMap().get("k3"));
+ assertNull(snapshot.getVersionedProperties().asMap().get("k1"));
+ assertNull(snapshot.getVersionedProperties().asMap().get("k2"));
+ }
+
+ @Test
+ public void eventChangeTest() {
+
+ var sysPropKey = SystemPropKey.of(instanceId);
+
+ expect(propStore.get(eq(sysPropKey))).andReturn(
+ new VersionedProperties(99, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "true")))
+ .once();
+
+ expect(propStore.get(eq(sysPropKey))).andReturn(
+ new VersionedProperties(100, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "false")))
+ .once();
+
+ replay(propStore);
+
+ PropSnapshot snapshot = PropSnapshot.create(sysPropKey, propStore);
+
+ assertEquals("true",
+ snapshot.getVersionedProperties().asMap().get(TABLE_BLOOM_ENABLED.getKey()));
+ snapshot.zkChangeEvent(sysPropKey);
+ assertEquals("false",
+ snapshot.getVersionedProperties().asMap().get(TABLE_BLOOM_ENABLED.getKey()));
+ }
+
+ @Test
+ public void deleteEventTest() {
+
+ var sysPropKey = SystemPropKey.of(instanceId);
+
+ expect(propStore.get(eq(sysPropKey))).andReturn(
+ new VersionedProperties(123, Instant.now(), Map.of(TABLE_BLOOM_ENABLED.getKey(), "true")))
+ .once();
+
+ expect(propStore.get(eq(sysPropKey))).andThrow(new IllegalStateException("Fake node delete"))
+ .once();
+
+ replay(propStore);
+ PropSnapshot snapshot = PropSnapshot.create(sysPropKey, propStore);
+
+ assertEquals("true",
+ snapshot.getVersionedProperties().asMap().get(TABLE_BLOOM_ENABLED.getKey()));
+ snapshot.deleteEvent(sysPropKey);
+ assertThrows(IllegalStateException.class, () -> snapshot.getVersionedProperties());
+ }
+
+}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/master/balancer/BaseHostRegexTableLoadBalancerTest.java b/server/base/src/test/java/org/apache/accumulo/server/master/balancer/BaseHostRegexTableLoadBalancerTest.java
index cf767c54c5..c58ac3bffd 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/master/balancer/BaseHostRegexTableLoadBalancerTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/master/balancer/BaseHostRegexTableLoadBalancerTest.java
@@ -18,7 +18,12 @@
*/
package org.apache.accumulo.server.master.balancer;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
import java.net.UnknownHostException;
import java.util.ArrayList;
@@ -30,6 +35,7 @@ import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
+import java.util.UUID;
import java.util.function.Predicate;
import org.apache.accumulo.core.Constants;
@@ -43,6 +49,7 @@ import org.apache.accumulo.core.conf.DefaultConfiguration;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.conf.SiteConfiguration;
import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.NamespaceId;
import org.apache.accumulo.core.data.TableId;
import org.apache.accumulo.core.dataImpl.KeyExtent;
import org.apache.accumulo.core.master.thrift.TableInfo;
@@ -53,6 +60,11 @@ import org.apache.accumulo.server.ServerContext;
import org.apache.accumulo.server.conf.NamespaceConfiguration;
import org.apache.accumulo.server.conf.ServerConfigurationFactory;
import org.apache.accumulo.server.conf.TableConfiguration;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.NamespacePropKey;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
import org.apache.hadoop.io.Text;
import org.easymock.EasyMock;
@@ -114,7 +126,7 @@ public abstract class BaseHostRegexTableLoadBalancerTest extends HostRegexTableL
public TableConfiguration getTableConfiguration(final TableId tableId) {
// create a dummy namespaceConfiguration to satisfy requireNonNull in TableConfiguration
// constructor
- NamespaceConfiguration dummyConf = new NamespaceConfiguration(Namespace.DEFAULT.id(), context,
+ NamespaceConfiguration dummyConf = new NamespaceConfiguration(context, Namespace.DEFAULT.id(),
DefaultConfiguration.getInstance());
return new TableConfiguration(context, tableId, dummyConf) {
@Override
@@ -158,13 +170,34 @@ public abstract class BaseHostRegexTableLoadBalancerTest extends HostRegexTableL
}
protected ServerContext createMockContext() {
- ServerContext mockContext = EasyMock.createMock(ServerContext.class);
+ InstanceId instanceId = InstanceId.of(UUID.randomUUID());
+
+ ServerContext mockContext = createMock(ServerContext.class);
+ PropStore propStore = createMock(ZooPropStore.class);
expect(mockContext.getProperties()).andReturn(new Properties()).anyTimes();
expect(mockContext.getZooKeepers()).andReturn("").anyTimes();
expect(mockContext.getInstanceName()).andReturn("test").anyTimes();
expect(mockContext.getZooKeepersSessionTimeOut()).andReturn(30).anyTimes();
- expect(mockContext.getInstanceID()).andReturn(InstanceId.of("1111")).anyTimes();
+ expect(mockContext.getInstanceID()).andReturn(instanceId).anyTimes();
expect(mockContext.getZooKeeperRoot()).andReturn(Constants.ZROOT + "/1111").anyTimes();
+
+ expect(mockContext.getPropStore()).andReturn(propStore).anyTimes();
+ propStore.registerAsListener(anyObject(), anyObject());
+ expectLastCall().anyTimes();
+
+ expect(propStore.get(eq(NamespacePropKey.of(instanceId, NamespaceId.of("+default")))))
+ .andReturn(new VersionedProperties()).anyTimes();
+
+ expect(propStore.get(eq(TablePropKey.of(instanceId, TableId.of("1")))))
+ .andReturn(new VersionedProperties()).anyTimes();
+
+ expect(propStore.get(eq(TablePropKey.of(instanceId, TableId.of("2")))))
+ .andReturn(new VersionedProperties()).anyTimes();
+
+ expect(propStore.get(eq(TablePropKey.of(instanceId, TableId.of("3")))))
+ .andReturn(new VersionedProperties()).anyTimes();
+
+ replay(propStore);
return mockContext;
}
diff --git a/server/base/src/test/java/org/apache/accumulo/server/security/UserImpersonationTest.java b/server/base/src/test/java/org/apache/accumulo/server/security/UserImpersonationTest.java
index fad93761ef..f35ea8f1d6 100644
--- a/server/base/src/test/java/org/apache/accumulo/server/security/UserImpersonationTest.java
+++ b/server/base/src/test/java/org/apache/accumulo/server/security/UserImpersonationTest.java
@@ -64,6 +64,11 @@ public class UserImpersonationTest {
public void getProperties(Map<String,String> props, Predicate<String> filter) {
cc.getProperties(props, filter);
}
+
+ @Override
+ public long getUpdateCount() {
+ return 0;
+ }
};
}
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/ManagerClientServiceHandler.java b/server/manager/src/main/java/org/apache/accumulo/manager/ManagerClientServiceHandler.java
index 1e2e5886de..020b7bc6e7 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/ManagerClientServiceHandler.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/ManagerClientServiceHandler.java
@@ -30,6 +30,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -83,13 +84,10 @@ import org.apache.accumulo.server.client.ClientServiceHandler;
import org.apache.accumulo.server.manager.LiveTServerSet.TServerConnection;
import org.apache.accumulo.server.replication.proto.Replication.Status;
import org.apache.accumulo.server.security.delegation.AuthenticationTokenSecretManager;
-import org.apache.accumulo.server.util.NamespacePropUtil;
import org.apache.accumulo.server.util.SystemPropUtil;
-import org.apache.accumulo.server.util.TablePropUtil;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.token.Token;
import org.apache.thrift.TException;
-import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -397,19 +395,16 @@ public class ManagerClientServiceHandler implements ManagerClientService.Iface {
try {
if (value == null) {
- NamespacePropUtil.removeNamespaceProperty(manager.getContext(), namespaceId, property);
+ manager.getContext().namespacePropUtil().removeProperties(namespaceId, List.of(property));
} else {
- NamespacePropUtil.setNamespaceProperty(manager.getContext(), namespaceId, property, value);
+ manager.getContext().namespacePropUtil().setProperties(namespaceId,
+ Map.of(property, value));
}
- } catch (KeeperException.NoNodeException e) {
- // race condition... namespace no longer exists? This call will throw an exception if the
- // namespace was deleted:
+ } catch (IllegalStateException ex) {
+ // race condition on delete... namespace no longer exists? An undelying ZooKeeper.NoNode
+ // exception will be thrown an exception if the namespace was deleted:
ClientServiceHandler.checkNamespaceId(manager.getContext(), namespace, op);
- log.info("Error altering namespace property", e);
- throw new ThriftTableOperationException(namespaceId.canonical(), namespace, op,
- TableOperationExceptionType.OTHER, "Problem altering namespaceproperty");
- } catch (Exception e) {
- log.error("Problem altering namespace property", e);
+ log.info("Error altering namespace property", ex);
throw new ThriftTableOperationException(namespaceId.canonical(), namespace, op,
TableOperationExceptionType.OTHER, "Problem altering namespace property");
}
@@ -424,21 +419,19 @@ public class ManagerClientServiceHandler implements ManagerClientService.Iface {
try {
if (value == null || value.isEmpty()) {
- TablePropUtil.removeTableProperty(manager.getContext(), tableId, property);
- } else if (!TablePropUtil.setTableProperty(manager.getContext(), tableId, property, value)) {
- throw new Exception("Invalid table property.");
+ manager.getContext().tablePropUtil().removeProperties(tableId, List.of(property));
+ } else {
+ manager.getContext().tablePropUtil().setProperties(tableId, Map.of(property, value));
}
- } catch (KeeperException.NoNodeException e) {
+ } catch (IllegalStateException ex) {
+ log.warn("Invalid table property, tried to set: tableId: " + tableId.canonical() + " to: "
+ + property + "=" + value);
// race condition... table no longer exists? This call will throw an exception if the table
// was deleted:
ClientServiceHandler.checkTableId(manager.getContext(), tableName, op);
- log.info("Error altering table property", e);
- throw new ThriftTableOperationException(tableId.canonical(), tableName, op,
- TableOperationExceptionType.OTHER, "Problem altering table property");
- } catch (Exception e) {
- log.error("Problem altering table property", e);
throw new ThriftTableOperationException(tableId.canonical(), tableName, op,
- TableOperationExceptionType.OTHER, "Problem altering table property");
+ TableOperationExceptionType.OTHER, "Invalid table property, tried to set: tableId: "
+ + tableId.canonical() + " to: " + property + "=" + value);
}
}
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateZookeeper.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateZookeeper.java
index 7efa000dcd..38137196e9 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateZookeeper.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/create/PopulateZookeeper.java
@@ -18,8 +18,6 @@
*/
package org.apache.accumulo.manager.tableOps.create;
-import java.util.Map.Entry;
-
import org.apache.accumulo.core.clientImpl.thrift.TableOperation;
import org.apache.accumulo.core.clientImpl.thrift.TableOperationExceptionType;
import org.apache.accumulo.core.clientImpl.thrift.ThriftTableOperationException;
@@ -28,7 +26,6 @@ import org.apache.accumulo.manager.Manager;
import org.apache.accumulo.manager.tableOps.ManagerRepo;
import org.apache.accumulo.manager.tableOps.TableInfo;
import org.apache.accumulo.manager.tableOps.Utils;
-import org.apache.accumulo.server.util.TablePropUtil;
class PopulateZookeeper extends ManagerRepo {
@@ -59,13 +56,13 @@ class PopulateZookeeper extends ManagerRepo {
manager.getTableManager().addTable(tableInfo.getTableId(), tableInfo.getNamespaceId(),
tableInfo.getTableName());
- for (Entry<String,String> entry : tableInfo.props.entrySet()) {
- if (!TablePropUtil.setTableProperty(manager.getContext(), tableInfo.getTableId(),
- entry.getKey(), entry.getValue())) {
- throw new ThriftTableOperationException(null, tableInfo.getTableName(),
- TableOperation.CREATE, TableOperationExceptionType.OTHER,
- "Property or value not valid " + entry.getKey() + "=" + entry.getValue());
- }
+ try {
+ manager.getContext().tablePropUtil().setProperties(tableInfo.getTableId(), tableInfo.props);
+ } catch (IllegalStateException ex) {
+ throw new ThriftTableOperationException(null, tableInfo.getTableName(),
+ TableOperation.CREATE, TableOperationExceptionType.OTHER,
+ "Property or value not valid for create " + tableInfo.getTableName() + " in "
+ + tableInfo.props);
}
manager.getContext().clearTableListCache();
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/namespace/create/PopulateZookeeperWithNamespace.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/namespace/create/PopulateZookeeperWithNamespace.java
index 4731b4f0c4..0011123e97 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/namespace/create/PopulateZookeeperWithNamespace.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/namespace/create/PopulateZookeeperWithNamespace.java
@@ -18,8 +18,6 @@
*/
package org.apache.accumulo.manager.tableOps.namespace.create;
-import java.util.Map.Entry;
-
import org.apache.accumulo.core.clientImpl.thrift.TableOperation;
import org.apache.accumulo.fate.Repo;
import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeExistsPolicy;
@@ -27,13 +25,12 @@ import org.apache.accumulo.manager.Manager;
import org.apache.accumulo.manager.tableOps.ManagerRepo;
import org.apache.accumulo.manager.tableOps.Utils;
import org.apache.accumulo.server.tables.TableManager;
-import org.apache.accumulo.server.util.NamespacePropUtil;
class PopulateZookeeperWithNamespace extends ManagerRepo {
private static final long serialVersionUID = 1L;
- private NamespaceInfo namespaceInfo;
+ private final NamespaceInfo namespaceInfo;
PopulateZookeeperWithNamespace(NamespaceInfo ti) {
this.namespaceInfo = ti;
@@ -53,13 +50,11 @@ class PopulateZookeeperWithNamespace extends ManagerRepo {
Utils.checkNamespaceDoesNotExist(manager.getContext(), namespaceInfo.namespaceName,
namespaceInfo.namespaceId, TableOperation.CREATE);
- TableManager.prepareNewNamespaceState(manager.getContext().getZooReaderWriter(),
- manager.getInstanceID(), namespaceInfo.namespaceId, namespaceInfo.namespaceName,
- NodeExistsPolicy.OVERWRITE);
+ TableManager.prepareNewNamespaceState(manager.getContext(), namespaceInfo.namespaceId,
+ namespaceInfo.namespaceName, NodeExistsPolicy.OVERWRITE);
- for (Entry<String,String> entry : namespaceInfo.props.entrySet())
- NamespacePropUtil.setNamespaceProperty(manager.getContext(), namespaceInfo.namespaceId,
- entry.getKey(), entry.getValue());
+ manager.getContext().namespacePropUtil().setProperties(namespaceInfo.namespaceId,
+ namespaceInfo.props);
manager.getContext().clearTableListCache();
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/tableImport/ImportPopulateZookeeper.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/tableImport/ImportPopulateZookeeper.java
index 4a6c78999d..53c0cc7b9d 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/tableImport/ImportPopulateZookeeper.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/tableImport/ImportPopulateZookeeper.java
@@ -20,7 +20,6 @@ package org.apache.accumulo.manager.tableOps.tableImport;
import java.io.IOException;
import java.util.Map;
-import java.util.Map.Entry;
import org.apache.accumulo.core.clientImpl.AcceptableThriftTableOperationException;
import org.apache.accumulo.core.clientImpl.Namespaces;
@@ -34,7 +33,6 @@ import org.apache.accumulo.manager.Manager;
import org.apache.accumulo.manager.tableOps.ManagerRepo;
import org.apache.accumulo.manager.tableOps.Utils;
import org.apache.accumulo.server.fs.VolumeManager;
-import org.apache.accumulo.server.util.TablePropUtil;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
@@ -88,13 +86,14 @@ class ImportPopulateZookeeper extends ManagerRepo {
}
VolumeManager volMan = env.getVolumeManager();
- for (Entry<String,String> entry : getExportedProps(volMan).entrySet())
- if (!TablePropUtil.setTableProperty(env.getContext(), tableInfo.tableId, entry.getKey(),
- entry.getValue())) {
- throw new AcceptableThriftTableOperationException(tableInfo.tableId.canonical(),
- tableInfo.tableName, TableOperation.IMPORT, TableOperationExceptionType.OTHER,
- "Invalid table property " + entry.getKey());
- }
+
+ try {
+ env.getContext().tablePropUtil().setProperties(tableInfo.tableId, getExportedProps(volMan));
+ } catch (IllegalStateException ex) {
+ throw new AcceptableThriftTableOperationException(tableInfo.tableId.canonical(),
+ tableInfo.tableName, TableOperation.IMPORT, TableOperationExceptionType.OTHER,
+ "failed to set table properties");
+ }
return new CreateImportDir(tableInfo);
}
diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/upgrade/Upgrader9to10.java b/server/manager/src/main/java/org/apache/accumulo/manager/upgrade/Upgrader9to10.java
index 20136e7fad..62a2cecc61 100644
--- a/server/manager/src/main/java/org/apache/accumulo/manager/upgrade/Upgrader9to10.java
+++ b/server/manager/src/main/java/org/apache/accumulo/manager/upgrade/Upgrader9to10.java
@@ -43,8 +43,6 @@ import org.apache.accumulo.core.client.MutationsRejectedException;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.admin.TimeType;
-import org.apache.accumulo.core.conf.ConfigurationCopy;
-import org.apache.accumulo.core.conf.DeprecatedPropertyUtil;
import org.apache.accumulo.core.conf.Property;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
@@ -74,13 +72,11 @@ import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeExistsPolicy;
import org.apache.accumulo.fate.zookeeper.ZooUtil.NodeMissingPolicy;
import org.apache.accumulo.server.ServerContext;
-import org.apache.accumulo.server.conf.ZooConfiguration;
+import org.apache.accumulo.server.conf.util.ConfigPropertyUpgrader;
import org.apache.accumulo.server.fs.VolumeManager;
import org.apache.accumulo.server.gc.GcVolumeUtil;
import org.apache.accumulo.server.metadata.RootGcCandidates;
import org.apache.accumulo.server.metadata.TabletMutatorBase;
-import org.apache.accumulo.server.util.SystemPropUtil;
-import org.apache.accumulo.server.util.TablePropUtil;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
@@ -123,9 +119,9 @@ public class Upgrader9to10 implements Upgrader {
@Override
public void upgradeZookeeper(ServerContext context) {
+ upgradePropertyStorage(context);
setMetaTableProps(context);
upgradeRootTabletMetadata(context);
- renameOldMasterPropsinZK(context);
createExternalCompactionNodes(context);
// special case where old files need to be deleted
dropSortedMapWALFiles(context);
@@ -145,24 +141,33 @@ public class Upgrader9to10 implements Upgrader {
upgradeFileDeletes(context, Ample.DataLevel.USER);
}
+ /**
+ * Convert system properties (if necessary) and all table properties to a single node
+ */
+ private void upgradePropertyStorage(ServerContext context) {
+ log.info("Starting property conversion");
+ ConfigPropertyUpgrader configUpgrader = new ConfigPropertyUpgrader();
+ configUpgrader.doUpgrade(context.getInstanceID(), context.getZooReaderWriter());
+ log.info("Completed property conversion");
+ }
+
/**
* Setup properties for External compactions.
*/
private void setMetaTableProps(ServerContext context) {
try {
- TablePropUtil.setTableProperty(context, RootTable.ID,
- Property.TABLE_COMPACTION_DISPATCHER.getKey(),
- SimpleCompactionDispatcher.class.getName());
- TablePropUtil.setTableProperty(context, RootTable.ID,
- Property.TABLE_COMPACTION_DISPATCHER_OPTS.getKey() + "service", "root");
-
- TablePropUtil.setTableProperty(context, MetadataTable.ID,
- Property.TABLE_COMPACTION_DISPATCHER.getKey(),
- SimpleCompactionDispatcher.class.getName());
- TablePropUtil.setTableProperty(context, MetadataTable.ID,
- Property.TABLE_COMPACTION_DISPATCHER_OPTS.getKey() + "service", "meta");
- } catch (KeeperException | InterruptedException e) {
- throw new RuntimeException("Unable to set system table properties", e);
+ // root compaction props
+ context.tablePropUtil().setProperties(RootTable.ID,
+ Map.of(Property.TABLE_COMPACTION_DISPATCHER.getKey(),
+ SimpleCompactionDispatcher.class.getName(),
+ Property.TABLE_COMPACTION_DISPATCHER_OPTS.getKey() + "service", "root"));
+ // metadata compaction props
+ context.tablePropUtil().setProperties(MetadataTable.ID,
+ Map.of(Property.TABLE_COMPACTION_DISPATCHER.getKey(),
+ SimpleCompactionDispatcher.class.getName(),
+ Property.TABLE_COMPACTION_DISPATCHER_OPTS.getKey() + "service", "meta"));
+ } catch (IllegalStateException ex) {
+ throw new RuntimeException("Unable to set system table properties", ex);
}
}
@@ -242,31 +247,9 @@ public class Upgrader9to10 implements Upgrader {
delete(context, ZROOT_TABLET_PATH);
}
- @SuppressWarnings("deprecation")
- private void renameOldMasterPropsinZK(ServerContext context) {
- // Rename all of the properties only set in ZooKeeper that start with "master." to rename and
- // store them starting with "manager." instead.
- var zooConfiguration =
- new ZooConfiguration(context, context.getZooCache(), new ConfigurationCopy());
- zooConfiguration.getAllPropertiesWithPrefix(Property.MASTER_PREFIX)
- .forEach((original, value) -> {
- DeprecatedPropertyUtil.getReplacementName(original, (log, replacement) -> {
- log.info("Automatically renaming deprecated property '{}' with its replacement '{}'"
- + " in ZooKeeper on upgrade.", original, replacement);
- try {
- // Set the property under the new name
- SystemPropUtil.setSystemProperty(context, replacement, value);
- SystemPropUtil.removePropWithoutDeprecationWarning(context, original);
- } catch (KeeperException | InterruptedException e) {
- throw new RuntimeException("Unable to upgrade system properties", e);
- }
- });
- });
- }
-
private static class UpgradeMutator extends TabletMutatorBase {
- private ServerContext context;
+ private final ServerContext context;
UpgradeMutator(ServerContext context) {
super(context, RootTable.EXTENT);
@@ -481,11 +464,10 @@ public class Upgrader9to10 implements Upgrader {
public void upgradeFileDeletes(ServerContext context, Ample.DataLevel level) {
String tableName = level.metaTable();
- AccumuloClient c = context;
Ample ample = context.getAmple();
// find all deletes
- try (BatchWriter writer = c.createBatchWriter(tableName)) {
+ try (BatchWriter writer = context.createBatchWriter(tableName)) {
log.info("looking for candidates in table {}", tableName);
Iterator<String> oldCandidates = getOldCandidates(context, tableName);
String upgradeProp =
@@ -580,10 +562,9 @@ public class Upgrader9to10 implements Upgrader {
*/
public void upgradeDirColumns(ServerContext context, Ample.DataLevel level) {
String tableName = level.metaTable();
- AccumuloClient c = context;
- try (Scanner scanner = c.createScanner(tableName, Authorizations.EMPTY);
- BatchWriter writer = c.createBatchWriter(tableName)) {
+ try (Scanner scanner = context.createScanner(tableName, Authorizations.EMPTY);
+ BatchWriter writer = context.createBatchWriter(tableName)) {
DIRECTORY_COLUMN.fetch(scanner);
for (Entry<Key,Value> entry : scanner) {
@@ -611,13 +592,12 @@ public class Upgrader9to10 implements Upgrader {
*/
public static void upgradeRelativePaths(ServerContext context, Ample.DataLevel level) {
String tableName = level.metaTable();
- AccumuloClient c = context;
VolumeManager fs = context.getVolumeManager();
String upgradeProp = context.getConfiguration().get(Property.INSTANCE_VOLUMES_UPGRADE_RELATIVE);
// first pass check for relative paths - if any, check existence of the file path
// constructed from the upgrade property + relative path
- if (checkForRelativePaths(c, fs, tableName, upgradeProp)) {
+ if (checkForRelativePaths(context, fs, tableName, upgradeProp)) {
log.info("Relative Tablet File paths exist in {}, replacing with absolute using {}",
tableName, upgradeProp);
} else {
@@ -626,7 +606,7 @@ public class Upgrader9to10 implements Upgrader {
}
// second pass, create atomic mutations to replace the relative path
- replaceRelativePaths(c, fs, tableName, upgradeProp);
+ replaceRelativePaths(context, fs, tableName, upgradeProp);
}
/**
diff --git a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java
index 74ef81c7ed..6cc29716c8 100644
--- a/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java
+++ b/server/tserver/src/main/java/org/apache/accumulo/tserver/TabletServerResourceManager.java
@@ -252,7 +252,6 @@ public class TabletServerResourceManager {
public TabletServerResourceManager(ServerContext context) {
this.context = context;
final AccumuloConfiguration acuConf = context.getConfiguration();
-
long maxMemory = acuConf.getAsBytes(Property.TSERV_MAXMEM);
boolean usingNativeMap = acuConf.getBoolean(Property.TSERV_NATIVEMAP_ENABLED);
if (usingNativeMap) {
diff --git a/test/pom.xml b/test/pom.xml
index 0cbcfb085f..0ee138e467 100644
--- a/test/pom.xml
+++ b/test/pom.xml
@@ -37,6 +37,10 @@
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.github.ben-manes.caffeine</groupId>
+ <artifactId>caffeine</artifactId>
+ </dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
diff --git a/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java b/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java
index f911774e17..099cf78450 100644
--- a/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/TableConfigurationUpdateIT.java
@@ -61,8 +61,8 @@ public class TableConfigurationUpdateIT extends AccumuloClusterHarness {
String table = getUniqueNames(1)[0];
client.tableOperations().create(table);
- final NamespaceConfiguration defaultConf = new NamespaceConfiguration(Namespace.DEFAULT.id(),
- context, DefaultConfiguration.getInstance());
+ final NamespaceConfiguration defaultConf = new NamespaceConfiguration(context,
+ Namespace.DEFAULT.id(), DefaultConfiguration.getInstance());
// Cache invalidates 25% of the time
int randomMax = 4;
@@ -70,8 +70,8 @@ public class TableConfigurationUpdateIT extends AccumuloClusterHarness {
int numThreads = 2;
// Number of iterations per thread
int iterations = 100000;
- AccumuloConfiguration tableConf =
- new TableConfiguration(context, TableId.of(table), defaultConf);
+ TableId tid = TableId.of(client.tableOperations().tableIdMap().get(table));
+ AccumuloConfiguration tableConf = new TableConfiguration(context, tid, defaultConf);
long start = System.currentTimeMillis();
ExecutorService svc = Executors.newFixedThreadPool(numThreads);
diff --git a/test/src/main/java/org/apache/accumulo/test/conf/PropStoreConfigIT.java b/test/src/main/java/org/apache/accumulo/test/conf/PropStoreConfigIT.java
new file mode 100644
index 0000000000..190b11a074
--- /dev/null
+++ b/test/src/main/java/org/apache/accumulo/test/conf/PropStoreConfigIT.java
@@ -0,0 +1,67 @@
+/*
+ * 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.accumulo.test.conf;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.accumulo.harness.AccumuloITBase.MINI_CLUSTER_ONLY;
+import static org.apache.accumulo.harness.AccumuloITBase.SUNNY_DAY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Map;
+
+import org.apache.accumulo.core.client.Accumulo;
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.harness.AccumuloClusterHarness;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Tag(MINI_CLUSTER_ONLY)
+@Tag(SUNNY_DAY)
+public class PropStoreConfigIT extends AccumuloClusterHarness {
+
+ private static final Logger log = LoggerFactory.getLogger(PropStoreConfigIT.class);
+
+ @Test
+ public void setTablePropTest() throws Exception {
+ String table = getUniqueNames(1)[0];
+
+ try (var client = Accumulo.newClient().from(getClientProps()).build()) {
+
+ client.tableOperations().create(table);
+
+ log.info("Tables: {}", client.tableOperations().list());
+
+ client.instanceOperations().setProperty(Property.TABLE_BLOOM_ENABLED.getKey(), "true");
+ client.tableOperations().setProperty(table, Property.TABLE_BLOOM_ENABLED.getKey(), "true");
+
+ Thread.sleep(SECONDS.toMillis(3L));
+
+ var props = client.tableOperations().getProperties(table);
+ log.info("Props: {}", props);
+ for (Map.Entry<String,String> e : props) {
+ if (e.getKey().contains("table.bloom.enabled")) {
+ log.info("after bloom property: {}={}", e.getKey(), e.getValue());
+ assertEquals("true", e.getValue());
+ }
+ }
+ }
+ }
+}
diff --git a/test/src/main/java/org/apache/accumulo/test/conf/store/PropCacheCaffeineImplZkIT.java b/test/src/main/java/org/apache/accumulo/test/conf/store/PropCacheCaffeineImplZkIT.java
new file mode 100644
index 0000000000..2f0b9034dd
--- /dev/null
+++ b/test/src/main/java/org/apache/accumulo/test/conf/store/PropCacheCaffeineImplZkIT.java
@@ -0,0 +1,180 @@
+/*
+ * 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.accumulo.test.conf.store;
+
+import static org.apache.accumulo.core.conf.Property.GC_PORT;
+import static org.apache.accumulo.core.conf.Property.MANAGER_CLIENTPORT;
+import static org.apache.accumulo.core.conf.Property.TSERV_CLIENTPORT;
+import static org.apache.accumulo.core.conf.Property.TSERV_NATIVEMAP_ENABLED;
+import static org.apache.accumulo.core.conf.Property.TSERV_SCAN_MAX_OPENFILES;
+import static org.apache.accumulo.harness.AccumuloITBase.ZOOKEEPER_TESTING_SERVER;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.core.metrics.MetricsUtil;
+import org.apache.accumulo.fate.zookeeper.ZooReaderWriter;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.accumulo.server.conf.store.impl.PropCacheCaffeineImpl;
+import org.apache.accumulo.server.conf.store.impl.PropStoreMetrics;
+import org.apache.accumulo.server.conf.store.impl.PropStoreWatcher;
+import org.apache.accumulo.server.conf.store.impl.ReadyMonitor;
+import org.apache.accumulo.server.conf.store.impl.ZooPropLoader;
+import org.apache.accumulo.test.zookeeper.ZooKeeperTestingServer;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.ZKUtil;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.ZooKeeper;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Tag(ZOOKEEPER_TESTING_SERVER)
+public class PropCacheCaffeineImplZkIT {
+
+ private static final Logger log = LoggerFactory.getLogger(PropCacheCaffeineImplZkIT.class);
+ private static final InstanceId INSTANCE_ID = InstanceId.of(UUID.randomUUID());
+
+ private static ZooKeeperTestingServer testZk = null;
+ private static ZooReaderWriter zrw;
+ private static ZooKeeper zooKeeper;
+
+ private final TableId tIdA = TableId.of("A");
+ private final TableId tIdB = TableId.of("B");
+ private final PropStoreMetrics cacheMetrics = new PropStoreMetrics();
+ private static ServerContext context;
+
+ @TempDir
+ private static File tempDir;
+
+ @BeforeAll
+ public static void setupZk() {
+ // using default zookeeper port - we don't have a full configuration
+ testZk = new ZooKeeperTestingServer(tempDir);
+ zooKeeper = testZk.getZooKeeper();
+
+ zrw = testZk.getZooReaderWriter();
+ context = createNiceMock(ServerContext.class);
+ expect(context.getInstanceID()).andReturn(INSTANCE_ID).anyTimes();
+ expect(context.getZooReaderWriter()).andReturn(zrw).anyTimes();
+
+ replay(context);
+ }
+
+ @AfterAll
+ public static void shutdownZK() throws Exception {
+ verify(context);
+ testZk.close();
+ }
+
+ @BeforeEach
+ public void setupZnodes() {
+ testZk.initPaths(ZooUtil.getRoot(INSTANCE_ID) + Constants.ZCONFIG);
+ try {
+ zooKeeper.create(ZooUtil.getRoot(INSTANCE_ID) + Constants.ZTABLES, new byte[0],
+ ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
+ zooKeeper.create(ZooUtil.getRoot(INSTANCE_ID) + Constants.ZTABLES + "/" + tIdA.canonical(),
+ new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
+ zooKeeper.create(
+ ZooUtil.getRoot(INSTANCE_ID) + Constants.ZTABLES + "/" + tIdA.canonical() + "/conf",
+ new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
+
+ zooKeeper.create(ZooUtil.getRoot(INSTANCE_ID) + Constants.ZTABLES + "/" + tIdB.canonical(),
+ new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
+ zooKeeper.create(
+ ZooUtil.getRoot(INSTANCE_ID) + Constants.ZTABLES + "/" + tIdB.canonical() + "/conf",
+ new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
+
+ } catch (KeeperException ex) {
+ log.trace("Issue during zk initialization, skipping", ex);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted during zookeeper path initialization", ex);
+ }
+ }
+
+ @AfterEach
+ public void cleanupZnodes() {
+ try {
+ ZKUtil.deleteRecursive(zooKeeper, "/accumulo");
+ } catch (KeeperException | InterruptedException ex) {
+ throw new IllegalStateException("Failed to clean-up test zooKeeper nodes.", ex);
+ }
+ }
+
+ @Test
+ public void init() throws Exception {
+ Map<String,String> props = new HashMap<>();
+ props.put(TSERV_CLIENTPORT.getKey(), "1234");
+ props.put(TSERV_NATIVEMAP_ENABLED.getKey(), "false");
+ props.put(TSERV_SCAN_MAX_OPENFILES.getKey(), "2345");
+ props.put(MANAGER_CLIENTPORT.getKey(), "3456");
+ props.put(GC_PORT.getKey(), "4567");
+ VersionedProperties vProps = new VersionedProperties(props);
+
+ // directly create prop node - simulate existing properties.
+ var propCacheKey = TablePropKey.of(INSTANCE_ID, tIdA);
+ var created = zrw.putPersistentData(propCacheKey.getPath(),
+ VersionedPropCodec.getDefault().toBytes(vProps), ZooUtil.NodeExistsPolicy.FAIL);
+
+ assertTrue(created, "expected properties to be created");
+
+ ReadyMonitor readyMonitor = new ReadyMonitor("test", zooKeeper.getSessionTimeout());
+
+ PropStoreWatcher propStoreWatcher = new PropStoreWatcher(readyMonitor);
+
+ MetricsUtil.initializeProducers(cacheMetrics);
+
+ ZooPropLoader propLoader =
+ new ZooPropLoader(zrw, VersionedPropCodec.getDefault(), propStoreWatcher, cacheMetrics);
+ PropCacheCaffeineImpl cache =
+ new PropCacheCaffeineImpl.Builder(propLoader, cacheMetrics).build();
+
+ VersionedProperties readProps = cache.get(propCacheKey);
+
+ if (readProps == null) {
+ fail("Received null for versioned properties");
+ } else {
+ log.debug("Props read from cache: {}", readProps.print(true));
+ }
+
+ }
+}
diff --git a/test/src/main/java/org/apache/accumulo/test/conf/store/PropStoreZooKeeperIT.java b/test/src/main/java/org/apache/accumulo/test/conf/store/PropStoreZooKeeperIT.java
new file mode 100644
index 0000000000..74518c5a48
--- /dev/null
+++ b/test/src/main/java/org/apache/accumulo/test/conf/store/PropStoreZooKeeperIT.java
@@ -0,0 +1,416 @@
+/*
+ * 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.accumulo.test.conf.store;
+
+import static org.apache.accumulo.harness.AccumuloITBase.ZOOKEEPER_TESTING_SERVER;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.accumulo.core.Constants;
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.core.data.InstanceId;
+import org.apache.accumulo.core.data.TableId;
+import org.apache.accumulo.fate.zookeeper.ZooUtil;
+import org.apache.accumulo.server.ServerContext;
+import org.apache.accumulo.server.conf.codec.VersionedPropCodec;
+import org.apache.accumulo.server.conf.codec.VersionedProperties;
+import org.apache.accumulo.server.conf.store.PropCacheKey;
+import org.apache.accumulo.server.conf.store.PropChangeListener;
+import org.apache.accumulo.server.conf.store.PropStore;
+import org.apache.accumulo.server.conf.store.TablePropKey;
+import org.apache.accumulo.server.conf.store.impl.ZooPropStore;
+import org.apache.accumulo.test.zookeeper.ZooKeeperTestingServer;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.ZKUtil;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.ZooKeeper;
+import org.apache.zookeeper.data.Stat;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Tag(ZOOKEEPER_TESTING_SERVER)
+public class PropStoreZooKeeperIT {
+
+ private static final Logger log = LoggerFactory.getLogger(PropStoreZooKeeperIT.class);
+ private static final VersionedPropCodec propCodec = VersionedPropCodec.getDefault();
+ private static ZooKeeperTestingServer testZk = null;
+ private static ZooKeeper zooKeeper;
+ private ServerContext context;
+ private InstanceId instanceId = null;
+ private PropStore propStore = null;
+ private final TableId tIdA = TableId.of("A");
+ private final TableId tIdB = TableId.of("B");
+
+ @TempDir
+ private static File tempDir;
+
... 1837 lines suppressed ...