package org.jgroups.protocols; import org.jgroups.*; import org.jgroups.conf.ClassConfigurator; import org.jgroups.stack.Protocol; import org.jgroups.stack.ProtocolStack; import org.jgroups.util.MessageBatch; import org.jgroups.util.Util; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** * @author Bela Ban * @since 4.0 */ @Test(groups=Global.FUNCTIONAL,singleThreaded=true) public class UNICAST3_Test { protected JChannel a, b; protected Address a_addr, b_addr; protected MyReceiver receiver=new MyReceiver(); protected UNICAST3 uni_a, uni_b; protected DropUnicastAck drop_ack=new DropUnicastAck((short)499); protected static final short UNICAST3_ID=ClassConfigurator.getProtocolId(UNICAST3.class); protected static final int CONN_CLOSE_TIMEOUT=60_000; // change to a low value (e.g. 1000) to make this test fail @BeforeMethod protected void setup() throws Exception { a=create("A").connect(getClass().getSimpleName()); b=create("B").connect(getClass().getSimpleName()); a_addr=a.getAddress(); b_addr=b.getAddress(); uni_a=a.getProtocolStack().findProtocol(UNICAST3.class); uni_b=b.getProtocolStack().findProtocol(UNICAST3.class); b.getProtocolStack().insertProtocol(drop_ack, ProtocolStack.Position.BELOW, UNICAST3.class); } @AfterMethod protected void destroy() {Util.close(b, a);} /** - A and B exchanging unicast messages - Seqno 499 is sent from A to B - B adds it to its table for A, and sends the ACK, then delivers the message - The ack for 499 from B to A is dropped - B excludes A (but A doesn't exclude B) and removes A's table * This happens only if conn_close_timeout is small (default: 10s) * If conn_close_timeout == 0, connections will not be removed - A retransmits 499 to B - B receives A:499, but asks for the first seqno - A has its highest seqno acked at 498, so resends 499 with first==true - B creates a receiver window for A at 499, receives 499 and delivers it (again) The issue is fixed by setting CONN_CLOSE_TIMEOUT to a highher value, or to 0 */ public void testDuplicateMessageDelivery() throws Exception { b.setReceiver(receiver); for(int i=1; i < 500; i++) a.send(b_addr, i); for(int i=0; i < 10; i++) { if(receiver.count >= 499) break; Util.sleep(50); } System.out.printf("B: received %d messages from A\n", receiver.count); assert receiver.count == 499; // remove A's receive window in B: System.out.printf("-- closing the receive-window for %s:\n", a_addr); // e.g. caused by an asymmetric network split: B excludes A, but not vice versa uni_b.closeReceiveConnection(a_addr); uni_a.setLevel("trace"); uni_b.setLevel("trace"); uni_b.setValue("conn_close_timeout", CONN_CLOSE_TIMEOUT); // wait until B closes the receive window for A: for(int i=0; i < 10; i++) { if(uni_b.getNumReceiveConnections() == 0) break; Util.sleep(500); } // assert uni_b.getNumReceiveConnections() > 0; // remove the DropUnicastAck protocol: System.out.printf("-- removing the %s protocol\n", DropUnicastAck.class.getSimpleName()); b.getProtocolStack().removeProtocol(DropUnicastAck.class); for(int i=0; i < 20; i++) { if(receiver.count >= 500) break; Util.sleep(100); } System.out.printf("B: received %d messages from A\n", receiver.count); assert receiver.count == 499 : String.format("received %d messages, but should only have received 499", receiver.count); } protected JChannel create(String name) throws Exception { return new JChannel(new SHARED_LOOPBACK(), new SHARED_LOOPBACK_PING(), new UNICAST3()).name(name); } /** * Inserted only into B (under UNICAST3). Drops ACK for 499 from B to A, only acks the previous message (498). * Then drops all traffic from A and closes A's receive window in B. * When removed, A:499 should get retransmitted and cause duplicate delivery in B. */ protected static class DropUnicastAck extends Protocol { protected final short start_drop_ack; protected volatile boolean discarding; public DropUnicastAck(short start_drop_ack) { this.start_drop_ack=start_drop_ack; } public Object down(Message msg) { UnicastHeader3 hdr=msg.getHeader(UNICAST3_ID); if(hdr != null && hdr.type() == UnicastHeader3.ACK && hdr.seqno() == start_drop_ack) { discarding=true; hdr.seqno=start_drop_ack-1; // change 499 to 489, so A nevers gets the 499 ACK } return down_prot.down(msg); } public Object up(Message msg) { if(discarding) return null; return up_prot.up(msg); } public void up(MessageBatch batch) { if(discarding) return; up_prot.up(batch); } } protected static class MyReceiver extends ReceiverAdapter { protected int count=0; public void receive(Message msg) { // System.out.printf("single msg from %s: %s, hdrs: %s\n", msg.src(), msg.getObject(), msg.printHeaders()); count++; } public void receive(MessageBatch batch) { // System.out.printf("batch from %s: %d msgs:\n", batch.sender(), batch.size()); for(Message ignored: batch) count++; } } }