/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.catalina.tribes.group.interceptors;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.catalina.tribes.ChannelException;
import org.apache.catalina.tribes.ChannelMessage;
import org.apache.catalina.tribes.Member;
import org.apache.catalina.tribes.group.ChannelInterceptorBase;
import org.apache.catalina.tribes.group.InterceptorPayload;
import org.apache.catalina.tribes.io.XByteBuffer;
import org.apache.catalina.tribes.util.StringManager;
/**
*
* The order interceptor guarantees that messages are received in the same order they were
* sent.
* This interceptor works best with the ack=true setting. <br>
* There is no point in
* using this with the replicationMode="fastasynchqueue" as this mode guarantees ordering.<BR>
* If you are using the mode ack=false replicationMode=pooled, and have a lot of concurrent threads,
* this interceptor can really slow you down, as many messages will be completely out of order
* and the queue might become rather large. If this is the case, then you might want to set
* the value OrderInterceptor.maxQueue = 25 (meaning that we will never keep more than 25 messages in our queue)
* <br><b>Configuration Options</b><br>
* OrderInterceptor.expire=<milliseconds> - if a message arrives out of order, how long before we act on it <b>default=3000ms</b><br>
* OrderInterceptor.maxQueue=<max queue size> - how much can the queue grow to ensure ordering.
* This setting is useful to avoid OutOfMemoryErrors<b>default=Integer.MAX_VALUE</b><br>
* OrderInterceptor.forwardExpired=<boolean> - this flag tells the interceptor what to
* do when a message has expired or the queue has grown larger than the maxQueue value.
* true means that the message is sent up the stack to the receiver that will receive and out of order message
* false means, forget the message and reset the message counter. <b>default=true</b>
*
*
* @version 1.1
*/
public class OrderInterceptor extends ChannelInterceptorBase {
protected static final StringManager sm = StringManager.getManager(OrderInterceptor.class);
private final Map<Member, Counter> outcounter = new HashMap<>();
private final Map<Member, Counter> incounter = new HashMap<>();
private final Map<Member, MessageOrder> incoming = new HashMap<>();
private long expire = 3000;
private boolean forwardExpired = true;
private int maxQueue = Integer.MAX_VALUE;
final ReentrantReadWriteLock inLock = new ReentrantReadWriteLock(true);
final ReentrantReadWriteLock outLock= new ReentrantReadWriteLock(true);
@Override
public void sendMessage(Member[] destination, ChannelMessage msg, InterceptorPayload payload) throws ChannelException {
if ( !okToProcess(msg.getOptions()) ) {
super.sendMessage(destination, msg, payload);
return;
}
ChannelException cx = null;
for (int i=0; i<destination.length; i++ ) {
try {
int nr = 0;
outLock.writeLock().lock();
try {
nr = incCounter(destination[i]);
} finally {
outLock.writeLock().unlock();
}
//reduce byte copy
msg.getMessage().append(nr);
try {
getNext().sendMessage(new Member[] {destination[i]}, msg, payload);
} finally {
msg.getMessage().trim(4);
}
}catch ( ChannelException x ) {
if ( cx == null ) cx = x;
cx.addFaultyMember(x.getFaultyMembers());
}
}//for
if ( cx != null ) throw cx;
}
@Override
public void messageReceived(ChannelMessage msg) {
if ( !okToProcess(msg.getOptions()) ) {
super.messageReceived(msg);
return;
}
int msgnr = XByteBuffer.toInt(msg.getMessage().getBytesDirect(),msg.getMessage().getLength()-4);
msg.getMessage().trim(4);
MessageOrder order = new MessageOrder(msgnr,(ChannelMessage)msg.deepclone());
inLock.writeLock().lock();
try {
if ( processIncoming(order) ) processLeftOvers(msg.getAddress(),false);
} finally {
inLock.writeLock().unlock();
}
}
protected void processLeftOvers(Member member, boolean force) {
MessageOrder tmp = incoming.get(member);
if ( force ) {
Counter cnt = getInCounter(member);
cnt.setCounter(Integer.MAX_VALUE);
}
if ( tmp!= null ) processIncoming(tmp);
}
/**
*
* @param order MessageOrder
* @return boolean - true if a message expired and was processed
*/
protected boolean processIncoming(MessageOrder order) {
boolean result = false;
Member member = order.getMessage().getAddress();
Counter cnt = getInCounter(member);
MessageOrder tmp = incoming.get(member);
if ( tmp != null ) {
order = MessageOrder.add(tmp,order);
}
while ( (order!=null) && (order.getMsgNr() <= cnt.getCounter()) ) {
//we are right on target. process orders
if ( order.getMsgNr() == cnt.getCounter() ) cnt.inc();
else if ( order.getMsgNr() > cnt.getCounter() ) cnt.setCounter(order.getMsgNr());
super.messageReceived(order.getMessage());
order.setMessage(null);
order = order.next;
}
MessageOrder head = order;
MessageOrder prev = null;
tmp = order;
//flag to empty out the queue when it larger than maxQueue
boolean empty = order!=null?order.getCount()>=maxQueue:false;
while ( tmp != null ) {
//process expired messages or empty out the queue
if ( tmp.isExpired(expire) || empty ) {
//reset the head
if ( tmp == head ) head = tmp.next;
cnt.setCounter(tmp.getMsgNr()+1);
if ( getForwardExpired() )
super.messageReceived(tmp.getMessage());
tmp.setMessage(null);
tmp = tmp.next;
if ( prev != null ) prev.next = tmp;
result = true;
} else {
prev = tmp;
tmp = tmp.next;
}
}
if ( head == null ) incoming.remove(member);
else incoming.put(member, head);
return result;
}
@Override
public void memberAdded(Member member) {
//notify upwards
super.memberAdded(member);
}
@Override
public void memberDisappeared(Member member) {
//reset counters - lock free
incounter.remove(member);
outcounter.remove(member);
//clear the remaining queue
processLeftOvers(member,true);
//notify upwards
super.memberDisappeared(member);
}
protected int incCounter(Member mbr) {
Counter cnt = getOutCounter(mbr);
return cnt.inc();
}
protected Counter getInCounter(Member mbr) {
Counter cnt = incounter.get(mbr);
if ( cnt == null ) {
cnt = new Counter();
cnt.inc(); //always start at 1 for incoming
incounter.put(mbr,cnt);
}
return cnt;
}
protected Counter getOutCounter(Member mbr) {
Counter cnt = outcounter.get(mbr);
if ( cnt == null ) {
cnt = new Counter();
outcounter.put(mbr,cnt);
}
return cnt;
}
protected static class Counter {
private final AtomicInteger value = new AtomicInteger(0);
public int getCounter() {
return value.get();
}
public void setCounter(int counter) {
this.value.set(counter);
}
public int inc() {
return value.addAndGet(1);
}
}
protected static class MessageOrder {
private final long received = System.currentTimeMillis();
private MessageOrder next;
private final int msgNr;
private ChannelMessage msg = null;
public MessageOrder(int msgNr,ChannelMessage msg) {
this.msgNr = msgNr;
this.msg = msg;
}
public boolean isExpired(long expireTime) {
return (System.currentTimeMillis()-received) > expireTime;
}
public ChannelMessage getMessage() {
return msg;
}
public void setMessage(ChannelMessage msg) {
this.msg = msg;
}
public void setNext(MessageOrder order) {
this.next = order;
}
public MessageOrder getNext() {
return next;
}
public int getCount() {
int counter = 1;
MessageOrder tmp = next;
while ( tmp != null ) {
counter++;
tmp = tmp.next;
}
return counter;
}
@SuppressWarnings("null") // prev cannot be null
public static MessageOrder add(MessageOrder head, MessageOrder add) {
if ( head == null ) return add;
if ( add == null ) return head;
if ( head == add ) return add;
if ( head.getMsgNr() > add.getMsgNr() ) {
add.next = head;
return add;
}
MessageOrder iter = head;
MessageOrder prev = null;
while ( iter.getMsgNr() < add.getMsgNr() && (iter.next !=null ) ) {
prev = iter;
iter = iter.next;
}
if ( iter.getMsgNr() < add.getMsgNr() ) {
//add after
add.next = iter.next;
iter.next = add;
} else if (iter.getMsgNr() > add.getMsgNr()) {
//add before
prev.next = add; // prev cannot be null here, warning suppressed
add.next = iter;
} else {
throw new ArithmeticException(sm.getString("orderInterceptor.messageAdded.sameCounter"));
}
return head;
}
public int getMsgNr() {
return msgNr;
}
}
public void setExpire(long expire) {
this.expire = expire;
}
public void setForwardExpired(boolean forwardExpired) {
this.forwardExpired = forwardExpired;
}
public void setMaxQueue(int maxQueue) {
this.maxQueue = maxQueue;
}
public long getExpire() {
return expire;
}
public boolean getForwardExpired() {
return forwardExpired;
}
public int getMaxQueue() {
return maxQueue;
}
}