package org.jgroups.tests;

import org.jgroups.Global;
import org.jgroups.Message;
import org.jgroups.util.AverageMinMax;
import org.jgroups.util.Util;
import org.testng.annotations.Test;

import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

/**
 * Tests an algorithm that has multiple threads adding to a queue but only one thread at a time consuming the queue's
 * elements, using a simple CAS counter and a (lock-synchronized) queue. The Pluscal code is at
 * https://github.com/belaban/pluscal/blob/master/add.tla
 * @author Bela Ban
 * @since  4.0
 */
@Test(groups=Global.FUNCTIONAL,singleThreaded=true)
public class DrainTest {
    protected final Queue<Message> queue=new ArrayBlockingQueue<Message>(50000);
    protected final AtomicInteger  counter=new AtomicInteger(0);
    protected final LongAdder      added=new LongAdder();
    protected final LongAdder      removed=new LongAdder();
    protected final LongAdder      num_removers=new LongAdder();
    protected final AverageMinMax  avg_removed=new AverageMinMax();

    public void testDraining() throws InterruptedException {
        MyThread[] threads=new MyThread[10];
        final CountDownLatch latch=new CountDownLatch(1);
        for(int i=0; i < threads.length; i++) {
            threads[i]=new MyThread(latch);
            threads[i].start();
        }
        latch.countDown();

        Util.sleep(5000);

        System.out.printf("\nStopping threads\n");
        for(MyThread thread: threads)
            thread.cancel();
        System.out.printf("done, joining threads\n");
        for(MyThread thread: threads)
            thread.join();

        System.out.printf("\ncounter=%d, added=%d, removed=%d, avg_removed=%s (removers=%d)\n",
                          counter.get(), added.sum(), removed.sum(), avg_removed, num_removers.sum());

        assert added.sum() == removed.sum();
        assert counter.get() == 0;
        assert this.queue.isEmpty();
    }

    protected void add(Message msg) {
        if(queue.offer(msg))
            added.increment();
        drain();
    }

    protected void drain() {
        if(counter.getAndIncrement() == 0) {
            num_removers.increment();
            int cnt=0;
            do {
                Message msg=queue.poll();
                if(msg == null)
                    continue; // System.err.printf("got empty message");
                removed.increment();
                cnt++;
            } while(counter.decrementAndGet() != 0);
            avg_removed.add(cnt);
        }
    }

    protected class MyThread extends Thread {
        protected final CountDownLatch latch;
        protected volatile boolean     running=true;

        public MyThread(CountDownLatch latch) {
            this.latch=latch;
        }

        protected void cancel() {running=false;}

        public void run() {
            try {
                latch.await();
            }
            catch(InterruptedException e) {
                e.printStackTrace();
            }
            while(running) {
                Message msg=new Message();
                add(msg);
            }
        }
    }

    protected Message[] create(int max) {
        int num=(int)Util.random(max);
        Message[] msgs=new Message[num];
        for(int i=0; i < msgs.length; i++)
            msgs[i]=new Message();
        return msgs;
    }
}
