--- /dev/null
+<!--
+ ~ Copyright 2017-present Open Networking Foundation
+ ~
+ ~ 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.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>io.atomix</groupId>
+ <artifactId>atomix-parent</artifactId>
+ <version>3.2.0-SNAPSHOT</version>
+ </parent>
+
+ <packaging>bundle</packaging>
+ <artifactId>atomix-storage</artifactId>
+ <name>Atomix Storage</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>io.atomix</groupId>
+ <artifactId>atomix-utils</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Export-Package>
+ io.atomix.storage.*
+ </Export-Package>
+ <Import-Package>
+ !sun.nio.ch,!sun.misc,*
+ </Import-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage;
+
+/**
+ * Log exception.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class StorageException extends RuntimeException {
+
+ public StorageException() {
+ }
+
+ public StorageException(String message) {
+ super(message);
+ }
+
+ public StorageException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public StorageException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Exception thrown when an entry being stored is too large.
+ */
+ public static class TooLarge extends StorageException {
+ public TooLarge(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Exception thrown when storage runs out of disk space.
+ */
+ public static class OutOfDiskSpace extends StorageException {
+ public OutOfDiskSpace(String message) {
+ super(message);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage;
+
+/**
+ * Storage level configuration values which control how logs are stored on disk or in memory.
+ */
+public enum StorageLevel {
+
+ /**
+ * Stores data in memory only.
+ */
+ @Deprecated
+ MEMORY,
+
+ /**
+ * Stores data in a memory-mapped file.
+ */
+ MAPPED,
+
+ /**
+ * Stores data on disk.
+ */
+ DISK
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.concurrent.ReferenceManager;
+import io.atomix.utils.memory.Memory;
+
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteOrder;
+import java.nio.InvalidMarkException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static io.atomix.storage.buffer.Bytes.BOOLEAN;
+import static io.atomix.storage.buffer.Bytes.BYTE;
+import static io.atomix.storage.buffer.Bytes.SHORT;
+
+/**
+ * Abstract buffer implementation.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public abstract class AbstractBuffer implements Buffer {
+ static final int DEFAULT_INITIAL_CAPACITY = 4096;
+ static final int MAX_SIZE = Integer.MAX_VALUE - 5;
+
+ protected final Bytes bytes;
+ private int offset;
+ private int initialCapacity;
+ private int capacity;
+ private int maxCapacity;
+ private int position;
+ private int limit = -1;
+ private int mark = -1;
+ private final AtomicInteger references = new AtomicInteger();
+ protected final ReferenceManager<Buffer> referenceManager;
+ private SwappedBuffer swap;
+
+ protected AbstractBuffer(Bytes bytes, ReferenceManager<Buffer> referenceManager) {
+ this(bytes, 0, 0, 0, referenceManager);
+ }
+
+ protected AbstractBuffer(Bytes bytes, int offset, int initialCapacity, int maxCapacity, ReferenceManager<Buffer> referenceManager) {
+ if (bytes == null) {
+ throw new NullPointerException("bytes cannot be null");
+ }
+ if (offset < 0) {
+ throw new IndexOutOfBoundsException("offset out of bounds of the underlying byte array");
+ }
+ this.bytes = bytes;
+ this.offset = offset;
+ this.capacity = 0;
+ this.initialCapacity = initialCapacity;
+ this.maxCapacity = maxCapacity;
+ capacity(initialCapacity);
+ this.referenceManager = referenceManager;
+ references.set(1);
+ }
+
+ /**
+ * Resets the buffer's internal offset and capacity.
+ */
+ protected AbstractBuffer reset(int offset, int capacity, int maxCapacity) {
+ this.offset = offset;
+ this.capacity = 0;
+ this.initialCapacity = capacity;
+ this.maxCapacity = maxCapacity;
+ capacity(initialCapacity);
+ references.set(0);
+ rewind();
+ return this;
+ }
+
+ @Override
+ public Buffer acquire() {
+ references.incrementAndGet();
+ return this;
+ }
+
+ @Override
+ public boolean release() {
+ if (references.decrementAndGet() == 0) {
+ if (referenceManager != null) {
+ referenceManager.release(this);
+ } else {
+ bytes.close();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int references() {
+ return references.get();
+ }
+
+ @Override
+ public Bytes bytes() {
+ return bytes;
+ }
+
+ @Override
+ public ByteOrder order() {
+ return bytes.order();
+ }
+
+ @Override
+ public Buffer order(ByteOrder order) {
+ if (order == null) {
+ throw new NullPointerException("order cannot be null");
+ }
+ if (order == order()) {
+ return this;
+ }
+ if (swap != null) {
+ return swap;
+ }
+ swap = new SwappedBuffer(this, offset, capacity, maxCapacity, referenceManager);
+ return swap;
+ }
+
+ @Override
+ public boolean isDirect() {
+ return bytes.isDirect();
+ }
+
+ @Override
+ public boolean isFile() {
+ return bytes.isFile();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @Override
+ public Buffer asReadOnlyBuffer() {
+ return new ReadOnlyBuffer(this, referenceManager)
+ .reset(offset, capacity, maxCapacity)
+ .position(position)
+ .limit(limit);
+ }
+
+ @Override
+ public Buffer compact() {
+ compact(offset(position), offset, (limit != -1 ? limit : capacity) - offset(position));
+ return clear();
+ }
+
+ /**
+ * Compacts the given bytes.
+ */
+ protected abstract void compact(int from, int to, int length);
+
+ @Override
+ public Buffer slice() {
+ int maxCapacity = this.maxCapacity - position;
+ int capacity = Math.min(Math.min(initialCapacity, maxCapacity), bytes.size() - offset(position));
+ if (limit != -1) {
+ capacity = maxCapacity = limit - position;
+ }
+ return new SlicedBuffer(this, bytes, offset(position), capacity, maxCapacity);
+ }
+
+ @Override
+ public Buffer slice(int length) {
+ checkSlice(position, length);
+ return new SlicedBuffer(this, bytes, offset(position), length, length);
+ }
+
+ @Override
+ public Buffer slice(int offset, int length) {
+ checkSlice(offset, length);
+ return new SlicedBuffer(this, bytes, offset(offset), length, length);
+ }
+
+ @Override
+ public int offset() {
+ return offset;
+ }
+
+ @Override
+ public int capacity() {
+ return capacity;
+ }
+
+ /**
+ * Updates the buffer capacity.
+ */
+ public Buffer capacity(int capacity) {
+ if (capacity > maxCapacity) {
+ throw new IllegalArgumentException("capacity cannot be greater than maximum capacity");
+ } else if (capacity < this.capacity) {
+ throw new IllegalArgumentException("capacity cannot be decreased");
+ } else if (capacity != this.capacity) {
+ // It's possible that the bytes could already meet the requirements of the capacity.
+ if (offset(capacity) > bytes.size()) {
+ bytes.resize((int) Math.min(Memory.Util.toPow2(offset(capacity)), Integer.MAX_VALUE));
+ }
+ this.capacity = capacity;
+ }
+ return this;
+ }
+
+ @Override
+ public int maxCapacity() {
+ return maxCapacity;
+ }
+
+ @Override
+ public int position() {
+ return position;
+ }
+
+ @Override
+ public Buffer position(int position) {
+ if (limit != -1 && position > limit) {
+ throw new IllegalArgumentException("position cannot be greater than limit");
+ } else if (limit == -1 && position > maxCapacity) {
+ throw new IllegalArgumentException("position cannot be greater than capacity");
+ }
+ if (position > capacity) {
+ capacity((int) Math.min(maxCapacity, Memory.Util.toPow2(position)));
+ }
+ this.position = position;
+ return this;
+ }
+
+ /**
+ * Returns the real offset of the given relative offset.
+ */
+ private int offset(int offset) {
+ return this.offset + offset;
+ }
+
+ @Override
+ public int limit() {
+ return limit;
+ }
+
+ @Override
+ public Buffer limit(int limit) {
+ if (limit > maxCapacity) {
+ throw new IllegalArgumentException("limit cannot be greater than buffer capacity");
+ }
+ if (limit < -1) {
+ throw new IllegalArgumentException("limit cannot be negative");
+ }
+ if (limit != -1 && offset(limit) > bytes.size()) {
+ bytes.resize(offset(limit));
+ }
+ this.limit = limit;
+ return this;
+ }
+
+ @Override
+ public int remaining() {
+ return (limit == -1 ? maxCapacity : limit) - position;
+ }
+
+ @Override
+ public boolean hasRemaining() {
+ return remaining() > 0;
+ }
+
+ @Override
+ public Buffer flip() {
+ limit = position;
+ position = 0;
+ mark = -1;
+ return this;
+ }
+
+ @Override
+ public Buffer mark() {
+ this.mark = position;
+ return this;
+ }
+
+ @Override
+ public Buffer rewind() {
+ position = 0;
+ mark = -1;
+ return this;
+ }
+
+ @Override
+ public Buffer reset() {
+ if (mark == -1) {
+ throw new InvalidMarkException();
+ }
+ position = mark;
+ return this;
+ }
+
+ @Override
+ public Buffer skip(int length) {
+ if (length > remaining()) {
+ throw new IndexOutOfBoundsException("length cannot be greater than remaining bytes in the buffer");
+ }
+ position += length;
+ return this;
+ }
+
+ @Override
+ public Buffer clear() {
+ position = 0;
+ limit = -1;
+ mark = -1;
+ return this;
+ }
+
+ /**
+ * Checks that the offset is within the bounds of the buffer.
+ */
+ protected void checkOffset(int offset) {
+ if (offset(offset) < this.offset) {
+ throw new IndexOutOfBoundsException();
+ } else if (limit == -1) {
+ if (offset > maxCapacity) {
+ throw new IndexOutOfBoundsException();
+ }
+ } else {
+ if (offset > limit) {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+ }
+
+ /**
+ * Checks bounds for a slice.
+ */
+ protected int checkSlice(int offset, int length) {
+ checkOffset(offset);
+ if (limit == -1) {
+ if (offset + length > capacity) {
+ if (capacity < maxCapacity) {
+ capacity(calculateCapacity(offset + length));
+ } else {
+ throw new BufferUnderflowException();
+ }
+ }
+ } else {
+ if (offset + length > limit) {
+ throw new BufferUnderflowException();
+ }
+ }
+ return offset(offset);
+ }
+
+ /**
+ * Checks bounds for a read for the given length.
+ */
+ protected int checkRead(int length) {
+ checkRead(position, length);
+ int previousPosition = this.position;
+ this.position = previousPosition + length;
+ return offset(previousPosition);
+ }
+
+ /**
+ * Checks bounds for a read.
+ */
+ protected int checkRead(int offset, int length) {
+ checkOffset(offset);
+ if (limit == -1) {
+ if (offset + length > capacity) {
+ if (capacity < maxCapacity) {
+ if (this.offset + offset + length <= bytes.size()) {
+ capacity = bytes.size() - this.offset;
+ } else {
+ capacity(calculateCapacity(offset + length));
+ }
+ } else {
+ throw new BufferUnderflowException();
+ }
+ }
+ } else {
+ if (offset + length > limit) {
+ throw new BufferUnderflowException();
+ }
+ }
+ return offset(offset);
+ }
+
+ /**
+ * Checks bounds for a write of the given length.
+ */
+ protected int checkWrite(int length) {
+ checkWrite(position, length);
+ int previousPosition = this.position;
+ this.position = previousPosition + length;
+ return offset(previousPosition);
+ }
+
+ /**
+ * Checks bounds for a write.
+ */
+ protected int checkWrite(int offset, int length) {
+ checkOffset(offset);
+ if (limit == -1) {
+ if (offset + length > capacity) {
+ if (capacity < maxCapacity) {
+ capacity(calculateCapacity(offset + length));
+ } else {
+ throw new BufferOverflowException();
+ }
+ }
+ } else {
+ if (offset + length > limit) {
+ throw new BufferOverflowException();
+ }
+ }
+ return offset(offset);
+ }
+
+ /**
+ * Calculates the next capacity that meets the given minimum capacity.
+ */
+ private int calculateCapacity(int minimumCapacity) {
+ int newCapacity = Math.min(Math.max(capacity, 2), minimumCapacity);
+ while (newCapacity < Math.min(minimumCapacity, maxCapacity)) {
+ newCapacity <<= 1;
+ }
+ return Math.min(newCapacity, maxCapacity);
+ }
+
+ @Override
+ public Buffer zero() {
+ bytes.zero(offset);
+ return this;
+ }
+
+ @Override
+ public Buffer zero(int offset) {
+ checkOffset(offset);
+ bytes.zero(offset(offset));
+ return this;
+ }
+
+ @Override
+ public Buffer zero(int offset, int length) {
+ checkOffset(offset);
+ bytes.zero(offset(offset), length);
+ return this;
+ }
+
+ @Override
+ public Buffer read(Buffer buffer) {
+ int length = Math.min(buffer.remaining(), remaining());
+ read(buffer.bytes(), buffer.offset() + buffer.position(), length);
+ buffer.position(buffer.position() + length);
+ return this;
+ }
+
+ @Override
+ public Buffer read(Bytes bytes) {
+ this.bytes.read(checkRead(bytes.size()), bytes, 0, bytes.size());
+ return this;
+ }
+
+ @Override
+ public Buffer read(Bytes bytes, int offset, int length) {
+ this.bytes.read(checkRead(length), bytes, offset, length);
+ return this;
+ }
+
+ @Override
+ public Buffer read(int srcOffset, Bytes bytes, int dstOffset, int length) {
+ this.bytes.read(checkRead(srcOffset, length), bytes, dstOffset, length);
+ return this;
+ }
+
+ @Override
+ public Buffer read(byte[] bytes) {
+ this.bytes.read(checkRead(bytes.length), bytes, 0, bytes.length);
+ return this;
+ }
+
+ @Override
+ public Buffer read(byte[] bytes, int offset, int length) {
+ this.bytes.read(checkRead(length), bytes, offset, length);
+ return this;
+ }
+
+ @Override
+ public Buffer read(int srcOffset, byte[] bytes, int dstOffset, int length) {
+ this.bytes.read(checkRead(srcOffset, length), bytes, dstOffset, length);
+ return this;
+ }
+
+ @Override
+ public int readByte() {
+ return bytes.readByte(checkRead(BYTE));
+ }
+
+ @Override
+ public int readByte(int offset) {
+ return bytes.readByte(checkRead(offset, BYTE));
+ }
+
+ @Override
+ public int readUnsignedByte() {
+ return bytes.readUnsignedByte(checkRead(BYTE));
+ }
+
+ @Override
+ public int readUnsignedByte(int offset) {
+ return bytes.readUnsignedByte(checkRead(offset, BYTE));
+ }
+
+ @Override
+ public char readChar() {
+ return bytes.readChar(checkRead(Bytes.CHARACTER));
+ }
+
+ @Override
+ public char readChar(int offset) {
+ return bytes.readChar(checkRead(offset, Bytes.CHARACTER));
+ }
+
+ @Override
+ public short readShort() {
+ return bytes.readShort(checkRead(SHORT));
+ }
+
+ @Override
+ public short readShort(int offset) {
+ return bytes.readShort(checkRead(offset, SHORT));
+ }
+
+ @Override
+ public int readUnsignedShort() {
+ return bytes.readUnsignedShort(checkRead(SHORT));
+ }
+
+ @Override
+ public int readUnsignedShort(int offset) {
+ return bytes.readUnsignedShort(checkRead(offset, SHORT));
+ }
+
+ @Override
+ public int readMedium() {
+ return bytes.readMedium(checkRead(3));
+ }
+
+ @Override
+ public int readMedium(int offset) {
+ return bytes.readMedium(checkRead(offset, 3));
+ }
+
+ @Override
+ public int readUnsignedMedium() {
+ return bytes.readUnsignedMedium(checkRead(3));
+ }
+
+ @Override
+ public int readUnsignedMedium(int offset) {
+ return bytes.readUnsignedMedium(checkRead(offset, 3));
+ }
+
+ @Override
+ public int readInt() {
+ return bytes.readInt(checkRead(Bytes.INTEGER));
+ }
+
+ @Override
+ public int readInt(int offset) {
+ return bytes.readInt(checkRead(offset, Bytes.INTEGER));
+ }
+
+ @Override
+ public long readUnsignedInt() {
+ return bytes.readUnsignedInt(checkRead(Bytes.INTEGER));
+ }
+
+ @Override
+ public long readUnsignedInt(int offset) {
+ return bytes.readUnsignedInt(checkRead(offset, Bytes.INTEGER));
+ }
+
+ @Override
+ public long readLong() {
+ return bytes.readLong(checkRead(Bytes.LONG));
+ }
+
+ @Override
+ public long readLong(int offset) {
+ return bytes.readLong(checkRead(offset, Bytes.LONG));
+ }
+
+ @Override
+ public float readFloat() {
+ return bytes.readFloat(checkRead(Bytes.FLOAT));
+ }
+
+ @Override
+ public float readFloat(int offset) {
+ return bytes.readFloat(checkRead(offset, Bytes.FLOAT));
+ }
+
+ @Override
+ public double readDouble() {
+ return bytes.readDouble(checkRead(Bytes.DOUBLE));
+ }
+
+ @Override
+ public double readDouble(int offset) {
+ return bytes.readDouble(checkRead(offset, Bytes.DOUBLE));
+ }
+
+ @Override
+ public boolean readBoolean() {
+ return bytes.readBoolean(checkRead(BYTE));
+ }
+
+ @Override
+ public boolean readBoolean(int offset) {
+ return bytes.readBoolean(checkRead(offset, BYTE));
+ }
+
+ @Override
+ public String readString(Charset charset) {
+ if (readBoolean(position)) {
+ byte[] bytes = new byte[readUnsignedShort(position + BOOLEAN)];
+ read(position + BOOLEAN + SHORT, bytes, 0, bytes.length);
+ this.position += BOOLEAN + SHORT + bytes.length;
+ return new String(bytes, charset);
+ } else {
+ this.position += BOOLEAN;
+ }
+ return null;
+ }
+
+ @Override
+ public String readString(int offset, Charset charset) {
+ if (readBoolean(offset)) {
+ byte[] bytes = new byte[readUnsignedShort(offset + BOOLEAN)];
+ read(offset + BOOLEAN + SHORT, bytes, 0, bytes.length);
+ return new String(bytes, charset);
+ }
+ return null;
+ }
+
+ @Override
+ public String readString() {
+ return readString(Charset.defaultCharset());
+ }
+
+ @Override
+ public String readString(int offset) {
+ return readString(offset, Charset.defaultCharset());
+ }
+
+ @Override
+ public String readUTF8() {
+ return readString(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public String readUTF8(int offset) {
+ return readString(offset, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Buffer write(Buffer buffer) {
+ int length = Math.min(buffer.remaining(), remaining());
+ write(buffer.bytes(), buffer.offset() + buffer.position(), length);
+ buffer.position(buffer.position() + length);
+ return this;
+ }
+
+ @Override
+ public Buffer write(Bytes bytes) {
+ this.bytes.write(checkWrite(bytes.size()), bytes, 0, bytes.size());
+ return this;
+ }
+
+ @Override
+ public Buffer write(Bytes bytes, int offset, int length) {
+ this.bytes.write(checkWrite(length), bytes, offset, length);
+ return this;
+ }
+
+ @Override
+ public Buffer write(int offset, Bytes bytes, int srcOffset, int length) {
+ this.bytes.write(checkWrite(offset, length), bytes, srcOffset, length);
+ return this;
+ }
+
+ @Override
+ public Buffer write(byte[] bytes) {
+ this.bytes.write(checkWrite(bytes.length), bytes, 0, bytes.length);
+ return this;
+ }
+
+ @Override
+ public Buffer write(byte[] bytes, int offset, int length) {
+ this.bytes.write(checkWrite(length), bytes, offset, length);
+ return this;
+ }
+
+ @Override
+ public Buffer write(int offset, byte[] bytes, int srcOffset, int length) {
+ this.bytes.write(checkWrite(offset, length), bytes, srcOffset, length);
+ return this;
+ }
+
+ @Override
+ public Buffer writeByte(int b) {
+ bytes.writeByte(checkWrite(BYTE), b);
+ return this;
+ }
+
+ @Override
+ public Buffer writeByte(int offset, int b) {
+ bytes.writeByte(checkWrite(offset, BYTE), b);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedByte(int b) {
+ bytes.writeUnsignedByte(checkWrite(BYTE), b);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedByte(int offset, int b) {
+ bytes.writeUnsignedByte(checkWrite(offset, BYTE), b);
+ return this;
+ }
+
+ @Override
+ public Buffer writeChar(char c) {
+ bytes.writeChar(checkWrite(Bytes.CHARACTER), c);
+ return this;
+ }
+
+ @Override
+ public Buffer writeChar(int offset, char c) {
+ bytes.writeChar(checkWrite(offset, Bytes.CHARACTER), c);
+ return this;
+ }
+
+ @Override
+ public Buffer writeShort(short s) {
+ bytes.writeShort(checkWrite(SHORT), s);
+ return this;
+ }
+
+ @Override
+ public Buffer writeShort(int offset, short s) {
+ bytes.writeShort(checkWrite(offset, SHORT), s);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedShort(int s) {
+ bytes.writeUnsignedShort(checkWrite(SHORT), s);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedShort(int offset, int s) {
+ bytes.writeUnsignedShort(checkWrite(offset, SHORT), s);
+ return this;
+ }
+
+ @Override
+ public Buffer writeMedium(int m) {
+ bytes.writeMedium(checkWrite(3), m);
+ return this;
+ }
+
+ @Override
+ public Buffer writeMedium(int offset, int m) {
+ bytes.writeMedium(checkWrite(offset, 3), m);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedMedium(int m) {
+ bytes.writeUnsignedMedium(checkWrite(3), m);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedMedium(int offset, int m) {
+ bytes.writeUnsignedMedium(checkWrite(offset, 3), m);
+ return this;
+ }
+
+ @Override
+ public Buffer writeInt(int i) {
+ bytes.writeInt(checkWrite(Bytes.INTEGER), i);
+ return this;
+ }
+
+ @Override
+ public Buffer writeInt(int offset, int i) {
+ bytes.writeInt(checkWrite(offset, Bytes.INTEGER), i);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedInt(long i) {
+ bytes.writeUnsignedInt(checkWrite(Bytes.INTEGER), i);
+ return this;
+ }
+
+ @Override
+ public Buffer writeUnsignedInt(int offset, long i) {
+ bytes.writeUnsignedInt(checkWrite(offset, Bytes.INTEGER), i);
+ return this;
+ }
+
+ @Override
+ public Buffer writeLong(long l) {
+ bytes.writeLong(checkWrite(Bytes.LONG), l);
+ return this;
+ }
+
+ @Override
+ public Buffer writeLong(int offset, long l) {
+ bytes.writeLong(checkWrite(offset, Bytes.LONG), l);
+ return this;
+ }
+
+ @Override
+ public Buffer writeFloat(float f) {
+ bytes.writeFloat(checkWrite(Bytes.FLOAT), f);
+ return this;
+ }
+
+ @Override
+ public Buffer writeFloat(int offset, float f) {
+ bytes.writeFloat(checkWrite(offset, Bytes.FLOAT), f);
+ return this;
+ }
+
+ @Override
+ public Buffer writeDouble(double d) {
+ bytes.writeDouble(checkWrite(Bytes.DOUBLE), d);
+ return this;
+ }
+
+ @Override
+ public Buffer writeDouble(int offset, double d) {
+ bytes.writeDouble(checkWrite(offset, Bytes.DOUBLE), d);
+ return this;
+ }
+
+ @Override
+ public Buffer writeBoolean(boolean b) {
+ bytes.writeBoolean(checkWrite(BYTE), b);
+ return this;
+ }
+
+ @Override
+ public Buffer writeBoolean(int offset, boolean b) {
+ bytes.writeBoolean(checkWrite(offset, BYTE), b);
+ return this;
+ }
+
+ @Override
+ public Buffer writeString(String s, Charset charset) {
+ if (s == null) {
+ return writeBoolean(checkWrite(BOOLEAN), Boolean.FALSE);
+ } else {
+ byte[] bytes = s.getBytes(charset);
+ checkWrite(position, BOOLEAN + SHORT + bytes.length);
+ writeBoolean(Boolean.TRUE)
+ .writeUnsignedShort(bytes.length)
+ .write(bytes, 0, bytes.length);
+ return this;
+ }
+ }
+
+ @Override
+ public Buffer writeString(int offset, String s, Charset charset) {
+ if (s == null) {
+ return writeBoolean(checkWrite(offset, BOOLEAN), Boolean.FALSE);
+ } else {
+ byte[] bytes = s.getBytes(charset);
+ checkWrite(offset, BOOLEAN + SHORT + bytes.length);
+ writeBoolean(offset, Boolean.TRUE)
+ .writeUnsignedShort(offset + BOOLEAN, bytes.length)
+ .write(offset + BOOLEAN + SHORT, bytes, 0, bytes.length);
+ return this;
+ }
+ }
+
+ @Override
+ public Buffer writeString(String s) {
+ return writeString(s, Charset.defaultCharset());
+ }
+
+ @Override
+ public Buffer writeString(int offset, String s) {
+ return writeString(offset, s, Charset.defaultCharset());
+ }
+
+ @Override
+ public Buffer writeUTF8(String s) {
+ return writeString(s, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Buffer writeUTF8(int offset, String s) {
+ return writeString(offset, s, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Buffer flush() {
+ bytes.flush();
+ return this;
+ }
+
+ @Override
+ public void close() {
+ references.set(0);
+ if (referenceManager != null) {
+ referenceManager.release(this);
+ } else {
+ bytes.close();
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Abstract bytes implementation.
+ * <p>
+ * This class provides common state and bounds checking functionality for all {@link Bytes} implementations.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public abstract class AbstractBytes implements Bytes {
+ static final int MAX_SIZE = Integer.MAX_VALUE - 5;
+
+ private boolean open = true;
+ private SwappedBytes swap;
+
+ /**
+ * Checks whether the block is open.
+ */
+ protected void checkOpen() {
+ if (!open) {
+ throw new IllegalStateException("bytes not open");
+ }
+ }
+
+ /**
+ * Checks that the offset is within the bounds of the buffer.
+ */
+ protected void checkOffset(int offset) {
+ checkOpen();
+ if (offset < 0 || offset > size()) {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+
+ /**
+ * Checks bounds for a read.
+ */
+ protected int checkRead(int offset, int length) {
+ checkOffset(offset);
+ int position = offset + length;
+ if (position > size()) {
+ throw new BufferUnderflowException();
+ }
+ return position;
+ }
+
+ /**
+ * Checks bounds for a write.
+ */
+ protected int checkWrite(int offset, int length) {
+ checkOffset(offset);
+ int position = offset + length;
+ if (position > size()) {
+ throw new BufferOverflowException();
+ }
+ return position;
+ }
+
+ @Override
+ public boolean isDirect() {
+ return false;
+ }
+
+ @Override
+ public boolean isFile() {
+ return false;
+ }
+
+ @Override
+ public ByteOrder order() {
+ return ByteOrder.BIG_ENDIAN;
+ }
+
+ @Override
+ public Bytes order(ByteOrder order) {
+ if (order == null) {
+ throw new NullPointerException("order cannot be null");
+ }
+ if (order == order()) {
+ return this;
+ }
+ if (swap != null) {
+ return swap;
+ }
+ swap = new SwappedBytes(this);
+ return swap;
+ }
+
+ @Override
+ public boolean readBoolean(int offset) {
+ return readByte(offset) == 1;
+ }
+
+ @Override
+ public int readUnsignedByte(int offset) {
+ return readByte(offset) & 0xFF;
+ }
+
+ @Override
+ public int readUnsignedShort(int offset) {
+ return readShort(offset) & 0xFFFF;
+ }
+
+ @Override
+ public int readMedium(int offset) {
+ return (readByte(offset)) << 16
+ | (readByte(offset + 1) & 0xff) << 8
+ | (readByte(offset + 2) & 0xff);
+ }
+
+ @Override
+ public int readUnsignedMedium(int offset) {
+ return (readByte(offset) & 0xff) << 16
+ | (readByte(offset + 1) & 0xff) << 8
+ | (readByte(offset + 2) & 0xff);
+ }
+
+ @Override
+ public long readUnsignedInt(int offset) {
+ return readInt(offset) & 0xFFFFFFFFL;
+ }
+
+ @Override
+ public String readString(int offset) {
+ return readString(offset, Charset.defaultCharset());
+ }
+
+ @Override
+ public String readString(int offset, Charset charset) {
+ if (readBoolean(offset)) {
+ byte[] bytes = new byte[readUnsignedShort(offset + BYTE)];
+ read(offset + BYTE + SHORT, bytes, 0, bytes.length);
+ return new String(bytes, charset);
+ }
+ return null;
+ }
+
+ @Override
+ public String readUTF8(int offset) {
+ return readString(offset, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Bytes writeBoolean(int offset, boolean b) {
+ return writeByte(offset, b ? 1 : 0);
+ }
+
+ @Override
+ public Bytes writeUnsignedByte(int offset, int b) {
+ return writeByte(offset, (byte) b);
+ }
+
+ @Override
+ public Bytes writeUnsignedShort(int offset, int s) {
+ return writeShort(offset, (short) s);
+ }
+
+ @Override
+ public Bytes writeMedium(int offset, int m) {
+ writeByte(offset, (byte) (m >>> 16));
+ writeByte(offset + 1, (byte) (m >>> 8));
+ writeByte(offset + 2, (byte) m);
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedMedium(int offset, int m) {
+ return writeMedium(offset, m);
+ }
+
+ @Override
+ public Bytes writeUnsignedInt(int offset, long i) {
+ return writeInt(offset, (int) i);
+ }
+
+ @Override
+ public Bytes writeString(int offset, String s) {
+ return writeString(offset, s, Charset.defaultCharset());
+ }
+
+ @Override
+ public Bytes writeString(int offset, String s, Charset charset) {
+ if (s == null) {
+ return writeBoolean(offset, Boolean.FALSE);
+ } else {
+ writeBoolean(offset, Boolean.TRUE);
+ byte[] bytes = s.getBytes(charset);
+ return writeUnsignedShort(offset + BYTE, bytes.length)
+ .write(offset + BYTE + SHORT, bytes, 0, bytes.length);
+ }
+ }
+
+ @Override
+ public Bytes writeUTF8(int offset, String s) {
+ return writeString(offset, s, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Bytes flush() {
+ return this;
+ }
+
+ @Override
+ public void close() {
+ open = false;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.concurrent.ReferenceCounted;
+
+import java.nio.ByteOrder;
+
+/**
+ * Navigable byte buffer for input/output operations.
+ * <p>
+ * The byte buffer provides a fluent interface for reading bytes from and writing bytes to some underlying storage
+ * implementation. The {@code Buffer} type is agnostic about the specific underlying storage implementation, but different
+ * buffer implementations may be designed for specific storage layers.
+ * <p>
+ * Aside from the underlying storage implementation, this buffer works very similarly to Java's {@link java.nio.ByteBuffer}.
+ * It intentionally exposes methods that can be easily understood by any developer with experience with {@code ByteBuffer}.
+ * <p>
+ * In order to support reading and writing from buffers, {@code Buffer} implementations maintain a series of pointers to
+ * aid in navigating the buffer.
+ * <p>
+ * Most notable of these pointers is the {@code position}. When values are written to or read from the buffer, the
+ * buffer increments its internal {@code position} according to the number of bytes read. This allows users to iterate
+ * through the bytes in the buffer without maintaining external pointers.
+ * <p>
+ * <pre>
+ * {@code
+ * try (Buffer buffer = DirectBuffer.allocate(1024)) {
+ * buffer.writeInt(1);
+ * buffer.flip();
+ * assert buffer.readInt() == 1;
+ * }
+ * }
+ * </pre>
+ * <p>
+ * Buffers implement {@link ReferenceCounted} in order to keep track of the number of currently
+ * held references to a given buffer. When a buffer is constructed, the buffer contains only {@code 1} reference. This
+ * reference can be released via the {@link Buffer#close()} method which can be called automatically
+ * by using a try-with-resources statement demonstrated above. Additional references to the buffer should be acquired via
+ * {@link ReferenceCounted#acquire()} and released via
+ * {@link ReferenceCounted#release}. Once all references to a buffer have been released - including
+ * the initial reference - the memory will be freed (for in-memory buffers) and writes will be automatically flushed to
+ * disk (for persistent buffers).
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface Buffer extends BytesInput<Buffer>, BufferInput<Buffer>, BytesOutput<Buffer>, BufferOutput<Buffer>, ReferenceCounted<Buffer> {
+
+ /**
+ * Returns whether the buffer has an array.
+ *
+ * @return Whether the buffer has an underlying array.
+ */
+ default boolean hasArray() {
+ return false;
+ }
+
+ /**
+ * Returns the underlying byte array.
+ *
+ * @return the underlying byte array
+ * @throws UnsupportedOperationException if a heap array is not supported
+ */
+ default byte[] array() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the byte order.
+ * <p>
+ * For consistency with {@link java.nio.ByteBuffer}, all buffer implementations are initially in {@link ByteOrder#BIG_ENDIAN} order.
+ *
+ * @return The byte order.
+ */
+ ByteOrder order();
+
+ /**
+ * Sets the byte order, returning a new swapped {@link Buffer} instance.
+ * <p>
+ * By default, all buffers are read and written in {@link ByteOrder#BIG_ENDIAN} order. This provides complete
+ * consistency with {@link java.nio.ByteBuffer}. To flip buffers to {@link ByteOrder#LITTLE_ENDIAN} order, this
+ * buffer's {@code Bytes} instance is decorated by a {@link SwappedBytes} instance which will reverse
+ * read and written bytes using, e.g. {@link Integer#reverseBytes(int)}.
+ *
+ * @param order The byte order.
+ * @return The updated buffer.
+ */
+ Buffer order(ByteOrder order);
+
+ /**
+ * Returns a boolean value indicating whether the buffer is a direct buffer.
+ *
+ * @return Indicates whether the buffer is a direct buffer.
+ */
+ boolean isDirect();
+
+ /**
+ * Returns a boolean value indicating whether the buffer is a read-only buffer.
+ *
+ * @return Indicates whether the buffer is a read-only buffer.
+ */
+ boolean isReadOnly();
+
+ /**
+ * Returns a boolean value indicating whether the buffer is backed by a file.
+ *
+ * @return Indicates whether the buffer is backed by a file.
+ */
+ boolean isFile();
+
+ /**
+ * Returns a read-only view of the buffer.
+ * <p>
+ * The returned buffer will share the underlying {@link Bytes} with which buffer, but the buffer's {@code limit},
+ * {@code capacity}, and {@code position} will be independent of this buffer.
+ *
+ * @return A read-only buffer.
+ */
+ Buffer asReadOnlyBuffer();
+
+ /**
+ * Returns the buffer's starting offset within the underlying {@link Bytes}.
+ * <p>
+ * The offset is used to calculate the absolute position of the buffer's relative {@link Buffer#position() position}
+ * within the underlying {@link Bytes}.
+ *
+ * @return The buffer's offset.
+ */
+ int offset();
+
+ /**
+ * Returns the buffer's capacity.
+ * <p>
+ * The capacity represents the total amount of storage space allocated to the buffer by the underlying storage
+ * implementation. As bytes are written to the buffer, the buffer's capacity may grow up to {@link Buffer#maxCapacity()}.
+ *
+ * @return The buffer's capacity.
+ */
+ int capacity();
+
+ /**
+ * Sets the buffer's capacity.
+ * <p>
+ * The given capacity must be greater than the current {@link Buffer#capacity() capacity} and less than or equal to
+ * {@link Buffer#maxCapacity()}. When the capacity is changed, the underlying {@link Bytes} will be resized via
+ * {@link Bytes#resize(int)}.
+ *
+ * @param capacity The capacity to which to resize the buffer.
+ * @return The resized buffer.
+ * @throws IllegalArgumentException If the given {@code capacity} is less than the current {@code capacity}
+ * or greater than {@link Buffer#maxCapacity()}
+ */
+ Buffer capacity(int capacity);
+
+ /**
+ * Returns the maximum allowed capacity for the buffer.
+ * <p>
+ * The maximum capacity is the limit up to which this buffer's {@link Buffer#capacity() capacity} can be expanded.
+ * While the capacity grows, the maximum capacity is fixed from the moment the buffer is created and cannot change.
+ *
+ * @return The buffer's maximum capacity.
+ */
+ int maxCapacity();
+
+ /**
+ * Sets the buffer's current read/write position.
+ * <p>
+ * The position is an internal cursor that tracks where to write/read bytes in the underlying storage implementation.
+ *
+ * @param position The position to set.
+ * @return This buffer.
+ * @throws IllegalArgumentException If the given position is less than {@code 0} or more than {@link Buffer#limit()}
+ */
+ Buffer position(int position);
+
+ /**
+ * Returns the buffer's read/write limit.
+ * <p>
+ * The limit dictates the highest position to which bytes can be read from or written to the buffer. If the limit is
+ * not explicitly set then it will always equal the buffer's {@link Buffer#maxCapacity() maxCapacity}. Note that the
+ * limit may be set by related methods such as {@link Buffer#flip()}
+ *
+ * @return The buffer's limit.
+ */
+ int limit();
+
+ /**
+ * Sets the buffer's read/write limit.
+ * <p>
+ * The limit dictates the highest position to which bytes can be read from or written to the buffer. The limit must
+ * be within the bounds of the buffer, i.e. greater than {@code 0} and less than or equal to {@link Buffer#capacity()}
+ *
+ * @param limit The limit to set.
+ * @return This buffer.
+ * @throws IllegalArgumentException If the given limit is less than {@code 0} or more than {@link Buffer#capacity()}
+ */
+ Buffer limit(int limit);
+
+ /**
+ * Returns the number of bytes remaining in the buffer until the {@link Buffer#limit()} is reached.
+ * <p>
+ * The bytes remaining is calculated by {@code buffer.limit() - buffer.position()}. If no limit is set on the buffer
+ * then the {@link Buffer#maxCapacity() maxCapacity} will be used.
+ *
+ * @return The number of bytes remaining in the buffer.
+ */
+ @Override
+ int remaining();
+
+ /**
+ * Returns a boolean indicating whether the buffer has bytes remaining.
+ * <p>
+ * If {@link Buffer#remaining()} is greater than {@code 0} then this method will return {@code true}, otherwise
+ * {@code false}
+ *
+ * @return Indicates whether bytes are remaining in the buffer. {@code true} if {@link Buffer#remaining()} is
+ * greater than {@code 0}, {@code false} otherwise.
+ */
+ @Override
+ boolean hasRemaining();
+
+ /**
+ * Flips the buffer.
+ * <p>
+ * The limit is set to the current position and then the position is set to zero. If the mark is defined then it is discarded.
+ * <pre>
+ * {@code
+ * assert buffer.writeLong(1234).flip().readLong() == 1234;
+ * }
+ * </pre>
+ *
+ * @return This buffer.
+ */
+ Buffer flip();
+
+ /**
+ * Sets a mark at the current position.
+ * <p>
+ * The mark is a simple internal reference to the buffer's current position. Marks can be used to reset the buffer
+ * to a specific position after some operation.
+ * <p>
+ * <pre>
+ * {@code
+ * buffer.mark();
+ * buffer.writeInt(1).writeBoolean(true);
+ * buffer.reset();
+ * assert buffer.readInt() == 1;
+ * }
+ * </pre>
+ *
+ * @return This buffer.
+ */
+ Buffer mark();
+
+ /**
+ * Resets the buffer's position to the previously-marked position.
+ * <p>
+ * Invoking this method neither changes nor discards the mark's value.
+ *
+ * @return This buffer.
+ * @throws java.nio.InvalidMarkException If no mark is set.
+ */
+ Buffer reset();
+
+ /**
+ * Rewinds the buffer. The position is set to zero and the mark is discarded.
+ *
+ * @return This buffer.
+ */
+ Buffer rewind();
+
+ /**
+ * Advances the buffer's {@code position} {@code length} bytes.
+ *
+ * @param length The number of bytes to advance this buffer's {@code position}.
+ * @return This buffer.
+ * @throws IndexOutOfBoundsException If {@code length} is greater than {@link Buffer#remaining()}
+ */
+ @Override
+ Buffer skip(int length);
+
+ /**
+ * Clears the buffer.
+ * <p>
+ * The position is set to zero, the limit is set to the capacity, and the mark is discarded.
+ *
+ * @return This buffer.
+ */
+ Buffer clear();
+
+ /**
+ * Compacts the buffer, moving bytes from the current position to the end of the buffer to the head of the buffer.
+ *
+ * @return This buffer.
+ */
+ Buffer compact();
+
+ /**
+ * Returns a duplicate of the buffer.
+ *
+ * @return A duplicate buffer.
+ */
+ Buffer duplicate();
+
+ /**
+ * Returns the bytes underlying the buffer.
+ * <p>
+ * The buffer is a wrapper around {@link Bytes} that handles writing sequences of bytes by tracking positions and
+ * limits. This method returns the {@link Bytes} that this buffer wraps.
+ *
+ * @return The underlying bytes.
+ */
+ Bytes bytes();
+
+ /**
+ * Returns a view of this buffer starting at the current position.
+ * <p>
+ * The returned buffer will contain the same underlying {@link Bytes} instance as this buffer, but its pointers will
+ * be offset by the current {@code position} of this buffer at the time the slice is created. Calls to
+ * {@link Buffer#rewind()} and similar methods on the resulting {@link Buffer} will result in
+ * the buffer's {@code position} being reset to the {@code position} of this buffer at the time the slice was created.
+ * <p>
+ * The returned buffer is reference counted separately from this buffer. Therefore, closing the returned buffer will
+ * release the buffer back to the internal buffer poll and will not result in this buffer being closed. Users should
+ * always call {@link Buffer#close()} once finished using the buffer slice.
+ *
+ * @return A slice of this buffer.
+ * @see Buffer#slice(int)
+ * @see Buffer#slice(int, int)
+ */
+ Buffer slice();
+
+ /**
+ * Returns a view of this buffer of the given length starting at the current position.
+ * <p>
+ * The returned buffer will contain the same underlying {@link Bytes} instance as this buffer, but its pointers will
+ * be offset by the current {@code position} of this buffer at the time the slice is created. Calls to
+ * {@link Buffer#rewind()} and similar methods on the resulting {@link Buffer} will result in
+ * the buffer's {@code position} being reset to the {@code position} of this buffer at the time the slice was created.
+ * <p>
+ * The returned buffer is reference counted separately from this buffer. Therefore, closing the returned buffer will
+ * release the buffer back to the internal buffer poll and will not result in this buffer being closed. Users should
+ * always call {@link Buffer#close()} once finished using the buffer slice.
+ *
+ * @param length The length of the slice.
+ * @return A slice of this buffer.
+ * @see Buffer#slice()
+ * @see Buffer#slice(int, int)
+ */
+ Buffer slice(int length);
+
+ /**
+ * Returns a view of this buffer starting at the given offset with the given length.
+ * <p>
+ * The returned buffer will contain the same underlying {@link Bytes} instance as this buffer, but its pointers will
+ * be offset by the given {@code offset} and its length limited by the given {@code length}. Calls to
+ * {@link Buffer#rewind()} and similar methods on the resulting {@link Buffer} will result in
+ * the buffer's {@code position} being reset to the given {@code offset}.
+ * <p>
+ * The returned buffer is reference counted separately from this buffer. Therefore, closing the returned buffer will
+ * release the buffer back to the internal buffer poll and will not result in this buffer being closed. Users should
+ * always call {@link Buffer#close()} once finished using the buffer slice.
+ *
+ * @param offset The offset at which to begin the slice.
+ * @param length The number of bytes in the slice.
+ * @return The buffer slice.
+ * @throws IndexOutOfBoundsException If the given offset is not contained within the bounds of this buffer
+ * @throws java.nio.BufferUnderflowException If the length of the remaining bytes in the buffer is less than {@code length}
+ * @see Buffer#slice()
+ * @see Buffer#slice(int)
+ */
+ Buffer slice(int offset, int length);
+
+ /**
+ * Reads bytes into the given buffer.
+ * <p>
+ * Bytes will be read starting at the current buffer position until either {@link Buffer#limit()} has been reached.
+ * If {@link Buffer#remaining()} is less than the {@link Buffer#remaining()} of the given buffer, a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @param buffer The buffer into which to read bytes.
+ * @return The buffer.
+ * @throws java.nio.BufferUnderflowException If the given {@link Buffer#remaining()} is greater than this buffer's
+ * {@link Buffer#remaining()}
+ */
+ @Override
+ Buffer read(Buffer buffer);
+
+ /**
+ * Reads bytes into the given byte array.
+ * <p>
+ * Bytes will be read starting at the current buffer position until either the byte array {@code length} or the
+ * {@link Buffer#limit()} has been reached. If {@link Buffer#remaining()}
+ * is less than the {@code length} of the given byte array, a {@link java.nio.BufferUnderflowException} will be
+ * thrown.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @return The buffer.
+ * @throws java.nio.BufferUnderflowException If the given byte array's {@code length} is greater than
+ * {@link Buffer#remaining()}
+ * @see Buffer#read(Bytes, int, int)
+ * @see Buffer#read(int, Bytes, int, int)
+ */
+ @Override
+ Buffer read(Bytes bytes);
+
+ /**
+ * Reads bytes into the given byte array.
+ * <p>
+ * Bytes will be read starting at the current buffer position until either the byte array {@code length} or the
+ * {@link Buffer#limit()} has been reached. If {@link Buffer#remaining()}
+ * is less than the {@code length} of the given byte array, a {@link java.nio.BufferUnderflowException} will be
+ * thrown.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @return The buffer.
+ * @throws java.nio.BufferUnderflowException If the given byte array's {@code length} is greater than
+ * {@link Buffer#remaining()}
+ * @see Buffer#read(byte[], int, int)
+ * @see Buffer#read(int, byte[], int, int)
+ */
+ @Override
+ Buffer read(byte[] bytes);
+
+ /**
+ * Reads bytes into the given byte array starting at the current position.
+ * <p>
+ * Bytes will be read from the current position up to the given length. If the provided {@code length} is
+ * greater than {@link Buffer#remaining()} then a {@link java.nio.BufferUnderflowException} will
+ * be thrown. If the {@code offset} is out of bounds of the buffer then an {@link IndexOutOfBoundsException}
+ * will be thrown.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @param dstOffset The offset at which to write bytes into the given buffer
+ * @return The buffer.
+ * @throws java.nio.BufferUnderflowException If {@code length} is greater than {@link Buffer#remaining()}
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#read(Bytes)
+ * @see Buffer#read(int, Bytes, int, int)
+ */
+ @Override
+ Buffer read(Bytes bytes, int dstOffset, int length);
+
+ /**
+ * Reads bytes into the given byte array starting at the given offset up to the given length.
+ * <p>
+ * Bytes will be read from the given starting offset up to the given length. If the provided {@code length} is
+ * greater than {@link Buffer#limit() - srcOffset} then a {@link java.nio.BufferUnderflowException} will
+ * be thrown. If the {@code srcOffset} is out of bounds of the buffer then an {@link IndexOutOfBoundsException}
+ * will be thrown.
+ *
+ * @param srcOffset The offset from which to start reading bytes.
+ * @param bytes The byte array into which to read bytes.
+ * @param dstOffset The offset at which to write bytes into the given buffer
+ * @return The buffer.
+ * @throws java.nio.BufferUnderflowException If {@code length} is greater than {@link Buffer#remaining()}
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#read(Bytes)
+ * @see Buffer#read(Bytes, int, int)
+ */
+ @Override
+ Buffer read(int srcOffset, Bytes bytes, int dstOffset, int length);
+
+ /**
+ * Reads bytes into the given byte array starting at current position up to the given length.
+ * <p>
+ * Bytes will be read from the current position up to the given length. If the provided {@code length} is
+ * greater than {@link Buffer#remaining()} then a {@link java.nio.BufferUnderflowException} will
+ * be thrown. If the {@code offset} is out of bounds of the buffer then an {@link IndexOutOfBoundsException}
+ * will be thrown.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @param offset The offset at which to write bytes into the given buffer
+ * @return The buffer.
+ * @throws java.nio.BufferUnderflowException If {@code length} is greater than {@link Buffer#remaining()}
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#read(byte[])
+ * @see Buffer#read(int, byte[], int, int)
+ */
+ @Override
+ Buffer read(byte[] bytes, int offset, int length);
+
+ /**
+ * Reads bytes into the given byte array starting at the given offset up to the given length.
+ * <p>
+ * Bytes will be read from the given starting offset up to the given length. If the provided {@code length} is
+ * greater than {@link Buffer#remaining()} then a {@link java.nio.BufferUnderflowException} will
+ * be thrown. If the {@code offset} is out of bounds of the buffer then an {@link IndexOutOfBoundsException}
+ * will be thrown.
+ *
+ * @param srcOffset The offset from which to start reading bytes.
+ * @param bytes The byte array into which to read bytes.
+ * @param dstOffset The offset at which to write bytes into the given buffer
+ * @return The buffer.
+ * @throws java.nio.BufferUnderflowException If {@code length} is greater than {@link Buffer#remaining()}
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#read(byte[])
+ * @see Buffer#read(byte[], int, int)
+ */
+ @Override
+ Buffer read(int srcOffset, byte[] bytes, int dstOffset, int length);
+
+ /**
+ * Reads a byte from the buffer at the current position.
+ * <p>
+ * When the byte is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#BYTE}. If
+ * there are no bytes remaining in the buffer then a {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read byte.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#BYTE}
+ * @see Buffer#readByte(int)
+ */
+ @Override
+ int readByte();
+
+ /**
+ * Reads a byte from the buffer at the given offset.
+ * <p>
+ * The byte will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the byte.
+ * @return The read byte.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readByte()
+ */
+ @Override
+ int readByte(int offset);
+
+ /**
+ * Reads an unsigned byte from the buffer at the current position.
+ * <p>
+ * When the byte is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#BYTE}. If
+ * there are no bytes remaining in the buffer then a {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read byte.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#BYTE}
+ * @see Buffer#readUnsignedByte(int)
+ */
+ @Override
+ int readUnsignedByte();
+
+ /**
+ * Reads an unsigned byte from the buffer at the given offset.
+ * <p>
+ * The byte will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the byte.
+ * @return The read byte.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readUnsignedByte()
+ */
+ @Override
+ int readUnsignedByte(int offset);
+
+ /**
+ * Reads a 16-bit character from the buffer at the current position.
+ * <p>
+ * When the character is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#CHARACTER}.
+ * If there are less than {@link Bytes#CHARACTER} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read character.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#CHARACTER}
+ * @see Buffer#readChar(int)
+ */
+ @Override
+ char readChar();
+
+ /**
+ * Reads a 16-bit character from the buffer at the given offset.
+ * <p>
+ * The character will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the character.
+ * @return The read character.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readChar()
+ */
+ @Override
+ char readChar(int offset);
+
+ /**
+ * Reads a 16-bit signed integer from the buffer at the current position.
+ * <p>
+ * When the short is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#SHORT}.
+ * If there are less than {@link Bytes#SHORT} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read short.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#SHORT}
+ * @see Buffer#readShort(int)
+ */
+ @Override
+ short readShort();
+
+ /**
+ * Reads a 16-bit signed integer from the buffer at the given offset.
+ * <p>
+ * The short will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the short.
+ * @return The read short.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readShort()
+ */
+ @Override
+ short readShort(int offset);
+
+ /**
+ * Reads a 16-bit unsigned integer from the buffer at the current position.
+ * <p>
+ * When the short is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#SHORT}.
+ * If there are less than {@link Bytes#SHORT} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read short.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#SHORT}
+ * @see Buffer#readUnsignedShort(int)
+ */
+ @Override
+ int readUnsignedShort();
+
+ /**
+ * Reads a 16-bit unsigned integer from the buffer at the given offset.
+ * <p>
+ * The short will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the short.
+ * @return The read short.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readUnsignedShort()
+ */
+ @Override
+ int readUnsignedShort(int offset);
+
+ /**
+ * Reads a 32-bit signed integer from the buffer at the current position.
+ * <p>
+ * When the integer is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#INTEGER}.
+ * If there are less than {@link Bytes#INTEGER} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read integer.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#INTEGER}
+ * @see Buffer#readInt(int)
+ */
+ @Override
+ int readInt();
+
+ /**
+ * Reads a 32-bit signed integer from the buffer at the given offset.
+ * <p>
+ * The integer will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the integer.
+ * @return The read integer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readInt()
+ */
+ @Override
+ int readInt(int offset);
+
+ /**
+ * Reads a 32-bit unsigned integer from the buffer at the current position.
+ * <p>
+ * When the integer is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#INTEGER}.
+ * If there are less than {@link Bytes#INTEGER} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read integer.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#INTEGER}
+ * @see Buffer#readUnsignedInt(int)
+ */
+ @Override
+ long readUnsignedInt();
+
+ /**
+ * Reads a 32-bit unsigned integer from the buffer at the given offset.
+ * <p>
+ * The integer will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the integer.
+ * @return The read integer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readUnsignedInt()
+ */
+ @Override
+ long readUnsignedInt(int offset);
+
+ /**
+ * Reads a 64-bit signed integer from the buffer at the current position.
+ * <p>
+ * When the long is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#LONG}.
+ * If there are less than {@link Bytes#LONG} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read long.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#LONG}
+ * @see Buffer#readLong(int)
+ */
+ @Override
+ long readLong();
+
+ /**
+ * Reads a 64-bit signed integer from the buffer at the given offset.
+ * <p>
+ * The long will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the long.
+ * @return The read long.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readLong()
+ */
+ @Override
+ long readLong(int offset);
+
+ /**
+ * Reads a single-precision 32-bit floating point number from the buffer at the current position.
+ * <p>
+ * When the float is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#FLOAT}.
+ * If there are less than {@link Bytes#FLOAT} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read float.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#FLOAT}
+ * @see Buffer#readFloat(int)
+ */
+ @Override
+ float readFloat();
+
+ /**
+ * Reads a single-precision 32-bit floating point number from the buffer at the given offset.
+ * <p>
+ * The float will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the float.
+ * @return The read float.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readFloat()
+ */
+ @Override
+ float readFloat(int offset);
+
+ /**
+ * Reads a double-precision 64-bit floating point number from the buffer at the current position.
+ * <p>
+ * When the double is read from the buffer, the buffer's {@code position} will be advanced by {@link Bytes#DOUBLE}.
+ * If there are less than {@link Bytes#DOUBLE} bytes remaining in the buffer then a
+ * {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read double.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@link Bytes#DOUBLE}
+ * @see Buffer#readDouble(int)
+ */
+ @Override
+ double readDouble();
+
+ /**
+ * Reads a double-precision 64-bit floating point number from the buffer at the given offset.
+ * <p>
+ * The double will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the double.
+ * @return The read double.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readDouble()
+ */
+ @Override
+ double readDouble(int offset);
+
+ /**
+ * Reads a 1 byte boolean from the buffer at the current position.
+ * <p>
+ * When the boolean is read from the buffer, the buffer's {@code position} will be advanced by {@code 1}.
+ * If there are no bytes remaining in the buffer then a {@link java.nio.BufferUnderflowException} will be thrown.
+ *
+ * @return The read boolean.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@code 1}
+ * @see Buffer#readBoolean(int)
+ */
+ @Override
+ boolean readBoolean();
+
+ /**
+ * Reads a 1 byte boolean from the buffer at the given offset.
+ * <p>
+ * The boolean will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the boolean.
+ * @return The read boolean.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readBoolean()
+ */
+ @Override
+ boolean readBoolean(int offset);
+
+ /**
+ * Reads a UTF-8 string from the buffer at the current position.
+ * <p>
+ * When the string is read from the buffer, the buffer's {@code position} will be advanced by 2 bytes plus the byte
+ * length of the string. If there are no bytes remaining in the buffer then a {@link java.nio.BufferUnderflowException}
+ * will be thrown.
+ *
+ * @return The read string.
+ * @throws java.nio.BufferUnderflowException If {@link Buffer#remaining()} is less than {@code 1}
+ * @see Buffer#readUTF8(int)
+ */
+ @Override
+ String readUTF8();
+
+ /**
+ * Reads a UTF-8 string from the buffer at the given offset.
+ * <p>
+ * The string will be read from the given offset. If the given index is out of the bounds of the buffer then a
+ * {@link IndexOutOfBoundsException} will be thrown.
+ *
+ * @param offset The offset at which to read the boolean.
+ * @return The read string.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#readUTF8()
+ */
+ @Override
+ String readUTF8(int offset);
+
+ /**
+ * Writes a buffer to the buffer.
+ * <p>
+ * When the buffer is written to the buffer, the buffer's {@code position} will be advanced by the number of bytes
+ * in the provided buffer. If the provided {@link Buffer#remaining()} exceeds {@link Buffer#remaining()} then an
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param buffer The buffer to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If the given buffer's {@link Buffer#remaining()} bytes exceeds this buffer's
+ * remaining bytes.
+ */
+ @Override
+ Buffer write(Buffer buffer);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ * <p>
+ * When the bytes are written to the buffer, the buffer's {@code position} will be advanced by the number of bytes
+ * in the provided byte array. If the number of bytes exceeds {@link Buffer#limit()} then an
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param bytes The array of bytes to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If the number of bytes exceeds the buffer's remaining bytes.
+ * @see Buffer#write(Bytes, int, int)
+ * @see Buffer#write(int, Bytes, int, int)
+ */
+ @Override
+ Buffer write(Bytes bytes);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ * <p>
+ * When the bytes are written to the buffer, the buffer's {@code position} will be advanced by the number of bytes
+ * in the provided byte array. If the number of bytes exceeds {@link Buffer#limit()} then an
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param bytes The array of bytes to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If the number of bytes exceeds the buffer's remaining bytes.
+ * @see Buffer#write(byte[], int, int)
+ * @see Buffer#write(int, byte[], int, int)
+ */
+ @Override
+ Buffer write(byte[] bytes);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ * <p>
+ * The bytes will be written starting at the current position up to the given length. If the length of the byte array
+ * is larger than the provided {@code length} then only {@code length} bytes will be read from the array. If the
+ * provided {@code length} is greater than the remaining bytes in this buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param bytes The array of bytes to write.
+ * @param offset The offset at which to start writing the bytes.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are not enough bytes remaining in the buffer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer.
+ * @see Buffer#write(Bytes)
+ * @see Buffer#write(int, Bytes, int, int)
+ */
+ @Override
+ Buffer write(Bytes bytes, int offset, int length);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ * <p>
+ * The bytes will be written starting at the given offset up to the given length. If the remaining bytes in the byte array
+ * is larger than the provided {@code length} then only {@code length} bytes will be read from the array. If the
+ * provided {@code length} is greater than {@link Buffer#limit()} minus {@code offset} then a
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to start writing the bytes.
+ * @param src The array of bytes to write.
+ * @param srcOffset The offset at which to begin reading bytes from the source.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are not enough bytes remaining in the buffer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer.
+ * @see Buffer#write(Bytes)
+ * @see Buffer#write(Bytes, int, int)
+ */
+ @Override
+ Buffer write(int offset, Bytes src, int srcOffset, int length);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ * <p>
+ * The bytes will be written starting at the current position up to the given length. If the length of the byte array
+ * is larger than the provided {@code length} then only {@code length} bytes will be read from the array. If the
+ * provided {@code length} is greater than the remaining bytes in this buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param bytes The array of bytes to write.
+ * @param offset The offset at which to start writing the bytes.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are not enough bytes remaining in the buffer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer.
+ * @see Buffer#write(byte[])
+ * @see Buffer#write(int, byte[], int, int)
+ */
+ @Override
+ Buffer write(byte[] bytes, int offset, int length);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ * <p>
+ * The bytes will be written starting at the given offset up to the given length. If the remaining bytes in the byte array
+ * is larger than the provided {@code length} then only {@code length} bytes will be read from the array. If the
+ * provided {@code length} is greater than {@link Buffer#limit()} minus {@code offset} then a
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to start writing the bytes.
+ * @param src The array of bytes to write.
+ * @param srcOffset The offset at which to begin reading bytes from the source.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are not enough bytes remaining in the buffer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer.
+ * @see Buffer#write(byte[])
+ * @see Buffer#write(byte[], int, int)
+ */
+ @Override
+ Buffer write(int offset, byte[] src, int srcOffset, int length);
+
+ /**
+ * Writes a byte to the buffer at the current position.
+ * <p>
+ * When the byte is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#BYTE}. If
+ * there are no bytes remaining in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param b The byte to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are no bytes remaining in the buffer.
+ * @see Buffer#writeByte(int, int)
+ */
+ @Override
+ Buffer writeByte(int b);
+
+ /**
+ * Writes a byte to the buffer at the given offset.
+ * <p>
+ * The byte will be written at the given offset. If there are no bytes remaining in the buffer then a
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the byte.
+ * @param b The byte to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are not enough bytes remaining in the buffer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeByte(int)
+ */
+ @Override
+ Buffer writeByte(int offset, int b);
+
+ /**
+ * Writes an unsigned byte to the buffer at the current position.
+ * <p>
+ * When the byte is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#BYTE}. If
+ * there are no bytes remaining in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param b The byte to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are no bytes remaining in the buffer.
+ * @see Buffer#writeUnsignedByte(int, int)
+ */
+ @Override
+ Buffer writeUnsignedByte(int b);
+
+ /**
+ * Writes an unsigned byte to the buffer at the given offset.
+ * <p>
+ * The byte will be written at the given offset. If there are no bytes remaining in the buffer then a
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the byte.
+ * @param b The byte to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If there are not enough bytes remaining in the buffer.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeUnsignedByte(int)
+ */
+ @Override
+ Buffer writeUnsignedByte(int offset, int b);
+
+ /**
+ * Writes a 16-bit character to the buffer at the current position.
+ * <p>
+ * When the character is written to the buffer, the buffer's {@code position} will be advanced by
+ * {@link Bytes#CHARACTER}. If less than {@code 2} bytes are remaining in the buffer then a
+ * {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param c The character to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#CHARACTER}.
+ * @see Buffer#writeChar(int, char)
+ */
+ @Override
+ Buffer writeChar(char c);
+
+ /**
+ * Writes a 16-bit character to the buffer at the given offset.
+ * <p>
+ * The character will be written at the given offset. If there are less than {@link Bytes#CHARACTER} bytes remaining
+ * in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the character.
+ * @param c The character to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#CHARACTER}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeChar(char)
+ */
+ @Override
+ Buffer writeChar(int offset, char c);
+
+ /**
+ * Writes a 16-bit signed integer to the buffer at the current position.
+ * <p>
+ * When the short is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#SHORT}. If
+ * less than {@link Bytes#SHORT} bytes are remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param s The short to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#SHORT}.
+ * @see Buffer#writeShort(int, short)
+ */
+ @Override
+ Buffer writeShort(short s);
+
+ /**
+ * Writes a 16-bit signed integer to the buffer at the given offset.
+ * <p>
+ * The short will be written at the given offset. If there are less than {@link Bytes#SHORT} bytes remaining in the buffer
+ * then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the short.
+ * @param s The short to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#SHORT}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeShort(short)
+ */
+ @Override
+ Buffer writeShort(int offset, short s);
+
+ /**
+ * Writes a 16-bit signed integer to the buffer at the current position.
+ * <p>
+ * When the short is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#SHORT}. If
+ * less than {@link Bytes#SHORT} bytes are remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param s The short to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#SHORT}.
+ * @see Buffer#writeUnsignedShort(int, int)
+ */
+ @Override
+ Buffer writeUnsignedShort(int s);
+
+ /**
+ * Writes a 16-bit signed integer to the buffer at the given offset.
+ * <p>
+ * The short will be written at the given offset. If there are less than {@link Bytes#SHORT} bytes remaining in the buffer
+ * then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the short.
+ * @param s The short to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#SHORT}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeUnsignedShort(int)
+ */
+ @Override
+ Buffer writeUnsignedShort(int offset, int s);
+
+ /**
+ * Writes a 32-bit signed integer to the buffer at the current position.
+ * <p>
+ * When the integer is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#INTEGER}.
+ * If less than {@link Bytes#INTEGER} bytes are remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param i The integer to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#INTEGER}.
+ * @see Buffer#writeInt(int, int)
+ */
+ @Override
+ Buffer writeInt(int i);
+
+ /**
+ * Writes a 32-bit signed integer to the buffer at the given offset.
+ * <p>
+ * The integer will be written at the given offset. If there are less than {@link Bytes#INTEGER} bytes remaining
+ * in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the integer.
+ * @param i The integer to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#INTEGER}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeInt(int)
+ */
+ @Override
+ Buffer writeInt(int offset, int i);
+
+ /**
+ * Writes a 32-bit signed integer to the buffer at the current position.
+ * <p>
+ * When the integer is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#INTEGER}.
+ * If less than {@link Bytes#INTEGER} bytes are remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param i The integer to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#INTEGER}.
+ * @see Buffer#writeUnsignedInt(int, long)
+ */
+ @Override
+ Buffer writeUnsignedInt(long i);
+
+ /**
+ * Writes a 32-bit signed integer to the buffer at the given offset.
+ * <p>
+ * The integer will be written at the given offset. If there are less than {@link Bytes#INTEGER} bytes remaining
+ * in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the integer.
+ * @param i The integer to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#INTEGER}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeUnsignedInt(long)
+ */
+ @Override
+ Buffer writeUnsignedInt(int offset, long i);
+
+ /**
+ * Writes a 64-bit signed integer to the buffer at the current position.
+ * <p>
+ * When the long is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#LONG}.
+ * If less than {@link Bytes#LONG} bytes are remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param l The long to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#LONG}.
+ * @see Buffer#writeLong(int, long)
+ */
+ @Override
+ Buffer writeLong(long l);
+
+ /**
+ * Writes a 64-bit signed integer to the buffer at the given offset.
+ * <p>
+ * The long will be written at the given offset. If there are less than {@link Bytes#LONG} bytes remaining
+ * in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the long.
+ * @param l The long to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#LONG}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeLong(long)
+ */
+ @Override
+ Buffer writeLong(int offset, long l);
+
+ /**
+ * Writes a single-precision 32-bit floating point number to the buffer at the current position.
+ * <p>
+ * When the float is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#FLOAT}.
+ * If less than {@link Bytes#FLOAT} bytes are remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param f The float to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#FLOAT}.
+ * @see Buffer#writeFloat(int, float)
+ */
+ @Override
+ Buffer writeFloat(float f);
+
+ /**
+ * Writes a single-precision 32-bit floating point number to the buffer at the given offset.
+ * <p>
+ * The float will be written at the given offset. If there are less than {@link Bytes#FLOAT} bytes remaining
+ * in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the float.
+ * @param f The float to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#FLOAT}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeFloat(float)
+ */
+ @Override
+ Buffer writeFloat(int offset, float f);
+
+ /**
+ * Writes a double-precision 64-bit floating point number to the buffer at the current position.
+ * <p>
+ * When the double is written to the buffer, the buffer's {@code position} will be advanced by {@link Bytes#DOUBLE}.
+ * If less than {@link Bytes#DOUBLE} bytes are remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param d The double to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#DOUBLE}.
+ * @see Buffer#writeDouble(int, double)
+ */
+ @Override
+ Buffer writeDouble(double d);
+
+ /**
+ * Writes a double-precision 64-bit floating point number to the buffer at the given offset.
+ * <p>
+ * The double will be written at the given offset. If there are less than {@link Bytes#DOUBLE} bytes remaining
+ * in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the double.
+ * @param d The double to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@link Bytes#DOUBLE}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeDouble(double)
+ */
+ @Override
+ Buffer writeDouble(int offset, double d);
+
+ /**
+ * Writes a 1 byte boolean to the buffer at the current position.
+ * <p>
+ * When the boolean is written to the buffer, the buffer's {@code position} will be advanced by {@code 1}.
+ * If there are no bytes remaining in the buffer then a {@link java.nio.BufferOverflowException}
+ * will be thrown.
+ *
+ * @param b The boolean to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If the number of bytes exceeds the buffer's remaining bytes.
+ * @see Buffer#writeBoolean(int, boolean)
+ */
+ @Override
+ Buffer writeBoolean(boolean b);
+
+ /**
+ * Writes a 1 byte boolean to the buffer at the given offset.
+ * <p>
+ * The boolean will be written as a single byte at the given offset. If there are no bytes remaining in the buffer
+ * then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the boolean.
+ * @param b The boolean to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@code 1}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeBoolean(boolean)
+ */
+ @Override
+ Buffer writeBoolean(int offset, boolean b);
+
+ /**
+ * Writes a UTF-8 string to the buffer at the current position.
+ * <p>
+ * The string will be written with a two-byte unsigned byte length followed by the UTF-8 bytes. If there are not enough
+ * bytes remaining in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param s The string to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If the number of bytes exceeds the buffer's remaining bytes.
+ * @see Buffer#writeUTF8(int, String)
+ */
+ @Override
+ Buffer writeUTF8(String s);
+
+ /**
+ * Writes a UTF-8 string to the buffer at the given offset.
+ * <p>
+ * The string will be written with a two-byte unsigned byte length followed by the UTF-8 bytes. If there are not enough
+ * bytes remaining in the buffer then a {@link java.nio.BufferOverflowException} will be thrown.
+ *
+ * @param offset The offset at which to write the string.
+ * @param s The string to write.
+ * @return The written buffer.
+ * @throws java.nio.BufferOverflowException If {@link Buffer#remaining()} is less than {@code 1}.
+ * @throws IndexOutOfBoundsException If the given offset is out of the bounds of the buffer. Note that
+ * bounds are determined by the buffer's {@link Buffer#limit()} rather than capacity.
+ * @see Buffer#writeUTF8(String)
+ */
+ @Override
+ Buffer writeUTF8(int offset, String s);
+
+ /**
+ * Closes the buffer.
+ * <p>
+ * This method effectively acts as an alias to {@link Buffer#release()} and allows buffers to
+ * be used as resources in try-with-resources statements. When the buffer is closed the internal reference counter
+ * defined by {@link ReferenceCounted} will be decremented. However, if references to the
+ * buffer still exist then those references will be allowed to continue to operate on the buffer until all references
+ * have been released.
+ */
+ @Override
+ void close();
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+/**
+ * Buffer allocator.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface BufferAllocator {
+
+ /**
+ * Allocates a dynamic capacity buffer.
+ *
+ * @return The allocated buffer.
+ */
+ Buffer allocate();
+
+ /**
+ * Allocates a dynamic capacity buffer with the given initial capacity.
+ *
+ * @param initialCapacity The initial buffer capacity.
+ * @return The allocated buffer.
+ */
+ Buffer allocate(int initialCapacity);
+
+ /**
+ * Allocates a new buffer.
+ *
+ * @param initialCapacity The initial buffer capacity.
+ * @param maxCapacity The maximum buffer capacity.
+ * @return The allocated buffer.
+ */
+ Buffer allocate(int initialCapacity, int maxCapacity);
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.charset.Charset;
+import java.util.function.Function;
+
+/**
+ * Readable buffer.
+ * <p>
+ * This interface exposes methods for reading from a byte buffer. Readable buffers maintain a small amount of state
+ * regarding current cursor positions and limits similar to the behavior of {@link java.nio.ByteBuffer}.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface BufferInput<T extends BufferInput<?>> extends AutoCloseable {
+
+ /**
+ * Returns the buffer's current read/write position.
+ * <p>
+ * The position is an internal cursor that tracks where to write/read bytes in the underlying storage implementation.
+ * As bytes are written to or read from the buffer, the position will advance based on the number of bytes read.
+ *
+ * @return The buffer's current position.
+ */
+ int position();
+
+ /**
+ * Returns the number of bytes remaining in the input.
+ *
+ * @return The number of bytes remaining in the input.
+ */
+ int remaining();
+
+ /**
+ * Returns a boolean value indicating whether the input has bytes remaining.
+ *
+ * @return Indicates whether bytes remain to be read from the input.
+ */
+ boolean hasRemaining();
+
+ /**
+ * Skips the given number of bytes in the input.
+ *
+ * @param bytes The number of bytes to attempt to skip.
+ * @return The skipped input.
+ */
+ T skip(int bytes);
+
+ /**
+ * Reads bytes into the given byte array.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @return The buffer.
+ */
+ T read(Bytes bytes);
+
+ /**
+ * Reads bytes into the given byte array.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @return The buffer.
+ */
+ T read(byte[] bytes);
+
+ /**
+ * Reads bytes into the given byte array starting at the current position.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @param offset The offset at which to write bytes into the given buffer
+ * @return The buffer.
+ */
+ T read(Bytes bytes, int offset, int length);
+
+ /**
+ * Reads bytes into the given byte array starting at current position up to the given length.
+ *
+ * @param bytes The byte array into which to read bytes.
+ * @param offset The offset at which to write bytes into the given buffer
+ * @return The buffer.
+ */
+ T read(byte[] bytes, int offset, int length);
+
+ /**
+ * Reads bytes into the given buffer.
+ *
+ * @param buffer The buffer into which to read bytes.
+ * @return The buffer.
+ */
+ T read(Buffer buffer);
+
+ /**
+ * Reads an object from the buffer.
+ *
+ * @param decoder the object decoder
+ * @param <U> the type of the object to read
+ * @return the read object.
+ */
+ default <U> U readObject(Function<byte[], U> decoder) {
+ byte[] bytes = readBytes(readInt());
+ return decoder.apply(bytes);
+ }
+
+ /**
+ * Reads a byte array.
+ *
+ * @param length The byte array length
+ * @return The read byte array.
+ */
+ default byte[] readBytes(int length) {
+ byte[] bytes = new byte[length];
+ read(bytes);
+ return bytes;
+ }
+
+ /**
+ * Reads a byte from the buffer at the current position.
+ *
+ * @return The read byte.
+ */
+ int readByte();
+
+ /**
+ * Reads an unsigned byte from the buffer at the current position.
+ *
+ * @return The read byte.
+ */
+ int readUnsignedByte();
+
+ /**
+ * Reads a 16-bit character from the buffer at the current position.
+ *
+ * @return The read character.
+ */
+ char readChar();
+
+ /**
+ * Reads a 16-bit signed integer from the buffer at the current position.
+ *
+ * @return The read short.
+ */
+ short readShort();
+
+ /**
+ * Reads a 16-bit unsigned integer from the buffer at the current position.
+ *
+ * @return The read short.
+ */
+ int readUnsignedShort();
+
+ /**
+ * Reads a 24-bit signed integer from the buffer at the current position.
+ *
+ * @return The read integer.
+ */
+ int readMedium();
+
+ /**
+ * Reads a 24-bit unsigned integer from the buffer at the current position.
+ *
+ * @return The read integer.
+ */
+ int readUnsignedMedium();
+
+ /**
+ * Reads a 32-bit signed integer from the buffer at the current position.
+ *
+ * @return The read integer.
+ */
+ int readInt();
+
+ /**
+ * Reads a 32-bit unsigned integer from the buffer at the current position.
+ *
+ * @return The read integer.
+ */
+ long readUnsignedInt();
+
+ /**
+ * Reads a 64-bit signed integer from the buffer at the current position.
+ *
+ * @return The read long.
+ */
+ long readLong();
+
+ /**
+ * Reads a single-precision 32-bit floating point number from the buffer at the current position.
+ *
+ * @return The read float.
+ */
+ float readFloat();
+
+ /**
+ * Reads a double-precision 64-bit floating point number from the buffer at the current position.
+ *
+ * @return The read double.
+ */
+ double readDouble();
+
+ /**
+ * Reads a 1 byte boolean from the buffer at the current position.
+ *
+ * @return The read boolean.
+ */
+ boolean readBoolean();
+
+ /**
+ * Reads a string from the buffer at the current position.
+ *
+ * @return The read string.
+ */
+ String readString();
+
+ /**
+ * Reads a string from the buffer at the current position.
+ *
+ * @param charset The character set with which to decode the string.
+ * @return The read string.
+ */
+ String readString(Charset charset);
+
+ /**
+ * Reads a UTF-8 string from the buffer at the current position.
+ *
+ * @return The read string.
+ */
+ String readUTF8();
+
+ @Override
+ void close();
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.charset.Charset;
+import java.util.function.Function;
+
+/**
+ * Writable buffer.
+ * <p>
+ * This interface exposes methods for writing to a byte buffer. Writable buffers maintain a small amount of state
+ * regarding current cursor positions and limits similar to the behavior of {@link java.nio.ByteBuffer}.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface BufferOutput<T extends BufferOutput<?>> extends AutoCloseable {
+
+ /**
+ * Writes an array of bytes to the buffer.
+ *
+ * @param bytes The array of bytes to write.
+ * @return The written buffer.
+ */
+ T write(Bytes bytes);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ *
+ * @param bytes The array of bytes to write.
+ * @return The written buffer.
+ */
+ T write(byte[] bytes);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ *
+ * @param bytes The array of bytes to write.
+ * @param offset The offset at which to start writing the bytes.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ */
+ T write(Bytes bytes, int offset, int length);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ *
+ * @param bytes The array of bytes to write.
+ * @param offset The offset at which to start writing the bytes.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ */
+ T write(byte[] bytes, int offset, int length);
+
+ /**
+ * Writes a buffer to the buffer.
+ *
+ * @param buffer The buffer to write.
+ * @return The written buffer.
+ */
+ T write(Buffer buffer);
+
+ /**
+ * Writes an object to the snapshot.
+ *
+ * @param object the object to write
+ * @param encoder the object encoder
+ * @return The snapshot writer.
+ */
+ @SuppressWarnings("unchecked")
+ default <U> T writeObject(U object, Function<U, byte[]> encoder) {
+ byte[] bytes = encoder.apply(object);
+ writeInt(bytes.length).write(bytes);
+ return (T) this;
+ }
+
+ /**
+ * Writes a byte array.
+ *
+ * @param bytes The byte array to write.
+ * @return The written buffer.
+ */
+ @SuppressWarnings("unchecked")
+ default T writeBytes(byte[] bytes) {
+ write(bytes);
+ return (T) this;
+ }
+
+ /**
+ * Writes a byte to the buffer.
+ *
+ * @param b The byte to write.
+ * @return The written buffer.
+ */
+ T writeByte(int b);
+
+ /**
+ * Writes an unsigned byte to the buffer.
+ *
+ * @param b The byte to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedByte(int b);
+
+ /**
+ * Writes a 16-bit character to the buffer.
+ *
+ * @param c The character to write.
+ * @return The written buffer.
+ */
+ T writeChar(char c);
+
+ /**
+ * Writes a 16-bit signed integer to the buffer.
+ *
+ * @param s The short to write.
+ * @return The written buffer.
+ */
+ T writeShort(short s);
+
+ /**
+ * Writes a 16-bit unsigned integer to the buffer.
+ *
+ * @param s The short to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedShort(int s);
+
+ /**
+ * Writes a 24-bit signed integer to the buffer.
+ *
+ * @param m The integer to write.
+ * @return The written buffer.
+ */
+ T writeMedium(int m);
+
+ /**
+ * Writes a 24-bit unsigned integer to the buffer.
+ *
+ * @param m The integer to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedMedium(int m);
+
+ /**
+ * Writes a 32-bit signed integer to the buffer.
+ *
+ * @param i The integer to write.
+ * @return The written buffer.
+ */
+ T writeInt(int i);
+
+ /**
+ * Writes a 32-bit unsigned integer to the buffer.
+ *
+ * @param i The integer to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedInt(long i);
+
+ /**
+ * Writes a 64-bit signed integer to the buffer.
+ *
+ * @param l The long to write.
+ * @return The written buffer.
+ */
+ T writeLong(long l);
+
+ /**
+ * Writes a single-precision 32-bit floating point number to the buffer.
+ *
+ * @param f The float to write.
+ * @return The written buffer.
+ */
+ T writeFloat(float f);
+
+ /**
+ * Writes a double-precision 64-bit floating point number to the buffer.
+ *
+ * @param d The double to write.
+ * @return The written buffer.
+ */
+ T writeDouble(double d);
+
+ /**
+ * Writes a 1 byte boolean to the buffer.
+ *
+ * @param b The boolean to write.
+ * @return The written buffer.
+ */
+ T writeBoolean(boolean b);
+
+ /**
+ * Writes a string to the buffer.
+ *
+ * @param s The string to write.
+ * @return The written buffer.
+ */
+ T writeString(String s);
+
+ /**
+ * Writes a string to the buffer.
+ *
+ * @param s The string to write.
+ * @param charset The character set with which to encode the string.
+ * @return The written buffer.
+ */
+ T writeString(String s, Charset charset);
+
+ /**
+ * Writes a UTF-8 string to the buffer.
+ *
+ * @param s The string to write.
+ * @return The written buffer.
+ */
+ T writeUTF8(String s);
+
+ /**
+ * Flushes the buffer to the underlying persistence layer.
+ *
+ * @return The flushed buffer.
+ */
+ T flush();
+
+ @Override
+ void close();
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.concurrent.ReferenceFactory;
+import io.atomix.utils.concurrent.ReferencePool;
+
+/**
+ * Buffer pool.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class BufferPool extends ReferencePool<Buffer> {
+
+ public BufferPool(ReferenceFactory<Buffer> factory) {
+ super(factory);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.concurrent.ReferenceManager;
+
+/**
+ * {@link java.nio.ByteBuffer} based buffer.
+ */
+public abstract class ByteBufferBuffer extends AbstractBuffer {
+ protected final ByteBufferBytes bytes;
+
+ public ByteBufferBuffer(ByteBufferBytes bytes, ReferenceManager<Buffer> referenceManager) {
+ super(bytes, referenceManager);
+ this.bytes = bytes;
+ }
+
+ public ByteBufferBuffer(ByteBufferBytes bytes, int offset, int initialCapacity, int maxCapacity, ReferenceManager<Buffer> referenceManager) {
+ super(bytes, offset, initialCapacity, maxCapacity, referenceManager);
+ this.bytes = bytes;
+ }
+
+ @Override
+ public byte[] array() {
+ return bytes.array();
+ }
+
+ @Override
+ protected void compact(int from, int to, int length) {
+ byte[] bytes = new byte[1024];
+ int position = from;
+ while (position < from + length) {
+ int size = Math.min((from + length) - position, 1024);
+ this.bytes.read(position, bytes, 0, size);
+ this.bytes.write(0, bytes, 0, size);
+ position += size;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Byte buffer bytes.
+ */
+public abstract class ByteBufferBytes extends AbstractBytes {
+ protected ByteBuffer buffer;
+
+ protected ByteBufferBytes(ByteBuffer buffer) {
+ this.buffer = buffer;
+ }
+
+ public Bytes reset(ByteBuffer buffer) {
+ buffer.clear();
+ this.buffer = checkNotNull(buffer, "buffer cannot be null");
+ return this;
+ }
+
+ /**
+ * Allocates a new byte buffer.
+ *
+ * @param size the buffer size
+ * @return a newly allocated byte buffer
+ */
+ protected abstract ByteBuffer newByteBuffer(int size);
+
+ @Override
+ public Bytes resize(int newSize) {
+ ByteBuffer oldBuffer = buffer;
+ ByteBuffer newBuffer = newByteBuffer(newSize);
+ oldBuffer.position(0).limit(oldBuffer.capacity());
+ newBuffer.position(0).limit(newBuffer.capacity());
+ newBuffer.put(oldBuffer);
+ newBuffer.clear();
+ return reset(newBuffer);
+ }
+
+ @Override
+ public byte[] array() {
+ return buffer.array();
+ }
+
+ /**
+ * Returns the underlying {@link ByteBuffer}.
+ *
+ * @return the underlying byte buffer
+ */
+ public ByteBuffer byteBuffer() {
+ return buffer;
+ }
+
+ @Override
+ public Bytes zero() {
+ return this;
+ }
+
+ @Override
+ public int size() {
+ return buffer.capacity();
+ }
+
+ @Override
+ public ByteOrder order() {
+ return buffer.order();
+ }
+
+ @Override
+ public Bytes order(ByteOrder order) {
+ return reset(buffer.order(order));
+ }
+
+ /**
+ * Returns the index for the given offset.
+ */
+ private int index(int offset) {
+ return (int) offset;
+ }
+
+ @Override
+ public Bytes zero(int offset) {
+ for (int i = index(offset); i < buffer.capacity(); i++) {
+ buffer.put(i, (byte) 0);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes zero(int offset, int length) {
+ for (int i = index(offset); i < offset + length; i++) {
+ buffer.put(i, (byte) 0);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes read(int position, byte[] bytes, int offset, int length) {
+ for (int i = 0; i < length; i++) {
+ bytes[index(offset) + i] = (byte) readByte(position + i);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes read(int position, Bytes bytes, int offset, int length) {
+ for (int i = 0; i < length; i++) {
+ bytes.writeByte(offset + i, readByte(position + i));
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes write(int position, byte[] bytes, int offset, int length) {
+ for (int i = 0; i < length; i++) {
+ buffer.put((int) position + i, (byte) bytes[index(offset) + i]);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes write(int position, Bytes bytes, int offset, int length) {
+ for (int i = 0; i < length; i++) {
+ buffer.put((int) position + i, (byte) bytes.readByte(offset + i));
+ }
+ return this;
+ }
+
+ @Override
+ public int readByte(int offset) {
+ return buffer.get(index(offset));
+ }
+
+ @Override
+ public char readChar(int offset) {
+ return buffer.getChar(index(offset));
+ }
+
+ @Override
+ public short readShort(int offset) {
+ return buffer.getShort(index(offset));
+ }
+
+ @Override
+ public int readInt(int offset) {
+ return buffer.getInt(index(offset));
+ }
+
+ @Override
+ public long readLong(int offset) {
+ return buffer.getLong(index(offset));
+ }
+
+ @Override
+ public float readFloat(int offset) {
+ return buffer.getFloat(index(offset));
+ }
+
+ @Override
+ public double readDouble(int offset) {
+ return buffer.getDouble(index(offset));
+ }
+
+ @Override
+ public Bytes writeByte(int offset, int b) {
+ buffer.put(index(offset), (byte) b);
+ return this;
+ }
+
+ @Override
+ public Bytes writeChar(int offset, char c) {
+ buffer.putChar(index(offset), c);
+ return this;
+ }
+
+ @Override
+ public Bytes writeShort(int offset, short s) {
+ buffer.putShort(index(offset), s);
+ return this;
+ }
+
+ @Override
+ public Bytes writeInt(int offset, int i) {
+ buffer.putInt(index(offset), i);
+ return this;
+ }
+
+ @Override
+ public Bytes writeLong(int offset, long l) {
+ buffer.putLong(index(offset), l);
+ return this;
+ }
+
+ @Override
+ public Bytes writeFloat(int offset, float f) {
+ buffer.putFloat(index(offset), f);
+ return this;
+ }
+
+ @Override
+ public Bytes writeDouble(int offset, double d) {
+ buffer.putDouble(index(offset), d);
+ return this;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.ByteOrder;
+
+/**
+ * Common interface for interacting with a memory or disk based array of bytes.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface Bytes extends BytesInput<Bytes>, BytesOutput<Bytes>, AutoCloseable {
+ int BYTE = 1;
+ int BOOLEAN = 1;
+ int CHARACTER = 2;
+ int SHORT = 2;
+ int MEDIUM = 3;
+ int INTEGER = 4;
+ int LONG = 8;
+ int FLOAT = 4;
+ int DOUBLE = 8;
+
+ /**
+ * Returns whether the bytes has an array.
+ *
+ * @return Whether the bytes has an underlying array.
+ */
+ default boolean hasArray() {
+ return false;
+ }
+
+ /**
+ * Returns the underlying byte array.
+ *
+ * @return the underlying byte array
+ * @throws UnsupportedOperationException if a heap array is not supported
+ */
+ default byte[] array() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the count of the bytes.
+ *
+ * @return The count of the bytes.
+ */
+ int size();
+
+ /**
+ * Resizes the bytes.
+ * <p>
+ * When the bytes are resized, underlying memory addresses in copies of this instance may no longer be valid. Additionally,
+ * if the {@code newSize} is smaller than the current {@code count} then some data may be lost during the resize. Use
+ * with caution.
+ *
+ * @param newSize The count to which to resize this instance.
+ * @return The resized bytes.
+ */
+ Bytes resize(int newSize);
+
+ /**
+ * Returns the byte order.
+ * <p>
+ * For consistency with {@link java.nio.ByteBuffer}, all bytes implementations are initially in {@link ByteOrder#BIG_ENDIAN} order.
+ *
+ * @return The byte order.
+ */
+ ByteOrder order();
+
+ /**
+ * Sets the byte order, returning a new swapped {@link Bytes} instance.
+ * <p>
+ * By default, all bytes are read and written in {@link ByteOrder#BIG_ENDIAN} order. This provides complete
+ * consistency with {@link java.nio.ByteBuffer}. To flip bytes to {@link ByteOrder#LITTLE_ENDIAN} order, this
+ * {@code Bytes} instance is decorated by a {@link SwappedBytes} instance which will reverse
+ * read and written bytes using, e.g. {@link Integer#reverseBytes(int)}.
+ *
+ * @param order The byte order.
+ * @return The updated bytes.
+ * @throws NullPointerException If the {@code order} is {@code null}
+ */
+ Bytes order(ByteOrder order);
+
+ /**
+ * Returns a boolean value indicating whether the bytes are direct.
+ *
+ * @return Indicates whether the bytes are direct.
+ */
+ boolean isDirect();
+
+ /**
+ * Returns a boolean value indicating whether the bytes are backed by a file.
+ *
+ * @return Indicates whether the bytes are backed by a file.
+ */
+ boolean isFile();
+
+ @Override
+ void close();
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.charset.Charset;
+
+/**
+ * Readable bytes.
+ * <p>
+ * This interface exposes methods for reading bytes from specific positions in a byte array.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface BytesInput<T extends BytesInput<T>> {
+
+ /**
+ * Reads bytes into the given byte array starting at the given offset up to the given length.
+ *
+ * @param offset The offset from which to start reading bytes.
+ * @param dst The byte array into which to read bytes.
+ * @param dstOffset The offset at which to write bytes into the given buffer.
+ * @param length The total number of bytes to read.
+ * @return The buffer.
+ */
+ T read(int offset, Bytes dst, int dstOffset, int length);
+
+ /**
+ * Reads bytes into the given byte array starting at the given offset up to the given length.
+ *
+ * @param offset The offset from which to start reading bytes.
+ * @param dst The byte array into which to read bytes.
+ * @param dstOffset The offset at which to write bytes into the given buffer
+ * @param length The total number of bytes to read.
+ * @return The buffer.
+ */
+ T read(int offset, byte[] dst, int dstOffset, int length);
+
+ /**
+ * Reads a byte from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the byte.
+ * @return The read byte.
+ */
+ int readByte(int offset);
+
+ /**
+ * Reads an unsigned byte from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the byte.
+ * @return The read unsigned byte.
+ */
+ int readUnsignedByte(int offset);
+
+ /**
+ * Reads a 16-bit character from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the character.
+ * @return The read character.
+ */
+ char readChar(int offset);
+
+ /**
+ * Reads a 16-bit signed integer from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the short.
+ * @return The read short.
+ */
+ short readShort(int offset);
+
+ /**
+ * Reads a 16-bit unsigned integer from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the short.
+ * @return The read short.
+ */
+ int readUnsignedShort(int offset);
+
+ /**
+ * Reads a 24-bit signed integer from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the integer.
+ * @return The read medium.
+ */
+ int readMedium(int offset);
+
+ /**
+ * Reads a 24-bin unsigned integer from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the integer.
+ * @return The read medium.
+ */
+ int readUnsignedMedium(int offset);
+
+ /**
+ * Reads a 32-bit signed integer from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the integer.
+ * @return The read integer.
+ */
+ int readInt(int offset);
+
+ /**
+ * Reads a 32-bit unsigned integer from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the integer.
+ * @return The read integer.
+ */
+ long readUnsignedInt(int offset);
+
+ /**
+ * Reads a 64-bit signed integer from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the long.
+ * @return The read long.
+ */
+ long readLong(int offset);
+
+ /**
+ * Reads a single-precision 32-bit floating point number from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the float.
+ * @return The read float.
+ */
+ float readFloat(int offset);
+
+ /**
+ * Reads a double-precision 64-bit floating point number from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the double.
+ * @return The read double.
+ */
+ double readDouble(int offset);
+
+ /**
+ * Reads a 1 byte boolean from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the boolean.
+ * @return The read boolean.
+ */
+ boolean readBoolean(int offset);
+
+ /**
+ * Reads a string from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the string.
+ * @return The read string.
+ */
+ String readString(int offset);
+
+ /**
+ * Reads a string from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the string.
+ * @param charset The character set with which to decode the string.
+ * @return The read string.
+ */
+ String readString(int offset, Charset charset);
+
+ /**
+ * Reads a UTF-8 string from the buffer at the given offset.
+ *
+ * @param offset The offset at which to read the string.
+ * @return The read string.
+ */
+ String readUTF8(int offset);
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.charset.Charset;
+
+/**
+ * Writable bytes.
+ * <p>
+ * This interface exposes methods for writing bytes to specific positions in a byte array.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface BytesOutput<T extends BytesOutput<T>> {
+
+ /**
+ * Zeros out all bytes in the array.
+ *
+ * @return The written bytes.
+ */
+ T zero();
+
+ /**
+ * Zeros out all bytes starting at the given offset in the array.
+ *
+ * @param offset The offset at which to start zeroing out bytes.
+ * @return The written bytes.
+ */
+ T zero(int offset);
+
+ /**
+ * Zeros out bytes starting at the given offset up to the given length.
+ *
+ * @param offset The offset at which to start zeroing out bytes.
+ * @param length THe total number of bytes to zero out.
+ * @return The written bytes.
+ */
+ T zero(int offset, int length);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ *
+ * @param offset The offset at which to start writing the bytes.
+ * @param src The array of bytes to write.
+ * @param srcOffset The offset at which to start reading bytes from the given source.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ */
+ T write(int offset, Bytes src, int srcOffset, int length);
+
+ /**
+ * Writes an array of bytes to the buffer.
+ *
+ * @param offset The offset at which to start writing the bytes.
+ * @param src The array of bytes to write.
+ * @param srcOffset The offset at which to start reading bytes from the given source.
+ * @param length The number of bytes from the provided byte array to write to the buffer.
+ * @return The written buffer.
+ */
+ T write(int offset, byte[] src, int srcOffset, int length);
+
+ /**
+ * Writes a byte to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the byte.
+ * @param b The byte to write.
+ * @return The written buffer.
+ */
+ T writeByte(int offset, int b);
+
+ /**
+ * Writes an unsigned byte to the buffer at the given position.
+ *
+ * @param offset The offset at which to write the byte.
+ * @param b The byte to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedByte(int offset, int b);
+
+ /**
+ * Writes a 16-bit character to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the character.
+ * @param c The character to write.
+ * @return The written buffer.
+ */
+ T writeChar(int offset, char c);
+
+ /**
+ * Writes a 16-bit signed integer to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the short.
+ * @param s The short to write.
+ * @return The written buffer.
+ */
+ T writeShort(int offset, short s);
+
+ /**
+ * Writes a 16-bit unsigned integer to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the short.
+ * @param s The short to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedShort(int offset, int s);
+
+ /**
+ * Writes a 24-bit signed integer to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the short.
+ * @param m The short to write.
+ * @return The written buffer.
+ */
+ T writeMedium(int offset, int m);
+
+ /**
+ * Writes a 24-bit unsigned integer to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the short.
+ * @param m The short to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedMedium(int offset, int m);
+
+ /**
+ * Writes a 32-bit signed integer to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the integer.
+ * @param i The integer to write.
+ * @return The written buffer.
+ */
+ T writeInt(int offset, int i);
+
+ /**
+ * Writes a 32-bit unsigned integer to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the integer.
+ * @param i The integer to write.
+ * @return The written buffer.
+ */
+ T writeUnsignedInt(int offset, long i);
+
+ /**
+ * Writes a 64-bit signed integer to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the long.
+ * @param l The long to write.
+ * @return The written buffer.
+ */
+ T writeLong(int offset, long l);
+
+ /**
+ * Writes a single-precision 32-bit floating point number to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the float.
+ * @param f The float to write.
+ * @return The written buffer.
+ */
+ T writeFloat(int offset, float f);
+
+ /**
+ * Writes a double-precision 64-bit floating point number to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the double.
+ * @param d The double to write.
+ * @return The written buffer.
+ */
+ T writeDouble(int offset, double d);
+
+ /**
+ * Writes a 1 byte boolean to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the boolean.
+ * @param b The boolean to write.
+ * @return The written buffer.
+ */
+ T writeBoolean(int offset, boolean b);
+
+ /**
+ * Writes a string to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the string.
+ * @param s The string to write.
+ * @return The written buffer.
+ */
+ T writeString(int offset, String s);
+
+ /**
+ * Writes a string to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the string.
+ * @param s The string to write.
+ * @param charset The character set with which to encode the string.
+ * @return The written buffer.
+ */
+ T writeString(int offset, String s, Charset charset);
+
+ /**
+ * Writes a UTF-8 string to the buffer at the given offset.
+ *
+ * @param offset The offset at which to write the string.
+ * @param s The string to write.
+ * @return The written buffer.
+ */
+ T writeUTF8(int offset, String s);
+
+ /**
+ * Flushes the bytes to the underlying persistence layer.
+ *
+ * @return The flushed buffer.
+ */
+ T flush();
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.memory.Memory;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Direct {@link java.nio.ByteBuffer} based buffer.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class DirectBuffer extends ByteBufferBuffer {
+
+ /**
+ * Allocates a direct buffer with an initial capacity of {@code 4096} and a maximum capacity of {@link Long#MAX_VALUE}.
+ *
+ * @return The direct buffer.
+ * @see DirectBuffer#allocate(int)
+ * @see DirectBuffer#allocate(int, int)
+ */
+ public static DirectBuffer allocate() {
+ return allocate(DEFAULT_INITIAL_CAPACITY, MAX_SIZE);
+ }
+
+ /**
+ * Allocates a direct buffer with the given initial capacity.
+ *
+ * @param initialCapacity The initial capacity of the buffer to allocate (in bytes).
+ * @return The direct buffer.
+ * @throws IllegalArgumentException If {@code capacity} is greater than the maximum allowed count for
+ * a {@link java.nio.ByteBuffer} - {@code Integer.MAX_VALUE - 5}
+ * @see DirectBuffer#allocate()
+ * @see DirectBuffer#allocate(int, int)
+ */
+ public static DirectBuffer allocate(int initialCapacity) {
+ return allocate(initialCapacity, MAX_SIZE);
+ }
+
+ /**
+ * Allocates a new direct buffer.
+ *
+ * @param initialCapacity The initial capacity of the buffer to allocate (in bytes).
+ * @param maxCapacity The maximum capacity of the buffer.
+ * @return The direct buffer.
+ * @throws IllegalArgumentException If {@code capacity} or {@code maxCapacity} is greater than the maximum
+ * allowed count for a {@link java.nio.ByteBuffer} - {@code Integer.MAX_VALUE - 5}
+ * @see DirectBuffer#allocate()
+ * @see DirectBuffer#allocate(int)
+ */
+ public static DirectBuffer allocate(int initialCapacity, int maxCapacity) {
+ checkArgument(initialCapacity <= maxCapacity, "initial capacity cannot be greater than maximum capacity");
+ return new DirectBuffer(DirectBytes.allocate((int) Math.min(Memory.Util.toPow2(initialCapacity), MAX_SIZE)), 0, initialCapacity, maxCapacity);
+ }
+
+ protected DirectBuffer(DirectBytes bytes, int offset, int initialCapacity, int maxCapacity) {
+ super(bytes, offset, initialCapacity, maxCapacity, null);
+ }
+
+ @Override
+ public DirectBuffer duplicate() {
+ return new DirectBuffer((DirectBytes) bytes, offset(), capacity(), maxCapacity());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@link ByteBuffer} based direct bytes.
+ */
+public class DirectBytes extends ByteBufferBytes {
+
+ /**
+ * Allocates a new direct byte array.
+ *
+ * @param size The count of the buffer to allocate (in bytes).
+ * @return The direct buffer.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed count for
+ * an array on the Java heap - {@code Integer.MAX_VALUE - 5}
+ */
+ public static DirectBytes allocate(int size) {
+ if (size > MAX_SIZE) {
+ throw new IllegalArgumentException("size cannot for DirectBytes cannot be greater than " + MAX_SIZE);
+ }
+ return new DirectBytes(ByteBuffer.allocateDirect((int) size));
+ }
+
+ protected DirectBytes(ByteBuffer buffer) {
+ super(buffer);
+ }
+
+ @Override
+ protected ByteBuffer newByteBuffer(int size) {
+ return ByteBuffer.allocateDirect((int) size);
+ }
+
+ @Override
+ public boolean isDirect() {
+ return buffer.isDirect();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.memory.Memory;
+
+import java.io.File;
+import java.nio.channels.FileChannel;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * File buffer.
+ * <p>
+ * File buffers wrap a simple {@link java.io.RandomAccessFile} instance to provide random access to a file on local disk. All
+ * operations are delegated directly to the {@link java.io.RandomAccessFile} interface, and limitations are dependent on the
+ * semantics of the underlying file.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class FileBuffer extends AbstractBuffer {
+
+ /**
+ * Allocates a file buffer of unlimited capacity.
+ * <p>
+ * The buffer will initially be allocated with {@code 4096} bytes. As bytes are written to the resulting buffer and
+ * the original capacity is reached, the buffer's capacity will double.
+ *
+ * @param file The file to allocate.
+ * @return The allocated buffer.
+ * @see FileBuffer#allocate(File, int)
+ * @see FileBuffer#allocate(File, int, int)
+ * @see FileBuffer#allocate(File, String, int, int)
+ */
+ public static FileBuffer allocate(File file) {
+ return allocate(file, FileBytes.DEFAULT_MODE, DEFAULT_INITIAL_CAPACITY, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Allocates a file buffer with the given initial capacity.
+ * <p>
+ * If the underlying file is empty, the file count will expand dynamically as bytes are written to the file.
+ * The underlying {@link FileBytes} will be initialized to the nearest power of {@code 2}.
+ *
+ * @param file The file to allocate.
+ * @param initialCapacity The initial capacity of the bytes to allocate.
+ * @return The allocated buffer.
+ * @see FileBuffer#allocate(File)
+ * @see FileBuffer#allocate(File, int, int)
+ * @see FileBuffer#allocate(File, String, int, int)
+ */
+ public static FileBuffer allocate(File file, int initialCapacity) {
+ return allocate(file, FileBytes.DEFAULT_MODE, initialCapacity, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Allocates a file buffer.
+ * <p>
+ * The underlying {@link java.io.RandomAccessFile} will be created in {@code rw} mode by default.
+ * The resulting buffer will be initialized with a capacity of {@code initialCapacity}. The underlying {@link FileBytes}
+ * will be initialized to the nearest power of {@code 2}. As bytes are written to the file the buffer's capacity will
+ * double up to {@code maxCapacity}.
+ *
+ * @param file The file to allocate.
+ * @param initialCapacity The initial capacity of the buffer.
+ * @param maxCapacity The maximum allowed capacity of the buffer.
+ * @return The allocated buffer.
+ * @see FileBuffer#allocate(File)
+ * @see FileBuffer#allocate(File, int)
+ * @see FileBuffer#allocate(File, String, int, int)
+ */
+ public static FileBuffer allocate(File file, int initialCapacity, int maxCapacity) {
+ return allocate(file, FileBytes.DEFAULT_MODE, initialCapacity, maxCapacity);
+ }
+
+ /**
+ * Allocates a file buffer.
+ * <p>
+ * The resulting buffer will be initialized with a capacity of {@code initialCapacity}. The underlying {@link FileBytes}
+ * will be initialized to the nearest power of {@code 2}. As bytes are written to the file the buffer's capacity will
+ * double up to {@code maxCapacity}.
+ *
+ * @param file The file to allocate.
+ * @param mode The mode in which to open the underlying {@link java.io.RandomAccessFile}.
+ * @param initialCapacity The initial capacity of the buffer.
+ * @param maxCapacity The maximum allowed capacity of the buffer.
+ * @return The allocated buffer.
+ * @see FileBuffer#allocate(File)
+ * @see FileBuffer#allocate(File, int)
+ * @see FileBuffer#allocate(File, int, int)
+ */
+ public static FileBuffer allocate(File file, String mode, int initialCapacity, int maxCapacity) {
+ checkArgument(initialCapacity <= maxCapacity, "initial capacity cannot be greater than maximum capacity");
+ return new FileBuffer(new FileBytes(file, mode, (int) Math.min(Memory.Util.toPow2(initialCapacity), maxCapacity)), 0, initialCapacity, maxCapacity);
+ }
+
+ private final FileBytes bytes;
+
+ private FileBuffer(FileBytes bytes, int offset, int initialCapacity, int maxCapacity) {
+ super(bytes, offset, initialCapacity, maxCapacity, null);
+ this.bytes = bytes;
+ }
+
+ /**
+ * Returns the underlying file object.
+ *
+ * @return The underlying file.
+ */
+ public File file() {
+ return ((FileBytes) bytes).file();
+ }
+
+ /**
+ * Maps a portion of the underlying file into memory in {@link FileChannel.MapMode#READ_WRITE} mode
+ * starting at the current position up to the given {@code count}.
+ *
+ * @param size The count of the bytes to map into memory.
+ * @return The mapped buffer.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed
+ * {@link java.nio.MappedByteBuffer} count: {@link Integer#MAX_VALUE}
+ */
+ public MappedBuffer map(int size) {
+ return map(position(), size, FileChannel.MapMode.READ_WRITE);
+ }
+
+ /**
+ * Maps a portion of the underlying file into memory starting at the current position up to the given {@code count}.
+ *
+ * @param size The count of the bytes to map into memory.
+ * @param mode The mode in which to map the bytes into memory.
+ * @return The mapped buffer.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed
+ * {@link java.nio.MappedByteBuffer} count: {@link Integer#MAX_VALUE}
+ */
+ public MappedBuffer map(int size, FileChannel.MapMode mode) {
+ return map(position(), size, mode);
+ }
+
+ /**
+ * Maps a portion of the underlying file into memory in {@link FileChannel.MapMode#READ_WRITE} mode
+ * starting at the given {@code offset} up to the given {@code count}.
+ *
+ * @param offset The offset from which to map bytes into memory.
+ * @param size The count of the bytes to map into memory.
+ * @return The mapped buffer.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed
+ * {@link java.nio.MappedByteBuffer} count: {@link Integer#MAX_VALUE}
+ */
+ public MappedBuffer map(int offset, int size) {
+ return map(offset, size, FileChannel.MapMode.READ_WRITE);
+ }
+
+ /**
+ * Maps a portion of the underlying file into memory starting at the given {@code offset} up to the given {@code count}.
+ *
+ * @param offset The offset from which to map bytes into memory.
+ * @param size The count of the bytes to map into memory.
+ * @param mode The mode in which to map the bytes into memory.
+ * @return The mapped buffer.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed
+ * {@link java.nio.MappedByteBuffer} count: {@link Integer#MAX_VALUE}
+ */
+ public MappedBuffer map(int offset, int size, FileChannel.MapMode mode) {
+ return new MappedBuffer(((FileBytes) bytes).map(offset, size, mode), 0, size, size);
+ }
+
+ @Override
+ protected void compact(int from, int to, int length) {
+ byte[] bytes = new byte[1024];
+ int position = from;
+ while (position < from + length) {
+ int size = Math.min((from + length) - position, 1024);
+ this.bytes.read(position, bytes, 0, size);
+ this.bytes.write(0, bytes, 0, size);
+ position += size;
+ }
+ }
+
+ @Override
+ public FileBuffer duplicate() {
+ return new FileBuffer(new FileBytes(bytes.file(), bytes.mode(), bytes.size()), offset(), capacity(), maxCapacity());
+ }
+
+ /**
+ * Duplicates the buffer using the given mode.
+ *
+ * @return The mode with which to open the duplicate buffer.
+ */
+ public FileBuffer duplicate(String mode) {
+ return new FileBuffer(new FileBytes(bytes.file(), mode, bytes.size()), offset(), capacity(), maxCapacity());
+ }
+
+ /**
+ * Deletes the underlying file.
+ */
+ public void delete() {
+ bytes.delete();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.memory.Memory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteOrder;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+
+/**
+ * File bytes.
+ * <p>
+ * File bytes wrap a simple {@link RandomAccessFile} instance to provide random access to a randomAccessFile on local disk. All
+ * operations are delegated directly to the {@link RandomAccessFile} interface, and limitations are dependent on the
+ * semantics of the underlying randomAccessFile.
+ * <p>
+ * Bytes are always stored in the underlying randomAccessFile in {@link ByteOrder#BIG_ENDIAN} order.
+ * To flip the byte order to read or write to/from a randomAccessFile in {@link ByteOrder#LITTLE_ENDIAN} order use
+ * {@link Bytes#order(ByteOrder)}.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class FileBytes extends AbstractBytes {
+ static final String DEFAULT_MODE = "rw";
+
+ /**
+ * Allocates a randomAccessFile buffer of unlimited count.
+ * <p>
+ * The buffer will be allocated with {@link Long#MAX_VALUE} bytes. As bytes are written to the buffer, the underlying
+ * {@link RandomAccessFile} will expand.
+ *
+ * @param file The randomAccessFile to allocate.
+ * @return The allocated buffer.
+ */
+ public static FileBytes allocate(File file) {
+ return allocate(file, DEFAULT_MODE, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Allocates a randomAccessFile buffer.
+ * <p>
+ * If the underlying randomAccessFile is empty, the randomAccessFile count will expand dynamically as bytes are written to the randomAccessFile.
+ *
+ * @param file The randomAccessFile to allocate.
+ * @param size The count of the bytes to allocate.
+ * @return The allocated buffer.
+ */
+ public static FileBytes allocate(File file, int size) {
+ return allocate(file, DEFAULT_MODE, size);
+ }
+
+ /**
+ * Allocates a randomAccessFile buffer.
+ * <p>
+ * If the underlying randomAccessFile is empty, the randomAccessFile count will expand dynamically as bytes are written to the randomAccessFile.
+ *
+ * @param file The randomAccessFile to allocate.
+ * @param mode The mode in which to open the underlying {@link RandomAccessFile}.
+ * @param size The count of the bytes to allocate.
+ * @return The allocated buffer.
+ */
+ public static FileBytes allocate(File file, String mode, int size) {
+ return new FileBytes(file, mode, (int) Math.min(Memory.Util.toPow2(size), Integer.MAX_VALUE));
+ }
+
+ private static final int PAGE_SIZE = 1024 * 4;
+ private static final byte[] BLANK_PAGE = new byte[PAGE_SIZE];
+
+ private final File file;
+ private final String mode;
+ private final RandomAccessFile randomAccessFile;
+ private int size;
+
+ FileBytes(File file, String mode, int size) {
+ if (file == null) {
+ throw new NullPointerException("file cannot be null");
+ }
+ if (mode == null) {
+ mode = DEFAULT_MODE;
+ }
+ if (size < 0) {
+ throw new IllegalArgumentException("size must be positive");
+ }
+
+ this.file = file;
+ this.mode = mode;
+ this.size = size;
+ try {
+ this.randomAccessFile = new RandomAccessFile(file, mode);
+ if (size > randomAccessFile.length()) {
+ randomAccessFile.setLength(size);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the underlying file object.
+ *
+ * @return The underlying file.
+ */
+ public File file() {
+ return file;
+ }
+
+ /**
+ * Returns the file mode.
+ *
+ * @return The file mode.
+ */
+ public String mode() {
+ return mode;
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public Bytes resize(int newSize) {
+ if (newSize < size) {
+ throw new IllegalArgumentException("cannot decrease file bytes size; use zero() to decrease file size");
+ }
+ int oldSize = this.size;
+ this.size = newSize;
+ try {
+ long length = randomAccessFile.length();
+ if (newSize > length) {
+ randomAccessFile.setLength(newSize);
+ zero(oldSize);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public boolean isFile() {
+ return true;
+ }
+
+ /**
+ * Maps a portion of the randomAccessFile into memory in {@link FileChannel.MapMode#READ_WRITE} mode and returns
+ * a {@link UnsafeMappedBytes} instance.
+ *
+ * @param offset The offset from which to map the randomAccessFile into memory.
+ * @param size The count of the bytes to map into memory.
+ * @return The mapped bytes.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed
+ * {@link java.nio.MappedByteBuffer} count: {@link Integer#MAX_VALUE}
+ */
+ public MappedBytes map(int offset, int size) {
+ return map(offset, size, parseMode(mode));
+ }
+
+ /**
+ * Maps a portion of the randomAccessFile into memory and returns a {@link UnsafeMappedBytes} instance.
+ *
+ * @param offset The offset from which to map the randomAccessFile into memory.
+ * @param size The count of the bytes to map into memory.
+ * @param mode The mode in which to map the randomAccessFile into memory.
+ * @return The mapped bytes.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed
+ * {@link java.nio.MappedByteBuffer} count: {@link Integer#MAX_VALUE}
+ */
+ public MappedBytes map(int offset, int size, FileChannel.MapMode mode) {
+ MappedByteBuffer mappedByteBuffer = mapFile(randomAccessFile, offset, size, mode);
+ return new MappedBytes(file, randomAccessFile, mappedByteBuffer, mode);
+ }
+
+ private static MappedByteBuffer mapFile(RandomAccessFile randomAccessFile, int offset, int size, FileChannel.MapMode mode) {
+ try {
+ return randomAccessFile.getChannel().map(mode, offset, size);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static FileChannel.MapMode parseMode(String mode) {
+ switch (mode) {
+ case "r":
+ return FileChannel.MapMode.READ_ONLY;
+ case "rw":
+ default:
+ return FileChannel.MapMode.READ_WRITE;
+ }
+ }
+
+ @Override
+ public ByteOrder order() {
+ return ByteOrder.BIG_ENDIAN;
+ }
+
+ /**
+ * Seeks to the given offset.
+ */
+ private void seekToOffset(int offset) throws IOException {
+ if (randomAccessFile.getFilePointer() != offset) {
+ randomAccessFile.seek(offset);
+ }
+ }
+
+ @Override
+ public Bytes zero() {
+ try {
+ randomAccessFile.setLength(0);
+ randomAccessFile.setLength(size);
+ for (int i = 0; i < size; i += PAGE_SIZE) {
+ randomAccessFile.write(BLANK_PAGE, 0, Math.min(size - i, PAGE_SIZE));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes zero(int offset) {
+ try {
+ int length = Math.max(offset, size);
+ randomAccessFile.setLength(offset);
+ randomAccessFile.setLength(length);
+ seekToOffset(offset);
+ for (int i = offset; i < length; i += PAGE_SIZE) {
+ randomAccessFile.write(BLANK_PAGE, 0, Math.min(length - i, PAGE_SIZE));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes zero(int offset, int length) {
+ for (int i = offset; i < offset + length; i++) {
+ writeByte(i, (byte) 0);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes read(int position, Bytes bytes, int offset, int length) {
+ checkRead(position, length);
+ if (bytes instanceof WrappedBytes) {
+ bytes = ((WrappedBytes) bytes).root();
+ }
+ if (bytes.hasArray()) {
+ try {
+ seekToOffset(position);
+ randomAccessFile.readFully(bytes.array(), (int) offset, (int) length);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ try {
+ seekToOffset(position);
+ byte[] readBytes = new byte[(int) length];
+ randomAccessFile.readFully(readBytes);
+ bytes.write(offset, readBytes, 0, length);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes read(int position, byte[] bytes, int offset, int length) {
+ checkRead(position, length);
+ try {
+ seekToOffset(position);
+ randomAccessFile.readFully(bytes, (int) offset, (int) length);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public int readByte(int offset) {
+ checkRead(offset, BYTE);
+ try {
+ seekToOffset(offset);
+ return randomAccessFile.readByte();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public char readChar(int offset) {
+ checkRead(offset, CHARACTER);
+ try {
+ seekToOffset(offset);
+ return randomAccessFile.readChar();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public short readShort(int offset) {
+ checkRead(offset, SHORT);
+ try {
+ seekToOffset(offset);
+ return randomAccessFile.readShort();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public int readInt(int offset) {
+ checkRead(offset, INTEGER);
+ try {
+ seekToOffset(offset);
+ return randomAccessFile.readInt();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public long readLong(int offset) {
+ checkRead(offset, LONG);
+ try {
+ seekToOffset(offset);
+ return randomAccessFile.readLong();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public float readFloat(int offset) {
+ checkRead(offset, FLOAT);
+ try {
+ seekToOffset(offset);
+ return randomAccessFile.readFloat();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public double readDouble(int offset) {
+ checkRead(offset, DOUBLE);
+ try {
+ seekToOffset(offset);
+ return randomAccessFile.readDouble();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public Bytes write(int position, Bytes bytes, int offset, int length) {
+ checkWrite(position, length);
+ if (bytes instanceof WrappedBytes) {
+ bytes = ((WrappedBytes) bytes).root();
+ }
+ if (bytes.hasArray()) {
+ try {
+ seekToOffset(position);
+ randomAccessFile.write(bytes.array(), (int) offset, (int) length);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ try {
+ seekToOffset(position);
+ byte[] writeBytes = new byte[(int) length];
+ bytes.read(offset, writeBytes, 0, length);
+ randomAccessFile.write(writeBytes);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes write(int position, byte[] bytes, int offset, int length) {
+ checkWrite(position, length);
+ try {
+ seekToOffset(position);
+ randomAccessFile.write(bytes, (int) offset, (int) length);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes writeByte(int offset, int b) {
+ checkWrite(offset, BYTE);
+ try {
+ seekToOffset(offset);
+ randomAccessFile.writeByte(b);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes writeChar(int offset, char c) {
+ checkWrite(offset, CHARACTER);
+ try {
+ seekToOffset(offset);
+ randomAccessFile.writeChar(c);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes writeShort(int offset, short s) {
+ checkWrite(offset, SHORT);
+ try {
+ seekToOffset(offset);
+ randomAccessFile.writeShort(s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes writeInt(int offset, int i) {
+ checkWrite(offset, INTEGER);
+ try {
+ seekToOffset(offset);
+ randomAccessFile.writeInt(i);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes writeLong(int offset, long l) {
+ checkWrite(offset, LONG);
+ try {
+ seekToOffset(offset);
+ randomAccessFile.writeLong(l);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes writeFloat(int offset, float f) {
+ checkWrite(offset, FLOAT);
+ try {
+ seekToOffset(offset);
+ randomAccessFile.writeFloat(f);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes writeDouble(int offset, double d) {
+ checkWrite(offset, DOUBLE);
+ try {
+ seekToOffset(offset);
+ randomAccessFile.writeDouble(d);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public Bytes flush() {
+ try {
+ randomAccessFile.getFD().sync();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public void close() {
+ try {
+ randomAccessFile.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ super.close();
+ }
+
+ /**
+ * Deletes the underlying file.
+ */
+ public void delete() {
+ try {
+ close();
+ Files.delete(file.toPath());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.memory.Memory;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Direct {@link java.nio.ByteBuffer} based buffer.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class HeapBuffer extends ByteBufferBuffer {
+
+ /**
+ * Allocates a direct buffer with an initial capacity of {@code 4096} and a maximum capacity of {@link Long#MAX_VALUE}.
+ *
+ * @return The direct buffer.
+ * @see HeapBuffer#allocate(int)
+ * @see HeapBuffer#allocate(int, int)
+ */
+ public static HeapBuffer allocate() {
+ return allocate(DEFAULT_INITIAL_CAPACITY, MAX_SIZE);
+ }
+
+ /**
+ * Allocates a direct buffer with the given initial capacity.
+ *
+ * @param initialCapacity The initial capacity of the buffer to allocate (in bytes).
+ * @return The direct buffer.
+ * @throws IllegalArgumentException If {@code capacity} is greater than the maximum allowed count for
+ * a {@link java.nio.ByteBuffer} - {@code Integer.MAX_VALUE - 5}
+ * @see HeapBuffer#allocate()
+ * @see HeapBuffer#allocate(int, int)
+ */
+ public static HeapBuffer allocate(int initialCapacity) {
+ return allocate(initialCapacity, MAX_SIZE);
+ }
+
+ /**
+ * Allocates a new direct buffer.
+ *
+ * @param initialCapacity The initial capacity of the buffer to allocate (in bytes).
+ * @param maxCapacity The maximum capacity of the buffer.
+ * @return The direct buffer.
+ * @throws IllegalArgumentException If {@code capacity} or {@code maxCapacity} is greater than the maximum
+ * allowed count for a {@link java.nio.ByteBuffer} - {@code Integer.MAX_VALUE - 5}
+ * @see HeapBuffer#allocate()
+ * @see HeapBuffer#allocate(int)
+ */
+ public static HeapBuffer allocate(int initialCapacity, int maxCapacity) {
+ checkArgument(initialCapacity <= maxCapacity, "initial capacity cannot be greater than maximum capacity");
+ return new HeapBuffer(HeapBytes.allocate((int) Math.min(Memory.Util.toPow2(initialCapacity), MAX_SIZE)), 0, initialCapacity, maxCapacity);
+ }
+
+ /**
+ * Wraps the given bytes in a heap buffer.
+ * <p>
+ * The buffer will be created with an initial capacity and maximum capacity equal to the byte array count.
+ *
+ * @param bytes The bytes to wrap.
+ * @return The wrapped bytes.
+ */
+ public static HeapBuffer wrap(byte[] bytes) {
+ return new HeapBuffer(HeapBytes.wrap(bytes), 0, bytes.length, bytes.length);
+ }
+
+ private final HeapBytes bytes;
+
+ protected HeapBuffer(HeapBytes bytes, int offset, int initialCapacity, int maxCapacity) {
+ super(bytes, offset, initialCapacity, maxCapacity, null);
+ this.bytes = bytes;
+ }
+
+ @Override
+ public boolean hasArray() {
+ return true;
+ }
+
+ @Override
+ public HeapBuffer duplicate() {
+ return new HeapBuffer(bytes, offset(), capacity(), maxCapacity());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * {@link ByteBuffer} based heap bytes.
+ */
+public class HeapBytes extends ByteBufferBytes {
+ public static final byte[] EMPTY = new byte[0];
+
+ /**
+ * Allocates a new heap byte array.
+ *
+ * @param size The count of the buffer to allocate (in bytes).
+ * @return The heap buffer.
+ * @throws IllegalArgumentException If {@code count} is greater than the maximum allowed count for
+ * an array on the Java heap - {@code Integer.MAX_VALUE - 5}
+ */
+ public static HeapBytes allocate(int size) {
+ if (size > MAX_SIZE) {
+ throw new IllegalArgumentException("size cannot for HeapBytes cannot be greater than " + MAX_SIZE);
+ }
+ return new HeapBytes(ByteBuffer.allocate((int) size));
+ }
+
+ /**
+ * Wraps the given bytes in a {@link HeapBytes} object.
+ * <p>
+ * The returned {@link Bytes} object will be backed by a {@link ByteBuffer} instance that
+ * wraps the given byte array. The {@link Bytes#size()} will be equivalent to the provided
+ * by array {@code length}.
+ *
+ * @param bytes The bytes to wrap.
+ */
+ public static HeapBytes wrap(byte[] bytes) {
+ return new HeapBytes(ByteBuffer.wrap(bytes));
+ }
+
+ protected HeapBytes(ByteBuffer buffer) {
+ super(buffer);
+ }
+
+ @Override
+ protected ByteBuffer newByteBuffer(int size) {
+ return ByteBuffer.allocate((int) size);
+ }
+
+ @Override
+ public boolean hasArray() {
+ return true;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.io.File;
+import java.nio.channels.FileChannel;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Direct {@link java.nio.ByteBuffer} based buffer.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class MappedBuffer extends ByteBufferBuffer {
+
+ /**
+ * Allocates a dynamic capacity mapped buffer in {@link FileChannel.MapMode#READ_WRITE} mode with an initial capacity
+ * of {@code 16MiB} and a maximum capacity of {@link Integer#MAX_VALUE}.
+ * <p>
+ * The resulting buffer will have a maximum capacity of {@link Integer#MAX_VALUE}. As bytes are written to the buffer
+ * its capacity will double in count each time the current capacity is reached. Memory will be mapped by opening and
+ * expanding the given {@link File} to the desired {@code capacity} and mapping the file contents into memory via
+ * {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ *
+ * @param file The file to map into memory.
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @see #allocate(File, FileChannel.MapMode)
+ * @see #allocate(File, int)
+ * @see #allocate(File, FileChannel.MapMode, int)
+ * @see #allocate(File, int, int)
+ * @see #allocate(File, FileChannel.MapMode, int, int)
+ */
+ public static MappedBuffer allocate(File file) {
+ return allocate(file, FileChannel.MapMode.READ_WRITE, DEFAULT_INITIAL_CAPACITY, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Allocates a dynamic capacity mapped buffer in {@link FileChannel.MapMode#READ_WRITE} mode with an initial capacity
+ * of {@code 16MiB} and a maximum capacity of {@link Integer#MAX_VALUE}.
+ * <p>
+ * The resulting buffer will be initialized to a capacity of {@code 4096} and have a maximum capacity of
+ * {@link Integer#MAX_VALUE}. As bytes are written to the buffer its capacity will double in count each time the current
+ * capacity is reached. Memory will be mapped by opening and expanding the given {@link File} to the desired
+ * {@code capacity} and mapping the file contents into memory via
+ * {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ *
+ * @param file The file to map into memory.
+ * @param mode The mode with which to map the file.
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @see #allocate(File)
+ * @see #allocate(File, int)
+ * @see #allocate(File, FileChannel.MapMode, int)
+ * @see #allocate(File, int, int)
+ * @see #allocate(File, FileChannel.MapMode, int, int)
+ */
+ public static MappedBuffer allocate(File file, FileChannel.MapMode mode) {
+ return allocate(file, mode, DEFAULT_INITIAL_CAPACITY, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Allocates a fixed capacity mapped buffer in {@link FileChannel.MapMode#READ_WRITE} mode.
+ * <p>
+ * Memory will be mapped by opening and expanding the given {@link File} to the desired {@code capacity} and mapping the
+ * file contents into memory via {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ * <p>
+ * The resulting buffer will have a capacity of {@code capacity}. The underlying {@link MappedBytes} will be
+ * initialized to the next power of {@code 2}.
+ *
+ * @param file The file to map into memory.
+ * @param capacity The fixed capacity of the buffer to allocate (in bytes).
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @throws IllegalArgumentException If the {@code capacity} is greater than {@link Integer#MAX_VALUE}.
+ * @see #allocate(File)
+ * @see #allocate(File, FileChannel.MapMode)
+ * @see #allocate(File, FileChannel.MapMode, int)
+ * @see #allocate(File, int, int)
+ * @see #allocate(File, FileChannel.MapMode, int, int)
+ */
+ public static MappedBuffer allocate(File file, int capacity) {
+ return allocate(file, FileChannel.MapMode.READ_WRITE, capacity, capacity);
+ }
+
+ /**
+ * Allocates a fixed capacity mapped buffer in the given {@link FileChannel.MapMode}.
+ * <p>
+ * Memory will be mapped by opening and expanding the given {@link File} to the desired {@code capacity} and mapping the
+ * file contents into memory via {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ * <p>
+ * The resulting buffer will have a capacity of {@code capacity}. The underlying {@link MappedBytes} will be
+ * initialized to the next power of {@code 2}.
+ *
+ * @param file The file to map into memory.
+ * @param mode The mode with which to map the file.
+ * @param capacity The fixed capacity of the buffer to allocate (in bytes).
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @throws IllegalArgumentException If the {@code capacity} is greater than {@link Integer#MAX_VALUE}.
+ * @see #allocate(File)
+ * @see #allocate(File, FileChannel.MapMode)
+ * @see #allocate(File, int)
+ * @see #allocate(File, int, int)
+ * @see #allocate(File, FileChannel.MapMode, int, int)
+ */
+ public static MappedBuffer allocate(File file, FileChannel.MapMode mode, int capacity) {
+ return allocate(file, mode, capacity, capacity);
+ }
+
+ /**
+ * Allocates a mapped buffer.
+ * <p>
+ * Memory will be mapped by opening and expanding the given {@link File} to the desired {@code count} and mapping the
+ * file contents into memory via {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ * <p>
+ * The resulting buffer will have a capacity of {@code initialCapacity}. The underlying {@link MappedBytes} will be
+ * initialized to the next power of {@code 2}. As bytes are written to the buffer, the buffer's capacity will double
+ * as int as {@code maxCapacity > capacity}.
+ *
+ * @param file The file to map into memory. If the file doesn't exist it will be automatically created.
+ * @param initialCapacity The initial capacity of the buffer.
+ * @param maxCapacity The maximum capacity of the buffer.
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @throws IllegalArgumentException If the {@code capacity} or {@code maxCapacity} is greater than
+ * {@link Integer#MAX_VALUE}.
+ * @see #allocate(File)
+ * @see #allocate(File, FileChannel.MapMode)
+ * @see #allocate(File, int)
+ * @see #allocate(File, FileChannel.MapMode, int)
+ * @see #allocate(File, FileChannel.MapMode, int, int)
+ */
+ public static MappedBuffer allocate(File file, int initialCapacity, int maxCapacity) {
+ return allocate(file, FileChannel.MapMode.READ_WRITE, initialCapacity, maxCapacity);
+ }
+
+ /**
+ * Allocates a mapped buffer.
+ * <p>
+ * Memory will be mapped by opening and expanding the given {@link File} to the desired {@code count} and mapping the
+ * file contents into memory via {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ * <p>
+ * The resulting buffer will have a capacity of {@code initialCapacity}. The underlying {@link MappedBytes} will be
+ * initialized to the next power of {@code 2}. As bytes are written to the buffer, the buffer's capacity will double
+ * as int as {@code maxCapacity > capacity}.
+ *
+ * @param file The file to map into memory. If the file doesn't exist it will be automatically created.
+ * @param mode The mode with which to map the file.
+ * @param initialCapacity The initial capacity of the buffer.
+ * @param maxCapacity The maximum capacity of the buffer.
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @throws IllegalArgumentException If the {@code capacity} or {@code maxCapacity} is greater than
+ * {@link Integer#MAX_VALUE}.
+ * @see #allocate(File)
+ * @see #allocate(File, FileChannel.MapMode)
+ * @see #allocate(File, int)
+ * @see #allocate(File, FileChannel.MapMode, int)
+ * @see #allocate(File, int, int)
+ */
+ public static MappedBuffer allocate(File file, FileChannel.MapMode mode, int initialCapacity, int maxCapacity) {
+ checkNotNull(file, "file cannot be null");
+ checkNotNull(mode, "mode cannot be null");
+ checkArgument(initialCapacity <= maxCapacity, "initial capacity cannot be greater than maximum capacity");
+ return new MappedBuffer(MappedBytes.allocate(file, mode, initialCapacity), 0, initialCapacity, maxCapacity);
+ }
+
+ protected MappedBuffer(MappedBytes bytes, int offset, int initialCapacity, int maxCapacity) {
+ super(bytes, offset, initialCapacity, maxCapacity, null);
+ }
+
+ @Override
+ public MappedBuffer duplicate() {
+ return new MappedBuffer((MappedBytes) bytes, offset(), capacity(), maxCapacity());
+ }
+
+ /**
+ * Deletes the underlying file.
+ */
+ public void delete() {
+ ((MappedBytes) bytes).delete();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.AtomixIOException;
+import io.atomix.utils.memory.BufferCleaner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+
+/**
+ * {@link ByteBuffer} based mapped bytes.
+ */
+public class MappedBytes extends ByteBufferBytes {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MappedBytes.class);
+
+ /**
+ * Allocates a mapped buffer in {@link FileChannel.MapMode#READ_WRITE} mode.
+ * <p>
+ * Memory will be mapped by opening and expanding the given {@link File} to the desired {@code count} and mapping the
+ * file contents into memory via {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ *
+ * @param file The file to map into memory. If the file doesn't exist it will be automatically created.
+ * @param size The count of the buffer to allocate (in bytes).
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @throws IllegalArgumentException If {@code count} is greater than {@link MappedBytes#MAX_SIZE}
+ * @see #allocate(File, FileChannel.MapMode, int)
+ */
+ public static MappedBytes allocate(File file, int size) {
+ return allocate(file, FileChannel.MapMode.READ_WRITE, size);
+ }
+
+ /**
+ * Allocates a mapped buffer.
+ * <p>
+ * Memory will be mapped by opening and expanding the given {@link File} to the desired {@code count} and mapping the
+ * file contents into memory via {@link FileChannel#map(FileChannel.MapMode, long, long)}.
+ *
+ * @param file The file to map into memory. If the file doesn't exist it will be automatically created.
+ * @param mode The mode with which to map the file.
+ * @param size The count of the buffer to allocate (in bytes).
+ * @return The mapped buffer.
+ * @throws NullPointerException If {@code file} is {@code null}
+ * @throws IllegalArgumentException If {@code count} is greater than {@link Integer#MAX_VALUE}
+ * @see #allocate(File, int)
+ */
+ public static MappedBytes allocate(File file, FileChannel.MapMode mode, int size) {
+ return FileBytes.allocate(file, size).map(0, size, mode);
+ }
+
+ private final File file;
+ private final RandomAccessFile randomAccessFile;
+ private final FileChannel.MapMode mode;
+
+ protected MappedBytes(File file, RandomAccessFile randomAccessFile, MappedByteBuffer buffer, FileChannel.MapMode mode) {
+ super(buffer);
+ this.file = file;
+ this.randomAccessFile = randomAccessFile;
+ this.mode = mode;
+ }
+
+ @Override
+ protected ByteBuffer newByteBuffer(int size) {
+ try {
+ return randomAccessFile.getChannel().map(mode, 0, size);
+ } catch (IOException e) {
+ throw new AtomixIOException(e);
+ }
+ }
+
+ @Override
+ public boolean isDirect() {
+ return true;
+ }
+
+ @Override
+ public Bytes flush() {
+ ((MappedByteBuffer) buffer).force();
+ return this;
+ }
+
+ @Override
+ public void close() {
+ try {
+ BufferCleaner.freeBuffer(buffer);
+ } catch (Exception e) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Failed to unmap direct buffer", e);
+ }
+ }
+ try {
+ randomAccessFile.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ super.close();
+ }
+
+ /**
+ * Deletes the underlying file.
+ */
+ public void delete() {
+ try {
+ close();
+ Files.delete(file.toPath());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String parseMode(FileChannel.MapMode mode) {
+ if (mode == FileChannel.MapMode.READ_ONLY) {
+ return "r";
+ } else if (mode == FileChannel.MapMode.READ_WRITE) {
+ return "rw";
+ }
+ throw new IllegalArgumentException("unsupported map mode");
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.concurrent.ReferencePool;
+
+/**
+ * Pooled buffer allocator.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public abstract class PooledAllocator implements BufferAllocator {
+ private final ReferencePool<AbstractBuffer> pool;
+
+ protected PooledAllocator(ReferencePool<AbstractBuffer> pool) {
+ this.pool = pool;
+ }
+
+ /**
+ * Returns the maximum buffer capacity.
+ *
+ * @return The maximum buffer capacity.
+ */
+ protected abstract int maxCapacity();
+
+ @Override
+ public Buffer allocate() {
+ return allocate(4096, maxCapacity());
+ }
+
+ @Override
+ public Buffer allocate(int capacity) {
+ return allocate(capacity, maxCapacity());
+ }
+
+ @Override
+ public Buffer allocate(int initialCapacity, int maxCapacity) {
+ return pool.acquire().reset(0, initialCapacity, maxCapacity).clear();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.concurrent.ReferenceManager;
+
+import java.nio.ReadOnlyBufferException;
+
+/**
+ * Read-only buffer.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class ReadOnlyBuffer extends AbstractBuffer {
+ private final Buffer root;
+
+ public ReadOnlyBuffer(Buffer buffer, ReferenceManager<Buffer> referenceManager) {
+ super(buffer.bytes(), referenceManager);
+ this.root = buffer;
+ }
+
+ @Override
+ public boolean isDirect() {
+ return root.isDirect();
+ }
+
+ @Override
+ public boolean isFile() {
+ return root.isFile();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public Buffer compact() {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ protected void compact(int from, int to, int length) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer duplicate() {
+ return new ReadOnlyBuffer(root, referenceManager);
+ }
+
+ @Override
+ public Buffer acquire() {
+ root.acquire();
+ return this;
+ }
+
+ @Override
+ public boolean release() {
+ return root.release();
+ }
+
+ @Override
+ public Buffer zero(int offset, int length) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer zero(int offset) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer zero() {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeBoolean(int offset, boolean b) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer write(Buffer buffer) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer write(Bytes bytes) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer write(Bytes bytes, int offset, int length) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer write(int offset, Bytes bytes, int srcOffset, int length) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer write(byte[] bytes) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer write(byte[] bytes, int offset, int length) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer write(int offset, byte[] bytes, int srcOffset, int length) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeByte(int b) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeByte(int offset, int b) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedByte(int b) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedByte(int offset, int b) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeChar(char c) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeChar(int offset, char c) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeShort(short s) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeShort(int offset, short s) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedShort(int s) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedShort(int offset, int s) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeMedium(int m) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeMedium(int offset, int m) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedMedium(int m) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedMedium(int offset, int m) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeInt(int i) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeInt(int offset, int i) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedInt(long i) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUnsignedInt(int offset, long i) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeLong(long l) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeLong(int offset, long l) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeFloat(float f) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeFloat(int offset, float f) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeDouble(double d) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeDouble(int offset, double d) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeBoolean(boolean b) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer writeUTF8(String s) {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public Buffer flush() {
+ throw new ReadOnlyBufferException();
+ }
+
+ @Override
+ public void close() {
+ root.release();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+/**
+ * Sliced buffer.
+ * <p>
+ * The sliced buffer provides a view of a subset of an underlying buffer. This buffer operates directly on the {@link Bytes}
+ * underlying the child {@link Buffer} instance.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class SlicedBuffer extends AbstractBuffer {
+ private final Buffer root;
+
+ public SlicedBuffer(Buffer root, Bytes bytes, int offset, int initialCapacity, int maxCapacity) {
+ super(bytes, offset, initialCapacity, maxCapacity, null);
+ this.root = root;
+ root.acquire();
+ }
+
+ /**
+ * Returns the root buffer.
+ *
+ * @return The root buffer.
+ */
+ public Buffer root() {
+ return root;
+ }
+
+ @Override
+ public boolean isDirect() {
+ return root.isDirect();
+ }
+
+ @Override
+ protected void compact(int from, int to, int length) {
+ if (root instanceof AbstractBuffer) {
+ ((AbstractBuffer) root).compact(from, to, length);
+ }
+ }
+
+ @Override
+ public boolean isFile() {
+ return root.isFile();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return root.isReadOnly();
+ }
+
+ @Override
+ public Buffer compact() {
+ return null;
+ }
+
+ @Override
+ public Buffer duplicate() {
+ return new SlicedBuffer(root, bytes, offset(), capacity(), maxCapacity());
+ }
+
+ @Override
+ public Buffer acquire() {
+ root.acquire();
+ return this;
+ }
+
+ @Override
+ public boolean release() {
+ return root.release();
+ }
+
+ @Override
+ public void close() {
+ root.release();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import io.atomix.utils.concurrent.ReferenceManager;
+
+import java.nio.ByteOrder;
+
+/**
+ * Byte order swapped buffer.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class SwappedBuffer extends AbstractBuffer {
+ private final Buffer root;
+
+ SwappedBuffer(Buffer root, Bytes bytes, ReferenceManager<Buffer> referenceManager) {
+ super(bytes, referenceManager);
+ this.root = root;
+ }
+
+ public SwappedBuffer(Buffer buffer, int offset, int initialCapacity, int maxCapacity, ReferenceManager<Buffer> referenceManager) {
+ super(buffer.bytes().order(buffer.order() == ByteOrder.BIG_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN), offset, initialCapacity, maxCapacity, referenceManager);
+ this.root = buffer instanceof SwappedBuffer ? ((SwappedBuffer) buffer).root : buffer;
+ root.acquire();
+ }
+
+ /**
+ * Returns the root buffer.
+ *
+ * @return The root buffer.
+ */
+ public Buffer root() {
+ return root;
+ }
+
+ @Override
+ public boolean isDirect() {
+ return root.isDirect();
+ }
+
+ @Override
+ public boolean isFile() {
+ return root.isFile();
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return root.isReadOnly();
+ }
+
+ @Override
+ protected void compact(int from, int to, int length) {
+ if (root instanceof AbstractBuffer) {
+ ((AbstractBuffer) root).compact(from, to, length);
+ }
+ }
+
+ @Override
+ public Buffer duplicate() {
+ return new SwappedBuffer(root, offset(), capacity(), maxCapacity(), referenceManager);
+ }
+
+ @Override
+ public Buffer acquire() {
+ root.acquire();
+ return this;
+ }
+
+ @Override
+ public boolean release() {
+ return root.release();
+ }
+
+ @Override
+ public void close() {
+ root.release();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.ByteOrder;
+
+/**
+ * Bytes in swapped order.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class SwappedBytes extends WrappedBytes {
+
+ public SwappedBytes(Bytes bytes) {
+ super(bytes);
+ }
+
+ @Override
+ public ByteOrder order() {
+ return bytes.order() == ByteOrder.BIG_ENDIAN ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN;
+ }
+
+ @Override
+ public char readChar(int offset) {
+ return Character.reverseBytes(bytes.readChar(offset));
+ }
+
+ @Override
+ public short readShort(int offset) {
+ return Short.reverseBytes(bytes.readShort(offset));
+ }
+
+ @Override
+ public int readUnsignedShort(int offset) {
+ return Short.reverseBytes(bytes.readShort(offset)) & 0xFFFF;
+ }
+
+ @Override
+ public int readMedium(int offset) {
+ return Integer.reverseBytes(bytes.readMedium(offset));
+ }
+
+ @Override
+ public int readUnsignedMedium(int offset) {
+ return Integer.reverseBytes(bytes.readUnsignedMedium(offset));
+ }
+
+ @Override
+ public int readInt(int offset) {
+ return Integer.reverseBytes(bytes.readInt(offset));
+ }
+
+ @Override
+ public long readUnsignedInt(int offset) {
+ return Integer.reverseBytes(bytes.readInt(offset)) & 0xFFFFFFFFL;
+ }
+
+ @Override
+ public long readLong(int offset) {
+ return Long.reverseBytes(bytes.readLong(offset));
+ }
+
+ @Override
+ public float readFloat(int offset) {
+ return Float.intBitsToFloat(readInt(offset));
+ }
+
+ @Override
+ public double readDouble(int offset) {
+ return Double.longBitsToDouble(readLong(offset));
+ }
+
+ @Override
+ public Bytes writeChar(int offset, char c) {
+ bytes.writeChar(offset, Character.reverseBytes(c));
+ return this;
+ }
+
+ @Override
+ public Bytes writeShort(int offset, short s) {
+ bytes.writeShort(offset, Short.reverseBytes(s));
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedShort(int offset, int s) {
+ bytes.writeUnsignedShort(offset, Short.reverseBytes((short) s));
+ return this;
+ }
+
+ @Override
+ public Bytes writeMedium(int offset, int m) {
+ bytes.writeMedium(offset, Integer.reverseBytes(m));
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedMedium(int offset, int m) {
+ bytes.writeUnsignedMedium(offset, Integer.reverseBytes(m));
+ return this;
+ }
+
+ @Override
+ public Bytes writeInt(int offset, int i) {
+ bytes.writeInt(offset, Integer.reverseBytes(i));
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedInt(int offset, long i) {
+ bytes.writeUnsignedInt(offset, Integer.reverseBytes((int) i));
+ return this;
+ }
+
+ @Override
+ public Bytes writeLong(int offset, long l) {
+ bytes.writeLong(offset, Long.reverseBytes(l));
+ return this;
+ }
+
+ @Override
+ public Bytes writeFloat(int offset, float f) {
+ return writeInt(offset, Float.floatToRawIntBits(f));
+ }
+
+ @Override
+ public Bytes writeDouble(int offset, double d) {
+ return writeLong(offset, Double.doubleToRawLongBits(d));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+/**
+ * Unpooled buffer allocator.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public abstract class UnpooledAllocator implements BufferAllocator {
+
+ /**
+ * Returns the maximum buffer capacity.
+ *
+ * @return The maximum buffer capacity.
+ */
+ protected abstract int maxCapacity();
+
+ @Override
+ public Buffer allocate() {
+ return allocate(4096, maxCapacity());
+ }
+
+ @Override
+ public Buffer allocate(int capacity) {
+ return allocate(capacity, capacity);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+/**
+ * Unpooled direct allocator.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class UnpooledDirectAllocator extends UnpooledAllocator {
+
+ @Override
+ public Buffer allocate(int initialCapacity, int maxCapacity) {
+ return DirectBuffer.allocate(initialCapacity, maxCapacity);
+ }
+
+ @Override
+ protected int maxCapacity() {
+ return Integer.MAX_VALUE;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+/**
+ * Unpooled heap allocator.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class UnpooledHeapAllocator extends UnpooledAllocator {
+
+ @Override
+ protected int maxCapacity() {
+ return HeapBuffer.MAX_SIZE;
+ }
+
+ @Override
+ public Buffer allocate(int initialCapacity, int maxCapacity) {
+ return HeapBuffer.allocate(initialCapacity, maxCapacity);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.nio.ByteOrder;
+
+/**
+ * Wrapped bytes.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class WrappedBytes extends AbstractBytes {
+ protected final Bytes bytes;
+ private final Bytes root;
+
+ public WrappedBytes(Bytes bytes) {
+ if (bytes == null) {
+ throw new NullPointerException("bytes cannot be null");
+ }
+ this.bytes = bytes;
+ this.root = bytes instanceof WrappedBytes ? ((WrappedBytes) bytes).root : bytes;
+ }
+
+ /**
+ * Returns the root bytes.
+ */
+ public Bytes root() {
+ return root;
+ }
+
+ @Override
+ public int size() {
+ return bytes.size();
+ }
+
+ @Override
+ public Bytes resize(int newSize) {
+ return bytes.resize(newSize);
+ }
+
+ @Override
+ public ByteOrder order() {
+ return bytes.order();
+ }
+
+ @Override
+ public Bytes zero() {
+ bytes.zero();
+ return this;
+ }
+
+ @Override
+ public Bytes zero(int offset) {
+ bytes.zero(offset);
+ return this;
+ }
+
+ @Override
+ public Bytes zero(int offset, int length) {
+ bytes.zero(offset, length);
+ return this;
+ }
+
+ @Override
+ public Bytes read(int offset, Bytes dst, int dstOffset, int length) {
+ bytes.read(offset, dst, dstOffset, length);
+ return this;
+ }
+
+ @Override
+ public Bytes read(int offset, byte[] dst, int dstOffset, int length) {
+ bytes.read(offset, dst, dstOffset, length);
+ return this;
+ }
+
+ @Override
+ public int readByte(int offset) {
+ return bytes.readByte(offset);
+ }
+
+ @Override
+ public int readUnsignedByte(int offset) {
+ return bytes.readUnsignedByte(offset);
+ }
+
+ @Override
+ public char readChar(int offset) {
+ return bytes.readChar(offset);
+ }
+
+ @Override
+ public short readShort(int offset) {
+ return bytes.readShort(offset);
+ }
+
+ @Override
+ public int readUnsignedShort(int offset) {
+ return bytes.readUnsignedShort(offset);
+ }
+
+ @Override
+ public int readMedium(int offset) {
+ return bytes.readMedium(offset);
+ }
+
+ @Override
+ public int readUnsignedMedium(int offset) {
+ return bytes.readUnsignedMedium(offset);
+ }
+
+ @Override
+ public int readInt(int offset) {
+ return bytes.readInt(offset);
+ }
+
+ @Override
+ public long readUnsignedInt(int offset) {
+ return bytes.readUnsignedInt(offset);
+ }
+
+ @Override
+ public long readLong(int offset) {
+ return bytes.readLong(offset);
+ }
+
+ @Override
+ public float readFloat(int offset) {
+ return bytes.readFloat(offset);
+ }
+
+ @Override
+ public double readDouble(int offset) {
+ return bytes.readDouble(offset);
+ }
+
+ @Override
+ public boolean readBoolean(int offset) {
+ return bytes.readBoolean(offset);
+ }
+
+ @Override
+ public String readString(int offset) {
+ return bytes.readString(offset);
+ }
+
+ @Override
+ public String readUTF8(int offset) {
+ return bytes.readUTF8(offset);
+ }
+
+ @Override
+ public Bytes write(int offset, Bytes src, int srcOffset, int length) {
+ bytes.write(offset, src, srcOffset, length);
+ return this;
+ }
+
+ @Override
+ public Bytes write(int offset, byte[] src, int srcOffset, int length) {
+ bytes.write(offset, src, srcOffset, length);
+ return this;
+ }
+
+ @Override
+ public Bytes writeByte(int offset, int b) {
+ bytes.writeByte(offset, b);
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedByte(int offset, int b) {
+ bytes.writeUnsignedByte(offset, b);
+ return this;
+ }
+
+ @Override
+ public Bytes writeChar(int offset, char c) {
+ bytes.writeChar(offset, c);
+ return this;
+ }
+
+ @Override
+ public Bytes writeShort(int offset, short s) {
+ bytes.writeShort(offset, s);
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedShort(int offset, int s) {
+ bytes.writeUnsignedShort(offset, s);
+ return this;
+ }
+
+ @Override
+ public Bytes writeMedium(int offset, int m) {
+ bytes.writeMedium(offset, m);
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedMedium(int offset, int m) {
+ bytes.writeUnsignedMedium(offset, m);
+ return this;
+ }
+
+ @Override
+ public Bytes writeInt(int offset, int i) {
+ bytes.writeInt(offset, i);
+ return this;
+ }
+
+ @Override
+ public Bytes writeUnsignedInt(int offset, long i) {
+ bytes.writeUnsignedInt(offset, i);
+ return this;
+ }
+
+ @Override
+ public Bytes writeLong(int offset, long l) {
+ bytes.writeLong(offset, l);
+ return this;
+ }
+
+ @Override
+ public Bytes writeFloat(int offset, float f) {
+ bytes.writeFloat(offset, f);
+ return this;
+ }
+
+ @Override
+ public Bytes writeDouble(int offset, double d) {
+ bytes.writeDouble(offset, d);
+ return this;
+ }
+
+ @Override
+ public Bytes writeBoolean(int offset, boolean b) {
+ bytes.writeBoolean(offset, b);
+ return this;
+ }
+
+ @Override
+ public Bytes writeString(int offset, String s) {
+ bytes.writeString(offset, s);
+ return this;
+ }
+
+ @Override
+ public Bytes writeUTF8(int offset, String s) {
+ bytes.writeUTF8(offset, s);
+ return this;
+ }
+
+ @Override
+ public Bytes flush() {
+ bytes.flush();
+ return this;
+ }
+
+ @Override
+ public void close() {
+ bytes.close();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides a low-level {@link io.atomix.storage.buffer.Buffer} abstraction backed by on- or off-heap memory, memory mapped
+ * files, or {@link java.io.RandomAccessFile}.
+ */
+package io.atomix.storage.buffer;
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Delegating journal.
+ */
+public class DelegatingJournal<E> implements Journal<E> {
+ private final Journal<E> delegate;
+
+ public DelegatingJournal(Journal<E> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public JournalWriter<E> writer() {
+ return delegate.writer();
+ }
+
+ @Override
+ public JournalReader<E> openReader(long index) {
+ return delegate.openReader(index);
+ }
+
+ @Override
+ public JournalReader<E> openReader(long index, JournalReader.Mode mode) {
+ return delegate.openReader(index, mode);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return delegate.isOpen();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("delegate", delegate)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Journal reader delegate.
+ */
+public class DelegatingJournalReader<E> implements JournalReader<E> {
+ private final JournalReader<E> delegate;
+
+ public DelegatingJournalReader(JournalReader<E> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public long getFirstIndex() {
+ return delegate.getFirstIndex();
+ }
+
+ @Override
+ public long getCurrentIndex() {
+ return delegate.getCurrentIndex();
+ }
+
+ @Override
+ public Indexed<E> getCurrentEntry() {
+ return delegate.getCurrentEntry();
+ }
+
+ @Override
+ public long getNextIndex() {
+ return delegate.getNextIndex();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return delegate.hasNext();
+ }
+
+ @Override
+ public Indexed<E> next() {
+ return delegate.next();
+ }
+
+ @Override
+ public void reset() {
+ delegate.reset();
+ }
+
+ @Override
+ public void reset(long index) {
+ delegate.reset(index);
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("delegate", delegate)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Journal writer delegate.
+ */
+public class DelegatingJournalWriter<E> implements JournalWriter<E> {
+ private final JournalWriter<E> delegate;
+
+ public DelegatingJournalWriter(JournalWriter<E> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public long getLastIndex() {
+ return delegate.getLastIndex();
+ }
+
+ @Override
+ public Indexed<E> getLastEntry() {
+ return delegate.getLastEntry();
+ }
+
+ @Override
+ public long getNextIndex() {
+ return delegate.getNextIndex();
+ }
+
+ @Override
+ public <T extends E> Indexed<T> append(T entry) {
+ return delegate.append(entry);
+ }
+
+ @Override
+ public void append(Indexed<E> entry) {
+ delegate.append(entry);
+ }
+
+ @Override
+ public void commit(long index) {
+ delegate.commit(index);
+ }
+
+ @Override
+ public void reset(long index) {
+ delegate.reset(index);
+ }
+
+ @Override
+ public void truncate(long index) {
+ delegate.truncate(index);
+ }
+
+ @Override
+ public void flush() {
+ delegate.flush();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("delegate", delegate)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.StorageException;
+import io.atomix.storage.journal.index.JournalIndex;
+import io.atomix.storage.journal.index.Position;
+import io.atomix.utils.serializer.Namespace;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.NoSuchElementException;
+import java.util.zip.CRC32;
+import java.util.zip.Checksum;
+
+/**
+ * Log segment reader.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+class FileChannelJournalSegmentReader<E> implements JournalReader<E> {
+ private final FileChannel channel;
+ private final int maxEntrySize;
+ private final JournalIndex index;
+ private final Namespace namespace;
+ private final ByteBuffer memory;
+ private final long firstIndex;
+ private Indexed<E> currentEntry;
+ private Indexed<E> nextEntry;
+
+ FileChannelJournalSegmentReader(
+ FileChannel channel,
+ JournalSegment<E> segment,
+ int maxEntrySize,
+ JournalIndex index,
+ Namespace namespace) {
+ this.channel = channel;
+ this.maxEntrySize = maxEntrySize;
+ this.index = index;
+ this.namespace = namespace;
+ this.memory = ByteBuffer.allocate((maxEntrySize + Integer.BYTES + Integer.BYTES) * 2);
+ this.firstIndex = segment.index();
+ reset();
+ }
+
+ @Override
+ public long getFirstIndex() {
+ return firstIndex;
+ }
+
+ @Override
+ public long getCurrentIndex() {
+ return currentEntry != null ? currentEntry.index() : 0;
+ }
+
+ @Override
+ public Indexed<E> getCurrentEntry() {
+ return currentEntry;
+ }
+
+ @Override
+ public long getNextIndex() {
+ return currentEntry != null ? currentEntry.index() + 1 : firstIndex;
+ }
+
+ @Override
+ public void reset(long index) {
+ reset();
+ Position position = this.index.lookup(index - 1);
+ if (position != null) {
+ currentEntry = new Indexed<>(position.index() - 1, null, 0);
+ try {
+ channel.position(position.position());
+ memory.clear().flip();
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ readNext();
+ }
+ while (getNextIndex() < index && hasNext()) {
+ next();
+ }
+ }
+
+ @Override
+ public void reset() {
+ try {
+ channel.position(JournalSegmentDescriptor.BYTES);
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ memory.clear().limit(0);
+ currentEntry = null;
+ nextEntry = null;
+ readNext();
+ }
+
+ @Override
+ public boolean hasNext() {
+ // If the next entry is null, check whether a next entry exists.
+ if (nextEntry == null) {
+ readNext();
+ }
+ return nextEntry != null;
+ }
+
+ @Override
+ public Indexed<E> next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ // Set the current entry to the next entry.
+ currentEntry = nextEntry;
+
+ // Reset the next entry to null.
+ nextEntry = null;
+
+ // Read the next entry in the segment.
+ readNext();
+
+ // Return the current entry.
+ return currentEntry;
+ }
+
+ /**
+ * Reads the next entry in the segment.
+ */
+ @SuppressWarnings("unchecked")
+ private void readNext() {
+ // Compute the index of the next entry in the segment.
+ final long index = getNextIndex();
+
+ try {
+ // Read more bytes from the segment if necessary.
+ if (memory.remaining() < maxEntrySize) {
+ long position = channel.position() + memory.position();
+ channel.position(position);
+ memory.clear();
+ channel.read(memory);
+ channel.position(position);
+ memory.flip();
+ }
+
+ // Mark the buffer so it can be reset if necessary.
+ memory.mark();
+
+ try {
+ // Read the length of the entry.
+ final int length = memory.getInt();
+
+ // If the buffer length is zero then return.
+ if (length <= 0 || length > maxEntrySize) {
+ memory.reset().limit(memory.position());
+ nextEntry = null;
+ return;
+ }
+
+ // Read the checksum of the entry.
+ long checksum = memory.getInt() & 0xFFFFFFFFL;
+
+ // Compute the checksum for the entry bytes.
+ final Checksum crc32 = new CRC32();
+ crc32.update(memory.array(), memory.position(), length);
+
+ // If the stored checksum equals the computed checksum, return the entry.
+ if (checksum == crc32.getValue()) {
+ int limit = memory.limit();
+ memory.limit(memory.position() + length);
+ E entry = namespace.deserialize(memory);
+ memory.limit(limit);
+ nextEntry = new Indexed<>(index, entry, length);
+ } else {
+ memory.reset().limit(memory.position());
+ nextEntry = null;
+ }
+ } catch (BufferUnderflowException e) {
+ memory.reset().limit(memory.position());
+ nextEntry = null;
+ }
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public void close() {
+ // Do nothing. The parent reader manages the channel.
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import com.esotericsoftware.kryo.KryoException;
+import io.atomix.storage.StorageException;
+import io.atomix.storage.journal.index.JournalIndex;
+import io.atomix.utils.serializer.Namespace;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.zip.CRC32;
+import java.util.zip.Checksum;
+
+/**
+ * Segment writer.
+ * <p>
+ * The format of an entry in the log is as follows:
+ * <ul>
+ * <li>64-bit index</li>
+ * <li>8-bit boolean indicating whether a term change is contained in the entry</li>
+ * <li>64-bit optional term</li>
+ * <li>32-bit signed entry length, including the entry type ID</li>
+ * <li>8-bit signed entry type ID</li>
+ * <li>n-bit entry bytes</li>
+ * </ul>
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+class FileChannelJournalSegmentWriter<E> implements JournalWriter<E> {
+ private final FileChannel channel;
+ private final JournalSegment segment;
+ private final int maxEntrySize;
+ private final JournalIndex index;
+ private final Namespace namespace;
+ private final ByteBuffer memory;
+ private final long firstIndex;
+ private Indexed<E> lastEntry;
+
+ FileChannelJournalSegmentWriter(
+ FileChannel channel,
+ JournalSegment segment,
+ int maxEntrySize,
+ JournalIndex index,
+ Namespace namespace) {
+ this.channel = channel;
+ this.segment = segment;
+ this.maxEntrySize = maxEntrySize;
+ this.index = index;
+ this.memory = ByteBuffer.allocate((maxEntrySize + Integer.BYTES + Integer.BYTES) * 2);
+ memory.limit(0);
+ this.namespace = namespace;
+ this.firstIndex = segment.index();
+ reset(0);
+ }
+
+ @Override
+ public void reset(long index) {
+ long nextIndex = firstIndex;
+
+ // Clear the buffer indexes.
+ try {
+ channel.position(JournalSegmentDescriptor.BYTES);
+ memory.clear().flip();
+
+ // Record the current buffer position.
+ long position = channel.position();
+
+ // Read more bytes from the segment if necessary.
+ if (memory.remaining() < maxEntrySize) {
+ memory.clear();
+ channel.read(memory);
+ channel.position(position);
+ memory.flip();
+ }
+
+ // Read the entry length.
+ memory.mark();
+ int length = memory.getInt();
+
+ // If the length is non-zero, read the entry.
+ while (0 < length && length <= maxEntrySize && (index == 0 || nextIndex <= index)) {
+
+ // Read the checksum of the entry.
+ final long checksum = memory.getInt() & 0xFFFFFFFFL;
+
+ // Compute the checksum for the entry bytes.
+ final Checksum crc32 = new CRC32();
+ crc32.update(memory.array(), memory.position(), length);
+
+ // If the stored checksum equals the computed checksum, return the entry.
+ if (checksum == crc32.getValue()) {
+ int limit = memory.limit();
+ memory.limit(memory.position() + length);
+ final E entry = namespace.deserialize(memory);
+ memory.limit(limit);
+ lastEntry = new Indexed<>(nextIndex, entry, length);
+ this.index.index(nextIndex, (int) position);
+ nextIndex++;
+ } else {
+ break;
+ }
+
+ // Update the current position for indexing.
+ position = channel.position() + memory.position();
+
+ // Read more bytes from the segment if necessary.
+ if (memory.remaining() < maxEntrySize) {
+ channel.position(position);
+ memory.clear();
+ channel.read(memory);
+ channel.position(position);
+ memory.flip();
+ }
+
+ memory.mark();
+ length = memory.getInt();
+ }
+
+ // Reset the buffer to the previous mark.
+ channel.position(channel.position() + memory.reset().position());
+ } catch (BufferUnderflowException e) {
+ try {
+ channel.position(channel.position() + memory.reset().position());
+ } catch (IOException e2) {
+ throw new StorageException(e2);
+ }
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public long getLastIndex() {
+ return lastEntry != null ? lastEntry.index() : segment.index() - 1;
+ }
+
+ @Override
+ public Indexed<E> getLastEntry() {
+ return lastEntry;
+ }
+
+ @Override
+ public long getNextIndex() {
+ if (lastEntry != null) {
+ return lastEntry.index() + 1;
+ } else {
+ return firstIndex;
+ }
+ }
+
+ /**
+ * Returns the size of the underlying buffer.
+ *
+ * @return The size of the underlying buffer.
+ */
+ public long size() {
+ try {
+ return channel.position();
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ /**
+ * Returns a boolean indicating whether the segment is empty.
+ *
+ * @return Indicates whether the segment is empty.
+ */
+ public boolean isEmpty() {
+ return lastEntry == null;
+ }
+
+ /**
+ * Returns a boolean indicating whether the segment is full.
+ *
+ * @return Indicates whether the segment is full.
+ */
+ public boolean isFull() {
+ return size() >= segment.descriptor().maxSegmentSize()
+ || getNextIndex() - firstIndex >= segment.descriptor().maxEntries();
+ }
+
+ /**
+ * Returns the first index written to the segment.
+ */
+ public long firstIndex() {
+ return firstIndex;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void append(Indexed<E> entry) {
+ final long nextIndex = getNextIndex();
+
+ // If the entry's index is greater than the next index in the segment, skip some entries.
+ if (entry.index() > nextIndex) {
+ throw new IndexOutOfBoundsException("Entry index is not sequential");
+ }
+
+ // If the entry's index is less than the next index, truncate the segment.
+ if (entry.index() < nextIndex) {
+ truncate(entry.index() - 1);
+ }
+ append(entry.entry());
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends E> Indexed<T> append(T entry) {
+ // Store the entry index.
+ final long index = getNextIndex();
+
+ try {
+ // Serialize the entry.
+ memory.clear();
+ memory.position(Integer.BYTES + Integer.BYTES);
+ try {
+ namespace.serialize(entry, memory);
+ } catch (KryoException e) {
+ throw new StorageException.TooLarge("Entry size exceeds maximum allowed bytes (" + maxEntrySize + ")");
+ }
+ memory.flip();
+
+ final int length = memory.limit() - (Integer.BYTES + Integer.BYTES);
+
+ // Ensure there's enough space left in the buffer to store the entry.
+ long position = channel.position();
+ if (segment.descriptor().maxSegmentSize() - position < length + Integer.BYTES + Integer.BYTES) {
+ throw new BufferOverflowException();
+ }
+
+ // If the entry length exceeds the maximum entry size then throw an exception.
+ if (length > maxEntrySize) {
+ throw new StorageException.TooLarge("Entry size " + length + " exceeds maximum allowed bytes (" + maxEntrySize + ")");
+ }
+
+ // Compute the checksum for the entry.
+ final Checksum crc32 = new CRC32();
+ crc32.update(memory.array(), Integer.BYTES + Integer.BYTES, memory.limit() - (Integer.BYTES + Integer.BYTES));
+ final long checksum = crc32.getValue();
+
+ // Create a single byte[] in memory for the entire entry and write it as a batch to the underlying buffer.
+ memory.putInt(0, length);
+ memory.putInt(Integer.BYTES, (int) checksum);
+ channel.write(memory);
+
+ // Update the last entry with the correct index/term/length.
+ Indexed<E> indexedEntry = new Indexed<>(index, entry, length);
+ this.lastEntry = indexedEntry;
+ this.index.index(index, (int) position);
+ return (Indexed<T>) indexedEntry;
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public void commit(long index) {
+
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void truncate(long index) {
+ // If the index is greater than or equal to the last index, skip the truncate.
+ if (index >= getLastIndex()) {
+ return;
+ }
+
+ // Reset the last entry.
+ lastEntry = null;
+
+ try {
+ // Truncate the index.
+ this.index.truncate(index);
+
+ if (index < segment.index()) {
+ channel.position(JournalSegmentDescriptor.BYTES);
+ channel.write(zero());
+ channel.position(JournalSegmentDescriptor.BYTES);
+ } else {
+ // Reset the writer to the given index.
+ reset(index);
+
+ // Zero entries after the given index.
+ long position = channel.position();
+ channel.write(zero());
+ channel.position(position);
+ }
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ /**
+ * Returns a zeroed out byte buffer.
+ */
+ private ByteBuffer zero() {
+ memory.clear();
+ for (int i = 0; i < memory.limit(); i++) {
+ memory.put(i, (byte) 0);
+ }
+ return memory;
+ }
+
+ @Override
+ public void flush() {
+ try {
+ if (channel.isOpen()) {
+ channel.force(true);
+ }
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public void close() {
+ flush();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Indexed journal entry.
+ */
+public class Indexed<E> {
+ private final long index;
+ private final E entry;
+ private final int size;
+
+ public Indexed(long index, E entry, int size) {
+ this.index = index;
+ this.entry = entry;
+ this.size = size;
+ }
+
+ /**
+ * Returns the entry index.
+ *
+ * @return The entry index.
+ */
+ public long index() {
+ return index;
+ }
+
+ /**
+ * Returns the indexed entry.
+ *
+ * @return The indexed entry.
+ */
+ public E entry() {
+ return entry;
+ }
+
+ /**
+ * Returns the serialized entry size.
+ *
+ * @return The serialized entry size.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Returns the entry type class.
+ *
+ * @return The entry class.
+ */
+ public Class<?> type() {
+ return entry.getClass();
+ }
+
+ /**
+ * Casts the entry to the given type.
+ *
+ * @return The cast entry.
+ */
+ @SuppressWarnings("unchecked")
+ public <E> Indexed<E> cast() {
+ return (Indexed<E>) this;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("index", index)
+ .add("entry", entry)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import java.io.Closeable;
+
+/**
+ * Journal.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface Journal<E> extends Closeable {
+
+ /**
+ * Returns the journal writer.
+ *
+ * @return The journal writer.
+ */
+ JournalWriter<E> writer();
+
+ /**
+ * Opens a new journal reader.
+ *
+ * @param index The index at which to start the reader.
+ * @return A new journal reader.
+ */
+ JournalReader<E> openReader(long index);
+
+ /**
+ * Opens a new journal reader.
+ *
+ * @param index The index at which to start the reader.
+ * @param mode the reader mode
+ * @return A new journal reader.
+ */
+ JournalReader<E> openReader(long index, JournalReader.Mode mode);
+
+ /**
+ * Returns a boolean indicating whether the journal is open.
+ *
+ * @return Indicates whether the journal is open.
+ */
+ boolean isOpen();
+
+ @Override
+ void close();
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import java.util.Iterator;
+
+/**
+ * Log reader.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface JournalReader<E> extends Iterator<Indexed<E>>, AutoCloseable {
+
+ /**
+ * Raft log reader mode.
+ */
+ enum Mode {
+
+ /**
+ * Reads all entries from the log.
+ */
+ ALL,
+
+ /**
+ * Reads committed entries from the log.
+ */
+ COMMITS,
+ }
+
+ /**
+ * Returns the first index in the journal.
+ *
+ * @return the first index in the journal
+ */
+ long getFirstIndex();
+
+ /**
+ * Returns the current reader index.
+ *
+ * @return The current reader index.
+ */
+ long getCurrentIndex();
+
+ /**
+ * Returns the last read entry.
+ *
+ * @return The last read entry.
+ */
+ Indexed<E> getCurrentEntry();
+
+ /**
+ * Returns the next reader index.
+ *
+ * @return The next reader index.
+ */
+ long getNextIndex();
+
+ /**
+ * Returns whether the reader has a next entry to read.
+ *
+ * @return Whether the reader has a next entry to read.
+ */
+ @Override
+ boolean hasNext();
+
+ /**
+ * Returns the next entry in the reader.
+ *
+ * @return The next entry in the reader.
+ */
+ @Override
+ Indexed<E> next();
+
+ /**
+ * Resets the reader to the start.
+ */
+ void reset();
+
+ /**
+ * Resets the reader to the given index.
+ *
+ * @param index The index to which to reset the reader.
+ */
+ void reset(long index);
+
+ @Override
+ void close();
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import com.google.common.collect.Sets;
+import io.atomix.storage.StorageException;
+import io.atomix.storage.StorageLevel;
+import io.atomix.storage.journal.index.JournalIndex;
+import io.atomix.storage.journal.index.SparseJournalIndex;
+import io.atomix.utils.serializer.Namespace;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Log segment.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class JournalSegment<E> implements AutoCloseable {
+ private final JournalSegmentFile file;
+ private final JournalSegmentDescriptor descriptor;
+ private final StorageLevel storageLevel;
+ private final int maxEntrySize;
+ private final JournalIndex index;
+ private final Namespace namespace;
+ private final MappableJournalSegmentWriter<E> writer;
+ private final Set<MappableJournalSegmentReader<E>> readers = Sets.newConcurrentHashSet();
+ private final AtomicInteger references = new AtomicInteger();
+ private boolean open = true;
+
+ public JournalSegment(
+ JournalSegmentFile file,
+ JournalSegmentDescriptor descriptor,
+ StorageLevel storageLevel,
+ int maxEntrySize,
+ double indexDensity,
+ Namespace namespace) {
+ this.file = file;
+ this.descriptor = descriptor;
+ this.storageLevel = storageLevel;
+ this.maxEntrySize = maxEntrySize;
+ this.index = new SparseJournalIndex(indexDensity);
+ this.namespace = namespace;
+ this.writer = new MappableJournalSegmentWriter<>(openChannel(file.file()), this, maxEntrySize, index, namespace);
+ }
+
+ private FileChannel openChannel(File file) {
+ try {
+ return FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ /**
+ * Returns the segment ID.
+ *
+ * @return The segment ID.
+ */
+ public long id() {
+ return descriptor.id();
+ }
+
+ /**
+ * Returns the segment version.
+ *
+ * @return The segment version.
+ */
+ public long version() {
+ return descriptor.version();
+ }
+
+ /**
+ * Returns the segment's starting index.
+ *
+ * @return The segment's starting index.
+ */
+ public long index() {
+ return descriptor.index();
+ }
+
+ /**
+ * Returns the last index in the segment.
+ *
+ * @return The last index in the segment.
+ */
+ public long lastIndex() {
+ return writer.getLastIndex();
+ }
+
+ /**
+ * Returns the size of the segment.
+ *
+ * @return the size of the segment
+ */
+ public int size() {
+ return writer.size();
+ }
+
+ /**
+ * Returns the segment file.
+ *
+ * @return The segment file.
+ */
+ public JournalSegmentFile file() {
+ return file;
+ }
+
+ /**
+ * Returns the segment descriptor.
+ *
+ * @return The segment descriptor.
+ */
+ public JournalSegmentDescriptor descriptor() {
+ return descriptor;
+ }
+
+ /**
+ * Returns a boolean value indicating whether the segment is empty.
+ *
+ * @return Indicates whether the segment is empty.
+ */
+ public boolean isEmpty() {
+ return length() == 0;
+ }
+
+ /**
+ * Returns the segment length.
+ *
+ * @return The segment length.
+ */
+ public long length() {
+ return writer.getNextIndex() - index();
+ }
+
+ /**
+ * Acquires a reference to the log segment.
+ */
+ void acquire() {
+ if (references.getAndIncrement() == 0 && open) {
+ map();
+ }
+ }
+
+ /**
+ * Releases a reference to the log segment.
+ */
+ void release() {
+ if (references.decrementAndGet() == 0 && open) {
+ unmap();
+ }
+ }
+
+ /**
+ * Maps the log segment into memory.
+ */
+ private void map() {
+ if (storageLevel == StorageLevel.MAPPED) {
+ MappedByteBuffer buffer = writer.map();
+ readers.forEach(reader -> reader.map(buffer));
+ }
+ }
+
+ /**
+ * Unmaps the log segment from memory.
+ */
+ private void unmap() {
+ if (storageLevel == StorageLevel.MAPPED) {
+ writer.unmap();
+ readers.forEach(reader -> reader.unmap());
+ }
+ }
+
+ /**
+ * Returns the segment writer.
+ *
+ * @return The segment writer.
+ */
+ public MappableJournalSegmentWriter<E> writer() {
+ checkOpen();
+ return writer;
+ }
+
+ /**
+ * Creates a new segment reader.
+ *
+ * @return A new segment reader.
+ */
+ MappableJournalSegmentReader<E> createReader() {
+ checkOpen();
+ MappableJournalSegmentReader<E> reader = new MappableJournalSegmentReader<>(
+ openChannel(file.file()), this, maxEntrySize, index, namespace);
+ MappedByteBuffer buffer = writer.buffer();
+ if (buffer != null) {
+ reader.map(buffer);
+ }
+ readers.add(reader);
+ return reader;
+ }
+
+ /**
+ * Closes a segment reader.
+ *
+ * @param reader the closed segment reader
+ */
+ void closeReader(MappableJournalSegmentReader<E> reader) {
+ readers.remove(reader);
+ }
+
+ /**
+ * Checks whether the segment is open.
+ */
+ private void checkOpen() {
+ checkState(open, "Segment not open");
+ }
+
+ /**
+ * Returns a boolean indicating whether the segment is open.
+ *
+ * @return indicates whether the segment is open
+ */
+ public boolean isOpen() {
+ return open;
+ }
+
+ /**
+ * Closes the segment.
+ */
+ @Override
+ public void close() {
+ unmap();
+ writer.close();
+ readers.forEach(reader -> reader.close());
+ open = false;
+ }
+
+ /**
+ * Deletes the segment.
+ */
+ public void delete() {
+ try {
+ Files.deleteIfExists(file.file().toPath());
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("id", id())
+ .add("version", version())
+ .add("index", index())
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.nio.ByteBuffer;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Stores information about a {@link JournalSegment} of the log.
+ * <p>
+ * The segment descriptor manages metadata related to a single segment of the log. Descriptors are stored within the
+ * first {@code 64} bytes of each segment in the following order:
+ * <ul>
+ * <li>{@code id} (64-bit signed integer) - A unique segment identifier. This is a monotonically increasing number within
+ * each log. Segments with in-sequence identifiers should contain in-sequence indexes.</li>
+ * <li>{@code index} (64-bit signed integer) - The effective first index of the segment. This indicates the index at which
+ * the first entry should be written to the segment. Indexes are monotonically increasing thereafter.</li>
+ * <li>{@code version} (64-bit signed integer) - The version of the segment. Versions are monotonically increasing
+ * starting at {@code 1}. Versions will only be incremented whenever the segment is rewritten to another memory/disk
+ * space, e.g. after log compaction.</li>
+ * <li>{@code maxSegmentSize} (32-bit unsigned integer) - The maximum number of bytes allowed in the segment.</li>
+ * <li>{@code maxEntries} (32-bit signed integer) - The total number of expected entries in the segment. This is the final
+ * number of entries allowed within the segment both before and after compaction. This entry count is used to determine
+ * the count of internal indexing and deduplication facilities.</li>
+ * <li>{@code updated} (64-bit signed integer) - The last update to the segment in terms of milliseconds since the epoch.
+ * When the segment is first constructed, the {@code updated} time is {@code 0}. Once all entries in the segment have
+ * been committed, the {@code updated} time should be set to the current time. Log compaction should not result in a
+ * change to {@code updated}.</li>
+ * <li>{@code locked} (8-bit boolean) - A boolean indicating whether the segment is locked. Segments will be locked once
+ * all entries have been committed to the segment. The lock state of each segment is used to determine log compaction
+ * and recovery behavior.</li>
+ * </ul>
+ * The remainder of the 64 segment header bytes are reserved for future metadata.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public final class JournalSegmentDescriptor {
+ public static final int BYTES = 64;
+
+ // Current segment version.
+ @VisibleForTesting
+ static final int VERSION = 1;
+
+ // The lengths of each field in the header.
+ private static final int VERSION_LENGTH = Integer.BYTES; // 32-bit signed integer
+ private static final int ID_LENGTH = Long.BYTES; // 64-bit signed integer
+ private static final int INDEX_LENGTH = Long.BYTES; // 64-bit signed integer
+ private static final int MAX_SIZE_LENGTH = Integer.BYTES; // 32-bit signed integer
+ private static final int MAX_ENTRIES_LENGTH = Integer.BYTES; // 32-bit signed integer
+ private static final int UPDATED_LENGTH = Long.BYTES; // 64-bit signed integer
+
+ // The positions of each field in the header.
+ private static final int VERSION_POSITION = 0; // 0
+ private static final int ID_POSITION = VERSION_POSITION + VERSION_LENGTH; // 4
+ private static final int INDEX_POSITION = ID_POSITION + ID_LENGTH; // 12
+ private static final int MAX_SIZE_POSITION = INDEX_POSITION + INDEX_LENGTH; // 20
+ private static final int MAX_ENTRIES_POSITION = MAX_SIZE_POSITION + MAX_SIZE_LENGTH; // 24
+ private static final int UPDATED_POSITION = MAX_ENTRIES_POSITION + MAX_ENTRIES_LENGTH; // 28
+
+ /**
+ * Returns a descriptor builder.
+ * <p>
+ * The descriptor builder will write segment metadata to a {@code 48} byte in-memory buffer.
+ *
+ * @return The descriptor builder.
+ */
+ public static Builder builder() {
+ return new Builder(ByteBuffer.allocate(BYTES));
+ }
+
+ /**
+ * Returns a descriptor builder for the given descriptor buffer.
+ *
+ * @param buffer The descriptor buffer.
+ * @return The descriptor builder.
+ * @throws NullPointerException if {@code buffer} is null
+ */
+ public static Builder builder(ByteBuffer buffer) {
+ return new Builder(buffer);
+ }
+
+ private final ByteBuffer buffer;
+ private final int version;
+ private final long id;
+ private final long index;
+ private final int maxSegmentSize;
+ private final int maxEntries;
+ private volatile long updated;
+ private volatile boolean locked;
+
+ /**
+ * @throws NullPointerException if {@code buffer} is null
+ */
+ public JournalSegmentDescriptor(ByteBuffer buffer) {
+ this.buffer = buffer;
+ this.version = buffer.getInt();
+ this.id = buffer.getLong();
+ this.index = buffer.getLong();
+ this.maxSegmentSize = buffer.getInt();
+ this.maxEntries = buffer.getInt();
+ this.updated = buffer.getLong();
+ this.locked = buffer.get() == 1;
+ }
+
+ /**
+ * Returns the segment version.
+ * <p>
+ * Versions are monotonically increasing starting at {@code 1}.
+ *
+ * @return The segment version.
+ */
+ public int version() {
+ return version;
+ }
+
+ /**
+ * Returns the segment identifier.
+ * <p>
+ * The segment ID is a monotonically increasing number within each log. Segments with in-sequence identifiers should
+ * contain in-sequence indexes.
+ *
+ * @return The segment identifier.
+ */
+ public long id() {
+ return id;
+ }
+
+ /**
+ * Returns the segment index.
+ * <p>
+ * The index indicates the index at which the first entry should be written to the segment. Indexes are monotonically
+ * increasing thereafter.
+ *
+ * @return The segment index.
+ */
+ public long index() {
+ return index;
+ }
+
+ /**
+ * Returns the maximum count of the segment.
+ *
+ * @return The maximum allowed count of the segment.
+ */
+ public int maxSegmentSize() {
+ return maxSegmentSize;
+ }
+
+ /**
+ * Returns the maximum number of entries allowed in the segment.
+ *
+ * @return The maximum number of entries allowed in the segment.
+ */
+ public int maxEntries() {
+ return maxEntries;
+ }
+
+ /**
+ * Returns last time the segment was updated.
+ * <p>
+ * When the segment is first constructed, the {@code updated} time is {@code 0}. Once all entries in the segment have
+ * been committed, the {@code updated} time should be set to the current time. Log compaction should not result in a
+ * change to {@code updated}.
+ *
+ * @return The last time the segment was updated in terms of milliseconds since the epoch.
+ */
+ public long updated() {
+ return updated;
+ }
+
+ /**
+ * Writes an update to the descriptor.
+ */
+ public void update(long timestamp) {
+ if (!locked) {
+ buffer.putLong(UPDATED_POSITION, timestamp);
+ this.updated = timestamp;
+ }
+ }
+
+ /**
+ * Copies the segment to a new buffer.
+ */
+ JournalSegmentDescriptor copyTo(ByteBuffer buffer) {
+ buffer.putInt(version);
+ buffer.putLong(id);
+ buffer.putLong(index);
+ buffer.putInt(maxSegmentSize);
+ buffer.putInt(maxEntries);
+ buffer.putLong(updated);
+ buffer.put(locked ? (byte) 1 : (byte) 0);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("version", version)
+ .add("id", id)
+ .add("index", index)
+ .add("updated", updated)
+ .toString();
+ }
+
+ /**
+ * Segment descriptor builder.
+ */
+ public static class Builder {
+ private final ByteBuffer buffer;
+
+ private Builder(ByteBuffer buffer) {
+ this.buffer = checkNotNull(buffer, "buffer cannot be null");
+ buffer.putInt(VERSION_POSITION, VERSION);
+ }
+
+ /**
+ * Sets the segment identifier.
+ *
+ * @param id The segment identifier.
+ * @return The segment descriptor builder.
+ */
+ public Builder withId(long id) {
+ buffer.putLong(ID_POSITION, id);
+ return this;
+ }
+
+ /**
+ * Sets the segment index.
+ *
+ * @param index The segment starting index.
+ * @return The segment descriptor builder.
+ */
+ public Builder withIndex(long index) {
+ buffer.putLong(INDEX_POSITION, index);
+ return this;
+ }
+
+ /**
+ * Sets maximum count of the segment.
+ *
+ * @param maxSegmentSize The maximum count of the segment.
+ * @return The segment descriptor builder.
+ */
+ public Builder withMaxSegmentSize(int maxSegmentSize) {
+ buffer.putInt(MAX_SIZE_POSITION, maxSegmentSize);
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of entries in the segment.
+ *
+ * @param maxEntries The maximum number of entries in the segment.
+ * @return The segment descriptor builder.
+ * @deprecated since 3.0.2
+ */
+ @Deprecated
+ public Builder withMaxEntries(int maxEntries) {
+ buffer.putInt(MAX_ENTRIES_POSITION, maxEntries);
+ return this;
+ }
+
+ /**
+ * Builds the segment descriptor.
+ *
+ * @return The built segment descriptor.
+ */
+ public JournalSegmentDescriptor build() {
+ buffer.rewind();
+ return new JournalSegmentDescriptor(buffer);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import java.io.File;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Segment file utility.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public final class JournalSegmentFile {
+ private static final char PART_SEPARATOR = '-';
+ private static final char EXTENSION_SEPARATOR = '.';
+ private static final String EXTENSION = "log";
+ private final File file;
+
+ /**
+ * Returns a boolean value indicating whether the given file appears to be a parsable segment file.
+ *
+ * @throws NullPointerException if {@code file} is null
+ */
+ public static boolean isSegmentFile(String name, File file) {
+ return isSegmentFile(name, file.getName());
+ }
+
+ /**
+ * Returns a boolean value indicating whether the given file appears to be a parsable segment file.
+ *
+ * @param journalName the name of the journal
+ * @param fileName the name of the file to check
+ * @throws NullPointerException if {@code file} is null
+ */
+ public static boolean isSegmentFile(String journalName, String fileName) {
+ checkNotNull(journalName, "journalName cannot be null");
+ checkNotNull(fileName, "fileName cannot be null");
+
+ int partSeparator = fileName.lastIndexOf(PART_SEPARATOR);
+ int extensionSeparator = fileName.lastIndexOf(EXTENSION_SEPARATOR);
+
+ if (extensionSeparator == -1
+ || partSeparator == -1
+ || extensionSeparator < partSeparator
+ || !fileName.endsWith(EXTENSION)) {
+ return false;
+ }
+
+ for (int i = partSeparator + 1; i < extensionSeparator; i++) {
+ if (!Character.isDigit(fileName.charAt(i))) {
+ return false;
+ }
+ }
+
+ return fileName.startsWith(journalName);
+ }
+
+ /**
+ * Creates a segment file for the given directory, log name, segment ID, and segment version.
+ */
+ static File createSegmentFile(String name, File directory, long id) {
+ return new File(directory, String.format("%s-%d.log", checkNotNull(name, "name cannot be null"), id));
+ }
+
+ /**
+ * @throws IllegalArgumentException if {@code file} is not a valid segment file
+ */
+ JournalSegmentFile(File file) {
+ this.file = file;
+ }
+
+ /**
+ * Returns the segment file.
+ *
+ * @return The segment file.
+ */
+ public File file() {
+ return file;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+/**
+ * Log writer.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface JournalWriter<E> extends AutoCloseable {
+
+ /**
+ * Returns the last written index.
+ *
+ * @return The last written index.
+ */
+ long getLastIndex();
+
+ /**
+ * Returns the last entry written.
+ *
+ * @return The last entry written.
+ */
+ Indexed<E> getLastEntry();
+
+ /**
+ * Returns the next index to be written.
+ *
+ * @return The next index to be written.
+ */
+ long getNextIndex();
+
+ /**
+ * Appends an entry to the journal.
+ *
+ * @param entry The entry to append.
+ * @return The appended indexed entry.
+ */
+ <T extends E> Indexed<T> append(T entry);
+
+ /**
+ * Appends an indexed entry to the log.
+ *
+ * @param entry The indexed entry to append.
+ */
+ void append(Indexed<E> entry);
+
+ /**
+ * Commits entries up to the given index.
+ *
+ * @param index The index up to which to commit entries.
+ */
+ void commit(long index);
+
+ /**
+ * Resets the head of the journal to the given index.
+ *
+ * @param index the index to which to reset the head of the journal
+ */
+ void reset(long index);
+
+ /**
+ * Truncates the log to the given index.
+ *
+ * @param index The index to which to truncate the log.
+ */
+ void truncate(long index);
+
+ /**
+ * Flushes written entries to disk.
+ */
+ void flush();
+
+ @Override
+ void close();
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.StorageException;
+import io.atomix.storage.journal.index.JournalIndex;
+import io.atomix.utils.serializer.Namespace;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * Mappable log segment reader.
+ */
+class MappableJournalSegmentReader<E> implements JournalReader<E> {
+ private final JournalSegment<E> segment;
+ private final FileChannel channel;
+ private final int maxEntrySize;
+ private final JournalIndex index;
+ private final Namespace namespace;
+ private JournalReader<E> reader;
+
+ MappableJournalSegmentReader(
+ FileChannel channel,
+ JournalSegment<E> segment,
+ int maxEntrySize,
+ JournalIndex index,
+ Namespace namespace) {
+ this.channel = channel;
+ this.segment = segment;
+ this.maxEntrySize = maxEntrySize;
+ this.index = index;
+ this.namespace = namespace;
+ this.reader = new FileChannelJournalSegmentReader<>(channel, segment, maxEntrySize, index, namespace);
+ }
+
+ /**
+ * Converts the reader to a mapped reader using the given buffer.
+ *
+ * @param buffer the mapped buffer
+ */
+ void map(ByteBuffer buffer) {
+ if (!(reader instanceof MappedJournalSegmentReader)) {
+ JournalReader<E> reader = this.reader;
+ this.reader = new MappedJournalSegmentReader<>(buffer, segment, maxEntrySize, index, namespace);
+ this.reader.reset(reader.getNextIndex());
+ reader.close();
+ }
+ }
+
+ /**
+ * Converts the reader to an unmapped reader.
+ */
+ void unmap() {
+ if (reader instanceof MappedJournalSegmentReader) {
+ JournalReader<E> reader = this.reader;
+ this.reader = new FileChannelJournalSegmentReader<>(channel, segment, maxEntrySize, index, namespace);
+ this.reader.reset(reader.getNextIndex());
+ reader.close();
+ }
+ }
+
+ @Override
+ public long getFirstIndex() {
+ return reader.getFirstIndex();
+ }
+
+ @Override
+ public long getCurrentIndex() {
+ return reader.getCurrentIndex();
+ }
+
+ @Override
+ public Indexed<E> getCurrentEntry() {
+ return reader.getCurrentEntry();
+ }
+
+ @Override
+ public long getNextIndex() {
+ return reader.getNextIndex();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return reader.hasNext();
+ }
+
+ @Override
+ public Indexed<E> next() {
+ return reader.next();
+ }
+
+ @Override
+ public void reset() {
+ reader.reset();
+ }
+
+ @Override
+ public void reset(long index) {
+ reader.reset(index);
+ }
+
+ @Override
+ public void close() {
+ reader.close();
+ try {
+ channel.close();
+ } catch (IOException e) {
+ throw new StorageException(e);
+ } finally {
+ segment.closeReader(this);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.StorageException;
+import io.atomix.storage.journal.index.JournalIndex;
+import io.atomix.utils.serializer.Namespace;
+
+import java.io.IOException;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * Mappable log segment writer.
+ */
+class MappableJournalSegmentWriter<E> implements JournalWriter<E> {
+ private final FileChannel channel;
+ private final JournalSegment<E> segment;
+ private final int maxEntrySize;
+ private final JournalIndex index;
+ private final Namespace namespace;
+ private JournalWriter<E> writer;
+
+ MappableJournalSegmentWriter(
+ FileChannel channel,
+ JournalSegment<E> segment,
+ int maxEntrySize,
+ JournalIndex index,
+ Namespace namespace) {
+ this.channel = channel;
+ this.segment = segment;
+ this.maxEntrySize = maxEntrySize;
+ this.index = index;
+ this.namespace = namespace;
+ this.writer = new FileChannelJournalSegmentWriter<>(channel, segment, maxEntrySize, index, namespace);
+ }
+
+ /**
+ * Maps the segment writer into memory, returning the mapped buffer.
+ *
+ * @return the buffer that was mapped into memory
+ */
+ MappedByteBuffer map() {
+ if (writer instanceof MappedJournalSegmentWriter) {
+ return ((MappedJournalSegmentWriter<E>) writer).buffer();
+ }
+
+ try {
+ JournalWriter<E> writer = this.writer;
+ MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, segment.descriptor().maxSegmentSize());
+ this.writer = new MappedJournalSegmentWriter<>(buffer, segment, maxEntrySize, index, namespace);
+ writer.close();
+ return buffer;
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ /**
+ * Unmaps the mapped buffer.
+ */
+ void unmap() {
+ if (writer instanceof MappedJournalSegmentWriter) {
+ JournalWriter<E> writer = this.writer;
+ this.writer = new FileChannelJournalSegmentWriter<>(channel, segment, maxEntrySize, index, namespace);
+ writer.close();
+ }
+ }
+
+ MappedByteBuffer buffer() {
+ JournalWriter<E> writer = this.writer;
+ if (writer instanceof MappedJournalSegmentWriter) {
+ return ((MappedJournalSegmentWriter<E>) writer).buffer();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the writer's first index.
+ *
+ * @return the writer's first index
+ */
+ public long firstIndex() {
+ return segment.index();
+ }
+
+ /**
+ * Returns the size of the segment.
+ *
+ * @return the size of the segment
+ */
+ public int size() {
+ try {
+ return (int) channel.size();
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
+ public long getLastIndex() {
+ return writer.getLastIndex();
+ }
+
+ @Override
+ public Indexed<E> getLastEntry() {
+ return writer.getLastEntry();
+ }
+
+ @Override
+ public long getNextIndex() {
+ return writer.getNextIndex();
+ }
+
+ @Override
+ public <T extends E> Indexed<T> append(T entry) {
+ return writer.append(entry);
+ }
+
+ @Override
+ public void append(Indexed<E> entry) {
+ writer.append(entry);
+ }
+
+ @Override
+ public void commit(long index) {
+ writer.commit(index);
+ }
+
+ @Override
+ public void reset(long index) {
+ writer.reset(index);
+ }
+
+ @Override
+ public void truncate(long index) {
+ writer.truncate(index);
+ }
+
+ @Override
+ public void flush() {
+ writer.flush();
+ }
+
+ @Override
+ public void close() {
+ writer.close();
+ try {
+ channel.close();
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.journal.index.JournalIndex;
+import io.atomix.storage.journal.index.Position;
+import io.atomix.utils.serializer.Namespace;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.NoSuchElementException;
+import java.util.zip.CRC32;
+
+/**
+ * Log segment reader.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+class MappedJournalSegmentReader<E> implements JournalReader<E> {
+ private final ByteBuffer buffer;
+ private final int maxEntrySize;
+ private final JournalIndex index;
+ private final Namespace namespace;
+ private final long firstIndex;
+ private Indexed<E> currentEntry;
+ private Indexed<E> nextEntry;
+
+ MappedJournalSegmentReader(
+ ByteBuffer buffer,
+ JournalSegment<E> segment,
+ int maxEntrySize,
+ JournalIndex index,
+ Namespace namespace) {
+ this.buffer = buffer.slice();
+ this.maxEntrySize = maxEntrySize;
+ this.index = index;
+ this.namespace = namespace;
+ this.firstIndex = segment.index();
+ reset();
+ }
+
+ @Override
+ public long getFirstIndex() {
+ return firstIndex;
+ }
+
+ @Override
+ public long getCurrentIndex() {
+ return currentEntry != null ? currentEntry.index() : 0;
+ }
+
+ @Override
+ public Indexed<E> getCurrentEntry() {
+ return currentEntry;
+ }
+
+ @Override
+ public long getNextIndex() {
+ return currentEntry != null ? currentEntry.index() + 1 : firstIndex;
+ }
+
+ @Override
+ public void reset(long index) {
+ reset();
+ Position position = this.index.lookup(index - 1);
+ if (position != null) {
+ currentEntry = new Indexed<>(position.index() - 1, null, 0);
+ buffer.position(position.position());
+ readNext();
+ }
+ while (getNextIndex() < index && hasNext()) {
+ next();
+ }
+ }
+
+ @Override
+ public void reset() {
+ buffer.position(JournalSegmentDescriptor.BYTES);
+ currentEntry = null;
+ nextEntry = null;
+ readNext();
+ }
+
+ @Override
+ public boolean hasNext() {
+ // If the next entry is null, check whether a next entry exists.
+ if (nextEntry == null) {
+ readNext();
+ }
+ return nextEntry != null;
+ }
+
+ @Override
+ public Indexed<E> next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ // Set the current entry to the next entry.
+ currentEntry = nextEntry;
+
+ // Reset the next entry to null.
+ nextEntry = null;
+
+ // Read the next entry in the segment.
+ readNext();
+
+ // Return the current entry.
+ return currentEntry;
+ }
+
+ /**
+ * Reads the next entry in the segment.
+ */
+ @SuppressWarnings("unchecked")
+ private void readNext() {
+ // Compute the index of the next entry in the segment.
+ final long index = getNextIndex();
+
+ // Mark the buffer so it can be reset if necessary.
+ buffer.mark();
+
+ try {
+ // Read the length of the entry.
+ final int length = buffer.getInt();
+
+ // If the buffer length is zero then return.
+ if (length <= 0 || length > maxEntrySize) {
+ buffer.reset();
+ nextEntry = null;
+ return;
+ }
+
+ // Read the checksum of the entry.
+ long checksum = buffer.getInt() & 0xFFFFFFFFL;
+
+ // Compute the checksum for the entry bytes.
+ final CRC32 crc32 = new CRC32();
+ ByteBuffer slice = buffer.slice();
+ slice.limit(length);
+ crc32.update(slice);
+
+ // If the stored checksum equals the computed checksum, return the entry.
+ if (checksum == crc32.getValue()) {
+ slice.rewind();
+ E entry = namespace.deserialize(slice);
+ nextEntry = new Indexed<>(index, entry, length);
+ buffer.position(buffer.position() + length);
+ } else {
+ buffer.reset();
+ nextEntry = null;
+ }
+ } catch (BufferUnderflowException e) {
+ buffer.reset();
+ nextEntry = null;
+ }
+ }
+
+ @Override
+ public void close() {
+ // Do nothing. The writer is responsible for cleaning the mapped buffer.
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import com.esotericsoftware.kryo.KryoException;
+import io.atomix.storage.StorageException;
+import io.atomix.storage.journal.index.JournalIndex;
+import io.atomix.utils.memory.BufferCleaner;
+import io.atomix.utils.serializer.Namespace;
+
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.util.zip.CRC32;
+
+/**
+ * Segment writer.
+ * <p>
+ * The format of an entry in the log is as follows:
+ * <ul>
+ * <li>64-bit index</li>
+ * <li>8-bit boolean indicating whether a term change is contained in the entry</li>
+ * <li>64-bit optional term</li>
+ * <li>32-bit signed entry length, including the entry type ID</li>
+ * <li>8-bit signed entry type ID</li>
+ * <li>n-bit entry bytes</li>
+ * </ul>
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+class MappedJournalSegmentWriter<E> implements JournalWriter<E> {
+ private final MappedByteBuffer mappedBuffer;
+ private final ByteBuffer buffer;
+ private final JournalSegment<E> segment;
+ private final int maxEntrySize;
+ private final JournalIndex index;
+ private final Namespace namespace;
+ private final long firstIndex;
+ private Indexed<E> lastEntry;
+
+ MappedJournalSegmentWriter(
+ MappedByteBuffer buffer,
+ JournalSegment<E> segment,
+ int maxEntrySize,
+ JournalIndex index,
+ Namespace namespace) {
+ this.mappedBuffer = buffer;
+ this.buffer = buffer.slice();
+ this.segment = segment;
+ this.maxEntrySize = maxEntrySize;
+ this.index = index;
+ this.namespace = namespace;
+ this.firstIndex = segment.index();
+ reset(0);
+ }
+
+ /**
+ * Returns the mapped buffer underlying the segment writer.
+ *
+ * @return the mapped buffer underlying the segment writer
+ */
+ MappedByteBuffer buffer() {
+ return mappedBuffer;
+ }
+
+ @Override
+ public void reset(long index) {
+ long nextIndex = firstIndex;
+
+ // Clear the buffer indexes.
+ buffer.position(JournalSegmentDescriptor.BYTES);
+
+ // Record the current buffer position.
+ int position = buffer.position();
+
+ // Read the entry length.
+ buffer.mark();
+
+ try {
+ int length = buffer.getInt();
+
+ // If the length is non-zero, read the entry.
+ while (0 < length && length <= maxEntrySize && (index == 0 || nextIndex <= index)) {
+
+ // Read the checksum of the entry.
+ final long checksum = buffer.getInt() & 0xFFFFFFFFL;
+
+ // Compute the checksum for the entry bytes.
+ final CRC32 crc32 = new CRC32();
+ ByteBuffer slice = buffer.slice();
+ slice.limit(length);
+ crc32.update(slice);
+
+ // If the stored checksum equals the computed checksum, return the entry.
+ if (checksum == crc32.getValue()) {
+ slice.rewind();
+ final E entry = namespace.deserialize(slice);
+ lastEntry = new Indexed<>(nextIndex, entry, length);
+ this.index.index(nextIndex, position);
+ nextIndex++;
+ } else {
+ break;
+ }
+
+ // Update the current position for indexing.
+ position = buffer.position() + length;
+ buffer.position(position);
+
+ buffer.mark();
+ length = buffer.getInt();
+ }
+
+ // Reset the buffer to the previous mark.
+ buffer.reset();
+ } catch (BufferUnderflowException e) {
+ buffer.reset();
+ }
+ }
+
+ @Override
+ public long getLastIndex() {
+ return lastEntry != null ? lastEntry.index() : segment.index() - 1;
+ }
+
+ @Override
+ public Indexed<E> getLastEntry() {
+ return lastEntry;
+ }
+
+ @Override
+ public long getNextIndex() {
+ if (lastEntry != null) {
+ return lastEntry.index() + 1;
+ } else {
+ return firstIndex;
+ }
+ }
+
+ /**
+ * Returns the size of the underlying buffer.
+ *
+ * @return The size of the underlying buffer.
+ */
+ public long size() {
+ return buffer.position() + JournalSegmentDescriptor.BYTES;
+ }
+
+ /**
+ * Returns a boolean indicating whether the segment is empty.
+ *
+ * @return Indicates whether the segment is empty.
+ */
+ public boolean isEmpty() {
+ return lastEntry == null;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void append(Indexed<E> entry) {
+ final long nextIndex = getNextIndex();
+
+ // If the entry's index is greater than the next index in the segment, skip some entries.
+ if (entry.index() > nextIndex) {
+ throw new IndexOutOfBoundsException("Entry index is not sequential");
+ }
+
+ // If the entry's index is less than the next index, truncate the segment.
+ if (entry.index() < nextIndex) {
+ truncate(entry.index() - 1);
+ }
+ append(entry.entry());
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends E> Indexed<T> append(T entry) {
+ // Store the entry index.
+ final long index = getNextIndex();
+
+ // Serialize the entry.
+ int position = buffer.position();
+ if (position + Integer.BYTES + Integer.BYTES > buffer.limit()) {
+ throw new BufferOverflowException();
+ }
+
+ buffer.position(position + Integer.BYTES + Integer.BYTES);
+
+ try {
+ namespace.serialize(entry, buffer);
+ } catch (KryoException e) {
+ throw new BufferOverflowException();
+ }
+
+ final int length = buffer.position() - (position + Integer.BYTES + Integer.BYTES);
+
+ // If the entry length exceeds the maximum entry size then throw an exception.
+ if (length > maxEntrySize) {
+ // Just reset the buffer. There's no need to zero the bytes since we haven't written the length or checksum.
+ buffer.position(position);
+ throw new StorageException.TooLarge("Entry size " + length + " exceeds maximum allowed bytes (" + maxEntrySize + ")");
+ }
+
+ // Compute the checksum for the entry.
+ final CRC32 crc32 = new CRC32();
+ buffer.position(position + Integer.BYTES + Integer.BYTES);
+ ByteBuffer slice = buffer.slice();
+ slice.limit(length);
+ crc32.update(slice);
+ final long checksum = crc32.getValue();
+
+ // Create a single byte[] in memory for the entire entry and write it as a batch to the underlying buffer.
+ buffer.position(position);
+ buffer.putInt(length);
+ buffer.putInt((int) checksum);
+ buffer.position(position + Integer.BYTES + Integer.BYTES + length);
+
+ // Update the last entry with the correct index/term/length.
+ Indexed<E> indexedEntry = new Indexed<>(index, entry, length);
+ this.lastEntry = indexedEntry;
+ this.index.index(index, position);
+ return (Indexed<T>) indexedEntry;
+ }
+
+ @Override
+ public void commit(long index) {
+
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void truncate(long index) {
+ // If the index is greater than or equal to the last index, skip the truncate.
+ if (index >= getLastIndex()) {
+ return;
+ }
+
+ // Reset the last entry.
+ lastEntry = null;
+
+ // Truncate the index.
+ this.index.truncate(index);
+
+ if (index < segment.index()) {
+ buffer.position(JournalSegmentDescriptor.BYTES);
+ buffer.putInt(0);
+ buffer.putInt(0);
+ buffer.position(JournalSegmentDescriptor.BYTES);
+ } else {
+ // Reset the writer to the given index.
+ reset(index);
+
+ // Zero entries after the given index.
+ int position = buffer.position();
+ buffer.putInt(0);
+ buffer.putInt(0);
+ buffer.position(position);
+ }
+ }
+
+ @Override
+ public void flush() {
+ mappedBuffer.force();
+ }
+
+ @Override
+ public void close() {
+ flush();
+ try {
+ BufferCleaner.freeBuffer(mappedBuffer);
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.StandardOpenOption;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+import com.google.common.collect.Sets;
+import io.atomix.storage.StorageException;
+import io.atomix.storage.StorageLevel;
+import io.atomix.utils.serializer.Namespace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Segmented journal.
+ */
+public class SegmentedJournal<E> implements Journal<E> {
+
+ /**
+ * Returns a new Raft log builder.
+ *
+ * @return A new Raft log builder.
+ */
+ public static <E> Builder<E> builder() {
+ return new Builder<>();
+ }
+
+ private static final int SEGMENT_BUFFER_FACTOR = 3;
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+ private final String name;
+ private final StorageLevel storageLevel;
+ private final File directory;
+ private final Namespace namespace;
+ private final int maxSegmentSize;
+ private final int maxEntrySize;
+ private final int maxEntriesPerSegment;
+ private final double indexDensity;
+ private final boolean flushOnCommit;
+ private final SegmentedJournalWriter<E> writer;
+ private volatile long commitIndex;
+
+ private final NavigableMap<Long, JournalSegment<E>> segments = new ConcurrentSkipListMap<>();
+ private final Collection<SegmentedJournalReader> readers = Sets.newConcurrentHashSet();
+ private JournalSegment<E> currentSegment;
+
+ private volatile boolean open = true;
+
+ public SegmentedJournal(
+ String name,
+ StorageLevel storageLevel,
+ File directory,
+ Namespace namespace,
+ int maxSegmentSize,
+ int maxEntrySize,
+ int maxEntriesPerSegment,
+ double indexDensity,
+ boolean flushOnCommit) {
+ this.name = checkNotNull(name, "name cannot be null");
+ this.storageLevel = checkNotNull(storageLevel, "storageLevel cannot be null");
+ this.directory = checkNotNull(directory, "directory cannot be null");
+ this.namespace = checkNotNull(namespace, "namespace cannot be null");
+ this.maxSegmentSize = maxSegmentSize;
+ this.maxEntrySize = maxEntrySize;
+ this.maxEntriesPerSegment = maxEntriesPerSegment;
+ this.indexDensity = indexDensity;
+ this.flushOnCommit = flushOnCommit;
+ open();
+ this.writer = openWriter();
+ }
+
+ /**
+ * Returns the segment file name prefix.
+ *
+ * @return The segment file name prefix.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns the storage directory.
+ * <p>
+ * The storage directory is the directory to which all segments write files. Segment files for multiple logs may be
+ * stored in the storage directory, and files for each log instance will be identified by the {@code prefix} provided
+ * when the log is opened.
+ *
+ * @return The storage directory.
+ */
+ public File directory() {
+ return directory;
+ }
+
+ /**
+ * Returns the storage level.
+ * <p>
+ * The storage level dictates how entries within individual journal segments should be stored.
+ *
+ * @return The storage level.
+ */
+ public StorageLevel storageLevel() {
+ return storageLevel;
+ }
+
+ /**
+ * Returns the maximum journal segment size.
+ * <p>
+ * The maximum segment size dictates the maximum size any segment in a segment may consume in bytes.
+ *
+ * @return The maximum segment size in bytes.
+ */
+ public int maxSegmentSize() {
+ return maxSegmentSize;
+ }
+
+ /**
+ * Returns the maximum journal entry size.
+ * <p>
+ * The maximum entry size dictates the maximum size any entry in the segment may consume in bytes.
+ *
+ * @return the maximum entry size in bytes
+ */
+ public int maxEntrySize() {
+ return maxEntrySize;
+ }
+
+ /**
+ * Returns the maximum number of entries per segment.
+ * <p>
+ * The maximum entries per segment dictates the maximum number of entries that are allowed to be stored in any segment
+ * in a journal.
+ *
+ * @return The maximum number of entries per segment.
+ * @deprecated since 3.0.2
+ */
+ @Deprecated
+ public int maxEntriesPerSegment() {
+ return maxEntriesPerSegment;
+ }
+
+ /**
+ * Returns the collection of journal segments.
+ *
+ * @return the collection of journal segments
+ */
+ public Collection<JournalSegment<E>> segments() {
+ return segments.values();
+ }
+
+ /**
+ * Returns the collection of journal segments with indexes greater than the given index.
+ *
+ * @param index the starting index
+ * @return the journal segments starting with indexes greater than or equal to the given index
+ */
+ public Collection<JournalSegment<E>> segments(long index) {
+ return segments.tailMap(index).values();
+ }
+
+ /**
+ * Returns the total size of the journal.
+ *
+ * @return the total size of the journal
+ */
+ public long size() {
+ return segments.values().stream()
+ .mapToLong(segment -> segment.size())
+ .sum();
+ }
+
+ @Override
+ public SegmentedJournalWriter<E> writer() {
+ return writer;
+ }
+
+ @Override
+ public SegmentedJournalReader<E> openReader(long index) {
+ return openReader(index, SegmentedJournalReader.Mode.ALL);
+ }
+
+ /**
+ * Opens a new Raft log reader with the given reader mode.
+ *
+ * @param index The index from which to begin reading entries.
+ * @param mode The mode in which to read entries.
+ * @return The Raft log reader.
+ */
+ public SegmentedJournalReader<E> openReader(long index, SegmentedJournalReader.Mode mode) {
+ SegmentedJournalReader<E> reader = new SegmentedJournalReader<>(this, index, mode);
+ readers.add(reader);
+ return reader;
+ }
+
+ /**
+ * Opens a new journal writer.
+ *
+ * @return A new journal writer.
+ */
+ protected SegmentedJournalWriter<E> openWriter() {
+ return new SegmentedJournalWriter<>(this);
+ }
+
+ /**
+ * Opens the segments.
+ */
+ private void open() {
+ // Load existing log segments from disk.
+ for (JournalSegment<E> segment : loadSegments()) {
+ segments.put(segment.descriptor().index(), segment);
+ }
+
+ // If a segment doesn't already exist, create an initial segment starting at index 1.
+ if (!segments.isEmpty()) {
+ currentSegment = segments.lastEntry().getValue();
+ } else {
+ JournalSegmentDescriptor descriptor = JournalSegmentDescriptor.builder()
+ .withId(1)
+ .withIndex(1)
+ .withMaxSegmentSize(maxSegmentSize)
+ .withMaxEntries(maxEntriesPerSegment)
+ .build();
+
+ currentSegment = createSegment(descriptor);
+ currentSegment.descriptor().update(System.currentTimeMillis());
+
+ segments.put(1L, currentSegment);
+ }
+ }
+
+ /**
+ * Asserts that the manager is open.
+ *
+ * @throws IllegalStateException if the segment manager is not open
+ */
+ private void assertOpen() {
+ checkState(currentSegment != null, "journal not open");
+ }
+
+ /**
+ * Asserts that enough disk space is available to allocate a new segment.
+ */
+ private void assertDiskSpace() {
+ if (directory().getUsableSpace() < maxSegmentSize() * SEGMENT_BUFFER_FACTOR) {
+ throw new StorageException.OutOfDiskSpace("Not enough space to allocate a new journal segment");
+ }
+ }
+
+ /**
+ * Resets the current segment, creating a new segment if necessary.
+ */
+ private synchronized void resetCurrentSegment() {
+ JournalSegment<E> lastSegment = getLastSegment();
+ if (lastSegment != null) {
+ currentSegment = lastSegment;
+ } else {
+ JournalSegmentDescriptor descriptor = JournalSegmentDescriptor.builder()
+ .withId(1)
+ .withIndex(1)
+ .withMaxSegmentSize(maxSegmentSize)
+ .withMaxEntries(maxEntriesPerSegment)
+ .build();
+
+ currentSegment = createSegment(descriptor);
+
+ segments.put(1L, currentSegment);
+ }
+ }
+
+ /**
+ * Resets and returns the first segment in the journal.
+ *
+ * @param index the starting index of the journal
+ * @return the first segment
+ */
+ JournalSegment<E> resetSegments(long index) {
+ assertOpen();
+
+ // If the index already equals the first segment index, skip the reset.
+ JournalSegment<E> firstSegment = getFirstSegment();
+ if (index == firstSegment.index()) {
+ return firstSegment;
+ }
+
+ for (JournalSegment<E> segment : segments.values()) {
+ segment.close();
+ segment.delete();
+ }
+ segments.clear();
+
+ JournalSegmentDescriptor descriptor = JournalSegmentDescriptor.builder()
+ .withId(1)
+ .withIndex(index)
+ .withMaxSegmentSize(maxSegmentSize)
+ .withMaxEntries(maxEntriesPerSegment)
+ .build();
+ currentSegment = createSegment(descriptor);
+ segments.put(index, currentSegment);
+ return currentSegment;
+ }
+
+ /**
+ * Returns the first segment in the log.
+ *
+ * @throws IllegalStateException if the segment manager is not open
+ */
+ JournalSegment<E> getFirstSegment() {
+ assertOpen();
+ Map.Entry<Long, JournalSegment<E>> segment = segments.firstEntry();
+ return segment != null ? segment.getValue() : null;
+ }
+
+ /**
+ * Returns the last segment in the log.
+ *
+ * @throws IllegalStateException if the segment manager is not open
+ */
+ JournalSegment<E> getLastSegment() {
+ assertOpen();
+ Map.Entry<Long, JournalSegment<E>> segment = segments.lastEntry();
+ return segment != null ? segment.getValue() : null;
+ }
+
+ /**
+ * Creates and returns the next segment.
+ *
+ * @return The next segment.
+ * @throws IllegalStateException if the segment manager is not open
+ */
+ synchronized JournalSegment<E> getNextSegment() {
+ assertOpen();
+ assertDiskSpace();
+
+ JournalSegment lastSegment = getLastSegment();
+ JournalSegmentDescriptor descriptor = JournalSegmentDescriptor.builder()
+ .withId(lastSegment != null ? lastSegment.descriptor().id() + 1 : 1)
+ .withIndex(currentSegment.lastIndex() + 1)
+ .withMaxSegmentSize(maxSegmentSize)
+ .withMaxEntries(maxEntriesPerSegment)
+ .build();
+
+ currentSegment = createSegment(descriptor);
+
+ segments.put(descriptor.index(), currentSegment);
+ return currentSegment;
+ }
+
+ /**
+ * Returns the segment following the segment with the given ID.
+ *
+ * @param index The segment index with which to look up the next segment.
+ * @return The next segment for the given index.
+ */
+ JournalSegment<E> getNextSegment(long index) {
+ Map.Entry<Long, JournalSegment<E>> nextSegment = segments.higherEntry(index);
+ return nextSegment != null ? nextSegment.getValue() : null;
+ }
+
+ /**
+ * Returns the segment for the given index.
+ *
+ * @param index The index for which to return the segment.
+ * @throws IllegalStateException if the segment manager is not open
+ */
+ synchronized JournalSegment<E> getSegment(long index) {
+ assertOpen();
+ // Check if the current segment contains the given index first in order to prevent an unnecessary map lookup.
+ if (currentSegment != null && index > currentSegment.index()) {
+ return currentSegment;
+ }
+
+ // If the index is in another segment, get the entry with the next lowest first index.
+ Map.Entry<Long, JournalSegment<E>> segment = segments.floorEntry(index);
+ if (segment != null) {
+ return segment.getValue();
+ }
+ return getFirstSegment();
+ }
+
+ /**
+ * Removes a segment.
+ *
+ * @param segment The segment to remove.
+ */
+ synchronized void removeSegment(JournalSegment segment) {
+ segments.remove(segment.index());
+ segment.close();
+ segment.delete();
+ resetCurrentSegment();
+ }
+
+ /**
+ * Creates a new segment.
+ */
+ JournalSegment<E> createSegment(JournalSegmentDescriptor descriptor) {
+ File segmentFile = JournalSegmentFile.createSegmentFile(name, directory, descriptor.id());
+
+ RandomAccessFile raf;
+ FileChannel channel;
+ try {
+ raf = new RandomAccessFile(segmentFile, "rw");
+ raf.setLength(descriptor.maxSegmentSize());
+ channel = raf.getChannel();
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(JournalSegmentDescriptor.BYTES);
+ descriptor.copyTo(buffer);
+ buffer.flip();
+ try {
+ channel.write(buffer);
+ } catch (IOException e) {
+ throw new StorageException(e);
+ } finally {
+ try {
+ channel.close();
+ raf.close();
+ } catch (IOException e) {
+ }
+ }
+ JournalSegment<E> segment = newSegment(new JournalSegmentFile(segmentFile), descriptor);
+ log.debug("Created segment: {}", segment);
+ return segment;
+ }
+
+ /**
+ * Creates a new segment instance.
+ *
+ * @param segmentFile The segment file.
+ * @param descriptor The segment descriptor.
+ * @return The segment instance.
+ */
+ protected JournalSegment<E> newSegment(JournalSegmentFile segmentFile, JournalSegmentDescriptor descriptor) {
+ return new JournalSegment<>(segmentFile, descriptor, storageLevel, maxEntrySize, indexDensity, namespace);
+ }
+
+ /**
+ * Loads a segment.
+ */
+ private JournalSegment<E> loadSegment(long segmentId) {
+ File segmentFile = JournalSegmentFile.createSegmentFile(name, directory, segmentId);
+ ByteBuffer buffer = ByteBuffer.allocate(JournalSegmentDescriptor.BYTES);
+ try (FileChannel channel = openChannel(segmentFile)) {
+ channel.read(buffer);
+ buffer.flip();
+ JournalSegmentDescriptor descriptor = new JournalSegmentDescriptor(buffer);
+ JournalSegment<E> segment = newSegment(new JournalSegmentFile(segmentFile), descriptor);
+ log.debug("Loaded disk segment: {} ({})", descriptor.id(), segmentFile.getName());
+ return segment;
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ private FileChannel openChannel(File file) {
+ try {
+ return FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ /**
+ * Loads all segments from disk.
+ *
+ * @return A collection of segments for the log.
+ */
+ protected Collection<JournalSegment<E>> loadSegments() {
+ // Ensure log directories are created.
+ directory.mkdirs();
+
+ TreeMap<Long, JournalSegment<E>> segments = new TreeMap<>();
+
+ // Iterate through all files in the log directory.
+ for (File file : directory.listFiles(File::isFile)) {
+
+ // If the file looks like a segment file, attempt to load the segment.
+ if (JournalSegmentFile.isSegmentFile(name, file)) {
+ JournalSegmentFile segmentFile = new JournalSegmentFile(file);
+ ByteBuffer buffer = ByteBuffer.allocate(JournalSegmentDescriptor.BYTES);
+ try (FileChannel channel = openChannel(file)) {
+ channel.read(buffer);
+ buffer.flip();
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+
+ JournalSegmentDescriptor descriptor = new JournalSegmentDescriptor(buffer);
+
+ // Load the segment.
+ JournalSegment<E> segment = loadSegment(descriptor.id());
+
+ // Add the segment to the segments list.
+ log.debug("Found segment: {} ({})", segment.descriptor().id(), segmentFile.file().getName());
+ segments.put(segment.index(), segment);
+ }
+ }
+
+ // Verify that all the segments in the log align with one another.
+ JournalSegment<E> previousSegment = null;
+ boolean corrupted = false;
+ Iterator<Map.Entry<Long, JournalSegment<E>>> iterator = segments.entrySet().iterator();
+ while (iterator.hasNext()) {
+ JournalSegment<E> segment = iterator.next().getValue();
+ if (previousSegment != null && previousSegment.lastIndex() != segment.index() - 1) {
+ log.warn("Journal is inconsistent. {} is not aligned with prior segment {}", segment.file().file(), previousSegment.file().file());
+ corrupted = true;
+ }
+ if (corrupted) {
+ segment.close();
+ segment.delete();
+ iterator.remove();
+ }
+ previousSegment = segment;
+ }
+
+ return segments.values();
+ }
+
+ /**
+ * Resets journal readers to the given head.
+ *
+ * @param index The index at which to reset readers.
+ */
+ void resetHead(long index) {
+ for (SegmentedJournalReader reader : readers) {
+ if (reader.getNextIndex() < index) {
+ reader.reset(index);
+ }
+ }
+ }
+
+ /**
+ * Resets journal readers to the given tail.
+ *
+ * @param index The index at which to reset readers.
+ */
+ void resetTail(long index) {
+ for (SegmentedJournalReader reader : readers) {
+ if (reader.getNextIndex() >= index) {
+ reader.reset(index);
+ }
+ }
+ }
+
+ void closeReader(SegmentedJournalReader reader) {
+ readers.remove(reader);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+
+ /**
+ * Returns a boolean indicating whether a segment can be removed from the journal prior to the given index.
+ *
+ * @param index the index from which to remove segments
+ * @return indicates whether a segment can be removed from the journal
+ */
+ public boolean isCompactable(long index) {
+ Map.Entry<Long, JournalSegment<E>> segmentEntry = segments.floorEntry(index);
+ return segmentEntry != null && segments.headMap(segmentEntry.getValue().index()).size() > 0;
+ }
+
+ /**
+ * Returns the index of the last segment in the log.
+ *
+ * @param index the compaction index
+ * @return the starting index of the last segment in the log
+ */
+ public long getCompactableIndex(long index) {
+ Map.Entry<Long, JournalSegment<E>> segmentEntry = segments.floorEntry(index);
+ return segmentEntry != null ? segmentEntry.getValue().index() : 0;
+ }
+
+ /**
+ * Compacts the journal up to the given index.
+ * <p>
+ * The semantics of compaction are not specified by this interface.
+ *
+ * @param index The index up to which to compact the journal.
+ */
+ public void compact(long index) {
+ Map.Entry<Long, JournalSegment<E>> segmentEntry = segments.floorEntry(index);
+ if (segmentEntry != null) {
+ SortedMap<Long, JournalSegment<E>> compactSegments = segments.headMap(segmentEntry.getValue().index());
+ if (!compactSegments.isEmpty()) {
+ log.debug("{} - Compacting {} segment(s)", name, compactSegments.size());
+ for (JournalSegment segment : compactSegments.values()) {
+ log.trace("Deleting segment: {}", segment);
+ segment.close();
+ segment.delete();
+ }
+ compactSegments.clear();
+ resetHead(segmentEntry.getValue().index());
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ segments.values().forEach(segment -> {
+ log.debug("Closing segment: {}", segment);
+ segment.close();
+ });
+ currentSegment = null;
+ open = false;
+ }
+
+ /**
+ * Returns whether {@code flushOnCommit} is enabled for the log.
+ *
+ * @return Indicates whether {@code flushOnCommit} is enabled for the log.
+ */
+ boolean isFlushOnCommit() {
+ return flushOnCommit;
+ }
+
+ /**
+ * Commits entries up to the given index.
+ *
+ * @param index The index up to which to commit entries.
+ */
+ void setCommitIndex(long index) {
+ this.commitIndex = index;
+ }
+
+ /**
+ * Returns the Raft log commit index.
+ *
+ * @return The Raft log commit index.
+ */
+ long getCommitIndex() {
+ return commitIndex;
+ }
+
+ /**
+ * Raft log builder.
+ */
+ public static class Builder<E> implements io.atomix.utils.Builder<SegmentedJournal<E>> {
+ private static final boolean DEFAULT_FLUSH_ON_COMMIT = false;
+ private static final String DEFAULT_NAME = "atomix";
+ private static final String DEFAULT_DIRECTORY = System.getProperty("user.dir");
+ private static final int DEFAULT_MAX_SEGMENT_SIZE = 1024 * 1024 * 32;
+ private static final int DEFAULT_MAX_ENTRY_SIZE = 1024 * 1024;
+ private static final int DEFAULT_MAX_ENTRIES_PER_SEGMENT = 1024 * 1024;
+ private static final double DEFAULT_INDEX_DENSITY = .005;
+ private static final int DEFAULT_CACHE_SIZE = 1024;
+
+ protected String name = DEFAULT_NAME;
+ protected StorageLevel storageLevel = StorageLevel.DISK;
+ protected File directory = new File(DEFAULT_DIRECTORY);
+ protected Namespace namespace;
+ protected int maxSegmentSize = DEFAULT_MAX_SEGMENT_SIZE;
+ protected int maxEntrySize = DEFAULT_MAX_ENTRY_SIZE;
+ protected int maxEntriesPerSegment = DEFAULT_MAX_ENTRIES_PER_SEGMENT;
+ protected double indexDensity = DEFAULT_INDEX_DENSITY;
+ protected int cacheSize = DEFAULT_CACHE_SIZE;
+ private boolean flushOnCommit = DEFAULT_FLUSH_ON_COMMIT;
+
+ protected Builder() {
+ }
+
+ /**
+ * Sets the storage name.
+ *
+ * @param name The storage name.
+ * @return The storage builder.
+ */
+ public Builder<E> withName(String name) {
+ this.name = checkNotNull(name, "name cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets the log storage level, returning the builder for method chaining.
+ * <p>
+ * The storage level indicates how individual entries should be persisted in the journal.
+ *
+ * @param storageLevel The log storage level.
+ * @return The storage builder.
+ */
+ public Builder<E> withStorageLevel(StorageLevel storageLevel) {
+ this.storageLevel = checkNotNull(storageLevel, "storageLevel cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets the log directory, returning the builder for method chaining.
+ * <p>
+ * The log will write segment files into the provided directory.
+ *
+ * @param directory The log directory.
+ * @return The storage builder.
+ * @throws NullPointerException If the {@code directory} is {@code null}
+ */
+ public Builder<E> withDirectory(String directory) {
+ return withDirectory(new File(checkNotNull(directory, "directory cannot be null")));
+ }
+
+ /**
+ * Sets the log directory, returning the builder for method chaining.
+ * <p>
+ * The log will write segment files into the provided directory.
+ *
+ * @param directory The log directory.
+ * @return The storage builder.
+ * @throws NullPointerException If the {@code directory} is {@code null}
+ */
+ public Builder<E> withDirectory(File directory) {
+ this.directory = checkNotNull(directory, "directory cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets the journal namespace, returning the builder for method chaining.
+ *
+ * @param namespace The journal serializer.
+ * @return The journal builder.
+ */
+ public Builder<E> withNamespace(Namespace namespace) {
+ this.namespace = checkNotNull(namespace, "namespace cannot be null");
+ return this;
+ }
+
+ /**
+ * Sets the maximum segment size in bytes, returning the builder for method chaining.
+ * <p>
+ * The maximum segment size dictates when logs should roll over to new segments. As entries are written to a segment
+ * of the log, once the size of the segment surpasses the configured maximum segment size, the log will create a new
+ * segment and append new entries to that segment.
+ * <p>
+ * By default, the maximum segment size is {@code 1024 * 1024 * 32}.
+ *
+ * @param maxSegmentSize The maximum segment size in bytes.
+ * @return The storage builder.
+ * @throws IllegalArgumentException If the {@code maxSegmentSize} is not positive
+ */
+ public Builder<E> withMaxSegmentSize(int maxSegmentSize) {
+ checkArgument(maxSegmentSize > JournalSegmentDescriptor.BYTES, "maxSegmentSize must be greater than " + JournalSegmentDescriptor.BYTES);
+ this.maxSegmentSize = maxSegmentSize;
+ return this;
+ }
+
+ /**
+ * Sets the maximum entry size in bytes, returning the builder for method chaining.
+ *
+ * @param maxEntrySize the maximum entry size in bytes
+ * @return the storage builder
+ * @throws IllegalArgumentException if the {@code maxEntrySize} is not positive
+ */
+ public Builder<E> withMaxEntrySize(int maxEntrySize) {
+ checkArgument(maxEntrySize > 0, "maxEntrySize must be positive");
+ this.maxEntrySize = maxEntrySize;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of allows entries per segment, returning the builder for method chaining.
+ * <p>
+ * The maximum entry count dictates when logs should roll over to new segments. As entries are written to a segment
+ * of the log, if the entry count in that segment meets the configured maximum entry count, the log will create a
+ * new segment and append new entries to that segment.
+ * <p>
+ * By default, the maximum entries per segment is {@code 1024 * 1024}.
+ *
+ * @param maxEntriesPerSegment The maximum number of entries allowed per segment.
+ * @return The storage builder.
+ * @throws IllegalArgumentException If the {@code maxEntriesPerSegment} not greater than the default max entries
+ * per segment
+ * @deprecated since 3.0.2
+ */
+ @Deprecated
+ public Builder<E> withMaxEntriesPerSegment(int maxEntriesPerSegment) {
+ checkArgument(maxEntriesPerSegment > 0, "max entries per segment must be positive");
+ checkArgument(maxEntriesPerSegment <= DEFAULT_MAX_ENTRIES_PER_SEGMENT,
+ "max entries per segment cannot be greater than " + DEFAULT_MAX_ENTRIES_PER_SEGMENT);
+ this.maxEntriesPerSegment = maxEntriesPerSegment;
+ return this;
+ }
+
+ /**
+ * Sets the journal index density.
+ * <p>
+ * The index density is the frequency at which the position of entries written to the journal will be recorded in an
+ * in-memory index for faster seeking.
+ *
+ * @param indexDensity the index density
+ * @return the journal builder
+ * @throws IllegalArgumentException if the density is not between 0 and 1
+ */
+ public Builder<E> withIndexDensity(double indexDensity) {
+ checkArgument(indexDensity > 0 && indexDensity < 1, "index density must be between 0 and 1");
+ this.indexDensity = indexDensity;
+ return this;
+ }
+
+ /**
+ * Sets the journal cache size.
+ *
+ * @param cacheSize the journal cache size
+ * @return the journal builder
+ * @throws IllegalArgumentException if the cache size is not positive
+ * @deprecated since 3.0.4
+ */
+ @Deprecated
+ public Builder<E> withCacheSize(int cacheSize) {
+ checkArgument(cacheSize >= 0, "cacheSize must be positive");
+ this.cacheSize = cacheSize;
+ return this;
+ }
+
+ /**
+ * Enables flushing buffers to disk when entries are committed to a segment, returning the builder for method
+ * chaining.
+ * <p>
+ * When flush-on-commit is enabled, log entry buffers will be automatically flushed to disk each time an entry is
+ * committed in a given segment.
+ *
+ * @return The storage builder.
+ */
+ public Builder<E> withFlushOnCommit() {
+ return withFlushOnCommit(true);
+ }
+
+ /**
+ * Sets whether to flush buffers to disk when entries are committed to a segment, returning the builder for method
+ * chaining.
+ * <p>
+ * When flush-on-commit is enabled, log entry buffers will be automatically flushed to disk each time an entry is
+ * committed in a given segment.
+ *
+ * @param flushOnCommit Whether to flush buffers to disk when entries are committed to a segment.
+ * @return The storage builder.
+ */
+ public Builder<E> withFlushOnCommit(boolean flushOnCommit) {
+ this.flushOnCommit = flushOnCommit;
+ return this;
+ }
+
+ @Override
+ public SegmentedJournal<E> build() {
+ return new SegmentedJournal<>(
+ name,
+ storageLevel,
+ directory,
+ namespace,
+ maxSegmentSize,
+ maxEntrySize,
+ maxEntriesPerSegment,
+ indexDensity,
+ flushOnCommit);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import java.util.NoSuchElementException;
+
+/**
+ * Raft log reader.
+ */
+public class SegmentedJournalReader<E> implements JournalReader<E> {
+
+ private final SegmentedJournal<E> journal;
+ private JournalSegment<E> currentSegment;
+ private Indexed<E> previousEntry;
+ private MappableJournalSegmentReader<E> currentReader;
+ private final Mode mode;
+
+ public SegmentedJournalReader(SegmentedJournal<E> journal, long index, Mode mode) {
+ this.journal = journal;
+ this.mode = mode;
+ initialize(index);
+ }
+
+ /**
+ * Initializes the reader to the given index.
+ */
+ private void initialize(long index) {
+ currentSegment = journal.getSegment(index);
+ currentSegment.acquire();
+ currentReader = currentSegment.createReader();
+ long nextIndex = getNextIndex();
+ while (index > nextIndex && hasNext()) {
+ next();
+ nextIndex = getNextIndex();
+ }
+ }
+
+ @Override
+ public long getFirstIndex() {
+ return journal.getFirstSegment().index();
+ }
+
+ @Override
+ public long getCurrentIndex() {
+ long currentIndex = currentReader.getCurrentIndex();
+ if (currentIndex != 0) {
+ return currentIndex;
+ }
+ if (previousEntry != null) {
+ return previousEntry.index();
+ }
+ return 0;
+ }
+
+ @Override
+ public Indexed<E> getCurrentEntry() {
+ Indexed<E> currentEntry = currentReader.getCurrentEntry();
+ if (currentEntry != null) {
+ return currentEntry;
+ }
+ return previousEntry;
+ }
+
+ @Override
+ public long getNextIndex() {
+ return currentReader.getNextIndex();
+ }
+
+ @Override
+ public void reset() {
+ currentReader.close();
+ currentSegment.release();
+ currentSegment = journal.getFirstSegment();
+ currentSegment.acquire();
+ currentReader = currentSegment.createReader();
+ previousEntry = null;
+ }
+
+ @Override
+ public void reset(long index) {
+ // If the current segment is not open, it has been replaced. Reset the segments.
+ if (!currentSegment.isOpen()) {
+ reset();
+ }
+
+ if (index < currentReader.getNextIndex()) {
+ rewind(index);
+ } else if (index > currentReader.getNextIndex()) {
+ forward(index);
+ } else {
+ currentReader.reset(index);
+ }
+ }
+
+ /**
+ * Rewinds the journal to the given index.
+ */
+ private void rewind(long index) {
+ if (currentSegment.index() >= index) {
+ JournalSegment<E> segment = journal.getSegment(index - 1);
+ if (segment != null) {
+ currentReader.close();
+ currentSegment.release();
+ currentSegment = segment;
+ currentSegment.acquire();
+ currentReader = currentSegment.createReader();
+ }
+ }
+
+ currentReader.reset(index);
+ previousEntry = currentReader.getCurrentEntry();
+ }
+
+ /**
+ * Fast forwards the journal to the given index.
+ */
+ private void forward(long index) {
+ while (getNextIndex() < index && hasNext()) {
+ next();
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (mode == Mode.ALL) {
+ return hasNextEntry();
+ }
+
+ long nextIndex = getNextIndex();
+ long commitIndex = journal.getCommitIndex();
+ return nextIndex <= commitIndex && hasNextEntry();
+ }
+
+ private boolean hasNextEntry() {
+ if (!currentReader.hasNext()) {
+ JournalSegment<E> nextSegment = journal.getNextSegment(currentSegment.index());
+ if (nextSegment != null && nextSegment.index() == getNextIndex()) {
+ previousEntry = currentReader.getCurrentEntry();
+ currentSegment.release();
+ currentSegment = nextSegment;
+ currentSegment.acquire();
+ currentReader = currentSegment.createReader();
+ return currentReader.hasNext();
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public Indexed<E> next() {
+ if (!currentReader.hasNext()) {
+ JournalSegment<E> nextSegment = journal.getNextSegment(currentSegment.index());
+ if (nextSegment != null && nextSegment.index() == getNextIndex()) {
+ previousEntry = currentReader.getCurrentEntry();
+ currentSegment.release();
+ currentSegment = nextSegment;
+ currentSegment.acquire();
+ currentReader = currentSegment.createReader();
+ return currentReader.next();
+ } else {
+ throw new NoSuchElementException();
+ }
+ } else {
+ previousEntry = currentReader.getCurrentEntry();
+ return currentReader.next();
+ }
+ }
+
+ @Override
+ public void close() {
+ currentReader.close();
+ journal.closeReader(this);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import java.nio.BufferOverflowException;
+
+/**
+ * Raft log writer.
+ */
+public class SegmentedJournalWriter<E> implements JournalWriter<E> {
+ private final SegmentedJournal<E> journal;
+ private JournalSegment<E> currentSegment;
+ private MappableJournalSegmentWriter<E> currentWriter;
+
+ public SegmentedJournalWriter(SegmentedJournal<E> journal) {
+ this.journal = journal;
+ this.currentSegment = journal.getLastSegment();
+ currentSegment.acquire();
+ this.currentWriter = currentSegment.writer();
+ }
+
+ @Override
+ public long getLastIndex() {
+ return currentWriter.getLastIndex();
+ }
+
+ @Override
+ public Indexed<E> getLastEntry() {
+ return currentWriter.getLastEntry();
+ }
+
+ @Override
+ public long getNextIndex() {
+ return currentWriter.getNextIndex();
+ }
+
+ @Override
+ public void reset(long index) {
+ if (index > currentSegment.index()) {
+ currentSegment.release();
+ currentSegment = journal.resetSegments(index);
+ currentSegment.acquire();
+ currentWriter = currentSegment.writer();
+ } else {
+ truncate(index - 1);
+ }
+ journal.resetHead(index);
+ }
+
+ @Override
+ public void commit(long index) {
+ if (index > journal.getCommitIndex()) {
+ journal.setCommitIndex(index);
+ if (journal.isFlushOnCommit()) {
+ flush();
+ }
+ }
+ }
+
+ @Override
+ public <T extends E> Indexed<T> append(T entry) {
+ try {
+ return currentWriter.append(entry);
+ } catch (BufferOverflowException e) {
+ if (currentSegment.index() == currentWriter.getNextIndex()) {
+ throw e;
+ }
+ currentWriter.flush();
+ currentSegment.release();
+ currentSegment = journal.getNextSegment();
+ currentSegment.acquire();
+ currentWriter = currentSegment.writer();
+ return currentWriter.append(entry);
+ }
+ }
+
+ @Override
+ public void append(Indexed<E> entry) {
+ try {
+ currentWriter.append(entry);
+ } catch (BufferOverflowException e) {
+ if (currentSegment.index() == currentWriter.getNextIndex()) {
+ throw e;
+ }
+ currentWriter.flush();
+ currentSegment.release();
+ currentSegment = journal.getNextSegment();
+ currentSegment.acquire();
+ currentWriter = currentSegment.writer();
+ currentWriter.append(entry);
+ }
+ }
+
+ @Override
+ public void truncate(long index) {
+ if (index < journal.getCommitIndex()) {
+ throw new IndexOutOfBoundsException("Cannot truncate committed index: " + index);
+ }
+
+ // Delete all segments with first indexes greater than the given index.
+ while (index < currentSegment.index() && currentSegment != journal.getFirstSegment()) {
+ currentSegment.release();
+ journal.removeSegment(currentSegment);
+ currentSegment = journal.getLastSegment();
+ currentSegment.acquire();
+ currentWriter = currentSegment.writer();
+ }
+
+ // Truncate the current index.
+ currentWriter.truncate(index);
+
+ // Reset segment readers.
+ journal.resetTail(index + 1);
+ }
+
+ @Override
+ public void flush() {
+ currentWriter.flush();
+ }
+
+ @Override
+ public void close() {
+ currentWriter.close();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal.index;
+
+/**
+ * Journal index.
+ */
+public interface JournalIndex {
+
+ /**
+ * Adds an entry for the given index at the given position.
+ *
+ * @param index the index for which to add the entry
+ * @param position the position of the given index
+ */
+ void index(long index, int position);
+
+ /**
+ * Looks up the position of the given index.
+ *
+ * @param index the index to lookup
+ * @return the position of the given index or a lesser index
+ */
+ Position lookup(long index);
+
+ /**
+ * Truncates the index to the given index.
+ *
+ * @param index the index to which to truncate the index
+ */
+ void truncate(long index);
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal.index;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Journal index position.
+ */
+public class Position {
+ private final long index;
+ private final int position;
+
+ public Position(long index, int position) {
+ this.index = index;
+ this.position = position;
+ }
+
+ public long index() {
+ return index;
+ }
+
+ public int position() {
+ return position;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("index", index)
+ .add("position", position)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal.index;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Sparse index.
+ */
+public class SparseJournalIndex implements JournalIndex {
+ private static final int MIN_DENSITY = 1000;
+ private final int density;
+ private final TreeMap<Long, Integer> positions = new TreeMap<>();
+
+ public SparseJournalIndex(double density) {
+ this.density = (int) Math.ceil(MIN_DENSITY / (density * MIN_DENSITY));
+ }
+
+ @Override
+ public void index(long index, int position) {
+ if (index % density == 0) {
+ positions.put(index, position);
+ }
+ }
+
+ @Override
+ public Position lookup(long index) {
+ Map.Entry<Long, Integer> entry = positions.floorEntry(index);
+ return entry != null ? new Position(entry.getKey(), entry.getValue()) : null;
+ }
+
+ @Override
+ public void truncate(long index) {
+ positions.tailMap(index, false).clear();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for efficiently managing journal indexes.
+ */
+package io.atomix.storage.journal.index;
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides a low-level journal abstraction for appending to logs and managing segmented logs.
+ */
+package io.atomix.storage.journal;
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for managing storage objects.
+ */
+package io.atomix.storage;
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.statistics;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+/**
+ * Atomix storage statistics.
+ */
+public class StorageStatistics {
+ private static final Logger LOGGER = LoggerFactory.getLogger(StorageStatistics.class);
+
+ private final File file;
+ private final MBeanServer mBeanServer;
+
+ public StorageStatistics(File file) {
+ this.file = file;
+ this.mBeanServer = ManagementFactory.getPlatformMBeanServer();
+ }
+
+ /**
+ * Returns the amount of usable space remaining.
+ *
+ * @return the amount of usable space remaining
+ */
+ public long getUsableSpace() {
+ return file.getUsableSpace();
+ }
+
+ /**
+ * Returns the amount of free space remaining.
+ *
+ * @return the amount of free space remaining
+ */
+ public long getFreeSpace() {
+ return file.getFreeSpace();
+ }
+
+ /**
+ * Returns the total amount of space.
+ *
+ * @return the total amount of space
+ */
+ public long getTotalSpace() {
+ return file.getTotalSpace();
+ }
+
+ /**
+ * Returns the amount of free memory remaining.
+ *
+ * @return the amount of free memory remaining if successful, -1 return indicates failure.
+ */
+ public long getFreeMemory() {
+ try {
+ return (long) mBeanServer.getAttribute(new ObjectName("java.lang", "type", "OperatingSystem"), "FreePhysicalMemorySize");
+ } catch (Exception e) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("An exception occurred during memory check", e);
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the total amount of memory.
+ *
+ * @return the total amount of memory if successful, -1 return indicates failure.
+ */
+ public long getTotalMemory() {
+ try {
+ return (long) mBeanServer.getAttribute(new ObjectName("java.lang", "type", "OperatingSystem"), "TotalPhysicalMemorySize");
+ } catch (Exception e) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("An exception occurred during memory check", e);
+ }
+ }
+ return -1;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides utilities for querying system storage information.
+ */
+package io.atomix.storage.statistics;
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import org.junit.Test;
+
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteOrder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Base buffer test.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public abstract class BufferTest {
+
+ /**
+ * Creates a new test buffer.
+ */
+ protected abstract Buffer createBuffer(int capacity);
+
+ /**
+ * Creates a new test buffer.
+ */
+ protected abstract Buffer createBuffer(int capacity, int maxCapacity);
+
+ @Test
+ public void testPosition() {
+ Buffer buffer = createBuffer(8);
+ assertEquals(0, buffer.position());
+ buffer.writeInt(10);
+ assertEquals(4, buffer.position());
+ buffer.position(0);
+ assertEquals(0, buffer.position());
+ assertEquals(10, buffer.readInt());
+ }
+
+ @Test
+ public void testFlip() {
+ Buffer buffer = createBuffer(8);
+ buffer.writeInt(10);
+ assertEquals(4, buffer.position());
+ assertEquals(8, buffer.capacity());
+ assertEquals(-1, buffer.limit());
+ assertEquals(8, buffer.capacity());
+ buffer.flip();
+ assertEquals(4, buffer.limit());
+ assertEquals(0, buffer.position());
+ }
+
+ @Test
+ public void testLimit() {
+ Buffer buffer = createBuffer(8);
+ assertEquals(0, buffer.position());
+ assertEquals(-1, buffer.limit());
+ assertEquals(8, buffer.capacity());
+ buffer.limit(4);
+ assertEquals(4, buffer.limit());
+ assertTrue(buffer.hasRemaining());
+ buffer.writeInt(10);
+ assertEquals(0, buffer.remaining());
+ assertFalse(buffer.hasRemaining());
+ }
+
+ @Test
+ public void testClear() {
+ Buffer buffer = createBuffer(8);
+ buffer.limit(6);
+ assertEquals(6, buffer.limit());
+ buffer.writeInt(10);
+ assertEquals(4, buffer.position());
+ buffer.clear();
+ assertEquals(-1, buffer.limit());
+ assertEquals(8, buffer.capacity());
+ assertEquals(0, buffer.position());
+ }
+
+ @Test
+ public void testMarkReset() {
+ assertTrue(createBuffer(12).writeInt(10).mark().writeBoolean(true).reset().readBoolean());
+ }
+
+ @Test(expected = BufferUnderflowException.class)
+ public void testReadIntThrowsBufferUnderflowWithNoRemainingBytesRelative() {
+ createBuffer(4, 4)
+ .writeInt(10)
+ .readInt();
+ }
+
+ @Test(expected = BufferUnderflowException.class)
+ public void testReadIntThrowsBufferUnderflowWithNoRemainingBytesAbsolute() {
+ createBuffer(4, 4).readInt(2);
+ }
+
+ @Test(expected = BufferOverflowException.class)
+ public void testWriteIntThrowsBufferOverflowWithNoRemainingBytesRelative() {
+ createBuffer(4, 4).writeInt(10).writeInt(20);
+ }
+
+ @Test(expected = BufferOverflowException.class)
+ public void testReadIntThrowsBufferOverflowWithNoRemainingBytesAbsolute() {
+ createBuffer(4, 4).writeInt(4, 10);
+ }
+
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testReadIntThrowsIndexOutOfBounds() {
+ createBuffer(4, 4).readInt(10);
+ }
+
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testWriteIntThrowsIndexOutOfBounds() {
+ createBuffer(4, 4).writeInt(10, 10);
+ }
+
+ @Test
+ public void testWriteReadByteRelative() {
+ assertEquals(10, createBuffer(16).writeByte(10).flip().readByte());
+ }
+
+ @Test
+ public void testWriteReadByteAbsolute() {
+ assertEquals(10, createBuffer(16).writeByte(4, 10).readByte(4));
+ }
+
+ @Test
+ public void testWriteReadUnsignedByteRelative() {
+ assertEquals(10, createBuffer(16).writeUnsignedByte(10).flip().readUnsignedByte());
+ }
+
+ @Test
+ public void testWriteReadUnsignedByteAbsolute() {
+ assertEquals(10, createBuffer(16).writeUnsignedByte(4, 10).readUnsignedByte(4));
+ }
+
+ @Test
+ public void testWriteReadShortRelative() {
+ assertEquals(10, createBuffer(16).writeShort((short) 10).flip().readShort());
+ }
+
+ @Test
+ public void testWriteReadShortAbsolute() {
+ assertEquals(10, createBuffer(16).writeShort(4, (short) 10).readShort(4));
+ }
+
+ @Test
+ public void testWriteReadUnsignedShortRelative() {
+ assertEquals(10, createBuffer(16).writeUnsignedShort((short) 10).flip().readUnsignedShort());
+ }
+
+ @Test
+ public void testWriteReadUnsignedShortAbsolute() {
+ assertEquals(10, createBuffer(16).writeUnsignedShort(4, (short) 10).readUnsignedShort(4));
+ }
+
+ @Test
+ public void testWriteReadIntRelative() {
+ assertEquals(10, createBuffer(16).writeInt(10).flip().readInt());
+ }
+
+ @Test
+ public void testWriteReadUnsignedIntAbsolute() {
+ assertEquals(10, createBuffer(16).writeUnsignedInt(4, 10).readUnsignedInt(4));
+ }
+
+ @Test
+ public void testWriteReadUnsignedIntRelative() {
+ assertEquals(10, createBuffer(16).writeUnsignedInt(10).flip().readUnsignedInt());
+ }
+
+ @Test
+ public void testWriteReadIntAbsolute() {
+ assertEquals(10, createBuffer(16).writeInt(4, 10).readInt(4));
+ }
+
+ @Test
+ public void testWriteReadLongRelative() {
+ assertEquals(12345, createBuffer(16).writeLong(12345).flip().readLong());
+ }
+
+ @Test
+ public void testWriteReadLongAbsolute() {
+ assertEquals(12345, createBuffer(16).writeLong(4, 12345).readLong(4));
+ }
+
+ @Test
+ public void testWriteReadFloatRelative() {
+ assertEquals(10.6f, createBuffer(16).writeFloat(10.6f).flip().readFloat(), .001);
+ }
+
+ @Test
+ public void testWriteReadFloatAbsolute() {
+ assertEquals(10.6f, createBuffer(16).writeFloat(4, 10.6f).readFloat(4), .001);
+ }
+
+ @Test
+ public void testWriteReadDoubleRelative() {
+ assertEquals(10.6, createBuffer(16).writeDouble(10.6).flip().readDouble(), .001);
+ }
+
+ @Test
+ public void testWriteReadDoubleAbsolute() {
+ assertEquals(10.6, createBuffer(16).writeDouble(4, 10.6).readDouble(4), .001);
+ }
+
+ @Test
+ public void testWriteReadBooleanRelative() {
+ assertTrue(createBuffer(16).writeBoolean(true).flip().readBoolean());
+ }
+
+ @Test
+ public void testWriteReadBooleanAbsolute() {
+ assertTrue(createBuffer(16).writeBoolean(4, true).readBoolean(4));
+ }
+
+ @Test
+ public void testWriteReadStringRelative() {
+ Buffer buffer = createBuffer(38)
+ .writeString("Hello world!")
+ .writeString("Hello world again!")
+ .flip();
+ assertEquals("Hello world!", buffer.readString());
+ assertEquals("Hello world again!", buffer.readString());
+ }
+
+ @Test
+ public void testWriteReadStringAbsolute() {
+ Buffer buffer = createBuffer(46)
+ .writeString(4, "Hello world!")
+ .writeString(20, "Hello world again!");
+ assertEquals("Hello world!", buffer.readString(4));
+ assertEquals("Hello world again!", buffer.readString(20));
+ }
+
+ @Test
+ public void testWriteReadUTF8Relative() {
+ Buffer buffer = createBuffer(38)
+ .writeUTF8("Hello world!")
+ .writeUTF8("Hello world again!")
+ .flip();
+ assertEquals("Hello world!", buffer.readUTF8());
+ assertEquals("Hello world again!", buffer.readUTF8());
+ }
+
+ @Test
+ public void testWriteReadUTF8Absolute() {
+ Buffer buffer = createBuffer(46)
+ .writeUTF8(4, "Hello world!")
+ .writeUTF8(20, "Hello world again!");
+ assertEquals("Hello world!", buffer.readUTF8(4));
+ assertEquals("Hello world again!", buffer.readUTF8(20));
+ }
+
+ @Test
+ public void testReadWriter() {
+ Buffer writeBuffer = createBuffer(8).writeLong(10).flip();
+ Buffer readBuffer = createBuffer(8);
+ writeBuffer.read(readBuffer);
+ assertEquals(10, readBuffer.flip().readLong());
+ }
+
+ @Test
+ public void testWriteReadSwappedIntRelative() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeInt(10).flip().readInt());
+ }
+
+ @Test
+ public void testWriteReadSwappedIntAbsolute() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeInt(4, 10).readInt(4));
+ }
+
+ @Test
+ public void testAbsoluteSlice() {
+ Buffer buffer = createBuffer(1024);
+ buffer.writeLong(10).writeLong(11).rewind();
+ Buffer slice = buffer.slice(8, 1016);
+ assertEquals(0, slice.position());
+ assertEquals(11, slice.readLong());
+ }
+
+ @Test
+ public void testRelativeSliceWithoutLength() {
+ Buffer buffer = createBuffer(1024, 1024);
+ buffer.writeLong(10).writeLong(11).writeLong(12).rewind();
+ assertEquals(10, buffer.readLong());
+ Buffer slice = buffer.slice();
+ assertEquals(0, slice.position());
+ assertEquals(-1, slice.limit());
+ assertEquals(1016, slice.capacity());
+ assertEquals(1016, slice.maxCapacity());
+ assertEquals(11, slice.readLong());
+ assertEquals(11, slice.readLong(0));
+ slice.close();
+ Buffer slice2 = buffer.skip(8).slice();
+ assertEquals(0, slice2.position());
+ assertEquals(-1, slice2.limit());
+ assertEquals(1008, slice2.capacity());
+ assertEquals(1008, slice2.maxCapacity());
+ assertEquals(12, slice2.readLong());
+ assertEquals(12, slice2.readLong(0));
+ }
+
+ @Test
+ public void testRelativeSliceWithLength() {
+ Buffer buffer = createBuffer(1024);
+ buffer.writeLong(10).writeLong(11).writeLong(12).rewind();
+ assertEquals(10, buffer.readLong());
+ Buffer slice = buffer.slice(8);
+ assertEquals(0, slice.position());
+ assertEquals(11, slice.readLong());
+ assertEquals(11, slice.readLong(0));
+ slice.close();
+ Buffer slice2 = buffer.skip(8).slice(8);
+ assertEquals(0, slice2.position());
+ assertEquals(12, slice2.readLong());
+ assertEquals(12, slice2.readLong(0));
+ slice2.close();
+ }
+
+ @Test
+ public void testSliceOfSlice() {
+ Buffer buffer = createBuffer(1024);
+ buffer.writeLong(10).writeLong(11).writeLong(12).rewind();
+ assertEquals(10, buffer.readLong());
+ Buffer slice = buffer.slice();
+ assertEquals(11, slice.readLong());
+ Buffer sliceOfSlice = slice.slice();
+ assertEquals(12, sliceOfSlice.readLong());
+ assertEquals(8, sliceOfSlice.position());
+ }
+
+ @Test
+ public void testSliceWithLimit() {
+ Buffer buffer = createBuffer(1024).limit(16);
+ buffer.writeLong(10);
+ Buffer slice = buffer.slice();
+ assertEquals(0, slice.position());
+ assertEquals(8, slice.capacity());
+ assertEquals(8, slice.maxCapacity());
+ assertEquals(8, slice.remaining());
+ }
+
+ @Test
+ public void testSliceWithLittleRemaining() {
+ Buffer buffer = createBuffer(1024, 2048);
+ buffer.position(1020);
+ Buffer slice = buffer.slice(8);
+ assertEquals(0, slice.position());
+ assertEquals(-1, slice.limit());
+ }
+
+ @Test
+ public void testCompact() {
+ Buffer buffer = createBuffer(1024);
+ buffer.position(100).writeLong(1234).position(100).compact();
+ assertEquals(0, buffer.position());
+ assertEquals(1234, buffer.readLong());
+ }
+
+ @Test
+ public void testSwappedPosition() {
+ Buffer buffer = createBuffer(8).order(ByteOrder.LITTLE_ENDIAN);
+ assertEquals(0, buffer.position());
+ buffer.writeInt(10);
+ assertEquals(4, buffer.position());
+ buffer.position(0);
+ assertEquals(0, buffer.position());
+ assertEquals(10, buffer.readInt());
+ }
+
+ @Test
+ public void testSwappedFlip() {
+ Buffer buffer = createBuffer(8).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.writeInt(10);
+ assertEquals(4, buffer.position());
+ assertEquals(8, buffer.capacity());
+ assertEquals(-1, buffer.limit());
+ assertEquals(8, buffer.capacity());
+ buffer.flip();
+ assertEquals(4, buffer.limit());
+ assertEquals(0, buffer.position());
+ }
+
+ @Test
+ public void testSwappedLimit() {
+ Buffer buffer = createBuffer(8).order(ByteOrder.LITTLE_ENDIAN);
+ assertEquals(0, buffer.position());
+ assertEquals(-1, buffer.limit());
+ assertEquals(8, buffer.capacity());
+ buffer.limit(4);
+ assertEquals(4, buffer.limit());
+ assertTrue(buffer.hasRemaining());
+ buffer.writeInt(10);
+ assertEquals(0, buffer.remaining());
+ assertFalse(buffer.hasRemaining());
+ }
+
+ @Test
+ public void testSwappedClear() {
+ Buffer buffer = createBuffer(8).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.limit(6);
+ assertEquals(6, buffer.limit());
+ buffer.writeInt(10);
+ assertEquals(4, buffer.position());
+ buffer.clear();
+ assertEquals(-1, buffer.limit());
+ assertEquals(8, buffer.capacity());
+ assertEquals(0, buffer.position());
+ }
+
+ @Test
+ public void testSwappedMarkReset() {
+ assertTrue(createBuffer(12).order(ByteOrder.LITTLE_ENDIAN).writeInt(10).mark().writeBoolean(true).reset().readBoolean());
+ }
+
+ @Test(expected = BufferUnderflowException.class)
+ public void testSwappedReadIntThrowsBufferUnderflowWithNoRemainingBytesRelative() {
+ createBuffer(4, 4).order(ByteOrder.LITTLE_ENDIAN)
+ .writeInt(10)
+ .readInt();
+ }
+
+ @Test(expected = BufferUnderflowException.class)
+ public void testSwappedReadIntThrowsBufferUnderflowWithNoRemainingBytesAbsolute() {
+ createBuffer(4, 4).order(ByteOrder.LITTLE_ENDIAN).readInt(2);
+ }
+
+ @Test(expected = BufferOverflowException.class)
+ public void testSwappedWriteIntThrowsBufferOverflowWithNoRemainingBytesRelative() {
+ createBuffer(4, 4).order(ByteOrder.LITTLE_ENDIAN).writeInt(10).writeInt(20);
+ }
+
+ @Test(expected = BufferOverflowException.class)
+ public void testSwappedReadIntThrowsBufferOverflowWithNoRemainingBytesAbsolute() {
+ createBuffer(4, 4).order(ByteOrder.LITTLE_ENDIAN).writeInt(4, 10);
+ }
+
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testSwappedReadIntThrowsIndexOutOfBounds() {
+ createBuffer(4, 4).order(ByteOrder.LITTLE_ENDIAN).readInt(10);
+ }
+
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testSwappedWriteIntThrowsIndexOutOfBounds() {
+ createBuffer(4, 4).order(ByteOrder.LITTLE_ENDIAN).writeInt(10, 10);
+ }
+
+ @Test
+ public void testSwappedWriteReadByteRelative() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeByte(10).flip().readByte());
+ }
+
+ @Test
+ public void testSwappedWriteReadByteAbsolute() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeByte(4, 10).readByte(4));
+ }
+
+ @Test
+ public void testSwappedWriteReadUnsignedByteRelative() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeUnsignedByte(10).flip().readUnsignedByte());
+ }
+
+ @Test
+ public void testSwappedWriteReadUnsignedByteAbsolute() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeUnsignedByte(4, 10).readUnsignedByte(4));
+ }
+
+ @Test
+ public void testSwappedWriteReadShortRelative() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeShort((short) 10).flip().readShort());
+ }
+
+ @Test
+ public void testSwappedWriteReadShortAbsolute() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeShort(4, (short) 10).readShort(4));
+ }
+
+ @Test
+ public void testSwappedWriteReadUnsignedShortRelative() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeUnsignedShort((short) 10).flip().readUnsignedShort());
+ }
+
+ @Test
+ public void testSwappedWriteReadUnsignedShortAbsolute() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeUnsignedShort(4, (short) 10).readUnsignedShort(4));
+ }
+
+ @Test
+ public void testSwappedWriteReadIntRelative() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeInt(10).flip().readInt());
+ }
+
+ @Test
+ public void testSwappedWriteReadUnsignedIntAbsolute() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeUnsignedInt(4, 10).readUnsignedInt(4));
+ }
+
+ @Test
+ public void testSwappedWriteReadUnsignedIntRelative() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeUnsignedInt(10).flip().readUnsignedInt());
+ }
+
+ @Test
+ public void testSwappedWriteReadIntAbsolute() {
+ assertEquals(10, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeInt(4, 10).readInt(4));
+ }
+
+ @Test
+ public void testSwappedWriteReadLongRelative() {
+ assertEquals(12345, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeLong(12345).flip().readLong());
+ }
+
+ @Test
+ public void testSwappedWriteReadLongAbsolute() {
+ assertEquals(12345, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeLong(4, 12345).readLong(4));
+ }
+
+ @Test
+ public void testSwappedWriteReadFloatRelative() {
+ assertEquals(10.6f, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeFloat(10.6f).flip().readFloat(), .001);
+ }
+
+ @Test
+ public void testSwappedWriteReadFloatAbsolute() {
+ assertEquals(10.6f, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeFloat(4, 10.6f).readFloat(4), .001);
+ }
+
+ @Test
+ public void testSwappedWriteReadDoubleRelative() {
+ assertEquals(10.6, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeDouble(10.6).flip().readDouble(), .001);
+ }
+
+ @Test
+ public void testSwappedWriteReadDoubleAbsolute() {
+ assertEquals(10.6, createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeDouble(4, 10.6).readDouble(4), .001);
+ }
+
+ @Test
+ public void testSwappedWriteReadBooleanRelative() {
+ assertTrue(createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeBoolean(true).flip().readBoolean());
+ }
+
+ @Test
+ public void testSwappedWriteReadBooleanAbsolute() {
+ assertTrue(createBuffer(16).order(ByteOrder.LITTLE_ENDIAN).writeBoolean(4, true).readBoolean(4));
+ }
+
+ @Test
+ public void testSwappedReadWriter() {
+ Buffer writeBuffer = createBuffer(8).order(ByteOrder.LITTLE_ENDIAN).writeLong(10).flip();
+ Buffer readBuffer = createBuffer(8).order(ByteOrder.LITTLE_ENDIAN);
+ writeBuffer.read(readBuffer);
+ assertEquals(10, readBuffer.flip().readLong());
+ }
+
+ @Test
+ public void testSwappedAbsoluteSlice() {
+ Buffer buffer = createBuffer(1024).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.writeLong(10).writeLong(11).rewind();
+ Buffer slice = buffer.slice(8, 1016);
+ assertEquals(0, slice.position());
+ assertEquals(11, slice.readLong());
+ }
+
+ @Test
+ public void testSwappedRelativeSliceWithoutLength() {
+ Buffer buffer = createBuffer(1024, 1024).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.writeLong(10).writeLong(11).writeLong(12).rewind();
+ assertEquals(10, buffer.readLong());
+ Buffer slice = buffer.slice();
+ assertEquals(0, slice.position());
+ assertEquals(-1, slice.limit());
+ assertEquals(1016, slice.capacity());
+ assertEquals(1016, slice.maxCapacity());
+ assertEquals(11, slice.readLong());
+ assertEquals(11, slice.readLong(0));
+ slice.close();
+ Buffer slice2 = buffer.skip(8).slice();
+ assertEquals(0, slice2.position());
+ assertEquals(-1, slice2.limit());
+ assertEquals(1008, slice2.capacity());
+ assertEquals(1008, slice2.maxCapacity());
+ assertEquals(12, slice2.readLong());
+ assertEquals(12, slice2.readLong(0));
+ }
+
+ @Test
+ public void testSwappedRelativeSliceWithLength() {
+ Buffer buffer = createBuffer(1024).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.writeLong(10).writeLong(11).writeLong(12).rewind();
+ assertEquals(10, buffer.readLong());
+ Buffer slice = buffer.slice(8);
+ assertEquals(0, slice.position());
+ assertEquals(11, slice.readLong());
+ assertEquals(11, slice.readLong(0));
+ slice.close();
+ Buffer slice2 = buffer.skip(8).slice(8);
+ assertEquals(0, slice2.position());
+ assertEquals(12, slice2.readLong());
+ assertEquals(12, slice2.readLong(0));
+ slice2.close();
+ }
+
+ @Test
+ public void testSwappedSliceOfSlice() {
+ Buffer buffer = createBuffer(1024).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.writeLong(10).writeLong(11).writeLong(12).rewind();
+ assertEquals(10, buffer.readLong());
+ Buffer slice = buffer.slice();
+ assertEquals(11, slice.readLong());
+ Buffer sliceOfSlice = slice.slice();
+ assertEquals(12, sliceOfSlice.readLong());
+ assertEquals(8, sliceOfSlice.position());
+ }
+
+ @Test
+ public void testSwappedSliceWithLimit() {
+ Buffer buffer = createBuffer(1024).order(ByteOrder.LITTLE_ENDIAN).limit(16);
+ buffer.writeLong(10);
+ Buffer slice = buffer.slice();
+ assertEquals(0, slice.position());
+ assertEquals(8, slice.capacity());
+ assertEquals(8, slice.maxCapacity());
+ assertEquals(8, slice.remaining());
+ }
+
+ @Test
+ public void testSwappedSliceWithLittleRemaining() {
+ Buffer buffer = createBuffer(1024, 2048).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(1020);
+ Buffer slice = buffer.slice(8);
+ assertEquals(0, slice.position());
+ assertEquals(-1, slice.limit());
+ }
+
+ @Test
+ public void testSwappedCompact() {
+ Buffer buffer = createBuffer(1024).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(100).writeLong(1234).position(100).compact();
+ assertEquals(0, buffer.position());
+ assertEquals(1234, buffer.readLong());
+ }
+
+ @Test
+ public void testCapacity0Read() {
+ Buffer buffer = createBuffer(0, 1024);
+ assertEquals(0, buffer.readLong());
+ }
+
+ @Test
+ public void testCapacity0Write() {
+ Buffer buffer = createBuffer(0, 1024);
+ buffer.writeLong(10);
+ assertEquals(10, buffer.readLong(0));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Direct buffer test.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class DirectBufferTest extends BufferTest {
+
+ @Override
+ protected Buffer createBuffer(int capacity) {
+ return DirectBuffer.allocate(capacity);
+ }
+
+ @Override
+ protected Buffer createBuffer(int capacity, int maxCapacity) {
+ return DirectBuffer.allocate(capacity, maxCapacity);
+ }
+
+ @Test
+ public void testByteBufferToDirectBuffer() {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(8);
+ byteBuffer.putLong(10);
+ byteBuffer.flip();
+
+ DirectBuffer directBuffer = DirectBuffer.allocate(8);
+ directBuffer.write(byteBuffer.array());
+ directBuffer.flip();
+ assertEquals(directBuffer.readLong(), byteBuffer.getLong());
+ assertTrue(directBuffer.isDirect());
+ directBuffer.release();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * File buffer test.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class FileBufferTest extends BufferTest {
+ @AfterClass
+ public static void afterTest() {
+ FileTesting.cleanFiles();
+ }
+
+ @Override
+ protected Buffer createBuffer(int capacity) {
+ return FileBuffer.allocate(FileTesting.createFile(), capacity);
+ }
+
+ @Override
+ protected Buffer createBuffer(int capacity, int maxCapacity) {
+ return FileBuffer.allocate(FileTesting.createFile(), capacity, maxCapacity);
+ }
+
+ @Test
+ public void testFileToHeapBuffer() {
+ File file = FileTesting.createFile();
+ try (FileBuffer buffer = FileBuffer.allocate(file, 16)) {
+ buffer.writeLong(10).writeLong(11).flip();
+ byte[] bytes = new byte[16];
+ buffer.read(bytes).rewind();
+ HeapBuffer heapBuffer = HeapBuffer.wrap(bytes);
+ assertEquals(buffer.readLong(), heapBuffer.readLong());
+ assertEquals(buffer.readLong(), heapBuffer.readLong());
+ }
+ }
+
+ /**
+ * Rests reopening a file that has been closed.
+ */
+ @Test
+ public void testPersist() {
+ File file = FileTesting.createFile();
+ try (FileBuffer buffer = FileBuffer.allocate(file, 16)) {
+ buffer.writeLong(10).writeLong(11).flip();
+ assertEquals(10, buffer.readLong());
+ assertEquals(11, buffer.readLong());
+ }
+ try (FileBuffer buffer = FileBuffer.allocate(file, 16)) {
+ assertEquals(10, buffer.readLong());
+ assertEquals(11, buffer.readLong());
+ }
+ }
+
+ /**
+ * Tests deleting a file.
+ */
+ @Test
+ public void testDelete() {
+ File file = FileTesting.createFile();
+ FileBuffer buffer = FileBuffer.allocate(file, 16);
+ buffer.writeLong(10).writeLong(11).flip();
+ assertEquals(10, buffer.readLong());
+ assertEquals(11, buffer.readLong());
+ assertTrue(Files.exists(file.toPath()));
+ buffer.delete();
+ assertFalse(Files.exists(file.toPath()));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.UUID;
+
+public abstract class FileTesting {
+ public static File createFile() {
+ File file = new File("target/test-files/" + UUID.randomUUID().toString());
+ file.getParentFile().mkdirs();
+ return file;
+ }
+
+ public static void cleanFiles() {
+ Path directory = Paths.get("target/test-files/");
+ try {
+ Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (Exception ignore) {
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Heap buffer test.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class HeapBufferTest extends BufferTest {
+
+ @Override
+ protected Buffer createBuffer(int capacity) {
+ return HeapBuffer.allocate(capacity);
+ }
+
+ @Override
+ protected Buffer createBuffer(int capacity, int maxCapacity) {
+ return HeapBuffer.allocate(capacity, maxCapacity);
+ }
+
+ @Test
+ public void testByteBufferToHeapBuffer() {
+ ByteBuffer byteBuffer = ByteBuffer.allocate(8);
+ byteBuffer.putLong(10);
+ byteBuffer.rewind();
+
+ HeapBuffer directBuffer = HeapBuffer.wrap(byteBuffer.array());
+ assertEquals(directBuffer.readLong(), byteBuffer.getLong());
+ }
+
+ @Test
+ public void testDirectToHeapBuffer() {
+ DirectBuffer directBuffer = DirectBuffer.allocate(8);
+ directBuffer.writeLong(10);
+ directBuffer.flip();
+
+ byte[] bytes = new byte[8];
+ directBuffer.read(bytes);
+ directBuffer.rewind();
+
+ HeapBuffer heapBuffer = HeapBuffer.wrap(bytes);
+ assertEquals(directBuffer.readLong(), heapBuffer.readLong());
+
+ directBuffer.release();
+ }
+
+ @Test
+ public void testHeapToDirectBuffer() {
+ HeapBuffer heapBuffer = HeapBuffer.allocate(8);
+ heapBuffer.writeLong(10);
+ heapBuffer.flip();
+
+ DirectBuffer directBuffer = DirectBuffer.allocate(8);
+ directBuffer.write(heapBuffer.array());
+ directBuffer.flip();
+
+ assertEquals(directBuffer.readLong(), heapBuffer.readLong());
+
+ directBuffer.release();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.storage.buffer;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Mapped buffer test.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class MappedBufferTest extends BufferTest {
+ @AfterClass
+ public static void afterTest() {
+ FileTesting.cleanFiles();
+ }
+
+ @Override
+ protected Buffer createBuffer(int capacity) {
+ return MappedBuffer.allocate(FileTesting.createFile(), capacity);
+ }
+
+ @Override
+ protected Buffer createBuffer(int capacity, int maxCapacity) {
+ return MappedBuffer.allocate(FileTesting.createFile(), capacity, maxCapacity);
+ }
+
+ /**
+ * Rests reopening a file that has been closed.
+ */
+ @Test
+ public void testPersist() {
+ File file = FileTesting.createFile();
+ try (MappedBuffer buffer = MappedBuffer.allocate(file, 16)) {
+ buffer.writeLong(10).writeLong(11).flip();
+ assertEquals(10, buffer.readLong());
+ assertEquals(11, buffer.readLong());
+ }
+ try (MappedBuffer buffer = MappedBuffer.allocate(file, 16)) {
+ assertEquals(10, buffer.readLong());
+ assertEquals(11, buffer.readLong());
+ }
+ }
+
+ /**
+ * Tests deleting a file.
+ */
+ @Test
+ public void testDelete() {
+ File file = FileTesting.createFile();
+ MappedBuffer buffer = MappedBuffer.allocate(file, 16);
+ buffer.writeLong(10).writeLong(11).flip();
+ assertEquals(10, buffer.readLong());
+ assertEquals(11, buffer.readLong());
+ assertTrue(Files.exists(file.toPath()));
+ buffer.delete();
+ assertFalse(Files.exists(file.toPath()));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.StorageLevel;
+import io.atomix.utils.serializer.Namespace;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Base journal test.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+@RunWith(Parameterized.class)
+public abstract class AbstractJournalTest {
+ private static final Namespace NAMESPACE = Namespace.builder()
+ .register(TestEntry.class)
+ .register(byte[].class)
+ .build();
+
+ protected static final TestEntry ENTRY = new TestEntry(32);
+ private static final Path PATH = Paths.get("target/test-logs/");
+
+ private final int maxSegmentSize;
+ private final int cacheSize;
+ protected final int entriesPerSegment;
+
+ protected AbstractJournalTest(int maxSegmentSize, int cacheSize) {
+ this.maxSegmentSize = maxSegmentSize;
+ this.cacheSize = cacheSize;
+ int entryLength = (NAMESPACE.serialize(ENTRY).length + 8);
+ this.entriesPerSegment = (maxSegmentSize - 64) / entryLength;
+ }
+
+ protected abstract StorageLevel storageLevel();
+
+ @Parameterized.Parameters
+ public static Collection primeNumbers() {
+ List<Object[]> runs = new ArrayList<>();
+ for (int i = 1; i <= 10; i++) {
+ for (int j = 1; j <= 10; j++) {
+ runs.add(new Object[]{64 + (i * (NAMESPACE.serialize(ENTRY).length + 8) + j), j});
+ }
+ }
+ return runs;
+ }
+
+ protected SegmentedJournal<TestEntry> createJournal() {
+ return SegmentedJournal.<TestEntry>builder()
+ .withName("test")
+ .withDirectory(PATH.toFile())
+ .withNamespace(NAMESPACE)
+ .withStorageLevel(storageLevel())
+ .withMaxSegmentSize(maxSegmentSize)
+ .withIndexDensity(.2)
+ .withCacheSize(cacheSize)
+ .build();
+ }
+
+ @Test
+ public void testCloseMultipleTimes() {
+ // given
+ final Journal<TestEntry> journal = createJournal();
+
+ // when
+ journal.close();
+
+ // then
+ journal.close();
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testWriteRead() throws Exception {
+ try (Journal<TestEntry> journal = createJournal()) {
+ JournalWriter<TestEntry> writer = journal.writer();
+ JournalReader<TestEntry> reader = journal.openReader(1);
+
+ // Append a couple entries.
+ Indexed<TestEntry> indexed;
+ assertEquals(1, writer.getNextIndex());
+ indexed = writer.append(ENTRY);
+ assertEquals(1, indexed.index());
+
+ assertEquals(2, writer.getNextIndex());
+ writer.append(new Indexed<>(2, ENTRY, 0));
+ reader.reset(2);
+ indexed = reader.next();
+ assertEquals(2, indexed.index());
+ assertFalse(reader.hasNext());
+
+ // Test reading an entry
+ Indexed<TestEntry> entry1;
+ reader.reset();
+ entry1 = (Indexed) reader.next();
+ assertEquals(1, entry1.index());
+ assertEquals(entry1, reader.getCurrentEntry());
+ assertEquals(1, reader.getCurrentIndex());
+
+ // Test reading a second entry
+ Indexed<TestEntry> entry2;
+ assertTrue(reader.hasNext());
+ assertEquals(2, reader.getNextIndex());
+ entry2 = (Indexed) reader.next();
+ assertEquals(2, entry2.index());
+ assertEquals(entry2, reader.getCurrentEntry());
+ assertEquals(2, reader.getCurrentIndex());
+ assertFalse(reader.hasNext());
+
+ // Test opening a new reader and reading from the journal.
+ reader = journal.openReader(1);
+ assertTrue(reader.hasNext());
+ entry1 = (Indexed) reader.next();
+ assertEquals(1, entry1.index());
+ assertEquals(entry1, reader.getCurrentEntry());
+ assertEquals(1, reader.getCurrentIndex());
+ assertTrue(reader.hasNext());
+
+ assertTrue(reader.hasNext());
+ assertEquals(2, reader.getNextIndex());
+ entry2 = (Indexed) reader.next();
+ assertEquals(2, entry2.index());
+ assertEquals(entry2, reader.getCurrentEntry());
+ assertEquals(2, reader.getCurrentIndex());
+ assertFalse(reader.hasNext());
+
+ // Reset the reader.
+ reader.reset();
+
+ // Test opening a new reader and reading from the journal.
+ reader = journal.openReader(1);
+ assertTrue(reader.hasNext());
+ entry1 = (Indexed) reader.next();
+ assertEquals(1, entry1.index());
+ assertEquals(entry1, reader.getCurrentEntry());
+ assertEquals(1, reader.getCurrentIndex());
+ assertTrue(reader.hasNext());
+
+ assertTrue(reader.hasNext());
+ assertEquals(2, reader.getNextIndex());
+ entry2 = (Indexed) reader.next();
+ assertEquals(2, entry2.index());
+ assertEquals(entry2, reader.getCurrentEntry());
+ assertEquals(2, reader.getCurrentIndex());
+ assertFalse(reader.hasNext());
+
+ // Truncate the journal and write a different entry.
+ writer.truncate(1);
+ assertEquals(2, writer.getNextIndex());
+ writer.append(new Indexed<>(2, ENTRY, 0));
+ reader.reset(2);
+ indexed = reader.next();
+ assertEquals(2, indexed.index());
+
+ // Reset the reader to a specific index and read the last entry again.
+ reader.reset(2);
+
+ assertNotNull(reader.getCurrentEntry());
+ assertEquals(1, reader.getCurrentIndex());
+ assertEquals(1, reader.getCurrentEntry().index());
+ assertTrue(reader.hasNext());
+ assertEquals(2, reader.getNextIndex());
+ entry2 = (Indexed) reader.next();
+ assertEquals(2, entry2.index());
+ assertEquals(entry2, reader.getCurrentEntry());
+ assertEquals(2, reader.getCurrentIndex());
+ assertFalse(reader.hasNext());
+ }
+ }
+
+ @Test
+ public void testResetTruncateZero() throws Exception {
+ try (SegmentedJournal<TestEntry> journal = createJournal()) {
+ JournalWriter<TestEntry> writer = journal.writer();
+ JournalReader<TestEntry> reader = journal.openReader(1);
+
+ assertEquals(0, writer.getLastIndex());
+ writer.append(ENTRY);
+ writer.append(ENTRY);
+ writer.reset(1);
+ assertEquals(0, writer.getLastIndex());
+ writer.append(ENTRY);
+ assertEquals(1, reader.next().index());
+ writer.reset(1);
+ assertEquals(0, writer.getLastIndex());
+ writer.append(ENTRY);
+ assertEquals(1, writer.getLastIndex());
+ assertEquals(1, writer.getLastEntry().index());
+
+ assertTrue(reader.hasNext());
+ assertEquals(1, reader.next().index());
+
+ writer.truncate(0);
+ assertEquals(0, writer.getLastIndex());
+ assertNull(writer.getLastEntry());
+ writer.append(ENTRY);
+ assertEquals(1, writer.getLastIndex());
+ assertEquals(1, writer.getLastEntry().index());
+
+ assertTrue(reader.hasNext());
+ assertEquals(1, reader.next().index());
+ }
+ }
+
+ @Test
+ public void testTruncateRead() throws Exception {
+ int i = 10;
+ try (Journal<TestEntry> journal = createJournal()) {
+ JournalWriter<TestEntry> writer = journal.writer();
+ JournalReader<TestEntry> reader = journal.openReader(1);
+
+ for (int j = 1; j <= i; j++) {
+ assertEquals(j, writer.append(new TestEntry(32)).index());
+ }
+
+ for (int j = 1; j <= i - 2; j++) {
+ assertTrue(reader.hasNext());
+ assertEquals(j, reader.next().index());
+ }
+
+ writer.truncate(i - 2);
+
+ assertFalse(reader.hasNext());
+ assertEquals(i - 1, writer.append(new TestEntry(32)).index());
+ assertEquals(i, writer.append(new TestEntry(32)).index());
+
+ assertTrue(reader.hasNext());
+ Indexed<TestEntry> entry = reader.next();
+ assertEquals(i - 1, entry.index());
+ assertTrue(reader.hasNext());
+ entry = reader.next();
+ assertEquals(i, entry.index());
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testWriteReadEntries() throws Exception {
+ try (Journal<TestEntry> journal = createJournal()) {
+ JournalWriter<TestEntry> writer = journal.writer();
+ JournalReader<TestEntry> reader = journal.openReader(1);
+
+ for (int i = 1; i <= entriesPerSegment * 5; i++) {
+ writer.append(ENTRY);
+ assertTrue(reader.hasNext());
+ Indexed<TestEntry> entry;
+ entry = (Indexed) reader.next();
+ assertEquals(i, entry.index());
+ assertEquals(32, entry.entry().bytes().length);
+ reader.reset(i);
+ entry = (Indexed) reader.next();
+ assertEquals(i, entry.index());
+ assertEquals(32, entry.entry().bytes().length);
+
+ if (i > 6) {
+ reader.reset(i - 5);
+ assertNotNull(reader.getCurrentEntry());
+ assertEquals(i - 6, reader.getCurrentIndex());
+ assertEquals(i - 6, reader.getCurrentEntry().index());
+ assertEquals(i - 5, reader.getNextIndex());
+ reader.reset(i + 1);
+ }
+
+ writer.truncate(i - 1);
+ writer.append(ENTRY);
+
+ assertTrue(reader.hasNext());
+ reader.reset(i);
+ assertTrue(reader.hasNext());
+ entry = (Indexed) reader.next();
+ assertEquals(i, entry.index());
+ assertEquals(32, entry.entry().bytes().length);
+ }
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testWriteReadCommittedEntries() throws Exception {
+ try (Journal<TestEntry> journal = createJournal()) {
+ JournalWriter<TestEntry> writer = journal.writer();
+ JournalReader<TestEntry> reader = journal.openReader(1, JournalReader.Mode.COMMITS);
+
+ for (int i = 1; i <= entriesPerSegment * 5; i++) {
+ writer.append(ENTRY);
+ assertFalse(reader.hasNext());
+ writer.commit(i);
+ assertTrue(reader.hasNext());
+ Indexed<TestEntry> entry;
+ entry = (Indexed) reader.next();
+ assertEquals(i, entry.index());
+ assertEquals(32, entry.entry().bytes().length);
+ reader.reset(i);
+ entry = (Indexed) reader.next();
+ assertEquals(i, entry.index());
+ assertEquals(32, entry.entry().bytes().length);
+ }
+ }
+ }
+
+ @Test
+ public void testReadAfterCompact() throws Exception {
+ try (SegmentedJournal<TestEntry> journal = createJournal()) {
+ JournalWriter<TestEntry> writer = journal.writer();
+ JournalReader<TestEntry> uncommittedReader = journal.openReader(1, JournalReader.Mode.ALL);
+ JournalReader<TestEntry> committedReader = journal.openReader(1, JournalReader.Mode.COMMITS);
+
+ for (int i = 1; i <= entriesPerSegment * 10; i++) {
+ assertEquals(i, writer.append(ENTRY).index());
+ }
+
+ assertEquals(1, uncommittedReader.getNextIndex());
+ assertTrue(uncommittedReader.hasNext());
+ assertEquals(1, committedReader.getNextIndex());
+ assertFalse(committedReader.hasNext());
+
+ writer.commit(entriesPerSegment * 9);
+
+ assertTrue(uncommittedReader.hasNext());
+ assertTrue(committedReader.hasNext());
+
+ for (int i = 1; i <= entriesPerSegment * 2.5; i++) {
+ assertEquals(i, uncommittedReader.next().index());
+ assertEquals(i, committedReader.next().index());
+ }
+
+ journal.compact(entriesPerSegment * 5 + 1);
+
+ assertNull(uncommittedReader.getCurrentEntry());
+ assertEquals(0, uncommittedReader.getCurrentIndex());
+ assertTrue(uncommittedReader.hasNext());
+ assertEquals(entriesPerSegment * 5 + 1, uncommittedReader.getNextIndex());
+ assertEquals(entriesPerSegment * 5 + 1, uncommittedReader.next().index());
+
+ assertNull(committedReader.getCurrentEntry());
+ assertEquals(0, committedReader.getCurrentIndex());
+ assertTrue(committedReader.hasNext());
+ assertEquals(entriesPerSegment * 5 + 1, committedReader.getNextIndex());
+ assertEquals(entriesPerSegment * 5 + 1, committedReader.next().index());
+ }
+ }
+
+ @Before
+ @After
+ public void cleanupStorage() throws IOException {
+ if (Files.exists(PATH)) {
+ Files.walkFileTree(PATH, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.StorageLevel;
+
+/**
+ * Disk journal test.
+ */
+public class DiskJournalTest extends PersistentJournalTest {
+ public DiskJournalTest(int maxSegmentSize, int cacheSize) {
+ super(maxSegmentSize, cacheSize);
+ }
+
+ @Override
+ protected StorageLevel storageLevel() {
+ return StorageLevel.DISK;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Segment descriptor test.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class JournalSegmentDescriptorTest {
+
+ /**
+ * Tests the segment descriptor builder.
+ */
+ @Test
+ public void testDescriptorBuilder() {
+ JournalSegmentDescriptor descriptor = JournalSegmentDescriptor.builder(ByteBuffer.allocate(JournalSegmentDescriptor.BYTES))
+ .withId(2)
+ .withIndex(1025)
+ .withMaxSegmentSize(1024 * 1024)
+ .withMaxEntries(2048)
+ .build();
+
+ assertEquals(2, descriptor.id());
+ assertEquals(JournalSegmentDescriptor.VERSION, descriptor.version());
+ assertEquals(1025, descriptor.index());
+ assertEquals(1024 * 1024, descriptor.maxSegmentSize());
+ assertEquals(2048, descriptor.maxEntries());
+
+ assertEquals(0, descriptor.updated());
+ long time = System.currentTimeMillis();
+ descriptor.update(time);
+ assertEquals(time, descriptor.updated());
+ }
+
+ /**
+ * Tests copying the segment descriptor.
+ */
+ @Test
+ public void testDescriptorCopy() {
+ JournalSegmentDescriptor descriptor = JournalSegmentDescriptor.builder()
+ .withId(2)
+ .withIndex(1025)
+ .withMaxSegmentSize(1024 * 1024)
+ .withMaxEntries(2048)
+ .build();
+
+ long time = System.currentTimeMillis();
+ descriptor.update(time);
+
+ descriptor = descriptor.copyTo(ByteBuffer.allocate(JournalSegmentDescriptor.BYTES));
+
+ assertEquals(2, descriptor.id());
+ assertEquals(JournalSegmentDescriptor.VERSION, descriptor.version());
+ assertEquals(1025, descriptor.index());
+ assertEquals(1024 * 1024, descriptor.maxSegmentSize());
+ assertEquals(2048, descriptor.maxEntries());
+ assertEquals(time, descriptor.updated());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import java.io.File;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Journal segment file test.
+ */
+public class JournalSegmentFileTest {
+
+ @Test
+ public void testIsSegmentFile() throws Exception {
+ assertTrue(JournalSegmentFile.isSegmentFile("foo", "foo-1.log"));
+ assertFalse(JournalSegmentFile.isSegmentFile("foo", "bar-1.log"));
+ assertTrue(JournalSegmentFile.isSegmentFile("foo", "foo-1-1.log"));
+ }
+
+ @Test
+ public void testCreateSegmentFile() throws Exception {
+ File file = JournalSegmentFile.createSegmentFile("foo", new File(System.getProperty("user.dir")), 1);
+ assertTrue(JournalSegmentFile.isSegmentFile("foo", file));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.StorageLevel;
+
+/**
+ * Memory mapped journal test.
+ */
+public class MappedJournalTest extends PersistentJournalTest {
+ public MappedJournalTest(int maxSegmentSize, int cacheSize) {
+ super(maxSegmentSize, cacheSize);
+ }
+
+ @Override
+ protected StorageLevel storageLevel() {
+ return StorageLevel.MAPPED;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.storage.StorageLevel;
+
+/**
+ * Memory journal test.
+ */
+public class MemoryJournalTest extends AbstractJournalTest {
+ public MemoryJournalTest(int maxSegmentSize, int cacheSize) {
+ super(maxSegmentSize, cacheSize);
+ }
+
+ @Override
+ protected StorageLevel storageLevel() {
+ return StorageLevel.MEMORY;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Persistent journal test base.
+ */
+public abstract class PersistentJournalTest extends AbstractJournalTest {
+ protected PersistentJournalTest(int maxSegmentSize, int cacheSize) {
+ super(maxSegmentSize, cacheSize);
+ }
+
+ /**
+ * Tests reading from a compacted journal.
+ */
+ @Test
+ public void testCompactAndRecover() throws Exception {
+ SegmentedJournal<TestEntry> journal = createJournal();
+
+ // Write three segments to the journal.
+ JournalWriter<TestEntry> writer = journal.writer();
+ for (int i = 0; i < entriesPerSegment * 3; i++) {
+ writer.append(ENTRY);
+ }
+
+ // Commit the entries and compact the first segment.
+ writer.commit(entriesPerSegment * 3);
+ journal.compact(entriesPerSegment + 1);
+
+ // Close the journal.
+ journal.close();
+
+ // Reopen the journal and create a reader.
+ journal = createJournal();
+ writer = journal.writer();
+ JournalReader<TestEntry> reader = journal.openReader(1, JournalReader.Mode.COMMITS);
+ writer.append(ENTRY);
+ writer.append(ENTRY);
+ writer.commit(entriesPerSegment * 3);
+
+ // Ensure the reader starts at the first physical index in the journal.
+ assertEquals(entriesPerSegment + 1, reader.getNextIndex());
+ assertEquals(reader.getFirstIndex(), reader.getNextIndex());
+ assertTrue(reader.hasNext());
+ assertEquals(entriesPerSegment + 1, reader.getNextIndex());
+ assertEquals(reader.getFirstIndex(), reader.getNextIndex());
+ assertEquals(entriesPerSegment + 1, reader.next().index());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal;
+
+import io.atomix.utils.misc.ArraySizeHashPrinter;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Test entry.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class TestEntry {
+ private final byte[] bytes;
+
+ public TestEntry(int size) {
+ this(new byte[size]);
+ }
+
+ public TestEntry(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ public byte[] bytes() {
+ return bytes;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("bytes", ArraySizeHashPrinter.of(bytes))
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.storage.journal.index;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Sparse journal index test.
+ */
+public class SparseJournalIndexTest {
+ @Test
+ public void testSparseJournalIndex() throws Exception {
+ JournalIndex index = new SparseJournalIndex(.2);
+ assertNull(index.lookup(1));
+ index.index(1, 2);
+ assertNull(index.lookup(1));
+ index.index(2, 4);
+ index.index(3, 6);
+ index.index(4, 8);
+ index.index(5, 10);
+ assertEquals(5, index.lookup(5).index());
+ assertEquals(10, index.lookup(5).position());
+ index.index(6, 12);
+ index.index(7, 14);
+ index.index(8, 16);
+ assertEquals(5, index.lookup(8).index());
+ assertEquals(10, index.lookup(8).position());
+ index.index(9, 18);
+ index.index(10, 20);
+ assertEquals(10, index.lookup(10).index());
+ assertEquals(20, index.lookup(10).position());
+ index.truncate(8);
+ assertEquals(5, index.lookup(8).index());
+ assertEquals(10, index.lookup(8).position());
+ assertEquals(5, index.lookup(10).index());
+ assertEquals(10, index.lookup(10).position());
+ index.truncate(4);
+ assertNull(index.lookup(4));
+ assertNull(index.lookup(8));
+
+ index = new SparseJournalIndex(.2);
+ assertNull(index.lookup(100));
+ index.index(101, 2);
+ assertNull(index.lookup(1));
+ index.index(102, 4);
+ index.index(103, 6);
+ index.index(104, 8);
+ index.index(105, 10);
+ assertEquals(105, index.lookup(105).index());
+ assertEquals(10, index.lookup(105).position());
+ index.index(106, 12);
+ index.index(107, 14);
+ index.index(108, 16);
+ assertEquals(105, index.lookup(108).index());
+ assertEquals(10, index.lookup(108).position());
+ index.index(109, 18);
+ index.index(110, 20);
+ assertEquals(110, index.lookup(110).index());
+ assertEquals(20, index.lookup(110).position());
+ index.truncate(108);
+ assertEquals(105, index.lookup(108).index());
+ assertEquals(10, index.lookup(108).position());
+ assertEquals(105, index.lookup(110).index());
+ assertEquals(10, index.lookup(110).position());
+ index.truncate(104);
+ assertNull(index.lookup(104));
+ assertNull(index.lookup(108));
+ }
+}
--- /dev/null
+<!--
+ ~ Copyright 2017-present Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<configuration>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <logger name="io.atomix.storage" level="INFO" />
+
+ <root level="${root.logging.level:-INFO}">
+ <appender-ref ref="STDOUT" />
+ </root>
+</configuration>
\ No newline at end of file
--- /dev/null
+<!--
+ ~ Copyright 2017-present Open Networking Foundation
+ ~
+ ~ 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.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>io.atomix</groupId>
+ <artifactId>atomix-parent</artifactId>
+ <version>3.2.0-SNAPSHOT</version>
+ </parent>
+
+ <packaging>bundle</packaging>
+ <artifactId>atomix-utils</artifactId>
+ <name>Atomix Utilities</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>${guava.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>${commons.lang3.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-math3</artifactId>
+ <version>${commons.math3.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.esotericsoftware</groupId>
+ <artifactId>kryo</artifactId>
+ <version>${kryo.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe</groupId>
+ <artifactId>config</artifactId>
+ <version>${config.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.github.classgraph</groupId>
+ <artifactId>classgraph</artifactId>
+ <version>${classgraph.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Export-Package>
+ io.atomix.utils.*
+ </Export-Package>
+ <Import-Package>
+ sun.nio.ch;resolution:=optional,sun.misc;resolution:=optional,*
+ </Import-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
--- /dev/null
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Abstract identifier backed by another value, e.g. string, int.
+ */
+public class AbstractIdentifier<T extends Comparable<T>> implements Identifier<T> {
+
+ protected final T identifier; // backing identifier value
+
+ /**
+ * Constructor for serialization.
+ */
+ protected AbstractIdentifier() {
+ this.identifier = null;
+ }
+
+ /**
+ * Constructs an identifier backed by the specified value.
+ *
+ * @param value the backing value
+ */
+ protected AbstractIdentifier(T value) {
+ this.identifier = checkNotNull(value, "Identifier cannot be null.");
+ }
+
+ /**
+ * Returns the backing identifier value.
+ *
+ * @return identifier
+ */
+ public T id() {
+ return identifier;
+ }
+
+ /**
+ * Returns the hashcode of the identifier.
+ *
+ * @return hashcode
+ */
+ @Override
+ public int hashCode() {
+ return identifier.hashCode();
+ }
+
+ /**
+ * Compares two device key identifiers for equality.
+ *
+ * @param obj to compare against
+ * @return true if the objects are equal, false otherwise.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof AbstractIdentifier) {
+ AbstractIdentifier that = (AbstractIdentifier) obj;
+ return this.getClass() == that.getClass()
+ && Objects.equals(this.identifier, that.identifier);
+ }
+ return false;
+ }
+
+ /**
+ * Returns a string representation of a DeviceKeyId.
+ *
+ * @return string
+ */
+ public String toString() {
+ return identifier.toString();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Abstract named object.
+ */
+public abstract class AbstractNamed implements Named {
+ private String name;
+
+ protected AbstractNamed() {
+ this(null);
+ }
+
+ protected AbstractNamed(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Atomix I/O exception.
+ */
+public class AtomixIOException extends AtomixRuntimeException {
+ public AtomixIOException() {
+ }
+
+ public AtomixIOException(String message) {
+ super(message);
+ }
+
+ public AtomixIOException(String message, Object... args) {
+ super(message, args);
+ }
+
+ public AtomixIOException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AtomixIOException(Throwable cause) {
+ super(cause);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Atomix runtime exception.
+ */
+public class AtomixRuntimeException extends RuntimeException {
+ public AtomixRuntimeException() {
+ }
+
+ public AtomixRuntimeException(String message) {
+ super(message);
+ }
+
+ public AtomixRuntimeException(String message, Object... args) {
+ super(String.format(message, args));
+ }
+
+ public AtomixRuntimeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AtomixRuntimeException(Throwable cause) {
+ super(cause);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Object builder.
+ * <p>
+ * This is a base interface for building objects in Catalyst.
+ *
+ * @param <T> type to build
+ */
+public interface Builder<T> {
+
+ /**
+ * Builds the object.
+ * <p>
+ * The returned object may be a new instance of the built class or a recycled instance, depending on the semantics
+ * of the builder implementation. Users should never assume that a builder allocates a new instance.
+ *
+ * @return The built object.
+ */
+ T build();
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import io.atomix.utils.config.TypedConfig;
+
+/**
+ * Configured type.
+ */
+public interface ConfiguredType<C extends TypedConfig> extends NamedType {
+
+ /**
+ * Returns a new configuration.
+ *
+ * @return a new configuration
+ */
+ C newConfig();
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+/**
+ * Generics utility.
+ */
+public class Generics {
+
+ /**
+ * Returns the generic type at the given position for the given class.
+ *
+ * @param instance the implementing instance
+ * @param clazz the generic class
+ * @param position the generic position
+ * @return the generic type at the given position
+ */
+ public static Type getGenericClassType(Object instance, Class<?> clazz, int position) {
+ Class<?> type = instance.getClass();
+ while (type != Object.class) {
+ if (type.getGenericSuperclass() instanceof ParameterizedType) {
+ ParameterizedType genericSuperclass = (ParameterizedType) type.getGenericSuperclass();
+ if (genericSuperclass.getRawType() == clazz) {
+ return genericSuperclass.getActualTypeArguments()[position];
+ } else {
+ type = type.getSuperclass();
+ }
+ } else {
+ type = type.getSuperclass();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the generic type at the given position for the given interface.
+ *
+ * @param instance the implementing instance
+ * @param iface the generic interface
+ * @param position the generic position
+ * @return the generic type at the given position
+ */
+ public static Type getGenericInterfaceType(Object instance, Class<?> iface, int position) {
+ Class<?> type = instance.getClass();
+ while (type != Object.class) {
+ for (Type genericType : type.getGenericInterfaces()) {
+ if (genericType instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) genericType;
+ if (parameterizedType.getRawType() == iface) {
+ return parameterizedType.getActualTypeArguments()[position];
+ }
+ }
+ }
+ type = type.getSuperclass();
+ }
+ return null;
+ }
+
+ private Generics() {
+ }
+}
--- /dev/null
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Abstract identifier backed by another value, e.g. string, int.
+ */
+public interface Identifier<T extends Comparable<T>> {
+
+ /**
+ * Returns the backing identifier value.
+ *
+ * @return identifier
+ */
+ T id();
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Interface for types that can be asynchronously started and stopped.
+ *
+ * @param <T> managed type
+ */
+public interface Managed<T> {
+
+ /**
+ * Starts the managed object.
+ *
+ * @return A completable future to be completed once the object has been started.
+ */
+ CompletableFuture<T> start();
+
+ /**
+ * Returns a boolean value indicating whether the managed object is running.
+ *
+ * @return Indicates whether the managed object is running.
+ */
+ boolean isRunning();
+
+ /**
+ * Stops the managed object.
+ *
+ * @return A completable future to be completed once the object has been stopped.
+ */
+ CompletableFuture<Void> stop();
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Named object.
+ */
+public interface Named {
+
+ /**
+ * Returns the object name.
+ *
+ * @return the object name
+ */
+ String name();
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Named type.
+ */
+public interface NamedType extends Named, Type {
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Service exception.
+ */
+public class ServiceException extends AtomixRuntimeException {
+ public ServiceException() {
+ }
+
+ public ServiceException(String message) {
+ super(message);
+ }
+
+ public ServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+/**
+ * Identifier interface for types.
+ */
+public interface Type {
+
+ /**
+ * Returns the type name.
+ *
+ * @return the type name
+ */
+ String name();
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import com.google.common.collect.ComparisonChain;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.Integer.parseInt;
+
+/**
+ * Atomix software version.
+ */
+public final class Version implements Comparable<Version> {
+
+ /**
+ * Returns a new version from the given version string.
+ *
+ * @param version the version string
+ * @return the version object
+ * @throws IllegalArgumentException if the version string is invalid
+ */
+ public static Version from(String version) {
+ String[] fields = version.split("[.-]", 4);
+ checkArgument(fields.length >= 3, "version number is invalid");
+ return new Version(
+ parseInt(fields[0]),
+ parseInt(fields[1]),
+ parseInt(fields[2]),
+ fields.length == 4 ? fields[3] : null);
+ }
+
+ /**
+ * Returns a new version from the given parts.
+ *
+ * @param major the major version number
+ * @param minor the minor version number
+ * @param patch the patch version number
+ * @param build the build version number
+ * @return the version object
+ */
+ public static Version from(int major, int minor, int patch, String build) {
+ return new Version(major, minor, patch, build);
+ }
+
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final String build;
+
+ private Version(int major, int minor, int patch, String build) {
+ checkArgument(major >= 0, "major version must be >= 0");
+ checkArgument(minor >= 0, "minor version must be >= 0");
+ checkArgument(patch >= 0, "patch version must be >= 0");
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.build = Build.from(build).toString();
+ }
+
+ /**
+ * Returns the major version number.
+ *
+ * @return the major version number
+ */
+ public int major() {
+ return major;
+ }
+
+ /**
+ * Returns the minor version number.
+ *
+ * @return the minor version number
+ */
+ public int minor() {
+ return minor;
+ }
+
+ /**
+ * Returns the patch version number.
+ *
+ * @return the patch version number
+ */
+ public int patch() {
+ return patch;
+ }
+
+ /**
+ * Returns the build version number.
+ *
+ * @return the build version number
+ */
+ public String build() {
+ return build;
+ }
+
+ @Override
+ public int compareTo(Version that) {
+ return ComparisonChain.start()
+ .compare(this.major, that.major)
+ .compare(this.minor, that.minor)
+ .compare(this.patch, that.patch)
+ .compare(Build.from(this.build), Build.from(that.build))
+ .result();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(major, minor, patch, build);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object == this) {
+ return true;
+ }
+ if (!(object instanceof Version)) {
+ return false;
+ }
+ Version that = (Version) object;
+ return this.major == that.major
+ && this.minor == that.minor
+ && this.patch == that.patch
+ && Objects.equals(this.build, that.build);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder()
+ .append(major)
+ .append('.')
+ .append(minor)
+ .append('.')
+ .append(patch);
+ String build = Build.from(this.build).toString();
+ if (build != null) {
+ builder.append('-').append(build);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Build version.
+ */
+ private static class Build implements Comparable<Build> {
+
+ /**
+ * Creates a new build version from the given string.
+ *
+ * @param build the build version string
+ * @return the build version
+ * @throws IllegalArgumentException if the build version string is invalid
+ */
+ public static Build from(String build) {
+ if (build == null) {
+ return new Build(Type.FINAL, 0);
+ } else if (build.equalsIgnoreCase(Type.SNAPSHOT.name())) {
+ return new Build(Type.SNAPSHOT, 0);
+ }
+
+ for (Type type : Type.values()) {
+ if (type.name != null && build.length() >= type.name.length() && build.substring(0, type.name.length()).equalsIgnoreCase(type.name)) {
+ try {
+ int version = parseInt(build.substring(type.name.length()));
+ return new Build(type, version);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(build + " is not a valid build version string");
+ }
+ }
+ }
+ throw new IllegalArgumentException(build + " is not a valid build version string");
+ }
+
+ private final Type type;
+ private final int version;
+
+ private Build(Type type, int version) {
+ this.type = type;
+ this.version = version;
+ }
+
+ @Override
+ public int compareTo(Build that) {
+ return ComparisonChain.start()
+ .compare(this.type.ordinal(), that.type.ordinal())
+ .compare(this.version, that.version)
+ .result();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, version);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object == this) {
+ return true;
+ }
+ if (!(object instanceof Build)) {
+ return false;
+ }
+ Build that = (Build) object;
+ return Objects.equals(this.type, that.type) && this.version == that.version;
+ }
+
+ @Override
+ public String toString() {
+ return type.format(version);
+ }
+
+ /**
+ * Build type.
+ */
+ private enum Type {
+ SNAPSHOT("snapshot"),
+ ALPHA("alpha"),
+ BETA("beta"),
+ RC("rc"),
+ FINAL(null);
+
+ private final String name;
+
+ Type(String name) {
+ this.name = name;
+ }
+
+ String format(int version) {
+ if (name == null) {
+ return null;
+ } else if ("snapshot".equals(name)) {
+ return "SNAPSHOT";
+ } else {
+ return String.format("%s%d", name, version);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Base implementation of an item accumulator. It allows triggering based on
+ * item inter-arrival time threshold, maximum batch life threshold and maximum
+ * batch size.
+ */
+public abstract class AbstractAccumulator<T> implements Accumulator<T> {
+
+ private Logger log = LoggerFactory.getLogger(AbstractAccumulator.class);
+
+ private final Timer timer;
+ private final int maxItems;
+ private final int maxBatchMillis;
+ private final int maxIdleMillis;
+
+ private final AtomicReference<TimerTask> idleTask = new AtomicReference<>();
+ private final AtomicReference<TimerTask> maxTask = new AtomicReference<>();
+
+ private final List<T> items;
+
+ /**
+ * Creates an item accumulator capable of triggering on the specified
+ * thresholds.
+ *
+ * @param timer timer to use for scheduling check-points
+ * @param maxItems maximum number of items to accumulate before
+ * processing is triggered
+ * <p>
+ * NB: It is possible that processItems will contain
+ * more than maxItems under high load or if isReady()
+ * can return false.
+ * </p>
+ * @param maxBatchMillis maximum number of millis allowed since the first
+ * item before processing is triggered
+ * @param maxIdleMillis maximum number millis between items before
+ * processing is triggered
+ */
+ protected AbstractAccumulator(Timer timer, int maxItems,
+ int maxBatchMillis, int maxIdleMillis) {
+ this.timer = checkNotNull(timer, "Timer cannot be null");
+
+ checkArgument(maxItems > 1, "Maximum number of items must be > 1");
+ checkArgument(maxBatchMillis > 0, "Maximum millis must be positive");
+ checkArgument(maxIdleMillis > 0, "Maximum idle millis must be positive");
+
+ this.maxItems = maxItems;
+ this.maxBatchMillis = maxBatchMillis;
+ this.maxIdleMillis = maxIdleMillis;
+
+ items = Lists.newArrayListWithExpectedSize(maxItems);
+ }
+
+ @Override
+ public void add(T item) {
+ final int sizeAtTimeOfAdd;
+ synchronized (items) {
+ items.add(item);
+ sizeAtTimeOfAdd = items.size();
+ }
+
+ /*
+ WARNING: It is possible that the item that was just added to the list
+ has been processed by an existing idle task at this point.
+
+ By rescheduling the following timers, it is possible that a
+ superfluous maxTask is generated now OR that the idle task and max
+ task are scheduled at their specified delays. This could result in
+ calls to processItems sooner than expected.
+ */
+
+ // Did we hit the max item threshold?
+ if (sizeAtTimeOfAdd >= maxItems) {
+ if (maxIdleMillis < maxBatchMillis) {
+ cancelTask(idleTask);
+ }
+ rescheduleTask(maxTask, 0 /* now! */);
+ } else {
+ // Otherwise, schedule idle task and if this is a first item
+ // also schedule the max batch age task.
+ if (maxIdleMillis < maxBatchMillis) {
+ rescheduleTask(idleTask, maxIdleMillis);
+ }
+ if (sizeAtTimeOfAdd == 1) {
+ rescheduleTask(maxTask, maxBatchMillis);
+ }
+ }
+ }
+
+ /**
+ * Reschedules the specified task, cancelling existing one if applicable.
+ *
+ * @param taskRef task reference
+ * @param millis delay in milliseconds
+ */
+ private void rescheduleTask(AtomicReference<TimerTask> taskRef, long millis) {
+ ProcessorTask newTask = new ProcessorTask();
+ timer.schedule(newTask, millis);
+ swapAndCancelTask(taskRef, newTask);
+ }
+
+ /**
+ * Cancels the specified task if it has not run or is not running.
+ *
+ * @param taskRef task reference
+ */
+ private void cancelTask(AtomicReference<TimerTask> taskRef) {
+ swapAndCancelTask(taskRef, null);
+ }
+
+ /**
+ * Sets the new task and attempts to cancelTask the old one.
+ *
+ * @param taskRef task reference
+ * @param newTask new task
+ */
+ private void swapAndCancelTask(AtomicReference<TimerTask> taskRef,
+ TimerTask newTask) {
+ TimerTask oldTask = taskRef.getAndSet(newTask);
+ if (oldTask != null) {
+ oldTask.cancel();
+ }
+ }
+
+ // Task for triggering processing of accumulated items
+ private class ProcessorTask extends TimerTask {
+ @Override
+ public void run() {
+ try {
+ if (isReady()) {
+
+ List<T> batch = finalizeCurrentBatch();
+ if (!batch.isEmpty()) {
+ processItems(batch);
+ }
+ } else {
+ rescheduleTask(idleTask, maxIdleMillis);
+ }
+ } catch (Exception e) {
+ log.warn("Unable to process batch due to", e);
+ }
+ }
+ }
+
+ /**
+ * Returns an immutable copy of the existing items and clear the list.
+ *
+ * @return list of existing items
+ */
+ private List<T> finalizeCurrentBatch() {
+ List<T> finalizedList;
+ synchronized (items) {
+ finalizedList = ImmutableList.copyOf(items);
+ items.clear();
+ /*
+ * To avoid reprocessing being triggered on an empty list.
+ */
+ cancelTask(maxTask);
+ cancelTask(idleTask);
+ }
+ return finalizedList;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ /**
+ * Returns the backing timer.
+ *
+ * @return backing timer
+ */
+ public Timer timer() {
+ return timer;
+ }
+
+ /**
+ * Returns the maximum number of items allowed to accumulate before
+ * processing is triggered.
+ *
+ * @return max number of items
+ */
+ public int maxItems() {
+ return maxItems;
+ }
+
+ /**
+ * Returns the maximum number of millis allowed to expire since the first
+ * item before processing is triggered.
+ *
+ * @return max number of millis a batch is allowed to last
+ */
+ public int maxBatchMillis() {
+ return maxBatchMillis;
+ }
+
+ /**
+ * Returns the maximum number of millis allowed to expire since the last
+ * item arrival before processing is triggered.
+ *
+ * @return max number of millis since the last item
+ */
+ public int maxIdleMillis() {
+ return maxIdleMillis;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+/**
+ * Abstract thread context.
+ */
+public abstract class AbstractThreadContext implements ThreadContext {
+ private volatile boolean blocked;
+
+ @Override
+ public boolean isBlocked() {
+ return blocked;
+ }
+
+ @Override
+ public void block() {
+ blocked = true;
+ }
+
+ @Override
+ public void unblock() {
+ blocked = false;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.List;
+
+/**
+ * Abstraction of an accumulator capable of collecting items and at some
+ * point in time triggers processing of all previously accumulated items.
+ *
+ * @param <T> item type
+ */
+public interface Accumulator<T> {
+
+ /**
+ * Adds an item to the current batch. This operation may, or may not
+ * trigger processing of the current batch of items.
+ *
+ * @param item item to be added to the current batch
+ */
+ void add(T item);
+
+ /**
+ * Processes the specified list of accumulated items.
+ *
+ * @param items list of accumulated items
+ */
+ void processItems(List<T> items);
+
+ /**
+ * Indicates whether the accumulator is ready to process items.
+ *
+ * @return true if ready to process
+ */
+ boolean isReady();
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * A {@link CompletableFuture} that tracks whether the future or one of its descendants has been blocked on
+ * a {@link CompletableFuture#get()} or {@link CompletableFuture#join()} call.
+ */
+public class AtomixFuture<T> extends CompletableFuture<T> {
+
+ /**
+ * Wraps the given future in a new blockable future.
+ *
+ * @param future the future to wrap
+ * @param <T> the future value type
+ * @return a new blockable future
+ */
+ public static <T> AtomixFuture<T> wrap(CompletableFuture<T> future) {
+ AtomixFuture<T> newFuture = new AtomixFuture<>();
+ future.whenComplete((result, error) -> {
+ if (error == null) {
+ newFuture.complete(result);
+ } else {
+ newFuture.completeExceptionally(error);
+ }
+ });
+ return newFuture;
+ }
+
+ /**
+ * Returns a new completed Atomix future.
+ *
+ * @param result the future result
+ * @param <T> the future result type
+ * @return the completed future
+ */
+ public static <T> CompletableFuture<T> completedFuture(T result) {
+ CompletableFuture<T> future = new AtomixFuture<>();
+ future.complete(result);
+ return future;
+ }
+
+ /**
+ * Returns a new exceptionally completed Atomix future.
+ *
+ * @param t the future exception
+ * @param <T> the future result type
+ * @return the completed future
+ */
+ public static <T> CompletableFuture<T> exceptionalFuture(Throwable t) {
+ CompletableFuture<T> future = new AtomixFuture<>();
+ future.completeExceptionally(t);
+ return future;
+ }
+
+ private static final ThreadContext NULL_CONTEXT = new NullThreadContext();
+
+ private ThreadContext getThreadContext() {
+ ThreadContext context = ThreadContext.currentContext();
+ return context != null ? context : NULL_CONTEXT;
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ ThreadContext context = getThreadContext();
+ context.block();
+ try {
+ return super.get();
+ } finally {
+ context.unblock();
+ }
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ ThreadContext context = getThreadContext();
+ context.block();
+ try {
+ return super.get(timeout, unit);
+ } finally {
+ context.unblock();
+ }
+ }
+
+ @Override
+ public synchronized T join() {
+ ThreadContext context = getThreadContext();
+ context.block();
+ try {
+ return super.join();
+ } finally {
+ context.unblock();
+ }
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {
+ return wrap(super.thenApply(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn) {
+ return wrap(super.thenApplyAsync(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor) {
+ return wrap(super.thenApplyAsync(fn, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
+ return wrap(super.thenAccept(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
+ return wrap(super.thenAcceptAsync(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor) {
+ return wrap(super.thenAcceptAsync(action, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenRun(Runnable action) {
+ return wrap(super.thenRun(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenRunAsync(Runnable action) {
+ return wrap(super.thenRunAsync(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor) {
+ return wrap(super.thenRunAsync(action, executor));
+ }
+
+ @Override
+ public <U, V> CompletableFuture<V> thenCombine(
+ CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) {
+ return wrap(super.thenCombine(other, fn));
+ }
+
+ @Override
+ public <U, V> CompletableFuture<V> thenCombineAsync(
+ CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) {
+ return wrap(super.thenCombineAsync(other, fn));
+ }
+
+ @Override
+ public <U, V> CompletableFuture<V> thenCombineAsync(
+ CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn, Executor executor) {
+ return wrap(super.thenCombineAsync(other, fn, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<Void> thenAcceptBoth(
+ CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action) {
+ return wrap(super.thenAcceptBoth(other, action));
+ }
+
+ @Override
+ public <U> CompletableFuture<Void> thenAcceptBothAsync(
+ CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action) {
+ return wrap(super.thenAcceptBothAsync(other, action));
+ }
+
+ @Override
+ public <U> CompletableFuture<Void> thenAcceptBothAsync(
+ CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor) {
+ return wrap(super.thenAcceptBothAsync(other, action, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action) {
+ return wrap(super.runAfterBoth(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action) {
+ return wrap(super.runAfterBothAsync(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor) {
+ return wrap(super.runAfterBothAsync(other, action, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn) {
+ return wrap(super.applyToEither(other, fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn) {
+ return wrap(super.applyToEitherAsync(other, fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> applyToEitherAsync(
+ CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor) {
+ return wrap(super.applyToEitherAsync(other, fn, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action) {
+ return wrap(super.acceptEither(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action) {
+ return wrap(super.acceptEitherAsync(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> acceptEitherAsync(
+ CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor) {
+ return wrap(super.acceptEitherAsync(other, action, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action) {
+ return wrap(super.runAfterEither(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action) {
+ return wrap(super.runAfterEitherAsync(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor) {
+ return wrap(super.runAfterEitherAsync(other, action, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) {
+ return wrap(super.thenCompose(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) {
+ return wrap(super.thenComposeAsync(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenComposeAsync(
+ Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) {
+ return wrap(super.thenComposeAsync(fn, executor));
+ }
+
+ @Override
+ public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {
+ return wrap(super.whenComplete(action));
+ }
+
+ @Override
+ public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action) {
+ return wrap(super.whenCompleteAsync(action));
+ }
+
+ @Override
+ public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor) {
+ return wrap(super.whenCompleteAsync(action, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) {
+ return wrap(super.handle(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) {
+ return wrap(super.handleAsync(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) {
+ return wrap(super.handleAsync(fn, executor));
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Atomix thread.
+ * <p>
+ * The Atomix thread primarily serves to store a {@link ThreadContext} for the current thread.
+ * The context is stored in a {@link WeakReference} in order to allow the thread to be garbage collected.
+ * <p>
+ * There is no {@link ThreadContext} associated with the thread when it is first created.
+ * It is the responsibility of thread creators to {@link #setContext(ThreadContext) set} the thread context when appropriate.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class AtomixThread extends Thread {
+ private WeakReference<ThreadContext> context;
+
+ public AtomixThread(Runnable target) {
+ super(target);
+ }
+
+ /**
+ * Sets the thread context.
+ *
+ * @param context The thread context.
+ */
+ public void setContext(ThreadContext context) {
+ this.context = new WeakReference<>(context);
+ }
+
+ /**
+ * Returns the thread context.
+ *
+ * @return The thread {@link ThreadContext} or {@code null} if no context has been configured.
+ */
+ public ThreadContext getContext() {
+ return context != null ? context.get() : null;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Named thread factory.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class AtomixThreadFactory implements ThreadFactory {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new AtomixThread(r);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadFactory;
+
+import static io.atomix.utils.concurrent.Threads.namedThreads;
+
+/**
+ * Blocking aware single thread context.
+ */
+public class BlockingAwareSingleThreadContext extends SingleThreadContext {
+ private final Executor threadPoolExecutor;
+
+ public BlockingAwareSingleThreadContext(String nameFormat, Executor threadPoolExecutor) {
+ this(namedThreads(nameFormat, LOGGER), threadPoolExecutor);
+ }
+
+ public BlockingAwareSingleThreadContext(ThreadFactory factory, Executor threadPoolExecutor) {
+ super(factory);
+ this.threadPoolExecutor = threadPoolExecutor;
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (isBlocked()) {
+ threadPoolExecutor.execute(command);
+ } else {
+ super.execute(command);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import org.slf4j.Logger;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static io.atomix.utils.concurrent.Threads.namedThreads;
+
+/**
+ * Single thread context factory.
+ */
+public class BlockingAwareSingleThreadContextFactory implements ThreadContextFactory {
+ private final ThreadFactory threadFactory;
+ private final Executor threadPoolExecutor;
+
+ public BlockingAwareSingleThreadContextFactory(String nameFormat, int threadPoolSize, Logger logger) {
+ this(threadPoolSize, namedThreads(nameFormat, logger));
+ }
+
+ public BlockingAwareSingleThreadContextFactory(int threadPoolSize, ThreadFactory threadFactory) {
+ this(threadFactory, Executors.newScheduledThreadPool(threadPoolSize, threadFactory));
+ }
+
+ public BlockingAwareSingleThreadContextFactory(ThreadFactory threadFactory, Executor threadPoolExecutor) {
+ this.threadFactory = checkNotNull(threadFactory);
+ this.threadPoolExecutor = checkNotNull(threadPoolExecutor);
+ }
+
+ @Override
+ public ThreadContext createContext() {
+ return new BlockingAwareSingleThreadContext(threadFactory, threadPoolExecutor);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Blocking aware thread pool context.
+ */
+public class BlockingAwareThreadPoolContext extends ThreadPoolContext {
+ public BlockingAwareThreadPoolContext(ScheduledExecutorService parent) {
+ super(parent);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (isBlocked()) {
+ parent.execute(command);
+ } else {
+ super.execute(command);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import org.slf4j.Logger;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+
+import static io.atomix.utils.concurrent.Threads.namedThreads;
+
+/**
+ * Thread pool context factory.
+ */
+public class BlockingAwareThreadPoolContextFactory implements ThreadContextFactory {
+ private final ScheduledExecutorService executor;
+
+ public BlockingAwareThreadPoolContextFactory(String name, int threadPoolSize, Logger logger) {
+ this(threadPoolSize, namedThreads(name, logger));
+ }
+
+ public BlockingAwareThreadPoolContextFactory(int threadPoolSize, ThreadFactory threadFactory) {
+ this(Executors.newScheduledThreadPool(threadPoolSize, threadFactory));
+ }
+
+ public BlockingAwareThreadPoolContextFactory(ScheduledExecutorService executor) {
+ this.executor = executor;
+ }
+
+ @Override
+ public ThreadContext createContext() {
+ return new BlockingAwareThreadPoolContext(executor);
+ }
+
+ @Override
+ public void close() {
+ executor.shutdownNow();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Special implementation of {@link CompletableFuture} with missing utility methods.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class ComposableFuture<T> extends CompletableFuture<T> implements BiConsumer<T, Throwable> {
+
+ @Override
+ public void accept(T result, Throwable error) {
+ if (error == null) {
+ complete(result);
+ } else {
+ completeExceptionally(error);
+ }
+ }
+
+ /**
+ * Sets a consumer to be called when the future is failed.
+ *
+ * @param consumer The consumer to call.
+ * @return A new future.
+ */
+ public CompletableFuture<T> except(Consumer<Throwable> consumer) {
+ return whenComplete((result, error) -> {
+ if (error != null) {
+ consumer.accept(error);
+ }
+ });
+ }
+
+ /**
+ * Sets a consumer to be called asynchronously when the future is failed.
+ *
+ * @param consumer The consumer to call.
+ * @return A new future.
+ */
+ public CompletableFuture<T> exceptAsync(Consumer<Throwable> consumer) {
+ return whenCompleteAsync((result, error) -> {
+ if (error != null) {
+ consumer.accept(error);
+ }
+ });
+ }
+
+ /**
+ * Sets a consumer to be called asynchronously when the future is failed.
+ *
+ * @param consumer The consumer to call.
+ * @param executor The executor with which to call the consumer.
+ * @return A new future.
+ */
+ public CompletableFuture<T> exceptAsync(Consumer<Throwable> consumer, Executor executor) {
+ return whenCompleteAsync((result, error) -> {
+ if (error != null) {
+ consumer.accept(error);
+ }
+ }, executor);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BinaryOperator;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Utilities for creating completed and exceptional futures.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public final class Futures {
+
+ /**
+ * Gets a future result with a default timeout.
+ *
+ * @param future the future to block
+ * @param <T> the future result type
+ * @return the future result
+ * @throws RuntimeException if a future exception occurs
+ */
+ public static <T> T get(Future<T> future) {
+ return get(future, 30, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Gets a future result with a default timeout.
+ *
+ * @param future the future to block
+ * @param timeout the future timeout
+ * @param timeUnit the future timeout time unit
+ * @param <T> the future result type
+ * @return the future result
+ * @throws RuntimeException if a future exception occurs
+ */
+ public static <T> T get(Future<T> future, long timeout, TimeUnit timeUnit) {
+ try {
+ return future.get(timeout, timeUnit);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Creates a future that is synchronously completed.
+ *
+ * @param result The future result.
+ * @return The completed future.
+ */
+ public static <T> CompletableFuture<T> completedFuture(T result) {
+ return CompletableFuture.completedFuture(result);
+ }
+
+ /**
+ * Creates a future that is asynchronously completed.
+ *
+ * @param result The future result.
+ * @param executor The executor on which to complete the future.
+ * @return The completed future.
+ */
+ public static <T> CompletableFuture<T> completedFutureAsync(T result, Executor executor) {
+ CompletableFuture<T> future = new CompletableFuture<>();
+ executor.execute(() -> future.complete(result));
+ return future;
+ }
+
+ /**
+ * Creates a future that is synchronously completed exceptionally.
+ *
+ * @param t The future exception.
+ * @return The exceptionally completed future.
+ */
+ public static <T> CompletableFuture<T> exceptionalFuture(Throwable t) {
+ CompletableFuture<T> future = new CompletableFuture<>();
+ future.completeExceptionally(t);
+ return future;
+ }
+
+ /**
+ * Creates a future that is asynchronously completed exceptionally.
+ *
+ * @param t The future exception.
+ * @param executor The executor on which to complete the future.
+ * @return The exceptionally completed future.
+ */
+ public static <T> CompletableFuture<T> exceptionalFutureAsync(Throwable t, Executor executor) {
+ CompletableFuture<T> future = new CompletableFuture<>();
+ executor.execute(() -> {
+ future.completeExceptionally(t);
+ });
+ return future;
+ }
+
+ /**
+ * Returns a future that completes callbacks in add order.
+ *
+ * @param <T> future value type
+ * @return a new completable future that will complete added callbacks in the order in which they were added
+ */
+ public static <T> CompletableFuture<T> orderedFuture() {
+ return new OrderedFuture<>();
+ }
+
+ /**
+ * Returns a future that completes callbacks in add order.
+ *
+ * @param <T> future value type
+ * @return a new completable future that will complete added callbacks in the order in which they were added
+ */
+ public static <T> CompletableFuture<T> orderedFuture(CompletableFuture<T> future) {
+ CompletableFuture<T> newFuture = new OrderedFuture<>();
+ future.whenComplete((r, e) -> {
+ if (e == null) {
+ newFuture.complete(r);
+ } else {
+ newFuture.completeExceptionally(e);
+ }
+ });
+ return newFuture;
+ }
+
+ /**
+ * Returns a wrapped future that will be completed on the given executor.
+ *
+ * @param future the future to be completed on the given executor
+ * @param executor the executor with which to complete the future
+ * @param <T> the future value type
+ * @return a wrapped future to be completed on the given executor
+ */
+ public static <T> CompletableFuture<T> asyncFuture(CompletableFuture<T> future, Executor executor) {
+ CompletableFuture<T> newFuture = new AtomixFuture<>();
+ future.whenComplete((result, error) -> {
+ executor.execute(() -> {
+ if (error == null) {
+ newFuture.complete(result);
+ } else {
+ newFuture.completeExceptionally(error);
+ }
+ });
+ });
+ return newFuture;
+ }
+
+ /**
+ * Returns a new CompletableFuture completed with a list of computed values
+ * when all of the given CompletableFuture complete.
+ *
+ * @param futures the CompletableFutures
+ * @param <T> value type of CompletableFuture
+ * @return a new CompletableFuture that is completed when all of the given CompletableFutures complete
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> CompletableFuture<Stream<T>> allOf(Stream<CompletableFuture<T>> futures) {
+ CompletableFuture<T>[] futuresArray = futures.toArray(CompletableFuture[]::new);
+ return AtomixFuture.wrap(CompletableFuture.allOf(futuresArray)
+ .thenApply(v -> Stream.of(futuresArray).map(CompletableFuture::join)));
+ }
+
+ /**
+ * Returns a new CompletableFuture completed with a list of computed values
+ * when all of the given CompletableFuture complete.
+ *
+ * @param futures the CompletableFutures
+ * @param <T> value type of CompletableFuture
+ * @return a new CompletableFuture that is completed when all of the given CompletableFutures complete
+ */
+ public static <T> CompletableFuture<List<T>> allOf(List<CompletableFuture<T>> futures) {
+ return AtomixFuture.wrap(CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]))
+ .thenApply(v -> futures.stream()
+ .map(CompletableFuture::join)
+ .collect(Collectors.toList())));
+ }
+
+ /**
+ * Returns a new CompletableFuture completed by reducing a list of computed values
+ * when all of the given CompletableFuture complete.
+ *
+ * @param futures the CompletableFutures
+ * @param reducer reducer for computing the result
+ * @param emptyValue zero value to be returned if the input future list is empty
+ * @param <T> value type of CompletableFuture
+ * @return a new CompletableFuture that is completed when all of the given CompletableFutures complete
+ */
+ public static <T> CompletableFuture<T> allOf(
+ List<CompletableFuture<T>> futures, BinaryOperator<T> reducer, T emptyValue) {
+ return allOf(futures).thenApply(resultList -> resultList.stream().reduce(reducer).orElse(emptyValue));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.time.Duration;
+
+/**
+ * Null thread context.
+ */
+public class NullThreadContext implements ThreadContext {
+ @Override
+ public Scheduled schedule(Duration delay, Runnable callback) {
+ return null;
+ }
+
+ @Override
+ public Scheduled schedule(Duration initialDelay, Duration interval, Runnable callback) {
+ return null;
+ }
+
+ @Override
+ public boolean isBlocked() {
+ return false;
+ }
+
+ @Override
+ public void block() {
+
+ }
+
+ @Override
+ public void unblock() {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public void execute(Runnable command) {
+
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.LinkedList;
+import java.util.concurrent.Executor;
+
+/**
+ * Executor that executes tasks in order on a shared thread pool.
+ * <p>
+ * The ordered executor behaves semantically like a single-threaded executor, but multiplexes tasks on a shared thread
+ * pool, ensuring blocked threads in the shared thread pool don't block individual ordered executors.
+ */
+public class OrderedExecutor implements Executor {
+ private final Executor parent;
+ private final LinkedList<Runnable> tasks = new LinkedList<>();
+ private boolean running;
+
+ public OrderedExecutor(Executor parent) {
+ this.parent = parent;
+ }
+
+ private void run() {
+ for (;;) {
+ final Runnable task;
+ synchronized (tasks) {
+ task = tasks.poll();
+ if (task == null) {
+ running = false;
+ return;
+ }
+ }
+ task.run();
+ }
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ synchronized (tasks) {
+ tasks.add(command);
+ if (!running) {
+ running = true;
+ parent.execute(this::run);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * A {@link CompletableFuture} that ensures callbacks are called in FIFO order.
+ * <p>
+ * The default {@link CompletableFuture} does not guarantee the ordering of callbacks, and indeed appears to
+ * execute them in LIFO order.
+ */
+public class OrderedFuture<T> extends CompletableFuture<T> {
+
+ /**
+ * Wraps the given future in a new blockable future.
+ *
+ * @param future the future to wrap
+ * @param <T> the future value type
+ * @return a new blockable future
+ */
+ public static <T> CompletableFuture<T> wrap(CompletableFuture<T> future) {
+ CompletableFuture<T> newFuture = new OrderedFuture<>();
+ future.whenComplete((result, error) -> {
+ if (error == null) {
+ newFuture.complete(result);
+ } else {
+ newFuture.completeExceptionally(error);
+ }
+ });
+ return newFuture;
+ }
+
+ private static final ThreadContext NULL_CONTEXT = new NullThreadContext();
+
+ private final Queue<CompletableFuture<T>> orderedFutures = new LinkedList<>();
+ private volatile boolean complete;
+ private volatile T result;
+ private volatile Throwable error;
+
+ public OrderedFuture() {
+ super.whenComplete(this::complete);
+ }
+
+ private ThreadContext getThreadContext() {
+ ThreadContext context = ThreadContext.currentContext();
+ return context != null ? context : NULL_CONTEXT;
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ ThreadContext context = getThreadContext();
+ context.block();
+ try {
+ return super.get();
+ } finally {
+ context.unblock();
+ }
+ }
+
+ @Override
+ public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ ThreadContext context = getThreadContext();
+ context.block();
+ try {
+ return super.get(timeout, unit);
+ } finally {
+ context.unblock();
+ }
+ }
+
+ @Override
+ public synchronized T join() {
+ ThreadContext context = getThreadContext();
+ context.block();
+ try {
+ return super.join();
+ } finally {
+ context.unblock();
+ }
+ }
+
+ /**
+ * Adds a new ordered future.
+ */
+ private CompletableFuture<T> orderedFuture() {
+ if (!complete) {
+ synchronized (orderedFutures) {
+ if (!complete) {
+ CompletableFuture<T> future = new CompletableFuture<>();
+ orderedFutures.add(future);
+ return future;
+ }
+ }
+ }
+
+ // Completed
+ if (error == null) {
+ return CompletableFuture.completedFuture(result);
+ } else {
+ return Futures.exceptionalFuture(error);
+ }
+ }
+
+ /**
+ * Completes futures in FIFO order.
+ */
+ private void complete(T result, Throwable error) {
+ synchronized (orderedFutures) {
+ this.result = result;
+ this.error = error;
+ this.complete = true;
+ if (error == null) {
+ for (CompletableFuture<T> future : orderedFutures) {
+ future.complete(result);
+ }
+ } else {
+ for (CompletableFuture<T> future : orderedFutures) {
+ future.completeExceptionally(error);
+ }
+ }
+ orderedFutures.clear();
+ }
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn) {
+ return wrap(orderedFuture().thenApply(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn) {
+ return wrap(orderedFuture().thenApplyAsync(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor) {
+ return wrap(orderedFuture().thenApplyAsync(fn, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
+ return wrap(orderedFuture().thenAccept(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
+ return wrap(orderedFuture().thenAcceptAsync(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor) {
+ return wrap(orderedFuture().thenAcceptAsync(action, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenRun(Runnable action) {
+ return wrap(orderedFuture().thenRun(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenRunAsync(Runnable action) {
+ return wrap(orderedFuture().thenRunAsync(action));
+ }
+
+ @Override
+ public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor) {
+ return wrap(orderedFuture().thenRunAsync(action, executor));
+ }
+
+ @Override
+ public <U, V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) {
+ return wrap(orderedFuture().thenCombine(other, fn));
+ }
+
+ @Override
+ public <U, V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn) {
+ return wrap(orderedFuture().thenCombineAsync(other, fn));
+ }
+
+ @Override
+ public <U, V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn, Executor executor) {
+ return wrap(orderedFuture().thenCombineAsync(other, fn, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action) {
+ return wrap(orderedFuture().thenAcceptBoth(other, action));
+ }
+
+ @Override
+ public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action) {
+ return wrap(orderedFuture().thenAcceptBothAsync(other, action));
+ }
+
+ @Override
+ public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor) {
+ return wrap(orderedFuture().thenAcceptBothAsync(other, action, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action) {
+ return wrap(orderedFuture().runAfterBoth(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action) {
+ return wrap(orderedFuture().runAfterBothAsync(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor) {
+ return wrap(orderedFuture().runAfterBothAsync(other, action, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn) {
+ return wrap(orderedFuture().applyToEither(other, fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn) {
+ return wrap(orderedFuture().applyToEitherAsync(other, fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor) {
+ return wrap(orderedFuture().applyToEitherAsync(other, fn, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action) {
+ return wrap(orderedFuture().acceptEither(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action) {
+ return wrap(orderedFuture().acceptEitherAsync(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor) {
+ return wrap(orderedFuture().acceptEitherAsync(other, action, executor));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action) {
+ return wrap(orderedFuture().runAfterEither(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action) {
+ return wrap(orderedFuture().runAfterEitherAsync(other, action));
+ }
+
+ @Override
+ public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor) {
+ return wrap(orderedFuture().runAfterEitherAsync(other, action, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) {
+ return wrap(orderedFuture().thenCompose(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) {
+ return wrap(orderedFuture().thenComposeAsync(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) {
+ return wrap(orderedFuture().thenComposeAsync(fn, executor));
+ }
+
+ @Override
+ public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {
+ return wrap(orderedFuture().whenComplete(action));
+ }
+
+ @Override
+ public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action) {
+ return wrap(orderedFuture().whenCompleteAsync(action));
+ }
+
+ @Override
+ public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor) {
+ return wrap(orderedFuture().whenCompleteAsync(action, executor));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) {
+ return wrap(orderedFuture().handle(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) {
+ return wrap(orderedFuture().handleAsync(fn));
+ }
+
+ @Override
+ public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) {
+ return wrap(orderedFuture().handleAsync(fn, executor));
+ }
+
+ @Override
+ public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn) {
+ return wrap(orderedFuture().exceptionally(fn));
+ }
+
+ @Override
+ public CompletableFuture<T> toCompletableFuture() {
+ return this;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+/**
+ * Reference counting interface.
+ * <p>
+ * Types that implement {@code ReferenceCounted} can be counted for references and thus used to clean up resources once
+ * a given instance of an object is no longer in use.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface ReferenceCounted<T> extends AutoCloseable {
+
+ /**
+ * Acquires a reference.
+ *
+ * @return The acquired reference.
+ */
+ T acquire();
+
+ /**
+ * Releases a reference.
+ *
+ * @return Indicates whether all references to the object have been released.
+ */
+ boolean release();
+
+ /**
+ * Returns the number of open references.
+ *
+ * @return The number of open references.
+ */
+ int references();
+
+ /**
+ * Defines an exception free close implementation.
+ */
+ @Override
+ void close();
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+/**
+ * Reference factory.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface ReferenceFactory<T extends ReferenceCounted<?>> {
+
+ /**
+ * Creates a new reference.
+ *
+ * @param manager The reference manager.
+ * @return The created reference.
+ */
+ T createReference(ReferenceManager<T> manager);
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+/**
+ * Reference manager. Manages {@link ReferenceCounted} objects.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface ReferenceManager<T> {
+
+ /**
+ * Releases the given reference.
+ * <p>
+ * This method should be called with a {@link ReferenceCounted} object that contains no
+ * additional references. This allows, for instance, pools to recycle dereferenced objects.
+ *
+ * @param reference The reference to release.
+ */
+ void release(T reference);
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * Pool of reference counted objects.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class ReferencePool<T extends ReferenceCounted<?>> implements ReferenceManager<T>, AutoCloseable {
+ private final ReferenceFactory<T> factory;
+ private final Queue<T> pool = new ConcurrentLinkedQueue<>();
+ private volatile boolean closed;
+
+ public ReferencePool(ReferenceFactory<T> factory) {
+ if (factory == null) {
+ throw new NullPointerException("factory cannot be null");
+ }
+ this.factory = factory;
+ }
+
+ /**
+ * Acquires a reference.
+ *
+ * @return The acquired reference.
+ */
+ public T acquire() {
+ if (closed) {
+ throw new IllegalStateException("pool closed");
+ }
+
+ T reference = pool.poll();
+ if (reference == null) {
+ reference = factory.createReference(this);
+ }
+ reference.acquire();
+ return reference;
+ }
+
+ @Override
+ public void release(T reference) {
+ if (!closed) {
+ pool.add(reference);
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ if (closed) {
+ throw new IllegalStateException("pool closed");
+ }
+
+ closed = true;
+ for (T reference : pool) {
+ reference.close();
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Retry utilities.
+ */
+public final class Retries {
+
+ /**
+ * Returns a function that retries execution on failure.
+ * @param base base function
+ * @param exceptionClass type of exception for which to retry
+ * @param maxRetries max number of retries before giving up
+ * @param maxDelayBetweenRetries max delay between successive retries. The actual delay is randomly picked from
+ * the interval (0, maxDelayBetweenRetries]
+ * @return function
+ * @param <U> type of function input
+ * @param <V> type of function output
+ */
+ public static <U, V> Function<U, V> retryable(Function<U, V> base,
+ Class<? extends Throwable> exceptionClass,
+ int maxRetries,
+ int maxDelayBetweenRetries) {
+ return new RetryingFunction<>(base, exceptionClass, maxRetries, maxDelayBetweenRetries);
+ }
+
+ /**
+ * Returns a Supplier that retries execution on failure.
+ * @param base base supplier
+ * @param exceptionClass type of exception for which to retry
+ * @param maxRetries max number of retries before giving up
+ * @param maxDelayBetweenRetries max delay between successive retries. The actual delay is randomly picked from
+ * the interval (0, maxDelayBetweenRetries]
+ * @return supplier
+ * @param <V> type of supplied result
+ */
+ public static <V> Supplier<V> retryable(Supplier<V> base,
+ Class<? extends Throwable> exceptionClass,
+ int maxRetries,
+ int maxDelayBetweenRetries) {
+ return () -> new RetryingFunction<>(v -> base.get(),
+ exceptionClass,
+ maxRetries,
+ maxDelayBetweenRetries).apply(null);
+ }
+
+ /**
+ * Suspends the current thread for a random number of millis between 0 and
+ * the indicated limit.
+ *
+ * @param ms max number of millis
+ */
+ public static void randomDelay(int ms) {
+ try {
+ Thread.sleep(ThreadLocalRandom.current().nextInt(ms));
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted", e);
+ }
+ }
+
+ /**
+ * Suspends the current thread for a specified number of millis and nanos.
+ *
+ * @param ms number of millis
+ * @param nanos number of nanos
+ */
+ public static void delay(int ms, int nanos) {
+ try {
+ Thread.sleep(ms, nanos);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted", e);
+ }
+ }
+
+ private Retries() {
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.function.Function;
+
+import static com.google.common.base.Throwables.throwIfUnchecked;
+
+/**
+ * Function that retries execution on failure.
+ *
+ * @param <U> input type
+ * @param <V> output type
+ */
+public class RetryingFunction<U, V> implements Function<U, V> {
+ private final Function<U, V> baseFunction;
+ private final Class<? extends Throwable> exceptionClass;
+ private final int maxRetries;
+ private final int maxDelayBetweenRetries;
+
+ public RetryingFunction(Function<U, V> baseFunction,
+ Class<? extends Throwable> exceptionClass,
+ int maxRetries,
+ int maxDelayBetweenRetries) {
+ this.baseFunction = baseFunction;
+ this.exceptionClass = exceptionClass;
+ this.maxRetries = maxRetries;
+ this.maxDelayBetweenRetries = maxDelayBetweenRetries;
+ }
+
+ @SuppressWarnings("squid:S1181")
+ // Yes we really do want to catch Throwable
+ @Override
+ public V apply(U input) {
+ int retryAttempts = 0;
+ while (true) {
+ try {
+ return baseFunction.apply(input);
+ } catch (Throwable t) {
+ if (!exceptionClass.isAssignableFrom(t.getClass()) || retryAttempts == maxRetries) {
+ throwIfUnchecked(t);
+ throw new RuntimeException(t);
+ }
+ Retries.randomDelay(maxDelayBetweenRetries);
+ retryAttempts++;
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+/**
+ * Scheduled task.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface Scheduled {
+
+ /**
+ * Cancels the scheduled task.
+ */
+ void cancel();
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Scheduler.
+ */
+public interface Scheduler {
+
+ /**
+ * Schedules a runnable after a delay.
+ *
+ * @param delay the delay after which to run the callback
+ * @param timeUnit the time unit
+ * @param callback the callback to run
+ * @return the scheduled callback
+ */
+ default Scheduled schedule(long delay, TimeUnit timeUnit, Runnable callback) {
+ return schedule(Duration.ofMillis(timeUnit.toMillis(delay)), callback);
+ }
+
+ /**
+ * Schedules a runnable after a delay.
+ *
+ * @param delay the delay after which to run the callback
+ * @param callback the callback to run
+ * @return the scheduled callback
+ */
+ Scheduled schedule(Duration delay, Runnable callback);
+
+ /**
+ * Schedules a runnable at a fixed rate.
+ *
+ * @param initialDelay the initial delay
+ * @param interval the interval at which to run the callback
+ * @param timeUnit the time unit
+ * @param callback the callback to run
+ * @return the scheduled callback
+ */
+ default Scheduled schedule(long initialDelay, long interval, TimeUnit timeUnit, Runnable callback) {
+ return schedule(Duration.ofMillis(timeUnit.toMillis(initialDelay)), Duration.ofMillis(timeUnit.toMillis(interval)), callback);
+ }
+
+ /**
+ * Schedules a runnable at a fixed rate.
+ *
+ * @param initialDelay the initial delay
+ * @param interval the interval at which to run the callback
+ * @param callback the callback to run
+ * @return the scheduled callback
+ */
+ Scheduled schedule(Duration initialDelay, Duration interval, Runnable callback);
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static com.google.common.base.Preconditions.checkState;
+import static io.atomix.utils.concurrent.Threads.namedThreads;
+
+/**
+ * Single threaded context.
+ * <p>
+ * This is a basic {@link ThreadContext} implementation that uses a
+ * {@link ScheduledExecutorService} to schedule events on the context thread.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class SingleThreadContext extends AbstractThreadContext {
+ protected static final Logger LOGGER = LoggerFactory.getLogger(SingleThreadContext.class);
+ private final ScheduledExecutorService executor;
+ private final Executor wrappedExecutor = new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ try {
+ executor.execute(() -> {
+ try {
+ command.run();
+ } catch (Exception e) {
+ LOGGER.error("An uncaught exception occurred", e);
+ }
+ });
+ } catch (RejectedExecutionException e) {
+ }
+ }
+ };
+
+ /**
+ * Creates a new single thread context.
+ * <p>
+ * The provided context name will be passed to {@link AtomixThreadFactory} and used
+ * when instantiating the context thread.
+ *
+ * @param nameFormat The context nameFormat which will be formatted with a thread number.
+ */
+ public SingleThreadContext(String nameFormat) {
+ this(namedThreads(nameFormat, LOGGER));
+ }
+
+ /**
+ * Creates a new single thread context.
+ *
+ * @param factory The thread factory.
+ */
+ public SingleThreadContext(ThreadFactory factory) {
+ this(new ScheduledThreadPoolExecutor(1, factory));
+ }
+
+ /**
+ * Creates a new single thread context.
+ *
+ * @param executor The executor on which to schedule events. This must be a single thread scheduled executor.
+ */
+ protected SingleThreadContext(ScheduledExecutorService executor) {
+ this(getThread(executor), executor);
+ }
+
+ private SingleThreadContext(Thread thread, ScheduledExecutorService executor) {
+ this.executor = executor;
+ checkState(thread instanceof AtomixThread, "not a Catalyst thread");
+ ((AtomixThread) thread).setContext(this);
+ }
+
+ /**
+ * Gets the thread from a single threaded executor service.
+ */
+ protected static AtomixThread getThread(ExecutorService executor) {
+ final AtomicReference<AtomixThread> thread = new AtomicReference<>();
+ try {
+ executor.submit(() -> {
+ thread.set((AtomixThread) Thread.currentThread());
+ }).get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new IllegalStateException("failed to initialize thread state", e);
+ }
+ return thread.get();
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ wrappedExecutor.execute(command);
+ }
+
+ @Override
+ public Scheduled schedule(Duration delay, Runnable runnable) {
+ ScheduledFuture<?> future = executor.schedule(runnable, delay.toMillis(), TimeUnit.MILLISECONDS);
+ return () -> future.cancel(false);
+ }
+
+ @Override
+ public Scheduled schedule(Duration delay, Duration interval, Runnable runnable) {
+ ScheduledFuture<?> future = executor.scheduleAtFixedRate(runnable, delay.toMillis(), interval.toMillis(), TimeUnit.MILLISECONDS);
+ return () -> future.cancel(false);
+ }
+
+ @Override
+ public void close() {
+ executor.shutdownNow();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import java.util.concurrent.Executor;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Thread context.
+ * <p>
+ * The thread context is used by Catalyst to determine the correct thread on which to execute asynchronous callbacks.
+ * All threads created within Catalyst must be instances of {@link AtomixThread}. Once
+ * a thread has been created, the context is stored in the thread object via
+ * {@link AtomixThread#setContext(ThreadContext)}. This means there is a one-to-one relationship
+ * between a context and a thread. That is, a context is representative of a thread and provides an interface for firing
+ * events on that thread.
+ * <p>
+ * Components of the framework that provide custom threads should use {@link AtomixThreadFactory}
+ * to allocate new threads and provide a custom {@link ThreadContext} implementation.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface ThreadContext extends AutoCloseable, Executor, Scheduler {
+
+ /**
+ * Returns the current thread context.
+ *
+ * @return The current thread context or {@code null} if no context exists.
+ */
+ static ThreadContext currentContext() {
+ Thread thread = Thread.currentThread();
+ return thread instanceof AtomixThread ? ((AtomixThread) thread).getContext() : null;
+ }
+
+ /**
+ * @throws IllegalStateException if the current thread is not a catalyst thread
+ */
+ static ThreadContext currentContextOrThrow() {
+ ThreadContext context = currentContext();
+ checkState(context != null, "not on a Catalyst thread");
+ return context;
+ }
+
+ /**
+ * Returns a boolean indicating whether the current thread is in this context.
+ *
+ * @return Indicates whether the current thread is in this context.
+ */
+ default boolean isCurrentContext() {
+ return currentContext() == this;
+ }
+
+ /**
+ * Checks that the current thread is the correct context thread.
+ */
+ default void checkThread() {
+ checkState(currentContext() == this, "not on a Catalyst thread");
+ }
+
+ /**
+ * Returns whether the thread context is currently marked blocked.
+ *
+ * @return whether the thread context is currently marked blocked
+ */
+ boolean isBlocked();
+
+ /**
+ * Marks the thread context as blocked.
+ */
+ void block();
+
+ /**
+ * Marks the thread context as unblocked.
+ */
+ void unblock();
+
+ /**
+ * Closes the context.
+ */
+ @Override
+ void close();
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+/**
+ * Thread context factory.
+ */
+public interface ThreadContextFactory {
+
+ /**
+ * Creates a new thread context.
+ *
+ * @return a new thread context
+ */
+ ThreadContext createContext();
+
+ /**
+ * Closes the factory.
+ */
+ default void close() {
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import org.slf4j.Logger;
+
+/**
+ * Raft thread model.
+ */
+public enum ThreadModel {
+
+ /**
+ * A thread model that creates a thread pool to be shared by all services.
+ */
+ SHARED_THREAD_POOL {
+ @Override
+ public ThreadContextFactory factory(String nameFormat, int threadPoolSize, Logger logger) {
+ return new BlockingAwareThreadPoolContextFactory(nameFormat, threadPoolSize, logger);
+ }
+ },
+
+ /**
+ * A thread model that creates a thread for each Raft service.
+ */
+ THREAD_PER_SERVICE {
+ @Override
+ public ThreadContextFactory factory(String nameFormat, int threadPoolSize, Logger logger) {
+ return new BlockingAwareSingleThreadContextFactory(nameFormat, threadPoolSize, logger);
+ }
+ };
+
+ /**
+ * Returns a thread context factory.
+ *
+ * @param nameFormat the thread name format
+ * @param threadPoolSize the thread pool size
+ * @param logger the thread logger
+ * @return the thread context factory
+ */
+ public abstract ThreadContextFactory factory(String nameFormat, int threadPoolSize, Logger logger);
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.LinkedList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Thread pool context.
+ * <p>
+ * This is a special {@link ThreadContext} implementation that schedules events to be executed
+ * on a thread pool. Events executed by this context are guaranteed to be executed on order but may be executed on different
+ * threads in the provided thread pool.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class ThreadPoolContext extends AbstractThreadContext {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolContext.class);
+ protected final ScheduledExecutorService parent;
+ private final Runnable runner;
+ private final LinkedList<Runnable> tasks = new LinkedList<>();
+ private boolean running;
+ private final Executor executor = new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ synchronized (tasks) {
+ tasks.add(command);
+ if (!running) {
+ running = true;
+ parent.execute(runner);
+ }
+ }
+ }
+ };
+
+ /**
+ * Creates a new thread pool context.
+ *
+ * @param parent The thread pool on which to execute events.
+ */
+ public ThreadPoolContext(ScheduledExecutorService parent) {
+ this.parent = checkNotNull(parent, "parent cannot be null");
+
+ // This code was shamelessly stolededed from Vert.x:
+ // https://github.com/eclipse/vert.x/blob/master/src/main/java/io/vertx/core/impl/OrderedExecutorFactory.java
+ runner = () -> {
+ ((AtomixThread) Thread.currentThread()).setContext(this);
+ for (;;) {
+ final Runnable task;
+ synchronized (tasks) {
+ task = tasks.poll();
+ if (task == null) {
+ running = false;
+ return;
+ }
+ }
+
+ try {
+ task.run();
+ } catch (Throwable t) {
+ LOGGER.error("An uncaught exception occurred", t);
+ throw t;
+ }
+ }
+ };
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ executor.execute(command);
+ }
+
+ @Override
+ public Scheduled schedule(Duration delay, Runnable runnable) {
+ ScheduledFuture<?> future = parent.schedule(() -> executor.execute(runnable), delay.toMillis(), TimeUnit.MILLISECONDS);
+ return () -> future.cancel(false);
+ }
+
+ @Override
+ public Scheduled schedule(Duration delay, Duration interval, Runnable runnable) {
+ ScheduledFuture<?> future = parent.scheduleAtFixedRate(() -> executor.execute(runnable), delay.toMillis(), interval.toMillis(), TimeUnit.MILLISECONDS);
+ return () -> future.cancel(false);
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.slf4j.Logger;
+
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Thread utilities.
+ */
+public final class Threads {
+
+ /**
+ * Returns a thread factory that produces threads named according to the
+ * supplied name pattern.
+ *
+ * @param pattern name pattern
+ * @return thread factory
+ */
+ public static ThreadFactory namedThreads(String pattern, Logger log) {
+ return new ThreadFactoryBuilder()
+ .setNameFormat(pattern)
+ .setThreadFactory(new AtomixThreadFactory())
+ .setUncaughtExceptionHandler((t, e) -> log.error("Uncaught exception on " + t.getName(), e))
+ .build();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for managing concurrency.
+ */
+package io.atomix.utils.concurrent;
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.config;
+
+/**
+ * Atomix configuration.
+ */
+public interface Config {
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.config;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigException;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigMemorySize;
+import com.typesafe.config.ConfigObject;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigValue;
+import io.atomix.utils.Named;
+import io.atomix.utils.memory.MemorySize;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Utility for applying Typesafe configurations to Atomix configuration objects.
+ */
+public class ConfigMapper {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConfigMapper.class);
+ private final ClassLoader classLoader;
+
+ public ConfigMapper(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ }
+
+ /**
+ * Loads the given configuration file using the mapper, falling back to the given resources.
+ *
+ * @param type the type to load
+ * @param files the files to load
+ * @param resources the resources to which to fall back
+ * @param <T> the resulting type
+ * @return the loaded configuration
+ */
+ public <T> T loadFiles(Class<T> type, List<File> files, List<String> resources) {
+ if (files == null) {
+ return loadResources(type, resources);
+ }
+
+ Config config = ConfigFactory.systemProperties();
+ for (File file : files) {
+ config = config.withFallback(ConfigFactory.parseFile(file, ConfigParseOptions.defaults().setAllowMissing(false)));
+ }
+
+ for (String resource : resources) {
+ config = config.withFallback(ConfigFactory.load(classLoader, resource));
+ }
+ return map(checkNotNull(config, "config cannot be null").resolve(), type);
+ }
+
+ /**
+ * Loads the given resources using the configuration mapper.
+ *
+ * @param type the type to load
+ * @param resources the resources to load
+ * @param <T> the resulting type
+ * @return the loaded configuration
+ */
+ public <T> T loadResources(Class<T> type, String... resources) {
+ return loadResources(type, Arrays.asList(resources));
+ }
+
+ /**
+ * Loads the given resources using the configuration mapper.
+ *
+ * @param type the type to load
+ * @param resources the resources to load
+ * @param <T> the resulting type
+ * @return the loaded configuration
+ */
+ public <T> T loadResources(Class<T> type, List<String> resources) {
+ if (resources == null || resources.isEmpty()) {
+ throw new IllegalArgumentException("resources must be defined");
+ }
+ Config config = null;
+ for (String resource : resources) {
+ if (config == null) {
+ config = ConfigFactory.load(classLoader, resource);
+ } else {
+ config = config.withFallback(ConfigFactory.load(classLoader, resource));
+ }
+ }
+ return map(checkNotNull(config, "config cannot be null").resolve(), type);
+ }
+
+ /**
+ * Applies the given configuration to the given type.
+ *
+ * @param config the configuration to apply
+ * @param clazz the class to which to apply the configuration
+ */
+ protected <T> T map(Config config, Class<T> clazz) {
+ return map(config, null, null, clazz);
+ }
+
+ protected <T> T newInstance(Config config, String key, Class<T> clazz) {
+ try {
+ return clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new ConfigurationException(clazz.getName() + " needs a public no-args constructor to be used as a bean", e);
+ }
+ }
+
+ /**
+ * Applies the given configuration to the given type.
+ *
+ * @param config the configuration to apply
+ * @param clazz the class to which to apply the configuration
+ */
+ @SuppressWarnings("unchecked")
+ protected <T> T map(Config config, String path, String name, Class<T> clazz) {
+ T instance = newInstance(config, name, clazz);
+
+ // Map config property names to bean properties.
+ Map<String, String> propertyNames = new HashMap<>();
+ for (Map.Entry<String, ConfigValue> configProp : config.root().entrySet()) {
+ String originalName = configProp.getKey();
+ String camelName = toCamelCase(originalName);
+ // if a setting is in there both as some hyphen name and the camel name,
+ // the camel one wins
+ if (!propertyNames.containsKey(camelName) || originalName.equals(camelName)) {
+ propertyNames.put(camelName, originalName);
+ }
+ }
+
+ // First use setters and then fall back to fields.
+ mapSetters(instance, clazz, path, name, propertyNames, config);
+ mapFields(instance, clazz, path, name, propertyNames, config);
+
+ // If any properties present in the configuration were not found on config beans, throw an exception.
+ if (!propertyNames.isEmpty()) {
+ checkRemainingProperties(propertyNames.keySet(), describeProperties(instance), toPath(path, name), clazz);
+ }
+ return instance;
+ }
+
+ protected void checkRemainingProperties(Set<String> missingProperties, List<String> availableProperties, String path, Class<?> clazz) {
+ Properties properties = System.getProperties();
+ List<String> cleanNames = missingProperties.stream()
+ .map(propertyName -> toPath(path, propertyName))
+ .filter(propertyName -> !properties.containsKey(propertyName))
+ .filter(propertyName -> properties.entrySet().stream().noneMatch(entry -> entry.getKey().toString().startsWith(propertyName + ".")))
+ .sorted()
+ .collect(Collectors.toList());
+ if (!cleanNames.isEmpty()) {
+ throw new ConfigurationException("Unknown properties present in configuration: " + Joiner.on(", ").join(cleanNames) + "\n"
+ + "Available properties:\n- " + Joiner.on("\n- ").join(availableProperties));
+ }
+ }
+
+ private List<String> describeProperties(Object instance) {
+ Stream<String> setters = getSetterDescriptors(instance.getClass())
+ .stream()
+ .map(descriptor -> descriptor.name);
+ Stream<String> fields = getFieldDescriptors(instance.getClass())
+ .stream()
+ .map(descriptor -> descriptor.name);
+ return Stream.concat(setters, fields)
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ private <T> void mapSetters(T instance, Class<T> clazz, String path, String name, Map<String, String> propertyNames, Config config) {
+ try {
+ for (SetterDescriptor descriptor : getSetterDescriptors(instance.getClass())) {
+ Method setter = descriptor.setter;
+ Type parameterType = setter.getGenericParameterTypes()[0];
+ Class<?> parameterClass = setter.getParameterTypes()[0];
+
+ String configPropName = propertyNames.remove(descriptor.name);
+ if (configPropName == null) {
+ if ((Named.class.isAssignableFrom(clazz) || NamedConfig.class.isAssignableFrom(clazz))
+ && descriptor.setter.getParameterTypes()[0] == String.class && name != null && "name".equals(descriptor.name)) {
+ if (descriptor.deprecated) {
+ if (path == null) {
+ LOGGER.warn("{} is deprecated!", name);
+ } else {
+ LOGGER.warn("{}.{} is deprecated!", path, name);
+ }
+ }
+ setter.invoke(instance, name);
+ }
+ continue;
+ }
+
+ Object value = getValue(instance.getClass(), parameterType, parameterClass, config, toPath(path, name), configPropName);
+ if (value != null) {
+ if (descriptor.deprecated) {
+ if (path == null) {
+ LOGGER.warn("{}.{} is deprecated!", name, configPropName);
+ } else {
+ LOGGER.warn("{}.{}.{} is deprecated!", path, name, configPropName);
+ }
+ }
+ setter.invoke(instance, value);
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new ConfigurationException(instance.getClass().getName() + " getters and setters are not accessible, they must be for use as a bean", e);
+ } catch (InvocationTargetException e) {
+ throw new ConfigurationException("Calling bean method on " + instance.getClass().getName() + " caused an exception", e);
+ }
+ }
+
+ private <T> void mapFields(T instance, Class<T> clazz, String path, String name, Map<String, String> propertyNames, Config config) {
+ try {
+ for (FieldDescriptor descriptor : getFieldDescriptors(instance.getClass())) {
+ Field field = descriptor.field;
+ field.setAccessible(true);
+
+ Type genericType = field.getGenericType();
+ Class<?> fieldClass = field.getType();
+
+ String configPropName = propertyNames.remove(descriptor.name);
+ if (configPropName == null) {
+ if (Named.class.isAssignableFrom(clazz) && field.getType() == String.class && name != null && "name".equals(descriptor.name)) {
+ if (descriptor.deprecated) {
+ LOGGER.warn("{}.{} is deprecated!", path, name);
+ }
+ field.set(instance, name);
+ }
+ continue;
+ }
+
+ Object value = getValue(instance.getClass(), genericType, fieldClass, config, toPath(path, name), configPropName);
+ if (value != null) {
+ if (descriptor.deprecated) {
+ LOGGER.warn("{}.{} is deprecated!", path, name);
+ }
+ field.set(instance, value);
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new ConfigurationException(instance.getClass().getName() + " fields are not accessible, they must be for use as a bean", e);
+ }
+ }
+
+ protected Object getValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
+ if (parameterClass == Boolean.class || parameterClass == boolean.class) {
+ try {
+ return config.getBoolean(configPropName);
+ } catch (ConfigException.WrongType e) {
+ return Boolean.parseBoolean(config.getString(configPropName));
+ }
+ } else if (parameterClass == Integer.class || parameterClass == int.class) {
+ try {
+ return config.getInt(configPropName);
+ } catch (ConfigException.WrongType e) {
+ try {
+ return Integer.parseInt(config.getString(configPropName));
+ } catch (NumberFormatException e1) {
+ throw e;
+ }
+ }
+ } else if (parameterClass == Double.class || parameterClass == double.class) {
+ try {
+ return config.getDouble(configPropName);
+ } catch (ConfigException.WrongType e) {
+ try {
+ return Double.parseDouble(config.getString(configPropName));
+ } catch (NumberFormatException e1) {
+ throw e;
+ }
+ }
+ } else if (parameterClass == Long.class || parameterClass == long.class) {
+ try {
+ return config.getLong(configPropName);
+ } catch (ConfigException.WrongType e) {
+ try {
+ return Long.parseLong(config.getString(configPropName));
+ } catch (NumberFormatException e1) {
+ throw e;
+ }
+ }
+ } else if (parameterClass == String.class) {
+ return config.getString(configPropName);
+ } else if (parameterClass == Duration.class) {
+ return config.getDuration(configPropName);
+ } else if (parameterClass == MemorySize.class) {
+ ConfigMemorySize size = config.getMemorySize(configPropName);
+ return new MemorySize(size.toBytes());
+ } else if (parameterClass == Object.class) {
+ return config.getAnyRef(configPropName);
+ } else if (parameterClass == List.class || parameterClass == Collection.class) {
+ return getListValue(beanClass, parameterType, parameterClass, config, configPath, configPropName);
+ } else if (parameterClass == Set.class) {
+ return getSetValue(beanClass, parameterType, parameterClass, config, configPath, configPropName);
+ } else if (parameterClass == Map.class) {
+ return getMapValue(beanClass, parameterType, parameterClass, config, configPath, configPropName);
+ } else if (parameterClass == Config.class) {
+ return config.getConfig(configPropName);
+ } else if (parameterClass == ConfigObject.class) {
+ return config.getObject(configPropName);
+ } else if (parameterClass == ConfigValue.class) {
+ return config.getValue(configPropName);
+ } else if (parameterClass == ConfigList.class) {
+ return config.getList(configPropName);
+ } else if (parameterClass == Class.class) {
+ String className = config.getString(configPropName);
+ try {
+ return classLoader.loadClass(className);
+ } catch (ClassNotFoundException e) {
+ throw new ConfigurationException("Failed to load class: " + className);
+ }
+ } else if (parameterClass.isEnum()) {
+ String value = config.getString(configPropName);
+ String enumName = value.replace("-", "_").toUpperCase();
+ @SuppressWarnings("unchecked")
+ Enum enumValue = Enum.valueOf((Class<Enum>) parameterClass, enumName);
+ try {
+ Deprecated deprecated = enumValue.getDeclaringClass().getField(enumName).getAnnotation(Deprecated.class);
+ if (deprecated != null) {
+ LOGGER.warn("{}.{} = {} is deprecated!", configPath, configPropName, value);
+ }
+ } catch (NoSuchFieldException e) {
+ }
+ return enumValue;
+ } else {
+ return map(config.getConfig(configPropName), configPath, configPropName, parameterClass);
+ }
+ }
+
+ protected Map getMapValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
+ Type[] typeArgs = ((ParameterizedType) parameterType).getActualTypeArguments();
+ Type keyType = typeArgs[0];
+ Type valueType = typeArgs[1];
+
+ Map<Object, Object> map = new HashMap<>();
+ Config childConfig = config.getConfig(configPropName);
+ Class valueClass = (Class) (valueType instanceof ParameterizedType ? ((ParameterizedType) valueType).getRawType() : valueType);
+ for (String key : config.getObject(configPropName).unwrapped().keySet()) {
+ Object value = getValue(Map.class, valueType, valueClass, childConfig, toPath(configPath, configPropName), key);
+ map.put(getKeyValue(keyType, key), value);
+ }
+ return map;
+ }
+
+ protected Object getKeyValue(Type keyType, String key) {
+ if (keyType == Boolean.class || keyType == boolean.class) {
+ return Boolean.parseBoolean(key);
+ } else if (keyType == Integer.class || keyType == int.class) {
+ return Integer.parseInt(key);
+ } else if (keyType == Double.class || keyType == double.class) {
+ return Double.parseDouble(key);
+ } else if (keyType == Long.class || keyType == long.class) {
+ return Long.parseLong(key);
+ } else if (keyType == String.class) {
+ return key;
+ } else {
+ throw new ConfigurationException("Invalid map key type: " + keyType);
+ }
+ }
+
+ protected Object getSetValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
+ return new HashSet((List) getListValue(beanClass, parameterType, parameterClass, config, configPath, configPropName));
+ }
+
+ protected Object getListValue(Class<?> beanClass, Type parameterType, Class<?> parameterClass, Config config, String configPath, String configPropName) {
+ Type elementType = ((ParameterizedType) parameterType).getActualTypeArguments()[0];
+ if (elementType instanceof ParameterizedType) {
+ elementType = ((ParameterizedType) elementType).getRawType();
+ }
+
+ if (elementType == Boolean.class) {
+ try {
+ return config.getBooleanList(configPropName);
+ } catch (ConfigException.WrongType e) {
+ return config.getStringList(configPropName)
+ .stream()
+ .map(Boolean::parseBoolean)
+ .collect(Collectors.toList());
+ }
+ } else if (elementType == Integer.class) {
+ try {
+ return config.getIntList(configPropName);
+ } catch (ConfigException.WrongType e) {
+ return config.getStringList(configPropName)
+ .stream()
+ .map(value -> {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e2) {
+ throw e;
+ }
+ }).collect(Collectors.toList());
+ }
+ } else if (elementType == Double.class) {
+ try {
+ return config.getDoubleList(configPropName);
+ } catch (ConfigException.WrongType e) {
+ return config.getStringList(configPropName)
+ .stream()
+ .map(value -> {
+ try {
+ return Double.parseDouble(value);
+ } catch (NumberFormatException e2) {
+ throw e;
+ }
+ }).collect(Collectors.toList());
+ }
+ } else if (elementType == Long.class) {
+ try {
+ return config.getLongList(configPropName);
+ } catch (ConfigException.WrongType e) {
+ return config.getStringList(configPropName)
+ .stream()
+ .map(value -> {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e2) {
+ throw e;
+ }
+ }).collect(Collectors.toList());
+ }
+ } else if (elementType == String.class) {
+ return config.getStringList(configPropName);
+ } else if (elementType == Duration.class) {
+ return config.getDurationList(configPropName);
+ } else if (elementType == MemorySize.class) {
+ List<ConfigMemorySize> sizes = config.getMemorySizeList(configPropName);
+ return sizes.stream()
+ .map(size -> new MemorySize(size.toBytes()))
+ .collect(Collectors.toList());
+ } else if (elementType == Class.class) {
+ return config.getStringList(configPropName)
+ .stream()
+ .map(className -> {
+ try {
+ return classLoader.loadClass(className);
+ } catch (ClassNotFoundException e) {
+ throw new ConfigurationException("Failed to load class: " + className);
+ }
+ })
+ .collect(Collectors.toList());
+ } else if (elementType == Object.class) {
+ return config.getAnyRefList(configPropName);
+ } else if (((Class<?>) elementType).isEnum()) {
+ @SuppressWarnings("unchecked")
+ List<Enum> enumValues = config.getEnumList((Class<Enum>) elementType, configPropName);
+ return enumValues;
+ } else {
+ List<Object> beanList = new ArrayList<>();
+ List<? extends Config> configList = config.getConfigList(configPropName);
+ int i = 0;
+ for (Config listMember : configList) {
+ beanList.add(map(listMember, toPath(configPath, configPropName), String.valueOf(i), (Class<?>) elementType));
+ }
+ return beanList;
+ }
+ }
+
+ protected String toPath(String path, String name) {
+ return path != null ? String.format("%s.%s", path, name) : name;
+ }
+
+ protected static boolean isSimpleType(Class<?> parameterClass) {
+ return parameterClass == Boolean.class || parameterClass == boolean.class
+ || parameterClass == Integer.class || parameterClass == int.class
+ || parameterClass == Double.class || parameterClass == double.class
+ || parameterClass == Long.class || parameterClass == long.class
+ || parameterClass == String.class
+ || parameterClass == Duration.class
+ || parameterClass == MemorySize.class
+ || parameterClass == List.class
+ || parameterClass == Map.class
+ || parameterClass == Class.class;
+ }
+
+ protected static String toCamelCase(String originalName) {
+ String[] words = originalName.split("-+");
+ if (words.length > 1) {
+ LOGGER.warn("Kebab case config name '" + originalName + "' is deprecated!");
+ StringBuilder nameBuilder = new StringBuilder(originalName.length());
+ for (String word : words) {
+ if (nameBuilder.length() == 0) {
+ nameBuilder.append(word);
+ } else {
+ nameBuilder.append(word.substring(0, 1).toUpperCase());
+ nameBuilder.append(word.substring(1));
+ }
+ }
+ return nameBuilder.toString();
+ }
+ return originalName;
+ }
+
+ protected static String toSetterName(String name) {
+ return "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
+ }
+
+ protected static Collection<SetterDescriptor> getSetterDescriptors(Class<?> clazz) {
+ Map<String, SetterDescriptor> descriptors = Maps.newHashMap();
+ for (Method method : clazz.getMethods()) {
+ String name = method.getName();
+ if (method.getParameterTypes().length == 1
+ && name.length() > 3
+ && "set".equals(name.substring(0, 3))
+ && name.charAt(3) >= 'A'
+ && name.charAt(3) <= 'Z') {
+
+ // Strip the "set" prefix from the property name.
+ name = method.getName().substring(3);
+ name = name.length() > 1
+ ? name.substring(0, 1).toLowerCase() + name.substring(1)
+ : name.toLowerCase();
+
+ // Strip the "Config" suffix from the property name.
+ if (name.endsWith("Config")) {
+ name = name.substring(0, name.length() - "Config".length());
+ }
+
+ // If a setter with this property name has already been registered, determine whether to override it.
+ // We favor simpler types over more complex types (i.e. beans).
+ SetterDescriptor descriptor = descriptors.get(name);
+ if (descriptor != null) {
+ Class<?> type = method.getParameterTypes()[0];
+ if (isSimpleType(type)) {
+ descriptors.put(name, new SetterDescriptor(name, method));
+ }
+ } else {
+ descriptors.put(name, new SetterDescriptor(name, method));
+ }
+ }
+ }
+ return descriptors.values();
+ }
+
+ protected static Collection<FieldDescriptor> getFieldDescriptors(Class<?> type) {
+ Class<?> clazz = type;
+ Map<String, FieldDescriptor> descriptors = Maps.newHashMap();
+ while (clazz != Object.class) {
+ for (Field field : clazz.getDeclaredFields()) {
+ // If the field is static or transient, ignore it.
+ if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
+
+ // If the field has a setter, ignore it and use the setter.
+ Method method = Stream.of(clazz.getMethods())
+ .filter(m -> m.getName().equals(toSetterName(field.getName())))
+ .findFirst()
+ .orElse(null);
+ if (method != null) {
+ continue;
+ }
+
+ // Strip the "Config" suffix from the field.
+ String name = field.getName();
+ if (name.endsWith("Config")) {
+ name = name.substring(0, name.length() - "Config".length());
+ }
+ descriptors.putIfAbsent(name, new FieldDescriptor(name, field));
+ }
+ clazz = clazz.getSuperclass();
+ }
+ return Lists.newArrayList(descriptors.values());
+ }
+
+ protected static class SetterDescriptor {
+ private final String name;
+ private final Method setter;
+ private final boolean deprecated;
+
+ SetterDescriptor(String name, Method setter) {
+ this.name = name;
+ this.setter = setter;
+ this.deprecated = setter.getAnnotation(Deprecated.class) != null;
+ }
+ }
+
+ protected static class FieldDescriptor {
+ private final String name;
+ private final Field field;
+ private final boolean deprecated;
+
+ FieldDescriptor(String name, Field field) {
+ this.name = name;
+ this.field = field;
+ this.deprecated = field.getAnnotation(Deprecated.class) != null;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.config;
+
+import io.atomix.utils.AtomixRuntimeException;
+
+/**
+ * Atomix configuration exception.
+ */
+public class ConfigurationException extends AtomixRuntimeException {
+ public ConfigurationException(String message) {
+ super(message);
+ }
+
+ public ConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.config;
+
+/**
+ * Interface for objects configured via a configuration object.
+ */
+public interface Configured<T extends Config> {
+
+ /**
+ * Returns the object configuration.
+ *
+ * @return the object configuration
+ */
+ T config();
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.config;
+
+/**
+ * Named configuration.
+ */
+public interface NamedConfig<C extends NamedConfig<C>> extends Config {
+
+ /**
+ * Returns the configuration name.
+ *
+ * @return the configuration name
+ */
+ String getName();
+
+ /**
+ * Sets the configuration name.
+ *
+ * @param name the configuration name
+ * @return the configuration object
+ */
+ C setName(String name);
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.config;
+
+/**
+ * Typed configuration.
+ */
+public interface TypedConfig<T> extends Config {
+
+ /**
+ * Returns the type name.
+ *
+ * @return the type name
+ */
+ T getType();
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for reading and mapping configuration files.
+ */
+package io.atomix.utils.config;
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+import io.atomix.utils.misc.TimestampPrinter;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Base event implementation.
+ */
+public class AbstractEvent<T extends Enum, S> implements Event<T, S> {
+ private final long time;
+ private final T type;
+ private final S subject;
+
+ /**
+ * Creates an event of a given type and for the specified subject and the
+ * current time.
+ *
+ * @param type event type
+ * @param subject event subject
+ */
+ protected AbstractEvent(T type, S subject) {
+ this(type, subject, System.currentTimeMillis());
+ }
+
+ /**
+ * Creates an event of a given type and for the specified subject and time.
+ *
+ * @param type event type
+ * @param subject event subject
+ * @param time occurrence time
+ */
+ protected AbstractEvent(T type, S subject, long time) {
+ this.type = type;
+ this.subject = subject;
+ this.time = time;
+ }
+
+ @Override
+ public long time() {
+ return time;
+ }
+
+ @Override
+ public T type() {
+ return type;
+ }
+
+ @Override
+ public S subject() {
+ return subject;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("time", new TimestampPrinter(time))
+ .add("type", type())
+ .add("subject", subject())
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+/**
+ * Basis for components which need to export listener mechanism.
+ */
+public abstract class AbstractListenerManager<E extends Event, L extends EventListener<E>> implements ListenerService<E, L> {
+
+ protected final ListenerRegistry<E, L> listenerRegistry = new ListenerRegistry<>();
+
+ @Override
+ public void addListener(L listener) {
+ listenerRegistry.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(L listener) {
+ listenerRegistry.removeListener(listener);
+ }
+
+ /**
+ * Posts the specified event to the local event dispatcher.
+ *
+ * @param event event to be posted; may be null
+ */
+ protected void post(E event) {
+ listenerRegistry.process(event);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+/**
+ * Abstraction of an of a time-stamped event pertaining to an arbitrary subject.
+ */
+public interface Event<T, S> {
+
+ /**
+ * Returns the timestamp of when the event occurred, given in milliseconds
+ * since the start of epoch.
+ *
+ * @return timestamp in milliseconds
+ */
+ long time();
+
+ /**
+ * Returns the type of the event.
+ *
+ * @return event type
+ */
+ T type();
+
+ /**
+ * Returns the subject of the event.
+ *
+ * @return subject to which this event pertains
+ */
+ S subject();
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+/**
+ * Entity capable of filtering events.
+ */
+public interface EventFilter<E extends Event> {
+
+ /**
+ * Indicates whether the specified event is of interest or not.
+ * Default implementation always returns true.
+ *
+ * @param event event to be inspected
+ * @return true if event is relevant; false otherwise
+ */
+ default boolean isRelevant(E event) {
+ return true;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+/**
+ * Entity capable of receiving events.
+ */
+@FunctionalInterface
+public interface EventListener<E extends Event> extends EventFilter<E> {
+
+ /**
+ * Reacts to the specified event.
+ *
+ * @param event event to be processed
+ */
+ void event(E event);
+
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+/**
+ * Abstraction of an event sink capable of processing the specified event types.
+ */
+public interface EventSink<E extends Event> {
+
+ /**
+ * Processes the specified event.
+ *
+ * @param event event to be processed
+ */
+ void process(E event);
+
+ /**
+ * Handles notification that event processing time limit has been exceeded.
+ */
+ default void onProcessLimit() {
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Base implementation of an event sink and a registry capable of tracking
+ * listeners and dispatching events to them as part of event sink processing.
+ */
+public class ListenerRegistry<E extends Event, L extends EventListener<E>>
+ implements ListenerService<E, L>, EventSink<E> {
+
+ private static final long LIMIT = 1_800; // ms
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ private long lastStart;
+ private L lastListener;
+
+ /**
+ * Set of listeners that have registered.
+ */
+ protected final Set<L> listeners = new CopyOnWriteArraySet<>();
+
+ @Override
+ public void addListener(L listener) {
+ checkNotNull(listener, "Listener cannot be null");
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(L listener) {
+ checkNotNull(listener, "Listener cannot be null");
+ if (!listeners.remove(listener)) {
+ log.warn("Listener {} not registered", listener);
+ }
+ }
+
+ @Override
+ public void process(E event) {
+ for (L listener : listeners) {
+ try {
+ lastListener = listener;
+ lastStart = System.currentTimeMillis();
+ if (listener.isRelevant(event)) {
+ listener.event(event);
+ }
+ lastStart = 0;
+ } catch (Exception error) {
+ reportProblem(event, error);
+ }
+ }
+ }
+
+ @Override
+ public void onProcessLimit() {
+ if (lastStart > 0) {
+ long duration = System.currentTimeMillis() - lastStart;
+ if (duration > LIMIT) {
+ log.error("Listener {} exceeded execution time limit: {} ms; ejected",
+ lastListener.getClass().getName(),
+ duration);
+ removeListener(lastListener);
+ }
+ lastStart = 0;
+ }
+ }
+
+ /**
+ * Reports a problem encountered while processing an event.
+ *
+ * @param event event being processed
+ * @param error error encountered while processing
+ */
+ protected void reportProblem(E event, Throwable error) {
+ log.warn("Exception encountered while processing event " + event, error);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.event;
+
+/**
+ * Abstraction of a service capable of asynchronously notifying listeners.
+ */
+public interface ListenerService<E extends Event, L extends EventListener<E>> {
+
+ /**
+ * Adds the specified listener.
+ *
+ * @param listener listener to be added
+ */
+ void addListener(L listener);
+
+ /**
+ * Removes the specified listener.
+ *
+ * @param listener listener to be removed
+ */
+ void removeListener(L listener);
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for creating and handling generic events.
+ */
+package io.atomix.utils.event;
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.logging;
+
+import org.slf4j.Logger;
+import org.slf4j.Marker;
+
+/**
+ * Contextual logger.
+ */
+public class ContextualLogger extends DelegatingLogger {
+ private static final String SEPARATOR = " - ";
+ private final LoggerContext context;
+
+ public ContextualLogger(Logger delegate, LoggerContext context) {
+ super(delegate);
+ this.context = context;
+ }
+
+ /**
+ * Returns a contextualized version of the given string.
+ *
+ * @param msg the message to contextualize
+ * @return the contextualized message
+ */
+ private String contextualize(String msg) {
+ return context + SEPARATOR + msg;
+ }
+
+ @Override
+ public void trace(String msg) {
+ if (isTraceEnabled()) {
+ super.trace(contextualize(msg));
+ }
+ }
+
+ @Override
+ public void trace(String format, Object arg) {
+ if (isTraceEnabled()) {
+ super.trace(contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void trace(String format, Object arg1, Object arg2) {
+ if (isTraceEnabled()) {
+ super.trace(contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void trace(String format, Object... arguments) {
+ if (isTraceEnabled()) {
+ super.trace(contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void trace(String msg, Throwable t) {
+ if (isTraceEnabled()) {
+ super.trace(contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void trace(Marker marker, String msg) {
+ if (isTraceEnabled()) {
+ super.trace(marker, contextualize(msg));
+ }
+ }
+
+ @Override
+ public void trace(Marker marker, String format, Object arg) {
+ if (isTraceEnabled()) {
+ super.trace(marker, contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void trace(Marker marker, String format, Object arg1, Object arg2) {
+ if (isTraceEnabled()) {
+ super.trace(marker, contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void trace(Marker marker, String format, Object... argArray) {
+ if (isTraceEnabled()) {
+ super.trace(marker, contextualize(format), argArray);
+ }
+ }
+
+ @Override
+ public void trace(Marker marker, String msg, Throwable t) {
+ if (isTraceEnabled()) {
+ super.trace(marker, contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void debug(String msg) {
+ if (isDebugEnabled()) {
+ super.debug(contextualize(msg));
+ }
+ }
+
+ @Override
+ public void debug(String format, Object arg) {
+ if (isDebugEnabled()) {
+ super.debug(contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void debug(String format, Object arg1, Object arg2) {
+ if (isDebugEnabled()) {
+ super.debug(contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void debug(String format, Object... arguments) {
+ if (isDebugEnabled()) {
+ super.debug(contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void debug(String msg, Throwable t) {
+ if (isDebugEnabled()) {
+ super.debug(contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void debug(Marker marker, String msg) {
+ if (isDebugEnabled()) {
+ super.debug(marker, contextualize(msg));
+ }
+ }
+
+ @Override
+ public void debug(Marker marker, String format, Object arg) {
+ if (isDebugEnabled()) {
+ super.debug(marker, contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void debug(Marker marker, String format, Object arg1, Object arg2) {
+ if (isDebugEnabled()) {
+ super.debug(marker, contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void debug(Marker marker, String format, Object... arguments) {
+ if (isDebugEnabled()) {
+ super.debug(marker, contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void debug(Marker marker, String msg, Throwable t) {
+ if (isDebugEnabled()) {
+ super.debug(marker, contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void info(String msg) {
+ if (isInfoEnabled()) {
+ super.info(contextualize(msg));
+ }
+ }
+
+ @Override
+ public void info(String format, Object arg) {
+ if (isInfoEnabled()) {
+ super.info(contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void info(String format, Object arg1, Object arg2) {
+ if (isInfoEnabled()) {
+ super.info(contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void info(String format, Object... arguments) {
+ if (isInfoEnabled()) {
+ super.info(contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void info(String msg, Throwable t) {
+ if (isInfoEnabled()) {
+ super.info(contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void info(Marker marker, String msg) {
+ if (isInfoEnabled()) {
+ super.info(marker, contextualize(msg));
+ }
+ }
+
+ @Override
+ public void info(Marker marker, String format, Object arg) {
+ if (isInfoEnabled()) {
+ super.info(marker, contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void info(Marker marker, String format, Object arg1, Object arg2) {
+ if (isInfoEnabled()) {
+ super.info(marker, contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void info(Marker marker, String format, Object... arguments) {
+ if (isInfoEnabled()) {
+ super.info(marker, contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void info(Marker marker, String msg, Throwable t) {
+ if (isInfoEnabled()) {
+ super.info(marker, contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void warn(String msg) {
+ if (isWarnEnabled()) {
+ super.warn(contextualize(msg));
+ }
+ }
+
+ @Override
+ public void warn(String format, Object arg) {
+ if (isWarnEnabled()) {
+ super.warn(contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void warn(String format, Object... arguments) {
+ if (isWarnEnabled()) {
+ super.warn(contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void warn(String format, Object arg1, Object arg2) {
+ if (isWarnEnabled()) {
+ super.warn(contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void warn(String msg, Throwable t) {
+ if (isWarnEnabled()) {
+ super.warn(contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void warn(Marker marker, String msg) {
+ if (isWarnEnabled()) {
+ super.warn(marker, contextualize(msg));
+ }
+ }
+
+ @Override
+ public void warn(Marker marker, String format, Object arg) {
+ if (isWarnEnabled()) {
+ super.warn(marker, contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void warn(Marker marker, String format, Object arg1, Object arg2) {
+ if (isWarnEnabled()) {
+ super.warn(marker, contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void warn(Marker marker, String format, Object... arguments) {
+ if (isWarnEnabled()) {
+ super.warn(marker, contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void warn(Marker marker, String msg, Throwable t) {
+ if (isWarnEnabled()) {
+ super.warn(marker, contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void error(String msg) {
+ if (isErrorEnabled()) {
+ super.error(contextualize(msg));
+ }
+ }
+
+ @Override
+ public void error(String format, Object arg) {
+ if (isErrorEnabled()) {
+ super.error(contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void error(String format, Object arg1, Object arg2) {
+ if (isErrorEnabled()) {
+ super.error(contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void error(String format, Object... arguments) {
+ if (isErrorEnabled()) {
+ super.error(contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void error(String msg, Throwable t) {
+ if (isErrorEnabled()) {
+ super.error(contextualize(msg), t);
+ }
+ }
+
+ @Override
+ public void error(Marker marker, String msg) {
+ if (isErrorEnabled()) {
+ super.error(marker, contextualize(msg));
+ }
+ }
+
+ @Override
+ public void error(Marker marker, String format, Object arg) {
+ if (isErrorEnabled()) {
+ super.error(marker, contextualize(format), arg);
+ }
+ }
+
+ @Override
+ public void error(Marker marker, String format, Object arg1, Object arg2) {
+ if (isErrorEnabled()) {
+ super.error(marker, contextualize(format), arg1, arg2);
+ }
+ }
+
+ @Override
+ public void error(Marker marker, String format, Object... arguments) {
+ if (isErrorEnabled()) {
+ super.error(marker, contextualize(format), arguments);
+ }
+ }
+
+ @Override
+ public void error(Marker marker, String msg, Throwable t) {
+ if (isErrorEnabled()) {
+ super.error(marker, contextualize(msg), t);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.logging;
+
+import org.slf4j.LoggerFactory;
+
+/**
+ * Contextual logger factory.
+ */
+public class ContextualLoggerFactory {
+
+ /**
+ * Returns a contextual logger.
+ *
+ * @param name the contextual logger name
+ * @param context the logger context
+ * @return the logger
+ */
+ public static ContextualLogger getLogger(String name, LoggerContext context) {
+ return new ContextualLogger(LoggerFactory.getLogger(name), context);
+ }
+
+ /**
+ * Returns a contextual logger.
+ *
+ * @param clazz the contextual logger class
+ * @param context the logger context
+ * @return the logger
+ */
+ public static ContextualLogger getLogger(Class clazz, LoggerContext context) {
+ return new ContextualLogger(LoggerFactory.getLogger(clazz), context);
+ }
+
+ private ContextualLoggerFactory() {
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.logging;
+
+import org.slf4j.Logger;
+import org.slf4j.Marker;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Delegating logger.
+ */
+public class DelegatingLogger implements Logger {
+ private final Logger delegate;
+
+ public DelegatingLogger(Logger delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String getName() {
+ return delegate.getName();
+ }
+
+ @Override
+ public boolean isTraceEnabled() {
+ return delegate.isTraceEnabled();
+ }
+
+ @Override
+ public void trace(String msg) {
+ delegate.trace(msg);
+ }
+
+ @Override
+ public void trace(String format, Object arg) {
+ delegate.trace(format, arg);
+ }
+
+ @Override
+ public void trace(String format, Object arg1, Object arg2) {
+ delegate.trace(format, arg1, arg2);
+ }
+
+ @Override
+ public void trace(String format, Object... arguments) {
+ delegate.trace(format, arguments);
+ }
+
+ @Override
+ public void trace(String msg, Throwable t) {
+ delegate.trace(msg, t);
+ }
+
+ @Override
+ public boolean isTraceEnabled(Marker marker) {
+ return delegate.isTraceEnabled(marker);
+ }
+
+ @Override
+ public void trace(Marker marker, String msg) {
+ delegate.trace(marker, msg);
+ }
+
+ @Override
+ public void trace(Marker marker, String format, Object arg) {
+ delegate.trace(marker, format, arg);
+ }
+
+ @Override
+ public void trace(Marker marker, String format, Object arg1, Object arg2) {
+ delegate.trace(marker, format, arg1, arg2);
+ }
+
+ @Override
+ public void trace(Marker marker, String format, Object... argArray) {
+ delegate.trace(marker, format, argArray);
+ }
+
+ @Override
+ public void trace(Marker marker, String msg, Throwable t) {
+ delegate.trace(marker, msg, t);
+ }
+
+ @Override
+ public boolean isDebugEnabled() {
+ return delegate.isDebugEnabled();
+ }
+
+ @Override
+ public void debug(String msg) {
+ delegate.debug(msg);
+ }
+
+ @Override
+ public void debug(String format, Object arg) {
+ delegate.debug(format, arg);
+ }
+
+ @Override
+ public void debug(String format, Object arg1, Object arg2) {
+ delegate.debug(format, arg1, arg2);
+ }
+
+ @Override
+ public void debug(String format, Object... arguments) {
+ delegate.debug(format, arguments);
+ }
+
+ @Override
+ public void debug(String msg, Throwable t) {
+ delegate.debug(msg, t);
+ }
+
+ @Override
+ public boolean isDebugEnabled(Marker marker) {
+ return delegate.isDebugEnabled(marker);
+ }
+
+ @Override
+ public void debug(Marker marker, String msg) {
+ delegate.debug(marker, msg);
+ }
+
+ @Override
+ public void debug(Marker marker, String format, Object arg) {
+ delegate.debug(marker, format, arg);
+ }
+
+ @Override
+ public void debug(Marker marker, String format, Object arg1, Object arg2) {
+ delegate.debug(marker, format, arg1, arg2);
+ }
+
+ @Override
+ public void debug(Marker marker, String format, Object... arguments) {
+ delegate.debug(marker, format, arguments);
+ }
+
+ @Override
+ public void debug(Marker marker, String msg, Throwable t) {
+ delegate.debug(marker, msg, t);
+ }
+
+ @Override
+ public boolean isInfoEnabled() {
+ return delegate.isInfoEnabled();
+ }
+
+ @Override
+ public void info(String msg) {
+ delegate.info(msg);
+ }
+
+ @Override
+ public void info(String format, Object arg) {
+ delegate.info(format, arg);
+ }
+
+ @Override
+ public void info(String format, Object arg1, Object arg2) {
+ delegate.info(format, arg1, arg2);
+ }
+
+ @Override
+ public void info(String format, Object... arguments) {
+ delegate.info(format, arguments);
+ }
+
+ @Override
+ public void info(String msg, Throwable t) {
+ delegate.info(msg, t);
+ }
+
+ @Override
+ public boolean isInfoEnabled(Marker marker) {
+ return delegate.isInfoEnabled(marker);
+ }
+
+ @Override
+ public void info(Marker marker, String msg) {
+ delegate.info(marker, msg);
+ }
+
+ @Override
+ public void info(Marker marker, String format, Object arg) {
+ delegate.info(marker, format, arg);
+ }
+
+ @Override
+ public void info(Marker marker, String format, Object arg1, Object arg2) {
+ delegate.info(marker, format, arg1, arg2);
+ }
+
+ @Override
+ public void info(Marker marker, String format, Object... arguments) {
+ delegate.info(marker, format, arguments);
+ }
+
+ @Override
+ public void info(Marker marker, String msg, Throwable t) {
+ delegate.info(marker, msg, t);
+ }
+
+ @Override
+ public boolean isWarnEnabled() {
+ return delegate.isWarnEnabled();
+ }
+
+ @Override
+ public void warn(String msg) {
+ delegate.warn(msg);
+ }
+
+ @Override
+ public void warn(String format, Object arg) {
+ delegate.warn(format, arg);
+ }
+
+ @Override
+ public void warn(String format, Object... arguments) {
+ delegate.warn(format, arguments);
+ }
+
+ @Override
+ public void warn(String format, Object arg1, Object arg2) {
+ delegate.warn(format, arg1, arg2);
+ }
+
+ @Override
+ public void warn(String msg, Throwable t) {
+ delegate.warn(msg, t);
+ }
+
+ @Override
+ public boolean isWarnEnabled(Marker marker) {
+ return delegate.isWarnEnabled(marker);
+ }
+
+ @Override
+ public void warn(Marker marker, String msg) {
+ delegate.warn(marker, msg);
+ }
+
+ @Override
+ public void warn(Marker marker, String format, Object arg) {
+ delegate.warn(marker, format, arg);
+ }
+
+ @Override
+ public void warn(Marker marker, String format, Object arg1, Object arg2) {
+ delegate.warn(marker, format, arg1, arg2);
+ }
+
+ @Override
+ public void warn(Marker marker, String format, Object... arguments) {
+ delegate.warn(marker, format, arguments);
+ }
+
+ @Override
+ public void warn(Marker marker, String msg, Throwable t) {
+ delegate.warn(marker, msg, t);
+ }
+
+ @Override
+ public boolean isErrorEnabled() {
+ return delegate.isErrorEnabled();
+ }
+
+ @Override
+ public void error(String msg) {
+ delegate.error(msg);
+ }
+
+ @Override
+ public void error(String format, Object arg) {
+ delegate.error(format, arg);
+ }
+
+ @Override
+ public void error(String format, Object arg1, Object arg2) {
+ delegate.error(format, arg1, arg2);
+ }
+
+ @Override
+ public void error(String format, Object... arguments) {
+ delegate.error(format, arguments);
+ }
+
+ @Override
+ public void error(String msg, Throwable t) {
+ delegate.error(msg, t);
+ }
+
+ @Override
+ public boolean isErrorEnabled(Marker marker) {
+ return delegate.isErrorEnabled(marker);
+ }
+
+ @Override
+ public void error(Marker marker, String msg) {
+ delegate.error(marker, msg);
+ }
+
+ @Override
+ public void error(Marker marker, String format, Object arg) {
+ delegate.error(marker, format, arg);
+ }
+
+ @Override
+ public void error(Marker marker, String format, Object arg1, Object arg2) {
+ delegate.error(marker, format, arg1, arg2);
+ }
+
+ @Override
+ public void error(Marker marker, String format, Object... arguments) {
+ delegate.error(marker, format, arguments);
+ }
+
+ @Override
+ public void error(Marker marker, String msg, Throwable t) {
+ delegate.error(marker, msg, t);
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .addValue(delegate)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.logging;
+
+import com.google.common.base.MoreObjects;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+
+import java.util.function.Supplier;
+
+/**
+ * Logger context.
+ */
+public class LoggerContext {
+
+ /**
+ * Returns a new contextual logger builder.
+ *
+ * @param name the logger name
+ * @return the logger builder
+ */
+ public static Builder builder(String name) {
+ return new Builder(name);
+ }
+
+ /**
+ * Returns a new contextual logger builder.
+ *
+ * @param clazz the logger class
+ * @return the logger builder
+ */
+ public static Builder builder(Class clazz) {
+ return new Builder(clazz.getSimpleName());
+ }
+
+ private final Supplier<String> stringProvider;
+
+ public LoggerContext(Supplier<String> stringProvider) {
+ this.stringProvider = stringProvider;
+ }
+
+ @Override
+ public String toString() {
+ return stringProvider.get();
+ }
+
+ /**
+ * Contextual logger builder.
+ */
+ public static class Builder implements io.atomix.utils.Builder<LoggerContext> {
+ private final MoreObjects.ToStringHelper identityStringHelper;
+ private MoreObjects.ToStringHelper argsStringHelper;
+ private boolean omitNullValues = false;
+
+ public Builder(String name) {
+ this.identityStringHelper = MoreObjects.toStringHelper(name);
+ }
+
+ /**
+ * Initializes the arguments string helper.
+ */
+ private void initializeArgs() {
+ if (argsStringHelper == null) {
+ argsStringHelper = MoreObjects.toStringHelper("");
+ }
+ }
+
+ /**
+ * Configures the {@link MoreObjects.ToStringHelper} so {@link #toString()} will ignore properties with null
+ * value. The order of calling this method, relative to the {@code add()}/{@code addValue()}
+ * methods, is not significant.
+ */
+ @CanIgnoreReturnValue
+ public Builder omitNullValues() {
+ this.omitNullValues = true;
+ return this;
+ }
+
+ /**
+ * Adds a name/value pair to the formatted output in {@code name=value} format. If {@code value}
+ * is {@code null}, the string {@code "null"} is used, unless {@link #omitNullValues()} is
+ * called, in which case this name/value pair will not be added.
+ */
+ @CanIgnoreReturnValue
+ public Builder add(String name, Object value) {
+ initializeArgs();
+ argsStringHelper.add(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a name/value pair to the formatted output in {@code name=value} format.
+ */
+ @CanIgnoreReturnValue
+ public Builder add(String name, boolean value) {
+ initializeArgs();
+ argsStringHelper.add(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a name/value pair to the formatted output in {@code name=value} format.
+ */
+ @CanIgnoreReturnValue
+ public Builder add(String name, char value) {
+ initializeArgs();
+ argsStringHelper.add(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a name/value pair to the formatted output in {@code name=value} format.
+ */
+ @CanIgnoreReturnValue
+ public Builder add(String name, double value) {
+ initializeArgs();
+ argsStringHelper.add(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a name/value pair to the formatted output in {@code name=value} format.
+ */
+ @CanIgnoreReturnValue
+ public Builder add(String name, float value) {
+ initializeArgs();
+ argsStringHelper.add(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a name/value pair to the formatted output in {@code name=value} format.
+ */
+ @CanIgnoreReturnValue
+ public Builder add(String name, int value) {
+ initializeArgs();
+ argsStringHelper.add(name, value);
+ return this;
+ }
+
+ /**
+ * Adds a name/value pair to the formatted output in {@code name=value} format.
+ */
+ @CanIgnoreReturnValue
+ public Builder add(String name, long value) {
+ initializeArgs();
+ argsStringHelper.add(name, value);
+ return this;
+ }
+
+ /**
+ * Adds an unnamed value to the formatted output.
+ *
+ * <p>It is strongly encouraged to use {@link #add(String, Object)} instead and give value a
+ * readable name.
+ */
+ @CanIgnoreReturnValue
+ public Builder addValue(Object value) {
+ identityStringHelper.addValue(value);
+ return this;
+ }
+
+ /**
+ * Adds an unnamed value to the formatted output.
+ *
+ * <p>It is strongly encouraged to use {@link #add(String, boolean)} instead and give value a
+ * readable name.
+ */
+ @CanIgnoreReturnValue
+ public Builder addValue(boolean value) {
+ identityStringHelper.addValue(value);
+ return this;
+ }
+
+ /**
+ * Adds an unnamed value to the formatted output.
+ *
+ * <p>It is strongly encouraged to use {@link #add(String, char)} instead and give value a
+ * readable name.
+ */
+ @CanIgnoreReturnValue
+ public Builder addValue(char value) {
+ identityStringHelper.addValue(value);
+ return this;
+ }
+
+ /**
+ * Adds an unnamed value to the formatted output.
+ *
+ * <p>It is strongly encouraged to use {@link #add(String, double)} instead and give value a
+ * readable name.
+ */
+ @CanIgnoreReturnValue
+ public Builder addValue(double value) {
+ identityStringHelper.addValue(value);
+ return this;
+ }
+
+ /**
+ * Adds an unnamed value to the formatted output.
+ *
+ * <p>It is strongly encouraged to use {@link #add(String, float)} instead and give value a
+ * readable name.
+ */
+ @CanIgnoreReturnValue
+ public Builder addValue(float value) {
+ identityStringHelper.addValue(value);
+ return this;
+ }
+
+ /**
+ * Adds an unnamed value to the formatted output.
+ *
+ * <p>It is strongly encouraged to use {@link #add(String, int)} instead and give value a
+ * readable name.
+ */
+ @CanIgnoreReturnValue
+ public Builder addValue(int value) {
+ identityStringHelper.addValue(value);
+ return this;
+ }
+
+ /**
+ * Adds an unnamed value to the formatted output.
+ *
+ * <p>It is strongly encouraged to use {@link #add(String, long)} instead and give value a
+ * readable name.
+ */
+ @CanIgnoreReturnValue
+ public Builder addValue(long value) {
+ identityStringHelper.addValue(value);
+ return this;
+ }
+
+ @Override
+ public LoggerContext build() {
+ MoreObjects.ToStringHelper identityStringHelper = this.identityStringHelper;
+ MoreObjects.ToStringHelper argsStringHelper = this.argsStringHelper;
+ if (omitNullValues) {
+ identityStringHelper.omitNullValues();
+ if (argsStringHelper != null) {
+ argsStringHelper.omitNullValues();
+ }
+ }
+ return new LoggerContext(() -> {
+ if (argsStringHelper == null) {
+ return identityStringHelper.toString();
+ } else {
+ return identityStringHelper.toString() + argsStringHelper.toString();
+ }
+ });
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides utility classes for logging in complex objects.
+ */
+package io.atomix.utils.logging;
--- /dev/null
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.atomix.utils.memory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Objects;
+
+import static java.lang.invoke.MethodHandles.constant;
+import static java.lang.invoke.MethodHandles.dropArguments;
+import static java.lang.invoke.MethodHandles.filterReturnValue;
+import static java.lang.invoke.MethodHandles.guardWithTest;
+import static java.lang.invoke.MethodHandles.lookup;
+import static java.lang.invoke.MethodType.methodType;
+
+/**
+ * Utility class which allows explicit calls to the DirectByteBuffer cleaner method instead of relying on GC.
+ */
+public class BufferCleaner {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BufferCleaner.class);
+
+ /**
+ * Reference to a Cleaner that does unmapping; no-op if not supported.
+ */
+ private static final Cleaner CLEANER;
+
+ static {
+ final Object hack = AccessController.doPrivileged((PrivilegedAction<Object>) BufferCleaner::unmapHackImpl);
+ if (hack instanceof Cleaner) {
+ CLEANER = (Cleaner) hack;
+ LOGGER.debug("java.nio.DirectByteBuffer.cleaner(): available");
+ } else {
+ CLEANER = (ByteBuffer buffer) -> {
+ // noop
+ };
+ LOGGER.debug("java.nio.DirectByteBuffer.cleaner(): unavailable", hack);
+ }
+ }
+
+ private static Object unmapHackImpl() {
+ final MethodHandles.Lookup lookup = lookup();
+ try {
+ try {
+ // *** sun.misc.Unsafe unmapping (Java 9+) ***
+ final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
+ // first check if Unsafe has the right method, otherwise we can give up
+ // without doing any security critical stuff:
+ final MethodHandle unmapper = lookup.findVirtual(unsafeClass, "invokeCleaner",
+ methodType(void.class, ByteBuffer.class));
+ // fetch the unsafe instance and bind it to the virtual MH:
+ final Field f = unsafeClass.getDeclaredField("theUnsafe");
+ f.setAccessible(true);
+ final Object theUnsafe = f.get(null);
+ return newBufferCleaner(ByteBuffer.class, unmapper.bindTo(theUnsafe));
+ } catch (SecurityException se) {
+ // rethrow to report errors correctly (we need to catch it here, as we also catch RuntimeException below!):
+ throw se;
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ // *** sun.misc.Cleaner unmapping (Java 8) ***
+ final Class<?> directBufferClass = Class.forName("java.nio.DirectByteBuffer");
+
+ final Method m = directBufferClass.getMethod("cleaner");
+ m.setAccessible(true);
+ final MethodHandle directBufferCleanerMethod = lookup.unreflect(m);
+ final Class<?> cleanerClass = directBufferCleanerMethod.type().returnType();
+
+ /* "Compile" a MH that basically is equivalent to the following code:
+ * void unmapper(ByteBuffer byteBuffer) {
+ * sun.misc.Cleaner cleaner = ((java.nio.DirectByteBuffer) byteBuffer).cleaner();
+ * if (Objects.nonNull(cleaner)) {
+ * cleaner.clean();
+ * } else {
+ * noop(cleaner); // the noop is needed because MethodHandles#guardWithTest always needs ELSE
+ * }
+ * }
+ */
+ final MethodHandle cleanMethod = lookup.findVirtual(cleanerClass, "clean", methodType(void.class));
+ final MethodHandle nonNullTest = lookup.findStatic(Objects.class, "nonNull", methodType(boolean.class, Object.class))
+ .asType(methodType(boolean.class, cleanerClass));
+ final MethodHandle noop = dropArguments(constant(Void.class, null).asType(methodType(void.class)), 0, cleanerClass);
+ final MethodHandle unmapper = filterReturnValue(directBufferCleanerMethod, guardWithTest(nonNullTest, cleanMethod, noop))
+ .asType(methodType(void.class, ByteBuffer.class));
+ return newBufferCleaner(directBufferClass, unmapper);
+ }
+ } catch (SecurityException se) {
+ return "Unmapping is not supported, because not all required permissions are given to the Lucene JAR file: "
+ + se + " [Please grant at least the following permissions: RuntimePermission(\"accessClassInPackage.sun.misc\") "
+ + " and ReflectPermission(\"suppressAccessChecks\")]";
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ return "Unmapping is not supported on this platform, because internal Java APIs are not compatible with this Atomix version: " + e;
+ }
+ }
+
+ private static Cleaner newBufferCleaner(final Class<?> unmappableBufferClass, final MethodHandle unmapper) {
+ return (ByteBuffer buffer) -> {
+ if (!buffer.isDirect()) {
+ return;
+ }
+ if (!unmappableBufferClass.isInstance(buffer)) {
+ throw new IllegalArgumentException("buffer is not an instance of " + unmappableBufferClass.getName());
+ }
+ final Throwable error = AccessController.doPrivileged((PrivilegedAction<Throwable>) () -> {
+ try {
+ unmapper.invokeExact(buffer);
+ return null;
+ } catch (Throwable t) {
+ return t;
+ }
+ });
+ if (error != null) {
+ throw new IOException("Unable to unmap the mapped buffer", error);
+ }
+ };
+ }
+
+ /**
+ * Free {@link ByteBuffer} if possible.
+ */
+ public static void freeBuffer(ByteBuffer buffer) throws IOException {
+ CLEANER.freeBuffer(buffer);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.memory;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+@FunctionalInterface
+interface Cleaner {
+
+ /**
+ * Free {@link ByteBuffer} if possible.
+ */
+ void freeBuffer(ByteBuffer buffer) throws IOException;
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.memory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * Mapped memory.
+ * <p>
+ * This is a special memory descriptor that handles management of {@link MappedByteBuffer} based memory. The
+ * mapped memory descriptor simply points to the memory address of the underlying byte buffer. When memory is reallocated,
+ * the parent {@link MappedMemoryAllocator} is used to create a new {@link MappedByteBuffer}
+ * and free the existing buffer.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class MappedMemory implements Memory {
+ private static final long MAX_SIZE = Integer.MAX_VALUE - 5;
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MappedMemory.class);
+
+ /**
+ * Allocates memory mapped to a file on disk.
+ *
+ * @param file The file to which to map memory.
+ * @param size The count of the memory to map.
+ * @return The mapped memory.
+ * @throws IllegalArgumentException If {@code count} is greater than {@link MappedMemory#MAX_SIZE}
+ */
+ public static MappedMemory allocate(File file, int size) {
+ return new MappedMemoryAllocator(file).allocate(size);
+ }
+
+ /**
+ * Allocates memory mapped to a file on disk.
+ *
+ * @param file The file to which to map memory.
+ * @param mode The mode with which to map memory.
+ * @param size The count of the memory to map.
+ * @return The mapped memory.
+ * @throws IllegalArgumentException If {@code count} is greater than {@link MappedMemory#MAX_SIZE}
+ */
+ public static MappedMemory allocate(File file, FileChannel.MapMode mode, int size) {
+ if (size > MAX_SIZE) {
+ throw new IllegalArgumentException("size cannot be greater than " + MAX_SIZE);
+ }
+ return new MappedMemoryAllocator(file, mode).allocate(size);
+ }
+
+ private final MappedByteBuffer buffer;
+ private final MappedMemoryAllocator allocator;
+ private final int size;
+
+ public MappedMemory(MappedByteBuffer buffer, MappedMemoryAllocator allocator) {
+ this.buffer = buffer;
+ this.allocator = allocator;
+ this.size = buffer.capacity();
+ }
+
+ /**
+ * Flushes the mapped buffer to disk.
+ */
+ public void flush() {
+ buffer.force();
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public void free() {
+ try {
+ BufferCleaner.freeBuffer(buffer);
+ } catch (Exception e) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Failed to unmap direct buffer", e);
+ }
+ }
+ allocator.release();
+ }
+
+ public void close() {
+ free();
+ allocator.close();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.memory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Mapped memory allocator.
+ * <p>
+ * The mapped memory allocator provides direct memory access to memory mapped from a file on disk. The mapped allocator
+ * supports allocating memory in any {@link FileChannel.MapMode}. Once the file is mapped and the
+ * memory has been allocated, the mapped allocator provides the memory address of the underlying
+ * {@link java.nio.MappedByteBuffer} for access via {@link sun.misc.Unsafe}.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public class MappedMemoryAllocator implements MemoryAllocator<MappedMemory> {
+ public static final FileChannel.MapMode DEFAULT_MAP_MODE = FileChannel.MapMode.READ_WRITE;
+
+ private final AtomicInteger referenceCount = new AtomicInteger();
+ private final RandomAccessFile file;
+ private final FileChannel channel;
+ private final FileChannel.MapMode mode;
+ private final long offset;
+
+ public MappedMemoryAllocator(File file) {
+ this(file, DEFAULT_MAP_MODE, 0);
+ }
+
+ public MappedMemoryAllocator(File file, FileChannel.MapMode mode) {
+ this(file, mode, 0);
+ }
+
+ public MappedMemoryAllocator(File file, FileChannel.MapMode mode, long offset) {
+ this(createFile(file, mode), mode, offset);
+ }
+
+ public MappedMemoryAllocator(RandomAccessFile file, FileChannel.MapMode mode, long offset) {
+ if (file == null) {
+ throw new NullPointerException("file cannot be null");
+ }
+ if (mode == null) {
+ throw new NullPointerException("mode cannot be null");
+ }
+ if (offset < 0) {
+ throw new IllegalArgumentException("offset cannot be negative");
+ }
+ this.file = file;
+ this.channel = this.file.getChannel();
+ this.mode = mode;
+ this.offset = offset;
+ }
+
+ private static RandomAccessFile createFile(File file, FileChannel.MapMode mode) {
+ if (file == null) {
+ throw new NullPointerException("file cannot be null");
+ }
+ if (mode == null) {
+ mode = DEFAULT_MAP_MODE;
+ }
+ try {
+ return new RandomAccessFile(file, parseMode(mode));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String parseMode(FileChannel.MapMode mode) {
+ if (mode == FileChannel.MapMode.READ_ONLY) {
+ return "r";
+ } else if (mode == FileChannel.MapMode.READ_WRITE) {
+ return "rw";
+ }
+ throw new IllegalArgumentException("unsupported map mode");
+ }
+
+ @Override
+ public MappedMemory allocate(int size) {
+ try {
+ if (file.length() < size) {
+ file.setLength(size);
+ }
+ referenceCount.incrementAndGet();
+ return new MappedMemory(channel.map(mode, offset, size), this);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public MappedMemory reallocate(MappedMemory memory, int size) {
+ MappedMemory newMemory = allocate(size);
+ memory.free();
+ return newMemory;
+ }
+
+ public void close() {
+ try {
+ file.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Releases a reference from the allocator.
+ */
+ void release() {
+ if (referenceCount.decrementAndGet() == 0) {
+ close();
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.memory;
+
+/**
+ * Memory allocator.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface Memory {
+
+ /**
+ * Returns the memory count.
+ *
+ * @return The memory count.
+ */
+ int size();
+
+ /**
+ * Frees the memory.
+ */
+ void free();
+
+ /**
+ * Memory utilities.
+ */
+ class Util {
+
+ /**
+ * Returns a boolean indicating whether the given count is a power of 2.
+ */
+ public static boolean isPow2(int size) {
+ return size > 0 & (size & (size - 1)) == 0;
+ }
+
+ /**
+ * Rounds the count to the nearest power of two.
+ */
+ public static long toPow2(int size) {
+ if ((size & (size - 1)) == 0) {
+ return size;
+ }
+ int i = 128;
+ while (i < size) {
+ i *= 2;
+ if (i <= 0) {
+ return 1L << 62;
+ }
+ }
+ return i;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.memory;
+
+/**
+ * Memory allocator.
+ * <p>
+ * Memory allocators handle allocation of memory for {@link Memory} objects.
+ *
+ * @author <a href="http://github.com/kuujo">Jordan Halterman</a>
+ */
+public interface MemoryAllocator<T extends Memory> {
+
+ /**
+ * Allocates memory.
+ *
+ * @param size The count of the memory to allocate.
+ * @return The allocated memory.
+ */
+ T allocate(int size);
+
+ /**
+ * Reallocates the given memory.
+ * <p>
+ * When the memory is reallocated, the memory address for the given {@link Memory} instance may change. The returned
+ * {@link Memory} instance will contain the updated address and count.
+ *
+ * @param memory The memory to reallocate.
+ * @param size The count to which to reallocate the given memory.
+ * @return The reallocated memory.
+ */
+ T reallocate(T memory, int size);
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.memory;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Memory size.
+ */
+public class MemorySize {
+
+ /**
+ * Creates a memory size from the given bytes.
+ *
+ * @param bytes the number of bytes
+ * @return the memory size
+ */
+ public static MemorySize from(long bytes) {
+ return new MemorySize(bytes);
+ }
+
+ private final long bytes;
+
+ public MemorySize(long bytes) {
+ this.bytes = bytes;
+ }
+
+ /**
+ * Returns the number of bytes.
+ *
+ * @return the number of bytes
+ */
+ public long bytes() {
+ return bytes;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.valueOf(bytes).hashCode();
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return object instanceof MemorySize && ((MemorySize) object).bytes == bytes;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .addValue(bytes)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for performing low-level on- and off-heap memory management.
+ */
+package io.atomix.utils.memory;
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.misc;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+
+/**
+ * Helper to print Object[] length and hashCode.
+ */
+public final class ArraySizeHashPrinter {
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given short[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(byte[] array) {
+ return new ArraySizeHashPrinter(toObjectArray(array), byte[].class);
+ }
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given short[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(short[] array) {
+ return new ArraySizeHashPrinter(toObjectArray(array), short[].class);
+ }
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given int[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(int[] array) {
+ return new ArraySizeHashPrinter(toObjectArray(array), int[].class);
+ }
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given long[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(long[] array) {
+ return new ArraySizeHashPrinter(toObjectArray(array), long[].class);
+ }
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given float[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(float[] array) {
+ return new ArraySizeHashPrinter(toObjectArray(array), float[].class);
+ }
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given double[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(double[] array) {
+ return new ArraySizeHashPrinter(toObjectArray(array), double[].class);
+ }
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given boolean[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(boolean[] array) {
+ return new ArraySizeHashPrinter(toObjectArray(array), boolean[].class);
+ }
+
+ /**
+ * Returns ByteArraySizeHashPrinter wrapping given Object[].
+ *
+ * @param array arrays to wrap around
+ * @return ObjectArraySizeHashPrinter
+ */
+ public static ArraySizeHashPrinter of(Object[] array) {
+ return new ArraySizeHashPrinter(array, Object[].class);
+ }
+
+ private static Object[] toObjectArray(Object val) {
+ if (val == null) {
+ return null;
+ }
+ if (val instanceof Object[]) {
+ return (Object[]) val;
+ }
+ int length = Array.getLength(val);
+ Object[] outputArray = new Object[length];
+ for (int i = 0; i < length; ++i) {
+ outputArray[i] = Array.get(val, i);
+ }
+ return outputArray;
+ }
+
+ private final Object[] array;
+ private final Class<?> type;
+
+ public ArraySizeHashPrinter(Object[] array, Class<?> type) {
+ this.array = array;
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ ToStringHelper helper = MoreObjects.toStringHelper(type);
+ if (array != null) {
+ helper.add("length", array.length)
+ .add("hash", Arrays.hashCode(array));
+ } else {
+ helper.addValue(array);
+ }
+ return helper.toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.atomix.utils.misc;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Function;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Utility class for checking matching values.
+ *
+ * @param <T> type of value
+ */
+public final class Match<T> {
+
+ public static final Match ANY = new Match<>();
+ public static final Match NULL = new Match<>(null, false);
+ public static final Match NOT_NULL = new Match<>(null, true);
+
+ private final boolean matchAny;
+ private final T value;
+ private final boolean negation;
+
+ /**
+ * Returns a Match that matches any value including null.
+ *
+ * @param <T> match type
+ * @return new instance
+ */
+ public static <T> Match<T> any() {
+ return ANY;
+ }
+
+ /**
+ * Returns a Match that matches null values.
+ *
+ * @param <T> match type
+ * @return new instance
+ */
+ public static <T> Match<T> ifNull() {
+ return NULL;
+ }
+
+ /**
+ * Returns a Match that matches all non-null values.
+ *
+ * @param <T> match type
+ * @return new instance
+ */
+ public static <T> Match<T> ifNotNull() {
+ return NOT_NULL;
+ }
+
+ /**
+ * Returns a Match that only matches the specified value.
+ *
+ * @param value value to match
+ * @param <T> match type
+ * @return new instance
+ */
+ public static <T> Match<T> ifValue(T value) {
+ return new Match<>(value, false);
+ }
+
+ /**
+ * Returns a Match that matches any value except the specified value.
+ *
+ * @param value value to not match
+ * @param <T> match type
+ * @return new instance
+ */
+ public static <T> Match<T> ifNotValue(T value) {
+ return new Match<>(value, true);
+ }
+
+ private Match() {
+ matchAny = true;
+ negation = false;
+ value = null;
+ }
+
+ private Match(T value, boolean negation) {
+ matchAny = false;
+ this.value = value;
+ this.negation = negation;
+ }
+
+ /**
+ * Maps this instance to a Match of another type.
+ *
+ * @param mapper transformation function
+ * @param <V> new match type
+ * @return new instance
+ */
+ public <V> Match<V> map(Function<T, V> mapper) {
+ if (matchAny) {
+ return any();
+ } else if (value == null) {
+ return negation ? ifNotNull() : ifNull();
+ } else {
+ return negation ? ifNotValue(mapper.apply(value)) : ifValue(mapper.apply(value));
+ }
+ }
+
+ /**
+ * Checks if this instance matches specified value.
+ *
+ * @param other other value
+ * @return true if matches; false otherwise
+ */
+ public boolean matches(T other) {
+ if (matchAny) {
+ return true;
+ } else if (other == null) {
+ return negation ? value != null : value == null;
+ } else {
+ if (value instanceof byte[]) {
+ boolean equal = Arrays.equals((byte[]) value, (byte[]) other);
+ return negation ? !equal : equal;
+ }
+ return negation ? !Objects.equals(value, other) : Objects.equals(value, other);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(matchAny, value, negation);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Match)) {
+ return false;
+ }
+ Match<T> that = (Match<T>) other;
+ return this.matchAny == that.matchAny
+ && Objects.equals(this.value, that.value)
+ && this.negation == that.negation;
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("matchAny", matchAny)
+ .add("negation", negation)
+ .add("value", value)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.misc;
+
+import io.atomix.utils.concurrent.Scheduled;
+import io.atomix.utils.concurrent.SingleThreadContext;
+import io.atomix.utils.concurrent.ThreadContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Maintains a sliding window of value counts. The sliding window counter is
+ * initialized with a number of window slots. Calls to #incrementCount() will
+ * increment the value in the current window slot. Periodically the window
+ * slides and the oldest value count is dropped. Calls to #get() will get the
+ * total count for the last N window slots.
+ */
+public final class SlidingWindowCounter {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+ private volatile int headSlot;
+ private final int windowSlots;
+
+ private final List<AtomicLong> counters;
+
+ private final Scheduled schedule;
+
+ private static final int SLIDE_WINDOW_PERIOD_SECONDS = 1;
+
+ public SlidingWindowCounter(int windowSlots) {
+ this(windowSlots, new SingleThreadContext("sliding-window-counter-%d"));
+ }
+
+ /**
+ * Creates a new sliding window counter with the given total number of
+ * window slots.
+ *
+ * @param windowSlots total number of window slots
+ */
+ public SlidingWindowCounter(int windowSlots, ThreadContext context) {
+ checkArgument(windowSlots > 0, "Window size must be a positive integer");
+
+ this.windowSlots = windowSlots;
+ this.headSlot = 0;
+
+ // Initialize each item in the list to an AtomicLong of 0
+ this.counters = Collections.nCopies(windowSlots, 0)
+ .stream()
+ .map(AtomicLong::new)
+ .collect(Collectors.toCollection(ArrayList::new));
+ this.schedule = context.schedule(0, SLIDE_WINDOW_PERIOD_SECONDS, TimeUnit.SECONDS, this::advanceHead);
+ }
+
+ /**
+ * Releases resources used by the SlidingWindowCounter.
+ */
+ public void destroy() {
+ schedule.cancel();
+ }
+
+ /**
+ * Increments the count of the current window slot by 1.
+ */
+ public void incrementCount() {
+ incrementCount(headSlot, 1);
+ }
+
+ /**
+ * Increments the count of the current window slot by the given value.
+ *
+ * @param value value to increment by
+ */
+ public void incrementCount(long value) {
+ incrementCount(headSlot, value);
+ }
+
+ private void incrementCount(int slot, long value) {
+ counters.get(slot).addAndGet(value);
+ }
+
+ /**
+ * Gets the total count for the last N window slots.
+ *
+ * @param slots number of slots to include in the count
+ * @return total count for last N slots
+ */
+ public long get(int slots) {
+ checkArgument(slots <= windowSlots,
+ "Requested window must be less than the total window slots");
+
+ long sum = 0;
+
+ for (int i = 0; i < slots; i++) {
+ int currentIndex = headSlot - i;
+ if (currentIndex < 0) {
+ currentIndex = counters.size() + currentIndex;
+ }
+ sum += counters.get(currentIndex).get();
+ }
+
+ return sum;
+ }
+
+ void advanceHead() {
+ counters.get(slotAfter(headSlot)).set(0);
+ headSlot = slotAfter(headSlot);
+ }
+
+ private int slotAfter(int slot) {
+ return (slot + 1) % windowSlots;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.atomix.utils.misc;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Collection of various helper methods to manipulate strings.
+ */
+public final class StringUtils {
+
+ private StringUtils() {
+ }
+
+ /**
+ * Splits the input string with the given regex and filters empty strings.
+ *
+ * @param input the string to split.
+ * @return the array of strings computed by splitting this string
+ */
+ public static String[] split(String input, String regex) {
+ if (input == null) {
+ return null;
+ }
+ String[] arr = input.split(regex);
+ List<String> results = new ArrayList<>(arr.length);
+ for (String a : arr) {
+ if (!a.trim().isEmpty()) {
+ results.add(a);
+ }
+ }
+ return results.toArray(new String[0]);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.misc;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Timestamp printer.
+ */
+public class TimestampPrinter {
+
+ /**
+ * Returns a new timestamp printer.
+ *
+ * @param timestamp the timestamp to print
+ * @return the timestamp printer
+ */
+ public static TimestampPrinter of(long timestamp) {
+ return new TimestampPrinter(timestamp);
+ }
+
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss,SSS");
+
+ private final long timestamp;
+
+ public TimestampPrinter(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ @Override
+ public String toString() {
+ return FORMATTER.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()));
+ }
+}
--- /dev/null
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Miscellaneous utilities.
+ */
+package io.atomix.utils.misc;
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.net;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/**
+ * Representation of a network address.
+ */
+public final class Address {
+ private static final int DEFAULT_PORT = 5679;
+
+ /**
+ * Address type.
+ */
+ public enum Type {
+ IPV4,
+ IPV6,
+ }
+
+ /**
+ * Returns an address that binds to all interfaces.
+ *
+ * @return the address
+ */
+ public static Address local() {
+ return from(DEFAULT_PORT);
+ }
+
+ /**
+ * Returns the address from the given host:port string.
+ *
+ * @param address the address string
+ * @return the address
+ */
+ public static Address from(String address) {
+ int lastColon = address.lastIndexOf(':');
+ int openBracket = address.indexOf('[');
+ int closeBracket = address.indexOf(']');
+
+ String host;
+ if (openBracket != -1 && closeBracket != -1) {
+ host = address.substring(openBracket + 1, closeBracket);
+ } else if (lastColon != -1) {
+ host = address.substring(0, lastColon);
+ } else {
+ host = address;
+ }
+
+ int port;
+ if (lastColon != -1) {
+ try {
+ port = Integer.parseInt(address.substring(lastColon + 1));
+ } catch (NumberFormatException e) {
+ throw new MalformedAddressException(address, e);
+ }
+ } else {
+ port = DEFAULT_PORT;
+ }
+ return new Address(host, port);
+ }
+
+ /**
+ * Returns an address for the given host/port.
+ *
+ * @param host the host name
+ * @param port the port
+ * @return a new address
+ */
+ public static Address from(String host, int port) {
+ return new Address(host, port);
+ }
+
+ /**
+ * Returns an address for the local host and the given port.
+ *
+ * @param port the port
+ * @return a new address
+ */
+ public static Address from(int port) {
+ try {
+ InetAddress address = getLocalAddress();
+ return new Address(address.getHostName(), port);
+ } catch (UnknownHostException e) {
+ throw new IllegalArgumentException("Failed to locate host", e);
+ }
+ }
+
+ /**
+ * Returns the local host.
+ */
+ private static InetAddress getLocalAddress() throws UnknownHostException {
+ try {
+ return InetAddress.getLocalHost(); // first NIC
+ } catch (Exception ignore) {
+ return InetAddress.getByName(null);
+ }
+ }
+
+ private final String host;
+ private final int port;
+ private transient volatile Type type;
+ private transient volatile InetAddress address;
+
+ public Address(String host, int port) {
+ this(host, port, null);
+ }
+
+ public Address(String host, int port, InetAddress address) {
+ this.host = host;
+ this.port = port;
+ this.address = address;
+ if (address != null) {
+ this.type = address instanceof Inet6Address ? Type.IPV6 : Type.IPV4;
+ }
+ }
+
+ /**
+ * Returns the host name.
+ *
+ * @return the host name
+ */
+ public String host() {
+ return host;
+ }
+
+ /**
+ * Returns the port.
+ *
+ * @return the port
+ */
+ public int port() {
+ return port;
+ }
+
+ /**
+ * Returns the IP address.
+ *
+ * @return the IP address
+ */
+ public InetAddress address() {
+ return address(false);
+ }
+
+ /**
+ * Returns the IP address.
+ *
+ * @param resolve whether to force resolve the hostname
+ * @return the IP address
+ */
+ public InetAddress address(boolean resolve) {
+ if (resolve) {
+ address = resolveAddress();
+ return address;
+ }
+
+ if (address == null) {
+ synchronized (this) {
+ if (address == null) {
+ address = resolveAddress();
+ }
+ }
+ }
+ return address;
+ }
+
+ /**
+ * Resolves the IP address from the hostname.
+ *
+ * @return the resolved IP address or {@code null} if the IP could not be resolved
+ */
+ private InetAddress resolveAddress() {
+ try {
+ return InetAddress.getByName(host);
+ } catch (UnknownHostException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the address type.
+ *
+ * @return the address type
+ */
+ public Type type() {
+ if (type == null) {
+ synchronized (this) {
+ if (type == null) {
+ type = address() instanceof Inet6Address ? Type.IPV6 : Type.IPV4;
+ }
+ }
+ }
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ String host = host();
+ int port = port();
+ if (host.matches("([0-9a-f]{1,4}:){7}([0-9a-f]){1,4}")) {
+ return String.format("[%s]:%d", host, port);
+ } else {
+ return String.format("%s:%d", host, port);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(host, port);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Address)) {
+ return false;
+ }
+ Address that = (Address) obj;
+ return this.host.equals(that.host)
+ && this.port == that.port;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.net;
+
+import io.atomix.utils.AtomixRuntimeException;
+
+/**
+ * Malformed address exception.
+ */
+public class MalformedAddressException extends AtomixRuntimeException {
+ public MalformedAddressException(String message) {
+ super(message);
+ }
+
+ public MalformedAddressException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for representing and operating on IP addresses.
+ */
+package io.atomix.utils.net;
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides utility classes and interfaces used throughout Atomix projects.
+ */
+package io.atomix.utils;
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import java.io.ByteArrayOutputStream;
+
+/**
+ * Exposes protected byte array length in {@link ByteArrayOutputStream}.
+ */
+final class BufferAwareByteArrayOutputStream extends ByteArrayOutputStream {
+
+ BufferAwareByteArrayOutputStream(int size) {
+ super(size);
+ }
+
+ int getBufferSize() {
+ return buf.length;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import com.esotericsoftware.kryo.io.Output;
+
+/**
+ * Convenience class to avoid extra object allocation and casting.
+ */
+final class ByteArrayOutput extends Output {
+
+ private final BufferAwareByteArrayOutputStream stream;
+
+ ByteArrayOutput(final int bufferSize, final int maxBufferSize, final BufferAwareByteArrayOutputStream stream) {
+ super(bufferSize, maxBufferSize);
+ super.setOutputStream(stream);
+ this.stream = stream;
+ }
+
+ BufferAwareByteArrayOutputStream getByteArrayOutputStream() {
+ return stream;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import java.lang.ref.SoftReference;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.function.Function;
+
+abstract class KryoIOPool<T> {
+
+ private final Queue<SoftReference<T>> queue = new ConcurrentLinkedQueue<>();
+
+ private T borrow(final int bufferSize) {
+ T element;
+ SoftReference<T> reference;
+ while ((reference = queue.poll()) != null) {
+ if ((element = reference.get()) != null) {
+ return element;
+ }
+ }
+ return create(bufferSize);
+ }
+
+ protected abstract T create(final int bufferSize);
+
+ protected abstract boolean recycle(final T element);
+
+ <R> R run(final Function<T, R> function, final int bufferSize) {
+ final T element = borrow(bufferSize);
+ try {
+ return function.apply(element);
+ } finally {
+ if (recycle(element)) {
+ queue.offer(new SoftReference<>(element));
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import com.esotericsoftware.kryo.io.Input;
+
+class KryoInputPool extends KryoIOPool<Input> {
+
+ static final int MAX_POOLED_BUFFER_SIZE = 512 * 1024;
+
+ @Override
+ protected Input create(int bufferSize) {
+ return new Input(bufferSize);
+ }
+
+ @Override
+ protected boolean recycle(Input input) {
+ if (input.getBuffer().length < MAX_POOLED_BUFFER_SIZE) {
+ input.setInputStream(null);
+ return true;
+ }
+ return false; // discard
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+class KryoOutputPool extends KryoIOPool<ByteArrayOutput> {
+
+ private static final int MAX_BUFFER_SIZE = 768 * 1024;
+ static final int MAX_POOLED_BUFFER_SIZE = 512 * 1024;
+
+ @Override
+ protected ByteArrayOutput create(int bufferSize) {
+ return new ByteArrayOutput(bufferSize, MAX_BUFFER_SIZE, new BufferAwareByteArrayOutputStream(bufferSize));
+ }
+
+ @Override
+ protected boolean recycle(ByteArrayOutput output) {
+ if (output.getByteArrayOutputStream().getBufferSize() < MAX_POOLED_BUFFER_SIZE) {
+ output.getByteArrayOutputStream().reset();
+ output.clear();
+ return true;
+ }
+ return false; // discard
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Registration;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.ByteBufferInput;
+import com.esotericsoftware.kryo.io.ByteBufferOutput;
+import com.esotericsoftware.kryo.pool.KryoCallback;
+import com.esotericsoftware.kryo.pool.KryoFactory;
+import com.esotericsoftware.kryo.pool.KryoPool;
+import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import io.atomix.utils.config.ConfigurationException;
+import org.apache.commons.lang3.tuple.Pair;
+import org.objenesis.strategy.StdInstantiatorStrategy;
+import org.slf4j.Logger;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Pool of Kryo instances, with classes pre-registered.
+ */
+//@ThreadSafe
+public final class Namespace implements KryoFactory, KryoPool {
+
+ /**
+ * Default buffer size used for serialization.
+ *
+ * @see #serialize(Object)
+ */
+ public static final int DEFAULT_BUFFER_SIZE = 4096;
+
+ /**
+ * Maximum allowed buffer size.
+ */
+ public static final int MAX_BUFFER_SIZE = 100 * 1000 * 1000;
+
+ /**
+ * ID to use if this KryoNamespace does not define registration id.
+ */
+ public static final int FLOATING_ID = -1;
+
+ /**
+ * Smallest ID free to use for user defined registrations.
+ */
+ public static final int INITIAL_ID = 16;
+
+ static final String NO_NAME = "(no name)";
+
+ private static final Logger LOGGER = getLogger(Namespace.class);
+
+ /**
+ * Default Kryo namespace.
+ */
+ public static final Namespace DEFAULT = builder().build();
+
+ private final KryoPool kryoPool = new KryoPool.Builder(this)
+ .softReferences()
+ .build();
+
+ private final KryoOutputPool kryoOutputPool = new KryoOutputPool();
+ private final KryoInputPool kryoInputPool = new KryoInputPool();
+
+ private final ImmutableList<RegistrationBlock> registeredBlocks;
+
+ private final ClassLoader classLoader;
+ private final boolean compatible;
+ private final boolean registrationRequired;
+ private final String friendlyName;
+
+ /**
+ * KryoNamespace builder.
+ */
+ //@NotThreadSafe
+ public static final class Builder {
+ private int blockHeadId = INITIAL_ID;
+ private List<Pair<Class<?>[], Serializer<?>>> types = new ArrayList<>();
+ private List<RegistrationBlock> blocks = new ArrayList<>();
+ private ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ private boolean registrationRequired = true;
+ private boolean compatible = false;
+
+ /**
+ * Builds a {@link Namespace} instance.
+ *
+ * @return KryoNamespace
+ */
+ public Namespace build() {
+ return build(NO_NAME);
+ }
+
+ /**
+ * Builds a {@link Namespace} instance.
+ *
+ * @param friendlyName friendly name for the namespace
+ * @return KryoNamespace
+ */
+ public Namespace build(String friendlyName) {
+ if (!types.isEmpty()) {
+ blocks.add(new RegistrationBlock(this.blockHeadId, types));
+ }
+ return new Namespace(blocks, classLoader, registrationRequired, compatible, friendlyName).populate(1);
+ }
+
+ /**
+ * Sets the next Kryo registration Id for following register entries.
+ *
+ * @param id Kryo registration Id
+ * @return this
+ * @see Kryo#register(Class, Serializer, int)
+ */
+ public Builder nextId(final int id) {
+ if (!types.isEmpty()) {
+ if (id != FLOATING_ID && id < blockHeadId + types.size()) {
+
+ if (LOGGER.isWarnEnabled()) {
+ LOGGER.warn("requested nextId {} could potentially overlap "
+ + "with existing registrations {}+{} ",
+ id, blockHeadId, types.size(), new RuntimeException());
+ }
+ }
+ blocks.add(new RegistrationBlock(this.blockHeadId, types));
+ types = new ArrayList<>();
+ }
+ this.blockHeadId = id;
+ return this;
+ }
+
+ /**
+ * Registers classes to be serialized using Kryo default serializer.
+ *
+ * @param expectedTypes list of classes
+ * @return this
+ */
+ public Builder register(final Class<?>... expectedTypes) {
+ for (Class<?> clazz : expectedTypes) {
+ types.add(Pair.of(new Class<?>[]{clazz}, null));
+ }
+ return this;
+ }
+
+ /**
+ * Registers serializer for the given set of classes.
+ * <p>
+ * When multiple classes are registered with an explicitly provided serializer, the namespace guarantees
+ * all instances will be serialized with the same type ID.
+ *
+ * @param classes list of classes to register
+ * @param serializer serializer to use for the class
+ * @return this
+ */
+ public Builder register(Serializer<?> serializer, final Class<?>... classes) {
+ types.add(Pair.of(classes, checkNotNull(serializer)));
+ return this;
+ }
+
+ private Builder register(RegistrationBlock block) {
+ if (block.begin() != FLOATING_ID) {
+ // flush pending types
+ nextId(block.begin());
+ blocks.add(block);
+ nextId(block.begin() + block.types().size());
+ } else {
+ // flush pending types
+ final int addedBlockBegin = blockHeadId + types.size();
+ nextId(addedBlockBegin);
+ blocks.add(new RegistrationBlock(addedBlockBegin, block.types()));
+ nextId(addedBlockBegin + block.types().size());
+ }
+ return this;
+ }
+
+ /**
+ * Registers all the class registered to given KryoNamespace.
+ *
+ * @param ns KryoNamespace
+ * @return this
+ */
+ public Builder register(final Namespace ns) {
+
+ if (blocks.containsAll(ns.registeredBlocks)) {
+ // Everything was already registered.
+ LOGGER.debug("Ignoring {}, already registered.", ns);
+ return this;
+ }
+ for (RegistrationBlock block : ns.registeredBlocks) {
+ this.register(block);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the namespace class loader.
+ *
+ * @param classLoader the namespace class loader
+ * @return the namespace builder
+ */
+ public Builder setClassLoader(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ return this;
+ }
+
+ /**
+ * Sets whether backwards/forwards compatible versioned serialization is enabled.
+ * <p>
+ * When compatible serialization is enabled, the {@link CompatibleFieldSerializer} will be set as the
+ * default serializer for types that do not otherwise explicitly specify a serializer.
+ *
+ * @param compatible whether versioned serialization is enabled
+ * @return this
+ */
+ public Builder setCompatible(boolean compatible) {
+ this.compatible = compatible;
+ return this;
+ }
+
+ /**
+ * Sets the registrationRequired flag.
+ *
+ * @param registrationRequired Kryo's registrationRequired flag
+ * @return this
+ * @see Kryo#setRegistrationRequired(boolean)
+ */
+ public Builder setRegistrationRequired(boolean registrationRequired) {
+ this.registrationRequired = registrationRequired;
+ return this;
+ }
+ }
+
+ /**
+ * Creates a new {@link Namespace} builder.
+ *
+ * @return builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<RegistrationBlock> buildRegistrationBlocks(NamespaceConfig config) {
+ List<Pair<Class<?>[], Serializer<?>>> types = new ArrayList<>();
+ List<RegistrationBlock> blocks = new ArrayList<>();
+ blocks.addAll(Namespaces.BASIC.registeredBlocks);
+ for (NamespaceTypeConfig type : config.getTypes()) {
+ try {
+ if (type.getId() == null) {
+ types.add(Pair.of(new Class[]{type.getType()}, type.getSerializer() != null ? type.getSerializer().newInstance() : null));
+ } else {
+ blocks.add(new RegistrationBlock(type.getId(), Collections.singletonList(Pair.of(new Class[]{type.getType()}, type.getSerializer().newInstance()))));
+ }
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new ConfigurationException("Failed to instantiate serializer from configuration", e);
+ }
+ }
+ blocks.add(new RegistrationBlock(FLOATING_ID, types));
+ return blocks;
+ }
+
+ public Namespace(NamespaceConfig config) {
+ this(buildRegistrationBlocks(config), Thread.currentThread().getContextClassLoader(), config.isRegistrationRequired(), config.isCompatible(), config.getName());
+ }
+
+ /**
+ * Creates a Kryo instance pool.
+ *
+ * @param registeredTypes types to register
+ * @param registrationRequired whether registration is required
+ * @param compatible whether compatible serialization is enabled
+ * @param friendlyName friendly name for the namespace
+ */
+ private Namespace(
+ final List<RegistrationBlock> registeredTypes,
+ ClassLoader classLoader,
+ boolean registrationRequired,
+ boolean compatible,
+ String friendlyName) {
+ this.registeredBlocks = ImmutableList.copyOf(registeredTypes);
+ this.registrationRequired = registrationRequired;
+ this.classLoader = classLoader;
+ this.compatible = compatible;
+ this.friendlyName = checkNotNull(friendlyName);
+ }
+
+ /**
+ * Populates the Kryo pool.
+ *
+ * @param instances to add to the pool
+ * @return this
+ */
+ public Namespace populate(int instances) {
+
+ for (int i = 0; i < instances; ++i) {
+ release(create());
+ }
+ return this;
+ }
+
+ /**
+ * Serializes given object to byte array using Kryo instance in pool.
+ * <p>
+ * Note: Serialized bytes must be smaller than {@link #MAX_BUFFER_SIZE}.
+ *
+ * @param obj Object to serialize
+ * @return serialized bytes
+ */
+ public byte[] serialize(final Object obj) {
+ return serialize(obj, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * Serializes given object to byte array using Kryo instance in pool.
+ *
+ * @param obj Object to serialize
+ * @param bufferSize maximum size of serialized bytes
+ * @return serialized bytes
+ */
+ public byte[] serialize(final Object obj, final int bufferSize) {
+ return kryoOutputPool.run(output -> {
+ return kryoPool.run(kryo -> {
+ kryo.writeClassAndObject(output, obj);
+ output.flush();
+ return output.getByteArrayOutputStream().toByteArray();
+ });
+ }, bufferSize);
+ }
+
+ /**
+ * Serializes given object to byte buffer using Kryo instance in pool.
+ *
+ * @param obj Object to serialize
+ * @param buffer to write to
+ */
+ public void serialize(final Object obj, final ByteBuffer buffer) {
+ ByteBufferOutput out = new ByteBufferOutput(buffer);
+ Kryo kryo = borrow();
+ try {
+ kryo.writeClassAndObject(out, obj);
+ out.flush();
+ } finally {
+ release(kryo);
+ }
+ }
+
+ /**
+ * Serializes given object to OutputStream using Kryo instance in pool.
+ *
+ * @param obj Object to serialize
+ * @param stream to write to
+ */
+ public void serialize(final Object obj, final OutputStream stream) {
+ serialize(obj, stream, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * Serializes given object to OutputStream using Kryo instance in pool.
+ *
+ * @param obj Object to serialize
+ * @param stream to write to
+ * @param bufferSize size of the buffer in front of the stream
+ */
+ public void serialize(final Object obj, final OutputStream stream, final int bufferSize) {
+ ByteBufferOutput out = new ByteBufferOutput(stream, bufferSize);
+ Kryo kryo = borrow();
+ try {
+ kryo.writeClassAndObject(out, obj);
+ out.flush();
+ } finally {
+ release(kryo);
+ }
+ }
+
+ /**
+ * Deserializes given byte array to Object using Kryo instance in pool.
+ *
+ * @param bytes serialized bytes
+ * @param <T> deserialized Object type
+ * @return deserialized Object
+ */
+ public <T> T deserialize(final byte[] bytes) {
+ return kryoInputPool.run(input -> {
+ input.setInputStream(new ByteArrayInputStream(bytes));
+ return kryoPool.run(kryo -> {
+ @SuppressWarnings("unchecked")
+ T obj = (T) kryo.readClassAndObject(input);
+ return obj;
+ });
+ }, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * Deserializes given byte buffer to Object using Kryo instance in pool.
+ *
+ * @param buffer input with serialized bytes
+ * @param <T> deserialized Object type
+ * @return deserialized Object
+ */
+ public <T> T deserialize(final ByteBuffer buffer) {
+ ByteBufferInput in = new ByteBufferInput(buffer);
+ Kryo kryo = borrow();
+ try {
+ @SuppressWarnings("unchecked")
+ T obj = (T) kryo.readClassAndObject(in);
+ return obj;
+ } finally {
+ release(kryo);
+ }
+ }
+
+ /**
+ * Deserializes given InputStream to an Object using Kryo instance in pool.
+ *
+ * @param stream input stream
+ * @param <T> deserialized Object type
+ * @return deserialized Object
+ */
+ public <T> T deserialize(final InputStream stream) {
+ return deserialize(stream, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * Deserializes given InputStream to an Object using Kryo instance in pool.
+ *
+ * @param stream input stream
+ * @param <T> deserialized Object type
+ * @param bufferSize size of the buffer in front of the stream
+ * @return deserialized Object
+ */
+ public <T> T deserialize(final InputStream stream, final int bufferSize) {
+ ByteBufferInput in = new ByteBufferInput(stream, bufferSize);
+ Kryo kryo = borrow();
+ try {
+ @SuppressWarnings("unchecked")
+ T obj = (T) kryo.readClassAndObject(in);
+ return obj;
+ } finally {
+ release(kryo);
+ }
+ }
+
+ private String friendlyName() {
+ return friendlyName;
+ }
+
+ /**
+ * Gets the number of classes registered in this Kryo namespace.
+ *
+ * @return size of namespace
+ */
+ public int size() {
+ return (int) registeredBlocks.stream()
+ .flatMap(block -> block.types().stream())
+ .count();
+ }
+
+ /**
+ * Creates a Kryo instance.
+ *
+ * @return Kryo instance
+ */
+ @Override
+ public Kryo create() {
+ LOGGER.trace("Creating Kryo instance for {}", this);
+ Kryo kryo = new Kryo();
+ kryo.setClassLoader(classLoader);
+ kryo.setRegistrationRequired(registrationRequired);
+
+ // If compatible serialization is enabled, override the default serializer.
+ if (compatible) {
+ kryo.setDefaultSerializer(CompatibleFieldSerializer::new);
+ }
+
+ // TODO rethink whether we want to use StdInstantiatorStrategy
+ kryo.setInstantiatorStrategy(
+ new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
+
+ for (RegistrationBlock block : registeredBlocks) {
+ int id = block.begin();
+ if (id == FLOATING_ID) {
+ id = kryo.getNextRegistrationId();
+ }
+ for (Pair<Class<?>[], Serializer<?>> entry : block.types()) {
+ register(kryo, entry.getLeft(), entry.getRight(), id++);
+ }
+ }
+ return kryo;
+ }
+
+ /**
+ * Register {@code type} and {@code serializer} to {@code kryo} instance.
+ *
+ * @param kryo Kryo instance
+ * @param types types to register
+ * @param serializer Specific serializer to register or null to use default.
+ * @param id type registration id to use
+ */
+ private void register(Kryo kryo, Class<?>[] types, Serializer<?> serializer, int id) {
+ Registration existing = kryo.getRegistration(id);
+ if (existing != null) {
+ boolean matches = false;
+ for (Class<?> type : types) {
+ if (existing.getType() == type) {
+ matches = true;
+ break;
+ }
+ }
+
+ if (!matches) {
+ LOGGER.error("{}: Failed to register {} as {}, {} was already registered.",
+ friendlyName(), types, id, existing.getType());
+
+ throw new IllegalStateException(String.format(
+ "Failed to register %s as %s, %s was already registered.",
+ Arrays.toString(types), id, existing.getType()));
+ }
+ // falling through to register call for now.
+ // Consider skipping, if there's reasonable
+ // way to compare serializer equivalence.
+ }
+
+ for (Class<?> type : types) {
+ Registration r = null;
+ if (serializer == null) {
+ r = kryo.register(type, id);
+ } else if (type.isInterface()) {
+ kryo.addDefaultSerializer(type, serializer);
+ } else {
+ r = kryo.register(type, serializer, id);
+ }
+ if (r != null) {
+ if (r.getId() != id) {
+ LOGGER.debug("{}: {} already registered as {}. Skipping {}.",
+ friendlyName(), r.getType(), r.getId(), id);
+ }
+ LOGGER.trace("{} registered as {}", r.getType(), r.getId());
+ }
+ }
+ }
+
+ @Override
+ public Kryo borrow() {
+ return kryoPool.borrow();
+ }
+
+ @Override
+ public void release(Kryo kryo) {
+ kryoPool.release(kryo);
+ }
+
+ @Override
+ public <T> T run(KryoCallback<T> callback) {
+ return kryoPool.run(callback);
+ }
+
+ @Override
+ public String toString() {
+ if (!NO_NAME.equals(friendlyName)) {
+ return MoreObjects.toStringHelper(getClass())
+ .omitNullValues()
+ .add("friendlyName", friendlyName)
+ // omit lengthy detail, when there's a name
+ .toString();
+ }
+ return MoreObjects.toStringHelper(getClass())
+ .add("registeredBlocks", registeredBlocks)
+ .toString();
+ }
+
+ static final class RegistrationBlock {
+ private final int begin;
+ private final ImmutableList<Pair<Class<?>[], Serializer<?>>> types;
+
+ RegistrationBlock(int begin, List<Pair<Class<?>[], Serializer<?>>> types) {
+ this.begin = begin;
+ this.types = ImmutableList.copyOf(types);
+ }
+
+ public int begin() {
+ return begin;
+ }
+
+ public ImmutableList<Pair<Class<?>[], Serializer<?>>> types() {
+ return types;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(getClass())
+ .add("begin", begin)
+ .add("types", types)
+ .toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return types.hashCode();
+ }
+
+ // Only the registered types are used for equality.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj instanceof RegistrationBlock) {
+ RegistrationBlock that = (RegistrationBlock) obj;
+ return Objects.equals(this.types, that.types);
+ }
+ return false;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import io.atomix.utils.config.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Namespace configuration.
+ */
+public class NamespaceConfig implements Config {
+ private String name = Namespace.NO_NAME;
+ private boolean registrationRequired = true;
+ private boolean compatible = false;
+ private List<NamespaceTypeConfig> types = new ArrayList<>();
+
+ /**
+ * Returns the serializer name.
+ *
+ * @return the serializer name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the serializer name.
+ *
+ * @param name the serializer name
+ * @return the serializer configuration
+ */
+ public NamespaceConfig setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Returns whether registration is required.
+ *
+ * @return whether registration is required
+ */
+ public boolean isRegistrationRequired() {
+ return registrationRequired;
+ }
+
+ /**
+ * Sets whether registration is required.
+ *
+ * @param registrationRequired whether registration is required
+ * @return the serializer configuration
+ */
+ public NamespaceConfig setRegistrationRequired(boolean registrationRequired) {
+ this.registrationRequired = registrationRequired;
+ return this;
+ }
+
+ /**
+ * Returns whether compatible serialization is enabled.
+ *
+ * @return whether compatible serialization is enabled
+ */
+ public boolean isCompatible() {
+ return compatible;
+ }
+
+ /**
+ * Sets whether compatible serialization is enabled.
+ *
+ * @param compatible whether compatible serialization is enabled
+ * @return the serializer configuration
+ */
+ public NamespaceConfig setCompatible(boolean compatible) {
+ this.compatible = compatible;
+ return this;
+ }
+
+ /**
+ * Returns the serializable types.
+ *
+ * @return the serializable types
+ */
+ public List<NamespaceTypeConfig> getTypes() {
+ return types;
+ }
+
+ /**
+ * Sets the serializable types.
+ *
+ * @param types the serializable types
+ * @return the serializer configuration
+ */
+ public NamespaceConfig setTypes(List<NamespaceTypeConfig> types) {
+ this.types = types;
+ return this;
+ }
+
+ /**
+ * Adds a serializable type to the configuration.
+ *
+ * @param type the serializable type to add
+ * @return the serializer configuration
+ */
+ public NamespaceConfig addType(NamespaceTypeConfig type) {
+ types.add(type);
+ return this;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import com.esotericsoftware.kryo.Serializer;
+import io.atomix.utils.config.Config;
+
+/**
+ * Namespace type configuration.
+ */
+public class NamespaceTypeConfig implements Config {
+ private Class<?> type;
+ private Integer id;
+ private Class<? extends com.esotericsoftware.kryo.Serializer> serializer;
+
+ /**
+ * Returns the serializable type.
+ *
+ * @return the serializable type
+ */
+ public Class<?> getType() {
+ return type;
+ }
+
+ /**
+ * Sets the serializable type.
+ *
+ * @param type the serializable type
+ * @return the type configuration
+ */
+ public NamespaceTypeConfig setType(Class<?> type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Returns the type identifier.
+ *
+ * @return the type identifier
+ */
+ public Integer getId() {
+ return id;
+ }
+
+ /**
+ * Sets the type identifier.
+ *
+ * @param id the type identifier
+ * @return the type configuration
+ */
+ public NamespaceTypeConfig setId(Integer id) {
+ this.id = id;
+ return this;
+ }
+
+ /**
+ * Returns the serializer class.
+ *
+ * @return the serializer class
+ */
+ public Class<? extends Serializer> getSerializer() {
+ return serializer;
+ }
+
+ /**
+ * Sets the serializer class.
+ *
+ * @param serializer the serializer class
+ * @return the type configuration
+ */
+ public NamespaceTypeConfig setSerializer(Class<? extends Serializer> serializer) {
+ this.serializer = serializer;
+ return this;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multisets;
+import com.google.common.collect.Sets;
+import io.atomix.utils.Version;
+import io.atomix.utils.serializer.serializers.ArraysAsListSerializer;
+import io.atomix.utils.serializer.serializers.ImmutableListSerializer;
+import io.atomix.utils.serializer.serializers.ImmutableMapSerializer;
+import io.atomix.utils.serializer.serializers.ImmutableSetSerializer;
+import io.atomix.utils.time.LogicalTimestamp;
+import io.atomix.utils.time.WallClockTimestamp;
+
+import java.time.Duration;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+public final class Namespaces {
+ public static final int BASIC_MAX_SIZE = 50;
+ public static final Namespace BASIC = Namespace.builder()
+ .nextId(Namespace.FLOATING_ID)
+ .register(byte[].class)
+ .register(AtomicBoolean.class)
+ .register(AtomicInteger.class)
+ .register(AtomicLong.class)
+ .register(new ImmutableListSerializer(),
+ ImmutableList.class,
+ ImmutableList.of(1).getClass(),
+ ImmutableList.of(1, 2).getClass(),
+ ImmutableList.of(1, 2, 3).subList(1, 3).getClass())
+ .register(new ImmutableSetSerializer(),
+ ImmutableSet.class,
+ ImmutableSet.of().getClass(),
+ ImmutableSet.of(1).getClass(),
+ ImmutableSet.of(1, 2).getClass())
+ .register(new ImmutableMapSerializer(),
+ ImmutableMap.class,
+ ImmutableMap.of().getClass(),
+ ImmutableMap.of("a", 1).getClass(),
+ ImmutableMap.of("R", 2, "D", 2).getClass())
+ .register(Collections.unmodifiableSet(Collections.emptySet()).getClass())
+ .register(HashMap.class)
+ .register(ConcurrentHashMap.class)
+ .register(CopyOnWriteArraySet.class)
+ .register(
+ ArrayList.class,
+ LinkedList.class,
+ HashSet.class,
+ LinkedHashSet.class,
+ ArrayDeque.class
+ )
+ .register(HashMultiset.class)
+ .register(Multisets.immutableEntry("", 0).getClass())
+ .register(Sets.class)
+ .register(Maps.immutableEntry("a", "b").getClass())
+ .register(new ArraysAsListSerializer(), Arrays.asList().getClass())
+ .register(Collections.singletonList(1).getClass())
+ .register(Duration.class)
+ .register(Collections.emptySet().getClass())
+ .register(Optional.class)
+ .register(Collections.emptyList().getClass())
+ .register(Collections.singleton(Object.class).getClass())
+ .register(Properties.class)
+ .register(int[].class)
+ .register(long[].class)
+ .register(short[].class)
+ .register(double[].class)
+ .register(float[].class)
+ .register(char[].class)
+ .register(String[].class)
+ .register(boolean[].class)
+ .register(Object[].class)
+ .register(LogicalTimestamp.class)
+ .register(WallClockTimestamp.class)
+ .register(Version.class)
+ .build("BASIC");
+
+ /**
+ * Kryo registration Id for user custom registration.
+ */
+ public static final int BEGIN_USER_CUSTOM_ID = 500;
+
+ // not to be instantiated
+ private Namespaces() {
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+/**
+ * Interface for serialization of store artifacts.
+ */
+public interface Serializer {
+
+ /**
+ * Creates a new serializer builder.
+ *
+ * @return a new serializer builder
+ */
+ static SerializerBuilder builder() {
+ return new SerializerBuilder();
+ }
+
+ /**
+ * Creates a new serializer builder.
+ *
+ * @param name the serializer name
+ * @return a new serializer builder
+ */
+ static SerializerBuilder builder(String name) {
+ return new SerializerBuilder(name);
+ }
+
+ /**
+ * Serialize the specified object.
+ *
+ * @param object object to serialize.
+ * @param <T> encoded type
+ * @return serialized bytes.
+ */
+ <T> byte[] encode(T object);
+
+ /**
+ * Deserialize the specified bytes.
+ *
+ * @param bytes byte array to deserialize.
+ * @param <T> decoded type
+ * @return deserialized object.
+ */
+ <T> T decode(byte[] bytes);
+
+ /**
+ * Creates a new Serializer instance from a Namespace.
+ *
+ * @param namespace serializer namespace
+ * @return Serializer instance
+ */
+ static Serializer using(Namespace namespace) {
+ return new Serializer() {
+ @Override
+ public <T> byte[] encode(T object) {
+ return namespace.serialize(object);
+ }
+
+ @Override
+ public <T> T decode(byte[] bytes) {
+ return namespace.deserialize(bytes);
+ }
+ };
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import io.atomix.utils.Builder;
+
+/**
+ * Serializer builder.
+ */
+public class SerializerBuilder implements Builder<Serializer> {
+ private final String name;
+ private final Namespace.Builder namespaceBuilder = Namespace.builder()
+ .register(Namespaces.BASIC)
+ .nextId(Namespaces.BEGIN_USER_CUSTOM_ID);
+
+ public SerializerBuilder() {
+ this(null);
+ }
+
+ public SerializerBuilder(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Requires explicit serializable type registration for serializable types.
+ *
+ * @return the serializer builder
+ */
+ public SerializerBuilder withRegistrationRequired() {
+ return withRegistrationRequired(true);
+ }
+
+ /**
+ * Sets whether serializable type registration is required for serializable types.
+ *
+ * @param registrationRequired whether serializable type registration is required for serializable types
+ * @return the serializer builder
+ */
+ public SerializerBuilder withRegistrationRequired(boolean registrationRequired) {
+ namespaceBuilder.setRegistrationRequired(registrationRequired);
+ return this;
+ }
+
+ /**
+ * Enables compatible serialization for serializable types.
+ *
+ * @return the serializer builder
+ */
+ public SerializerBuilder withCompatibleSerialization() {
+ return withCompatibleSerialization(true);
+ }
+
+ /**
+ * Sets whether compatible serialization is enabled for serializable types.
+ *
+ * @param compatibleSerialization whether compatible serialization is enabled for user types
+ * @return the serializer builder
+ */
+ public SerializerBuilder withCompatibleSerialization(boolean compatibleSerialization) {
+ namespaceBuilder.setCompatible(compatibleSerialization);
+ return this;
+ }
+
+ /**
+ * Adds a namespace to the serializer.
+ *
+ * @param namespace the namespace to add
+ * @return the serializer builder
+ */
+ public SerializerBuilder withNamespace(Namespace namespace) {
+ namespaceBuilder.register(namespace);
+ return this;
+ }
+
+ /**
+ * Sets the serializable types.
+ *
+ * @param types the types to register
+ * @return the serializer builder
+ */
+ public SerializerBuilder withTypes(Class<?>... types) {
+ namespaceBuilder.register(types);
+ return this;
+ }
+
+ /**
+ * Adds a serializable type to the builder.
+ *
+ * @param type the type to add
+ * @return the serializer builder
+ */
+ public SerializerBuilder addType(Class<?> type) {
+ namespaceBuilder.register(type);
+ return this;
+ }
+
+ /**
+ * Adds a serializer to the builder.
+ *
+ * @param serializer the serializer to add
+ * @param types the serializable types
+ * @return the serializer builder
+ */
+ public SerializerBuilder addSerializer(com.esotericsoftware.kryo.Serializer serializer, Class<?>... types) {
+ namespaceBuilder.register(serializer, types);
+ return this;
+ }
+
+ @Override
+ public Serializer build() {
+ return Serializer.using(name != null ? namespaceBuilder.build(name) : namespaceBuilder.build());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for binary serialization.
+ */
+package io.atomix.utils.serializer;
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer.serializers;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Kryo Serializer for {@link java.util.Arrays#asList(Object...)}.
+ */
+public final class ArraysAsListSerializer extends Serializer<List<?>> {
+
+ @Override
+ public void write(Kryo kryo, Output output, List<?> object) {
+ output.writeInt(object.size(), true);
+ for (Object elm : object) {
+ kryo.writeClassAndObject(output, elm);
+ }
+ }
+
+ @Override
+ public List<?> read(Kryo kryo, Input input, Class<List<?>> type) {
+ final int size = input.readInt(true);
+ List<Object> list = new ArrayList<>(size);
+ for (int i = 0; i < size; ++i) {
+ list.add(kryo.readClassAndObject(input));
+ }
+ return list;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer.serializers;
+
+import io.atomix.utils.serializer.Serializer;
+
+/**
+ * Default serializers.
+ */
+public class DefaultSerializers {
+
+ /**
+ * Basic serializer.
+ */
+ public static final Serializer BASIC = Serializer.builder().build();
+
+ private DefaultSerializers() {
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer.serializers;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Creates {@link ImmutableList} serializer instance.
+ */
+public class ImmutableListSerializer extends Serializer<ImmutableList<?>> {
+
+ /**
+ * Creates {@link ImmutableList} serializer instance.
+ */
+ public ImmutableListSerializer() {
+ // non-null, immutable
+ super(false, true);
+ }
+
+ @Override
+ public void write(Kryo kryo, Output output, ImmutableList<?> object) {
+ output.writeInt(object.size());
+ for (Object e : object) {
+ kryo.writeClassAndObject(output, e);
+ }
+ }
+
+ @Override
+ public ImmutableList<?> read(Kryo kryo, Input input,
+ Class<ImmutableList<?>> type) {
+ final int size = input.readInt();
+ switch (size) {
+ case 0:
+ return ImmutableList.of();
+ case 1:
+ return ImmutableList.of(kryo.readClassAndObject(input));
+ default:
+ Object[] elms = new Object[size];
+ for (int i = 0; i < size; ++i) {
+ elms[i] = kryo.readClassAndObject(input);
+ }
+ return ImmutableList.copyOf(elms);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer.serializers;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+
+import java.util.Map.Entry;
+
+/**
+ * Kryo Serializer for {@link ImmutableMap}.
+ */
+public class ImmutableMapSerializer extends Serializer<ImmutableMap<?, ?>> {
+
+ /**
+ * Creates {@link ImmutableMap} serializer instance.
+ */
+ public ImmutableMapSerializer() {
+ // non-null, immutable
+ super(false, true);
+ }
+
+ @Override
+ public void write(Kryo kryo, Output output, ImmutableMap<?, ?> object) {
+ output.writeInt(object.size());
+ for (Entry<?, ?> e : object.entrySet()) {
+ kryo.writeClassAndObject(output, e.getKey());
+ kryo.writeClassAndObject(output, e.getValue());
+ }
+ }
+
+ @Override
+ public ImmutableMap<?, ?> read(Kryo kryo, Input input,
+ Class<ImmutableMap<?, ?>> type) {
+ final int size = input.readInt();
+ switch (size) {
+ case 0:
+ return ImmutableMap.of();
+ case 1:
+ return ImmutableMap.of(kryo.readClassAndObject(input),
+ kryo.readClassAndObject(input));
+
+ default:
+ Builder<Object, Object> builder = ImmutableMap.builder();
+ for (int i = 0; i < size; ++i) {
+ builder.put(kryo.readClassAndObject(input),
+ kryo.readClassAndObject(input));
+ }
+ return builder.build();
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer.serializers;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Kryo Serializer for {@link ImmutableSet}.
+ */
+public class ImmutableSetSerializer extends Serializer<ImmutableSet<?>> {
+
+ /**
+ * Creates {@link ImmutableSet} serializer instance.
+ */
+ public ImmutableSetSerializer() {
+ // non-null, immutable
+ super(false, true);
+ }
+
+ @Override
+ public void write(Kryo kryo, Output output, ImmutableSet<?> object) {
+ output.writeInt(object.size());
+ for (Object e : object) {
+ kryo.writeClassAndObject(output, e);
+ }
+ }
+
+ @Override
+ public ImmutableSet<?> read(Kryo kryo, Input input,
+ Class<ImmutableSet<?>> type) {
+ final int size = input.readInt();
+ switch (size) {
+ case 0:
+ return ImmutableSet.of();
+ case 1:
+ return ImmutableSet.of(kryo.readClassAndObject(input));
+ default:
+ Object[] elms = new Object[size];
+ for (int i = 0; i < size; ++i) {
+ elms[i] = kryo.readClassAndObject(input);
+ }
+ return ImmutableSet.copyOf(elms);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Common serializer implementations.
+ */
+package io.atomix.utils.serializer.serializers;
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+/**
+ * Clock.
+ */
+public interface Clock<T extends Timestamp> {
+
+ /**
+ * Returns the current time of the clock.
+ *
+ * @return the current time
+ */
+ T getTime();
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+/**
+ * Epoch.
+ * <p>
+ * An epoch is a specific type of {@link LogicalTimestamp} that represents a long term section of logical time.
+ */
+public class Epoch extends LogicalTimestamp {
+
+ /**
+ * Returns a new logical timestamp for the given logical time.
+ *
+ * @param value the logical time for which to create a new logical timestamp
+ * @return the logical timestamp
+ */
+ public static Epoch of(long value) {
+ return new Epoch(value);
+ }
+
+ /**
+ * Creates a new epoch timestamp.
+ *
+ * @param value the epoch value
+ */
+ public Epoch(long value) {
+ super(value);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Logical clock.
+ */
+public class LogicalClock implements Clock<LogicalTimestamp> {
+ private LogicalTimestamp currentTimestamp;
+
+ public LogicalClock() {
+ this(new LogicalTimestamp(0));
+ }
+
+ public LogicalClock(LogicalTimestamp currentTimestamp) {
+ this.currentTimestamp = currentTimestamp;
+ }
+
+ @Override
+ public LogicalTimestamp getTime() {
+ return currentTimestamp;
+ }
+
+ /**
+ * Increments the clock and returns the new timestamp.
+ *
+ * @return the updated clock time
+ */
+ public LogicalTimestamp increment() {
+ return update(new LogicalTimestamp(currentTimestamp.value() + 1));
+ }
+
+ /**
+ * Updates the clock using the given timestamp.
+ *
+ * @param timestamp the timestamp with which to update the clock
+ * @return the updated clock time
+ */
+ public LogicalTimestamp update(LogicalTimestamp timestamp) {
+ if (timestamp.value() > currentTimestamp.value()) {
+ this.currentTimestamp = timestamp;
+ }
+ return currentTimestamp;
+ }
+
+ /**
+ * Increments the clock and updates it using the given timestamp.
+ *
+ * @param timestamp the timestamp with which to update the clock
+ * @return the updated clock time
+ */
+ public LogicalTimestamp incrementAndUpdate(LogicalTimestamp timestamp) {
+ long nextValue = currentTimestamp.value() + 1;
+ if (timestamp.value() > nextValue) {
+ return update(timestamp);
+ }
+ return increment();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("time", getTime())
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ComparisonChain;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Timestamp based on logical sequence value.
+ * <p>
+ * LogicalTimestamps are ordered by their sequence values.
+ */
+public class LogicalTimestamp implements Timestamp {
+
+ /**
+ * Returns a new logical timestamp for the given logical time.
+ *
+ * @param value the logical time for which to create a new logical timestamp
+ * @return the logical timestamp
+ */
+ public static LogicalTimestamp of(long value) {
+ return new LogicalTimestamp(value);
+ }
+
+ private final long value;
+
+ public LogicalTimestamp(long value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the sequence value.
+ *
+ * @return sequence value
+ */
+ public long value() {
+ return this.value;
+ }
+
+ /**
+ * Returns the timestamp as a version.
+ *
+ * @return the timestamp as a version
+ */
+ public Version asVersion() {
+ return new Version(value);
+ }
+
+ @Override
+ public int compareTo(Timestamp o) {
+ Preconditions.checkArgument(o instanceof LogicalTimestamp,
+ "Must be LogicalTimestamp", o);
+ LogicalTimestamp that = (LogicalTimestamp) o;
+
+ return ComparisonChain.start()
+ .compare(this.value, that.value)
+ .result();
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof LogicalTimestamp)) {
+ return false;
+ }
+ LogicalTimestamp that = (LogicalTimestamp) obj;
+ return Objects.equals(this.value, that.value);
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(getClass())
+ .add("value", value)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ComparisonChain;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * A logical timestamp that derives its value from two input values. The first
+ * value always takes precedence over the second value when comparing timestamps.
+ */
+public class MultiValuedTimestamp<T extends Comparable<T>, U extends Comparable<U>> implements Timestamp {
+ private final T value1;
+ private final U value2;
+
+ /**
+ * Creates a new timestamp based on two values. The first value has higher
+ * precedence than the second when comparing timestamps.
+ *
+ * @param value1 first value
+ * @param value2 second value
+ */
+ public MultiValuedTimestamp(T value1, U value2) {
+ this.value1 = Preconditions.checkNotNull(value1);
+ this.value2 = Preconditions.checkNotNull(value2);
+ }
+
+ @Override
+ public int compareTo(Timestamp o) {
+ Preconditions.checkArgument(o instanceof MultiValuedTimestamp,
+ "Must be MultiValuedTimestamp", o);
+ MultiValuedTimestamp that = (MultiValuedTimestamp) o;
+
+ return ComparisonChain.start()
+ .compare(this.value1, that.value1)
+ .compare(this.value2, that.value2)
+ .result();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(value1, value2);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof MultiValuedTimestamp)) {
+ return false;
+ }
+ MultiValuedTimestamp that = (MultiValuedTimestamp) obj;
+ return Objects.equals(this.value1, that.value1)
+ && Objects.equals(this.value2, that.value2);
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(getClass())
+ .add("value1", value1)
+ .add("value2", value2)
+ .toString();
+ }
+
+ /**
+ * Returns the first value.
+ *
+ * @return first value
+ */
+ public T value1() {
+ return value1;
+ }
+
+ /**
+ * Returns the second value.
+ *
+ * @return second value
+ */
+ public U value2() {
+ return value2;
+ }
+
+ // Default constructor for serialization
+ @SuppressWarnings("unused")
+ private MultiValuedTimestamp() {
+ this.value1 = null;
+ this.value2 = null;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2014-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Opaque version structure.
+ * <p>
+ * Classes implementing this interface must also implement
+ * {@link #hashCode()} and {@link #equals(Object)}.
+ */
+public interface Timestamp extends Comparable<Timestamp> {
+
+ @Override
+ int hashCode();
+
+ @Override
+ boolean equals(Object obj);
+
+ /**
+ * Tests if this timestamp is newer than the specified timestamp.
+ *
+ * @param other timestamp to compare against
+ * @return true if this instance is newer
+ */
+ default boolean isNewerThan(Timestamp other) {
+ return this.compareTo(Preconditions.checkNotNull(other)) > 0;
+ }
+
+ /**
+ * Tests if this timestamp is older than the specified timestamp.
+ *
+ * @param other timestamp to compare against
+ * @return true if this instance is older
+ */
+ default boolean isOlderThan(Timestamp other) {
+ return this.compareTo(Preconditions.checkNotNull(other)) < 0;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.annotations.Beta;
+import io.atomix.utils.Identifier;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Vector clock.
+ */
+@Beta
+public class VectorClock<T extends Identifier> implements Clock<VectorTimestamp<T>> {
+ private final T localIdentifier;
+ private final Map<T, VectorTimestamp<T>> vector = new HashMap<>();
+
+ public VectorClock(T localIdentifier) {
+ this(new VectorTimestamp<T>(localIdentifier, 0));
+ }
+
+ public VectorClock(VectorTimestamp<T> localTimestamp) {
+ this(localTimestamp, Collections.emptyList());
+ }
+
+ public VectorClock(VectorTimestamp<T> localTimestamp, Collection<VectorTimestamp<T>> vector) {
+ this.localIdentifier = localTimestamp.identifier();
+ this.vector.put(localTimestamp.identifier(), localTimestamp);
+ for (VectorTimestamp<T> timestamp : vector) {
+ this.vector.put(timestamp.identifier(), timestamp);
+ }
+ }
+
+ @Override
+ public VectorTimestamp<T> getTime() {
+ return vector.get(localIdentifier);
+ }
+
+ /**
+ * Returns the local logical timestamp.
+ *
+ * @return the logical timestamp for the local identifier
+ */
+ public LogicalTimestamp getLocalTimestamp() {
+ return getTime();
+ }
+
+ /**
+ * Returns the logical timestamp for the given identifier.
+ *
+ * @param identifier the identifier for which to return the timestamp
+ * @return the logical timestamp for the given identifier
+ */
+ public LogicalTimestamp getTimestamp(T identifier) {
+ return vector.get(identifier);
+ }
+
+ /**
+ * Returns a collection of identifier-timestamp pairs.
+ *
+ * @return a collection of identifier-timestamp pairs
+ */
+ public Collection<VectorTimestamp<T>> getTimestamps() {
+ return vector.values();
+ }
+
+ /**
+ * Updates the given timestamp.
+ *
+ * @param timestamp the timestamp to update
+ */
+ public void update(VectorTimestamp<T> timestamp) {
+ VectorTimestamp<T> currentTimestamp = vector.get(timestamp.identifier());
+ if (currentTimestamp == null || currentTimestamp.value() < timestamp.value()) {
+ vector.put(timestamp.identifier(), timestamp);
+ }
+ }
+
+ /**
+ * Updates the vector clock.
+ *
+ * @param clock the vector clock with which to update this clock
+ */
+ public void update(VectorClock<T> clock) {
+ for (VectorTimestamp<T> timestamp : clock.vector.values()) {
+ update(timestamp);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("time", getTime())
+ .add("vector", getTimestamps())
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.collect.ComparisonChain;
+import io.atomix.utils.Identifier;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Vector clock timestamp.
+ */
+public class VectorTimestamp<T extends Identifier> extends LogicalTimestamp {
+ private final T identifier;
+
+ public VectorTimestamp(T identifier, long value) {
+ super(value);
+ this.identifier = identifier;
+ }
+
+ /**
+ * Returns the timestamp identifier.
+ *
+ * @return the timestamp identifier
+ */
+ public T identifier() {
+ return identifier;
+ }
+
+ @Override
+ public int compareTo(Timestamp o) {
+ checkArgument(o instanceof VectorTimestamp, "Must be VectorTimestamp", o);
+ VectorTimestamp that = (VectorTimestamp) o;
+
+ return ComparisonChain.start()
+ .compare(this.identifier.id(), that.identifier.id())
+ .compare(this.value(), that.value())
+ .result();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(identifier(), value());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof VectorTimestamp)) {
+ return false;
+ }
+ VectorTimestamp that = (VectorTimestamp) obj;
+ return Objects.equals(this.identifier, that.identifier)
+ && Objects.equals(this.value(), that.value());
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("identifier", identifier())
+ .add("value", value())
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.collect.ComparisonChain;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Logical timestamp for versions.
+ * <p>
+ * The version is a logical timestamp that represents a point in logical time at which an event occurs.
+ * This is used in both pessimistic and optimistic locking protocols to ensure that the state of a shared resource
+ * has not changed at the end of a transaction.
+ */
+public class Version implements Timestamp {
+ private final long version;
+
+ public Version(long version) {
+ this.version = version;
+ }
+
+ /**
+ * Returns the version.
+ *
+ * @return the version
+ */
+ public long value() {
+ return this.version;
+ }
+
+ @Override
+ public int compareTo(Timestamp o) {
+ checkArgument(o instanceof Version,
+ "Must be LockVersion", o);
+ Version that = (Version) o;
+
+ return ComparisonChain.start()
+ .compare(this.version, that.version)
+ .result();
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(version);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Version)) {
+ return false;
+ }
+ Version that = (Version) obj;
+ return Objects.equals(this.version, that.version);
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(getClass())
+ .add("version", version)
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Objects;
+import io.atomix.utils.misc.ArraySizeHashPrinter;
+import io.atomix.utils.misc.TimestampPrinter;
+
+import java.util.function.Function;
+
+/**
+ * Versioned value.
+ *
+ * @param <V> value type.
+ */
+public class Versioned<V> {
+ private final V value;
+ private final long version;
+ private final long creationTime;
+
+ /**
+ * Constructs a new versioned value.
+ *
+ * @param value value
+ * @param version version
+ * @param creationTime milliseconds of the creation event
+ * from the Java epoch of 1970-01-01T00:00:00Z
+ */
+ public Versioned(V value, long version, long creationTime) {
+ this.value = value;
+ this.version = version;
+ this.creationTime = creationTime;
+ }
+
+ /**
+ * Constructs a new versioned value.
+ *
+ * @param value value
+ * @param version version
+ */
+ public Versioned(V value, long version) {
+ this(value, version, System.currentTimeMillis());
+ }
+
+ /**
+ * Returns the value.
+ *
+ * @return value.
+ */
+ public V value() {
+ return value;
+ }
+
+ /**
+ * Returns the version.
+ *
+ * @return version
+ */
+ public long version() {
+ return version;
+ }
+
+ /**
+ * Returns the system time when this version was created.
+ * <p>
+ * Care should be taken when relying on creationTime to
+ * implement any behavior in a distributed setting. Due
+ * to the possibility of clock skew it is likely that
+ * even creationTimes of causally related versions can be
+ * out or order.
+ *
+ * @return creation time
+ */
+ public long creationTime() {
+ return creationTime;
+ }
+
+ /**
+ * Maps this instance into another after transforming its
+ * value while retaining the same version and creationTime.
+ *
+ * @param transformer function for mapping the value
+ * @param <U> value type of the returned instance
+ * @return mapped instance
+ */
+ public synchronized <U> Versioned<U> map(Function<V, U> transformer) {
+ return new Versioned<>(value != null ? transformer.apply(value) : null, version, creationTime);
+ }
+
+ /**
+ * Returns the value of the specified Versioned object if non-null or else returns
+ * a default value.
+ *
+ * @param versioned versioned object
+ * @param defaultValue default value to return if versioned object is null
+ * @param <U> type of the versioned value
+ * @return versioned value or default value if versioned object is null
+ */
+ public static <U> U valueOrElse(Versioned<U> versioned, U defaultValue) {
+ return versioned == null ? defaultValue : versioned.value();
+ }
+
+ /**
+ * Returns the value of the specified Versioned object if non-null or else returns null.
+ *
+ * @param versioned versioned object
+ * @param <U> type of the versioned value
+ * @return versioned value or null if versioned object is null
+ */
+ public static <U> U valueOrNull(Versioned<U> versioned) {
+ return valueOrElse(versioned, null);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value, version, creationTime);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Versioned)) {
+ return false;
+ }
+ Versioned<V> that = (Versioned) other;
+ return Objects.equal(this.value, that.value)
+ && Objects.equal(this.version, that.version)
+ && Objects.equal(this.creationTime, that.creationTime);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("value", value instanceof byte[] ? ArraySizeHashPrinter.of((byte[]) value) : value)
+ .add("version", version)
+ .add("creationTime", new TimestampPrinter(creationTime))
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Wall clock.
+ */
+public class WallClock implements Clock<WallClockTimestamp> {
+ @Override
+ public WallClockTimestamp getTime() {
+ return new WallClockTimestamp();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper(this)
+ .add("time", getTime())
+ .toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.collect.ComparisonChain;
+import io.atomix.utils.misc.TimestampPrinter;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * A Timestamp that derives its value from the prevailing
+ * wallclock time on the controller where it is generated.
+ */
+public class WallClockTimestamp implements Timestamp {
+
+ /**
+ * Returns a new wall clock timestamp for the given unix timestamp.
+ *
+ * @param unixTimestamp the unix timestamp for which to create a new wall clock timestamp
+ * @return the wall clock timestamp
+ */
+ public static WallClockTimestamp from(long unixTimestamp) {
+ return new WallClockTimestamp(unixTimestamp);
+ }
+
+ private final long unixTimestamp;
+
+ public WallClockTimestamp() {
+ unixTimestamp = System.currentTimeMillis();
+ }
+
+ public WallClockTimestamp(long timestamp) {
+ unixTimestamp = timestamp;
+ }
+
+ @Override
+ public int compareTo(Timestamp o) {
+ checkArgument(o instanceof WallClockTimestamp,
+ "Must be WallClockTimestamp", o);
+ WallClockTimestamp that = (WallClockTimestamp) o;
+
+ return ComparisonChain.start()
+ .compare(this.unixTimestamp, that.unixTimestamp)
+ .result();
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(unixTimestamp);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof WallClockTimestamp)) {
+ return false;
+ }
+ WallClockTimestamp that = (WallClockTimestamp) obj;
+ return Objects.equals(this.unixTimestamp, that.unixTimestamp);
+ }
+
+ @Override
+ public String toString() {
+ return new TimestampPrinter(unixTimestamp).toString();
+ }
+
+ /**
+ * Returns the unixTimestamp.
+ *
+ * @return unix timestamp
+ */
+ public long unixTimestamp() {
+ return unixTimestamp;
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * Provides classes and interfaces for representing and operating on both logical and physical representations of time.
+ */
+package io.atomix.utils.time;
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import io.atomix.utils.misc.ArraySizeHashPrinter;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Array size hash printer test.
+ */
+public class ArraySizeHashPrinterTest {
+ @Test
+ public void testArraySizeHashPrinter() throws Exception {
+ ArraySizeHashPrinter printer = ArraySizeHashPrinter.of(new byte[]{1, 2, 3});
+ assertEquals("byte[]{length=3, hash=30817}", printer.toString());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Generics test.
+ */
+public class GenericsTest {
+ @Test
+ public void testGetInterfaceType() throws Exception {
+ assertEquals(String.class, Generics.getGenericInterfaceType(new ConcreteInterface(), GenericInterface.class, 0));
+ assertEquals(SomeClass.class, Generics.getGenericInterfaceType(new ConcreteInterface(), GenericInterface.class, 1));
+ }
+
+ @Test
+ public void testGetClassType() throws Exception {
+ assertEquals(SomeClass.class, Generics.getGenericClassType(new ConcreteClass(), GenericClass.class, 0));
+ }
+
+ public interface GenericInterface<T1, T2> {
+ T1 type1();
+
+ T2 type2();
+ }
+
+ public static class ConcreteInterface implements GenericInterface<String, SomeClass> {
+ @Override
+ public String type1() {
+ return null;
+ }
+
+ @Override
+ public SomeClass type2() {
+ return null;
+ }
+ }
+
+ public abstract class GenericClass<T> {
+ public abstract T type();
+ }
+
+ public class ConcreteClass extends GenericClass<SomeClass> {
+ @Override
+ public SomeClass type() {
+ return null;
+ }
+ }
+
+ public class SomeClass {
+ }
+}
--- /dev/null
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import com.google.common.base.Objects;
+import io.atomix.utils.misc.Match;
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
+/**
+ * Unit tests for Match.
+ */
+public class MatchTest {
+
+ @Test
+ public void testMatches() {
+ Match<String> m1 = Match.any();
+ assertTrue(m1.matches(null));
+ assertTrue(m1.matches("foo"));
+ assertTrue(m1.matches("bar"));
+
+ Match<String> m2 = Match.ifNull();
+ assertTrue(m2.matches(null));
+ assertFalse(m2.matches("foo"));
+
+ Match<String> m3 = Match.ifValue("foo");
+ assertFalse(m3.matches(null));
+ assertFalse(m3.matches("bar"));
+ assertTrue(m3.matches("foo"));
+
+ Match<byte[]> m4 = Match.ifValue(new byte[8]);
+ assertTrue(m4.matches(new byte[8]));
+ assertFalse(m4.matches(new byte[7]));
+ }
+
+ @Test
+ public void testEquals() {
+ Match<String> m1 = Match.any();
+ Match<String> m2 = Match.any();
+ Match<String> m3 = Match.ifNull();
+ Match<String> m4 = Match.ifValue("bar");
+ assertEquals(m1, m2);
+ assertFalse(Objects.equal(m1, m3));
+ assertFalse(Objects.equal(m3, m4));
+ Object o = new Object();
+ assertFalse(Objects.equal(m1, o));
+ }
+
+ @Test
+ public void testMap() {
+ Match<String> m1 = Match.ifNull();
+ assertEquals(m1.map(s -> "bar"), Match.ifNull());
+ Match<String> m2 = Match.ifValue("foo");
+ Match<String> m3 = m2.map(s -> "bar");
+ assertTrue(m3.matches("bar"));
+ }
+
+ @Test
+ public void testIfNotNull() {
+ Match<String> m = Match.ifNotNull();
+ assertFalse(m.matches(null));
+ assertTrue(m.matches("foo"));
+ }
+
+ @Test
+ public void testIfNotValue() {
+ Match<String> m1 = Match.ifNotValue(null);
+ Match<String> m2 = Match.ifNotValue("foo");
+ assertFalse(m1.matches(null));
+ assertFalse(m2.matches("foo"));
+ }
+
+ @Test
+ public void testToString() {
+ Match<String> m1 = Match.any();
+ Match<String> m2 = Match.any();
+ Match<String> m3 = Match.ifValue("foo");
+ Match<String> m4 = Match.ifValue("foo");
+ Match<String> m5 = Match.ifNotValue("foo");
+
+ String note = "Results of toString() should be consistent -- ";
+
+ assertTrue(note, m1.toString().equals(m2.toString()));
+ assertTrue(note, m3.toString().equals(m4.toString()));
+ assertFalse(note, m4.toString().equals(m5.toString()));
+ }
+
+ @Test
+ public void testHashCode() {
+ Match<String> m1 = Match.ifValue("foo");
+ Match<String> m2 = Match.ifNotValue("foo");
+ Match<String> m3 = Match.ifValue("foo");
+ Match<String> m4 = Match.ifNotNull();
+ Match<String> m5 = Match.ifNull();
+
+ assertTrue(m1.hashCode() == m3.hashCode());
+ assertFalse(m2.hashCode() == m1.hashCode());
+ assertFalse(m4.hashCode() == m5.hashCode());
+
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import io.atomix.utils.misc.TimestampPrinter;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Timestamp printer test.
+ */
+public class TimestampPrinterTest {
+ @Test
+ @Ignore // Timestamp is environment specific
+ public void testTimestampPrinter() throws Exception {
+ TimestampPrinter printer = TimestampPrinter.of(1);
+ assertEquals("1969-12-31 04:00:00,001", printer.toString());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Version test.
+ */
+public class VersionTest {
+ @Test
+ public void testVersionComparison() {
+ assertTrue(Version.from("1.0.0").compareTo(Version.from("2.0.0")) < 0);
+ assertTrue(Version.from("2.0.0").compareTo(Version.from("1.0.0")) > 0);
+ assertTrue(Version.from("1.0.0").compareTo(Version.from("0.1.0")) > 0);
+ assertTrue(Version.from("0.1.0").compareTo(Version.from("1.0.0")) < 0);
+ assertTrue(Version.from("0.1.0").compareTo(Version.from("0.1.1")) < 0);
+ assertTrue(Version.from("1.0.0").compareTo(Version.from("0.0.1")) > 0);
+ assertTrue(Version.from("1.1.1").compareTo(Version.from("1.0.3")) > 0);
+ assertTrue(Version.from("1.0.0").compareTo(Version.from("1.0.0-beta1")) > 0);
+ assertTrue(Version.from("1.0.0-rc2").compareTo(Version.from("1.0.0-rc1")) > 0);
+ assertTrue(Version.from("1.0.0-rc1").compareTo(Version.from("1.0.0-beta1")) > 0);
+ assertTrue(Version.from("2.0.0-beta1").compareTo(Version.from("1.0.0")) > 0);
+ assertTrue(Version.from("1.0.0-alpha1").compareTo(Version.from("1.0.0-SNAPSHOT")) > 0);
+ }
+
+ @Test
+ public void testVersionToString() {
+ assertEquals("1.0.0", Version.from("1.0.0").toString());
+ assertEquals("1.0.0-alpha1", Version.from("1.0.0-alpha1").toString());
+ assertEquals("1.0.0-beta1", Version.from("1.0.0-beta1").toString());
+ assertEquals("1.0.0-rc1", Version.from("1.0.0-rc1").toString());
+ assertEquals("1.0.0-SNAPSHOT", Version.from("1.0.0-SNAPSHOT").toString());
+ }
+
+ @Test
+ public void testInvalidVersions() {
+ assertIllegalArgument(() -> Version.from("1"));
+ assertIllegalArgument(() -> Version.from("1.0"));
+ assertIllegalArgument(() -> Version.from("1.0-beta1"));
+ assertIllegalArgument(() -> Version.from("1.0.0.0"));
+ assertIllegalArgument(() -> Version.from("1.0.0.0-beta1"));
+ assertIllegalArgument(() -> Version.from("1.0.0-not1"));
+ assertIllegalArgument(() -> Version.from("1.0.0-alpha"));
+ assertIllegalArgument(() -> Version.from("1.0.0-beta"));
+ assertIllegalArgument(() -> Version.from("1.0.0-rc"));
+ }
+
+ private void assertIllegalArgument(Runnable callback) {
+ try {
+ callback.run();
+ fail();
+ } catch (IllegalArgumentException e) {
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import org.junit.Test;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Ordered completable future test.
+ */
+public class OrderedFutureTest {
+
+ /**
+ * Tests ordered completion of future callbacks.
+ */
+ @Test
+ public void testOrderedCompletion() throws Throwable {
+ CompletableFuture<String> future = new OrderedFuture<>();
+ AtomicInteger order = new AtomicInteger();
+ future.whenComplete((r, e) -> assertEquals(1, order.incrementAndGet()));
+ future.whenComplete((r, e) -> assertEquals(2, order.incrementAndGet()));
+ future.handle((r, e) -> {
+ assertEquals(3, order.incrementAndGet());
+ assertEquals("foo", r);
+ return "bar";
+ });
+ future.thenRun(() -> assertEquals(3, order.incrementAndGet()));
+ future.thenAccept(r -> {
+ assertEquals(5, order.incrementAndGet());
+ assertEquals("foo", r);
+ });
+ future.thenApply(r -> {
+ assertEquals(6, order.incrementAndGet());
+ assertEquals("foo", r);
+ return "bar";
+ });
+ future.whenComplete((r, e) -> {
+ assertEquals(7, order.incrementAndGet());
+ assertEquals("foo", r);
+ });
+ future.complete("foo");
+ }
+
+ /**
+ * Tests ordered failure of future callbacks.
+ */
+ public void testOrderedFailure() throws Throwable {
+ CompletableFuture<String> future = new OrderedFuture<>();
+ AtomicInteger order = new AtomicInteger();
+ future.whenComplete((r, e) -> assertEquals(1, order.incrementAndGet()));
+ future.whenComplete((r, e) -> assertEquals(2, order.incrementAndGet()));
+ future.handle((r, e) -> {
+ assertEquals(3, order.incrementAndGet());
+ return "bar";
+ });
+ future.thenRun(() -> fail());
+ future.thenAccept(r -> fail());
+ future.exceptionally(e -> {
+ assertEquals(3, order.incrementAndGet());
+ return "bar";
+ });
+ future.completeExceptionally(new RuntimeException("foo"));
+ }
+
+ /**
+ * Tests calling callbacks that are added after completion.
+ */
+ public void testAfterComplete() throws Throwable {
+ CompletableFuture<String> future = new OrderedFuture<>();
+ future.whenComplete((result, error) -> assertEquals("foo", result));
+ future.complete("foo");
+ AtomicInteger count = new AtomicInteger();
+ future.whenComplete((result, error) -> {
+ assertEquals("foo", result);
+ assertEquals(1, count.incrementAndGet());
+ });
+ future.thenAccept(result -> {
+ assertEquals("foo", result);
+ assertEquals(2, count.incrementAndGet());
+ });
+ assertEquals(2, count.get());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.concurrent;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Retrying function test.
+ */
+public class RetryingFunctionTest {
+ private int round;
+
+ @Before
+ public void setUp() {
+ round = 1;
+ }
+
+ @After
+ public void tearDown() {
+ round = 0;
+ }
+
+ @Test(expected = RetryableException.class)
+ public void testNoRetries() {
+ new RetryingFunction<>(this::succeedAfterOneFailure, RetryableException.class, 0, 10).apply(null);
+ }
+
+ @Test
+ public void testSuccessAfterOneRetry() {
+ new RetryingFunction<>(this::succeedAfterOneFailure, RetryableException.class, 1, 10).apply(null);
+ }
+
+ @Test(expected = RetryableException.class)
+ public void testFailureAfterOneRetry() {
+ new RetryingFunction<>(this::succeedAfterTwoFailures, RetryableException.class, 1, 10).apply(null);
+ }
+
+ @Test
+ public void testFailureAfterTwoRetries() {
+ new RetryingFunction<>(this::succeedAfterTwoFailures, RetryableException.class, 2, 10).apply(null);
+ }
+
+ @Test(expected = NonRetryableException.class)
+ public void testFailureWithNonRetryableFailure() {
+ new RetryingFunction<>(this::failCompletely, RetryableException.class, 2, 10).apply(null);
+ }
+
+ private String succeedAfterOneFailure(String input) {
+ if (round++ <= 1) {
+ throw new RetryableException();
+ } else {
+ return "pass";
+ }
+ }
+
+ private String succeedAfterTwoFailures(String input) {
+ if (round++ <= 2) {
+ throw new RetryableException();
+ } else {
+ return "pass";
+ }
+ }
+
+ private String failCompletely(String input) {
+ if (round++ <= 1) {
+ throw new NonRetryableException();
+ } else {
+ return "pass";
+ }
+ }
+
+ private static class RetryableException extends RuntimeException {
+ }
+
+ private static class NonRetryableException extends RuntimeException {
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.logging;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Contextual logger test.
+ */
+public class LoggerContextTest {
+ @Test
+ public void testLoggerContext() throws Exception {
+ LoggerContext context = LoggerContext.builder("test")
+ .addValue(1)
+ .add("foo", "bar")
+ .build();
+ assertEquals("test{1}{foo=bar}", context.toString());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.atomix.utils.misc;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class StringUtilsTest {
+
+ @Test
+ public void testNull() {
+ assertNull(StringUtils.split(null, ","));
+ }
+
+ @Test
+ public void testFilter() {
+ String[] result = StringUtils.split("1, ,,", ",");
+ assertNotNull(result);
+ assertEquals(1, result.length);
+ assertEquals("1", result[0]);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * 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.atomix.utils.net;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Address test.
+ */
+public class AddressTest {
+ @Test
+ public void testIPv4Address() throws Exception {
+ Address address = Address.from("127.0.0.1:5000");
+ assertEquals("127.0.0.1", address.host());
+ assertEquals(5000, address.port());
+ assertEquals("localhost", address.address().getHostName());
+ assertEquals("127.0.0.1:5000", address.toString());
+ }
+
+ @Test
+ public void testIPv6Address() throws Exception {
+ Address address = Address.from("[fe80:cd00:0000:0cde:1257:0000:211e:729c]:5000");
+ assertEquals("fe80:cd00:0000:0cde:1257:0000:211e:729c", address.host());
+ assertEquals(5000, address.port());
+ assertEquals("fe80:cd00:0:cde:1257:0:211e:729c", address.address().getHostName());
+ assertEquals("[fe80:cd00:0000:0cde:1257:0000:211e:729c]:5000", address.toString());
+ }
+
+ @Test
+ @Ignore
+ public void testResolveAddress() throws Exception {
+ Address address = Address.from("localhost", 5000);
+ assertEquals("127.0.0.1", address.address().getHostAddress());
+ assertEquals(5000, address.port());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class BufferAwareByteArrayOutputStreamTest {
+
+ @Test
+ public void testBufferSize() throws Exception {
+ BufferAwareByteArrayOutputStream outputStream = new BufferAwareByteArrayOutputStream(8);
+ assertEquals(8, outputStream.getBufferSize());
+ outputStream.write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
+ assertEquals(8, outputStream.getBufferSize());
+ outputStream.write(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
+ assertEquals(16, outputStream.getBufferSize());
+ outputStream.reset();
+ assertEquals(16, outputStream.getBufferSize());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import com.esotericsoftware.kryo.io.Input;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class KryoInputPoolTest {
+
+ private KryoInputPool kryoInputPool;
+
+ @Before
+ public void setUp() throws Exception {
+ kryoInputPool = new KryoInputPool();
+ }
+
+ @Test
+ public void discardOutput() {
+ final Input[] result = new Input[2];
+ kryoInputPool.run(input -> {
+ result[0] = input;
+ return null;
+ }, KryoInputPool.MAX_POOLED_BUFFER_SIZE + 1);
+ kryoInputPool.run(input -> {
+ result[1] = input;
+ return null;
+ }, 0);
+ assertTrue(result[0] != result[1]);
+ }
+
+ @Test
+ public void recycleOutput() {
+ final Input[] result = new Input[2];
+ kryoInputPool.run(input -> {
+ assertEquals(0, input.position());
+ byte[] payload = new byte[]{1, 2, 3, 4};
+ input.setBuffer(payload);
+ assertArrayEquals(payload, input.readBytes(4));
+ result[0] = input;
+ return null;
+ }, 0);
+ assertNull(result[0].getInputStream());
+ assertEquals(0, result[0].position());
+ kryoInputPool.run(input -> {
+ result[1] = input;
+ return null;
+ }, 0);
+ assertTrue(result[0] == result[1]);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.serializer;
+
+import com.esotericsoftware.kryo.io.Output;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class KryoOutputPoolTest {
+
+ private KryoOutputPool kryoOutputPool;
+
+ @Before
+ public void setUp() throws Exception {
+ kryoOutputPool = new KryoOutputPool();
+ }
+
+ @Test
+ public void discardOutput() {
+ final Output[] result = new Output[2];
+ kryoOutputPool.run(output -> {
+ result[0] = output;
+ return null;
+ }, KryoOutputPool.MAX_POOLED_BUFFER_SIZE + 1);
+ kryoOutputPool.run(output -> {
+ result[1] = output;
+ return null;
+ }, 0);
+ assertTrue(result[0] != result[1]);
+ }
+
+ @Test
+ public void recycleOutput() {
+ final ByteArrayOutput[] result = new ByteArrayOutput[2];
+ kryoOutputPool.run(output -> {
+ output.writeInt(1);
+ assertEquals(Integer.BYTES, output.position());
+ result[0] = output;
+ return null;
+ }, 0);
+ assertEquals(0, result[0].position());
+ assertEquals(0, result[0].getByteArrayOutputStream().size());
+ kryoOutputPool.run(output -> {
+ assertEquals(0, output.position());
+ result[1] = output;
+ return null;
+ }, 0);
+ assertTrue(result[0] == result[1]);
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Logical timestamp test.
+ */
+public class EpochTest {
+ @Test
+ public void testLogicalTimestamp() throws Exception {
+ Epoch epoch = Epoch.of(1);
+ assertEquals(1, epoch.value());
+ assertTrue(epoch.isNewerThan(Epoch.of(0)));
+ assertFalse(epoch.isNewerThan(Epoch.of(2)));
+ assertTrue(epoch.isOlderThan(Epoch.of(2)));
+ assertFalse(epoch.isOlderThan(Epoch.of(0)));
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Logical clock test.
+ */
+public class LogicalClockTest {
+ @Test
+ public void testLogicalClock() throws Exception {
+ LogicalClock clock = new LogicalClock();
+ assertEquals(1, clock.increment().value());
+ assertEquals(1, clock.getTime().value());
+ assertEquals(2, clock.increment().value());
+ assertEquals(2, clock.getTime().value());
+ assertEquals(5, clock.update(LogicalTimestamp.of(5)).value());
+ assertEquals(5, clock.getTime().value());
+ assertEquals(5, clock.update(LogicalTimestamp.of(3)).value());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Logical timestamp test.
+ */
+public class LogicalTimestampTest {
+ @Test
+ public void testLogicalTimestamp() throws Exception {
+ LogicalTimestamp timestamp = LogicalTimestamp.of(1);
+ assertEquals(1, timestamp.value());
+ assertTrue(timestamp.isNewerThan(LogicalTimestamp.of(0)));
+ assertFalse(timestamp.isNewerThan(LogicalTimestamp.of(2)));
+ assertTrue(timestamp.isOlderThan(LogicalTimestamp.of(2)));
+ assertFalse(timestamp.isOlderThan(LogicalTimestamp.of(0)));
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.lang.reflect.Constructor;
+import java.util.Arrays;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+/**
+ * MultiValuedTimestamp unit tests.
+ */
+public class MultiValuedTimestampTest {
+ private final MultiValuedTimestamp<Integer, Integer> stats1 = new MultiValuedTimestamp<>(1, 3);
+ private final MultiValuedTimestamp<Integer, Integer> stats2 = new MultiValuedTimestamp<>(1, 2);
+
+ /**
+ * Tests the creation of the MapEvent object.
+ */
+ @Test
+ public void testConstruction() {
+ assertThat(stats1.value1(), is(1));
+ assertThat(stats1.value2(), is(3));
+ }
+
+ /**
+ * Tests the toCompare function.
+ */
+ @Test
+ public void testToCompare() {
+ assertThat(stats1.compareTo(stats2), is(1));
+ }
+
+ /**
+ * Tests the equals, hashCode and toString methods using Guava EqualsTester.
+ */
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(stats1, stats1)
+ .addEqualityGroup(stats2)
+ .testEquals();
+ }
+
+ /**
+ * Tests that the empty argument list constructor for serialization
+ * is present and creates a proper object.
+ */
+ @Test
+ public void testSerializerConstructor() {
+ try {
+ Constructor[] constructors = MultiValuedTimestamp.class.getDeclaredConstructors();
+ assertThat(constructors, notNullValue());
+ Arrays.stream(constructors).filter(ctor ->
+ ctor.getParameterTypes().length == 0)
+ .forEach(noParamsCtor -> {
+ try {
+ noParamsCtor.setAccessible(true);
+ MultiValuedTimestamp stats =
+ (MultiValuedTimestamp) noParamsCtor.newInstance();
+ assertThat(stats, notNullValue());
+ } catch (Exception e) {
+ Assert.fail("Exception instantiating no parameters constructor");
+ }
+ });
+ } catch (Exception e) {
+ Assert.fail("Exception looking up constructors");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Version test.
+ */
+public class VersionTest {
+ @Test
+ public void testVersion() {
+ Version version1 = new Version(1);
+ Version version2 = new Version(1);
+ assertTrue(version1.equals(version2));
+ assertTrue(version1.hashCode() == version2.hashCode());
+ assertTrue(version1.value() == version2.value());
+
+ Version version3 = new Version(2);
+ assertFalse(version1.equals(version3));
+ assertFalse(version1.hashCode() == version3.hashCode());
+ assertFalse(version1.value() == version3.value());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Versioned unit tests.
+ */
+public class VersionedTest {
+
+ private final Versioned<Integer> stats1 = new Versioned<>(1, 2, 3);
+
+ private final Versioned<Integer> stats2 = new Versioned<>(1, 2);
+
+ /**
+ * Tests the creation of the MapEvent object.
+ */
+ @Test
+ public void testConstruction() {
+ assertThat(stats1.value(), is(1));
+ assertThat(stats1.version(), is(2L));
+ assertThat(stats1.creationTime(), is(3L));
+ }
+
+ /**
+ * Maps an Integer to a String - Utility function to test the map function.
+ *
+ * @param a Actual Integer parameter.
+ * @return String Mapped valued.
+ */
+ public static String transform(Integer a) {
+ return Integer.toString(a);
+ }
+
+ /**
+ * Tests the map function.
+ */
+ @Test
+ public void testMap() {
+ Versioned<String> tempObj = stats1.map(VersionedTest::transform);
+ assertThat(tempObj.value(), is("1"));
+ }
+
+ /**
+ * Tests the valueOrElse method.
+ */
+ @Test
+ public void testOrElse() {
+ Versioned<String> vv = new Versioned<>("foo", 1);
+ Versioned<String> nullVV = null;
+ assertThat(Versioned.valueOrElse(vv, "bar"), is("foo"));
+ assertThat(Versioned.valueOrElse(nullVV, "bar"), is("bar"));
+ }
+
+ /**
+ * Tests the equals, hashCode and toString methods using Guava EqualsTester.
+ */
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(stats1, stats1)
+ .addEqualityGroup(stats2)
+ .testEquals();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Wall clock test.
+ */
+public class WallClockTest {
+ @Test
+ public void testWallClock() throws Exception {
+ WallClock clock = new WallClock();
+ WallClockTimestamp time = clock.getTime();
+ assertNotNull(time);
+ Thread.sleep(5);
+ assertTrue(clock.getTime().unixTimestamp() > time.unixTimestamp());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * 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.atomix.utils.time;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for {@link WallClockTimestamp}.
+ */
+public class WallClockTimestampTest {
+ @Test
+ public final void testBasic() throws InterruptedException {
+ WallClockTimestamp ts1 = new WallClockTimestamp();
+ Thread.sleep(50);
+ WallClockTimestamp ts2 = new WallClockTimestamp();
+ long stamp = System.currentTimeMillis() + 10000;
+ WallClockTimestamp ts3 = new WallClockTimestamp(stamp);
+
+ assertTrue(ts1.compareTo(ts1) == 0);
+ assertTrue(ts2.compareTo(ts1) > 0);
+ assertTrue(ts1.compareTo(ts2) < 0);
+ assertTrue(ts3.unixTimestamp() == stamp);
+ }
+}