package org.jgroups.protocols; import org.jgroups.*; import org.jgroups.annotations.*; import org.jgroups.stack.AddressGenerator; import org.jgroups.stack.IpAddress; import org.jgroups.stack.Protocol; import org.jgroups.util.*; import org.jgroups.util.UUID; import java.io.DataInput; import java.io.DataOutput; import java.util.*; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** * Simple relaying protocol: RELAY is added to the top of the stack, creates a channel to a bridge cluster, * and - if coordinator - relays all multicast messages via the bridge cluster to the remote cluster.<p/> * * This is <em>not</em> a big virtual cluster, e.g. consisting of {A,B,C,X,Y,Z}, but 2 <em>autonomous</em> clusters * {A,B,C} and {X,Y,Z}, bridged together by RELAY. For example, when B multicasts a message M, A (if it happens to be * the coord) relays M to X (which happens to be the other cluster's coordinator). X then re-broadcasts M, with M.src * being a ProxyUUID(X,B). This means that the sender of M in the {X,Y,Z} cluster will be X for all practical purposes, * but the original sender B is also recorded, for sending back a response.<p/> * * See [1] and [2] for details.<p/> * [1] https://jira.jboss.org/browse/JGRP-747<p/> * [2] doc/design/RELAY.txt * * @author Bela Ban * @since 2.12 */ @MBean(description="RELAY protocol") public class RELAY extends Protocol { /* ------------------------------------------ Properties ---------------------------------------------- */ @Property(description="Description of the local cluster, e.g. \"nyc\". This is added to every address, so it" + "should be short. This is a mandatory property and must be set",writable=false) protected String site; @Property(description="Properties of the bridge cluster (e.g. tcp.xml)") protected String bridge_props=null; @Property(description="Name of the bridge cluster") protected String bridge_name="bridge-cluster"; // @Property(description="If true, messages are relayed asynchronously, ie. via submission of a task to the timer thread pool") // protected boolean async=false; @Property(description="If set to false, don't perform relaying. Used e.g. for backup clusters; " + "unidirectional replication from one cluster to another, but not back. Can be changed at runtime") protected boolean relay=true; @Property(description="Drops views received from below and instead generates global views and passes them up. " + "A global view consists of the local view and the remote view, ordered by view ID. If true, no protocol" + "which requires (local) views can sit on top of RELAY") protected boolean present_global_views=true; /* --------------------------------------------- Fields ------------------------------------------------ */ protected Address local_addr; @ManagedAttribute protected volatile boolean is_coord=false; protected volatile Address coord=null; /** The bridge between the two local clusters, usually based on a TCP config */ protected JChannel bridge; /** The view of the local cluster */ protected View local_view; /** The view of the bridge cluster, usually consists of max 2 nodes */ protected View bridge_view; /** The view of the remote cluster */ protected View remote_view; /** The combined view of local and remote cluster */ protected View global_view; /** To generate new global views */ protected long global_view_id=0; protected TimeScheduler timer; protected Future<?> remote_view_fetcher_future; @ManagedOperation public void setRelay(boolean relay) { this.relay=relay; } @ManagedAttribute public String getLocalView() { return local_view != null? local_view.toString() : "n/a"; } @ManagedAttribute public String getBridgeView() { return bridge_view != null? bridge_view.toString() : "n/a"; } @ManagedAttribute public String getRemoteView() { return remote_view != null? remote_view.toString() : "n/a"; } @ManagedAttribute public String getGlobalView() { return global_view != null? global_view.toString() : "n/a"; } public void init() throws Exception { super.init(); if(site == null || site.length() == 0) throw new IllegalArgumentException("\"site\" must be set"); timer=getTransport().getTimer(); JChannel channel=getProtocolStack().getChannel(); if(channel == null) throw new IllegalStateException("channel must be set"); channel.setAddressGenerator(new AddressGenerator() { public Address generateAddress() { return PayloadUUID.randomUUID(site); } }); } public void stop() { stopRemoteViewFetcher(); Util.close(bridge); } public Object down(Event evt) { switch(evt.getType()) { case Event.MSG: Message msg=(Message)evt.getArg(); Address dest=msg.getDest(); if(dest == null) break; // forward non local destinations to the coordinator, to relay to the remote cluster if(!isLocal(dest)) { forwardToCoord(msg); return null; } break; case Event.VIEW_CHANGE: handleView((View)evt.getArg()); break; case Event.DISCONNECT: Util.close(bridge); break; case Event.SET_LOCAL_ADDRESS: local_addr=(Address)evt.getArg(); break; case Event.GET_PHYSICAL_ADDRESS: // fix to prevent exception by JBossAS, which checks whether a physical // address is present and throw an ex if not // Remove this when the AS code removes that check PhysicalAddress addr=(PhysicalAddress)down_prot.down(evt); if(addr == null) addr=new IpAddress(6666); return addr; } return down_prot.down(evt); } public Object up(Event evt) { switch(evt.getType()) { case Event.MSG: Message msg=(Message)evt.getArg(); Address dest=msg.getDest(); RelayHeader hdr=(RelayHeader)msg.getHeader(getId()); if(hdr != null) { switch(hdr.type) { case DISSEMINATE: Message copy=msg.copy(); if(hdr.original_sender != null) copy.setSrc(hdr.original_sender); return up_prot.up(new Event(Event.MSG, copy)); case FORWARD: if(is_coord) forward(msg.getRawBuffer(), msg.getOffset(), msg.getLength()); break; case VIEW: return installView(msg.getRawBuffer(), msg.getOffset(), msg.getLength()); case BROADCAST_VIEW: // sendViewOnLocalCluster(msg.getSrc(), remote_view, global_view, false); break; default: throw new IllegalArgumentException(hdr.type + " is not a valid type"); } return null; } if(is_coord && relay && dest == null && !msg.isFlagSet(Message.NO_RELAY)) { Message tmp=msg.copy(true, Global.BLOCKS_START_ID); // we only copy headers from building blocks try { byte[] buf=Util.streamableToByteBuffer(tmp); forward(buf, 0, buf.length); } catch(Exception e) { log.warn("failed relaying message", e); } } break; case Event.VIEW_CHANGE: handleView((View)evt.getArg()); // already sends up new view if needed if(present_global_views) return null; else break; } return up_prot.up(evt); } protected void handleView(final View view) { List<Address> new_mbrs=null; if(local_view != null) new_mbrs=Util.newMembers(local_view.getMembers(), view.getMembers()); local_view=view; coord=view.getMembers().iterator().next(); boolean create_bridge=false; boolean is_new_coord=Util.isCoordinator(view, local_addr); if(is_coord) { if(!is_new_coord) { if(log.isTraceEnabled()) log.trace("I'm not coordinator anymore, closing the channel"); Util.close(bridge); is_coord=false; bridge=null; } } else if(is_new_coord) is_coord=create_bridge=true; if(is_coord) { // need to have a local view before JChannel.connect() returns; we don't want to change the viewAccepted() semantics sendViewOnLocalCluster(remote_view, generateGlobalView(view, remote_view, view instanceof MergeView), true, new_mbrs); if(create_bridge) createBridge(); sendViewToRemote(ViewData.create(view, null), false); } } protected Object installView(byte[] buf, int offset, int length) { try { ViewData data=(ViewData)Util.streamableFromByteBuffer(ViewData.class, buf, offset, length); if(data.uuids != null) UUID.add(data.uuids); remote_view=data.remote_view; if(global_view == null || (data.global_view != null &&!global_view.equals(data.global_view))) { global_view=data.global_view; synchronized(this) { if(data.global_view.getVid().getId() > global_view_id) global_view_id=data.global_view.getViewId().getId(); } if(present_global_views) return up_prot.up(new Event(Event.VIEW_CHANGE, global_view)); } } catch(Exception e) { log.error("failed installing view", e); } return null; } /** Forwards the message across the TCP link to the other local cluster */ protected void forward(byte[] buffer, int offset, int length) { Message msg=new Message(null, null, buffer, offset, length); msg.putHeader(id, new RelayHeader(RelayHeader.Type.FORWARD)); if(bridge != null) { try { bridge.send(msg); } catch(Throwable t) { log.error("failed forwarding message over bridge", t); } } } /** Wraps the message annd sends it to the current coordinator */ protected void forwardToCoord(Message msg) { Message tmp=msg.copy(true, Global.BLOCKS_START_ID); // // we only copy headers from building blocks if(tmp.getSrc() == null) tmp.setSrc(local_addr); try { byte[] buf=Util.streamableToByteBuffer(tmp); if(coord != null) { // optimization: if I'm the coord, simply relay to the remote cluster via the bridge if(coord.equals(local_addr)) { forward(buf, 0, buf.length); return; } tmp=new Message(coord, null, buf, 0, buf.length); // reusing tmp is OK here ... tmp.putHeader(id, new RelayHeader(RelayHeader.Type.FORWARD)); down_prot.down(new Event(Event.MSG, tmp)); } } catch(Exception e) { log.error("failed forwarding unicast message to coord", e); } } protected void sendViewToRemote(ViewData view_data, boolean use_seperate_thread) { try { if(bridge != null && bridge.isConnected()) { byte[] buf=Util.streamableToByteBuffer(view_data); final Message msg=new Message(null, null, buf); msg.putHeader(id, RelayHeader.create(RelayHeader.Type.VIEW)); if(use_seperate_thread) { timer.execute(new Runnable() { public void run() { try { bridge.send(msg); } catch(Exception e) { log.error("failed sending view to remote", e); } } }); } else bridge.send(msg); } } catch(Exception e) { log.error("failed sending view to remote", e); } } protected View generateGlobalView(View local_view, View remote_view) { return generateGlobalView(local_view, remote_view, false); } protected View generateGlobalView(View local_view, View remote_view, boolean merge) { List<View> views=new ArrayList<View>(2); if(local_view != null) views.add(local_view); if(remote_view != null) views.add(remote_view); Collections.sort(views, new Comparator<View>() { public int compare(View v1, View v2) { ViewId vid1=v1.getViewId(), vid2=v2.getViewId(); Address creator1=vid1.getCreator(), creator2=vid2.getCreator(); int rc=creator1.compareTo(creator2); if(rc != 0) return rc; long id1=vid1.getId(), id2=vid2.getId(); return id1 > id2 ? 1 : id1 < id2? -1 : 0; } }); List<Address> combined_members=new ArrayList<Address>(); for(View view: views) combined_members.addAll(view.getMembers()); long new_view_id; synchronized(this) { new_view_id=global_view_id++; } Address view_creator=combined_members.isEmpty()? local_addr : combined_members.get(0); if(merge) return new MergeView(view_creator, new_view_id, combined_members, views); else return new View(view_creator, new_view_id, combined_members); } protected void createBridge() { try { if(log.isTraceEnabled()) log.trace("I'm the coordinator, creating a channel (props=" + bridge_props + ", cluster_name=" + bridge_name + ")"); bridge=new JChannel(bridge_props); bridge.setDiscardOwnMessages(true); // don't receive my own messages bridge.setReceiver(new Receiver()); bridge.connect(bridge_name); } catch(Exception e) { log.error("failed creating bridge channel (props=" + bridge_props + ")", e); } } protected void sendOnLocalCluster(byte[] buf, int offset, int length) { try { Message msg=(Message)Util.streamableFromByteBuffer(Message.class, buf, offset, length); Address sender=msg.getSrc(); Address dest=msg.getDest(); if(!isLocal(dest)) { if(log.isWarnEnabled()) log.warn("[" + local_addr + "] dest=" + dest + " is not local (site=" + this.site + "); discarding it"); return; } // set myself to be the sender msg.setSrc(local_addr); // later, in RELAY, we'll take the original sender from the header and make it the sender msg.putHeader(id, RelayHeader.createDisseminateHeader(sender)); if(log.isTraceEnabled()) log.trace("received msg from " + sender + ", passing down the stack with dest=" + msg.getDest() + " and src=" + msg.getSrc()); down_prot.down(new Event(Event.MSG, msg)); } catch(Exception e) { log.error("failed sending on local cluster", e); } } protected void sendViewOnLocalCluster(View remote_view, View global_view, boolean use_seperate_thread, List<Address> new_mbrs) { sendViewOnLocalCluster(ViewData.create(remote_view, global_view), use_seperate_thread, new_mbrs); } protected void sendViewOnLocalCluster(ViewData data, boolean use_seperate_thread, final List<Address> new_mbrs) { try { final byte[] buffer=Util.streamableToByteBuffer(data); final List<Address> destinations=new ArrayList<Address>(); destinations.add(null); // send to all if(new_mbrs != null) destinations.addAll(new_mbrs); if(use_seperate_thread) { timer.execute(new Runnable() { public void run() { sendViewOnLocalCluster(destinations, buffer); } }); } else sendViewOnLocalCluster(destinations, buffer); } catch(Exception e) { log.error("failed sending view to local cluster", e); } } protected void sendViewOnLocalCluster(final List<Address> destinations, final byte[] buffer) { for(Address dest: destinations) { Message view_msg=new Message(dest, null, buffer); RelayHeader hdr=RelayHeader.create(RelayHeader.Type.VIEW); view_msg.putHeader(id, hdr); down_prot.down(new Event(Event.MSG, view_msg)); } } /** Does the payload match the 'site' ID. Checks only unicast destinations (multicast destinations return true) */ protected boolean isLocal(Address dest) { if(dest instanceof PayloadUUID) { String tmp=((PayloadUUID)dest).getPayload(); return tmp != null && tmp.equals(this.site); } else if(dest instanceof TopologyUUID) { String tmp=((TopologyUUID)dest).getSiteId(); return tmp != null && tmp.equals(this.site); } return true; } protected synchronized void startRemoteViewFetcher() { if(remote_view_fetcher_future == null || remote_view_fetcher_future.isDone()) { remote_view_fetcher_future=timer.scheduleWithFixedDelay(new RemoteViewFetcher(), 500, 2000, TimeUnit.MILLISECONDS); } } protected synchronized void stopRemoteViewFetcher() { if(remote_view_fetcher_future != null) { remote_view_fetcher_future.cancel(false); remote_view_fetcher_future=null; } } protected class Receiver extends ReceiverAdapter { public void receive(Message msg) { Address sender=msg.getSrc(); if(bridge.getAddress().equals(sender)) // discard my own messages return; RelayHeader hdr=(RelayHeader)msg.getHeader(id); switch(hdr.type) { case DISSEMINATE: // should not occur here, but we'll ignore it anyway break; case FORWARD: sendOnLocalCluster(msg.getRawBuffer(), msg.getOffset(), msg.getLength()); break; case VIEW: try { ViewData data=(ViewData)Util.streamableFromByteBuffer(ViewData.class, msg.getRawBuffer(), msg.getOffset(), msg.getLength()); // replace addrs with proxies if(data.remote_view != null) { List<Address> mbrs=new LinkedList<Address>(); for(Address mbr: data.remote_view.getMembers()) { mbrs.add(mbr); } data.remote_view=new View(data.remote_view.getViewId(), mbrs); } boolean merge=remote_view == null; stopRemoteViewFetcher(); data.global_view=generateGlobalView(local_view, data.remote_view, merge); sendViewOnLocalCluster(data, false, null); } catch(Exception e) { log.error("failed unmarshalling view from remote cluster", e); } break; case BROADCAST_VIEW: // no-op // our local view is seen as the remote view on the other side ! sendViewToRemote(ViewData.create(local_view, null), true); break; default: throw new IllegalArgumentException(hdr.type + " is not a valid type"); } } public void viewAccepted(View view) { if(bridge_view != null && bridge_view.getViewId().equals(view.getViewId())) return; int prev_members=bridge_view != null? bridge_view.size() : 0; bridge_view=view; switch(view.size()) { case 1: // the remote cluster disappeared, remove all of its addresses from the view if(prev_members > 1 && view.getMembers().iterator().next().equals(bridge.getAddress())) { remote_view=null; View new_global_view=generateGlobalView(local_view, null); sendViewOnLocalCluster(null, new_global_view, false, null); } break; case 2: startRemoteViewFetcher(); break; default: // System.err.println("View size of > 2 is invalid: view=" + view); break; } } } protected class RemoteViewFetcher implements Runnable { public void run() { if(bridge == null || !bridge.isConnected() || remote_view != null) return; Message msg=new Message(); msg.putHeader(id, RelayHeader.create(RELAY.RelayHeader.Type.BROADCAST_VIEW)); try { bridge.send(msg); } catch(Exception e) { } } } public static class RelayHeader extends Header { public static enum Type {DISSEMINATE, FORWARD, VIEW, BROADCAST_VIEW}; protected Type type; protected Address original_sender; // with DISSEMINATE public RelayHeader() { } private RelayHeader(Type type) { this.type=type; } public static RelayHeader create(Type type) { return new RelayHeader(type); } public static RelayHeader createDisseminateHeader(Address original_sender) { RelayHeader retval=new RelayHeader(Type.DISSEMINATE); retval.original_sender=original_sender; return retval; } public int size() { int retval=Global.BYTE_SIZE; // type switch(type) { case DISSEMINATE: retval+=Util.size(original_sender); break; case FORWARD: case VIEW: case BROADCAST_VIEW: break; } return retval; } public void writeTo(DataOutput out) throws Exception { out.writeByte(type.ordinal()); switch(type) { case DISSEMINATE: Util.writeAddress(original_sender, out); break; case FORWARD: case VIEW: case BROADCAST_VIEW: break; } } public void readFrom(DataInput in) throws Exception { type=Type.values()[in.readByte()]; switch(type) { case DISSEMINATE: original_sender=Util.readAddress(in); break; case FORWARD: case VIEW: case BROADCAST_VIEW: break; } } public String toString() { StringBuilder sb=new StringBuilder(type.toString()); switch(type) { case DISSEMINATE: sb.append(" (original sender=" + original_sender + ")"); break; case FORWARD: case VIEW: case BROADCAST_VIEW: break; } return sb.toString(); } } /** Contains local and remote views, and UUID information */ protected static class ViewData implements Streamable { protected View remote_view; protected View global_view; protected Map<Address,String> uuids; public ViewData() { } private ViewData(View remote_view, View global_view, Map<Address,String> uuids) { this.remote_view=remote_view; this.global_view=global_view; this.uuids=uuids; } public static ViewData create(View remote_view, View global_view) { Map<Address,String> tmp=UUID.getContents(); View rv=remote_view != null? remote_view.copy() : null; View gv=global_view != null? global_view.copy() : null; return new ViewData(rv, gv, tmp); } public void writeTo(DataOutput out) throws Exception { Util.writeView(remote_view, out); Util.writeView(global_view, out); out.writeInt(uuids.size()); for(Map.Entry<Address,String> entry: uuids.entrySet()) { Util.writeAddress(entry.getKey(), out); out.writeUTF(entry.getValue()); } } public void readFrom(DataInput in) throws Exception { remote_view=Util.readView(in); global_view=Util.readView(in); int size=in.readInt(); uuids=new HashMap<Address,String>(); for(int i=0; i < size; i++) { Address addr=Util.readAddress(in); String name=in.readUTF(); uuids.put(addr, name); } } public String toString() { StringBuilder sb=new StringBuilder(); sb.append("global_view: " + global_view).append(", remote_view: ").append(remote_view); return sb.toString(); } } }