According to the spec, option (b) is the correct behavior. If you are getting option (a) and rely on it, the application will not be portable.
In the explanation below, I'm referring specifically to the JMS spec Version 1.1 April 12, 2002.
There is some confusion caused by the fact that the acknowledge method is called from the message object when in fact it operates at the session level. Because it is a message method, intuitively it seems correct that one could pick a point in the message stream at which to generate an acknowledgment.
What is really happening though is that message acknowledgment is driving commit calls at the session level. Since a session can have only one transaction active at a time, each acknowledgment delimits not a point in the message stream but rather a point in time. The ack commits the existing unit of work and starts the next. Messages delivered prior to the ack must be included in the unit of work that was committed by the ack.
The term "delivered" is generally thought of as completion of the API call that removes the message from the queue and results in a populated object in the program's memory. In reality, the message is considered delivered when it is removed from the queue, regardless of whether it makes it to the program. For example, consider the following sequence of events:
- The app requests a message.
- The request is passed over a TCP socket to a process on the server which acts as a proxy for the application.
- The proxy issues the GET against the queue.
- The message is locked in a unit of work and passed to the proxy process.
- The proxy process attempts to deliver the message over a TCP socket to the calling application. If the connection is severed at this point, the application will not have ever seen the message but the JMS provider thinks it has been delivered. When the broken connection is detected, the unit of work is canceled, the message is rolled back onto the queue and the redelivery count is incremented.
- The application receives the message.
- The application acknowledges the message.
- The proxy process receives the commit call and executes it.
There is a window between 6 & 8 during which the TCP socket can be disconnected. On the JMS provider side, it can't really distinguish between this and failure at Step 5. Either way, the message is rolled back and redelivered later. However in this case the application will see the message twice. The spec anticipates this situation in 4.4.13 where it states:
If a failure occurs between the time a client commits its work on a Session and
the commit method returns, the client cannot determine if the transaction was
committed or rolled back. The same ambiguity exists when a failure occurs
between the non-transactional send of a PERSISTENT message and the return
from the sending method.
It is up to a JMS application to deal with this ambiguity. In some cases, this
may cause a client to produce functionally duplicate messages.
A message that is redelivered due to session recovery is not considered a
duplicate message.
In your example where you "call consumer.receive() 6 times, and then call .acknowledge on the 3rd message" and are observing that messages 4 through 6 are redelivered, the possible explanations are that a) the six messages are not all from the same session, or b) the behavior is not compliant with the spec.