From 0706a4ce7cba7bca2d482749fc9367d9bd0fb872 Mon Sep 17 00:00:00 2001 From: Robert Davies Date: Tue, 7 Oct 2008 12:31:59 +0000 Subject: [PATCH] added groups git-svn-id: https://svn.apache.org/repos/asf/activemq/trunk@702452 13f79535-47bb-0310-9956-ffa450edef68 --- activemq-core/pom.xml | 2 + .../perf/AMQStoreDurableTopicTest.java | 4 +- .../activemq/perf/SimpleDurableTopicTest.java | 8 +- .../transport/failover/AMQ1925Test.java | 4 +- .../src/test/resources/log4j.properties | 2 +- activemq-groups/pom.xml | 93 + .../DefaultMapChangedListener.java | 33 + .../java/org/apache/activegroups/Group.java | 1836 +++++++++++++++++ .../activegroups/GroupMessageListener.java | 34 + .../GroupStateChangedListener.java | 50 + .../activegroups/GroupUpdateException.java | 32 + .../java/org/apache/activegroups/Member.java | 163 ++ .../activegroups/MemberChangedListener.java | 37 + .../activegroups/command/AsyncMapRequest.java | 61 + .../activegroups/command/ElectionMessage.java | 106 + .../apache/activegroups/command/EntryKey.java | 210 ++ .../activegroups/command/EntryMessage.java | 204 ++ .../activegroups/command/EntryValue.java | 71 + .../activegroups/command/MapRequest.java | 65 + .../activegroups/command/RequestCallback.java | 30 + .../java/org/apache/activegroups/package.html | 25 + .../test/eclipse-resources/log4j.properties | 37 + .../apache/activegroups/GroupMemberTest.java | 155 ++ .../apache/activegroups/GroupMessageTest.java | 230 +++ .../apache/activegroups/GroupStateTest.java | 548 +++++ 25 files changed, 4031 insertions(+), 9 deletions(-) create mode 100755 activemq-groups/pom.xml create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/DefaultMapChangedListener.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/Group.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/GroupMessageListener.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/GroupStateChangedListener.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/GroupUpdateException.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/Member.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/MemberChangedListener.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/command/AsyncMapRequest.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/command/ElectionMessage.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/command/EntryKey.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/command/EntryMessage.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/command/EntryValue.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/command/MapRequest.java create mode 100644 activemq-groups/src/main/java/org/apache/activegroups/command/RequestCallback.java create mode 100755 activemq-groups/src/main/java/org/apache/activegroups/package.html create mode 100755 activemq-groups/src/test/eclipse-resources/log4j.properties create mode 100644 activemq-groups/src/test/java/org/apache/activegroups/GroupMemberTest.java create mode 100644 activemq-groups/src/test/java/org/apache/activegroups/GroupMessageTest.java create mode 100644 activemq-groups/src/test/java/org/apache/activegroups/GroupStateTest.java diff --git a/activemq-core/pom.xml b/activemq-core/pom.xml index 2c344ff6ef..2321234815 100755 --- a/activemq-core/pom.xml +++ b/activemq-core/pom.xml @@ -449,8 +449,10 @@ **/NetworkConnectionsCleanedupTest.*/** **/amq1490/* + **/AMQ1925/* **/archive/* **/NetworkFailoverTest.*/** + **/AMQDeadlockTest3.* diff --git a/activemq-core/src/test/java/org/apache/activemq/perf/AMQStoreDurableTopicTest.java b/activemq-core/src/test/java/org/apache/activemq/perf/AMQStoreDurableTopicTest.java index 36e7d02bb4..73f9261405 100644 --- a/activemq-core/src/test/java/org/apache/activemq/perf/AMQStoreDurableTopicTest.java +++ b/activemq-core/src/test/java/org/apache/activemq/perf/AMQStoreDurableTopicTest.java @@ -30,8 +30,8 @@ public class AMQStoreDurableTopicTest extends SimpleDurableTopicTest { dataFileDir.mkdirs(); answer.setDeleteAllMessagesOnStartup(true); AMQPersistenceAdapter adaptor = new AMQPersistenceAdapter(); - adaptor.setArchiveDataLogs(true); - adaptor.setMaxFileLength(1024 * 64); + //adaptor.setArchiveDataLogs(true); + //adaptor.setMaxFileLength(1024 * 64); answer.setDataDirectoryFile(dataFileDir); answer.setPersistenceAdapter(adaptor); diff --git a/activemq-core/src/test/java/org/apache/activemq/perf/SimpleDurableTopicTest.java b/activemq-core/src/test/java/org/apache/activemq/perf/SimpleDurableTopicTest.java index 47433e6f24..39f4f6a77d 100644 --- a/activemq-core/src/test/java/org/apache/activemq/perf/SimpleDurableTopicTest.java +++ b/activemq-core/src/test/java/org/apache/activemq/perf/SimpleDurableTopicTest.java @@ -30,8 +30,8 @@ import org.apache.activemq.store.amq.AMQPersistenceAdapterFactory; public class SimpleDurableTopicTest extends SimpleTopicTest { protected void setUp() throws Exception { - numberOfDestinations=1; - numberOfConsumers = 2; + numberOfDestinations=10; + numberOfConsumers = 10; numberofProducers = 2; sampleCount=1000; playloadSize = 1024; @@ -44,7 +44,7 @@ public class SimpleDurableTopicTest extends SimpleTopicTest { persistenceFactory.setPersistentIndex(true); persistenceFactory.setCleanupInterval(10000); answer.setPersistenceFactory(persistenceFactory); - answer.setDeleteAllMessagesOnStartup(true); + //answer.setDeleteAllMessagesOnStartup(true); answer.addConnector(uri); answer.setUseShutdownHook(false); } @@ -63,7 +63,7 @@ public class SimpleDurableTopicTest extends SimpleTopicTest { protected ActiveMQConnectionFactory createConnectionFactory(String uri) throws Exception { ActiveMQConnectionFactory result = super.createConnectionFactory(uri); - result.setSendAcksAsync(false); + //result.setSendAcksAsync(false); return result; } diff --git a/activemq-core/src/test/java/org/apache/activemq/transport/failover/AMQ1925Test.java b/activemq-core/src/test/java/org/apache/activemq/transport/failover/AMQ1925Test.java index 3cd51b93ad..5cc39773b4 100644 --- a/activemq-core/src/test/java/org/apache/activemq/transport/failover/AMQ1925Test.java +++ b/activemq-core/src/test/java/org/apache/activemq/transport/failover/AMQ1925Test.java @@ -60,7 +60,7 @@ public class AMQ1925Test extends TestCase { private URI tcpUri; private ActiveMQConnectionFactory cf; - public void testAMQ1925_TXInProgress() throws Exception { + public void XtestAMQ1925_TXInProgress() throws Exception { Connection connection = cf.createConnection(); connection.start(); Session session = connection.createSession(true, @@ -372,8 +372,8 @@ public class AMQ1925Test extends TestCase { protected void setUp() throws Exception { bs = new BrokerService(); + bs.setDeleteAllMessagesOnStartup(true); bs.setPersistent(true); - bs.deleteAllMessages(); bs.setUseJmx(true); TransportConnector connector = bs.addConnector("tcp://localhost:0"); bs.start(); diff --git a/activemq-core/src/test/resources/log4j.properties b/activemq-core/src/test/resources/log4j.properties index 3f12e671d1..d95fe6f509 100755 --- a/activemq-core/src/test/resources/log4j.properties +++ b/activemq-core/src/test/resources/log4j.properties @@ -18,7 +18,7 @@ # # The logging properties used during tests.. # -log4j.rootLogger=INFO, out +log4j.rootLogger=INFO, out, stdout log4j.logger.org.apache.activemq.spring=WARN diff --git a/activemq-groups/pom.xml b/activemq-groups/pom.xml new file mode 100755 index 0000000000..22c10a37da --- /dev/null +++ b/activemq-groups/pom.xml @@ -0,0 +1,93 @@ + + + + 4.0.0 + + + org.apache.activemq + activemq-parent + 5.3-SNAPSHOT + + + activemq-groups + bundle + ActiveMQ :: Groups + A JMS based collaboration framework + + + * + + + + + org.apache.geronimo.specs + geronimo-jms_1.1_spec + 1.1 + + + org.apache.activemq + activemq-core + 5.2-SNAPSHOT + + + org.apache.xbean + xbean-spring + 3.3 + + + org.springframework + spring + 2.5.4 + + + + commons-logging + commons-logging + 1.1.1 + test + + + log4j + log4j + 1.2.14 + test + + + junit + junit + 3.8.1 + test + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + + diff --git a/activemq-groups/src/main/java/org/apache/activegroups/DefaultMapChangedListener.java b/activemq-groups/src/main/java/org/apache/activegroups/DefaultMapChangedListener.java new file mode 100644 index 0000000000..c0f73c4a52 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/DefaultMapChangedListener.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +/** + * Default implementation of a MapChangedListener + * + */ +public class DefaultMapChangedListener implements GroupStateChangedListener{ + + public void mapInsert(Member owner, Object key, Object value) { + } + + public void mapRemove(Member owner, Object key, Object value,boolean expired) { + } + + public void mapUpdate(Member owner, Object key, Object oldValue,Object newValue) { + } +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/Group.java b/activemq-groups/src/main/java/org/apache/activegroups/Group.java new file mode 100644 index 0000000000..0e87c0feba --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/Group.java @@ -0,0 +1,1836 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.jms.Connection; +import javax.jms.DeliveryMode; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.MessageProducer; +import javax.jms.ObjectMessage; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; +import org.apache.activegroups.command.AsyncMapRequest; +import org.apache.activegroups.command.ElectionMessage; +import org.apache.activegroups.command.EntryKey; +import org.apache.activegroups.command.EntryMessage; +import org.apache.activegroups.command.EntryValue; +import org.apache.activegroups.command.MapRequest; +import org.apache.activemq.ActiveMQMessageConsumer; +import org.apache.activemq.Service; +import org.apache.activemq.advisory.ConsumerEvent; +import org.apache.activemq.advisory.ConsumerEventSource; +import org.apache.activemq.advisory.ConsumerListener; +import org.apache.activemq.thread.SchedulerTimerTask; +import org.apache.activemq.util.IdGenerator; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + *

+ * A Group is a distributed collaboration implementation that is + * used to shared state and process messages amongst a distributed group of + * other Group instances. Membership of a group is handled + * automatically using discovery. + *

+ * The underlying transport is JMS and there are some optimizations that occur + * for membership if used with ActiveMQ - but Group can be used + * with any JMS implementation. + * + *

+ * Updates to the group shared map are controlled by a coordinator. The + * coordinator is elected by the member with the lowest lexicographical id - + * based on the bully algorithm [Silberschatz et al. 1993] + *

+ * The {@link #selectCordinator(Collection members)} method may be + * overridden to implement a custom mechanism for choosing how the coordinator + * is elected for the map. + *

+ * New Group instances have their state updated by the + * coordinator, and coordinator failure is handled automatically within the + * group. + *

+ * All map updates are totally ordered through the coordinator, whilst read + * operations happen locally. + *

+ * A Group supports the concept of owner only updates(write + * locks), shared updates, entry expiration times and removal on owner exit - + * all of which are optional. In addition, you can grab and release locks for + * values in the map, independently of who created them. + *

+ * In addition, members of a group can broadcast messages and implement + * request/response with other Group instances. + * + *

+ * + * @param + * the key type + * @param + * the value type + * + */ +public class Group implements Map, Service { + /** + * default interval within which to detect a member failure + */ + public static final long DEFAULT_HEART_BEAT_INTERVAL = 1000; + private static final long EXPIRATION_SWEEP_INTERVAL = 500; + private static final Log LOG = LogFactory.getLog(Group.class); + private static final String STATE_PREFIX = "STATE." + Group.class.getName() + + "."; + private static final String GROUP_MESSAGE_PREFIX = "MESSAGE." + + Group.class.getName() + "."; + private static final String STATE_TYPE = "state"; + private static final String MESSAGE_TYPE = "message"; + private static final String MEMBER_ID_PROPERTY = "memberId"; + protected Member local; + private final Object mapMutex = new Object(); + private Map> localMap; + Map members = new ConcurrentHashMap(); + private Map stateRequests = new HashMap(); + private Map messageRequests = new HashMap(); + private List membershipListeners = new CopyOnWriteArrayList(); + private List mapChangedListeners = new CopyOnWriteArrayList(); + private List groupMessageListeners = new CopyOnWriteArrayList(); + private Member coordinator; + private String groupName; + private boolean alwaysLock; + private Connection connection; + private Session stateSession; + private Session messageSession; + private Topic stateTopic; + private Topic heartBeatTopic; + private Topic inboxTopic; + private Topic messageTopic; + private Queue messageQueue; + private MessageProducer stateProducer; + private MessageProducer messageProducer; + private ConsumerEventSource consumerEvents; + private AtomicBoolean started = new AtomicBoolean(); + private SchedulerTimerTask heartBeatTask; + private SchedulerTimerTask checkMembershipTask; + private SchedulerTimerTask expirationTask; + private Timer timer; + private long heartBeatInterval = DEFAULT_HEART_BEAT_INTERVAL; + private IdGenerator idGenerator = new IdGenerator(); + private boolean removeOwnedObjectsOnExit; + private boolean releaseLockOnExit = true; + private int timeToLive; + private int lockTimeToLive; + private int minimumGroupSize = 1; + private int coordinatorWeight = 0; + private final AtomicBoolean electionFinished = new AtomicBoolean(true); + private ExecutorService stateExecutor; + private ExecutorService messageExecutor; + private ThreadPoolExecutor electionExecutor; + private final Object memberMutex = new Object(); + + /** + * @param connection + * @param name + */ + public Group(Connection connection, String name) { + this(connection, "default", name); + } + + /** + * @param connection + * @param groupName + * @param name + */ + public Group(Connection connection, String groupName, String name) { + this.connection = connection; + this.local = new Member(name); + this.coordinator = this.local; + this.groupName = groupName; + } + + /** + * Set the local map implementation to be used By default its a HashMap - + * but you could use a Cache for example + * + * @param map + */ + public void setLocalMap(Map map) { + synchronized (this.mapMutex) { + this.localMap = map; + } + } + + /** + * Start membership to the group + * + * @throws Exception + * + */ + public void start() throws Exception { + if (this.started.compareAndSet(false, true)) { + synchronized (this.mapMutex) { + if (this.localMap == null) { + this.localMap = new HashMap>(); + } + } + this.connection.start(); + this.stateSession = this.connection.createSession(false, + Session.AUTO_ACKNOWLEDGE); + this.messageSession = this.connection.createSession(false, + Session.AUTO_ACKNOWLEDGE); + this.stateProducer = this.stateSession.createProducer(null); + this.stateProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); + this.inboxTopic = this.stateSession.createTemporaryTopic(); + String stateTopicName = STATE_PREFIX + this.groupName; + this.stateTopic = this.stateSession.createTopic(stateTopicName); + this.heartBeatTopic = this.stateSession.createTopic(stateTopicName + + ".heartbeat"); + String messageDestinationName = GROUP_MESSAGE_PREFIX + + this.groupName; + this.messageTopic = this.messageSession + .createTopic(messageDestinationName); + this.messageQueue = this.messageSession + .createQueue(messageDestinationName); + MessageConsumer privateInbox = this.messageSession + .createConsumer(this.inboxTopic); + MessageConsumer memberChangeConsumer = this.stateSession + .createConsumer(this.stateTopic); + String memberId = null; + if (memberChangeConsumer instanceof ActiveMQMessageConsumer) { + memberId = ((ActiveMQMessageConsumer) memberChangeConsumer) + .getConsumerId().toString(); + } else { + memberId = this.idGenerator.generateId(); + } + this.local.setId(memberId); + this.local.setInBoxDestination(this.inboxTopic); + this.local.setCoordinatorWeight(getCoordinatorWeight()); + privateInbox.setMessageListener(new MessageListener() { + public void onMessage(Message message) { + processJMSMessage(message); + } + }); + memberChangeConsumer.setMessageListener(new MessageListener() { + public void onMessage(Message message) { + processJMSMessage(message); + } + }); + this.messageProducer = this.messageSession.createProducer(null); + this.messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); + MessageConsumer topicMessageConsumer = this.messageSession + .createConsumer(this.messageTopic); + topicMessageConsumer.setMessageListener(new MessageListener() { + public void onMessage(Message message) { + processJMSMessage(message); + } + }); + MessageConsumer queueMessageConsumer = this.messageSession + .createConsumer(this.messageQueue); + queueMessageConsumer.setMessageListener(new MessageListener() { + public void onMessage(Message message) { + processJMSMessage(message); + } + }); + MessageConsumer heartBeatConsumer = this.stateSession + .createConsumer(this.heartBeatTopic); + heartBeatConsumer.setMessageListener(new MessageListener() { + public void onMessage(Message message) { + handleHeartbeats(message); + } + }); + this.consumerEvents = new ConsumerEventSource(this.connection, + this.stateTopic); + this.consumerEvents.setConsumerListener(new ConsumerListener() { + public void onConsumerEvent(ConsumerEvent event) { + handleConsumerEvents(event); + } + }); + this.consumerEvents.start(); + this.electionExecutor = new ThreadPoolExecutor(1, 1, 0L, + TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), + new ThreadFactory() { + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "Election{" + + Group.this.local + "}"); + thread.setDaemon(true); + return thread; + } + }); + this.stateExecutor = Executors + .newSingleThreadExecutor(new ThreadFactory() { + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "Group State{" + + Group.this.local + "}"); + thread.setDaemon(true); + return thread; + } + }); + this.messageExecutor = Executors + .newSingleThreadExecutor(new ThreadFactory() { + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, + "Group Messages{" + Group.this.local + "}"); + thread.setDaemon(true); + return thread; + } + }); + sendHeartBeat(); + this.heartBeatTask = new SchedulerTimerTask(new Runnable() { + public void run() { + sendHeartBeat(); + } + }); + this.checkMembershipTask = new SchedulerTimerTask(new Runnable() { + public void run() { + checkMembership(); + } + }); + this.expirationTask = new SchedulerTimerTask(new Runnable() { + public void run() { + expirationSweep(); + } + }); + this.timer = new Timer("Distributed heart beat", true); + this.timer.scheduleAtFixedRate(this.heartBeatTask, + getHeartBeatInterval() / 3, getHeartBeatInterval() / 2); + this.timer.scheduleAtFixedRate(this.checkMembershipTask, + getHeartBeatInterval(), getHeartBeatInterval()); + this.timer.scheduleAtFixedRate(this.expirationTask, + EXPIRATION_SWEEP_INTERVAL, EXPIRATION_SWEEP_INTERVAL); + // await for members to join + long timeout = (long) (this.heartBeatInterval + * this.minimumGroupSize *1.5); + long deadline = System.currentTimeMillis() + timeout; + while ((this.members.size() < this.minimumGroupSize || !this.electionFinished + .get()) + && timeout > 0) { + synchronized (this.electionFinished) { + this.electionFinished.wait(timeout); + } + timeout = Math.max(deadline - System.currentTimeMillis(), 0); + } + } + } + + /** + * stop membership to the group + * + * @throws Exception + */ + public void stop() { + if (this.started.compareAndSet(true, false)) { + this.expirationTask.cancel(); + this.checkMembershipTask.cancel(); + this.heartBeatTask.cancel(); + this.expirationTask.cancel(); + this.timer.purge(); + if (this.electionExecutor != null) { + this.electionExecutor.shutdownNow(); + } + if (this.stateExecutor != null) { + this.stateExecutor.shutdownNow(); + } + if (this.messageExecutor != null) { + this.messageExecutor.shutdownNow(); + } + try { + this.consumerEvents.stop(); + this.stateSession.close(); + this.messageSession.close(); + this.connection.close(); + } catch (Exception e) { + LOG.debug("Caught exception stopping", e); + } + + } + } + + /** + * @return true if started + */ + public boolean isStarted() { + return this.started.get(); + } + + /** + * @return true if there is elections have finished + */ + public boolean isElectionFinished() { + return this.electionFinished.get(); + } + + void setElectionFinished(boolean flag) { + this.electionFinished.set(flag); + } + + /** + * @return the partitionName + */ + public String getGroupName() { + return this.groupName; + } + + /** + * @return the name ofthis map + */ + public String getName() { + return this.local.getName(); + } + + /** + * @return true if by default always lock objects (default is false) + */ + public boolean isAlwaysLock() { + return this.alwaysLock; + } + + /** + * @param alwaysLock - + * set true if objects inserted will always be locked (default is + * false) + */ + public void setAlwaysLock(boolean alwaysLock) { + this.alwaysLock = alwaysLock; + } + + /** + * @return the heartBeatInterval + */ + public long getHeartBeatInterval() { + return this.heartBeatInterval; + } + + /** + * @param heartBeatInterval + * the heartBeatInterval to set + */ + public void setHeartBeatInterval(long heartBeatInterval) { + this.heartBeatInterval = heartBeatInterval; + } + + /** + * Add a listener for membership changes + * + * @param l + */ + public void addMemberChangedListener(MemberChangedListener l) { + this.membershipListeners.add(l); + } + + /** + * Remove a listener for membership changes + * + * @param l + */ + public void removeMemberChangedListener(MemberChangedListener l) { + this.membershipListeners.remove(l); + } + + /** + * Add a listener for map changes + * + * @param l + */ + public void addMapChangedListener(GroupStateChangedListener l) { + this.mapChangedListeners.add(l); + } + + /** + * Remove a listener for map changes + * + * @param l + */ + public void removeMapChangedListener(GroupStateChangedListener l) { + this.mapChangedListeners.remove(l); + } + + /** + * Add a listener for group messages + * + * @param l + */ + public void addGroupMessageListener(GroupMessageListener l) { + this.groupMessageListeners.add(l); + } + + /** + * remove a listener for group messages + * + * @param l + */ + public void removeGroupMessageListener(GroupMessageListener l) { + this.groupMessageListeners.remove(l); + } + + /** + * @return the timeToLive + */ + public int getTimeToLive() { + return this.timeToLive; + } + + /** + * @param timeToLive + * the timeToLive to set + */ + public void setTimeToLive(int timeToLive) { + this.timeToLive = timeToLive; + } + + /** + * @return the removeOwnedObjectsOnExit + */ + public boolean isRemoveOwnedObjectsOnExit() { + return this.removeOwnedObjectsOnExit; + } + + /** + * Sets the policy for owned objects in the group If set to true, when this + * GroupMap stops, + * any objects it owns will be removed from the group map + * @param removeOwnedObjectsOnExit the removeOwnedObjectsOnExit to set + */ + public void setRemoveOwnedObjectsOnExit(boolean removeOwnedObjectsOnExit) { + this.removeOwnedObjectsOnExit = removeOwnedObjectsOnExit; + } + + /** + * @return releaseLockOnExit - true by default + */ + public boolean isReleaseLockOnExit() { + return releaseLockOnExit; + } + + /** + * set release lock on exit - true by default + * + * @param releaseLockOnExit + * the releaseLockOnExit to set + */ + public void setReleaseLockOnExit(boolean releaseLockOnExit) { + this.releaseLockOnExit = releaseLockOnExit; + } + + /** + * @return the lockTimeToLive + */ + public int getLockTimeToLive() { + return lockTimeToLive; + } + + /** + * @param lockTimeToLive + * the lockTimeToLive to set + */ + public void setLockTimeToLive(int lockTimeToLive) { + this.lockTimeToLive = lockTimeToLive; + } + + /** + * @return the minimumGroupSize + */ + public int getMinimumGroupSize() { + return this.minimumGroupSize; + } + + /** + * @param minimumGroupSize + * the minimumGroupSize to set + */ + public void setMinimumGroupSize(int minimumGroupSize) { + this.minimumGroupSize = minimumGroupSize; + } + + /** + * @return the coordinatorWeight + */ + public int getCoordinatorWeight() { + return this.coordinatorWeight; + } + + /** + * @param coordinatorWeight + * the coordinatorWeight to set + */ + public void setCoordinatorWeight(int coordinatorWeight) { + this.coordinatorWeight = coordinatorWeight; + } + + /** + * clear entries from the Map + * + * @throws IllegalStateException + */ + public void clear() throws IllegalStateException { + checkStatus(); + if (this.localMap != null && !this.localMap.isEmpty()) { + Set keys = null; + synchronized (this.mapMutex) { + keys = new HashSet(this.localMap.keySet()); + } + for (K key : keys) { + remove(key); + } + } + this.localMap.clear(); + } + + public boolean containsKey(Object key) { + synchronized (this.mapMutex) { + return this.localMap != null ? this.localMap.containsKey(key) + : false; + } + } + + public boolean containsValue(Object value) { + EntryValue entryValue = new EntryValue(null, value); + synchronized (this.mapMutex) { + return this.localMap != null ? this.localMap + .containsValue(entryValue) : false; + } + } + + public Set> entrySet() { + Map result = new HashMap(); + synchronized (this.mapMutex) { + if (this.localMap != null) { + for (EntryValue entry : this.localMap.values()) { + result.put((K) entry.getKey(), entry.getValue()); + } + } + } + return result.entrySet(); + } + + public V get(Object key) { + EntryValue value = null; + synchronized (this.mapMutex) { + value = this.localMap != null ? this.localMap.get(key) : null; + } + return value != null ? value.getValue() : null; + } + + public boolean isEmpty() { + synchronized (this.mapMutex) { + return this.localMap != null ? this.localMap.isEmpty() : true; + } + } + + public Set keySet() { + Set result = null; + synchronized (this.mapMutex) { + result = new HashSet(this.localMap.keySet()); + } + return result; + } + + /** + * Puts an value into the map associated with the key + * + * @param key + * @param value + * @return the old value or null + * @throws GroupUpdateException + * @throws IllegalStateException + * + */ + public V put(K key, V value) throws GroupUpdateException, + IllegalStateException { + return put(key, value, isAlwaysLock(), isRemoveOwnedObjectsOnExit(), + isReleaseLockOnExit(), getTimeToLive(), getLockTimeToLive()); + } + + /** + * Puts an value into the map associated with the key + * + * @param key + * @param value + * @param lock + * @param removeOnExit + * @param releaseLockOnExit + * @param timeToLive + * @param leaseTime + * @return the old value or null + * @throws GroupUpdateException + * @throws IllegalStateException + * + */ + public V put(K key, V value, boolean lock, boolean removeOnExit, + boolean releaseLockOnExit, long timeToLive, long leaseTime) + throws GroupUpdateException, IllegalStateException { + checkStatus(); + EntryKey entryKey = new EntryKey(this.local, key); + entryKey.setLocked(lock); + entryKey.setRemoveOnExit(removeOnExit); + entryKey.setReleaseLockOnExit(releaseLockOnExit); + entryKey.setTimeToLive(timeToLive); + entryKey.setLockLeaseTime(leaseTime); + EntryMessage entryMsg = new EntryMessage(); + entryMsg.setKey(entryKey); + entryMsg.setValue(value); + entryMsg.setType(EntryMessage.MessageType.INSERT); + return (V) sendStateRequest(getCoordinator(), entryMsg); + } + + /** + * Remove a lock on a key + * + * @param key + * @throws GroupUpdateException + */ + public void unlock(K key) throws GroupUpdateException { + EntryKey entryKey = new EntryKey(this.local, key); + entryKey.setLocked(false); + EntryMessage entryMsg = new EntryMessage(); + entryMsg.setKey(entryKey); + entryMsg.setLockUpdate(true); + sendStateRequest(getCoordinator(), entryMsg); + } + + /** + * Lock a key in the distributed map + * + * @param key + * @throws GroupUpdateException + */ + public void lock(K key) throws GroupUpdateException { + lock(key, getLockTimeToLive()); + } + + /** + * Lock a key in the distributed map + * + * @param key + * @param leaseTime + * @throws GroupUpdateException + */ + public void lock(K key, long leaseTime) throws GroupUpdateException { + EntryKey entryKey = new EntryKey(this.local, key); + entryKey.setLocked(true); + entryKey.setLockLeaseTime(leaseTime); + EntryMessage entryMsg = new EntryMessage(); + entryMsg.setKey(entryKey); + entryMsg.setLockUpdate(true); + sendStateRequest(getCoordinator(), entryMsg); + } + + /** + * Add the Map to the distribution + * + * @param t + * @throws GroupUpdateException + * @throws IllegalStateException + */ + public void putAll(Map t) + throws GroupUpdateException, IllegalStateException { + putAll(t, isAlwaysLock(), isRemoveOwnedObjectsOnExit(), + isReleaseLockOnExit(), getTimeToLive(), getLockTimeToLive()); + } + + /** + * Add the Map to the distribution + * + * @param t + * @param lock + * @param removeOnExit + * @param releaseLockOnExit + * @param timeToLive + * @param lockTimeToLive + * @throws GroupUpdateException + * @throws IllegalStateException + */ + public void putAll(Map t, boolean lock, + boolean removeOnExit, boolean releaseLockOnExit, long timeToLive, + long lockTimeToLive) throws GroupUpdateException, + IllegalStateException { + for (java.util.Map.Entry entry : t.entrySet()) { + put(entry.getKey(), entry.getValue(), lock, removeOnExit, + releaseLockOnExit, timeToLive, lockTimeToLive); + } + } + + /** + * remove a value from the map associated with the key + * + * @param key + * @return the Value or null + * @throws GroupUpdateException + * @throws IllegalStateException + * + */ + public V remove(Object key) throws GroupUpdateException, + IllegalStateException { + EntryKey entryKey = new EntryKey(this.local, (K) key); + return doRemove(entryKey); + } + + V doRemove(EntryKey key) throws GroupUpdateException, + IllegalStateException { + checkStatus(); + EntryMessage entryMsg = new EntryMessage(); + entryMsg.setKey(key); + entryMsg.setType(EntryMessage.MessageType.DELETE); + return (V) sendStateRequest(getCoordinator(), entryMsg); + } + + public int size() { + synchronized (this.mapMutex) { + return this.localMap != null ? this.localMap.size() : 0; + } + } + + public Collection values() { + List result = new ArrayList(); + synchronized (this.mapMutex) { + if (this.localMap != null) { + for (EntryValue value : this.localMap.values()) { + result.add(value.getValue()); + } + } + } + return result; + } + + /** + * @return a set of the members + */ + public Set getMembers() { + Set result = new HashSet(); + result.addAll(this.members.values()); + return result; + } + + /** + * Get a member by its unique id + * + * @param id + * @return + */ + public Member getMemberById(String id) { + return this.members.get(id); + } + + /** + * Return a member of the Group with the matching name + * + * @param name + * @return + */ + public Member getMemberByName(String name) { + if (name != null) { + for (Member member : this.members.values()) { + if (member.getName().equals(name)) { + return member; + } + } + } + return null; + } + + /** + * @return the local member that represents this Group + * instance + */ + public Member getLocalMember() { + return this.local; + } + + /** + * @param key + * @return true if this is the owner of the key + */ + public boolean isOwner(K key) { + EntryValue entryValue = null; + synchronized (this.mapMutex) { + entryValue = this.localMap != null ? this.localMap.get(key) : null; + } + boolean result = false; + if (entryValue != null) { + result = entryValue.getKey().getOwner().getId().equals( + this.local.getId()); + } + return result; + } + + /** + * Get the owner of a key + * + * @param key + * @return the owner - or null if the key doesn't exist + */ + EntryKey getKey(Object key) { + EntryValue entryValue = null; + synchronized (this.mapMutex) { + entryValue = this.localMap != null ? this.localMap.get(key) : null; + } + return entryValue != null ? entryValue.getKey() : null; + } + + /** + * @return true if the coordinator for the map + */ + protected boolean isCoordinator() { + return isCoordinatorMatch() && this.electionFinished.get(); + } + + /** + * @return true if the coordinator for the map + */ + protected boolean isCoordinatorMatch() { + return this.local.equals(this.coordinator); + } + + /** + * @return the coordinator + */ + public Member getCoordinator() { + return this.coordinator; + } + + void setCoordinator(Member member) { + this.coordinator = member; + } + + /** + * Broadcast a message to the group + * + * @param message + * @throws JMSException + */ + public void broadcastMessage(Object message) throws JMSException { + checkStatus(); + ObjectMessage objMsg = this.messageSession + .createObjectMessage((Serializable) message); + objMsg.setJMSCorrelationID(this.idGenerator.generateId()); + objMsg.setJMSType(MESSAGE_TYPE); + objMsg.setStringProperty(MEMBER_ID_PROPERTY, this.local.getId()); + this.messageProducer.send(this.messageTopic, objMsg); + } + + /** + * As the group for a response - one will be selected from the group + * + * @param member + * @param message + * @param timeout + * in milliseconds - a value if 0 means wait until complete + * @return + * @throws JMSException + */ + public Serializable broadcastMessageRequest(Object message, long timeout) + throws JMSException { + checkStatus(); + Object result = null; + MapRequest request = new MapRequest(); + String id = this.idGenerator.generateId(); + synchronized (this.messageRequests) { + this.messageRequests.put(id, request); + } + ObjectMessage objMsg = this.stateSession + .createObjectMessage((Serializable) message); + objMsg.setJMSReplyTo(this.inboxTopic); + objMsg.setJMSCorrelationID(id); + objMsg.setJMSType(MESSAGE_TYPE); + objMsg.setStringProperty(MEMBER_ID_PROPERTY, this.local.getId()); + this.messageProducer.send(this.messageQueue, objMsg); + result = request.get(timeout); + return (Serializable) result; + } + + /** + * Send a message to the group - but only the least loaded member will + * process it + * + * @param message + * @throws JMSException + */ + public void sendMessage(Object message) throws JMSException { + checkStatus(); + ObjectMessage objMsg = this.messageSession + .createObjectMessage((Serializable) message); + objMsg.setJMSCorrelationID(this.idGenerator.generateId()); + objMsg.setJMSType(MESSAGE_TYPE); + objMsg.setStringProperty(MEMBER_ID_PROPERTY, this.local.getId()); + this.messageProducer.send(this.messageQueue, objMsg); + } + + /** + * Send a message to an individual member + * + * @param member + * @param message + * @throws JMSException + */ + public void sendMessage(Member member, Object message) throws JMSException { + checkStatus(); + ObjectMessage objMsg = this.messageSession + .createObjectMessage((Serializable) message); + objMsg.setJMSCorrelationID(this.idGenerator.generateId()); + objMsg.setJMSType(MESSAGE_TYPE); + objMsg.setStringProperty(MEMBER_ID_PROPERTY, this.local.getId()); + this.messageProducer.send(member.getInBoxDestination(), objMsg); + } + + /** + * Send a request to a member + * + * @param member + * @param message + * @param timeout + * in milliseconds - a value if 0 means wait until complete + * @return the request or null + * @throws JMSException + */ + public Object sendMessageRequest(Member member, Object message, long timeout) + throws JMSException { + checkStatus(); + Object result = null; + MapRequest request = new MapRequest(); + String id = this.idGenerator.generateId(); + synchronized (this.messageRequests) { + this.messageRequests.put(id, request); + } + ObjectMessage objMsg = this.stateSession + .createObjectMessage((Serializable) message); + objMsg.setJMSReplyTo(this.inboxTopic); + objMsg.setJMSCorrelationID(id); + objMsg.setJMSType(MESSAGE_TYPE); + objMsg.setStringProperty(MEMBER_ID_PROPERTY, this.local.getId()); + this.messageProducer.send(member.getInBoxDestination(), objMsg); + result = request.get(timeout); + return result; + } + + /** + * send a response to a message + * + * @param member + * @param replyId + * @param message + * @throws JMSException + */ + public void sendMessageResponse(Member member, String replyId, + Object message) throws JMSException { + checkStatus(); + ObjectMessage objMsg = this.messageSession + .createObjectMessage((Serializable) message); + objMsg.setJMSCorrelationID(replyId); + objMsg.setJMSType(MESSAGE_TYPE); + objMsg.setStringProperty(MEMBER_ID_PROPERTY, this.local.getId()); + this.messageProducer.send(member.getInBoxDestination(), objMsg); + } + + /** + * Select a coordinator - coordinator weighting is used - or if everything + * is equal - a comparison of member ids. + * + * @param members + * @return + */ + protected Member selectCordinator(List list) { + List sorted = sortMemberList(list); + Member result = sorted.isEmpty() ? this.local : sorted + .get(list.size() - 1); + return result; + } + + protected List sortMemberList(List list) { + Collections.sort(list, new Comparator() { + public int compare(Member m1, Member m2) { + int result = m1.getCoordinatorWeight() + - m2.getCoordinatorWeight(); + if (result == 0) { + result = m1.getId().compareTo(m2.getId()); + } + return result; + } + }); + return list; + } + + Object sendStateRequest(Member member, Serializable payload) { + Object result = null; + MapRequest request = new MapRequest(); + String id = this.idGenerator.generateId(); + synchronized (this.stateRequests) { + this.stateRequests.put(id, request); + } + try { + ObjectMessage objMsg = this.stateSession + .createObjectMessage(payload); + objMsg.setJMSReplyTo(this.inboxTopic); + objMsg.setJMSCorrelationID(id); + objMsg.setJMSType(STATE_TYPE); + this.stateProducer.send(member.getInBoxDestination(), objMsg); + result = request.get(getHeartBeatInterval()); + } catch (JMSException e) { + if (this.started.get()) { + LOG.error("Failed to send request " + payload, e); + } + } + if (result instanceof GroupUpdateException) { + throw (GroupUpdateException) result; + } + if (result instanceof EntryMessage) { + EntryMessage entryMsg = (EntryMessage) result; + result = entryMsg.getOldValue(); + } + return result; + } + + void sendAsyncStateRequest(AsyncMapRequest asyncRequest, Member member, + Serializable payload) { + MapRequest request = new MapRequest(); + String id = this.idGenerator.generateId(); + asyncRequest.add(id, request); + synchronized (this.stateRequests) { + this.stateRequests.put(id, request); + } + try { + ObjectMessage objMsg = this.stateSession + .createObjectMessage(payload); + objMsg.setJMSReplyTo(this.inboxTopic); + objMsg.setJMSCorrelationID(id); + objMsg.setJMSType(STATE_TYPE); + this.stateProducer.send(member.getInBoxDestination(), objMsg); + } catch (JMSException e) { + if (this.started.get()) { + LOG.error("Failed to send async request " + payload, e); + } + } + } + + void sendReply(Object reply, Destination replyTo, String id) { + if (this.started.get()) { + if (replyTo != null) { + if (replyTo.equals(this.local.getInBoxDestination())) { + processRequest(id, reply); + } else { + try { + ObjectMessage replyMsg = this.stateSession + .createObjectMessage((Serializable) reply); + replyMsg.setJMSCorrelationID(id); + replyMsg.setJMSType(STATE_TYPE); + this.stateProducer.send(replyTo, replyMsg); + } catch (JMSException e) { + LOG.error("Couldn't send reply from co-ordinator", e); + } + } + } else { + LOG.error("NULL replyTo destination"); + } + } + } + + void broadcastMapUpdate(EntryMessage entry, String correlationId) { + if (this.started.get()) { + try { + EntryMessage copy = entry.copy(); + copy.setMapUpdate(true); + ObjectMessage objMsg = this.stateSession + .createObjectMessage(copy); + objMsg.setJMSCorrelationID(correlationId); + objMsg.setJMSType(STATE_TYPE); + this.stateProducer.send(this.stateTopic, objMsg); + } catch (JMSException e) { + if (this.started.get()) { + LOG.error("Failed to send EntryMessage " + entry, e); + } + } + } + } + + void processJMSMessage(Message message) { + if (message instanceof ObjectMessage) { + ObjectMessage objMsg = (ObjectMessage) message; + try { + String messageType = objMsg.getJMSType(); + String id = objMsg.getJMSCorrelationID(); + String memberId = objMsg.getStringProperty(MEMBER_ID_PROPERTY); + Destination replyTo = objMsg.getJMSReplyTo(); + Object payload = objMsg.getObject(); + if (messageType != null) { + if (messageType.equals(STATE_TYPE)) { + if (payload instanceof Member) { + handleHeartbeats((Member) payload); + } else if (payload instanceof EntryMessage) { + EntryMessage entryMsg = (EntryMessage) payload; + entryMsg = entryMsg.copy(); + if (entryMsg.isLockUpdate()) { + processLockUpdate(entryMsg, replyTo, id); + } else if (entryMsg.isMapUpdate()) { + processMapUpdate(entryMsg); + } else { + processEntryMessage(entryMsg, replyTo, id); + } + } else if (payload instanceof ElectionMessage) { + ElectionMessage electionMsg = (ElectionMessage) payload; + electionMsg = electionMsg.copy(); + processElectionMessage(electionMsg, replyTo, id); + } + } else if (messageType.equals(MESSAGE_TYPE)) { + processGroupMessage(memberId, id, replyTo, payload); + } else { + LOG.error("Unknown message type: " + messageType); + } + processRequest(id, payload); + } else { + LOG.error("Can't process a message type of null"); + } + } catch (JMSException e) { + LOG.warn("Failed to process message: " + message, e); + } + } + } + + void processRequest(String id, Object value) { + if (id != null) { + MapRequest result = null; + synchronized (this.stateRequests) { + result = this.stateRequests.remove(id); + } + if (result != null) { + result.put(id, value); + } + } + } + + void processLockUpdate(EntryMessage entryMsg, Destination replyTo, + String correlationId) { + waitForElection(); + synchronized (this.mapMutex) { + boolean newLock = entryMsg.getKey().isLocked(); + Member newOwner = entryMsg.getKey().getOwner(); + long newLockExpiration = newLock ? entryMsg.getKey() + .getLockExpiration() : 0l; + if (isCoordinator() && !entryMsg.isMapUpdate()) { + EntryKey originalKey = getKey(entryMsg.getKey().getKey()); + if (originalKey != null) { + if (originalKey.isLocked()) { + if (!originalKey.getOwner().equals( + entryMsg.getKey().getOwner())) { + Serializable reply = new GroupUpdateException( + "Owned by " + originalKey.getOwner()); + sendReply(reply, replyTo, correlationId); + } else { + originalKey.setLocked(newLock); + originalKey.setOwner(newOwner); + originalKey.setLockExpiration(newLockExpiration); + broadcastMapUpdate(entryMsg, correlationId); + } + } else { + originalKey.setLocked(newLock); + originalKey.setOwner(newOwner); + originalKey.setLockExpiration(newLockExpiration); + broadcastMapUpdate(entryMsg, correlationId); + } + } + } else { + EntryKey originalKey = getKey(entryMsg.getKey().getKey()); + if (originalKey != null) { + originalKey.setLocked(newLock); + originalKey.setOwner(newOwner); + originalKey.setLockExpiration(newLockExpiration); + } + } + } + } + + void processEntryMessage(EntryMessage entryMsg, Destination replyTo, + String correlationId) { + waitForElection(); + if (isCoordinator()) { + EntryKey key = entryMsg.getKey(); + EntryValue value = new EntryValue(key, (V) entryMsg + .getValue()); + boolean insert = entryMsg.isInsert(); + boolean containsKey = false; + synchronized (this.mapMutex) { + containsKey = this.localMap.containsKey(key.getKey()); + } + if (containsKey) { + EntryKey originalKey = getKey(key.getKey()); + if (originalKey.equals(key.getOwner()) + || !originalKey.isLocked()) { + EntryValue old = null; + if (insert) { + synchronized (this.mapMutex) { + old = this.localMap.put(key.getKey(), value); + } + } else { + synchronized (this.mapMutex) { + old = this.localMap.remove(key.getKey()); + } + } + entryMsg.setOldValue(old.getValue()); + broadcastMapUpdate(entryMsg, correlationId); + fireMapChanged(key.getOwner(), key.getKey(), + old.getValue(), value.getValue(), false); + } else { + Serializable reply = new GroupUpdateException( + "Owned by " + originalKey.getOwner()); + sendReply(reply, replyTo, correlationId); + } + } else { + if (insert) { + synchronized (this.mapMutex) { + this.localMap.put(key.getKey(), value); + } + broadcastMapUpdate(entryMsg, correlationId); + fireMapChanged(key.getOwner(), key.getKey(), null, value + .getValue(), false); + } else { + sendReply(null, replyTo, correlationId); + } + } + } + } + + void processMapUpdate(EntryMessage entryMsg) { + boolean containsKey = false; + EntryKey key = entryMsg.getKey(); + EntryValue value = new EntryValue(key, (V) entryMsg.getValue()); + boolean insert = entryMsg.isInsert() || entryMsg.isSync(); + synchronized (this.mapMutex) { + containsKey = this.localMap.containsKey(key.getKey()); + } + waitForElection(); + if (!isCoordinator() || entryMsg.isSync()) { + if (containsKey) { + if (key.isLockExpired()) { + EntryValue old = this.localMap.get(key.getKey()); + if (old != null) { + old.getKey().setLocked(false); + } + } else { + EntryValue old = null; + if (insert) { + synchronized (this.mapMutex) { + old = this.localMap.put(key.getKey(), value); + } + } else { + synchronized (this.mapMutex) { + old = this.localMap.remove(key.getKey()); + value.setValue(null); + } + } + fireMapChanged(key.getOwner(), key.getKey(), + old.getValue(), value.getValue(), entryMsg + .isExpired()); + } + } else { + if (insert) { + synchronized (this.mapMutex) { + this.localMap.put(key.getKey(), value); + } + fireMapChanged(key.getOwner(), key.getKey(), null, value + .getValue(), false); + } + } + } + } + + void processGroupMessage(String memberId, String replyId, + Destination replyTo, Object payload) { + Member member = this.members.get(memberId); + if (member != null) { + fireMemberMessage(member, replyId, payload); + } + if (replyId != null) { + MapRequest result = null; + synchronized (this.messageRequests) { + result = this.messageRequests.remove(replyId); + } + if (result != null) { + result.put(replyId, payload); + } + } + } + + void handleHeartbeats(Message message) { + try { + if (message instanceof ObjectMessage) { + ObjectMessage objMsg = (ObjectMessage) message; + Member member = (Member) objMsg.getObject(); + handleHeartbeats(member); + } else { + LOG.warn("Unexpected message: " + message); + } + } catch (JMSException e) { + LOG.warn("Failed to handle heart beat", e); + } + } + + void handleHeartbeats(Member member) { + member.setTimeStamp(System.currentTimeMillis()); + if (this.members.put(member.getId(), member) == null) { + fireMemberStarted(member); + if (!member.equals(this.local)) { + sendHeartBeat(member.getInBoxDestination()); + } + election(member, true); + synchronized (this.memberMutex) { + this.memberMutex.notifyAll(); + } + } + } + + void handleConsumerEvents(ConsumerEvent event) { + if (!event.isStarted()) { + Member member = this.members.remove(event.getConsumerId() + .toString()); + if (member != null) { + fireMemberStopped(member); + election(member, false); + } + } + } + + void checkMembership() { + if (this.started.get() && this.electionFinished.get()) { + long checkTime = System.currentTimeMillis() + - getHeartBeatInterval(); + boolean doElection = false; + for (Member member : this.members.values()) { + if (member.getTimeStamp() < checkTime) { + LOG.info("Member timestamp expired " + member); + this.members.remove(member.getId()); + fireMemberStopped(member); + doElection = true; + } + } + if (doElection) { + election(null, false); + } + } + } + + void expirationSweep() { + waitForElection(); + if (isCoordinator() && this.started.get() + && this.electionFinished.get()) { + List expiredMessages = null; + List expiredLocks = null; + synchronized (this.mapMutex) { + Map> map = this.localMap; + if (map != null) { + long currentTime = System.currentTimeMillis(); + for (EntryValue value : map.values()) { + EntryKey k = value.getKey(); + if (k.isExpired(currentTime)) { + if (expiredMessages == null) { + expiredMessages = new ArrayList(); + expiredMessages.add(k); + } + } else if (k.isLockExpired(currentTime)) { + k.setLocked(false); + if (expiredLocks == null) { + expiredLocks = new ArrayList(); + expiredLocks.add(k); + } + expiredLocks.add(k); + } + } + } + } + // do the actual removal of entries in a separate thread + if (expiredMessages != null) { + final List expire = expiredMessages; + this.stateExecutor.execute(new Runnable() { + public void run() { + doMessageExpiration(expire); + } + }); + } + if (expiredLocks != null) { + final List expire = expiredLocks; + this.stateExecutor.execute(new Runnable() { + public void run() { + doLockExpiration(expire); + } + }); + } + } + } + + void doMessageExpiration(List list) { + if (this.started.get() && this.electionFinished.get() + && isCoordinator()) { + for (EntryKey k : list) { + EntryValue old = null; + synchronized (this.mapMutex) { + old = this.localMap.remove(k.getKey()); + } + if (old != null) { + EntryMessage entryMsg = new EntryMessage(); + entryMsg.setType(EntryMessage.MessageType.DELETE); + entryMsg.setExpired(true); + entryMsg.setKey(k); + entryMsg.setValue(old.getValue()); + broadcastMapUpdate(entryMsg, ""); + fireMapChanged(k.getOwner(), k.getKey(), old.getValue(), + null, true); + } + } + } + } + + void doLockExpiration(List list) { + if (this.started.get() && this.electionFinished.get() + && isCoordinator()) { + for (EntryKey k : list) { + EntryMessage entryMsg = new EntryMessage(); + entryMsg.setType(EntryMessage.MessageType.DELETE); + entryMsg.setLockExpired(true); + entryMsg.setKey(k); + broadcastMapUpdate(entryMsg, ""); + } + } + } + + void sendHeartBeat() { + sendHeartBeat(this.heartBeatTopic); + } + + void sendHeartBeat(Destination destination) { + if (this.started.get()) { + try { + ObjectMessage msg = this.stateSession + .createObjectMessage(this.local); + msg.setJMSType(STATE_TYPE); + this.stateProducer.send(destination, msg); + } catch (javax.jms.IllegalStateException e) { + // ignore - as we are probably stopping + } catch (Throwable e) { + if (this.started.get()) { + LOG.warn("Failed to send heart beat", e); + } + } + } + } + + void updateNewMemberMap(Member member) { + List>> list = new ArrayList>>(); + synchronized (this.mapMutex) { + if (this.localMap != null) { + for (Map.Entry> entry : this.localMap + .entrySet()) { + list.add(entry); + } + } + } + try { + for (Map.Entry> entry : list) { + EntryMessage entryMsg = new EntryMessage(); + entryMsg.setKey(entry.getValue().getKey()); + entryMsg.setValue(entry.getValue().getValue()); + entryMsg.setType(EntryMessage.MessageType.SYNC); + entryMsg.setMapUpdate(true); + ObjectMessage objMsg = this.stateSession + .createObjectMessage(entryMsg); + if (!member.equals(entry.getValue().getKey().getOwner())) { + objMsg.setJMSType(STATE_TYPE); + this.stateProducer.send(member.getInBoxDestination(), + objMsg); + } + } + } catch (javax.jms.IllegalStateException e) { + // ignore - as closing + } catch (JMSException e) { + if (started.get()) { + LOG.warn("Failed to update new member ", e); + } + } + } + + void fireMemberStarted(Member member) { + LOG.info(this.local.getName() + " Member started " + member); + for (MemberChangedListener l : this.membershipListeners) { + l.memberStarted(member); + } + } + + void fireMemberStopped(Member member) { + LOG.info(this.local.getName() + " Member stopped " + member); + for (MemberChangedListener l : this.membershipListeners) { + l.memberStopped(member); + } + // remove all entries owned by the stopped member + List> tmpList = new ArrayList>(); + boolean mapExists = false; + synchronized (this.mapMutex) { + mapExists = this.localMap != null; + if (mapExists) { + for (EntryValue value : this.localMap.values()) { + EntryKey entryKey = value.getKey(); + if (entryKey.getOwner().equals(member)) { + if (entryKey.isRemoveOnExit()) { + tmpList.add(entryKey); + } + if (entryKey.isReleaseLockOnExit()) { + entryKey.setLocked(false); + } + } + } + } + } + if (mapExists) { + for (EntryKey entryKey : tmpList) { + EntryValue value = null; + synchronized (this.mapMutex) { + value = this.localMap.remove(entryKey); + } + fireMapChanged(member, entryKey.getKey(), value.getValue(), + null, false); + } + } + } + + void fireMemberMessage(final Member member, final String replyId, + final Object message) { + if (this.started.get() && this.stateExecutor != null + && !this.messageExecutor.isShutdown()) { + this.messageExecutor.execute(new Runnable() { + public void run() { + doFireMemberMessage(member, replyId, message); + } + }); + } + } + + void doFireMemberMessage(Member sender, String replyId, Object message) { + if (this.started.get()) { + for (GroupMessageListener l : this.groupMessageListeners) { + l.messageDelivered(sender, replyId, message); + } + } + } + + void fireMapChanged(final Member owner, final Object key, + final Object oldValue, final Object newValue, final boolean expired) { + if (this.started.get() && this.stateExecutor != null + && !this.stateExecutor.isShutdown()) { + this.stateExecutor.execute(new Runnable() { + public void run() { + doFireMapChanged(owner, key, oldValue, newValue, expired); + } + }); + } + } + + void doFireMapChanged(Member owner, Object key, Object oldValue, + Object newValue, boolean expired) { + if (this.started.get()) { + for (GroupStateChangedListener l : this.mapChangedListeners) { + if (oldValue == null) { + l.mapInsert(owner, key, newValue); + } else if (newValue == null) { + l.mapRemove(owner, key, oldValue, expired); + } else { + l.mapUpdate(owner, key, oldValue, newValue); + } + } + } + } + + void checkStatus() throws IllegalStateException { + if (!started.get()) { + throw new IllegalStateException("GroupMap " + this.local.getName() + + " not started"); + } + waitForElection(); + } + + public String toString() { + return "Group:" + getName() + "{id=" + this.local.getId() + + ",coordinator=" + isCoordinator() + ",inbox=" + + this.local.getInBoxDestination() + "}"; + } + + void election(final Member member, final boolean memberStarted) { + if (this.started.get() && this.stateExecutor != null + && !this.electionExecutor.isShutdown()) { + synchronized (this.electionFinished) { + this.electionFinished.set(false); + } + synchronized (this.electionExecutor) { + // remove any queued election tasks + List list = new ArrayList( + this.electionExecutor.getQueue()); + for (Runnable r : list) { + ElectionService es = (ElectionService) r; + es.stop(); + this.electionExecutor.remove(es); + } + } + ElectionService es = new ElectionService(member, memberStarted); + es.start(); + this.electionExecutor.execute(es); + } + } + + boolean callElection() { + List members = new ArrayList(this.members.values()); + List sorted = sortMemberList(members); + AsyncMapRequest request = new AsyncMapRequest(); + boolean doCall = false; + for (Member member : sorted) { + if (this.local.equals(member)) { + doCall = true; + } else if (doCall) { + ElectionMessage msg = new ElectionMessage(); + msg.setMember(this.local); + msg.setType(ElectionMessage.MessageType.ELECTION); + sendAsyncStateRequest(request, member, msg); + } + } + boolean result = request.isSuccess(getHeartBeatInterval()); + return result; + } + + void processElectionMessage(ElectionMessage msg, Destination replyTo, + String correlationId) { + if (msg.isElection()) { + msg.setType(ElectionMessage.MessageType.ANSWER); + msg.setMember(this.local); + sendReply(msg, replyTo, correlationId); + election(null, false); + } else if (msg.isCoordinator()) { + synchronized (this.electionFinished) { + this.coordinator = msg.getMember(); + this.electionFinished.set(true); + this.electionFinished.notifyAll(); + } + } + } + + void broadcastElectionType(ElectionMessage.MessageType type) { + if (started.get()) { + try { + ElectionMessage msg = new ElectionMessage(); + msg.setMember(this.local); + msg.setType(type); + ObjectMessage objMsg = this.stateSession + .createObjectMessage(msg); + objMsg.setJMSType(STATE_TYPE); + this.stateProducer.send(this.stateTopic, objMsg); + } catch (javax.jms.IllegalStateException e) { + // ignore - we are stopping + } catch (JMSException e) { + if (this.started.get()) { + LOG.error("Failed to broadcast election message: " + type, + e); + } + } + } + } + + void waitForElection() { + synchronized (this.electionFinished) { + while (started.get() && !this.electionFinished.get()) { + try { + this.electionFinished.wait(200); + } catch (InterruptedException e) { + LOG.warn("Interrupted in waitForElection"); + stop(); + } + } + } + } + class ElectionService implements Runnable { + private AtomicBoolean started = new AtomicBoolean(); + private Member member; + private boolean memberStarted; + + ElectionService(Member member, boolean memberStarted) { + this.member = member; + this.memberStarted = memberStarted; + } + + void start() { + this.started.set(true); + } + + void stop() { + this.started.set(false); + } + + public void run() { + doElection(); + } + + void doElection() { + if ((this.member == null || (!this.member.equals(Group.this.local) || Group.this.members + .size() == getMinimumGroupSize()))) { + boolean wasCoordinator = isCoordinatorMatch() && !isEmpty(); + // call an election + while (!callElection() && isStarted() && this.started.get()) + ; + if (isStarted() && this.started.get()) { + List members = new ArrayList( + Group.this.members.values()); + Group.this.coordinator = selectCordinator(members); + if (isCoordinatorMatch()) { + broadcastElectionType(ElectionMessage.MessageType.COORDINATOR); + } + if (this.memberStarted && this.member != null) { + if (wasCoordinator || isCoordinator() + && this.started.get()) { + updateNewMemberMap(this.member); + } + } + if (!isElectionFinished() && this.started.get()) { + try { + synchronized (Group.this.electionFinished) { + Group.this.electionFinished + .wait(Group.this.heartBeatInterval * 2); + } + } catch (InterruptedException e) { + } + } + if (!isElectionFinished() && this.started.get()) { + // we must be the coordinator + setCoordinator(getLocalMember()); + setElectionFinished(true); + broadcastElectionType(ElectionMessage.MessageType.COORDINATOR); + } + } + } + } + } +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/GroupMessageListener.java b/activemq-groups/src/main/java/org/apache/activegroups/GroupMessageListener.java new file mode 100644 index 0000000000..817e6edca1 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/GroupMessageListener.java @@ -0,0 +1,34 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.activegroups; + + +/** + * A listener for message communication + * + */ +public interface GroupMessageListener { + + /** + * Called when a message is delivered to the Group from another member + * @param sender the member who sent the message + * @param replyId the id to use to respond to a message + * @param message the message object + */ + void messageDelivered(Member sender, String replyId, Object message); +} \ No newline at end of file diff --git a/activemq-groups/src/main/java/org/apache/activegroups/GroupStateChangedListener.java b/activemq-groups/src/main/java/org/apache/activegroups/GroupStateChangedListener.java new file mode 100644 index 0000000000..96e1dda42d --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/GroupStateChangedListener.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +/** + *Get notifications about changes to the state of the map + * + */ +public interface GroupStateChangedListener { + + /** + * Called when a key/value pair is inserted into the map + * @param owner + * @param key + * @param value + */ + void mapInsert(Member owner,Object key, Object value); + + /** + * Called when a key value is updated in the map + * @param owner + * @param Key + * @param oldValue + * @param newValue + */ + void mapUpdate(Member owner,Object Key,Object oldValue,Object newValue); + + /** + * Called when a key value is removed from the map + * @param owner + * @param key + * @param value + * @param expired + */ + void mapRemove(Member owner,Object key, Object value,boolean expired); +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/GroupUpdateException.java b/activemq-groups/src/main/java/org/apache/activegroups/GroupUpdateException.java new file mode 100644 index 0000000000..5d1361681a --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/GroupUpdateException.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +/** + * thrown when updating a key to map that the local client doesn't own + * + */ +public class GroupUpdateException extends RuntimeException { + private static final long serialVersionUID = -7584658017201604560L; + + /** + * @param message + */ + public GroupUpdateException(String message) { + super(message); + } +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/Member.java b/activemq-groups/src/main/java/org/apache/activegroups/Member.java new file mode 100644 index 0000000000..244fe70c9e --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/Member.java @@ -0,0 +1,163 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import javax.jms.Destination; +import org.apache.activemq.util.IdGenerator; + +/** + *

