blob: 1a3e2cf4961c9c2a894b58a42f6c895b937c0897 [file] [log] [blame]
/*
* 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.junit.jupiter.api.Test;
/**
* Test class for {@code EventCountCircuitBreaker}.
*/
public class EventCountCircuitBreakerTest {
/** 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(new Boolean[0]));
}
}
}