diff --git a/README.adoc b/README.adoc index fda6e9e076..25e4f859d2 100644 --- a/README.adoc +++ b/README.adoc @@ -240,7 +240,7 @@ static Supplier decorateSupplier(Supplier supplier, CircuitBreaker cir The state of the CircuitBreaker changes from `CLOSED` to `OPEN` when the failure rate is above a (configurable) threshold. Then, all access to the backend is blocked for a (configurable) time duration. `CircuitBreaker::isCallPermitted()` throws a `CircuitBreakerOpenException`, if the CircuitBreaker is `OPEN`. -The CircuitBreaker uses a Ring Bit Buffer in the `CLOSED` state to store the success or failure statuses of the calls. A successful call is stored as a `0` bit and a failed call is stored as a `1` bit. The Ring Bit Buffer has a (configurable) fixed-size. The Ring Bit Buffer uses internally a https://docs.oracle.com/javase/8/docs/api/java/util/BitSet.html[BitSet] to store the bits which is saving memory compared to a boolean array. The BitSet uses a long[] array to store the bits. That means the BitSet only needs an array of 16 long (64-bit) values to store the status of 1024 calls. +The CircuitBreaker uses a Ring Bit Buffer in the `CLOSED` state to store the success or failure statuses of the calls. A successful call is stored as a `0` bit and a failed call is stored as a `1` bit. The Ring Bit Buffer has a (configurable) fixed-size. The Ring Bit Buffer uses internally a https://docs.oracle.com/javase/8/docs/api/java/util/BitSet.html[BitSet] like data structure to store the bits which is saving memory compared to a boolean array. The BitSet uses a long[] array to store the bits. That means the BitSet only needs an array of 16 long (64-bit) values to store the status of 1024 calls. image::src/docs/asciidoc/images/ring_buffer.jpg[Ring Bit Buffer] @@ -279,7 +279,7 @@ So you can easily restrict not only network calls but your local in-memory opera == License -Copyright 2016 Robert Winkler +Copyright 2016 Robert Winkler and Bohdan Storozhuk Licensed 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 diff --git a/build.gradle b/build.gradle index 864a4728b5..f75b85ea81 100644 --- a/build.gradle +++ b/build.gradle @@ -105,12 +105,6 @@ artifacts { cobertura { coverageFormats = ['html', 'xml'] -// afterEvaluate { -// classDirectories = files(classDirectories.files.collect { -// fileTree(dir: it, -// exclude: ['**/**Benchmark**']) -// }) -// } } tasks.coveralls { diff --git a/src/docs/asciidoc/usage_guide.adoc b/src/docs/asciidoc/usage_guide.adoc index 96c0e5883f..0b8d33cb04 100644 --- a/src/docs/asciidoc/usage_guide.adoc +++ b/src/docs/asciidoc/usage_guide.adoc @@ -342,7 +342,7 @@ static Supplier decorateSupplier(Supplier supplier, CircuitBreaker cir The state of the CircuitBreaker changes from `CLOSED` to `OPEN` when the failure rate is above a (configurable) threshold. Then, all access to the backend is blocked for a (configurable) time duration. `CircuitBreaker::isCallPermitted()` throws a `CircuitBreakerOpenException`, if the CircuitBreaker is `OPEN`. -The CircuitBreaker uses a Ring Bit Buffer in the `CLOSED` state to store the success or failure statuses of the calls. A successful call is stored as a `0` bit and a failed call is stored as a `1` bit. The Ring Bit Buffer has a (configurable) fixed-size. The Ring Bit Buffer uses internally a https://docs.oracle.com/javase/8/docs/api/java/util/BitSet.html[BitSet] to store the bits which is saving memory compared to a boolean array. The BitSet uses a long[] array to store the bits. That means the BitSet only needs an array of 16 long (64-bit) values to store the status of 1024 calls. +The CircuitBreaker uses a Ring Bit Buffer in the `CLOSED` state to store the success or failure statuses of the calls. A successful call is stored as a `0` bit and a failed call is stored as a `1` bit. The Ring Bit Buffer has a (configurable) fixed-size. The Ring Bit Buffer uses internally a https://docs.oracle.com/javase/8/docs/api/java/util/BitSet.html[BitSet] like data structure to store the bits which is saving memory compared to a boolean array. The BitSet uses a long[] array to store the bits. That means the BitSet only needs an array of 16 long (64-bit) values to store the status of 1024 calls. image::images/ring_buffer.jpg[Ring Bit Buffer] diff --git a/src/jmh/java/io/github/robwin/circuitbreaker/CircularBufferBenchmark.java b/src/jmh/java/io/github/robwin/circuitbreaker/CircularBufferBenchmark.java deleted file mode 100644 index 379cf4331c..0000000000 --- a/src/jmh/java/io/github/robwin/circuitbreaker/CircularBufferBenchmark.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * - * Copyright 2016 Robert Winkler and Bohdan Storozhuk - * - * Licensed 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 io.github.robwin.circuitbreaker; - -import io.github.robwin.circularbuffer.CircularFifoBuffer; -import io.github.robwin.circularbuffer.ConcurrentCircularFifoBuffer; -import javaslang.collection.List; -import javaslang.control.Option; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Group; -import org.openjdk.jmh.annotations.GroupThreads; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; - -import java.util.concurrent.TimeUnit; - -/** - * @author bstorozhuk - */ -@State(Scope.Benchmark) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@BenchmarkMode(Mode.AverageTime) -public class CircularBufferBenchmark { - public static final int FORK_COUNT = 2; - private static final int WARMUP_COUNT = 10; - private static final int ITERATION_COUNT = 10; - private static final int CAPACITY = 10; - private CircularFifoBuffer circularFifoBuffer; - private Object event; - - @Setup - public void setUp() { - event = new Object(); - circularFifoBuffer = new ConcurrentCircularFifoBuffer<>(CAPACITY); - } - - @Benchmark - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - @Group("circularBuffer") - @GroupThreads(1) - public void circularBufferAddEvent() { - circularFifoBuffer.add(event); - } - - @Benchmark - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - @Group("circularBuffer") - @GroupThreads(1) - public void circularBufferToList(Blackhole bh) { - List events = circularFifoBuffer.toList(); - bh.consume(events); - } - - @Benchmark - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - @Group("circularBuffer") - @GroupThreads(1) - public void circularBufferSize(Blackhole bh) { - int size = circularFifoBuffer.size(); - bh.consume(size); - } - - @Benchmark - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - @Group("circularBuffer") - @GroupThreads(1) - public void circularBufferTakeEvent(Blackhole bh) { - Option event = circularFifoBuffer.take(); - bh.consume(event); - } -} diff --git a/src/jmh/java/io/github/robwin/circuitbreaker/RingBitSetBenchmark.java b/src/jmh/java/io/github/robwin/circuitbreaker/RingBitSetBenchmark.java index 0694cbd8af..7036358697 100644 --- a/src/jmh/java/io/github/robwin/circuitbreaker/RingBitSetBenchmark.java +++ b/src/jmh/java/io/github/robwin/circuitbreaker/RingBitSetBenchmark.java @@ -1,6 +1,6 @@ /* * - * Copyright 2016 Robert Winkler + * Copyright 2016 Robert Winkler and Bohdan Storozhuk * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,24 +19,38 @@ package io.github.robwin.circuitbreaker; import io.github.robwin.circuitbreaker.internal.RingBitSet; -import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Group; +import org.openjdk.jmh.annotations.GroupThreads; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; import java.util.concurrent.TimeUnit; @State(Scope.Benchmark) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.All) public class RingBitSetBenchmark { - private RingBitSet ringBitSet; + private static final int CAPACITY = 1000; private static final int ITERATION_COUNT = 10; private static final int WARMUP_COUNT = 10; - private static final int THREAD_COUNT = 10; - private static final int FORK_COUNT = 1; + private static final int THREAD_COUNT = 2; + private static final int FORK_COUNT = 2; + + private RingBitSet ringBitSet; @Setup public void setUp() { - ringBitSet = new RingBitSet(1000); + ringBitSet = new RingBitSet(CAPACITY); } @Benchmark @@ -45,18 +59,21 @@ public void setUp() { @GroupThreads(THREAD_COUNT) @Warmup(iterations = WARMUP_COUNT) @Measurement(iterations = ITERATION_COUNT) - public void setBits(){ - ringBitSet.setNextBit(true); - ringBitSet.setNextBit(false); + public void concurrentSetBits(Blackhole bh) { + int firstCardinality = ringBitSet.setNextBit(true); + bh.consume(firstCardinality); + int secondCardinality = ringBitSet.setNextBit(false); + bh.consume(secondCardinality); } @Benchmark @Fork(value = FORK_COUNT) @Group("ringBitSet") - @GroupThreads(THREAD_COUNT) + @GroupThreads(1) @Warmup(iterations = WARMUP_COUNT) @Measurement(iterations = ITERATION_COUNT) - public int cardinality(){ - return ringBitSet.cardinality(); + public void concurrentCardinality(Blackhole bh) { + int cardinality = ringBitSet.cardinality(); + bh.consume(cardinality); } } diff --git a/src/main/java/io/github/robwin/circuitbreaker/internal/BitSetMod.java b/src/main/java/io/github/robwin/circuitbreaker/internal/BitSetMod.java new file mode 100644 index 0000000000..baac728cd1 --- /dev/null +++ b/src/main/java/io/github/robwin/circuitbreaker/internal/BitSetMod.java @@ -0,0 +1,68 @@ +/* + * + * Copyright 2016 Robert Winkler and Bohdan Storozhuk + * + * Licensed 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 io.github.robwin.circuitbreaker.internal; + +/** + * {@link BitSetMod} is simplified version of {@link java.util.BitSet}. + * It has no dynamic allocation, expanding logic, boundary checks + * and it's set method returns previous bit state. + */ +class BitSetMod { + + private final static int ADDRESS_BITS_PER_WORD = 6; + private final int size; + private final long[] words; + + + BitSetMod(final int capacity) { + int countOfWordsRequired = wordIndex(capacity - 1) + 1; + size = countOfWordsRequired << ADDRESS_BITS_PER_WORD; + words = new long[countOfWordsRequired]; + } + + /** + * Given a bit index, return word index containing it. + */ + private static int wordIndex(int bitIndex) { + return bitIndex >> ADDRESS_BITS_PER_WORD; + } + + /** + * Sets the bit at the specified index to value. + * + * @param bitIndex a bit index + * @return previous state of bitIndex that can be {@code 1} or {@code 0} + * @throws IndexOutOfBoundsException if the specified index is negative + */ + int set(int bitIndex, boolean value) { + int wordIndex = wordIndex(bitIndex); + long bitMask = 1L << bitIndex; + int previous = (words[wordIndex] & bitMask) != 0 ? 1 : 0; + if (value) { + words[wordIndex] |= bitMask; + } else { + words[wordIndex] &= ~bitMask; + } + return previous; + } + + int size() { + return size; + } +} diff --git a/src/main/java/io/github/robwin/circuitbreaker/internal/CircuitBreakerMetrics.java b/src/main/java/io/github/robwin/circuitbreaker/internal/CircuitBreakerMetrics.java index 1a97103bc9..575353add1 100644 --- a/src/main/java/io/github/robwin/circuitbreaker/internal/CircuitBreakerMetrics.java +++ b/src/main/java/io/github/robwin/circuitbreaker/internal/CircuitBreakerMetrics.java @@ -1,6 +1,6 @@ /* * - * Copyright 2016 Robert Winkler + * Copyright 2016 Robert Winkler and Bohdan Storozhuk * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,16 +23,12 @@ class CircuitBreakerMetrics implements CircuitBreaker.Metrics { + private final int ringBufferSize; private final RingBitSet ringBitSet; - /** - * Maximum number of buffered calls - */ - private int maxNumberOfBufferedCalls; - CircuitBreakerMetrics(int ringBufferSize) { - this.ringBitSet = new RingBitSet(ringBufferSize); - this.maxNumberOfBufferedCalls = ringBufferSize; + this.ringBufferSize = ringBufferSize; + this.ringBitSet = new RingBitSet(this.ringBufferSize); } /** @@ -40,9 +36,9 @@ class CircuitBreakerMetrics implements CircuitBreaker.Metrics { * * @return the current failure rate in percentage. */ - synchronized float onError(){ - ringBitSet.setNextBit(true); - return getFailureRate(); + float onError() { + int currentNumberOfFailedCalls = ringBitSet.setNextBit(true); + return getFailureRate(currentNumberOfFailedCalls); } /** @@ -50,39 +46,56 @@ synchronized float onError(){ * * @return the current failure rate in percentage. */ - synchronized float onSuccess(){ - ringBitSet.setNextBit(false); - return getFailureRate(); + float onSuccess() { + int currentNumberOfFailedCalls = ringBitSet.setNextBit(false); + return getFailureRate(currentNumberOfFailedCalls); } + /** + * {@inheritDoc} + */ @Override - public synchronized float getFailureRate(){ - int numOfMeasuredCalls = getNumberOfBufferedCalls(); - if(numOfMeasuredCalls == maxNumberOfBufferedCalls){ - return getNumberOfFailedCalls() * 100.0f / numOfMeasuredCalls; - }else{ - return -1f; - } + public float getFailureRate() { + return getFailureRate(getNumberOfFailedCalls()); } + /** + * {@inheritDoc} + */ @Override public int getMaxNumberOfBufferedCalls() { - return maxNumberOfBufferedCalls; + return ringBufferSize; } + /** + * {@inheritDoc} + */ @Override - public synchronized int getNumberOfSuccessfulCalls() { + public int getNumberOfSuccessfulCalls() { return getNumberOfBufferedCalls() - getNumberOfFailedCalls(); } + /** + * {@inheritDoc} + */ @Override - public synchronized int getNumberOfBufferedCalls() { + public int getNumberOfBufferedCalls() { return this.ringBitSet.length(); } + /** + * {@inheritDoc} + */ @Override - public synchronized int getNumberOfFailedCalls() { + public int getNumberOfFailedCalls() { return this.ringBitSet.cardinality(); } + + private float getFailureRate(int numberOfFailedCalls) { + if (getNumberOfBufferedCalls() < ringBufferSize) { + return -1.0f; + } + return numberOfFailedCalls * 100.0f / ringBufferSize; + } } diff --git a/src/main/java/io/github/robwin/circuitbreaker/internal/RingBitSet.java b/src/main/java/io/github/robwin/circuitbreaker/internal/RingBitSet.java index a91f4d9b8b..3abea927ae 100644 --- a/src/main/java/io/github/robwin/circuitbreaker/internal/RingBitSet.java +++ b/src/main/java/io/github/robwin/circuitbreaker/internal/RingBitSet.java @@ -1,6 +1,6 @@ /* * - * Copyright 2016 Robert Winkler + * Copyright 2016 Robert Winkler and Bohdan Storozhuk * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,30 +18,19 @@ */ package io.github.robwin.circuitbreaker.internal; -import java.util.BitSet; - /** * A ring bit set which stores bits up to a maximum size of bits. */ public class RingBitSet { - private final BitSet bitSet; + private final int size; + private final BitSetMod bitSet; - /** - * The maximum size of the ring bit set - */ - private final int maxSize; - - /** - * The current index of the ring bit set - */ + private boolean notFull; private int index = -1; - - /** - * The current size of the ring bit set. - */ - private int size = 0; + private volatile int length; + private volatile int cardinality = 0; /** @@ -49,51 +38,30 @@ public class RingBitSet { * represent bits with indices in the range {@code 0} through * {@code bitSetSize-1}. All bits are initially {@code false}. * - * @param bitSetSize the size of the ring bit set + * @param bitSetSize the size of the ring bit set * @throws NegativeArraySizeException if the specified initial size - * is negative + * is negative */ public RingBitSet(int bitSetSize) { - this.maxSize = bitSetSize; - this.bitSet = new BitSet(bitSetSize); + notFull = true; + size = bitSetSize; + bitSet = new BitSetMod(bitSetSize); } /** * Sets the bit at the next index to the specified value. * - * @param value a boolean value to set + * @param value a boolean value to set + * @return the number of bits set to {@code true} */ - public synchronized void setNextBit(boolean value) { - this.bitSet.set(nextIndex(), value); - this.increaseSize(); - } - - /** - * Returns the current index of this {@code RingBitSet}. - * - * @return the current index of this {@code RingBitSet} - */ - public synchronized int getIndex(){ - return index; - } - - /** - * Returns the next index. If the index reaches the maximum number of bits, it is set back to zero. - * It is this behaviour which turns the bit set into a ring bit set. - * - * @return the next index - */ - private synchronized int nextIndex(){ - index++; - if (index == maxSize) index = 0; - return index; - } - - /** - * Increases the size of this {@code RingBitSet} up to the maximum size of this {@code RingBitSet}. - */ - private synchronized void increaseSize(){ - if(size < maxSize) size++; + public synchronized int setNextBit(boolean value) { + increaseLength(); + index = (index + 1) % size; + + int previous = bitSet.set(index, value); + int current = value ? 1 : 0; + cardinality = cardinality - previous + current; + return cardinality; } /** @@ -101,8 +69,8 @@ private synchronized void increaseSize(){ * * @return the number of bits set to {@code true} in this {@code RingBitSet} */ - public synchronized int cardinality(){ - return bitSet.cardinality(); + public int cardinality() { + return cardinality; } /** @@ -112,7 +80,7 @@ public synchronized int cardinality(){ * * @return the number of bits currently in this ring bit set */ - public synchronized int size(){ + public int size() { return bitSet.size(); } @@ -122,7 +90,29 @@ public synchronized int size(){ * * @return the logical size of this {@code RingBitSet} */ - public synchronized int length(){ - return this.size; + public int length() { + return length; + } + + /** + * Returns the current index of this {@code RingBitSet}. + * Use only for debugging and testing + * + * @return the current index of this {@code RingBitSet} + */ + synchronized int getIndex() { + return index; + } + + private void increaseLength() { + if (notFull) { + int nextLength = length + 1; + if (nextLength < size) { + length = nextLength; + } else { + length = size; + notFull = false; + } + } } } diff --git a/src/test/java/io/github/robwin/circuitbreaker/internal/RingBitSetTest.java b/src/test/java/io/github/robwin/circuitbreaker/internal/RingBitSetTest.java index 1c2d1c459d..56c6458668 100644 --- a/src/test/java/io/github/robwin/circuitbreaker/internal/RingBitSetTest.java +++ b/src/test/java/io/github/robwin/circuitbreaker/internal/RingBitSetTest.java @@ -1,6 +1,6 @@ /* * - * Copyright 2016 Robert Winkler + * Copyright 2016 Robert Winkler and Bohdan Storozhuk * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,37 +18,43 @@ */ package io.github.robwin.circuitbreaker.internal; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.ThreadLocalRandom; import java.util.stream.IntStream; - -import static org.assertj.core.api.Assertions.assertThat; +import java.util.stream.Stream; public class RingBitSetTest { private static final Logger LOG = LoggerFactory.getLogger(RingBitSetTest.class); @Test - public void testRingBitSet(){ + public void testRingBitSet() { RingBitSet ringBitSet = new RingBitSet(4); // The initial index is -1 assertThat(ringBitSet.getIndex()).isEqualTo(-1); - ringBitSet.setNextBit(true); + assertThat(ringBitSet.setNextBit(true)).isEqualTo(1); + assertThat(ringBitSet.getIndex()).isEqualTo(0); - ringBitSet.setNextBit(false); + assertThat(ringBitSet.setNextBit(false)).isEqualTo(1); + assertThat(ringBitSet.getIndex()).isEqualTo(1); - ringBitSet.setNextBit(true); + assertThat(ringBitSet.setNextBit(true)).isEqualTo(2); + assertThat(ringBitSet.getIndex()).isEqualTo(2); - ringBitSet.setNextBit(true); + assertThat(ringBitSet.setNextBit(true)).isEqualTo(3); + assertThat(ringBitSet.getIndex()).isEqualTo(3); - ringBitSet.setNextBit(false); + assertThat(ringBitSet.setNextBit(false)).isEqualTo(2); // The index has reached the maximum size and is set back to 0 assertThat(ringBitSet.getIndex()).isEqualTo(0); - ringBitSet.setNextBit(false); + assertThat(ringBitSet.setNextBit(false)).isEqualTo(2); assertThat(ringBitSet.getIndex()).isEqualTo(1); // The cardinality must be 2 because the first true was overwritten by the 5th setNextBit() @@ -62,13 +68,13 @@ public void testRingBitSet(){ } @Test - public void testRingBitSetParallel(){ + public void testRingBitSetParallel() { System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20"); RingBitSet ringBitSet = new RingBitSet(1000); - IntStream.range(0, 1000).parallel().forEach((i) -> { - if(i < 500){ + IntStream.range(0, 1000).parallel().forEach((i) -> { + if (i < 500) { ringBitSet.setNextBit(true); - }else{ + } else { ringBitSet.setNextBit(false); } }); @@ -82,4 +88,18 @@ public void testRingBitSetParallel(){ assertThat(ringBitSet.length()).isEqualTo(1000); } -} + @Test + public void testRingBitSetWithSlightlyLessCapacity() { + RingBitSet ringBitSet = new RingBitSet(100); + long expectedCardinality = Stream.generate(ThreadLocalRandom.current()::nextBoolean) + .limit(1000) + .peek(ringBitSet::setNextBit) + .skip(900) + .mapToInt(b -> b ? 1 : 0) + .sum(); + + assertThat(ringBitSet.cardinality()).isEqualTo((int) expectedCardinality); + assertThat(ringBitSet.size()).isEqualTo(128); + assertThat(ringBitSet.length()).isEqualTo(100); + } +} \ No newline at end of file