

package org.jgroups.tests;


import org.jgroups.Global;
import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.ReceiverAdapter;
import org.jgroups.protocols.SHUFFLE;
import org.jgroups.protocols.pbcast.NAKACK;
import org.jgroups.stack.ProtocolStack;
import org.jgroups.util.Util;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * Tests a SEQUENCER based stack: demonstrates race condition where thread#1
 * gets seqno, thread#2 gets seqno, thread#2 sends, thread#1 tries to send but
 * is out of order.
 * 
 * In order to test total ordering, make sure that messages are sent from 
 * concurrent senders; using one sender will cause NAKACK to FIFO order 
 * the messages and the assertions in this test will still hold true, whether
 * SEQUENCER is present or not. 
 */
@Test(groups=Global.STACK_INDEPENDENT,sequential=true)
public class SequencerOrderTest {
    private JChannel    c1, c2, c3;
    private MyReceiver  r1, r2, r3;
    static final String GROUP="SequencerOrderTest";
    static final int    NUM_MSGS=50; // messages per thread
    static final int    NUM_THREADS=10;
    static final int    EXPECTED_MSGS=NUM_MSGS * NUM_THREADS;
    static final String props="sequencer.xml";
    private Sender[]    senders=new Sender[NUM_THREADS];


    @BeforeMethod
    void setUp() throws Exception {
        c1=new JChannel(props);
        c1.setName("A");
        c1.connect(GROUP);
        r1=new MyReceiver("A");
        c1.setReceiver(r1);

        c2=new JChannel(props);
        c2.setName("B");
        c2.connect(GROUP);
        r2=new MyReceiver("B");
        c2.setReceiver(r2);

        c3=new JChannel(props);
        c3.setName("C");
        c3.connect(GROUP);
        r3=new MyReceiver("C");
        c3.setReceiver(r3);

        AtomicInteger num=new AtomicInteger(1);

        for(int i=0; i < senders.length; i++) {
            senders[i]=new Sender(NUM_MSGS, num, c1, c2, c3);
        }
    }

    @AfterMethod
    void tearDown() throws Exception {
        Util.close(c3, c2, c1);
    }

    @Test @SuppressWarnings("unchecked")
    public void testBroadcastSequence() throws Exception {
        insertShuffle(c1, c2, c3);
        
        // use concurrent senders to send messages to the group
        System.out.println("Starting " + senders.length + " sender threads (each sends " + NUM_MSGS + " messages)");
        for(Sender sender: senders)
            sender.start();

        for(Sender sender: senders)
            sender.join(20000);
        System.out.println("Ok, senders have completed");

        final List<String> l1=r1.getMsgs();
        final List<String> l2=r2.getMsgs();
        final List<String> l3=r3.getMsgs();
        
        System.out.println("-- verifying messages on A and B");
        verifyNumberOfMessages(EXPECTED_MSGS, l1, l2, l3);
        verifySameOrder(EXPECTED_MSGS, l1, l2, l3);
    }

    private static void insertShuffle(JChannel... channels) throws Exception {
        for(JChannel ch: channels) {
            SHUFFLE shuffle=new SHUFFLE();
            shuffle.setDown(false);
            shuffle.setUp(true);
            shuffle.setMaxSize(10);
            shuffle.setMaxTime(1000);
            ch.getProtocolStack().insertProtocol(shuffle, ProtocolStack.BELOW, NAKACK.class);
            shuffle.init(); // gets the timer
        }
    }

    private static void verifyNumberOfMessages(int num_msgs, List<String> ... lists) throws Exception {
        long end_time=System.currentTimeMillis() + 10000;
        while(System.currentTimeMillis() < end_time) {
            boolean all_correct=true;
            for(List<String> list: lists) {
                if(list.size() != num_msgs) {
                    all_correct=false;
                    break;
                }
            }
            if(all_correct)
                break;
            Util.sleep(1000);
        }

        for(int i=0; i < lists.length; i++)
            System.out.println("list #" + (i+1) + ": " + lists[i]);

        for(int i=0; i < lists.length; i++)
            assert lists[i].size() == num_msgs : "list #" + (i+1) + " should have " + num_msgs + " elements";
        System.out.println("OK, all lists have the same size (" + num_msgs + ")\n");
    }



    private static void verifySameOrder(int expected_msgs, List<String> ... lists) throws Exception {
        for(int index=0; index < expected_msgs; index++) {
            String val=null;
            for(List<String> list: lists) {
                if(val == null)
                    val=list.get(index);
                else {
                    String val2=list.get(index);
                    assert val.equals(val2) : "found different values at index " + index + ": " + val + " != " + val2;
                }
            }
        }
        System.out.println("OK, all lists have the same order");
    }

    private static class Sender extends Thread {
        final int        num_msgs;
        final JChannel[] channels;
        final AtomicInteger num;

        public Sender(int num_msgs, AtomicInteger num, JChannel ... channels) {
            this.num_msgs=num_msgs;
            this.num=num;
            this.channels=channels;
        }

        public void run() {
            for(int i=1; i <= num_msgs; i++) {
                try {
                    JChannel ch=(JChannel)Util.pickRandomElement(channels);
                    String channel_name=ch.getName();
                    ch.send(null, null, channel_name + ":" + num.getAndIncrement());
                }
                catch(Exception e) {
                }
            }
        }
    }

    private static class MyReceiver extends ReceiverAdapter {
        final String name;
        final List<String> msgs=new LinkedList<String>();

        private MyReceiver(String name) {
            this.name=name;
        }

        public List<String> getMsgs() {
            return msgs;
        }

        public void receive(Message msg) {
            String val=(String)msg.getObject();
            if(val != null) {
                synchronized(msgs) {
                    msgs.add(val);
                }
            }
        }
    }



}
