/** * 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.hadoop.conf; import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.collect.Lists; import org.apache.hadoop.test.GenericTestUtils; import org.apache.hadoop.util.Time; import org.apache.hadoop.conf.ReconfigurationUtil.PropertyChange; import org.junit.Test; import org.junit.Before; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.*; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.spy; import java.io.IOException; import java.util.Collection; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeoutException; public class TestReconfiguration { private Configuration conf1; private Configuration 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"; @Before public void setUp () { conf1 = new Configuration(); conf2 = new Configuration(); // 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 ReconfigurationUtil.getChangedProperties. */ @Test public void testGetChangedProperties() { Collection<ReconfigurationUtil.PropertyChange> changes = ReconfigurationUtil.getChangedProperties(conf2, conf1); assertTrue("expected 3 changed properties but got " + changes.size(), changes.size() == 3); boolean changeFound = false; boolean unsetFound = false; boolean setFound = false; for (ReconfigurationUtil.PropertyChange c: changes) { if (c.prop.equals(PROP2) && c.oldVal != null && c.oldVal.equals(VAL1) && c.newVal != null && c.newVal.equals(VAL2)) { changeFound = true; } else if (c.prop.equals(PROP3) && c.oldVal != null && c.oldVal.equals(VAL1) && c.newVal == null) { unsetFound = true; } else if (c.prop.equals(PROP4) && c.oldVal == null && c.newVal != null && c.newVal.equals(VAL1)) { setFound = true; } } assertTrue("not all changes have been applied", changeFound && unsetFound && setFound); } /** * a simple reconfigurable class */ public static class ReconfigurableDummy extends ReconfigurableBase implements Runnable { public volatile boolean running = true; public ReconfigurableDummy(Configuration conf) { super(conf); } @Override protected Configuration getNewConf() { return new Configuration(); } @Override public Collection<String> getReconfigurableProperties() { return Arrays.asList(PROP1, PROP2, PROP4); } @Override public synchronized String reconfigurePropertyImpl( String property, String newVal) throws ReconfigurationException { // do nothing return newVal; } /** * Run until PROP1 is no longer VAL1. */ @Override public void run() { while (running && getConf().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); assertTrue(PROP1 + " set to wrong value ", dummy.getConf().get(PROP1).equals(VAL1)); assertTrue(PROP2 + " set to wrong value ", dummy.getConf().get(PROP2).equals(VAL1)); assertTrue(PROP3 + " set to wrong value ", dummy.getConf().get(PROP3).equals(VAL1)); assertTrue(PROP4 + " set to wrong value ", dummy.getConf().get(PROP4) == null); assertTrue(PROP5 + " set to wrong value ", dummy.getConf().get(PROP5) == null); assertTrue(PROP1 + " should be reconfigurable ", dummy.isPropertyReconfigurable(PROP1)); assertTrue(PROP2 + " should be reconfigurable ", dummy.isPropertyReconfigurable(PROP2)); assertFalse(PROP3 + " should not be reconfigurable ", dummy.isPropertyReconfigurable(PROP3)); assertTrue(PROP4 + " should be reconfigurable ", dummy.isPropertyReconfigurable(PROP4)); 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); assertTrue(PROP1 + " set to wrong value ", dummy.getConf().get(PROP1).equals(VAL1)); } catch (ReconfigurationException e) { exceptionCaught = true; } assertFalse("received unexpected exception", exceptionCaught); } // change something to null { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP1, null); assertTrue(PROP1 + "set to wrong value ", dummy.getConf().get(PROP1) == null); } catch (ReconfigurationException e) { exceptionCaught = true; } assertFalse("received unexpected exception", exceptionCaught); } // change something to a different value than before { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP1, VAL2); assertTrue(PROP1 + "set to wrong value ", dummy.getConf().get(PROP1).equals(VAL2)); } catch (ReconfigurationException e) { exceptionCaught = true; } assertFalse("received unexpected exception", exceptionCaught); } // set unset property to null { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP4, null); assertTrue(PROP4 + "set to wrong value ", dummy.getConf().get(PROP4) == null); } catch (ReconfigurationException e) { exceptionCaught = true; } assertFalse("received unexpected exception", exceptionCaught); } // set unset property { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP4, VAL1); assertTrue(PROP4 + "set to wrong value ", dummy.getConf().get(PROP4).equals(VAL1)); } catch (ReconfigurationException e) { exceptionCaught = true; } assertFalse("received unexpected exception", exceptionCaught); } // try to set unset property to null (not reconfigurable) { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP5, null); } catch (ReconfigurationException e) { exceptionCaught = true; } assertTrue("did not receive expected exception", exceptionCaught); } // try to set unset property to value (not reconfigurable) { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP5, VAL1); } catch (ReconfigurationException e) { exceptionCaught = true; } assertTrue("did not receive expected exception", exceptionCaught); } // try to change property to value (not reconfigurable) { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP3, VAL2); } catch (ReconfigurationException e) { exceptionCaught = true; } assertTrue("did not receive expected exception", exceptionCaught); } // try to change property to null (not reconfigurable) { boolean exceptionCaught = false; try { dummy.reconfigureProperty(PROP3, null); } catch (ReconfigurationException e) { exceptionCaught = true; } assertTrue("did not receive expected exception", exceptionCaught); } } /** * Test whether configuration changes are visible in another thread. */ @Test public void testThread() throws ReconfigurationException { ReconfigurableDummy dummy = new ReconfigurableDummy(conf1); assertTrue(dummy.getConf().get(PROP1).equals(VAL1)); Thread dummyThread = new Thread(dummy); dummyThread.start(); try { Thread.sleep(500); } catch (InterruptedException ignore) { // do nothing } dummy.reconfigureProperty(PROP1, VAL2); long endWait = Time.now() + 2000; while (dummyThread.isAlive() && Time.now() < endWait) { try { Thread.sleep(50); } catch (InterruptedException ignore) { // do nothing } } assertFalse("dummy thread should not be alive", dummyThread.isAlive()); dummy.running = false; try { dummyThread.join(); } catch (InterruptedException ignore) { // do nothing } assertTrue(PROP1 + " is set to wrong value", dummy.getConf().get(PROP1).equals(VAL2)); } private static class AsyncReconfigurableDummy extends ReconfigurableBase { AsyncReconfigurableDummy(Configuration conf) { super(conf); } @Override protected Configuration getNewConf() { return new Configuration(); } final CountDownLatch latch = new CountDownLatch(1); @Override public Collection<String> getReconfigurableProperties() { return Arrays.asList(PROP1, PROP2, PROP4); } @Override public synchronized String reconfigurePropertyImpl(String property, String newVal) throws ReconfigurationException { try { latch.await(); } catch (InterruptedException e) { // Ignore } return newVal; } } private static void waitAsyncReconfigureTaskFinish(ReconfigurableBase rb) throws InterruptedException { ReconfigurationTaskStatus status = null; int count = 20; while (count > 0) { status = rb.getReconfigurationTaskStatus(); if (status.stopped()) { break; } count--; Thread.sleep(500); } assert(status.stopped()); } @Test public void testAsyncReconfigure() throws ReconfigurationException, IOException, InterruptedException { AsyncReconfigurableDummy dummy = spy(new AsyncReconfigurableDummy(conf1)); List<PropertyChange> changes = Lists.newArrayList(); changes.add(new PropertyChange("name1", "new1", "old1")); changes.add(new PropertyChange("name2", "new2", "old2")); changes.add(new PropertyChange("name3", "new3", "old3")); doReturn(changes).when(dummy).getChangedProperties( any(Configuration.class), any(Configuration.class)); doReturn(true).when(dummy).isPropertyReconfigurable(eq("name1")); doReturn(false).when(dummy).isPropertyReconfigurable(eq("name2")); doReturn(true).when(dummy).isPropertyReconfigurable(eq("name3")); doReturn("dummy").when(dummy) .reconfigurePropertyImpl(eq("name1"), anyString()); doReturn("dummy").when(dummy) .reconfigurePropertyImpl(eq("name2"), anyString()); doThrow(new ReconfigurationException("NAME3", "NEW3", "OLD3", new IOException("io exception"))) .when(dummy).reconfigurePropertyImpl(eq("name3"), anyString()); dummy.startReconfigurationTask(); waitAsyncReconfigureTaskFinish(dummy); ReconfigurationTaskStatus status = dummy.getReconfigurationTaskStatus(); assertEquals(2, status.getStatus().size()); for (Map.Entry<PropertyChange, Optional<String>> result : status.getStatus().entrySet()) { PropertyChange change = result.getKey(); if (change.prop.equals("name1")) { assertFalse(result.getValue().isPresent()); } else if (change.prop.equals("name2")) { assertThat(result.getValue().get(), containsString("Property name2 is not reconfigurable")); } else if (change.prop.equals("name3")) { assertThat(result.getValue().get(), containsString("io exception")); } else { fail("Unknown property: " + change.prop); } } } @Test(timeout=30000) public void testStartReconfigurationFailureDueToExistingRunningTask() throws InterruptedException, IOException { AsyncReconfigurableDummy dummy = spy(new AsyncReconfigurableDummy(conf1)); List<PropertyChange> changes = Lists.newArrayList( new PropertyChange(PROP1, "new1", "old1") ); doReturn(changes).when(dummy).getChangedProperties( any(Configuration.class), any(Configuration.class)); ReconfigurationTaskStatus status = dummy.getReconfigurationTaskStatus(); assertFalse(status.hasTask()); dummy.startReconfigurationTask(); status = dummy.getReconfigurationTaskStatus(); assertTrue(status.hasTask()); assertFalse(status.stopped()); // An active reconfiguration task is running. try { dummy.startReconfigurationTask(); fail("Expect to throw IOException."); } catch (IOException e) { GenericTestUtils.assertExceptionContains( "Another reconfiguration task is running", e); } status = dummy.getReconfigurationTaskStatus(); assertTrue(status.hasTask()); assertFalse(status.stopped()); dummy.latch.countDown(); waitAsyncReconfigureTaskFinish(dummy); status = dummy.getReconfigurationTaskStatus(); assertTrue(status.hasTask()); assertTrue(status.stopped()); // The first task has finished. dummy.startReconfigurationTask(); waitAsyncReconfigureTaskFinish(dummy); ReconfigurationTaskStatus status2 = dummy.getReconfigurationTaskStatus(); assertTrue(status2.getStartTime() >= status.getEndTime()); dummy.shutdownReconfigurationTask(); try { dummy.startReconfigurationTask(); fail("Expect to throw IOException"); } catch (IOException e) { GenericTestUtils.assertExceptionContains("The server is stopped", e); } } /** * Ensure that {@link ReconfigurableBase#reconfigureProperty} updates the * parent's cached configuration on success. * @throws IOException */ @Test (timeout=300000) public void testConfIsUpdatedOnSuccess() throws ReconfigurationException { final String property = "FOO"; final String value1 = "value1"; final String value2 = "value2"; final Configuration conf = new Configuration(); conf.set(property, value1); final Configuration newConf = new Configuration(); newConf.set(property, value2); final ReconfigurableBase reconfigurable = makeReconfigurable( conf, newConf, Arrays.asList(property)); reconfigurable.reconfigureProperty(property, value2); assertThat(reconfigurable.getConf().get(property), is(value2)); } /** * Ensure that {@link ReconfigurableBase#startReconfigurationTask} updates * its parent's cached configuration on success. * @throws IOException */ @Test (timeout=300000) public void testConfIsUpdatedOnSuccessAsync() throws ReconfigurationException, TimeoutException, InterruptedException, IOException { final String property = "FOO"; final String value1 = "value1"; final String value2 = "value2"; final Configuration conf = new Configuration(); conf.set(property, value1); final Configuration newConf = new Configuration(); newConf.set(property, value2); final ReconfigurableBase reconfigurable = makeReconfigurable( conf, newConf, Arrays.asList(property)); // Kick off a reconfiguration task and wait until it completes. reconfigurable.startReconfigurationTask(); GenericTestUtils.waitFor(new Supplier<Boolean>() { @Override public Boolean get() { return reconfigurable.getReconfigurationTaskStatus().stopped(); } }, 100, 60000); assertThat(reconfigurable.getConf().get(property), is(value2)); } /** * Ensure that {@link ReconfigurableBase#reconfigureProperty} unsets the * property in its parent's configuration when the new value is null. * @throws IOException */ @Test (timeout=300000) public void testConfIsUnset() throws ReconfigurationException { final String property = "FOO"; final String value1 = "value1"; final Configuration conf = new Configuration(); conf.set(property, value1); final Configuration newConf = new Configuration(); final ReconfigurableBase reconfigurable = makeReconfigurable( conf, newConf, Arrays.asList(property)); reconfigurable.reconfigureProperty(property, null); assertNull(reconfigurable.getConf().get(property)); } /** * Ensure that {@link ReconfigurableBase#startReconfigurationTask} 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 Configuration conf = new Configuration(); conf.set(property, value1); final Configuration newConf = new Configuration(); final ReconfigurableBase reconfigurable = makeReconfigurable( conf, newConf, Arrays.asList(property)); // Kick off a reconfiguration task and wait until it completes. reconfigurable.startReconfigurationTask(); GenericTestUtils.waitFor(new Supplier<Boolean>() { @Override public Boolean get() { return reconfigurable.getReconfigurationTaskStatus().stopped(); } }, 100, 60000); assertNull(reconfigurable.getConf().get(property)); } private ReconfigurableBase makeReconfigurable( final Configuration oldConf, final Configuration newConf, final Collection<String> reconfigurableProperties) { return new ReconfigurableBase(oldConf) { @Override protected Configuration getNewConf() { return newConf; } @Override public Collection<String> getReconfigurableProperties() { return reconfigurableProperties; } @Override protected String reconfigurePropertyImpl( String property, String newVal) throws ReconfigurationException { return newVal; } }; } }