correct circular buffer
This commit is contained in:
parent
824e9ac712
commit
5a8f35fc29
4 changed files with 303 additions and 0 deletions
|
|
@ -0,0 +1,110 @@
|
||||||
|
package com.github.shautvast.exceptional;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Circular buffer for variable sized byte arrays
|
||||||
|
* The singlethread version
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("StringTemplateMigration")
|
||||||
|
public class CircularByteBuffer {
|
||||||
|
|
||||||
|
final ByteBuffer data;
|
||||||
|
int readIndex = 0;
|
||||||
|
int writeIndex = 0;
|
||||||
|
|
||||||
|
public CircularByteBuffer(int capacity) {
|
||||||
|
data = ByteBuffer.allocate(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean put(byte[] bytes) {
|
||||||
|
int len = bytes.length;
|
||||||
|
int remaining;
|
||||||
|
// check capacity for bytes to insert
|
||||||
|
if (writeIndex >= readIndex) {
|
||||||
|
remaining = data.capacity() - writeIndex + readIndex;
|
||||||
|
} else {
|
||||||
|
remaining = readIndex - writeIndex;
|
||||||
|
}
|
||||||
|
if (remaining < len + 2) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
int remainingUntilEnd = data.capacity() - writeIndex;
|
||||||
|
if (remainingUntilEnd < len + 2) {
|
||||||
|
if (remainingUntilEnd > 1) {
|
||||||
|
// we can write the length
|
||||||
|
data.putShort(writeIndex, (short) len);
|
||||||
|
writeIndex += 2;
|
||||||
|
remainingUntilEnd -= 2;
|
||||||
|
if (remainingUntilEnd > 0) {
|
||||||
|
data.put(writeIndex, bytes, 0, remainingUntilEnd);
|
||||||
|
}
|
||||||
|
writeIndex = 0;
|
||||||
|
data.put(writeIndex, bytes, remainingUntilEnd, len - remainingUntilEnd);
|
||||||
|
writeIndex += len - remainingUntilEnd;
|
||||||
|
} else {
|
||||||
|
// we can write only one byte of the length
|
||||||
|
data.put(writeIndex, (byte) (len >> 8));
|
||||||
|
writeIndex = 0;
|
||||||
|
data.put(writeIndex, (byte) (len & 0xff));
|
||||||
|
writeIndex += 1;
|
||||||
|
data.put(writeIndex, bytes);
|
||||||
|
writeIndex += len;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.putShort(writeIndex, (short) len);
|
||||||
|
writeIndex += 2;
|
||||||
|
data.put(writeIndex, bytes);
|
||||||
|
writeIndex += len;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] get() {
|
||||||
|
int remainingUntilEnd = data.capacity() - readIndex;
|
||||||
|
int len;
|
||||||
|
if (remainingUntilEnd == 1) {
|
||||||
|
byte high = data.get(readIndex);
|
||||||
|
readIndex = 0;
|
||||||
|
byte low = data.get(readIndex);
|
||||||
|
readIndex = 1;
|
||||||
|
len = high << 8 | low;
|
||||||
|
remainingUntilEnd = len;
|
||||||
|
} else if (remainingUntilEnd == 2) {
|
||||||
|
len = data.getShort(readIndex);
|
||||||
|
readIndex = 0;
|
||||||
|
remainingUntilEnd = 0;
|
||||||
|
} else {
|
||||||
|
len = data.getShort(readIndex);
|
||||||
|
readIndex += 2;
|
||||||
|
remainingUntilEnd -= 2;
|
||||||
|
}
|
||||||
|
byte[] result = new byte[len];
|
||||||
|
if (len <= remainingUntilEnd) {
|
||||||
|
data.get(readIndex, result);
|
||||||
|
readIndex += len;
|
||||||
|
} else {
|
||||||
|
data.get(readIndex, result, 0, remainingUntilEnd);
|
||||||
|
readIndex = 0;
|
||||||
|
data.get(readIndex, result, remainingUntilEnd, len - remainingUntilEnd);
|
||||||
|
readIndex += len - remainingUntilEnd;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CircularBuffer {r=" + this.readIndex +
|
||||||
|
", w=" +
|
||||||
|
this.writeIndex +
|
||||||
|
", data=" +
|
||||||
|
IntStream.range(0, this.data.array().length)
|
||||||
|
.map(x -> this.data.array()[x])
|
||||||
|
.mapToObj(Integer::toString)
|
||||||
|
.collect(Collectors.joining(",", "[", "]")) +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.github.shautvast.exceptional;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables multithreaded writing, while keeping CircularByteBuffer simpler (only suitable for single-threaded writing)
|
||||||
|
*/
|
||||||
|
public class MPSCBufferWriter implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final ConcurrentLinkedDeque<byte[]> writeQueue = new ConcurrentLinkedDeque<>(); // unbounded
|
||||||
|
private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||||
|
private final AtomicBoolean active = new AtomicBoolean(false);
|
||||||
|
private final CircularByteBuffer buffer;
|
||||||
|
|
||||||
|
public MPSCBufferWriter(CircularByteBuffer buffer) {
|
||||||
|
this.buffer = buffer;
|
||||||
|
startWriteQueueListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startWriteQueueListener() {
|
||||||
|
active.set(true);
|
||||||
|
executorService.submit(() -> {
|
||||||
|
while (active.get()) {
|
||||||
|
var element = writeQueue.pollFirst();
|
||||||
|
if (element != null) {
|
||||||
|
while (!buffer.put(element) && active.get()) {
|
||||||
|
Thread.yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void put(byte[] bytes) {
|
||||||
|
writeQueue.offerLast(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuts down the background thread
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
active.set(false);
|
||||||
|
executorService.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
package com.github.shautvast.exceptional;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class CircularByteBufferTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPutAndGet() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(9);
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
boolean written = buffer.put(bytes);
|
||||||
|
assertTrue(written);
|
||||||
|
assertArrayEquals(bytes, buffer.get());
|
||||||
|
assertArrayEquals(new byte[]{0, 5, 104, 101, 108, 108, 111, 0, 0}, buffer.data.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPutFitsBeforeGet() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(14);
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
buffer.writeIndex = 7;
|
||||||
|
buffer.readIndex = 7;
|
||||||
|
buffer.put(bytes);
|
||||||
|
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 5, 104, 101, 108, 108, 111}, buffer.data.array());
|
||||||
|
buffer.writeIndex = 0;
|
||||||
|
// end of setup, situation where writeIndex < readIndex
|
||||||
|
boolean written = buffer.put(bytes);
|
||||||
|
assertTrue(written);
|
||||||
|
assertArrayEquals(new byte[]{0, 5, 104, 101, 108, 108, 111, 0, 5, 104, 101, 108, 108, 111}, buffer.data.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPutFitsNotBeforeGet() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(13);
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
buffer.writeIndex = 6;
|
||||||
|
buffer.readIndex = 6;
|
||||||
|
buffer.put(bytes);
|
||||||
|
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 5, 104, 101, 108, 108, 111}, buffer.data.array());
|
||||||
|
buffer.writeIndex = 0;
|
||||||
|
// end of setup, situation where writeIndex < readIndex
|
||||||
|
boolean written = buffer.put(bytes);
|
||||||
|
assertFalse(written);
|
||||||
|
assertArrayEquals(new byte[]{0, 0, 0, 0, 0, 0, 0, 5, 104, 101, 108, 108, 111}, buffer.data.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testWrapAroundPutLenAndOneCharBeforeWrap() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(9);
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
buffer.writeIndex = 6;
|
||||||
|
buffer.readIndex = 6;
|
||||||
|
boolean written = buffer.put(bytes);
|
||||||
|
assertTrue(written);
|
||||||
|
assertArrayEquals(new byte[]{101, 108, 108, 111, 0, 0, 0, 5, 104}, buffer.data.array());
|
||||||
|
assertArrayEquals(bytes, buffer.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testWrapAroundPutLenBeforeWrap() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(9);
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
buffer.writeIndex = 7;
|
||||||
|
buffer.readIndex = 7;
|
||||||
|
boolean written = buffer.put(bytes);
|
||||||
|
assertTrue(written);
|
||||||
|
assertArrayEquals(new byte[]{104, 101, 108, 108, 111, 0, 0, 0, 5}, buffer.data.array());
|
||||||
|
assertArrayEquals(bytes, buffer.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testWrapAroundPutLenSplitBeforeWrap() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(9);
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
buffer.writeIndex = 8;
|
||||||
|
buffer.readIndex = 8;
|
||||||
|
boolean written = buffer.put(bytes);
|
||||||
|
assertTrue(written);
|
||||||
|
assertArrayEquals(new byte[]{5, 104, 101, 108, 108, 111, 0, 0, 0}, buffer.data.array());
|
||||||
|
assertArrayEquals(bytes, buffer.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNoFreeSpace() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(9);
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
boolean written1 = buffer.put(bytes);
|
||||||
|
assertTrue(written1);
|
||||||
|
boolean written2 = buffer.put(bytes);
|
||||||
|
assertFalse(written2); // no space left
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFreeSpaceReclaimed() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(9);
|
||||||
|
assertEquals(0, buffer.readIndex);
|
||||||
|
assertEquals(0, buffer.writeIndex);
|
||||||
|
|
||||||
|
byte[] bytes = "hello".getBytes(UTF_8);
|
||||||
|
boolean written1 = buffer.put(bytes);
|
||||||
|
assertTrue(written1);
|
||||||
|
assertEquals(0, buffer.readIndex);
|
||||||
|
assertEquals(7, buffer.writeIndex);
|
||||||
|
|
||||||
|
assertArrayEquals(bytes, buffer.get());
|
||||||
|
assertEquals(7, buffer.readIndex);
|
||||||
|
assertEquals(7, buffer.writeIndex);
|
||||||
|
|
||||||
|
boolean written2 = buffer.put(bytes);
|
||||||
|
assertTrue(written2); // the read has freed space
|
||||||
|
assertEquals(7, buffer.readIndex);
|
||||||
|
assertEquals(5, buffer.writeIndex);
|
||||||
|
|
||||||
|
|
||||||
|
assertArrayEquals(bytes, buffer.get());
|
||||||
|
assertEquals(5, buffer.readIndex);
|
||||||
|
assertEquals(5, buffer.writeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.github.shautvast.exceptional;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
|
||||||
|
class MPSCBufferWriterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test() {
|
||||||
|
CircularByteBuffer buffer = new CircularByteBuffer(9);
|
||||||
|
try (MPSCBufferWriter writer = new MPSCBufferWriter(buffer)) {
|
||||||
|
byte[] bytes = "cow".getBytes(UTF_8);
|
||||||
|
writer.put(bytes);
|
||||||
|
writer.put(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue