diff options
Diffstat (limited to 'src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java')
-rw-r--r-- | src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java new file mode 100644 index 000000000..ef9bf2561 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/EventCountCircuitBreakerTest.java @@ -0,0 +1,414 @@ +/* + * 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.commons.lang3.concurrent; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.AbstractLangTest; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@code EventCountCircuitBreaker}. + */ +public class EventCountCircuitBreakerTest extends AbstractLangTest { + /** Constant for the opening threshold. */ + private static final int OPENING_THRESHOLD = 10; + + /** Constant for the closing threshold. */ + private static final int CLOSING_THRESHOLD = 5; + + /** Constant for the factor for converting nanoseconds. */ + private static final long NANO_FACTOR = 1000L * 1000L * 1000L; + + /** + * Tests that time units are correctly taken into account by constructors. + */ + @Test + public void testIntervalCalculation() { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 2, TimeUnit.MILLISECONDS); + assertEquals(NANO_FACTOR, breaker.getOpeningInterval(), "Wrong opening interval"); + assertEquals(2 * NANO_FACTOR / 1000, breaker.getClosingInterval(), "Wrong closing interval"); + } + + /** + * Tests that the closing interval is the same as the opening interval if it is not + * specified. + */ + @Test + public void testDefaultClosingInterval() { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS, CLOSING_THRESHOLD); + assertEquals(NANO_FACTOR, breaker.getClosingInterval(), "Wrong closing interval"); + } + + /** + * Tests that the closing threshold is the same as the opening threshold if not + * specified otherwise. + */ + @Test + public void testDefaultClosingThreshold() { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS); + assertEquals(NANO_FACTOR, breaker.getClosingInterval(), "Wrong closing interval"); + assertEquals(OPENING_THRESHOLD, breaker.getClosingThreshold(), "Wrong closing threshold"); + } + + /** + * Tests that a circuit breaker is closed after its creation. + */ + @Test + public void testInitiallyClosed() { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS); + assertFalse(breaker.isOpen(), "Open"); + assertTrue(breaker.isClosed(), "Not closed"); + } + + /** + * Tests whether the current time is correctly determined. + */ + @Test + public void testNow() { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS); + final long nowNanos = breaker.nanoTime(); + final long deltaNanos = Math.abs(System.nanoTime() - nowNanos); + assertTrue(deltaNanos < 100_000, String.format("Delta %,d ns to current time too large", deltaNanos)); + } + + /** + * Tests that the circuit breaker stays closed if the number of received events stays + * below the threshold. + */ + @Test + public void testNotOpeningUnderThreshold() { + long startTime = 1000; + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + for (int i = 0; i < OPENING_THRESHOLD - 1; i++) { + assertTrue(breaker.at(startTime).incrementAndCheckState(), "In open state"); + startTime++; + } + assertTrue(breaker.isClosed(), "Not closed"); + } + + /** + * Tests that the circuit breaker stays closed if there are a number of received + * events, but not in a single check interval. + */ + @Test + public void testNotOpeningCheckIntervalExceeded() { + long startTime = 0L; + final long timeIncrement = 3 * NANO_FACTOR / (2 * OPENING_THRESHOLD); + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + for (int i = 0; i < 5 * OPENING_THRESHOLD; i++) { + assertTrue(breaker.at(startTime).incrementAndCheckState(), "In open state"); + startTime += timeIncrement; + } + assertTrue(breaker.isClosed(), "Not closed"); + } + + /** + * Tests that the circuit breaker opens if all conditions are met. + */ + @Test + public void testOpeningWhenThresholdReached() { + long startTime = 0; + final long timeIncrement = NANO_FACTOR / OPENING_THRESHOLD - 1; + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + boolean open = false; + for (int i = 0; i < OPENING_THRESHOLD + 1; i++) { + open = !breaker.at(startTime).incrementAndCheckState(); + startTime += timeIncrement; + } + assertTrue(open, "Not open"); + assertFalse(breaker.isClosed(), "Closed"); + } + + /** + * Tests that the circuit breaker opens if all conditions are met when using + * {@link EventCountCircuitBreaker#incrementAndCheckState(Integer increment)}. + */ + @Test + public void testOpeningWhenThresholdReachedThroughBatch() { + final long timeIncrement = NANO_FACTOR / OPENING_THRESHOLD - 1; + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + final long startTime = timeIncrement * (OPENING_THRESHOLD + 1); + final boolean open = !breaker.at(startTime).incrementAndCheckState(OPENING_THRESHOLD + 1); + assertTrue(open, "Not open"); + assertFalse(breaker.isClosed(), "Closed"); + } + + /** + * Tests that an open circuit breaker does not close itself when the number of events + * received is over the threshold. + */ + @Test + public void testNotClosingOverThreshold() { + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, + 10, TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + long startTime = 0; + breaker.open(); + for (int i = 0; i <= CLOSING_THRESHOLD; i++) { + assertFalse(breaker.at(startTime).incrementAndCheckState(), "Not open"); + startTime += 1000; + } + assertFalse(breaker.at(startTime + NANO_FACTOR).incrementAndCheckState(), "Closed in new interval"); + assertTrue(breaker.isOpen(), "Not open at end"); + } + + /** + * Tests that the circuit breaker closes automatically if the number of events + * received goes under the closing threshold. + */ + @Test + public void testClosingWhenThresholdReached() { + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, + 10, TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + breaker.open(); + breaker.at(1000).incrementAndCheckState(); + assertFalse(breaker.at(2000).checkState(), "Already closed"); + assertFalse(breaker.at(NANO_FACTOR).checkState(), "Closed at interval end"); + assertTrue(breaker.at(NANO_FACTOR + 1).checkState(), "Not closed after interval end"); + assertTrue(breaker.isClosed(), "Not closed at end"); + } + + /** + * Tests whether an explicit open operation fully initializes the internal check data + * object. Otherwise, the circuit breaker may close itself directly afterwards. + */ + @Test + public void testOpenStartsNewCheckInterval() { + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + breaker.at(NANO_FACTOR - 1000).open(); + assertTrue(breaker.isOpen(), "Not open"); + assertFalse(breaker.at(NANO_FACTOR + 100).checkState(), "Already closed"); + } + + /** + * Tests whether a new check interval is started if the circuit breaker has a + * transition to open state. + */ + @Test + public void testAutomaticOpenStartsNewCheckInterval() { + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + long time = 10 * NANO_FACTOR; + for (int i = 0; i <= OPENING_THRESHOLD; i++) { + breaker.at(time++).incrementAndCheckState(); + } + assertTrue(breaker.isOpen(), "Not open"); + time += NANO_FACTOR - 1000; + assertFalse(breaker.at(time).incrementAndCheckState(), "Already closed"); + time += 1001; + assertTrue(breaker.at(time).checkState(), "Not closed in time interval"); + } + + /** + * Tests whether the circuit breaker can be closed explicitly. + */ + @Test + public void testClose() { + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + long time = 0; + for (int i = 0; i <= OPENING_THRESHOLD; i++, time += 1000) { + breaker.at(time).incrementAndCheckState(); + } + assertTrue(breaker.isOpen(), "Not open"); + breaker.close(); + assertTrue(breaker.isClosed(), "Not closed"); + assertTrue(breaker.at(time + 1000).incrementAndCheckState(), "Open again"); + } + + /** + * Tests whether events are generated when the state is changed. + */ + @Test + public void testChangeEvents() { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS); + final ChangeListener listener = new ChangeListener(breaker); + breaker.addChangeListener(listener); + breaker.open(); + breaker.close(); + listener.verify(Boolean.TRUE, Boolean.FALSE); + } + + /** + * Tests whether a change listener can be removed. + */ + @Test + public void testRemoveChangeListener() { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS); + final ChangeListener listener = new ChangeListener(breaker); + breaker.addChangeListener(listener); + breaker.open(); + breaker.removeChangeListener(listener); + breaker.close(); + listener.verify(Boolean.TRUE); + } + + /** + * Tests that a state transition triggered by multiple threads is handled correctly. + * Only the first transition should cause an event to be sent. + */ + @Test + public void testStateTransitionGuarded() throws InterruptedException { + final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1, + TimeUnit.SECONDS); + final ChangeListener listener = new ChangeListener(breaker); + breaker.addChangeListener(listener); + + final int threadCount = 128; + final CountDownLatch latch = new CountDownLatch(1); + final Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread() { + @Override + public void run() { + try { + latch.await(); + } catch (final InterruptedException iex) { + // ignore + } + breaker.open(); + } + }; + threads[i].start(); + } + latch.countDown(); + for (final Thread thread : threads) { + thread.join(); + } + listener.verify(Boolean.TRUE); + } + + /** + * Tests that automatic state transitions generate change events as well. + */ + @Test + public void testChangeEventsGeneratedByAutomaticTransitions() { + final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2, + TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS); + final ChangeListener listener = new ChangeListener(breaker); + breaker.addChangeListener(listener); + long time = 0; + for (int i = 0; i <= OPENING_THRESHOLD; i++, time += 1000) { + breaker.at(time).incrementAndCheckState(); + } + breaker.at(NANO_FACTOR + 1).checkState(); + breaker.at(3 * NANO_FACTOR).checkState(); + listener.verify(Boolean.TRUE, Boolean.FALSE); + } + + /** + * A test implementation of {@code EventCountCircuitBreaker} which supports mocking the timer. + * This is useful for the creation of deterministic tests for switching the circuit + * breaker's state. + */ + private static class EventCountCircuitBreakerTestImpl extends EventCountCircuitBreaker { + /** The current time in nanoseconds. */ + private long currentTime; + + EventCountCircuitBreakerTestImpl(final int openingThreshold, final long openingInterval, + final TimeUnit openingUnit, final int closingThreshold, final long closingInterval, + final TimeUnit closingUnit) { + super(openingThreshold, openingInterval, openingUnit, closingThreshold, + closingInterval, closingUnit); + } + + /** + * Sets the current time to be used by this test object for the next operation. + * + * @param time the time to set + * @return a reference to this object + */ + public EventCountCircuitBreakerTestImpl at(final long time) { + currentTime = time; + return this; + } + + /** + * {@inheritDoc} This implementation returns the value passed to the {@code at()} + * method. + */ + @Override + long nanoTime() { + return currentTime; + } + } + + /** + * A test change listener for checking whether correct change events are generated. + */ + private static class ChangeListener implements PropertyChangeListener { + /** The expected event source. */ + private final Object expectedSource; + + /** A list with the updated values extracted from received change events. */ + private final List<Boolean> changedValues; + + /** + * Creates a new instance of {@code ChangeListener} and sets the expected event + * source. + * + * @param source the expected event source + */ + ChangeListener(final Object source) { + expectedSource = source; + changedValues = new ArrayList<>(); + } + + @Override + public void propertyChange(final PropertyChangeEvent evt) { + assertEquals(expectedSource, evt.getSource(), "Wrong event source"); + assertEquals("open", evt.getPropertyName(), "Wrong property name"); + final Boolean newValue = (Boolean) evt.getNewValue(); + final Boolean oldValue = (Boolean) evt.getOldValue(); + assertNotEquals(newValue, oldValue, "Old and new value are equal"); + changedValues.add(newValue); + } + + /** + * Verifies that change events for the expected values have been received. + * + * @param values the expected values + */ + public void verify(final Boolean... values) { + assertArrayEquals(values, changedValues.toArray(ArrayUtils.EMPTY_BOOLEAN_OBJECT_ARRAY)); + } + } +} |