![JAR search and dependency download from the Maven repository](/logo.png)
io.nextop.client.MessageControlState Maven / Gradle / Ivy
package io.nextop.client;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import io.nextop.Id;
import io.nextop.Message;
import io.nextop.Route;
import io.nextop.WireValue;
import io.nextop.sortedlist.SortedList;
import io.nextop.sortedlist.SplaySortedList;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action0;
import rx.functions.Func1;
import rx.subjects.BehaviorSubject;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.TimeUnit;
/** Shared state for all {@link MessageControlChannel} objects.
*
* Each {@link MessageControl} object is controlled by at most one channel object.
* Groups are locked, such that if the message control at the front of the group is controlled,
* the rest of group is inaccessible.
* Channels take/release control via {@link #take}/{@link #release}.
*
* The state allows introspection via the {@link #get} variants.
*
* Thread safe. */
// FIXME all MessageControl should go in here (not just send)
public final class MessageControlState {
private final MessageContext context;
private final Object mutex = new Object();
private int headIndex = 0;
private final Map entries;
private final Set pending;
/** these need to be attached to an entry on {@link #add} */
private final Multimap> pendingSubscribers;
private final Map groups;
private final SortedList groupsByPriority;
private final BehaviorSubject publish;
public MessageControlState(MessageContext context) {
this.context = context;
entries = new HashMap(32);
groups = new HashMap(8);
groupsByPriority = new SplaySortedList(COMPARATOR_GROUP_AVAILABLE);
pending = new HashSet(4);
pendingSubscribers = HashMultimap.create(4, 4);
publish = BehaviorSubject.create(this);
}
/////// QUEUE MANAGEMENT ///////
/** non-blocking */
@Nullable
public Entry takeFirstAvailable(MessageControlChannel owner) {
return takeFirstAvailable(null, null, owner);
}
/** non-blocking */
public Entry takeFirstAvailable(Id minExclusive, MessageControlChannel owner) {
if (null == minExclusive) {
throw new IllegalArgumentException();
}
return takeFirstAvailable(null, minExclusive, owner);
}
/** non-blocking */
public Entry takeFirstAvailable(Func1 predicate, MessageControlChannel owner) {
if (null == predicate) {
throw new IllegalArgumentException();
}
return takeFirstAvailable(predicate, null, owner);
}
/** non-blocking.
* this version is useful if testing to replace the head of a transfer
* with a more important entry.
* @param predicate takes the first eligible entry that passes this test
* @param minExclusive the minimum priority to search for the first available.
* If none available with greater priority, returns null. */
@Nullable
public Entry takeFirstAvailable(@Nullable Func1 predicate, @Nullable Id minExclusive, MessageControlChannel owner) {
synchronized (mutex) {
for (Group group : groupsByPriority) {
if (!group.entries.isEmpty()) {
Entry first = group.entries.get(0);
if (null != minExclusive && minExclusive.equals(first.id)) {
return null;
} else if (null == first.owner && (null == predicate || predicate.call(first))) {
take(first.id, owner);
return first;
}
}
}
return null;
}
}
/** blocking */
@Nullable
public Entry takeFirstAvailable(MessageControlChannel owner, long timeout, TimeUnit timeUnit) throws InterruptedException {
return takeFirstAvailable(null, null, owner, timeout, timeUnit);
}
/** blocking */
@Nullable
public Entry takeFirstAvailable(Id minExclusive, MessageControlChannel owner, long timeout, TimeUnit timeUnit) throws InterruptedException {
return takeFirstAvailable(null, minExclusive, owner, timeout, timeUnit);
}
/** blocking */
@Nullable
public Entry takeFirstAvailable(Func1 predicate, MessageControlChannel owner, long timeout, TimeUnit timeUnit) throws InterruptedException {
return takeFirstAvailable(predicate, null, owner, timeout, timeUnit);
}
/** blocking */
@Nullable
public Entry takeFirstAvailable(@Nullable Func1 predicate, @Nullable Id minExclusive, MessageControlChannel owner,
long timeout, TimeUnit timeUnit) throws InterruptedException {
final long nanosPerMillis = TimeUnit.MILLISECONDS.toNanos(1);
synchronized (mutex) {
long timeoutNanos = timeUnit.toNanos(timeout);
Entry entry;
while (null == (entry = takeFirstAvailable(predicate, minExclusive, owner)) && 0 < timeoutNanos) {
long nanos = System.nanoTime();
mutex.wait(timeoutNanos / nanosPerMillis, (int) (timeoutNanos % nanosPerMillis));
timeoutNanos -= (System.nanoTime() - nanos);
}
return entry;
}
}
/** non-blocking */
@Nullable
public boolean hasFirstAvailable() {
synchronized (mutex) {
for (Group group : groupsByPriority) {
if (!group.entries.isEmpty()) {
Entry first = group.entries.get(0);
if (null == first.owner) {
return true;
}
}
}
return false;
}
}
/** non-blocking. */
public boolean hasFirstAvailable(Id min) {
if (null == min) {
throw new IllegalArgumentException();
}
synchronized (mutex) {
for (Group group : groupsByPriority) {
if (!group.entries.isEmpty()) {
Entry first = group.entries.get(0);
if (min.equals(first.id)) {
return false;
} else if (null == first.owner) {
return true;
}
}
}
return false;
}
}
/** blocking */
public boolean hasFirstAvailable(long timeout, TimeUnit timeUnit) throws InterruptedException {
final long nanosPerMillis = TimeUnit.MILLISECONDS.toNanos(1);
synchronized (mutex) {
long timeoutNanos = timeUnit.toNanos(timeout);
boolean a;
while (!(a = hasFirstAvailable()) && 0 < timeoutNanos) {
long nanos = System.nanoTime();
mutex.wait(timeoutNanos / nanosPerMillis, (int) (timeoutNanos % nanosPerMillis));
timeoutNanos -= (System.nanoTime() - nanos);
}
return a;
}
}
/** blocking */
public boolean hasFirstAvailable(Id min, long timeout, TimeUnit timeUnit) throws InterruptedException {
final long nanosPerMillis = TimeUnit.MILLISECONDS.toNanos(1);
synchronized (mutex) {
long timeoutNanos = timeUnit.toNanos(timeout);
boolean a;
while (!(a = hasFirstAvailable(min)) && 0 < timeoutNanos) {
long nanos = System.nanoTime();
mutex.wait(timeoutNanos / nanosPerMillis, (int) (timeoutNanos % nanosPerMillis));
timeoutNanos -= (System.nanoTime() - nanos);
}
return a;
}
}
/** available if all:
* - no owner
* - first in group */
public boolean isAvailable(Id id) {
synchronized (mutex) {
@Nullable Entry entry = entries.get(id);
if (null == entry) {
return false;
}
// owner
if (null != entry.owner) {
return false;
} // else no owner
Group group = entry.group;
assert null != group;
// first in group
if (group.entries.isEmpty()) {
return false;
}
Entry first = group.entries.get(0);
return id.equals(first.id);
}
}
public void take(Id id, MessageControlChannel owner) {
synchronized (mutex) {
@Nullable Entry entry = entries.get(id);
if (null == entry) {
throw new IllegalArgumentException();
}
if (null != entry.owner) {
throw new IllegalArgumentException();
}
Group group = entry.group;
assert null != group;
groupsByPriority.remove(group);
try {
group.take(entry, owner);
} finally {
groupsByPriority.insert(group);
}
mutex.notifyAll();
}
publish();
}
public void release(Id id, MessageControlChannel owner) {
synchronized (mutex) {
@Nullable Entry entry = entries.get(id);
if (null == entry) {
throw new IllegalArgumentException();
}
if (owner != entry.owner) {
throw new IllegalArgumentException();
}
Group group = entry.group;
assert null != group;
groupsByPriority.remove(group);
try {
group.release(entry, owner);
} finally {
groupsByPriority.insert(group);
}
mutex.notifyAll();
}
publish();
}
/** this should be called immediately before inserting a message control
* into the channel. It helps provide a fast negative for queries
* for bad IDs (could be for a number of reasons).
* @see #getObservable(io.nextop.Id, long, java.util.concurrent.TimeUnit) */
public void notifyPending(Id id) {
synchronized (mutex) {
pending.add(id);
}
}
public boolean add(MessageControl mc) {
// see notes at top - only SEND.MESSAGE message control
Entry entry;
Collection> subscribers;
synchronized (mutex) {
// check already added
if (entries.containsKey(mc.message.id)) {
return false;
}
entry = new Entry(headIndex++, mc);
entries.put(entry.id, entry);
pending.remove(entry.id);
subscribers = pendingSubscribers.removeAll(entry.id);
@Nullable Group group = groups.get(entry.groupId);
if (null == group) {
Id groupId = entry.groupId;
group = new Group(groupId);
groups.put(groupId, group);
// don't remove from groupsByPriority because not present
} else {
groupsByPriority.remove(group);
}
group.add(entry);
groupsByPriority.insert(group);
mutex.notifyAll();
}
// add the subscribers (which publishes to them)
for (Subscriber subscriber : subscribers) {
entry.publish.subscribe(subscriber);
}
publish();
return true;
}
@Nullable
public MessageControl remove(Id id, End end) {
Entry entry;
synchronized (mutex) {
entry = entries.remove(id);
if (null == entry) {
return null;
}
assert null == entry.end;
Group group = entry.group;
assert null != group;
groupsByPriority.remove(group);
group.remove(entry);
if (!group.entries.isEmpty()) {
groupsByPriority.insert(group);
}
entry.end = end;
mutex.notifyAll();
}
entry.publish();
entry.publishComplete();
publish();
return entry.mc;
}
public boolean yield(Id id) {
Entry entry;
synchronized (mutex) {
entry = entries.get(id);
if (null == entry) {
return false;
}
assert null == entry.end;
Group group = entry.group;
assert null != group;
groupsByPriority.remove(group);
group.yield(entry);
groupsByPriority.insert(group);
mutex.notifyAll();
}
entry.publish();
publish();
return true;
}
public boolean setInboxTransferProgress(Id id, TransferProgress transferProgress) {
Entry entry;
synchronized (mutex) {
entry = entries.get(id);
if (null == entry) {
return false;
}
if (null != entry.end) {
return false;
}
entry.inboxTransferProgress = transferProgress;
}
entry.publish();
publish();
return true;
}
public boolean setOutboxTransferProgress(Id id, TransferProgress transferProgress) {
Entry entry;
synchronized (mutex) {
entry = entries.get(id);
if (null == entry) {
return false;
}
if (null != entry.end) {
return false;
}
entry.outboxTransferProgress = transferProgress;
// FIXME remove
// System.out.printf(" outbox transfer progress %s\n", transferProgress);
}
entry.publish();
publish();
return true;
}
/////// INSPECTION ///////
// triggers when groups or indexes change
// does not trigger when entry-only properties change (e.g. progress, active, etc)
public Observable getObservable() {
return publish;
}
private void publish() {
publish.onNext(this);
}
public Observable getObservable(final Id id) {
return getObservable(id, 0, TimeUnit.MILLISECONDS);
}
public Observable getObservable(final Id id, final long timeout, final TimeUnit timeUnit) {
// on subscribe, if no entry, add subscriber to pending observers for entry
return Observable.create(new Observable.OnSubscribe() {
@Override
public void call(final Subscriber super Entry> subscriber) {
@Nullable Entry entry;
synchronized (mutex) {
entry = entries.get(id);
if (null == entry) {
if (0 < timeout && /* see #notifyPending */ pending.contains(id)) {
pendingSubscribers.put(id, subscriber);
// TODO manually clean up the timeout when the subscriber is taken
// add the timeout
subscriber.add(context.getScheduler().createWorker().schedule(new Action0() {
@Override
public void call() {
synchronized (mutex) {
if (pendingSubscribers.containsEntry(id, subscriber)) {
pendingSubscribers.remove(id, subscriber);
subscriber.onCompleted();
subscriber.unsubscribe();
}
}
}
}, timeout, timeUnit));
} else {
subscriber.onCompleted();
subscriber.unsubscribe();
}
}
}
if (null != entry) {
entry.publish.subscribe(subscriber);
}
}
});
}
public int size() {
synchronized (mutex) {
int c = 0;
for (Group g : groupsByPriority) {
c += g.entries.size();
}
return c;
}
}
public int indexOf(Id id) {
synchronized (mutex) {
@Nullable Entry entry = entries.get(id);
if (null == entry) {
return -1;
}
@Nullable Group group = groups.get(entry.message.groupId);
if (null == group) {
return -1;
}
int c = 0;
for (Group g : groupsByPriority) {
if (group == g) {
break;
}
c += g.entries.size();
}
return c + group.entries.indexOf(entry);
}
}
public Entry get(int index) {
synchronized (mutex) {
if (index < 0) {
throw new IndexOutOfBoundsException();
}
int c = index;
for (Group g : groupsByPriority) {
int n = g.entries.size();
if (c < n) {
return g.entries.get(c);
}
c -= n;
}
throw new IndexOutOfBoundsException();
}
}
public List getGroups() {
synchronized (mutex) {
final List groupSnapshots = new ArrayList(groupsByPriority.size());
for (Group g : groupsByPriority) {
groupSnapshots.add(new GroupSnapshot(g.groupId, ImmutableList.copyOf(g.entries)));
}
return Collections.unmodifiableList(groupSnapshots);
}
}
public Entry get(Id groupId, int index) {
synchronized (mutex) {
@Nullable Group group = groups.get(groupId);
if (null == group) {
throw new IndexOutOfBoundsException();
}
int n = group.entries.size();
if (index < 0 || n <= index) {
throw new IndexOutOfBoundsException();
}
return group.entries.get(index);
}
}
/////// MessageControlChannel SUPPORT ///////
// TODO (stuff to think about)
// TODO there will likely be a version where some intermediary node persists to disk
// TODO and rewrites the WireValue to have a pointer to disk location
// TODO in those cases, the message out will need to be translated in the reverse direction
/** standard implementation to respond to internal control messages */
public boolean onActiveMessageControl(MessageControl mc, MessageControlChannel upstream) {
Message message = mc.message;
Route route = message.route;
if (Message.isLocal(route)) {
Id id = Message.getLocalId(route);
if (null != id) {
if (MessageControl.Type.ERROR.equals(mc.type) && Message.outboxRoute(id).equals(route)) {
// cancel
if (null != remove(id, End.ERROR)) {
upstream.onMessageControl(MessageControl.receive(MessageControl.Type.ERROR, Message.inboxRoute(id)));
}
} else if (MessageControl.Type.MESSAGE.equals(mc.type) && Message.echoRoute(id).equals(route)) {
@Nullable MessageControl rmc = createRedirect(id, message.inboxRoute());
if (null != rmc) {
upstream.onMessageControl(rmc);
return true;
} else {
return false;
}
} // else fall through
} // else no entry for message; fall through
}
return false;
}
@Nullable
private MessageControl createRedirect(Id id, Route newRoute) {
@Nullable Entry entry;
synchronized (mutex) {
entry = entries.get(id);
}
if (null == entry) {
return null;
}
Message message = entry.message;
return MessageControl.receive(MessageControl.Type.MESSAGE, message.toBuilder()
.setHeader(Message.H_REDIRECT, WireValue.of(Collections.singletonList(message.route.toString())))
.setRoute(newRoute)
.build());
}
public static enum End {
COMPLETED,
ERROR
}
public static final class Entry {
int index;
/** alias from message */
public final Id id;
/** alias from message */
public final Id groupId;
/** alias from message */
public final int groupPriority;
public final Message message;
public final MessageControl mc;
/////// PROPERTIES ///////
// read-only to clients
// these are updated in a lock
// the volatile is for reading
@Nullable
public volatile MessageControlChannel owner = null;
public volatile TransferProgress outboxTransferProgress;
public volatile TransferProgress inboxTransferProgress;
@Nullable
public volatile End end = null;
// internal
final BehaviorSubject publish;
/** set by {@link Group#add}/{@link Group#remove} */
@Nullable Group group = null;
Entry(int index, MessageControl mc) {
this.index = index;
this.mc = mc;
message = mc.message;
groupId = mc.message.groupId;
id = mc.message.id;
groupPriority = mc.message.groupPriority;
publish = BehaviorSubject.create(this);
outboxTransferProgress = TransferProgress.none(id);
inboxTransferProgress = TransferProgress.none(id);
}
private void publish() {
publish.onNext(this);
}
private void publishComplete() {
publish.onCompleted();
}
}
public static final class TransferProgress {
public static TransferProgress none(Id id) {
return create(id, 0, 0);
}
public static TransferProgress create(Id id, long completedBytes, long totalBytes) {
if (totalBytes < 0) {
throw new IllegalArgumentException(String.format("%d", totalBytes));
}
if (completedBytes < 0 || 0 < totalBytes && totalBytes < completedBytes) {
throw new IllegalArgumentException(String.format("%d %d", completedBytes, totalBytes));
}
return new TransferProgress(id, completedBytes, totalBytes);
}
public final Id id;
public final long completedBytes;
public final long totalBytes;
TransferProgress(Id id, long completedBytes, long totalBytes) {
this.id = id;
this.completedBytes = completedBytes;
this.totalBytes = totalBytes;
}
public boolean isNone() {
return 0 == completedBytes && 0 == totalBytes;
}
public float asFloat() {
final int q = 1000;
return 0 < totalBytes ? (q * completedBytes / totalBytes) / (float) q : 0.f;
}
@Override
public String toString() {
if (isNone()) {
return "-";
} else {
return String.format("%s %d/%d (%.2f%%)", id, completedBytes, totalBytes, asFloat());
}
}
@Override
public int hashCode() {
int c = id.hashCode();
c = 31 * c + (int)(completedBytes ^ (completedBytes >>> 32));
c = 31 * c + (int)(totalBytes ^ (totalBytes >>> 32));
return c;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TransferProgress)) {
return false;
}
TransferProgress p = (TransferProgress) obj;
return completedBytes == p.completedBytes
&& totalBytes == p.totalBytes
&& id.equals(p.id);
}
}
public static final class GroupSnapshot {
public final Id groupId;
public final List entries;
// FIXME priority
GroupSnapshot(Id groupId, List entries) {
this.groupId = groupId;
this.entries = entries;
}
}
// internal
private final class Group {
final Id groupId;
final PriorityQueue entriesByPriority;
final SortedList entries;
Group(Id groupId) {
this.groupId = groupId;
entriesByPriority = new PriorityQueue(8, COMPARATOR_ENTRY_DESCENDING_PRIORITY);
entries = new SplaySortedList(COMPARATOR_ENTRY_AVAILABLE);
}
void add(Entry entry) {
if (null != entry.group) {
throw new IllegalArgumentException();
}
entry.group = this;
entriesByPriority.add(entry);
entries.insert(entry);
}
void remove(Entry entry) {
if (this != entry.group) {
throw new IllegalArgumentException();
}
entries.remove(entry);
entriesByPriority.remove(entry);
entry.group = null;
}
void yield(Entry entry) {
if (this != entry.group) {
throw new IllegalArgumentException();
}
entries.remove(entry);
entriesByPriority.remove(entry);
entry.index = headIndex++;
entriesByPriority.add(entry);
entries.insert(entry);
}
void take(Entry entry, MessageControlChannel owner) {
if (this != entry.group) {
throw new IllegalArgumentException();
}
// this should be strictly enforced by the caller
assert null == entry.owner;
entries.remove(entry);
try {
entry.owner = owner;
} finally {
entries.insert(entry);
}
}
void release(Entry entry, MessageControlChannel owner) {
if (this != entry.group) {
throw new IllegalArgumentException();
}
// this should be strictly enforced by the caller
assert owner == entry.owner;
entries.remove(entry);
try {
entry.owner = null;
} finally {
entries.insert(entry);
}
}
}
private static final Comparator COMPARATOR_GROUP_AVAILABLE = new Comparator() {
@Override
public int compare(Group a, Group b) {
// compare by emptiness
// -- if both empty, get an absolute order using the group id
// compare by max prio of group
// compare by index
if (a == b) {
return 0;
}
boolean aEmpty = a.entries.isEmpty();
boolean bEmpty = b.entries.isEmpty();
if (aEmpty && bEmpty) {
// stable
return a.groupId.compareTo(b.groupId);
} else if (aEmpty) {
return 1;
} else if (bEmpty) {
return -1;
}
int aMaxGroupPriority = a.entriesByPriority.peek().groupPriority;
int bMaxGroupPriority = b.entriesByPriority.peek().groupPriority;
if (aMaxGroupPriority < bMaxGroupPriority) {
return 1;
} else if (bMaxGroupPriority < aMaxGroupPriority) {
return -1;
}
int aIndex = a.entries.get(0).index;
int bIndex = b.entries.get(0).index;
if (aIndex < bIndex) {
return -1;
} else if (bIndex < aIndex) {
return 1;
} else {
// same entry in two different groups
throw new IllegalStateException();
}
}
};
private static final Comparator COMPARATOR_ENTRY_AVAILABLE = new Comparator() {
@Override
public int compare(Entry a, Entry b) {
// compare by owner (owned at front)
// compare by index
if (a == b) {
return 0;
}
boolean aOwned = null != a.owner;
boolean bOwned = null != b.owner;
if (aOwned != bOwned) {
if (aOwned) {
return -1;
} else {
return 1;
}
}
int aIndex = a.index;
int bIndex = b.index;
if (aIndex < bIndex) {
return -1;
} else if (bIndex < aIndex) {
return 1;
} else {
// same index in two different entries
throw new IllegalStateException();
}
}
};
private static final Comparator COMPARATOR_ENTRY_DESCENDING_PRIORITY = new Comparator() {
@Override
public int compare(Entry a, Entry b) {
if (a.groupPriority < b.groupPriority) {
return 1;
} else if (b.groupPriority < a.groupPriority) {
return -1;
} else {
return 0;
}
}
};
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy