blob: 44fae2358629620035b5074d98bd470b9096a94f [file] [log] [blame]
<div class="wiki-content maincontent"><h2 id="MessageGroups-MessageGroups">Message Groups</h2><p>Message Groups are an enhancement to the <a shape="rect" href="exclusive-consumer.xml">Exclusive Consumer</a> feature. They provide:</p><ul><li>Guaranteed ordering of the processing of related messages across a single queue.</li><li>Load balancing of the processing of messages across multiple consumers.</li><li>High availability / auto-failover to other consumers if a JVM goes down.</li></ul><p>So logically Message Groups are like a parallel <a shape="rect" href="exclusive-consumer.xml">Exclusive Consumer</a>. Rather than all messages going to a single consumer, the standard JMS header&#160;<strong><code>JMSXGroupID</code></strong> is used to define which <em>message group</em> the message belongs to. The Message Group feature then ensures that all messages for the <em>same</em> message group will be sent to the <em>same</em> JMS consumer - whilst that consumer stays alive. As soon as the consumer dies another will be chosen.</p><p>Another way of explaining Message Groups is that it provides sticky load balancing of messages across consumers; where the&#160;<strong><code>JMSXGroupID</code></strong> is kinda like a HTTP session ID or cookie value and the message broker is acting like a HTTP load balancer.</p><h3 id="MessageGroups-ExampleUseCase">Example Use Case</h3><p>Lets say we are doing some kind of order matching system where people are buying and selling things (stocks, shares, placing online bets, whatever). You want to have consumers who match bids and offers for different items (stocks / bets) so they want to keep in RAM for performance a sub-set of the data set. Therefore set the&#160;<strong><code>JMSXGroupID</code></strong> to be <strong><code>MSFT</code></strong>, <strong><code>IBM</code></strong>,&#160;<strong><code>SUNW</code></strong> and so forth to use the stock symbol to define the message group. (It can be any string whatsoever; maybe combining trading book, trading exchange, date and so forth - the more specific the group ID, the more concurrent you can run). Assume we are buying and selling <strong><code>MSFT</code>, <code>IBM</code>,&#160;<code>SUNW</code></strong> shares; the Message Groups feature guarantees that all the&#160;<strong><code>MSFT</code></strong> messages will be processed in order by the same consumer; ditto for&#160;<strong><code>IBM</code></strong> and <strong><code>SUNW</code></strong>.</p><h3 id="MessageGroups-HowMessageGroupsWork">How Message Groups Work</h3><p>When a message is being dispatched to a consumer, the&#160;<strong><code>JMSXGroupID</code></strong> is checked. If one is present then the broker checks to see if a consumer owns that message group. Since there could be a large number of message groups hash buckets are used rather than the actual&#160;<strong><code>JMSXGroupID</code></strong> string.</p><p>If no consumer is associated with a message group a consumer is chosen. Said JMS&#160;<strong><code>MessageConsumer</code></strong> will receive all further messages with the same&#160;<strong><code>JMSXGroupID</code></strong> value until:</p><ul><li>the consumer closes (or the client which created the consumer dies etc).</li><li>someone closes the message group by sending a message with a negative value for&#160;<strong><code>JMSXGroupSeq</code></strong> (see below for more details).</li></ul><p><strong>Note</strong>: as with message selector matching, grouping based on&#160;<strong><code>JMSXGroupID</code></strong> occurs before dispatch on messages in memory. With the default&#160;<strong><code>maxPageSize</code></strong> option, large backlogs of messages destined for one group can block receipt of messages to other groups if they don't all fit in memory. You can change the default&#160;<strong><code>maxPageSize</code></strong> setting for destinations as follows:</p><div class="code panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
<script class="brush: java; gutter: false; theme: Default" type="syntaxhighlighter"><![CDATA[&lt;destinationPolicy&gt;
&lt;policyMap&gt;
&lt;policyEntries&gt;
&lt;policyEntry queue=&quot;&gt;&quot; maxPageSize=&quot;1000&quot;/&gt;
&lt;/policyEntries&gt;
&lt;/policyMap&gt;
&lt;/destinationPolicy&gt;]]></script>
</div></div><h3 id="MessageGroups-UsingMessageGroups">Using Message Groups</h3><p>You just need to change your JMS producers to fill in the&#160;<strong><code>JMSXGroupID</code></strong> message header with some&#160;<strong><code>String</code></strong> value of your choice.</p><p>Example:</p><div class="code panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
<script class="brush: java; gutter: false; theme: Default" type="syntaxhighlighter"><![CDATA[Mesasge message = session.createTextMessage(&quot;&lt;foo&gt;hey&lt;/foo&gt;&quot;);
message.setStringProperty(&quot;JMSXGroupID&quot;, &quot;IBM_NASDAQ_20/4/05&quot;);
...
producer.send(message);
]]></script>
</div></div><h3 id="MessageGroups-ClosingaMessageGroup">Closing a Message Group</h3><p>You generally don't need to close a message group; just keep using it. However if you really do want to close a group you can add a negative sequence number.</p><p>Example:</p><div class="code panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
<script class="brush: java; gutter: false; theme: Default" type="syntaxhighlighter"><![CDATA[Mesasge message = session.createTextMessage(&quot;&lt;foo&gt;hey&lt;/foo&gt;&quot;);
message.setStringProperty(&quot;JMSXGroupID&quot;, &quot;IBM_NASDAQ_20/4/05&quot;);
message.setIntProperty(&quot;JMSXGroupSeq&quot;, -1);
...
producer.send(message);
]]></script>
</div></div><p>This then <em>closes</em> the message group so if another message is sent in the future with the same message group ID it will be reassigned to a new consumer.</p><h3 id="MessageGroups-Implications">Implications</h3><p>Message Groups mean you get the power of <strong>grid</strong> processing of messages across a cluster of consumers with reliability, auto-failover, load balancing but you can also order the processing of messages too. So its the best of both worlds. However using the above example, what Message Groups actually do is to partition your work load across consumers using a user definable partition strategy - the&#160;<strong><code>JMSXGroupID</code></strong> value.</p><p>The neat thing about this is that you can do neat things like use lots of RAM caching; keep the order for&#160;<strong><code>MSFT</code></strong> in RAM in the&#160;<strong><code>MSFT</code></strong> consumer; keep the&#160;<strong><code>IBM</code></strong> orders in RAM in the&#160;<strong><code>IBM</code></strong> consumer - since the message broker is partitioning for you, you do not have to rely on a distributed cache with inter-cache synchronization and locking to take advantage of caching.</p><p>The great thing is - to the application developer, it looks like a simple 1 consumer world where you process messages and do your job; leaving the broker to do all the hard stuff for you</p><ul><li>partitioning the traffic</li><li>load balancing of message groups across consumers</li><li>auto-failover of groups to different consumers as consumers come and go</li></ul><p>In summary; if ordering or per-message caching and synchronization are in any way important to you then we highly recommend you use message groups to partition your traffic.</p><h3 id="MessageGroups-GettingNotifiedofOwnershipChangesofMessageGroups">Getting Notified of Ownership Changes of Message Groups</h3><p>ActiveMQ support a boolean header called&#160;<strong><code>JMSXGroupFirstForConsumer</code></strong>. This header is set on the first message sent to a consumer for a particular message group.</p><p>If the JMS connection is using&#160;<strong><code>failover:</code></strong> and a temporary network error occurs so that the connection disconnects from the broker and reconnects some time later, a new consumer instance will be created under the covers of the JMS client leading to the possibility of another message with this header being set for the same message group.</p><p>Example:</p><div class="code panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
<script class="brush: java; gutter: false; theme: Default" type="syntaxhighlighter"><![CDATA[String groupId = message.getStringProperty(&quot;JMSXGroupId&quot;);
if (message.getBooleanProperty(&quot;JMSXGroupFirstForConsumer&quot;)) {
// flush cache for groupId
}
]]></script>
</div></div><p>To flush caches to ensure consistent state when faced with network errors.</p><h3 id="MessageGroups-AddingNewConsumers">Adding New Consumers</h3><p>If you have existing messages in the broker and add consumers at a later stage, it is a good idea to delay message dispatch start until all consumers are present (or at least to give enough time for them to subscribe). If you don't do that the first consumer will probably acquire all message groups and all messages will be dispatched to it. You can achieve this by using <strong><code>consumersBeforeDispatchStarts</code></strong> and <strong><code>timeBeforeDispatchStarts</code></strong> <a shape="rect" href="per-destination-policies.xml">destination policies</a>.</p><p>When both <strong>consumersBeforeDispatchStarts</strong> and <strong>timeBeforeDispatchStarts</strong> are set to a value greater than zero, the dispatching will start as soon as the required number of consumers are present or the timeBeforeDispatchStarts timeout expires. If only consumersBeforeDispatchStarts is set then the timeout for consumers to connect is 1 second. If all consumers disconnect then message dispatch delay will be applied again at the next consumer connection.</p><p>&#160;</p><p>Here's the example of the destination policy that delays dispatch for&#160;<strong><code>200ms</code></strong>:</p><div class="code panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
<script class="brush: java; gutter: false; theme: Default" type="syntaxhighlighter"><![CDATA[&lt;destinationPolicy&gt;
&lt;policyMap&gt;
&lt;policyEntries&gt;
&lt;policyEntry queue=&quot;&gt;&quot; timeBeforeDispatchStarts=&quot;200&quot;/&gt;
&lt;/policyEntries&gt;
&lt;/policyMap&gt;
&lt;/destinationPolicy&gt;
]]></script>
</div></div><p>The following code snippet shows how to wait for two consumers (or two seconds) before dispatch starts:</p><div class="code panel pdl" style="border-width: 1px;"><div class="codeContent panelContent pdl">
<script class="brush: xml; gutter: false; theme: Default" type="syntaxhighlighter"><![CDATA[&lt;destinationPolicy&gt;
&lt;policyMap&gt;
&lt;policyEntries&gt;
&lt;policyEntry queue=&quot;&gt;&quot; consumersBeforeDispatchStarts=&quot;2&quot; timeBeforeDispatchStarts=&quot;2000&quot;/&gt;
&lt;/policyEntries&gt;
&lt;/policyMap&gt;
&lt;/destinationPolicy&gt;
]]></script>
</div></div><p>As <a shape="rect" class="external-link" href="https://github.com/apache/activemq/blob/master/activemq-unit-tests/src/test/java/org/apache/activemq/usecases/MessageGroupDelayedTest.java" rel="nofollow">the appropriate test case</a> shows, adding a small time pause before dispatching or setting a minimum consumer number, ensures equal message group distribution.</p><p>&#160;</p><h3 id="MessageGroups-Competingdemandsofmemoryconsumption,loadbalancing,complexity,etc."><span style="color: rgb(33,33,33);">Competing demands of memory consumption, load balancing, complexity, etc.</span></h3><p>The default behavior which is limited to 1024 message groups in an LRU cache may not match you expectation w.r.t message order... some detail to explain:</p><p>MessageGroupHashBucket and SimpleMessageGroupMap message groups work by associating each group with a consumer.</p><p>SimpleMessageGroupMap keeps track of every group but suffers from unbounded memory use.</p><p>MessageGroupHashBucked keeps track of every group and has bounded memory use.</p><p>CachedMessageGroupMap has bounded memory use, but only keeps track of up to 1024 (or the maximum configured size) groups, then loses track of any groups older than the newest 1024.</p><p>In this way, if there are more groups than the maximum, <strong>ordering will be lost for the oldest groups</strong>.</p><p>Typically users would close groups such that the in memory set can be retained below the configured limits. Some usefull discussion at&#160;[
<span class="jira-issue resolved">
<a shape="rect" class="jira-issue-key" href="https://issues.apache.org/jira/browse/AMQ-6851?src=confmacro"><img class="icon" src="https://issues.apache.org/jira/images/icons/issuetypes/bug.png">AMQ-6851</a>
-
<span class="summary">Messages using Message Groups can arrive out of order when using CachedMessageGroupMap</span>
<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-success jira-macro-single-issue-export-pdf">Resolved</span>
</span>
]</p><h3 id="MessageGroups-See">See</h3><ul><li><a shape="rect" href="how-do-message-groups-compare-to-selectors.xml">How do Message Groups compare to Selectors</a></li></ul></div>