[ 
https://issues.apache.org/jira/browse/QPID-8757?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18114915#comment-18114915
 ] 

Marco Geri commented on QPID-8757:
----------------------------------

Hi Daniil,

Apologies for the delay. I've now run the combined patch through our testing 
and everything passes: the websocket unit tests and system tests. No failures 
anywhere.

Congratulations on this. It's a much larger piece of work than what I reported, 
and I think it's better for it. Removing the connection monitor entirely is 
what convinced me: with the protocol lock separated from the write queue and 
writes completing through a callback, the deflate no longer runs anywhere the 
checker can observe, so the cause is gone rather than bounded. And making the 
close deadline absolute from the moment close() starts is a better answer than 
the idle timeout I suggested, since a peer that keeps sending can't postpone it.

One small thing I'd like to offer back. WebSocketCloseTimeoutTest covers the 
unanswered closing handshake on an idle, uncompressed connection. I added the 
same test with permessage-deflate negotiated and output still queued when the 
close begins, since that is what our deployment actually had: credit is granted 
for a queue of messages and never consumed, so the broker is pushing compressed 
deliveries when the close arrives and still has output behind the CLOSE it is 
about to send. The peer withholds both the AMQP CLOSE response and the 
WebSocket CLOSE response, with half closure enabled so the writable half of the 
socket survives.

It passes. To check it was worth having, I made getRemainingCloseTime() return 
Long.MAX_VALUE, the behaviour before your change, and the test then hangs and 
fails after thirty seconds with the connection still registered.

I've attached it as {{QPID-8757-compressed-close-test.patch}}: one test class, 
no production changes, applying on top of the combined patch. Take it or drop 
it as you prefer.

One question, and I may well have missed it: is there a test that starts the 
scheduler thread itself? The existing ones call scheduleDueConnections() 
directly, so the wait and wakeup path isn't exercised. It might be worth adding 
one, since an idle scheduler never rescans and that path only really shows 
itself under the wakeup traffic of a busy broker.

On our side we've had the patch running in our environment for a few days now 
with no issues. Between that and the testing above, as far as I'm concerned it 
fixes the crash we reported, and this is clearly a better handling of the whole 
area than the previous implementation.

Thanks again for all the work on this.

Marco

