1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2025-09-10 02:39:42 +02:00

Properly sync PacketWriter's queue

by using a custom ArrayBlockingQueueWithShutdown. Fixes a race condition
where nextpacket() would wait for a notification that would never
arrive, because all all put(Packet) calls are still blocking.

SMACK-560
This commit is contained in:
Florian Schmaus 2014-04-30 14:05:52 +02:00
parent ed8b80c2ff
commit 7041e90522
3 changed files with 639 additions and 36 deletions

View file

@ -18,11 +18,10 @@
package org.jivesoftware.smack;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown;
import java.io.IOException;
import java.io.Writer;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -37,12 +36,16 @@ import java.util.logging.Logger;
* @author Matt Tucker
*/
class PacketWriter {
public static final int QUEUE_SIZE = 500;
private static final Logger LOGGER = Logger.getLogger(PacketWriter.class.getName());
private final XMPPTCPConnection connection;
private final ArrayBlockingQueueWithShutdown<Packet> queue = new ArrayBlockingQueueWithShutdown<Packet>(QUEUE_SIZE, true);
private Thread writerThread;
private Writer writer;
private XMPPTCPConnection connection;
private final BlockingQueue<Packet> queue;
volatile boolean done;
/**
@ -51,7 +54,6 @@ class PacketWriter {
* @param connection the connection.
*/
protected PacketWriter(XMPPTCPConnection connection) {
this.queue = new ArrayBlockingQueue<Packet>(500, true);
this.connection = connection;
init();
}
@ -64,6 +66,7 @@ class PacketWriter {
this.writer = connection.writer;
done = false;
queue.start();
writerThread = new Thread() {
public void run() {
writePackets(this);
@ -79,17 +82,17 @@ class PacketWriter {
* @param packet the packet to send.
*/
public void sendPacket(Packet packet) {
if (!done) {
try {
queue.put(packet);
}
catch (InterruptedException ie) {
LOGGER.log(Level.SEVERE, "Failed to queue packet to send to server: " + packet.toString(), ie);
return;
}
synchronized (queue) {
queue.notifyAll();
}
if (done) {
return;
}
try {
queue.put(packet);
}
catch (InterruptedException ie) {
LOGGER.log(Level.SEVERE,
"Failed to queue packet to send to server: " + packet.toString(), ie);
return;
}
}
@ -112,9 +115,7 @@ class PacketWriter {
*/
public void shutdown() {
done = true;
synchronized (queue) {
queue.notifyAll();
}
queue.shutdown();
}
/**
@ -123,17 +124,16 @@ class PacketWriter {
* @return the next packet for writing.
*/
private Packet nextPacket() {
if (done) {
return null;
}
Packet packet = null;
// Wait until there's a packet or we're done.
while (!done && (packet = queue.poll()) == null) {
try {
synchronized (queue) {
queue.wait();
}
}
catch (InterruptedException ie) {
// Do nothing
}
try {
packet = queue.take();
}
catch (InterruptedException e) {
// Do nothing
}
return packet;
}
@ -191,12 +191,8 @@ class PacketWriter {
// The exception can be ignored if the the connection is 'done'
// or if the it was caused because the socket got closed
if (!(done || connection.isSocketClosed())) {
done = true;
// packetReader could be set to null by an concurrent disconnect() call.
// Therefore Prevent NPE exceptions by checking packetReader.
if (connection.packetReader != null) {
connection.notifyConnectionError(ioe);
}
shutdown();
connection.notifyConnectionError(ioe);
}
}
}

View file

@ -0,0 +1,112 @@
/**
*
* Copyright 2014 Florian Schmaus
*
* Licensed 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.jivesoftware.smack;
import java.io.IOException;
import java.io.Writer;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import org.jivesoftware.smack.packet.Message;
import org.junit.Test;
import static org.junit.Assert.fail;
public class PacketWriterTest {
volatile boolean shutdown;
volatile boolean prematureUnblocked;
/**
* Make sure that packet writer does block once the queue reaches
* {@link PacketWriter#QUEUE_SIZE} and that
* {@link PacketWriter#sendPacket(org.jivesoftware.smack.packet.Packet)} does unblock after the
* interrupt.
*
* @throws InterruptedException
* @throws BrokenBarrierException
*/
@SuppressWarnings("javadoc")
@Test
public void shouldBlockAndUnblockTest() throws InterruptedException, BrokenBarrierException {
XMPPTCPConnection connection = new XMPPTCPConnection("foobar.com");
final PacketWriter pw = new PacketWriter(connection);
pw.setWriter(new BlockingStringWriter());
pw.startup();
for (int i = 0; i < PacketWriter.QUEUE_SIZE; i++) {
pw.sendPacket(new Message());
}
final CyclicBarrier barrier = new CyclicBarrier(2);
shutdown = false;
prematureUnblocked = false;
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
barrier.await();
}
catch (InterruptedException | BrokenBarrierException e1) {
}
pw.sendPacket(new Message());
// should only return after the pw was shutdown
if (!shutdown) {
prematureUnblocked = true;
}
try {
barrier.await();
}
catch (InterruptedException | BrokenBarrierException e) {
}
}
});
t.start();
// This barrier is not strictly necessary, but may increases the chances that the threat
// will block before we call shutdown. Otherwise we may get false positives (which is still
// better then false negatives).
barrier.await();
// Not really cool, but may increases the chances for 't' to block in sendPacket.
Thread.sleep(250);
// Shutdown the packetwriter
pw.shutdown();
shutdown = true;
barrier.await();
if (prematureUnblocked) {
fail("Should not unblock before the thread got shutdown");
}
}
public class BlockingStringWriter extends Writer {
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
try {
Thread.sleep(Integer.MAX_VALUE);
}
catch (InterruptedException e) {
}
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
}
}
}