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{
+}


Reply via email to