Marco Geri created QPID-8751:
--------------------------------

             Summary: [Broker-J] AMQP 1.0 consumer is never resumed when the 
session incoming window is reopened by a flow naming another link
                 Key: QPID-8751
                 URL: https://issues.apache.org/jira/browse/QPID-8751
             Project: Qpid
          Issue Type: Bug
          Components: Broker-J
    Affects Versions: qpid-java-broker-10.1.0
         Environment: Reproduced against Broker-J 10.1.0. The affected code is 
unchanged on {{main}} and on the
6.1.x, 7.1.x and 8.0.x branches.
            Reporter: Marco Geri
         Attachments: QPID-XXXX-proposed-fix.patch, 
QPID-XXXX-reproducer-test.patch

h2. Problem

On an AMQP 1.0 session with two or more sending links, a consumer suspended 
because
the session incoming window was exhausted is never resumed. The link keeps its 
credit
and stays attached, but the queue never serves it again for the life of the 
connection, and SUB-1003 is logged for it without bound. Only reconnecting 
clears it.
h2. Reproduce

One session, two consumers A and B on the same queue:
|| ||Event||A||B||
|1|both links are granted link credit|sendable|sendable|
|2|the broker fills the session incoming window sending transfers, and each 
consumer is suspended the next time the queue tries to deliver to it, because 
{{ConsumerTarget_1_0#allocateCredit}} refuses|suspended|suspended|
|3|the peer settles a delivery on B and replenishes the window with {{{}flow 
{handle=B{}}}}|*still suspended*|sendable|

After step 3 the session can send again and A still holds link credit on an 
attached
link, but nothing ever re-evaluates A.

Two or more links on the same session are required: with a single link the
replenishing flow necessarily names it, so it recovers.
h2. Cause

{{Session_1_0#receiveFlow}} recomputes the session scoped 
{{_remoteIncomingWindow }}unconditionally, but notifies at most one link - the 
one named by the flow's optional {{{}handle{}}}:
{code:java}
_remoteIncomingWindow = ...;                        // session scoped
...
if (handle != null) { endpoint.receiveFlow(flow); } // only this link is 
re-evaluated
else                { /* all links */ }
{code}
That value feeds *every* sending link's sendability, through 
{{SendingLinkEndpoint#hasCreditToSend()}} and 
{{{}ConsumerTarget_1_0#updateNotifyWorkDesired(){}}}.
A session scoped change is therefore delivered as a link scoped notification, 
and a
link not named by the flow is never told the session can send again.

There is no second chance: a suspended consumer is offered no message, so its 
own link
credit never changes, and a well behaved peer has no reason to send a flow 
naming its
handle.
h2. Why the handle-less branch does not save us

The {{handle}} field is optional (2.7.4), but both Qpid Java clients always set 
it on
the frame that replenishes the session window, so that branch is effectively 
dead:
 * protonj2: {{ProtonSessionIncomingWindow#deliveryRead}} -> 
{{ProtonSession#writeFlow(link)}}
-> {{ProtonReceiver#decorateOutgoingFlow}} -> {{Flow#setHandle}}
 * proton-j: {{TransportImpl#processSenderFlow}} writes a per receiver flow 
carrying that
receiver's handle whenever the incoming window is zero

The first link to replenish the window is resumed; the others are stranded.
h2. Attachments

*{{QPID-XXXX-reproducer-test.patch}}* - a unit test for 
{{{}Session_1_0Test{}}}. Applied on
its own to {{main}} it fails with:
{noformat}
org.opentest4j.AssertionFailedError: Consumer A should be resumed once the 
session
window is reopened by a flow naming another link ==> expected: <true> but was: 
<false>
{noformat}
It closes the window with an explicit flow carrying {{incoming-window=0}} 
rather than by
sending transfers. Both paths end with {{{}_remoteIncomingWindow == 0{}}}, and 
the flow form keeps the test free of message delivery.

*{{QPID-XXXX-proposed-fix.patch}}* - a possible fix, offered as a starting 
point rather
than a finished change. It sweeps the remaining link endpoints when session 
sendability
transitions from false to true, mirroring what {{transportStateChanged}} 
already does for the connection scoped condition. Only the resume direction 
needs the sweep, since losing session credit is already handled accurately by 
{{allocateCredit}} refusing the next
delivery attempt. With both patches applied, {{Session_1_0Test}} passes on 
{{{}main{}}}.

This one would benefit from review by a maintainer familiar with the link and 
session
flow control code. Two points I am unsure about:
 * whether the sweep should be restricted to {{{}SendingLinkEndpoint{}}}, as
{{transportStateChanged}} does - 
{{AbstractReceivingLinkEndpoint#flowStateChanged}} is a
no-op and {{ErrantLinkEndpoint#flowStateChanged}} throws
 * whether the transition should be evaluated before 
{{endpoint.receiveFlow(flow)}} rather
than after, since that call can consume the freshly granted window via
{{_resumeAcceptedTransfers}}

 



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to