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 b48f79f99c..f51bc731f7 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.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 void clear() { properties.clear(); } + /** @return the property entry set. */ + Set> 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 0000000000..6e8a527da5 --- /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. + *

+ * 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 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 0000000000..ea6ba225e4 --- /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 getChangedProperties( + RaftProperties newProperties, RaftProperties oldProperties) { + final Map changes = new HashMap<>(); + + // iterate over old properties + for (Map.Entry 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 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 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 changes = getChangedProperties(getNewProperties(), properties); + final Map 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 0000000000..15c8c82254 --- /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 0000000000..c584fe068a --- /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? "": "\"" + 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 changes; + /** The daemon to run the reconfiguration. */ + private final Daemon daemon; + + ReconfigurationStatus(Timestamp startTime, Timestamp endTime, Map 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 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 0000000000..4535406a77 --- /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 extends BaseTest + implements MiniRaftCluster.Factory.Get { + + { + 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 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 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 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 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 0000000000..a57fb86ad5 --- /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 + implements MiniRaftClusterWithGrpc.FactoryGet{ +}