package org.jgroups.protocols;
import org.jgroups.*;
import org.jgroups.annotations.Experimental;
import org.jgroups.annotations.MBean;
import org.jgroups.annotations.ManagedAttribute;
import org.jgroups.annotations.Property;
import org.jgroups.stack.Protocol;
import org.jgroups.util.AckCollector;
import org.jgroups.util.TimeScheduler;
import java.io.DataInput;
import java.io.DataOutput;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Protocol which implements synchronous messages (https://issues.jboss.org/browse/JGRP-1389). A send of a message M
* with flag RSVP set will block until all non-faulty recipients (one for unicasts, N for multicasts) have acked M, or
* until a timeout kicks in.
* @author Bela Ban
* @since 3.1
*/
@Experimental
@MBean(description="Implements synchronous acks for messages which have their RSVP flag set)")
public class RSVP extends Protocol {
/* ----------------------------------------- Properties -------------------------------------------------- */
@Property(description="Max time in milliseconds to block for an RSVP'ed message (0 blocks forever).")
protected long timeout=10000;
@Property(description="Whether an exception should be thrown when the timeout kicks in, and we haven't yet received " +
"all acks. An exception would be thrown all the way up to JChannel.send()")
protected boolean throw_exception_on_timeout=true;
@Property(description="When true, we pass the message up to the application and only then send an ack. When false, " +
"we send an ack first and only then pass the message up to the application.")
protected boolean ack_on_delivery=true;
@Property(description="Interval (in milliseconds) at which we resend the RSVP request. Needs to be < timeout. 0 disables it.")
protected long resend_interval=2000;
/* --------------------------------------------- Fields ------------------------------------------------------ */
/** ID to be used to identify messages. Short.MAX_VALUE (ca 32K plus 32K negative) should be enough, and wrap-around
* shouldn't be an issue. Using Message.Flag.RSVP should be the exception, not the rule... */
protected short current_id=0;
protected TimeScheduler timer;
protected volatile List<Address> members=new ArrayList<Address>();
protected Address local_addr;
/** Used to store IDs and their acks */
protected final Map<Short,Entry> ids=new HashMap<Short,Entry>();
@ManagedAttribute(description="Number of pending RSVP requests")
public int getPendingRsvpRequests() {synchronized(ids) {return ids.size();}}
public void init() throws Exception {
super.init();
timer=getTransport().getTimer();
if(timeout > 0 && resend_interval > 0 && resend_interval >= timeout) {
log.warn("resend_interval (" + resend_interval + ") is >= timeout (" + timeout + "); setting " +
"resend_interval to timeout / 3");
resend_interval=timeout / 3;
}
}
public void stop() {
synchronized(ids) {
for(Entry entry: ids.values())
entry.destroy();
ids.clear();
}
super.destroy();
}
public Object down(Event evt) {
switch(evt.getType()) {
case Event.MSG:
Message msg=(Message)evt.getArg();
if(!msg.isFlagSet(Message.Flag.RSVP))
break;
short next_id=getNextId();
RsvpHeader hdr=new RsvpHeader(RsvpHeader.REQ, next_id);
msg.putHeader(id, hdr);
// 1. put into hashmap
Address target=msg.getDest();
Entry entry=target != null? new Entry(target) : new Entry(members); // volatile read of members
Object retval=null;
try {
synchronized(ids) {
ids.put(next_id, entry);
}
// sync members again - if a view was received after reading members intro Entry, but
// before adding Entry to ids (https://issues.jboss.org/browse/JGRP-1503)
entry.retainAll(members);
// 2. start timer task
entry.startTask(next_id);
// 3. Send the message
if(log.isTraceEnabled())
log.trace(local_addr + ": " + hdr.typeToString() + " --> " + target);
retval=down_prot.down(evt);
// 4. Block on AckCollector
entry.block(timeout);
}
catch(TimeoutException e) {
if(throw_exception_on_timeout)
throw e;
else if(log.isWarnEnabled())
log.warn("message ran into a timeout, missing acks: " + entry);
}
finally {
synchronized(ids) {
Entry tmp=ids.remove(next_id);
if(tmp != null)
tmp.destroy();
}
}
return retval;
case Event.VIEW_CHANGE:
handleView((View)evt.getArg());
break;
case Event.SET_LOCAL_ADDRESS:
local_addr=(Address)evt.getArg();
break;
}
return down_prot.down(evt);
}
public Object up(Event evt) {
switch(evt.getType()) {
case Event.MSG:
Message msg=(Message)evt.getArg();
if(msg.isFlagSet(Message.Flag.RSVP)) {
RsvpHeader hdr=(RsvpHeader)msg.getHeader(id);
if(hdr == null) {
log.error("message with RSVP flag needs to have an RsvpHeader");
break;
}
Address sender=msg.getSrc();
if(log.isTraceEnabled())
log.trace(local_addr + ": " + hdr.typeToString() + " <-- " + sender);
switch(hdr.type) {
case RsvpHeader.REQ:
if(this.ack_on_delivery) {
try {
return up_prot.up(evt);
}
finally {
sendResponse(sender, hdr.id);
}
}
else {
sendResponse(sender, hdr.id);
return up_prot.up(evt);
}
case RsvpHeader.REQ_ONLY:
return null;
case RsvpHeader.RSP:
handleResponse(msg.getSrc(), hdr.id);
return null;
}
}
break;
case Event.VIEW_CHANGE:
handleView((View)evt.getArg());
break;
}
return up_prot.up(evt);
}
protected void handleView(View view) {
members=view.getMembers();
synchronized(ids) {
for(Iterator<Map.Entry<Short,Entry>> it=ids.entrySet().iterator(); it.hasNext();) {
Entry entry=it.next().getValue();
if(entry != null && entry.retainAll(view.getMembers()) && entry.size() == 0) {
entry.destroy();
it.remove();
}
}
}
}
protected void handleResponse(Address member, short id) {
synchronized(ids) {
Entry entry=ids.get(id);
if(entry != null) {
entry.ack(member);
if(entry.size() == 0) {
entry.destroy();
ids.remove(id);
}
}
}
}
protected void sendResponse(Address dest, short id) {
try {
Message msg=new Message(dest);
msg.setFlag(Message.Flag.RSVP, Message.Flag.OOB);
RsvpHeader hdr=new RsvpHeader(RsvpHeader.RSP,id);
msg.putHeader(this.id, hdr);
if(log.isTraceEnabled())
log.trace(local_addr + ": " + hdr.typeToString() + " --> " + dest);
down_prot.down(new Event(Event.MSG, msg));
}
catch(Throwable t) {
log.error("failed sending response", t);
}
}
protected synchronized short getNextId() {
return current_id++;
}
protected class Entry {
protected final AckCollector ack_collector;
protected final Address target; // if null --> multicast, else --> unicast
protected Future<?> resend_task;
/** Unicast entry */
protected Entry(Address member) {
this.target=member;
this.ack_collector=new AckCollector(member);
}
/** Multicast entry */
protected Entry(Collection<Address> members) {
this.target=null;
this.ack_collector=new AckCollector(members);
}
protected void startTask(final short rsvp_id) {
if(resend_task != null && !resend_task.isDone())
resend_task.cancel(false);
resend_task=timer.scheduleWithFixedDelay(new Runnable() {
public void run() {
if(ack_collector.size() == 0) {
cancelTask();
return;
}
Message msg=new Message(target);
msg.setFlag(Message.Flag.RSVP);
RsvpHeader hdr=new RsvpHeader(RsvpHeader.REQ_ONLY, rsvp_id);
msg.putHeader(id, hdr);
if(log.isTraceEnabled())
log.trace(local_addr + ": " + hdr.typeToString() + " --> " + target);
down_prot.down(new Event(Event.MSG, msg));
}
}, resend_interval, resend_interval, TimeUnit.MILLISECONDS);
}
protected void cancelTask() {
if(resend_task != null)
resend_task.cancel(false);
ack_collector.destroy();
}
protected void ack(Address member) {ack_collector.ack(member);}
protected boolean retainAll(Collection<Address> members) {return ack_collector.retainAll(members);}
protected int size() {return ack_collector.size();}
protected void block(long timeout) throws TimeoutException {ack_collector.waitForAllAcks(timeout);}
protected void destroy() {cancelTask();}
public String toString() {return ack_collector.toString();}
}
protected static class RsvpHeader extends Header {
protected static final byte REQ = 1;
protected static final byte REQ_ONLY = 2;
protected static final byte RSP = 3;
protected byte type;
protected short id;
public RsvpHeader() {
}
public RsvpHeader(byte type, short id) {
this.type=type;
this.id=id;
}
public int size() {
return Global.BYTE_SIZE + Global.SHORT_SIZE;
}
public void writeTo(DataOutput out) throws Exception {
out.writeByte(type);
out.writeShort(id);
}
public void readFrom(DataInput in) throws Exception {
type=in.readByte();
id=in.readShort();
}
public String toString() {
String tmp=typeToString();
return tmp + "(" + id + ")";
}
protected String typeToString() {
switch(type) {
case REQ : return "REQ";
case REQ_ONLY: return "REQ-ONLY";
case RSP: return "RSP";
default: return "unknown";
}
}
}
}