package er.extensions.remoteSynchronizer;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOApplication;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSMutableDictionary;
import er.extensions.eof.ERXDatabase;
import er.extensions.eof.ERXDatabase.CacheChange;
import er.extensions.eof.ERXObjectStoreCoordinatorSynchronizer.IChangeListener;
import er.extensions.eof.ERXObjectStoreCoordinatorSynchronizer.RemoteChange;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
/**
* This is a simple implementation of a remote synchronizer. It does a multicast
* notification to other members of the group. You should probably be looking
* at ERJGroupsSynchronizer framework instead, though, which is a much more
* robust implementation on top of the JGroups framework. This one will get
* you by in a pinch, but multicast communication is an unreliable protocol
* by definition, and this implementation provides no facilities to recover from
* that unreliability.
*
* @property er.extensions.ERXObjectStoreCoordinatorPool.maxCoordinators you should set this property to at least "1" to trigger ERXObjectStoreCoordinatorSynchronizer to turn on
* @property er.extensions.remoteSynchronizer.enabled if true, remote synchronization is enabled
* @property er.extensions.remoteSynchronizer "er.extensions.ERXSimpleMulticastSynchronizer" for this implementation
* @property er.extensions.multicastSynchronizer.localBindAddress the local address to bind to
* @property er.extensions.multicastSynchronizer.group the multicast address to send to
* @property er.extensions.multicastSynchronizer.port the multicast port to send to
* @property er.extensions.multicastSynchronizer.whitelist the comma-separated list of addresses that can send to this group
* @property er.extensions.multicastSynchronizer.maxPacketSize the maximum multicast packet size
* @property er.extensions.multicastSynchronizer.identifier the unique identifier for this host (autogenerated by default)
* @property er.extensions.remoteSynchronizer.includeEntities the list of entities to synchronize (all by default)
* @property er.extensions.remoteSynchronizer.excludeEntities the list of entities to NOT synchronize (none by default)
*
* @author mschrag
*/
public class ERXSimpleMulticastSynchronizer extends ERXRemoteSynchronizer {
private static final Logger log = LoggerFactory.getLogger(ERXRemoteSynchronizer.class);
public static final int IDENTIFIER_LENGTH = 6;
private static final int JOIN = 1;
private static final int LEAVE = 2;
private byte[] _identifier;
private InetAddress _localBindAddress;
private NetworkInterface _localNetworkInterface;
private InetSocketAddress _multicastGroup;
private int _multicastPort;
private MulticastSocket _multicastSocket;
private boolean _listening;
private int _maxSendPacketSize;
private int _maxReceivePacketSize;
private NSArray<String> _whitelist;
private NSMutableDictionary<String, RemoteChange> _incomingCacheChanges;
public ERXSimpleMulticastSynchronizer(IChangeListener listener) throws IOException {
super(listener);
_incomingCacheChanges = new NSMutableDictionary<>();
String localBindAddressStr = ERXProperties.stringForKey("er.extensions.multicastSynchronizer.localBindAddress");
if (localBindAddressStr == null) {
_localBindAddress = WOApplication.application().hostAddress();
}
else {
_localBindAddress = InetAddress.getByName(localBindAddressStr);
}
String multicastGroup = ERXProperties.stringForKeyWithDefault("er.extensions.multicastSynchronizer.group", "230.0.0.1");
_multicastPort = ERXProperties.intForKeyWithDefault("er.extensions.multicastSynchronizer.port", 9753);
String whitelist = ERXProperties.stringForKey("er.extensions.multicastSynchronizer.whitelist");
if (whitelist != null) {
_whitelist = NSArray.componentsSeparatedByString(whitelist, ",");
}
int maxPacketSize = ERXProperties.intForKeyWithDefault("er.extensions.multicastSynchronizer.maxPacketSize", 1024);
_maxSendPacketSize = maxPacketSize;
_maxReceivePacketSize = 2 * maxPacketSize;
String multicastIdentifierStr = ERXProperties.stringForKey("er.extensions.multicastSynchronizer.identifier");
if (multicastIdentifierStr == null) {
_identifier = new byte[ERXSimpleMulticastSynchronizer.IDENTIFIER_LENGTH];
byte[] hostAddressBytes = _localBindAddress.getAddress();
System.arraycopy(hostAddressBytes, 0, _identifier, 0, hostAddressBytes.length);
int multicastInstance = WOApplication.application().port().shortValue();
_identifier[4] = (byte) (multicastInstance & 0xff);
_identifier[5] = (byte) ((multicastInstance >>> 8) & 0xff);
}
else {
_identifier = ERXStringUtilities.hexStringToByteArray(multicastIdentifierStr);
}
_localNetworkInterface = NetworkInterface.getByInetAddress(_localBindAddress);
_multicastGroup = new InetSocketAddress(InetAddress.getByName(multicastGroup), _multicastPort);
_multicastSocket = new MulticastSocket(null);
_multicastSocket.setInterface(_localBindAddress);
_multicastSocket.setTimeToLive(4);
_multicastSocket.setReuseAddress(true);
_multicastSocket.bind(new InetSocketAddress(_multicastPort));
}
@Override
public void join() throws IOException {
if (log.isInfoEnabled()) {
log.info("Multicast instance {} joining.", ERXStringUtilities.byteArrayToHexString(_identifier));
}
_multicastSocket.joinGroup(_multicastGroup, _localNetworkInterface);
try (MulticastByteArrayOutputStream baos = new MulticastByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) {
dos.write(_identifier);
dos.writeInt(0);
dos.writeShort(0);
dos.writeShort(0);
dos.writeByte(ERXSimpleMulticastSynchronizer.JOIN);
_multicastSocket.send(baos.createDatagramPacket());
}
}
@Override
public void leave() throws IOException {
if (log.isInfoEnabled()) {
log.info("Multicast instance {} leaving.", ERXStringUtilities.byteArrayToHexString(_identifier));
}
try (MulticastByteArrayOutputStream baos = new MulticastByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) {
dos.write(_identifier);
dos.writeInt(0);
dos.writeShort(0);
dos.writeShort(0);
dos.writeByte(ERXSimpleMulticastSynchronizer.LEAVE);
_multicastSocket.send(baos.createDatagramPacket());
}
_multicastSocket.leaveGroup(_multicastGroup, _localNetworkInterface);
_listening = false;
}
@Override
protected boolean handleMessageType(int messageType, RemoteChange remoteChange, DataInputStream dis) {
boolean handled = false;
if (messageType == ERXSimpleMulticastSynchronizer.JOIN) {
handled = true;
}
else if (messageType == ERXSimpleMulticastSynchronizer.LEAVE) {
handled = true;
}
return handled;
}
@Override
public void listen() throws IOException {
Thread listenThread = new Thread(new Runnable() {
public void run() {
_listening = true;
byte[] buffer = new byte[_maxReceivePacketSize];
while (_listening) {
DatagramPacket receivePacket = new DatagramPacket(buffer, 0, buffer.length);
try {
_multicastSocket.receive(receivePacket);
ByteArrayInputStream bais = new ByteArrayInputStream(receivePacket.getData(), 0, receivePacket.getLength());
DataInputStream dis = new DataInputStream(bais);
boolean processPacket = true;
if (_whitelist != null) {
InetAddress remoteAddress = receivePacket.getAddress();
String remoteHostAddress = remoteAddress.getHostAddress();
processPacket = _whitelist.containsObject(remoteHostAddress);
}
byte[] identifier = new byte[ERXSimpleMulticastSynchronizer.IDENTIFIER_LENGTH];
dis.readFully(identifier);
if (processPacket && !Arrays.equals(identifier, _identifier)) {
int transactionID = dis.readInt();
short transactionNum = dis.readShort();
short transactionSize = dis.readShort();
String identifierHex = ERXStringUtilities.byteArrayToHexString(identifier);
String transactionIdentifierStr = identifierHex + "-" + transactionID;
RemoteChange remoteChange = _incomingCacheChanges.objectForKey(transactionIdentifierStr);
if (remoteChange == null) {
remoteChange = new RemoteChange(identifierHex, transactionID, transactionSize);
_incomingCacheChanges.setObjectForKey(remoteChange, transactionIdentifierStr);
}
_readCacheChange(remoteChange, dis);
if (remoteChange.isComplete()) {
_incomingCacheChanges.removeObjectForKey(transactionIdentifierStr);
addChange(remoteChange);
}
// TODO: Sweep the _cacheChanges dictionary for expired partial cache updates. If a
// machine
// crashes in the middle of a broadcast, it would leave half-open cache updates in all
// of the
// multicast member _cacheChanges dictionaries.
}
}
catch (Throwable t) {
log.error("Failed to read multicast notification.", t);
}
}
}
});
listenThread.setName("ERXSimpleMultiCastListener");
listenThread.setDaemon(true);
listenThread.start();
}
@Override
protected void _writeCacheChanges(int transactionID, NSArray<ERXDatabase.CacheChange> cacheChanges) throws IOException {
short transactionSize = (short) cacheChanges.count();
short transactionNum = 0;
for (Iterator<ERXDatabase.CacheChange> iter = cacheChanges.iterator(); iter.hasNext(); transactionNum++) {
ERXDatabase.CacheChange cacheChange = iter.next();
writeCacheChange(cacheChange, transactionID, transactionNum, transactionSize);
}
}
public void writeCacheChange(CacheChange cacheChange, int transactionID, short transactionNum, short transactionSize) throws IOException {
// System.out.println("MulticastSynchronizer.writeCacheChange: Writing " + transactionID + ", " +
// transactionNum + " of " + transactionSize);
try (MulticastByteArrayOutputStream baos = new MulticastByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) {
dos.write(_identifier);
dos.writeInt(transactionID);
dos.writeShort(transactionNum);
dos.writeShort(transactionSize);
_writeCacheChange(dos, cacheChange);
_multicastSocket.send(baos.createDatagramPacket());
}
if (log.isDebugEnabled()) {
log.debug("Multicast instance {}: Writing {}", ERXStringUtilities.byteArrayToHexString(_identifier), cacheChange);
}
}
protected class MulticastByteArrayOutputStream extends RefByteArrayOutputStream {
public DatagramPacket createDatagramPacket() throws SocketException {
return new DatagramPacket(buf, 0, count, _multicastGroup);
}
}
}