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); } } }