package er.jgroups; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.URL; import java.util.Enumeration; import org.jgroups.JChannel; import org.jgroups.Message; import org.jgroups.ReceiverAdapter; import org.jgroups.View; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSSelector; import er.extensions.appserver.ERXShutdownHook; import er.extensions.eof.ERXDatabase; import er.extensions.eof.ERXObjectStoreCoordinatorSynchronizer.IChangeListener; import er.extensions.eof.ERXObjectStoreCoordinatorSynchronizer.RemoteChange; import er.extensions.foundation.ERXProperties; import er.extensions.remoteSynchronizer.ERXRemoteSynchronizer; /** * A multicast synchronizer built on top of the JGroups library. This is a much * more robust implementation than the default synchronizer used by ERXObjectStoreCoordinatorSynchronizer. * * @property er.extensions.ERXObjectStoreCoordinatorPool.maxCoordinators you should set this property to at least "1" to trigger * ERXObjectStoreCoordinatorSynchronizer to turn on * @property er.extensions.jgroupsSynchronizer.applicationWillTerminateNotificationName the name of the NSNotification that is sent when * the application is terminating. Leave blank to disable this feature. * @property er.extensions.jgroupsSynchronizer.autoReconnect whether to auto reconnect when shunned (defaults to false) * @property er.extensions.jgroupsSynchronizer.groupName the JGroups group name to use (defaults to WOApplication.application.name) * @property er.extensions.jgroupsSynchronizer.localBindAddress * @property er.extensions.jgroupsSynchronizer.multicastAddress the multicast address to use (defaults to 230.0.0.1, and only necessary if you are using multicast) * @property er.extensions.jgroupsSynchronizer.multicastPort the multicast port to use (defaults to 9753, and only necessary if you are using multicast) * @property er.extensions.jgroupsSynchronizer.properties an XML JGroups configuration file (defaults to jgroups-default.xml in this framework) * @property er.extensions.jgroupsSynchronizer.useShutdownHook whether to register a JVM shutdown hook to clean up the JChannel (defaults to true) * @property er.extensions.remoteSynchronizer "er.jgroups.ERJGroupsSynchronizer" for this implementation * @property er.extensions.remoteSynchronizer.enabled if true, remote synchronization is enabled * @property er.extensions.remoteSynchronizer.excludeEntities the list of entities to NOT synchronize (none by default) * @property er.extensions.remoteSynchronizer.includeEntities the list of entities to synchronize (all by default) * * @author mschrag */ public class ERJGroupsSynchronizer extends ERXRemoteSynchronizer { private static final Logger log = LoggerFactory.getLogger(ERXRemoteSynchronizer.class); private String _groupName; private JChannel _channel; public ERJGroupsSynchronizer(IChangeListener listener) throws Exception { super(listener); String jgroupsPropertiesFile = ERXProperties.stringForKey("er.extensions.jgroupsSynchronizer.properties"); String jgroupsPropertiesFramework = null; if (jgroupsPropertiesFile == null) { jgroupsPropertiesFile = "jgroups-default.xml"; jgroupsPropertiesFramework = "ERJGroupsSynchronizer"; } _groupName = ERXProperties.stringForKeyWithDefault("er.extensions.jgroupsSynchronizer.groupName", WOApplication.application().name()); String localBindAddressStr = ERXProperties.stringForKey("er.extensions.jgroupsSynchronizer.localBindAddress"); if (localBindAddressStr == null) { System.setProperty("bind.address", WOApplication.application().hostAddress().getHostAddress()); } else { System.out.println("localBindAddressStr = " + localBindAddressStr); System.setProperty("bind.address", localBindAddressStr); } URL propertiesUrl = WOApplication.application().resourceManager().pathURLForResourceNamed(jgroupsPropertiesFile, jgroupsPropertiesFramework, null); _channel = new JChannel(propertiesUrl); _channel.setDiscardOwnMessages(Boolean.FALSE); _registerForCleanup(); } @Override public void join() throws Exception { _channel.connect(_groupName); } @Override public void leave() { _channel.disconnect(); } @Override public void listen() { _channel.setReceiver(new ReceiverAdapter() { public void receive(Message message) { try { byte[] buffer = message.getBuffer(); ByteArrayInputStream bais = new ByteArrayInputStream(buffer); DataInputStream dis = new DataInputStream(bais); int transactionCount = dis.readInt(); RemoteChange remoteChange = new RemoteChange("AnotherInstance", -1, transactionCount); for (int transactionNum = 0; transactionNum < transactionCount; transactionNum++) { _readCacheChange(remoteChange, dis); } addChange(remoteChange); log.info("Received {} changes from {}", transactionCount, message.getSrc()); if (log.isDebugEnabled()) { log.debug(" Changes = {}", remoteChange.remoteCacheChanges()); } } catch (IOException e) { log.error("Failed to apply remote changes. This is bad.", e); } } public void viewAccepted(View view) { // System.out.println(".viewAccepted: " + view); } }); } @Override protected void _writeCacheChanges(int transactionID, NSArray cacheChanges) throws Exception, IOException { if (!_channel.isConnected()) { log.info("Channel not connected: Not Sending {} changes.", cacheChanges.count()); log.debug("Channel not connected: Changes = {}", cacheChanges); return; } if (cacheChanges.count() == 0) { log.info("No changes to send!"); return; } RefByteArrayOutputStream baos = new RefByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); dos.writeInt(cacheChanges.count()); for (Enumeration cacheChangesEnum = cacheChanges.objectEnumerator(); cacheChangesEnum.hasMoreElements();) { ERXDatabase.CacheChange cacheChange = (ERXDatabase.CacheChange) cacheChangesEnum.nextElement(); _writeCacheChange(dos, cacheChange); } dos.flush(); dos.close(); log.info("Sending {} changes.", cacheChanges.count()); log.debug(" Changes = {}", cacheChanges); Message message = new Message(null, null, baos.buffer(), 0, baos.size()); _channel.send(message); } private void _registerForCleanup() { String notificationName = ERXProperties.stringForKey("er.extensions.jgroupsSynchronizer.applicationWillTerminateNotificationName"); if (notificationName != null && notificationName.length() > 0) { NSSelector applicationLaunchedNotification = new NSSelector("_applicationWillTerminateNotification", new Class[] { NSNotification.class }); NSNotificationCenter.defaultCenter().addObserver(this, applicationLaunchedNotification, notificationName, null); } if (ERXProperties.booleanForKeyWithDefault("er.extensions.jgroupsSynchronizer.useShutdownHook", true)) { new ERJGroupsCleanupTask(_channel); } } private static void cleanUpJChannel(JChannel channel) { try { if (channel == null || !channel.isOpen()) { return; } if (channel.isConnected()) { channel.disconnect(); } channel.close(); } catch (Throwable e) { log.error("Error closing JChannel: {}", channel, e); } } public void _applicationWillTerminateNotification(NSNotification notification) { cleanUpJChannel(_channel); } private static class ERJGroupsCleanupTask extends ERXShutdownHook { private final JChannel channel; public ERJGroupsCleanupTask(JChannel channel) { super("JGroups Cleanup"); this.channel = channel; } @Override public void hook() { cleanUpJChannel(channel); } } }