This is an automated email from the ASF dual-hosted git repository.
szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git
The following commit(s) were added to refs/heads/master by this push:
new 2b1b1b57f RATIS-1636. Support re-config ratis properties (#800)
2b1b1b57f is described below
commit 2b1b1b57f01dd629147ea1c956721520761e9126
Author: Yaolong Liu <[email protected]>
AuthorDate: Sat Dec 31 20:32:20 2022 +0800
RATIS-1636. Support re-config ratis properties (#800)
---
.../java/org/apache/ratis/conf/RaftProperties.java | 8 +-
.../java/org/apache/ratis/conf/Reconfigurable.java | 59 +++
.../org/apache/ratis/conf/ReconfigurationBase.java | 181 ++++++++
.../ratis/conf/ReconfigurationException.java | 63 +++
.../apache/ratis/conf/ReconfigurationStatus.java | 151 +++++++
.../org/apache/ratis/TestReConfigProperty.java | 478 +++++++++++++++++++++
.../ratis/grpc/TestReConfigPropertyWithGrpc.java | 26 ++
7 files changed, 965 insertions(+), 1 deletion(-)
diff --git
a/ratis-common/src/main/java/org/apache/ratis/conf/RaftProperties.java
b/ratis-common/src/main/java/org/apache/ratis/conf/RaftProperties.java
index b48f79f99..f51bc731f 100644
--- a/ratis-common/src/main/java/org/apache/ratis/conf/RaftProperties.java
+++ b/ratis-common/src/main/java/org/apache/ratis/conf/RaftProperties.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -31,6 +31,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -708,6 +709,11 @@ public class RaftProperties {
properties.clear();
}
+ /** @return the property entry set. */
+ Set<Map.Entry<String, String>> entrySet() {
+ return properties.entrySet();
+ }
+
@Override
public String toString() {
return JavaUtils.getClassSimpleName(getClass()) + ":" + size();
diff --git
a/ratis-common/src/main/java/org/apache/ratis/conf/Reconfigurable.java
b/ratis-common/src/main/java/org/apache/ratis/conf/Reconfigurable.java
new file mode 100644
index 000000000..6e8a527da
--- /dev/null
+++ b/ratis-common/src/main/java/org/apache/ratis/conf/Reconfigurable.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.ratis.conf;
+
+import java.util.Collection;
+
+/**
+ * To reconfigure {@link RaftProperties} in runtime.
+ */
+public interface Reconfigurable {
+ /** @return the {@link RaftProperties} to be reconfigured. */
+ RaftProperties getProperties();
+
+ /**
+ * Change a property on this object to the new value specified.
+ * If the new value specified is null, reset the property to its default
value.
+ * <p>
+ * This method must apply the change to all internal data structures derived
+ * from the configuration property that is being changed.
+ * If this object owns other {@link Reconfigurable} objects,
+ * it must call this method recursively in order to update all these objects.
+ *
+ * @param property the name of the given property.
+ * @param newValue the new value.
+ * @return the effective value, which could possibly be different from
specified new value,
+ * of the property after reconfiguration.
+ * @throws ReconfigurationException if the property is not reconfigurable or
there is an error applying the new value.
+ */
+ String reconfigureProperty(String property, String newValue) throws
ReconfigurationException;
+
+ /**
+ * Is the given property reconfigurable at runtime?
+ *
+ * @param property the name of the given property.
+ * @return true iff the given property is reconfigurable.
+ */
+ default boolean isPropertyReconfigurable(String property) {
+ return getReconfigurableProperties().contains(property);
+ }
+
+ /** @return all the properties that are reconfigurable at runtime. */
+ Collection<String> getReconfigurableProperties();
+}
diff --git
a/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationBase.java
b/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationBase.java
new file mode 100644
index 000000000..ea6ba225e
--- /dev/null
+++ b/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationBase.java
@@ -0,0 +1,181 @@
+/*
+ * 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.ratis.conf;
+
+import org.apache.ratis.conf.ReconfigurationStatus.PropertyChange;
+import org.apache.ratis.util.Daemon;
+import org.apache.ratis.util.Timestamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Base class for implementing the {@link Reconfigurable} interface.
+ * Subclasses must override
+ * (1) {@link #getReconfigurableProperties()} to return all properties that
can be reconfigurable at runtime,
+ * (2) {@link #getNewProperties()} to return the new {@link RaftProperties} to
be reconfigured to, and
+ * (3) {@link #reconfigureProperty(String, String)} to change individual
properties.
+ */
+public abstract class ReconfigurationBase implements Reconfigurable {
+ private static final Logger LOG =
LoggerFactory.getLogger(ReconfigurationBase.class);
+
+ public static Collection<PropertyChange> getChangedProperties(
+ RaftProperties newProperties, RaftProperties oldProperties) {
+ final Map<String, PropertyChange> changes = new HashMap<>();
+
+ // iterate over old properties
+ for (Map.Entry<String, String> oldEntry: oldProperties.entrySet()) {
+ final String prop = oldEntry.getKey();
+ final String oldVal = oldEntry.getValue();
+ final String newVal = newProperties.getRaw(prop);
+
+ if (!Objects.equals(newVal, oldVal)) {
+ changes.put(prop, new PropertyChange(prop, newVal, oldVal));
+ }
+ }
+
+ // now iterate over new properties in order to look for properties not
present in old properties
+ for (Map.Entry<String, String> newEntry: newProperties.entrySet()) {
+ final String prop = newEntry.getKey();
+ final String newVal = newEntry.getValue();
+ if (newVal != null && oldProperties.get(prop) == null) {
+ changes.put(prop, new PropertyChange(prop, newVal, null));
+ }
+ }
+
+ return changes.values();
+ }
+
+ class Context {
+ /** The current reconfiguration status. */
+ private ReconfigurationStatus status = new ReconfigurationStatus(null,
null, null, null);
+ /** Is this context stopped? */
+ private boolean isStopped;
+
+ synchronized ReconfigurationStatus getStatus() {
+ return status;
+ }
+
+ synchronized void start() {
+ if (isStopped) {
+ throw new IllegalStateException(name + " is stopped.");
+ }
+ final Daemon previous = status.getDaemon();
+ if (previous != null) {
+ throw new IllegalStateException(name + ": a reconfiguration task " +
previous + " is already running.");
+ }
+ final Timestamp startTime = Timestamp.currentTime();
+ final Daemon task = Daemon.newBuilder()
+ .setName("started@" + startTime)
+ .setRunnable(ReconfigurationBase.this::batchReconfiguration)
+ .build();
+ status = new ReconfigurationStatus(startTime, null, null, task);
+ task.start();
+ }
+
+ synchronized void end(Map<PropertyChange, Throwable> results) {
+ status = new ReconfigurationStatus(status.getStartTime(),
Timestamp.currentTime(), results, null);
+ }
+
+ synchronized Daemon stop() {
+ isStopped = true;
+ final Daemon task = status.getDaemon();
+ status = new ReconfigurationStatus(status.getStartTime(), null, null,
null);
+ return task;
+ }
+ }
+
+ private final String name;
+ private final RaftProperties properties;
+ private final Context context;
+
+ /**
+ * Construct a ReconfigurableBase with the {@link RaftProperties}
+ * @param properties raft properties.
+ */
+ public ReconfigurationBase(String name, RaftProperties properties) {
+ this.name = name;
+ this.properties = properties;
+ this.context = new Context();
+ }
+
+ @Override
+ public RaftProperties getProperties() {
+ return properties;
+ }
+
+ /** @return the new {@link RaftProperties} to be reconfigured to. */
+ protected abstract RaftProperties getNewProperties();
+
+ /**
+ * Start a reconfiguration task to reload raft property in background.
+ * @throws IOException raised on errors performing I/O.
+ */
+ public void startReconfiguration() throws IOException {
+ context.start();
+ }
+
+ public ReconfigurationStatus getReconfigurationStatus() {
+ return context.getStatus();
+ }
+
+ public void shutdown() throws InterruptedException {
+ context.stop().join();
+ }
+
+ /**
+ * Run a batch reconfiguration to change the current properties
+ * to the properties returned by {@link #getNewProperties()}.
+ */
+ private void batchReconfiguration() {
+ LOG.info("{}: Starting batch reconfiguration {}", name,
Thread.currentThread());
+ final Collection<PropertyChange> changes =
getChangedProperties(getNewProperties(), properties);
+ final Map<PropertyChange, Throwable> results = new HashMap<>();
+ for (PropertyChange change : changes) {
+ LOG.info("Change property: " + change);
+ try {
+ singleReconfiguration(change.getProperty(), change.getNewValue());
+ results.put(change, null);
+ } catch (Throwable t) {
+ results.put(change, t);
+ }
+ }
+ context.end(results);
+ }
+
+ /** Run a single reconfiguration to change the given property to the given
value. */
+ private void singleReconfiguration(String property, String newValue) throws
ReconfigurationException {
+ if (!isPropertyReconfigurable(property)) {
+ throw new ReconfigurationException("Property is not reconfigurable.",
+ property, newValue, properties.get(property));
+ }
+ final String effective = reconfigureProperty(property, newValue);
+ LOG.info("{}: changed property {} to {} (effective {})", name, property,
newValue, effective);
+ if (newValue != null) {
+ properties.set(property, effective);
+ } else {
+ properties.unset(property);
+ }
+ }
+}
\ No newline at end of file
diff --git
a/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationException.java
b/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationException.java
new file mode 100644
index 000000000..15c8c8225
--- /dev/null
+++
b/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationException.java
@@ -0,0 +1,63 @@
+/*
+ * 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.ratis.conf;
+
+import static org.apache.ratis.conf.ReconfigurationStatus.propertyString;
+
+public class ReconfigurationException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private final String property;
+ private final String newValue;
+ private final String oldValue;
+
+ /**
+ * Create a new instance of {@link ReconfigurationException}.
+ * @param property the property name.
+ * @param newValue the new value.
+ * @param oldValue the old value.
+ * @param cause the cause of this exception.
+ */
+ public ReconfigurationException(String reason, String property, String
newValue, String oldValue, Throwable cause) {
+ super("Failed to change property " + propertyString(property, newValue,
oldValue) + ": " + reason, cause);
+ this.property = property;
+ this.newValue = newValue;
+ this.oldValue = oldValue;
+ }
+
+ /** The same as new ReconfigurationException(reason, property, newValue,
oldValue, null). */
+ public ReconfigurationException(String reason, String property, String
newValue, String oldValue) {
+ this(reason, property, newValue, oldValue, null);
+ }
+
+ /** @return the property name related to this exception. */
+ public String getProperty() {
+ return property;
+ }
+
+ /** @return the value that the property was supposed to be changed. */
+ public String getNewValue() {
+ return newValue;
+ }
+
+ /** @return the old value of the property. */
+ public String getOldValue() {
+ return oldValue;
+ }
+}
diff --git
a/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationStatus.java
b/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationStatus.java
new file mode 100644
index 000000000..c584fe068
--- /dev/null
+++
b/ratis-common/src/main/java/org/apache/ratis/conf/ReconfigurationStatus.java
@@ -0,0 +1,151 @@
+/*
+ * 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.ratis.conf;
+
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.ratis.util.Daemon;
+import org.apache.ratis.util.Timestamp;
+
+/** The status of a reconfiguration task. */
+public class ReconfigurationStatus {
+ private static String quote(String value) {
+ return value == null? "<default>": "\"" + value + "\"";
+ }
+
+ static String propertyString(String property, String newValue, String
oldValue) {
+ Objects.requireNonNull(property, "property == null");
+ return property + " from " + quote(oldValue) + " to " + quote(newValue);
+ }
+
+ /** The change of a configuration property. */
+ public static class PropertyChange {
+ private final String property;
+ private final String newValue;
+ private final String oldValue;
+
+ public PropertyChange(String property, String newValue, String oldValue) {
+ this.property = property;
+ this.newValue = newValue;
+ this.oldValue = oldValue;
+ }
+
+ /** @return the name of the property being changed. */
+ public String getProperty() {
+ return property;
+ }
+
+ /** @return the new value to be changed to. */
+ public String getNewValue() {
+ return newValue;
+ }
+
+ /** @return the old value of the property. */
+ public String getOldValue() {
+ return oldValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return property.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (!(obj instanceof PropertyChange)) {
+ return false;
+ }
+ final PropertyChange that = (PropertyChange)obj;
+ return Objects.equals(this.property, that.property)
+ && Objects.equals(this.oldValue, that.oldValue)
+ && Objects.equals(this.newValue, that.newValue);
+ }
+
+ @Override
+ public String toString() {
+ return propertyString(getProperty(), getNewValue(), getOldValue());
+ }
+ }
+
+ /** The timestamp when the reconfiguration starts. */
+ private final Timestamp startTime;
+ /** The timestamp when the reconfiguration completes. */
+ private final Timestamp endTime;
+ /**
+ * A property-change map.
+ * For a particular change, if the error is null,
+ * it indicates that the change has been applied successfully.
+ * Otherwise, it is the error occurred when applying the change.
+ */
+ private final Map<PropertyChange, Throwable> changes;
+ /** The daemon to run the reconfiguration. */
+ private final Daemon daemon;
+
+ ReconfigurationStatus(Timestamp startTime, Timestamp endTime,
Map<PropertyChange, Throwable> changes, Daemon daemon) {
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.changes = changes;
+ this.daemon = daemon;
+ }
+
+ /** @return true iff a reconfiguration task has started (it may either be
running or already has finished). */
+ public boolean started() {
+ return getStartTime() != null;
+ }
+
+ /** @return true if the latest reconfiguration task has ended and there are
no new active tasks started. */
+ public boolean ended() {
+ return getEndTime() != null;
+ }
+
+ /**
+ * @return the start time of the reconfiguration task if the reconfiguration
task has been started;
+ * otherwise, return null.
+ */
+ public Timestamp getStartTime() {
+ return startTime;
+ }
+
+ /**
+ * @return the end time of the reconfiguration task if the reconfiguration
task has been ended;
+ * otherwise, return null.
+ */
+ public Timestamp getEndTime() {
+ return endTime;
+ }
+
+ /**
+ * @return the changes of the reconfiguration task if the reconfiguration
task has been ended;
+ * otherwise, return null.
+ */
+ public Map<PropertyChange, Throwable> getChanges() {
+ return changes;
+ }
+
+ /**
+ * @return the daemon running the reconfiguration task if the task has been
started;
+ * otherwise, return null.
+ */
+ Daemon getDaemon() {
+ return daemon;
+ }
+}
diff --git
a/ratis-server/src/test/java/org/apache/ratis/TestReConfigProperty.java
b/ratis-server/src/test/java/org/apache/ratis/TestReConfigProperty.java
new file mode 100644
index 000000000..4535406a7
--- /dev/null
+++ b/ratis-server/src/test/java/org/apache/ratis/TestReConfigProperty.java
@@ -0,0 +1,478 @@
+/*
+ * 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.ratis;
+
+import org.apache.ratis.client.impl.OrderedAsync;
+import org.apache.ratis.conf.RaftProperties;
+import org.apache.ratis.conf.ReconfigurationBase;
+import org.apache.ratis.conf.ReconfigurationException;
+import org.apache.ratis.conf.ReconfigurationStatus.PropertyChange;
+import org.apache.ratis.server.impl.MiniRaftCluster;
+import org.apache.ratis.statemachine.StateMachine;
+import org.apache.ratis.statemachine.impl.SimpleStateMachine4Testing;
+import org.apache.ratis.util.Slf4jUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.event.Level;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.TimeoutException;
+
+public abstract class TestReConfigProperty<CLUSTER extends MiniRaftCluster>
extends BaseTest
+ implements MiniRaftCluster.Factory.Get<CLUSTER> {
+
+ {
+ Slf4jUtils.setLogLevel(OrderedAsync.LOG, Level.DEBUG);
+ getProperties().setClass(MiniRaftCluster.STATEMACHINE_CLASS_KEY,
+ SimpleStateMachine4Testing.class, StateMachine.class);
+ }
+
+ private RaftProperties conf1;
+ private RaftProperties conf2;
+
+ private static final String PROP1 = "test.prop.one";
+ private static final String PROP2 = "test.prop.two";
+ private static final String PROP3 = "test.prop.three";
+ private static final String PROP4 = "test.prop.four";
+ private static final String PROP5 = "test.prop.five";
+
+ private static final String VAL1 = "val1";
+ private static final String VAL2 = "val2";
+ private static final String DEFAULT = "default";
+
+ @Before
+ public void setup () {
+ conf1 = new RaftProperties();
+ conf2 = new RaftProperties();
+
+ // set some test properties
+ conf1.set(PROP1, VAL1);
+ conf1.set(PROP2, VAL1);
+ conf1.set(PROP3, VAL1);
+
+ conf2.set(PROP1, VAL1); // same as conf1
+ conf2.set(PROP2, VAL2); // different value as conf1
+ // PROP3 not set in conf2
+ conf2.set(PROP4, VAL1); // not set in conf1
+
+ }
+
+ @Test
+ public void testGetChangedProperty() {
+ Collection<PropertyChange> changes
+ = ReconfigurationBase.getChangedProperties(conf2, conf1);
+
+ Assert.assertTrue("expected 3 changed properties but got " +
changes.size(),
+ changes.size() == 3);
+
+ boolean changeFound = false;
+ boolean unsetFound = false;
+ boolean setFound = false;
+
+ for (PropertyChange c: changes) {
+ if (c.getProperty().equals(PROP2) && c.getOldValue() != null &&
c.getOldValue().equals(VAL1) &&
+ c.getNewValue() != null && c.getNewValue().equals(VAL2)) {
+ changeFound = true;
+ } else if (c.getProperty().equals(PROP3) && c.getOldValue() != null &&
c.getOldValue().equals(VAL1) &&
+ c.getNewValue() == null) {
+ unsetFound = true;
+ } else if (c.getProperty().equals(PROP4) && c.getOldValue() == null &&
+ c.getNewValue() != null && c.getNewValue().equals(VAL1)) {
+ setFound = true;
+ }
+ }
+ Assert.assertTrue("not all changes have been applied",
+ changeFound && unsetFound && setFound);
+ }
+
+ /**
+ * a simple reconfigurable class
+ */
+ public static class ReconfigurableDummy extends ReconfigurationBase
+ implements Runnable {
+ public volatile boolean running = true;
+ private RaftProperties newProp;
+
+ public ReconfigurableDummy(RaftProperties prop) {
+ super("reConfigDummy", prop);
+ }
+
+ @Override
+ protected RaftProperties getNewProperties() {
+ return newProp;
+ }
+
+ @Override
+ public synchronized String reconfigureProperty(String property, String
newValue)
+ throws ReconfigurationException {
+ newProp = new RaftProperties();
+ newProp.set(property, newValue != null ? newValue : DEFAULT);
+ return newValue;
+ }
+
+ @Override
+ public Collection<String> getReconfigurableProperties() {
+ return Arrays.asList(PROP1, PROP2, PROP4);
+ }
+
+ /**
+ * Run until PROP1 is no longer VAL1.
+ */
+ @Override
+ public void run() {
+ while (running && getProperties().get(PROP1).equals(VAL1)) {
+ try {
+ Thread.sleep(1);
+ } catch (InterruptedException ignore) {
+ // do nothing
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Test reconfiguring a Reconfigurable.
+ */
+ @Test
+ public void testReconfigure() {
+ ReconfigurableDummy dummy = new ReconfigurableDummy(conf1);
+
+ Assert.assertEquals(PROP1 + " set to wrong value ", VAL1,
dummy.getProperties().get(PROP1));
+ Assert.assertEquals(PROP2 + " set to wrong value ", VAL1,
dummy.getProperties().get(PROP2));
+ Assert.assertEquals(PROP3 + " set to wrong value ", VAL1,
dummy.getProperties().get(PROP3));
+ Assert.assertNull(PROP4 + " set to wrong value ",
dummy.getProperties().get(PROP4));
+ Assert.assertNull(PROP5 + " set to wrong value ",
dummy.getProperties().get(PROP5));
+
+ Assert.assertTrue(PROP1 + " should be reconfigurable ",
+ dummy.isPropertyReconfigurable(PROP1));
+ Assert.assertTrue(PROP2 + " should be reconfigurable ",
+ dummy.isPropertyReconfigurable(PROP2));
+ Assert.assertFalse(PROP3 + " should not be reconfigurable ",
+ dummy.isPropertyReconfigurable(PROP3));
+ Assert.assertTrue(PROP4 + " should be reconfigurable ",
+ dummy.isPropertyReconfigurable(PROP4));
+ Assert.assertFalse(PROP5 + " should not be reconfigurable ",
+ dummy.isPropertyReconfigurable(PROP5));
+
+ // change something to the same value as before
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP1, VAL1);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ Assert.assertEquals(PROP1 + " set to wrong value ", VAL1,
dummy.getProperties().get(PROP1));
+ } catch (ReconfigurationException | IOException | TimeoutException |
InterruptedException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertFalse("received unexpected exception",
+ exceptionCaught);
+ }
+
+ // change something to null
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP1, null);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ Assert.assertEquals(PROP1 + "set to wrong value ", DEFAULT,
+ dummy.getProperties().get(PROP1));
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertFalse("received unexpected exception",
+ exceptionCaught);
+ }
+
+ // change something to a different value than before
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP1, VAL2);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ Assert.assertEquals(PROP1 + "set to wrong value ", VAL2,
dummy.getProperties().get(PROP1));
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertFalse("received unexpected exception",
+ exceptionCaught);
+ }
+
+ // set unset property to null
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP4, null);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ Assert.assertSame(PROP4 + "set to wrong value ", DEFAULT,
dummy.getProperties().get(PROP4));
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertFalse("received unexpected exception",
+ exceptionCaught);
+ }
+
+ // set unset property
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP4, VAL1);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ Assert.assertEquals(PROP4 + "set to wrong value ", VAL1,
dummy.getProperties().get(PROP4));
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertFalse("received unexpected exception",
+ exceptionCaught);
+ }
+
+ // try to set unset property to null (not reconfigurable)
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP5, null);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertTrue("did not receive expected exception",
+ dummy.getReconfigurationStatus().getChanges()
+ .get(new PropertyChange(PROP5, DEFAULT, null))
+ .getMessage().contains("Property is not reconfigurable.") &&
!exceptionCaught);
+ }
+
+ // try to set unset property to value (not reconfigurable)
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP5, VAL1);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertTrue("did not receive expected exception",
+ dummy.getReconfigurationStatus().getChanges()
+ .get(new PropertyChange(PROP5, VAL1, null))
+ .getMessage().contains("Property is not reconfigurable.") &&
!exceptionCaught);
+ }
+
+ // try to change property to value (not reconfigurable)
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP3, VAL2);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertTrue("did not receive expected exception",
+ dummy.getReconfigurationStatus().getChanges()
+ .get(new PropertyChange(PROP3, VAL2, VAL1))
+ .getMessage().contains("Property is not reconfigurable.") &&
!exceptionCaught);
+ }
+
+ // try to change property to null (not reconfigurable)
+ {
+ boolean exceptionCaught = false;
+ try {
+ dummy.reconfigureProperty(PROP3, null);
+ dummy.startReconfiguration();
+ RaftTestUtil.waitFor(() -> dummy.getReconfigurationStatus().ended(),
100, 60000);
+ } catch (ReconfigurationException | IOException | InterruptedException |
TimeoutException e) {
+ exceptionCaught = true;
+ }
+ Assert.assertTrue("did not receive expected exception",
+ dummy.getReconfigurationStatus().getChanges()
+ .get(new PropertyChange(PROP3, DEFAULT, VAL1))
+ .getMessage().contains("Property is not reconfigurable.") &&
!exceptionCaught);
+ }
+ }
+
+ /**
+ * Test whether configuration changes are visible in another thread.
+ */
+ @Test
+ public void testThread() throws ReconfigurationException, IOException {
+ ReconfigurableDummy dummy = new ReconfigurableDummy(conf1);
+ Assert.assertEquals(VAL1, dummy.getProperties().get(PROP1));
+ Thread dummyThread = new Thread(dummy);
+ dummyThread.start();
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException ignore) {
+ // do nothing
+ }
+ dummy.reconfigureProperty(PROP1, VAL2);
+ dummy.startReconfiguration();
+
+ long endWait = System.currentTimeMillis() + 2000;
+ while (dummyThread.isAlive() && System.currentTimeMillis() < endWait) {
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException ignore) {
+ // do nothing
+ }
+ }
+
+ Assert.assertFalse("dummy thread should not be alive",
+ dummyThread.isAlive());
+ dummy.running = false;
+ try {
+ dummyThread.join();
+ } catch (InterruptedException ignore) {
+ // do nothing
+ }
+ Assert.assertTrue(PROP1 + " is set to wrong value",
+ dummy.getProperties().get(PROP1).equals(VAL2));
+
+ }
+
+ /**
+ * Ensure that {@link ReconfigurationBase#reconfigureProperty} updates the
+ * parent's cached configuration on success.
+ * @throws IOException
+ */
+ @Test (timeout=300000)
+ public void testConfIsUpdatedOnSuccess()
+ throws ReconfigurationException, IOException, InterruptedException,
TimeoutException {
+ final String property = "FOO";
+ final String value1 = "value1";
+ final String value2 = "value2";
+
+ final RaftProperties conf = new RaftProperties();
+ conf.set(property, value1);
+ final RaftProperties newConf = new RaftProperties();
+ newConf.set(property, value2);
+
+ final ReconfigurationBase reconfigurable = makeReconfigurable(
+ conf, newConf, Arrays.asList(property));
+
+ reconfigurable.reconfigureProperty(property, value2);
+ reconfigurable.startReconfiguration();
+ RaftTestUtil.waitFor(() ->
reconfigurable.getReconfigurationStatus().ended(), 100, 60000);
+ Assert.assertEquals(value2, reconfigurable.getProperties().get(property));
+ }
+
+ /**
+ * Ensure that {@link ReconfigurationBase#startReconfiguration} updates
+ * its parent's cached configuration on success.
+ * @throws IOException
+ */
+ @Test (timeout=300000)
+ public void testConfIsUpdatedOnSuccessAsync()
+ throws InterruptedException, IOException, TimeoutException {
+ final String property = "FOO";
+ final String value1 = "value1";
+ final String value2 = "value2";
+
+ final RaftProperties conf = new RaftProperties();
+ conf.set(property, value1);
+ final RaftProperties newConf = new RaftProperties();
+ newConf.set(property, value2);
+
+ final ReconfigurationBase reconfigurable = makeReconfigurable(
+ conf, newConf, Arrays.asList(property));
+
+ // Kick off a reconfiguration task and wait until it completes.
+ reconfigurable.startReconfiguration();
+
+ RaftTestUtil.waitFor(() ->
reconfigurable.getReconfigurationStatus().ended(), 100, 60000);
+ Assert.assertEquals(value2, reconfigurable.getProperties().get(property));
+ }
+
+ /**
+ * Ensure that {@link ReconfigurationBase#reconfigureProperty} unsets the
+ * property in its parent's configuration when the new value is null.
+ * @throws IOException
+ */
+ @Test (timeout=300000)
+ public void testConfIsUnset()
+ throws InterruptedException, TimeoutException, IOException {
+ final String property = "FOO";
+ final String value1 = "value1";
+
+ final RaftProperties conf = new RaftProperties();
+ conf.set(property, value1);
+ final RaftProperties newConf = new RaftProperties();
+
+ final ReconfigurationBase reconfigurable = makeReconfigurable(
+ conf, newConf, Arrays.asList(property));
+
+ reconfigurable.startReconfiguration();
+ RaftTestUtil.waitFor(() ->
reconfigurable.getReconfigurationStatus().ended(), 100, 60000);
+ Assert.assertNull(reconfigurable.getProperties().get(property));
+ }
+
+ /**
+ * Ensure that {@link ReconfigurationBase#startReconfiguration} unsets the
+ * property in its parent's configuration when the new value is null.
+ * @throws IOException
+ */
+ @Test (timeout=300000)
+ public void testConfIsUnsetAsync() throws ReconfigurationException,
+ IOException, TimeoutException, InterruptedException {
+ final String property = "FOO";
+ final String value1 = "value1";
+
+ final RaftProperties conf = new RaftProperties();
+ conf.set(property, value1);
+ final RaftProperties newConf = new RaftProperties();
+
+ final ReconfigurationBase reconfigurable = makeReconfigurable(
+ conf, newConf, Arrays.asList(property));
+
+ // Kick off a reconfiguration task and wait until it completes.
+ reconfigurable.startReconfiguration();
+ RaftTestUtil.waitFor(() ->
reconfigurable.getReconfigurationStatus().ended(), 100, 60000);
+ Assert.assertNull(reconfigurable.getProperties().get(property));
+ }
+
+ private ReconfigurationBase makeReconfigurable(
+ final RaftProperties oldProperties, final RaftProperties newProperties,
+ final Collection<String> reconfigurableProperties) {
+
+ return new ReconfigurationBase("tempReConfigDummy", oldProperties) {
+ @Override
+ protected RaftProperties getNewProperties() {
+ return newProperties;
+ }
+
+ @Override
+ public String reconfigureProperty(String property, String newValue) {
+ return newValue;
+ }
+
+ @Override
+ public Collection<String> getReconfigurableProperties() {
+ return reconfigurableProperties;
+ }
+ };
+ }
+}
diff --git
a/ratis-test/src/test/java/org/apache/ratis/grpc/TestReConfigPropertyWithGrpc.java
b/ratis-test/src/test/java/org/apache/ratis/grpc/TestReConfigPropertyWithGrpc.java
new file mode 100644
index 000000000..a57fb86ad
--- /dev/null
+++
b/ratis-test/src/test/java/org/apache/ratis/grpc/TestReConfigPropertyWithGrpc.java
@@ -0,0 +1,26 @@
+/*
+ * 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.ratis.grpc;
+
+
+import org.apache.ratis.TestReConfigProperty;
+
+public class TestReConfigPropertyWithGrpc extends
TestReConfigProperty<MiniRaftClusterWithGrpc>
+ implements MiniRaftClusterWithGrpc.FactoryGet{
+}