> [Broker-J] WebSocket idle checker queues unbounded tick jobs while a 
> connection is writing, exhausting the broker heap
> ----------------------------------------------------------------------------------------------------------------------
>
>                 Key: QPID-8757
>                 URL: https://issues.apache.org/jira/browse/QPID-8757
>             Project: Qpid
>          Issue Type: Bug
>          Components: Broker-J
>    Affects Versions: qpid-java-broker-10.1.0
>            Reporter: Marco Geri
>            Assignee: Daniil Kirilyuk
>            Priority: Major
>             Fix For: qpid-java-broker-10.1.1
>
>         Attachments: IdleCheckerBenchmark.java, 
> QPID-8757-compressed-close-test.patch, QPID-8757.diff, 
> QPID-8757_QPID-8758_combined.patch, QPID-websocket-idle-checker.patch, 
> jmh-results.txt
>
>
> We hit this on a 10.x broker while moving a client onto AMQP over WebSocket 
> with {{{}permessage-deflate{}}}, to help a user on a slow link. The broker 
> died partway through a bulk read:
> {noformat}
> Unhandled Exception java.lang.OutOfMemoryError: Java heap space in Thread 
> WebSocket Idle Checker: null
> Exiting
> java.lang.OutOfMemoryError: Java heap space
> at 
> org.eclipse.jetty.util.BlockingArrayQueue.lockedGrow(BlockingArrayQueue.java:803)
> at 
> org.eclipse.jetty.util.BlockingArrayQueue.offer(BlockingArrayQueue.java:429)
> at 
> org.eclipse.jetty.util.thread.QueuedThreadPool.execute(QueuedThreadPool.java:820)
> at 
> org.apache.qpid.server.transport.websocket.WebSocketProvider$ConnectionWrapper.tick(WebSocketProvider.java:701)
> at 
> org.apache.qpid.server.transport.websocket.WebSocketProvider$WebSocketIdleTimeoutChecker.run(WebSocketProvider.java:756)
> {noformat}
> What caught our attention is where the memory went. The allocation that 
> failed is the thread pool's own task queue growing, not a message or a 
> connection, so something was queueing work faster than the pool could run it, 
> and for long enough to fill the heap. We went looking for the producer.
> h2. What we think is happening
> The idle checker reads a connection's ticker without holding that 
> connection's monitor, but the only thing that advances the ticker holds it. 
> That is {{{}_tickJob{}}}, around line 512:
> {code:java}
> _tickJob = () ->
> {
>   synchronized (ConnectionWrapper.this)
>   {
>     protocolEngine.getAggregateTicker().tick(System.currentTimeMillis());
>     doWrite();
>   }
> };
> {code}
> {{tick()}} at line 699 hands that job to the pool, and there is nothing to 
> stop it handing over the same job again while the first is still waiting to 
> run. 
> {{_tickJob}} is one shared instance, so queueing it a thousand times runs it 
> a thousand times:
> {code:java}
> public void tick()
> {
>    _threadPool.execute(_tickJob);
> }
> {code}
> And in {{WebSocketIdleTimeoutChecker.run()}} at line 707, a due tick means 
> the loop does not wait at all before coming back round:
> {code:java}
> long tick = ticker.getTimeToNextTick(currentTime);
> if(tick <= 0)
> {
>   connectionToTick = connection;
>   nextTick = -1;
>   break;
> }
> ...
> if(nextTick > 0) // nextTick is -1 here, so no wait happens
> {
>   wait(nextTick);
> }
> ...
> if(connectionToTick != null)
> {
>   connectionToTick.tick();
> }
> {code}
> Put together: while the monitor is held, the ticker stays overdue, so the 
> checker spins and queues one more job on every pass. Nothing bounds that 
> except how long the monitor stays held.
> It does not take anything unusual to hold it. {{doWrite()}} at line 642 and 
> {{doWork()}} at line 675 are both {{synchronized}} on the connection, and 
> {{doWrite()}} allocates an array the size of everything pending, copies it 
> all in, and calls {{Session.sendBinary}} without letting go.
> With {{permessage-deflate}} negotiated, Jetty deflates inside that 
> {{sendBinary }}call, so a connection draining a deep queue holds the monitor 
> for long stretches at a time. We suspect that is why we only met this after 
> turning compression on, though compression is clearly not required: any slow 
> write should do it, including a client that has simply  topped reading.
> h2. A second thing, in the same loop
> The {{break}} above stops the scan at the first overdue connection, so one 
> connection that stays overdue keeps every other connection's timeouts from 
> being looked at.
> We have not bundled that in out of tidiness. Fixing it on its own would make 
> the first problem worse, because the checker would then queue a job for every 
> overdue connection on each pass instead of one. The two seemed safer to 
> change together.
> h2. What the patch does
> Three things, all in {{{}WebSocketProvider{}}}:
>  * an {{AtomicBoolean}} per {{{}ConnectionWrapper{}}}, set when a tick job is 
> queued and cleared inside the monitor before the ticker is advanced, so at 
> most one job is ever outstanding;
>  * the scan pulled out into a package-private {{findDueConnections}} that 
> returns every due connection instead of the first, with the checker ticking 
> all of them;
>  * a minimum wait of 1 ms in the loop, so that a connection whose job is 
> already queued cannot spin the checker thread.
> The module had no test sources, so the patch adds them along with the two 
> test dependencies the sibling plugin modules already declare. Getting at 
> {{tick()}} from a test meant making {{ConnectionWrapper}} package-private, 
> which is the one change we made purely for testability, and we would happily 
> take a better suggestion.
> Against unpatched code the two new tests fail like this:
> {noformat}
> [ERROR] WebSocketProviderTest.tickDoesNotQueueASecondJobWhileOneIsPending
> queued 99996 tick jobs from 100000 calls to tick();
> at most one should be pending
> [ERROR] WebSocketProviderTest.everyDueConnectionIsReturnedNotJustTheFirst
> both overdue connections should be returned ==> expected: <2> but was: <1>
> {noformat}
> The first holds the connection monitor from another thread, the way 
> {{doWrite()}} would, and then calls {{tick()}} as the checker does while the 
> ticker is overdue. The second hands the provider two overdue connections and 
> asks which ones are due. {{mvn -pl broker-plugins/websocket test}} is green 
> with the patch applied.
> h2. Versions, and where we might be wrong
> Two caveats worth stating. We have not identified what left the ticker 
> overdue in our own crash, only the code path that turns an overdue ticker 
> into an unbounded queue, so the trigger may deserve a look of its own. And if 
> the unbounded submission is deliberate, load shedding of some sort we have 
> not understood, we would rather be told than have the patch quietly declined.



--
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