+ * A Member holds information about a member of the group + * + */ +public class Member implements Externalizable { + private String name; + private String id; + private String hostname; + private long startTime; + private int coordinatorWeight; + private Destination inBoxDestination; + private transient long timeStamp; + + + /** + * Default constructor - only used by serialization + */ + public Member() { + } + /** + * @param name + */ + public Member(String name) { + this.name = name; + this.hostname = IdGenerator.getHostName(); + this.startTime=System.currentTimeMillis(); + } + + /** + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * @return the id + */ + public String getId() { + return this.id; + } + + void setId(String id) { + this.id=id; + } + + /** + * @return the hostname + */ + public String getHostname() { + return this.hostname; + } + + /** + * @return the startTime + */ + public long getStartTime() { + return this.startTime; + } + + /** + * @return the inbox destination + */ + public Destination getInBoxDestination() { + return this.inBoxDestination; + } + + void setInBoxDestination(Destination dest) { + this.inBoxDestination=dest; + } + + /** + * @return the timeStamp + */ + long getTimeStamp() { + return this.timeStamp; + } + + /** + * @param timeStamp the timeStamp to set + */ + void setTimeStamp(long timeStamp) { + this.timeStamp = timeStamp; + } + /** + * @return the coordinatorWeight + */ + public int getCoordinatorWeight() { + return this.coordinatorWeight; + } + /** + * @param coordinatorWeight the coordinatorWeight to set + */ + public void setCoordinatorWeight(int coordinatorWeight) { + this.coordinatorWeight = coordinatorWeight; + } + + + + public String toString() { + return this.name+"["+this.id+"]@"+this.hostname; + } + + + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + this.coordinatorWeight=in.readInt();; + this.name = in.readUTF(); + this.id = in.readUTF(); + this.hostname = in.readUTF(); + this.startTime=in.readLong(); + this.inBoxDestination=(Destination) in.readObject(); + this.coordinatorWeight=in.readInt(); + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeInt(this.coordinatorWeight); + out.writeUTF(this.name != null ? this.name : ""); + out.writeUTF(this.id != null ? this.id : ""); + out.writeUTF(this.hostname != null ? this.hostname : ""); + out.writeLong(this.startTime); + out.writeObject(this.inBoxDestination); + out.writeInt(this.coordinatorWeight); + } + + public int hashCode() { + return this.id.hashCode(); + } + + public boolean equals(Object obj) { + boolean result = false; + if (obj instanceof Member) { + Member other = (Member)obj; + result = this.id.equals(other.id); + } + return result; + } +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/MemberChangedListener.java b/activemq-groups/src/main/java/org/apache/activegroups/MemberChangedListener.java new file mode 100644 index 0000000000..cec5236614 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/MemberChangedListener.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.activegroups; + +/** + * A listener for membership changes to a group + * + */ +public interface MemberChangedListener { + + /** + * Notification a member has started + * @param member + */ + void memberStarted(Member member); + + /** + * Notification a member has stopped + * @param member + */ + void memberStopped(Member member); +} \ No newline at end of file diff --git a/activemq-groups/src/main/java/org/apache/activegroups/command/AsyncMapRequest.java b/activemq-groups/src/main/java/org/apache/activegroups/command/AsyncMapRequest.java new file mode 100644 index 0000000000..a0df0b153e --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/command/AsyncMapRequest.java @@ -0,0 +1,61 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups.command; + +import java.util.HashSet; +import java.util.Set; + +/** + * Return information about map update + * + */ +public class AsyncMapRequest implements RequestCallback{ + private final Object mutex = new Object(); + + private Set requests = new HashSet(); + + public void add(String id, MapRequest request) { + request.setCallback(this); + this.requests.add(id); + } + + /** + * Wait for requests + * @param timeout + * @return + */ + public boolean isSuccess(long timeout) { + long deadline = System.currentTimeMillis() + timeout; + while (!this.requests.isEmpty()) { + synchronized (this.mutex) { + try { + this.mutex.wait(timeout); + } catch (InterruptedException e) { + break; + } + } + timeout = Math.max(deadline - System.currentTimeMillis(), 0); + } + return this.requests.isEmpty(); + } + + + public void finished(String id) { + this.requests.remove(id); + + } +} \ No newline at end of file diff --git a/activemq-groups/src/main/java/org/apache/activegroups/command/ElectionMessage.java b/activemq-groups/src/main/java/org/apache/activegroups/command/ElectionMessage.java new file mode 100644 index 0000000000..5ce23d6918 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/command/ElectionMessage.java @@ -0,0 +1,106 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups.command; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import org.apache.activegroups.Member; + +/** + * Used to pass information around + * + */ +public class ElectionMessage implements Externalizable{ + public static enum MessageType{ELECTION,ANSWER,COORDINATOR}; + private Member member; + private MessageType type; + + /** + * @return the member + */ + public Member getMember() { + return this.member; + } + + /** + * @param member the member to set + */ + public void setMember(Member member) { + this.member = member; + } + + /** + * @return the type + */ + public MessageType getType() { + return this.type; + } + + /** + * @param type the type to set + */ + public void setType(MessageType type) { + this.type = type; + } + + /** + * @return true if election message + */ + public boolean isElection() { + return this.type != null && this.type.equals(MessageType.ELECTION); + } + + /** + * @return true if answer message + */ + public boolean isAnswer() { + return this.type != null && this.type.equals(MessageType.ANSWER); + } + + /** + * @return true if coordinator message + */ + public boolean isCoordinator() { + return this.type != null && this.type.equals(MessageType.COORDINATOR); + } + + + public ElectionMessage copy() { + ElectionMessage result = new ElectionMessage(); + result.member=this.member; + result.type=this.type; + return result; + } + + + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.member=(Member) in.readObject(); + this.type=(MessageType) in.readObject(); + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(this.member); + out.writeObject(this.type); + } + + public String toString() { + return "ElectionMessage: "+ this.member + "{"+this.type+ "}"; + } +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/command/EntryKey.java b/activemq-groups/src/main/java/org/apache/activegroups/command/EntryKey.java new file mode 100644 index 0000000000..3d89e77718 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/command/EntryKey.java @@ -0,0 +1,210 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups.command; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import org.apache.activegroups.Member; + +/** + * Holds information about an EntryKey + * + */ +public class EntryKey implements Externalizable { + private Member owner; + private K key; + private boolean locked; + private boolean removeOnExit; + private boolean releaseLockOnExit; + private long expiration; + private long lockExpiration; + + /** + * Default constructor - for serialization + */ + public EntryKey() { + } + + public EntryKey(Member owner, K key) { + this.owner = owner; + this.key = key; + } + + public int hashCode() { + return this.key != null ? this.key.hashCode() : super.hashCode(); + } + + /** + * @return the owner + */ + public Member getOwner() { + return this.owner; + } + + public void setOwner(Member member) { + this.owner=member; + } + + /** + * @return the key + */ + public K getKey() { + return this.key; + } + + /** + * @return the share + */ + public boolean isLocked() { + return this.locked; + } + + /** + * @param share the share to set + */ + public void setLocked(boolean locked) { + this.locked = locked; + } + + /** + * @return the removeOnExit + */ + public boolean isRemoveOnExit() { + return this.removeOnExit; + } + + /** + * @param removeOnExit + * the removeOnExit to set + */ + public void setRemoveOnExit(boolean removeOnExit) { + this.removeOnExit = removeOnExit; + } + + /** + * @return the expiration + */ + public long getExpiration() { + return expiration; + } + + /** + * @param expiration the expiration to set + */ + public void setExpiration(long expiration) { + this.expiration = expiration; + } + + /** + * @return the lockExpiration + */ + public long getLockExpiration() { + return lockExpiration; + } + + /** + * @param lockExpiration the lockExpiration to set + */ + public void setLockExpiration(long lockExpiration) { + this.lockExpiration = lockExpiration; + } + + /** + * @return the releaseLockOnExit + */ + public boolean isReleaseLockOnExit() { + return releaseLockOnExit; + } + + /** + * @param releaseLockOnExit the releaseLockOnExit to set + */ + public void setReleaseLockOnExit(boolean releaseLockOnExit) { + this.releaseLockOnExit = releaseLockOnExit; + } + + public void setTimeToLive(long ttl) { + if (ttl > 0 ) { + this.expiration=ttl+System.currentTimeMillis(); + }else { + this.expiration =0l; + } + } + + public void setLockLeaseTime(long ttl) { + if(ttl > 0) { + this.lockExpiration=ttl+System.currentTimeMillis(); + }else { + this.lockExpiration=0l; + } + } + + public boolean isExpired() { + return isExpired(System.currentTimeMillis()); + } + + public boolean isExpired(long currentTime) { + return this.expiration > 0 && this.expiration < currentTime; + } + + public boolean isLockExpired() { + return isLockExpired(System.currentTimeMillis()); + } + + public boolean isLockExpired(long currentTime) { + return this.lockExpiration > 0 && this.lockExpiration < currentTime; + } + + + public boolean equals(Object obj) { + boolean result = false; + if (obj instanceof EntryKey) { + EntryKey other = (EntryKey) obj; + result = other.key.equals(this.key); + } + return result; + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(this.owner); + out.writeObject(this.key); + out.writeBoolean(isLocked()); + out.writeBoolean(isRemoveOnExit()); + out.writeBoolean(isReleaseLockOnExit()); + out.writeLong(getExpiration()); + out.writeLong(getLockExpiration()); + } + + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.owner = (Member) in.readObject(); + this.key = (K) in.readObject(); + this.locked = in.readBoolean(); + this.removeOnExit=in.readBoolean(); + this.releaseLockOnExit=in.readBoolean(); + this.expiration=in.readLong(); + this.lockExpiration=in.readLong(); + } + + public String toString() { + return "key:"+this.key; + } + + +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/command/EntryMessage.java b/activemq-groups/src/main/java/org/apache/activegroups/command/EntryMessage.java new file mode 100644 index 0000000000..4343ff4e54 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/command/EntryMessage.java @@ -0,0 +1,204 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups.command; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +/** + * Used to pass information around + * + */ +public class EntryMessage implements Externalizable{ + public static enum MessageType{INSERT,DELETE,SYNC}; + private EntryKey key; + private Object value; + private Object oldValue; + private MessageType type; + private boolean mapUpdate; + private boolean expired; + private boolean lockExpired; + private boolean lockUpdate; + + /** + * @return the owner + */ + public EntryKey getKey() { + return this.key; + } + /** + * @param key + */ + public void setKey(EntryKey key) { + this.key = key; + } + /** + * @return the value + */ + public Object getValue() { + return this.value; + } + /** + * @param value the value to set + */ + public void setValue(Object value) { + this.value = value; + } + + /** + * @return the oldValue + */ + public Object getOldValue() { + return this.oldValue; + } + /** + * @param oldValue the oldValue to set + */ + public void setOldValue(Object oldValue) { + this.oldValue = oldValue; + } + + /** + * @return the type + */ + public MessageType getType() { + return this.type; + } + /** + * @param type the type to set + */ + public void setType(MessageType type) { + this.type = type; + } + + /** + * @return the mapUpdate + */ + public boolean isMapUpdate() { + return this.mapUpdate; + } + /** + * @param mapUpdate the mapUpdate to set + */ + public void setMapUpdate(boolean mapUpdate) { + this.mapUpdate = mapUpdate; + } + + /** + * @return the expired + */ + public boolean isExpired() { + return expired; + } + /** + * @param expired the expired to set + */ + public void setExpired(boolean expired) { + this.expired = expired; + } + + /** + * @return the lockExpired + */ + public boolean isLockExpired() { + return lockExpired; + } + /** + * @param lockExpired the lockExpired to set + */ + public void setLockExpired(boolean lockExpired) { + this.lockExpired = lockExpired; + } + + /** + * @return the lockUpdate + */ + public boolean isLockUpdate() { + return lockUpdate; + } + /** + * @param lockUpdate the lockUpdate to set + */ + public void setLockUpdate(boolean lockUpdate) { + this.lockUpdate = lockUpdate; + } + + /** + * @return if insert message + */ + public boolean isInsert() { + return this.type != null && this.type.equals(MessageType.INSERT); + } + + /** + * @return true if delete message + */ + public boolean isDelete() { + return this.type != null && this.type.equals(MessageType.DELETE); + } + + public boolean isSync() { + return this.type != null && this.type.equals(MessageType.SYNC); + } + + + public EntryMessage copy() { + EntryMessage result = new EntryMessage(); + result.key=this.key; + result.value=this.value; + result.oldValue=this.oldValue; + result.type=this.type; + result.mapUpdate=this.mapUpdate; + result.expired=this.expired; + result.lockExpired=this.lockExpired; + result.lockUpdate=this.lockUpdate; + return result; + } + + + + public void readExternal(ObjectInput in) throws IOException, + ClassNotFoundException { + this.key=(EntryKey) in.readObject(); + this.value=in.readObject(); + this.oldValue=in.readObject(); + this.type=(MessageType) in.readObject(); + this.mapUpdate=in.readBoolean(); + this.expired=in.readBoolean(); + this.lockExpired=in.readBoolean(); + this.lockUpdate=in.readBoolean(); + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(this.key); + out.writeObject(this.value); + out.writeObject(this.oldValue); + out.writeObject(this.type); + out.writeBoolean(this.mapUpdate); + out.writeBoolean(this.expired); + out.writeBoolean(this.lockExpired); + out.writeBoolean(this.lockUpdate); + } + + public String toString() { + return "EntryMessage: "+this.type + "[" + this.key + "," + this.value + + "]{update=" + this.mapUpdate + "}"; + } + +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/command/EntryValue.java b/activemq-groups/src/main/java/org/apache/activegroups/command/EntryValue.java new file mode 100644 index 0000000000..caa05f8ae3 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/command/EntryValue.java @@ -0,0 +1,71 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY VIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups.command; + + + +/** + * Holds information about the Value in the Map + * + */ +public class EntryValue { + private EntryKey key; + private V value; + + + public EntryValue(EntryKey key, V value){ + this.key=key; + this.value=value; + } + + /** + * @return the owner + */ + public EntryKey getKey() { + return this.key; + } + + /** + * @return the key + */ + public V getValue() { + return this.value; + } + + /** + * set the value + * @param value + */ + public void setValue(V value) { + this.value=value; + } + + public int hashCode() { + return this.value != null ? this.value.hashCode() : super.hashCode(); + } + + public boolean equals(Object obj) { + boolean result = false; + if (obj instanceof EntryValue) { + EntryValue other = (EntryValue)obj; + result = (this.value==null && other.value==null) || + (this.value != null && other.value != null && this.value.equals(other.value)); + } + return result; + } +} + diff --git a/activemq-groups/src/main/java/org/apache/activegroups/command/MapRequest.java b/activemq-groups/src/main/java/org/apache/activegroups/command/MapRequest.java new file mode 100644 index 0000000000..5f6a02e9e3 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/command/MapRequest.java @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups.command; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Return information about map update + * + */ +public class MapRequest { + private static final Log LOG = LogFactory.getLog(MapRequest.class); + private final AtomicBoolean done = new AtomicBoolean(); + private Object response; + private RequestCallback callback; + + public Object get(long timeout) { + synchronized (this.done) { + if (this.done.get() == false && this.response == null) { + try { + this.done.wait(timeout); + } catch (InterruptedException e) { + LOG.warn("Interrupted in get("+timeout+")",e); + } + } + } + return this.response; + } + + public void put(String id,Object response) { + this.response = response; + cancel(); + RequestCallback callback = this.callback; + if (callback != null) { + callback.finished(id); + } + } + + public void cancel() { + this.done.set(true); + synchronized (this.done) { + this.done.notifyAll(); + } + } + + public void setCallback(RequestCallback callback) { + this.callback=callback; + } +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/command/RequestCallback.java b/activemq-groups/src/main/java/org/apache/activegroups/command/RequestCallback.java new file mode 100644 index 0000000000..c5e5f2210a --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/command/RequestCallback.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups.command; + + +/** + * Return information about map update + * + */ +public interface RequestCallback{ + /** + * Optionally called when a request is finished + * @param id + */ + void finished(String id); +} diff --git a/activemq-groups/src/main/java/org/apache/activegroups/package.html b/activemq-groups/src/main/java/org/apache/activegroups/package.html new file mode 100755 index 0000000000..d4af9c3fd1 --- /dev/null +++ b/activemq-groups/src/main/java/org/apache/activegroups/package.html @@ -0,0 +1,25 @@ + + + + + + +Shared state, messaging and membership information between members of a distributed group + + + diff --git a/activemq-groups/src/test/eclipse-resources/log4j.properties b/activemq-groups/src/test/eclipse-resources/log4j.properties new file mode 100755 index 0000000000..f4e23ba55b --- /dev/null +++ b/activemq-groups/src/test/eclipse-resources/log4j.properties @@ -0,0 +1,37 @@ +## ------------------------------------------------------------------------ +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You under the Apache License, Version 2.0 +## (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## ------------------------------------------------------------------------ + +# +# The logging properties used for eclipse testing, We want to see debug output on the console. +# +log4j.rootLogger=INFO, out + + + +# CONSOLE appender not used by default +log4j.appender.out=org.apache.log4j.ConsoleAppender +log4j.appender.out.layout=org.apache.log4j.PatternLayout +log4j.appender.out.layout.ConversionPattern=[%30.30t] %-30.30c{1} %-5p %m%n +#log4j.appender.out.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} - %m%n + +# File appender +log4j.appender.fout=org.apache.log4j.FileAppender +log4j.appender.fout.layout=org.apache.log4j.PatternLayout +log4j.appender.fout.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} - %m%n +log4j.appender.fout.file=target/amq-testlog.log +log4j.appender.fout.append=true + diff --git a/activemq-groups/src/test/java/org/apache/activegroups/GroupMemberTest.java b/activemq-groups/src/test/java/org/apache/activegroups/GroupMemberTest.java new file mode 100644 index 0000000000..642a43b634 --- /dev/null +++ b/activemq-groups/src/test/java/org/apache/activegroups/GroupMemberTest.java @@ -0,0 +1,155 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +import java.util.ArrayList; +import java.util.List; +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import junit.framework.TestCase; +import org.apache.activemq.ActiveMQConnection; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.broker.BrokerService; + + +public class GroupMemberTest extends TestCase { + protected BrokerService broker; + protected String bindAddress = ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL; + + + public void testCoordinatorSelection() throws Exception{ + Group group = new Group(null,""); + Listlist = new ArrayList(); + final int number =10; + Member choosen = null; + for (int i =0;i< number;i++) { + Member m = new Member("group"+i); + m.setId(""+i); + if (number/2==i) { + m.setCoordinatorWeight(10); + choosen=m; + } + list.add(m); + } + Member c = group.selectCordinator(list); + assertEquals(c,choosen); + } + /** + * Test method for + * {@link org.apache.activemq.group.Group#addMemberChangedListener(org.apache.activemq.group.MemberChangedListener)}. + * @throws Exception + */ + public void testGroup() throws Exception { + + final int number = 10; + ListgroupMaps = new ArrayList(); + ConnectionFactory factory = createConnectionFactory(); + for (int i =0; i < number; i++) { + Connection connection = factory.createConnection(); + Group map = new Group(connection,"map"+i); + map.setHeartBeatInterval(200); + map.setMinimumGroupSize(i+1); + map.start(); + groupMaps.add(map); + } + + int coordinatorNumber = 0; + for (Group map:groupMaps) { + if (map.isCoordinator()) { + coordinatorNumber++; + } + } + for(Group map:groupMaps) { + map.stop(); + } + + } + +public void testWeightedGroup() throws Exception { + + final int number = 10; + ListgroupMaps = new ArrayList(); + Group last = null; + ConnectionFactory factory = createConnectionFactory(); + for (int i =0; i < number; i++) { + Connection connection = factory.createConnection(); + Group map = new Group(connection,"map"+i); + if(i ==number/2) { + map.setCoordinatorWeight(10); + last=map; + } + + map.setMinimumGroupSize(i+1); + map.start(); + groupMaps.add(map); + } + Thread.sleep(2000); + int coordinator = 0; + Group groupCoordinator = null; + for (Group map:groupMaps) { + if (map.isCoordinator()) { + coordinator++; + groupCoordinator=map; + } + } + + + assertNotNull(groupCoordinator); + assertEquals(1,coordinator); + assertEquals(last.getName(),groupCoordinator.getName()); + + for(Group map:groupMaps) { + map.stop(); + } + } + + + + protected void setUp() throws Exception { + if (broker == null) { + broker = createBroker(); + } + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + if (broker != null) { + broker.stop(); + } + } + + protected ActiveMQConnectionFactory createConnectionFactory()throws Exception { + ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory( + ActiveMQConnection.DEFAULT_BROKER_URL); + return cf; + } + + protected BrokerService createBroker() throws Exception { + BrokerService answer = new BrokerService(); + configureBroker(answer); + answer.start(); + return answer; + } + + protected void configureBroker(BrokerService answer) throws Exception { + answer.setPersistent(false); + answer.addConnector(bindAddress); + answer.setDeleteAllMessagesOnStartup(true); + } +} + diff --git a/activemq-groups/src/test/java/org/apache/activegroups/GroupMessageTest.java b/activemq-groups/src/test/java/org/apache/activegroups/GroupMessageTest.java new file mode 100644 index 0000000000..a5e6acc128 --- /dev/null +++ b/activemq-groups/src/test/java/org/apache/activegroups/GroupMessageTest.java @@ -0,0 +1,230 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import junit.framework.TestCase; +import org.apache.activemq.ActiveMQConnection; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.broker.BrokerService; + +public class GroupMessageTest extends TestCase { + protected BrokerService broker; + protected String bindAddress = ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL; + + public void testGroupBroadcast() throws Exception { + final int number = 10; + final AtomicInteger count = new AtomicInteger(); + List groups = new ArrayList(); + ConnectionFactory factory = createConnectionFactory(); + for (int i = 0; i < number; i++) { + Connection connection = factory.createConnection(); + Group group = new Group(connection, "group" + i); + group.setMinimumGroupSize(i+1); + group.start(); + groups.add(group); + group.addGroupMessageListener(new GroupMessageListener() { + public void messageDelivered(Member sender, String replyId, + Object message) { + synchronized (count) { + if (count.incrementAndGet() == number) { + count.notifyAll(); + } + } + } + }); + } + groups.get(0).broadcastMessage("hello"); + synchronized (count) { + if (count.get() < number) { + count.wait(5000); + } + } + assertEquals(number, count.get()); + for (Group map : groups) { + map.stop(); + } + } + + public void testsendMessage() throws Exception { + final int number = 10; + final AtomicInteger count = new AtomicInteger(); + List groups = new ArrayList(); + ConnectionFactory factory = createConnectionFactory(); + for (int i = 0; i < number; i++) { + Connection connection = factory.createConnection(); + Group group = new Group(connection, "group" + i); + group.setMinimumGroupSize(i+1); + group.start(); + groups.add(group); + group.addGroupMessageListener(new GroupMessageListener() { + public void messageDelivered(Member sender, String replyId, + Object message) { + synchronized (count) { + count.incrementAndGet(); + count.notifyAll(); + } + } + }); + } + groups.get(0).sendMessage("hello"); + synchronized (count) { + if (count.get() == 0) { + count.wait(5000); + } + } + // wait a while to check that only one got it + Thread.sleep(2000); + assertEquals(1, count.get()); + for (Group map : groups) { + map.stop(); + } + } + + public void testSendToSingleMember() throws Exception { + ConnectionFactory factory = createConnectionFactory(); + Connection connection1 = factory.createConnection(); + Connection connection2 = factory.createConnection(); + Group group1 = new Group(connection1, "group1"); + final AtomicBoolean called = new AtomicBoolean(); + group1.addGroupMessageListener(new GroupMessageListener() { + public void messageDelivered(Member sender, String replyId, + Object message) { + synchronized (called) { + called.set(true); + called.notifyAll(); + } + } + }); + group1.start(); + Group group2 = new Group(connection2, "group2"); + group2.setMinimumGroupSize(2); + group2.start(); + Member member1 = group2.getMemberByName("group1"); + group2.sendMessage(member1, "hello"); + synchronized (called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(called.get()); + group1.stop(); + group2.stop(); + } + + public void testSendRequestReply() throws Exception { + ConnectionFactory factory = createConnectionFactory(); + Connection connection1 = factory.createConnection(); + Connection connection2 = factory.createConnection(); + final int number = 1000; + final AtomicInteger requestCount = new AtomicInteger(); + final AtomicInteger replyCount = new AtomicInteger(); + final List requests = new ArrayList(); + final List replies = new ArrayList(); + for (int i = 0; i < number; i++) { + requests.add("request" + i); + replies.add("reply" + i); + } + final Group group1 = new Group(connection1, "group1"); + final AtomicBoolean finished = new AtomicBoolean(); + group1.addGroupMessageListener(new GroupMessageListener() { + public void messageDelivered(Member sender, String replyId, + Object message) { + if (!replies.isEmpty()) { + String reply = replies.remove(0); + try { + group1.sendMessageResponse(sender, replyId, reply); + } catch (JMSException e) { + e.printStackTrace(); + } + } + } + }); + group1.start(); + final Group group2 = new Group(connection2, "group2"); + group2.setMinimumGroupSize(2); + group2.addGroupMessageListener(new GroupMessageListener() { + public void messageDelivered(Member sender, String replyId, + Object message) { + if (!requests.isEmpty()) { + String request = requests.remove(0); + try { + group2.sendMessage(sender, request); + } catch (JMSException e) { + e.printStackTrace(); + } + }else { + synchronized (finished) { + finished.set(true); + finished.notifyAll(); + } + } + } + }); + group2.start(); + Member member1 = group2.getMemberByName("group1"); + group2.sendMessage(member1, requests.remove(0)); + synchronized (finished) { + if (!finished.get()) { + finished.wait(10000); + } + } + assertTrue(finished.get()); + group1.stop(); + group2.stop(); + } + + protected void setUp() throws Exception { + if (broker == null) { + broker = createBroker(); + } + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + if (broker != null) { + broker.stop(); + } + } + + protected ActiveMQConnectionFactory createConnectionFactory() + throws Exception { + ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory( + ActiveMQConnection.DEFAULT_BROKER_URL); + return cf; + } + + protected BrokerService createBroker() throws Exception { + BrokerService answer = new BrokerService(); + configureBroker(answer); + answer.start(); + return answer; + } + + protected void configureBroker(BrokerService answer) throws Exception { + answer.setPersistent(false); + answer.addConnector(bindAddress); + answer.setDeleteAllMessagesOnStartup(true); + } +} diff --git a/activemq-groups/src/test/java/org/apache/activegroups/GroupStateTest.java b/activemq-groups/src/test/java/org/apache/activegroups/GroupStateTest.java new file mode 100644 index 0000000000..12a96313a7 --- /dev/null +++ b/activemq-groups/src/test/java/org/apache/activegroups/GroupStateTest.java @@ -0,0 +1,548 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activegroups; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import junit.framework.TestCase; +import org.apache.activemq.ActiveMQConnection; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.broker.BrokerService; + + +public class GroupStateTest extends TestCase { + protected BrokerService broker; + protected Connection connection1; + protected Connection connection2; + protected String bindAddress = ActiveMQConnectionFactory.DEFAULT_BROKER_BIND_URL; + + /** + * Test method for + * {@link org.apache.activemq.group.Group#addMemberChangedListener(org.apache.activemq.group.MemberChangedListener)}. + * @throws Exception + */ + public void testAddMemberChangedListener() throws Exception { + final AtomicInteger counter = new AtomicInteger(); + Group map1 = new Group(connection1,"map1"); + map1.addMemberChangedListener(new MemberChangedListener(){ + + public void memberStarted(Member member) { + synchronized(counter) { + counter.incrementAndGet(); + counter.notifyAll(); + } + + } + + public void memberStopped(Member member) { + synchronized(counter) { + counter.decrementAndGet(); + counter.notifyAll(); + } + } + + }); + map1.start(); + synchronized(counter) { + if (counter.get()<1) { + counter.wait(5000); + } + } + assertEquals(1, counter.get()); + Group map2 = new Group(connection2,"map2"); + map2.start(); + synchronized(counter) { + if (counter.get()<2) { + counter.wait(5000); + } + } + assertEquals(2, counter.get()); + map2.stop(); + synchronized(counter) { + if (counter.get()>=2) { + counter.wait(Group.DEFAULT_HEART_BEAT_INTERVAL*3); + } + } + assertEquals(1, counter.get()); + map1.stop(); + } + + /** + * Test method for + * {@link org.apache.activemq.group.Group#addMapChangedListener(org.apache.activemq.group.MapChangedListener)}. + * @throws Exception + */ + public void testAddMapChangedListener() throws Exception { + final AtomicBoolean called1 = new AtomicBoolean(); + final AtomicBoolean called2 = new AtomicBoolean(); + + Group map1 = new Group(connection1,"map1"); + + map1.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called1) { + called1.set(true); + called1.notifyAll(); + } + } + }); + map1.start(); + + Group map2 = new Group(connection2,"map2"); + + map2.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called2) { + called2.set(true); + called2.notifyAll(); + } + } + }); + map2.start(); + + + map1.put("test", "blob"); + synchronized(called1) { + if (!called1.get()) { + called1.wait(5000); + } + } + synchronized(called2) { + if (!called2.get()) { + called2.wait(5000); + } + } + assertTrue(called1.get()); + assertTrue(called2.get()); + map1.stop(); + map2.stop(); + } + + public void testGetImplicitWriteLock() throws Exception { + Group map1 = new Group(connection1, "map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.start(); + Group map2 = new Group(connection2, "map2"); + map2.setAlwaysLock(true); + map2.setMinimumGroupSize(2); + map2.start(); + map2.put("test", "foo"); + try { + map1.put("test", "bah"); + fail("Should have thrown an exception!"); + } catch (GroupUpdateException e) { + } + map1.stop(); + map2.stop(); + } + + public void testExpireImplicitWriteLock() throws Exception { + Group map1 = new Group(connection1, "map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.start(); + Group map2 = new Group(connection2, "map2"); + map2.setAlwaysLock(true); + map2.setLockTimeToLive(1000); + map2.setMinimumGroupSize(2); + map2.start(); + map2.put("test", "foo"); + try { + map1.put("test", "bah"); + fail("Should have thrown an exception!"); + } catch (GroupUpdateException e) { + } + Thread.sleep(2000); + map1.put("test", "bah"); + map1.stop(); + map2.stop(); + } + + public void XtestExpireImplicitLockOnExit() throws Exception { + Group map1 = new Group(connection1, "map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.start(); + Group map2 = new Group(connection2, "map2"); + map2.setAlwaysLock(true); + map2.setMinimumGroupSize(2); + map2.start(); + map2.put("test", "foo"); + try { + map1.put("test", "bah"); + fail("Should have thrown an exception!"); + } catch (GroupUpdateException e) { + } + map2.stop(); + map1.put("test", "bah"); + map1.stop(); + + } + + public void testGetExplicitWriteLock() throws Exception { + Group map1 = new Group(connection1, "map1"); + map1.setAlwaysLock(true); + final AtomicBoolean called = new AtomicBoolean(); + map1.start(); + Group map2 = new Group(connection2, "map2"); + map2.setAlwaysLock(true); + map2.setMinimumGroupSize(2); + map2.start(); + map2.put("test", "foo"); + map2.lock("test"); + try { + map1.put("test", "bah"); + fail("Should have thrown an exception!"); + } catch (GroupUpdateException e) { + } + map2.unlock("test"); + map1.lock("test"); + try { + map2.lock("test"); + fail("Should have thrown an exception!"); + } catch (GroupUpdateException e) { + } + map1.stop(); + map2.stop(); + } + + + + /** + * Test method for {@link org.apache.activemq.group.Group#clear()}. + * + * @throws Exception + */ + public void testClear() throws Exception { + Group map1 = new Group(connection1,"map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + + public void mapRemove(Member owner, Object key, Object value,boolean expired) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + }); + map1.start(); + Group map2 = new Group(connection2,"map2"); + map2.start(); + map2.put("test","foo"); + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(called.get()); + called.set(false); + assertTrue(map1.isEmpty()==false); + map2.clear(); + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(map1.isEmpty()); + map1.stop(); + map2.stop(); + } + + /** + * Test a new map is populated for existing values + */ + public void testMapUpdatedOnStart() throws Exception { + Group map1 = new Group(connection1,"map1"); + final AtomicBoolean called = new AtomicBoolean(); + + map1.start(); + map1.put("test", "foo"); + Group map2 = new Group(connection2,"map2"); + map2.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + }); + map2.start(); + + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(called.get()); + called.set(false); + assertTrue(map2.containsKey("test")); + assertTrue(map2.containsValue("foo")); + map1.stop(); + map2.stop(); + } + + public void testContainsKey() throws Exception { + Group map1 = new Group(connection1,"map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + }); + map1.start(); + Group map2 = new Group(connection2,"map2"); + map2.start(); + map2.put("test","foo"); + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(called.get()); + called.set(false); + assertTrue(map1.containsKey("test")); + map1.stop(); + map2.stop(); + } + + + /** + * Test method for + * {@link org.apache.activemq.group.Group#containsValue(java.lang.Object)}. + * @throws Exception + */ + public void testContainsValue() throws Exception { + Group map1 = new Group(connection1,"map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + }); + map1.start(); + Group map2 = new Group(connection2,"map2"); + map2.start(); + map2.put("test","foo"); + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(called.get()); + called.set(false); + assertTrue(map1.containsValue("foo")); + map1.stop(); + map2.stop(); + } + + /** + * Test method for {@link org.apache.activemq.group.GroupMap#entrySet()}. + * @throws Exception + */ + + + /** + * Test method for + * {@link org.apache.activemq.group.Group#get(java.lang.Object)}. + * @throws Exception + */ + public void testGet() throws Exception { + Group map1 = new Group(connection1,"map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + }); + map1.start(); + Group map2 = new Group(connection2,"map2"); + map2.start(); + map2.put("test","foo"); + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(called.get()); + assertTrue(map1.get("test").equals("foo")); + map1.stop(); + map2.stop(); + } + + public void testPut() throws Exception { + Group map1 = new Group(connection1,"map1"); + map1.start(); + Group map2 = new Group(connection2,"map2"); + map2.setMinimumGroupSize(2); + map2.start(); + Object value = map1.put("foo", "blob"); + assertNull(value); + value = map1.put("foo", "blah"); + assertEquals(value, "blob"); + map1.stop(); + map2.stop(); + } + + + + /** + * Test method for + * {@link org.apache.activemq.group.Group#remove(java.lang.Object)}. + */ + public void testRemove() throws Exception{ + Group map1 = new Group(connection1,"map1"); + final AtomicBoolean called = new AtomicBoolean(); + map1.addMapChangedListener(new DefaultMapChangedListener() { + public void mapInsert(Member owner,Object Key, Object Value) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + + public void mapRemove(Member owner, Object key, Object value,boolean expired) { + synchronized(called) { + called.set(true); + called.notifyAll(); + } + } + }); + map1.start(); + Group map2 = new Group(connection2,"map2"); + map2.start(); + map2.put("test","foo"); + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(called.get()); + called.set(false); + assertTrue(map1.isEmpty()==false); + map2.remove("test"); + synchronized(called) { + if (!called.get()) { + called.wait(5000); + } + } + assertTrue(map1.isEmpty()); + + map1.stop(); + map2.stop(); + } + + public void testExpire() throws Exception{ + final AtomicBoolean called1 = new AtomicBoolean(); + final AtomicBoolean called2 = new AtomicBoolean(); + + Group map1 = new Group(connection1,"map1"); + map1.setTimeToLive(1000); + map1.addMapChangedListener(new DefaultMapChangedListener() { + public void mapRemove(Member owner, Object key, Object value,boolean expired) { + synchronized(called1) { + called1.set(expired); + called1.notifyAll(); + } + } + }); + map1.start(); + + Group map2 = new Group(connection2,"map2"); + + map2.addMapChangedListener(new DefaultMapChangedListener() { + public void mapRemove(Member owner, Object key, Object value,boolean expired) { + synchronized(called2) { + called2.set(expired); + called2.notifyAll(); + } + } + }); + map2.start(); + + + map1.put("test", "blob"); + synchronized(called1) { + if (!called1.get()) { + called1.wait(5000); + } + } + synchronized(called2) { + if (!called2.get()) { + called2.wait(5000); + } + } + assertTrue(called1.get()); + assertTrue(called2.get()); + map1.stop(); + map2.stop(); + } + + protected void setUp() throws Exception { + if (broker == null) { + broker = createBroker(); + } + ConnectionFactory factory = createConnectionFactory(); + connection1 = factory.createConnection(); + connection1.start(); + connection2 = factory.createConnection(); + connection2.start(); + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + connection1.close(); + connection2.close(); + if (broker != null) { + broker.stop(); + } + } + + protected ActiveMQConnectionFactory createConnectionFactory()throws Exception { + ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory( + ActiveMQConnection.DEFAULT_BROKER_URL); + return cf; + } + + protected BrokerService createBroker() throws Exception { + BrokerService answer = new BrokerService(); + configureBroker(answer); + answer.start(); + return answer; + } + + protected void configureBroker(BrokerService answer) throws Exception { + answer.setPersistent(false); + answer.addConnector(bindAddress); + answer.setDeleteAllMessagesOnStartup(true); + } +}