/*
* Copyright (C) 2014 Indeed Inc.
*
* 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 com.indeed.imhotep.io;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.nio.ByteBuffer;
/**
*
* lock free circular buffer (except in the waiting for input/waiting for space states to avoid busy waiting)
* this assumes a single reader and a single writer
*
* @author jplaisance
*/
public final class CircularInputStream extends InputStream {
private static final Logger log = Logger.getLogger(CircularInputStream.class);
private final byte[] buffer;
private final int bufferMask;
//empty if reader.pointer.value == writer.pointer.value
//full if (writer.pointer.value+1) % buffer.length == reader.pointer.value
private final State reader = new State();
private final State writer;
static final class State {
private final PaddedVolatileInt pointer = new PaddedVolatileInt();
private final PaddedVolatileBool closed = new PaddedVolatileBool();
private final PaddedVolatileBool waiting;
private final PaddedInt cache = new PaddedInt();
private final Object lock;
State() {
this(new PaddedInt(), new PaddedVolatileBool());
}
State(final PaddedInt lock, final PaddedVolatileBool waiting) {
this.lock = lock;
this.waiting = waiting;
}
}
// x86/sun 64 bit jdk alignment:
// 16 bytes object header
// 40 bytes padding
// 8 bytes data
// 64 bytes padding (to ensure header for next object which contains the futex
// x86 currently uses 64 byte cache lines so it is impossible for the data to end up on the same cache line as anything else
private static class PrePadding {
long pad1;
long pad2;
long pad3;
long pad4;
long pad5;
}
private static class VolatileInt extends PrePadding {
volatile int value;
int pad;
}
private static class Int extends PrePadding {
int value;
int pad;
}
private static final class PaddedVolatileInt extends VolatileInt {
long pad1;
long pad2;
long pad3;
long pad4;
long pad5;
long pad6;
long pad7;
long pad8;
}
static final class PaddedInt extends Int {
long pad1;
long pad2;
long pad3;
long pad4;
long pad5;
long pad6;
long pad7;
long pad8;
}
private static class VolatileBool extends PrePadding {
volatile boolean value;
boolean pad1;
boolean pad2;
boolean pad3;
boolean pad4;
boolean pad5;
boolean pad6;
boolean pad7;
}
static final class PaddedVolatileBool extends VolatileBool {
long pad1;
long pad2;
long pad3;
long pad4;
long pad5;
long pad6;
long pad7;
long pad8;
}
public CircularInputStream(int bufferSize) {
this(bufferSize, new State());
}
public CircularInputStream(int bufferSize, State writer) {
if (bufferSize != Integer.lowestOneBit(bufferSize)) throw new IllegalArgumentException();
buffer = new byte[bufferSize];
bufferMask = bufferSize-1;
this.writer = writer;
}
private int getTailFromCache(final int head) {
int tail = writer.cache.value;
if (head == tail) {
tail = writer.pointer.value;
writer.cache.value = tail;
}
return tail;
}
public int read() throws IOException {
while (true) {
final int head = reader.pointer.value;
final int tail = getTailFromCache(head);
if (head != tail) {
final int ret = buffer[head]&0xFF;
reader.pointer.value = (head+1)&bufferMask;
notifyWriter();
return ret;
} else {
if (writer.closed.value && reader.pointer.value == writer.pointer.value) return -1;
waitForInput();
}
}
}
public int read(final byte[] b, final int off, final int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
while (true) {
final int head = reader.pointer.value;
final int tail = getTailFromCache(head);
if (head != tail) {
final int ret;
if (head < tail) {
ret = Math.min(tail-head, len);
System.arraycopy(buffer, head, b, off, ret);
reader.pointer.value += ret;
} else {
final int endLength = buffer.length - head;
ret = Math.min(endLength+tail, len);
if (ret <= endLength) {
System.arraycopy(buffer, head, b, off, ret);
reader.pointer.value = (head+ret)&bufferMask;
} else {
System.arraycopy(buffer, head, b, off, endLength);
final int newHead = ret-endLength;
System.arraycopy(buffer, 0, b, off+endLength, newHead);
reader.pointer.value = newHead;
}
}
notifyWriter();
return ret;
} else {
if (writer.closed.value && reader.pointer.value == writer.pointer.value) return -1;
waitForInput();
}
}
}
private void waitForInput() throws IOException {
synchronized (reader.lock) {
reader.waiting.value = true;
try {
while (reader.pointer.value == writer.pointer.value) {
if (writer.closed.value) break;
try {
reader.lock.wait();
} catch (InterruptedException e) {
throw new InterruptedIOException();
}
}
} finally {
reader.waiting.value = false;
}
}
}
private void notifyWriter() {
if (writer.waiting.value) {
synchronized (writer.lock) {
if (writer.waiting.value) {
if (((writer.pointer.value +1)&bufferMask) != reader.pointer.value || reader.closed.value) {
writer.lock.notify();
writer.waiting.value = false;
}
}
}
}
}
private int getHeadFromCache(final int tail) {
int head = reader.cache.value;
if (((tail+1)&bufferMask) == head) {
head = reader.pointer.value;
reader.cache.value = head;
}
return head;
}
public void write(final int b) throws IOException {
while (true) {
final int tail = writer.pointer.value;
final int head = getHeadFromCache(tail);
if (((tail+1)&bufferMask) == head) {
if (reader.closed.value) throw new IOException("InputStream is closed");
waitForSpace();
} else {
buffer[tail] = (byte)b;
writer.pointer.value = (tail+1)&bufferMask;
notifyReader();
return;
}
}
}
public void write(final byte[] b, int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
while (true) {
if (len == 0) return;
final int tail = writer.pointer.value;
final int head = getHeadFromCache(tail);
if (((tail+1)&bufferMask) == head) {
if (reader.closed.value) throw new IOException("InputStream is closed");
waitForSpace();
} else {
final int copyLen;
if (tail < head) {
copyLen = Math.min(len, head-tail-1);
} else if (head == 0) {
copyLen = Math.min(len, buffer.length - tail - 1);
} else {
copyLen = Math.min(len, buffer.length - tail);
}
System.arraycopy(b, off, buffer, tail, copyLen);
off += copyLen;
len -= copyLen;
writer.pointer.value = (tail+copyLen)&bufferMask;
notifyReader();
}
}
}
public boolean writeNonBlocking(ByteBuffer bytes) throws IOException {
while (true) {
if (bytes.remaining() == 0) return true;
final int tail = writer.pointer.value;
final int head = getHeadFromCache(tail);
if (((tail+1)&bufferMask) == head) {
if (reader.closed.value) throw new IOException("InputStream is closed");
return false;
} else {
final int copyLen;
if (tail < head) {
copyLen = Math.min(bytes.remaining(), head-tail-1);
} else if (head == 0) {
copyLen = Math.min(bytes.remaining(), buffer.length - tail - 1);
} else {
copyLen = Math.min(bytes.remaining(), buffer.length - tail);
}
bytes.get(buffer, tail, copyLen);
writer.pointer.value = (tail+copyLen)&bufferMask;
notifyReader();
}
}
}
private void waitForSpace() throws IOException {
synchronized (writer.lock) {
writer.waiting.value = true;
try {
while (((writer.pointer.value+1)&bufferMask) == reader.pointer.value) {
if (reader.closed.value) break;
try {
writer.lock.wait();
} catch (InterruptedException e) {
throw new InterruptedIOException();
}
}
} finally {
writer.waiting.value = false;
}
}
}
private void notifyReader() {
if (reader.waiting.value) {
synchronized (reader.lock) {
if (reader.waiting.value) {
if (reader.pointer.value != writer.pointer.value || writer.closed.value) {
reader.lock.notify();
reader.waiting.value = false;
}
}
}
}
}
public void end() throws IOException {
writer.closed.value = true;
notifyReader();
}
public int available() throws IOException {
final int head = reader.pointer.value;
final int tail = getTailFromCache(head);
return head < tail ? tail-head : buffer.length-head+tail;
}
public void close() throws IOException {
reader.closed.value = true;
notifyWriter();
}
}