/* Copyright (c) 2006, Sriram Srinivasan
*
* You may distribute this software under the terms of the license
* specified in the file "License"
*/
package kilim;
import java.util.LinkedList;
import java.util.TimerTask;
/**
* This is a typed buffer that supports multiple producers and a single
* consumer. It is the basic construct used for tasks to interact and
* synchronize with each other (as opposed to direct java calls or static member
* variables). put() and get() are the two essential functions.
*
* We use the term "block" to mean thread block, and "pause" to mean
* fiber pausing. The suffix "nb" on some methods (such as getnb())
* stands for non-blocking. Both put() and get() have blocking and
* non-blocking variants in the form of putb(), putnb
*/
public class Mailbox<T> implements PauseReason, EventPublisher {
// TODO. Give mbox a config name and id and make monitorable
T[] msgs;
private int iprod = 0; // producer index
private int icons = 0; // consumer index;
private int numMsgs = 0;
private int maxMsgs = 300;
EventSubscriber sink;
// FIX: I don't like this event design. The only good thing is that
// we don't create new event objects every time we signal a client
// (subscriber) that's blocked on this mailbox.
public static final int SPACE_AVAILABLE = 1;
public static final int MSG_AVAILABLE = 2;
public static final int TIMED_OUT = 3;
public static final Event spaceAvailble = new Event(MSG_AVAILABLE);
public static final Event messageAvailable = new Event(SPACE_AVAILABLE);
public static final Event timedOut = new Event(TIMED_OUT);
LinkedList<EventSubscriber> srcs = new LinkedList<EventSubscriber>();
// DEBUG stuff
// To do: move into monitorable stat object
/*
* public int nPut = 0; public int nGet = 0; public int nWastedPuts = 0;
* public int nWastedGets = 0;
*/
public Mailbox() {
this(10);
}
public Mailbox(int initialSize) {
this(initialSize, Integer.MAX_VALUE);
}
@SuppressWarnings("unchecked")
public Mailbox(int initialSize, int maxSize) {
if (initialSize > maxSize)
throw new IllegalArgumentException("initialSize: " + initialSize
+ " cannot exceed maxSize: " + maxSize);
msgs = (T[]) new Object[initialSize];
maxMsgs = maxSize;
}
/**
* Non-blocking, nonpausing get.
* @param eo. If non-null, registers this observer and calls it with a MessageAvailable event when
* a put() is done.
* @return buffered message if there's one, or null
*/
public T get(EventSubscriber eo) {
T msg;
EventSubscriber producer = null;
synchronized(this) {
int n = numMsgs;
if (n > 0) {
int ic = icons;
msg = msgs[ic]; msgs[ic]=null;
icons = (ic + 1) % msgs.length;
numMsgs = n - 1;
if (srcs.size() > 0) {
producer = srcs.poll();
}
} else {
msg = null;
addMsgAvailableListener(eo);
}
}
if (producer != null) {
producer.onEvent(this, spaceAvailble);
}
return msg;
}
/**
* Non-blocking, nonpausing put.
* @param eo. If non-null, registers this observer and calls it with an SpaceAvailable event
* when there's space.
* @return buffered message if there's one, or null
* @see #putnb(Object)
* @see #putb(Object)
*/
@SuppressWarnings("unchecked")
public boolean put(T msg, EventSubscriber eo) {
boolean ret = true; // assume we will be able to enqueue
EventSubscriber subscriber;
synchronized(this) {
if (msg == null) {
throw new NullPointerException("Null message supplied to put");
}
int ip = iprod;
int ic = icons;
int n = numMsgs;
if (n == msgs.length) {
assert ic == ip : "numElements == msgs.length && ic != ip";
if (n < maxMsgs) {
T[] newmsgs = (T[]) new Object[Math.min(n * 2, maxMsgs)];
System.arraycopy(msgs, ic, newmsgs, 0, n - ic);
if (ic > 0) {
System.arraycopy(msgs, 0, newmsgs, n - ic, ic);
}
msgs = newmsgs;
ip = n;
ic = 0;
} else {
ret = false;
}
}
if (ret) {
numMsgs = n + 1;
msgs[ip] = msg;
iprod = (ip + 1) % msgs.length;
icons = ic;
subscriber = sink;
sink = null;
} else {
subscriber = null;
// unable to enqueue
if (eo != null) {
srcs.add(eo);
}
}
}
// notify get's subscriber that something is available
if (subscriber != null) {
subscriber.onEvent(this, messageAvailable);
}
return ret;
}
/**
* Get, don't pause or block.
*
* @return stored message, or null if no message found.
*/
public T getnb() {
return get(null);
}
/**
* @return non-null message.
* @throws Pausable
*/
public T get() throws Pausable{
Task t = Task.getCurrentTask();
T msg = get(t);
while (msg == null) {
Task.pause(this);
removeMsgAvailableListener(t);
msg = get(t);
}
return msg;
}
/**
* @return non-null message, or null if timed out.
* @throws Pausable
*/
public T get(long timeoutMillis) throws Pausable {
final Task t = Task.getCurrentTask();
T msg = get(t);
long end = System.currentTimeMillis() + timeoutMillis;
while (msg == null) {
TimerTask tt = new TimerTask() {
public void run() {
Mailbox.this.removeMsgAvailableListener(t);
t.onEvent(Mailbox.this, timedOut);
}
};
Task.timer.schedule(tt, timeoutMillis);
Task.pause(this);
tt.cancel();
removeMsgAvailableListener(t);
msg = get(t);
timeoutMillis = end - System.currentTimeMillis();
if (timeoutMillis <= 0) {
removeMsgAvailableListener(t);
break;
}
}
return msg;
}
/**
* Block caller until at least one message is available.
* @throws Pausable
*/
public void untilHasMessage() throws Pausable {
while (hasMessage(Task.getCurrentTask()) == false) {
Task.pause(this);
}
}
/**
* Block caller until <code>num</code> messages are available.
* @param num
* @throws Pausable
*/
public void untilHasMessages(int num) throws Pausable {
while (hasMessages(num, Task.getCurrentTask()) == false) {
Task.pause(this);
}
}
/**
* Block caller (with timeout) until a message is available.
* @return non-null message.
* @throws Pausable
*/
public boolean untilHasMessage(long timeoutMillis) throws Pausable {
final Task t = Task.getCurrentTask();
boolean has_msg = hasMessage(t);
long end = System.currentTimeMillis() + timeoutMillis;
while (has_msg == false) {
TimerTask tt = new TimerTask() {
public void run() {
Mailbox.this.removeMsgAvailableListener(t);
t.onEvent(Mailbox.this, timedOut);
}
};
Task.timer.schedule(tt, timeoutMillis);
Task.pause(this);
tt.cancel();
has_msg = hasMessage(t);
timeoutMillis = end - System.currentTimeMillis();
if (timeoutMillis <= 0) {
removeMsgAvailableListener(t);
break;
}
}
return has_msg;
}
/**
* Block caller (with timeout) until <code>num</code> messages are available.
*
* @param num
* @param timeoutMillis
* @return Message or <code>null</code> on timeout
* @throws Pausable
*/
public boolean untilHasMessages(int num, long timeoutMillis)
throws Pausable {
final Task t = Task.getCurrentTask();
final long end = System.currentTimeMillis() + timeoutMillis;
boolean has_msg = hasMessages(num, t);
while (has_msg == false) {
TimerTask tt = new TimerTask() {
public void run() {
Mailbox.this.removeMsgAvailableListener(t);
t.onEvent(Mailbox.this, timedOut);
}
};
Task.timer.schedule(tt, timeoutMillis);
Task.pause(this);
if (!tt.cancel()) {
removeMsgAvailableListener(t);
}
has_msg = hasMessages(num, t);
timeoutMillis = end - System.currentTimeMillis();
if (!has_msg && timeoutMillis <= 0) {
removeMsgAvailableListener(t);
break;
}
}
return has_msg;
}
public boolean hasMessage(Task eo) {
boolean has_msg;
synchronized (this) {
int n = numMsgs;
if (n > 0) {
has_msg = true;
} else {
has_msg = false;
addMsgAvailableListener(eo);
}
}
return has_msg;
}
public boolean hasMessages(int num, Task eo) {
boolean has_msg;
synchronized (this) {
int n = numMsgs;
if (n >= num) {
has_msg = true;
} else {
has_msg = false;
addMsgAvailableListener(eo);
}
}
return has_msg;
}
public T peek(int idx) {
assert idx >= 0 : "negative index";
T msg;
synchronized (this) {
int n = numMsgs;
if (idx < n) {
int ic = icons;
msg = msgs[(ic + idx) % msgs.length];
assert msg != null : "peeked null message!";
} else {
msg = null;
}
}
return msg;
}
public T remove(final int idx) {
assert idx >= 0 : "negative index";
T msg;
synchronized (this) {
int n = numMsgs;
assert idx < numMsgs;
if (idx < n) {
int ic = icons;
int mlen = msgs.length;
msg = msgs[(ic + idx) % mlen];
for (int i = idx; i > 0; i--) {
msgs[(ic + i) % mlen] = msgs[(ic + i - 1) % mlen];
}
msgs[icons] = null;
numMsgs -= 1;
icons = (icons + 1) % mlen;
} else {
throw new IllegalStateException();
}
}
return msg;
}
public synchronized Object[] messages() {
synchronized (this) {
Object[] result = new Object[numMsgs];
for (int i = 0; i < numMsgs; i++) {
result[i] = msgs[(icons + i) % msgs.length];
}
return result;
}
}
/**
* Takes an array of mailboxes and returns the index of the first mailbox
* that has a message. It is possible that because of race conditions, an
* earlier mailbox in the list may also have received a message.
*/
// TODO: need timeout variant
@SuppressWarnings("unchecked")
public static int select(Mailbox... mboxes) throws Pausable {
while (true) {
for (int i = 0; i < mboxes.length; i++) {
if (mboxes[i].hasMessage()) {
return i;
}
}
Task t = Task.getCurrentTask();
EmptySet_MsgAvListener pauseReason =
new EmptySet_MsgAvListener(t, mboxes);
for (int i = 0; i < mboxes.length; i++) {
mboxes[i].addMsgAvailableListener(pauseReason);
}
Task.pause(pauseReason);
for (int i = 0; i < mboxes.length; i++) {
mboxes[i].removeMsgAvailableListener(pauseReason);
}
}
}
public synchronized void addSpaceAvailableListener(EventSubscriber spcSub) {
srcs.add(spcSub);
}
public synchronized void removeSpaceAvailableListener(EventSubscriber spcSub) {
srcs.remove(spcSub);
}
public synchronized void addMsgAvailableListener(EventSubscriber msgSub) {
if (sink != null && sink != msgSub) {
throw new AssertionError(
"Error: A mailbox can not be shared by two consumers. New = "
+ msgSub + ", Old = " + sink);
}
sink = msgSub;
}
public synchronized void removeMsgAvailableListener(EventSubscriber msgSub) {
if (sink == msgSub) {
sink = null;
}
}
/**
* Attempt to put a message, and return true if successful. The thread is not blocked, nor is the task
* paused under any circumstance.
*/
public boolean putnb(T msg) {
return put(msg, null);
}
/**
* put a non-null message in the mailbox, and pause the calling task until the
* mailbox has space
*/
public void put(T msg) throws Pausable {
Task t = Task.getCurrentTask();
while (!put(msg, t)) {
Task.pause(this);
removeSpaceAvailableListener(t);
}
}
/**
* put a non-null message in the mailbox, and pause the calling task for timeoutMillis
* if the mailbox is full.
*/
public boolean put(T msg, int timeoutMillis) throws Pausable {
final Task t = Task.getCurrentTask();
long begin = System.currentTimeMillis();
while (!put(msg, t)) {
TimerTask tt = new TimerTask() {
public void run() {
Mailbox.this.removeSpaceAvailableListener(t);
t.onEvent(Mailbox.this, timedOut);
}
};
Task.timer.schedule(tt, timeoutMillis);
Task.pause(this);
removeSpaceAvailableListener(t);
if (System.currentTimeMillis() - begin >= timeoutMillis) {
return false;
}
}
return true;
}
public void putb(T msg) {
putb(msg, 0 /* infinite wait */);
}
public class BlockingSubscriber implements EventSubscriber {
public volatile boolean eventRcvd = false;
public void onEvent(EventPublisher ep, Event e) {
synchronized (Mailbox.this) {
eventRcvd = true;
Mailbox.this.notify();
}
}
public void blockingWait(final long timeoutMillis) {
long start = System.currentTimeMillis();
long remaining = timeoutMillis;
boolean infiniteWait = timeoutMillis == 0;
synchronized (Mailbox.this) {
while (!eventRcvd && (infiniteWait || remaining > 0)) {
try {
Mailbox.this.wait(infiniteWait? 0 : remaining);
} catch (InterruptedException ie) {}
long elapsed = System.currentTimeMillis() - start;
remaining -= elapsed;
}
}
}
}
/**
* put a non-null message in the mailbox, and block the calling thread for timeoutMillis
* if the mailbox is full.
*/
public void putb(T msg, final long timeoutMillis) {
BlockingSubscriber evs = new BlockingSubscriber();
if (!put(msg, evs)) {
evs.blockingWait(timeoutMillis);
}
if (!evs.eventRcvd) {
removeSpaceAvailableListener(evs);
}
}
public synchronized int size() {
return numMsgs;
}
public synchronized boolean hasMessage() {
return numMsgs > 0;
}
public synchronized boolean hasSpace() {
return (maxMsgs - numMsgs) > 0;
}
/**
* retrieve a message, blocking the thread indefinitely. Note, this is a
* heavyweight block, unlike #get() that pauses the Fiber but doesn't block
* the thread.
*/
public T getb() {
return getb(0);
}
/**
* retrieve a msg, and block the Java thread for the time given.
*
* @param millis. max wait time
* @return null if timed out.
*/
public T getb(final long timeoutMillis) {
BlockingSubscriber evs = new BlockingSubscriber();
T msg;
if ((msg = get(evs)) == null) {
evs.blockingWait(timeoutMillis);
if (evs.eventRcvd) {
msg = get(null); // non-blocking get.
assert msg != null: "Received event, but message is null";
}
}
if (msg == null) {
removeMsgAvailableListener(evs);
}
return msg;
}
public synchronized String toString() {
return "id:" + System.identityHashCode(this) + " " +
// DEBUG "nGet:" + nGet + " " +
// "nPut:" + nPut + " " +
// "numWastedPuts:" + nWastedPuts + " " +
// "nWastedGets:" + nWastedGets + " " +
"numMsgs:" + numMsgs;
}
// Implementation of PauseReason
public boolean isValid(Task t) {
synchronized(this) {
return (t == sink) || srcs.contains(t);
}
}
}
class EmptySet_MsgAvListener implements PauseReason, EventSubscriber {
final Task task;
final Mailbox<?>[] mbxs;
EmptySet_MsgAvListener(Task t, Mailbox<?>[] mbs) {
task = t;
mbxs = mbs;
}
public boolean isValid(Task t) {
// The pauseReason is true (there is valid reason to continue
// pausing) if none of the mboxes have any elements
for (Mailbox<?> mb : mbxs) {
if (mb.hasMessage())
return false;
}
return true;
}
public void onEvent(EventPublisher ep, Event e) {
for (Mailbox<?> m : mbxs) {
if (m != ep) {
((Mailbox<?>)ep).removeMsgAvailableListener(this);
}
}
task.resume();
}
public void cancel() {
for (Mailbox<?> mb : mbxs) {
mb.removeMsgAvailableListener(this);
}
}
}