com.tinspx.util.concurrent.DelayedSemaphoreTest.java Source code

Java tutorial

Introduction

Here is the source code for com.tinspx.util.concurrent.DelayedSemaphoreTest.java

Source

/* Copyright (C) 2013-2014 Ian Teune <ian.teune@gmail.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.tinspx.util.concurrent;

import static com.google.common.base.Preconditions.*;
import com.google.common.base.Throwables;
import com.google.common.base.Ticker;
import com.google.common.collect.Range;
import com.google.common.util.concurrent.Uninterruptibles;
import com.tinspx.util.base.NumberUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import lombok.ToString;
import lombok.experimental.Builder;
import org.apache.commons.lang3.mutable.MutableInt;
import static org.junit.Assert.*;
import org.junit.Test;

/**
 * Tests {@link DelayedSemaphore}.
 * <p>
 * Most tests take a very long time to run and are therefore not run.
 * {@link #testAll()} tests nearly every feature.
 *
 * @author Ian
 */
public class DelayedSemaphoreTest {

    private final Object lock = new Object();
    private long last;
    private int tests;
    private volatile long attempts;
    private static long testCount;

    public DelayedSemaphoreTest() {
    }

    /**
     * Test of create method, of class DelayedSemaphore.
     */
    @Test
    @SuppressWarnings("ResultOfObjectAllocationIgnored")
    public void testInit() throws InterruptedException {
        new DelayedSemaphore(1, 0, null);
        DelayedSemaphore.create(1, 0);
        DelayedSemaphore.create(1, 1);

        DelayedSemaphore ls = DelayedSemaphore.create(5, 50000000);
        assertEquals(5, ls.permits());
        assertEquals(5, ls.availablePermits());
        assertEquals(50000000, ls.delay());
        assertSame(Ticker.systemTicker(), ls.ticker());
        ls.acquire(); //4
        Thread.sleep(10);
        assertEquals(5, ls.permits());
        assertEquals(4, ls.availablePermits());
        ls.acquire();//3
        ls.acquire();//2
        ls.release();//3
        assertEquals(2, ls.availablePermits());
        Thread.sleep(51);
        assertEquals(3, ls.availablePermits());
        assertEquals(5, ls.permits());

        ls.release(); //4
        ls.release();//5
        try {
            ls.release();
            fail();
        } catch (IllegalStateException ex) {
        }

        assertTrue(ls.tryAcquire(-1, TimeUnit.DAYS));
        assertTrue(ls.tryAcquire(1, -1, TimeUnit.DAYS));
        Thread.sleep(51);
        assertTrue(ls.tryAcquire(2, -1, TimeUnit.DAYS));
        assertEquals(1, ls.availablePermits(0));
        try {
            ls.release(5);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        ls.release(4);
        assertEquals(5, ls.availablePermits(0));
        assertEquals(0, ls.availablePermits(TimeUnit.HOURS.toNanos(1)));

        //acquire invalid args
        try {
            ls.acquire(-1L);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.acquire(0);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.acquire(-1);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.acquire(0, 10);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.acquire(-1, 10);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.acquire(1, -1);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.acquire(2, -1);
            fail();
        } catch (IllegalArgumentException ex) {
        }

        //tryAcquire no timeout, invalid args
        try {
            ls.tryAcquire(-1L);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(0);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(-1);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(0, 10);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(-1, 10);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(1, -1);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(2, -1);
            fail();
        } catch (IllegalArgumentException ex) {
        }

        //tryAcquire with timeout, invalid arguments
        try {
            ls.tryAcquire(-1L, 10, TimeUnit.NANOSECONDS);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(0, 10, TimeUnit.NANOSECONDS);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(-1, 10, TimeUnit.NANOSECONDS);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(0, 10, 10, TimeUnit.NANOSECONDS);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(-1, 10, 10, TimeUnit.NANOSECONDS);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(1, -1, 10, TimeUnit.NANOSECONDS);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(2, -1, 10, TimeUnit.NANOSECONDS);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            ls.tryAcquire(0L, 10, null);
            fail();
        } catch (NullPointerException ex) {
        }
        try {
            ls.tryAcquire(1, 10, null);
            fail();
        } catch (NullPointerException ex) {
        }
        try {
            ls.tryAcquire(2, 10, null);
            fail();
        } catch (NullPointerException ex) {
        }
        try {
            ls.tryAcquire(1, 0, 10, null);
            fail();
        } catch (NullPointerException ex) {
        }
        try {
            ls.tryAcquire(2, 0, 10, null);
            fail();
        } catch (NullPointerException ex) {
        }

        DelayedSemaphore.create(1, 0);
        try {
            DelayedSemaphore.create(0, 1);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            DelayedSemaphore.create(1, -1);
            fail();
        } catch (IllegalArgumentException ex) {
        }
        try {
            DelayedSemaphore.create(1, 0, null);
            fail();
        } catch (NullPointerException ex) {
        }
    }

    static class AssertListener implements DelayedSemaphore.Listener {

        final DelayedSemaphore ds;
        int acquire;
        int release;

        int releaseExpect = 1;
        int acquireExpect = 1;

        public AssertListener(DelayedSemaphore ds) {
            this.ds = ds;
        }

        @Override
        public void onAcquire(DelayedSemaphore ls, int permitsAcquired) {
            assertSame(ls, ds);
            assertEquals(acquireExpect, permitsAcquired);
            acquire++;
        }

        @Override
        public void onRelease(DelayedSemaphore ls, int permitsReleased) {
            assertSame(ls, ds);
            assertEquals(releaseExpect, permitsReleased);
            release++;
        }

        public void assertAcquire(int a) {
            assertEquals(a, acquire);
        }

        public void assertRelease(int r) {
            assertEquals(r, release);
        }
    }

    @Test
    public void testListeners() throws InterruptedException {
        DelayedSemaphore ds = DelayedSemaphore.create(3, 100000000); //100 ms
        AssertListener al = new AssertListener(ds);
        AssertListener el = new AssertListener(ds);
        ThreadUtilsTest.AssertExecutor ex = new ThreadUtilsTest.AssertExecutor();

        ds.addListener(al);
        assertEquals(1, ds.listenerCount());
        ds.addListener(el, ex);
        assertEquals(2, ds.listenerCount());
        ds.removeListener(el);
        assertEquals(1, ds.listenerCount());
        ds.removeListener(al);
        assertEquals(0, ds.listenerCount());
        ds.addListener(al);
        ds.addListener(el, ex);
        assertEquals(2, ds.listenerCount());

        ds.acquire(); //2
        al.assertAcquire(1);
        el.assertAcquire(1);
        ex.assertExactly(1);

        assertTrue(ds.tryAcquire()); //1
        al.assertAcquire(2);
        el.assertAcquire(2);
        ex.assertExactly(2);

        ds.release(); //2
        al.assertRelease(1);
        el.assertRelease(1);
        ex.assertExactly(3);

        ds.release(); //3
        al.assertRelease(2);
        el.assertRelease(2);
        ex.assertExactly(4);

        try {
            ds.release();
            fail();
        } catch (IllegalStateException e) {
        }
        al.assertRelease(2);
        el.assertRelease(2);
        ex.assertExactly(4);

        assertEquals(0, ds.estimatedWait());
        ds.acquire(); //2
        long est = ds.estimatedWait();
        //        System.out.println(est);
        assertTrue(est + "", est > 90000000); //90 ms
        assertTrue(est + "", est < 100000000); //100 ms

        long time = System.nanoTime();
        ds.acquire(); //1
        ds.acquire(); //0
        time = System.nanoTime() - time;
        assertTrue(time + "", time > 90000000); //180 ms
        assertTrue(time + "", time < 110000000); //100 ms
        //        System.out.println("time: "  + time);
        assertEquals(-1, ds.estimatedWait());

        al.assertAcquire(5);
        el.assertAcquire(5);
        ex.assertExactly(7);

        try {
            ds.release(4);
            fail();
        } catch (IllegalArgumentException e) {
        }

        al.releaseExpect = 3;
        al.acquireExpect = 3;
        el.releaseExpect = 3;
        el.acquireExpect = 3;

        ds.release(3);
        al.assertRelease(3);
        el.assertRelease(3);
        ex.assertExactly(8);

        try {
            ds.acquire(4);
            fail();
        } catch (IllegalArgumentException e) {
        }

        ds.acquire(3);
        al.assertAcquire(6);
        el.assertAcquire(6);
        ex.assertExactly(9);
    }

    private static final int THREADS = 8;
    private static final int TESTS = 20;

    //    @Test
    public void testSingleVary() throws InterruptedException {
        testSingle(false);
    }

    //    @Test
    public void testSingleAcquire() throws InterruptedException {
        testSingle(true);
    }

    public void testSingle(boolean onlyAcquire) throws InterruptedException {
        Ticker ticker = Ticker.systemTicker();
        final long delay = TimeUnit.MILLISECONDS.toNanos(50);
        DelayedSemaphore ls = new DelayedSemaphore(1, delay, ticker);

        SingleRunner testers[] = new SingleRunner[THREADS];
        Thread threads[] = new Thread[THREADS];
        tests = 0;
        for (int i = 0; i < THREADS; i++) {
            testers[i] = new SingleRunner(ls, ticker, delay, TESTS, onlyAcquire);
            threads[i] = new Thread(ThreadUtils.suppress(testers[i]));
            threads[i].start();
        }
        for (int i = 0; i < THREADS; i++) {
            threads[i].join();
        }
        for (int i = 0; i < THREADS; i++) {
            if (testers[i].fail != null) {
                fail(testers[i].fail);
            }
        }
        assertEquals(tests, THREADS * TESTS);
        if (!onlyAcquire) {
            System.out.println("single: " + attempts);
        }
    }

    private String acquire(DelayedSemaphore ls, int count) throws InterruptedException {
        final long MAX_WAIT = TimeUnit.MICROSECONDS.toNanos(50000),
                MAX_CHECK = TimeUnit.MICROSECONDS.toNanos(75000);
        switch (count % 3) {
        case 0:
            ls.acquire();
            return null;
        case 1:
            while (!ls.tryAcquire()) {
            }
            return null;
        case 2:
            while (true) {
                long time = ls.ticker().read();
                boolean acquired = ls.tryAcquire(MAX_WAIT, TimeUnit.NANOSECONDS);
                time = ls.ticker().read() - time;
                if (time > MAX_CHECK) {
                    return String.format("%d > %d (max %d), acquired: %b", time, MAX_CHECK, MAX_WAIT, acquired);
                }
                //                    System.out.println("passed");
                if (acquired) {
                    return null;
                } else {
                    attempts++;
                }
            }
        default:
            throw new AssertionError();
        }
    }

    private class SingleRunner implements Runnable {

        final Ticker ticker;
        final long delay;
        final int tests;
        final DelayedSemaphore ls;
        String fail;
        final boolean onlyAcquire;

        public SingleRunner(DelayedSemaphore ls, Ticker ticker, long delay, int tests, boolean onlyAcquire) {
            checkArgument(delay >= 0);
            checkArgument(tests > 0);
            this.delay = delay;
            this.tests = tests;
            this.ls = checkNotNull(ls);
            this.ticker = checkNotNull(ticker);
            this.onlyAcquire = onlyAcquire;
        }

        @Override
        public void run() {
            try {
                runTests();
            } catch (InterruptedException ex) {
                fail = Throwables.getStackTraceAsString(ex);
            }
        }

        private void runTests() throws InterruptedException {
            for (int i = 0; i < tests; i++) {
                if (onlyAcquire) {
                    ls.acquire();
                } else {
                    fail = acquire(ls, i);
                    if (fail != null) {
                        return;
                    }
                }
                synchronized (lock) {
                    final long now = ticker.read();
                    DelayedSemaphoreTest.this.tests++;
                    if (now - last < delay) {
                        fail = (now - last) + " < " + delay;
                        return;
                    } else {
                        last = now;
                    }
                }
                ls.release();
            }
        }
    }

    private static final int PERMITS = 4;

    //    @Test
    public void testMultipleVary() throws InterruptedException {
        testMultiple(false);
    }

    //    @Test
    public void testMultipleAcquire() throws InterruptedException {
        testMultiple(true);
    }

    public void testMultiple(boolean onlyAcquire) throws InterruptedException {
        Ticker ticker = Ticker.systemTicker();
        final long delay = TimeUnit.MILLISECONDS.toNanos(100);
        DelayedSemaphore ls = new DelayedSemaphore(PERMITS, delay, ticker);

        int th = THREADS;
        int t = TESTS * 3;
        MultipleRunner testers[] = new MultipleRunner[th];
        Thread threads[] = new Thread[th];
        tests = 0;
        long times[] = new long[PERMITS];
        for (int i = 0; i < th; i++) {
            testers[i] = new MultipleRunner(ls, ticker, delay, PERMITS, t, times, onlyAcquire);
            threads[i] = new Thread(ThreadUtils.suppress(testers[i]));
            threads[i].start();
        }
        for (int i = 0; i < th; i++) {
            threads[i].join();
        }
        for (int i = 0; i < th; i++) {
            if (testers[i].fail != null) {
                System.out.println(testers[i].fail);
                fail(testers[i].fail);
            }
        }
        assertEquals(tests, th * t);
        if (!onlyAcquire) {
            System.out.println("multiple: " + attempts);
        }
    }

    private class MultipleRunner implements Runnable {

        final Ticker ticker;
        final long delay;
        final int tests;
        final int permits;
        final DelayedSemaphore ls;
        String fail;
        final long times[];
        final boolean onlyAcquire;

        public MultipleRunner(DelayedSemaphore ls, Ticker ticker, long delay, int permits, int tests, long times[],
                boolean onlyAcquire) {
            checkArgument(delay >= 0);
            checkArgument(tests > 0);
            checkArgument(permits > 0);
            this.permits = permits;
            this.delay = delay;
            this.tests = tests;
            this.ls = checkNotNull(ls);
            this.ticker = checkNotNull(ticker);
            this.times = checkNotNull(times);
            this.onlyAcquire = onlyAcquire;
        }

        @Override
        public void run() {
            try {
                runTests();
            } catch (InterruptedException ex) {
                fail = Throwables.getStackTraceAsString(ex);
            }
        }

        private void runTests() throws InterruptedException {
            for (int i = 0; i < tests; i++) {
                if (onlyAcquire) {
                    ls.acquire();
                } else {
                    fail = acquire(ls, i);
                    if (fail != null) {
                        return;
                    }
                }
                synchronized (lock) {
                    final long now = ticker.read();
                    DelayedSemaphoreTest.this.tests++;
                    final long last = shiftAndGet(times);
                    times[times.length - 1] = now;
                    if (now - last < delay - TimeUnit.MILLISECONDS.toNanos(0)) {
                        fail = (now - last) + " < " + delay;
                        return;
                    }
                }
                ls.release();
            }
        }
    }

    private static long shiftAndGet(long values[]) {
        long r = values[0];
        for (int i = 1; i < values.length; i++) {
            values[i - 1] = values[i];
        }
        return r;
    }

    private static long[] shiftAndGet(long values[], int permits, long fill) {
        assert permits > 0 : permits;
        assert permits <= values.length;
        long[] out = new long[permits];
        System.arraycopy(values, 0, out, 0, permits);
        System.arraycopy(values, permits, values, 0, values.length - permits);
        Arrays.fill(values, values.length - permits, values.length, fill);
        return out;
    }

    @SuppressWarnings("UnnecessaryUnboxing")
    static void runTest(Executor executor, DelayedSemaphore ds, Ticker ticker, int threadCount, int acquisitions,
            Acquire acquire, Permits permits, Range<Integer> acquireRange, Release release,
            DelayConstraint constraint) throws InterruptedException {
        checkArgument(threadCount > 0);

        DelayTest.DelayTestBuilder builder = DelayTest.builder();
        builder.stop(new AtomicBoolean());
        builder.start(new CountDownLatch(threadCount));
        builder.lock(new ReentrantLock());
        builder.releaseTimes(new long[ds.permits()]);
        builder.acquisitions(acquisitions);
        builder.ticker(ticker).ds(ds);
        builder.acquire(acquire).permits(permits).permits(permits).acquireRange(acquireRange);
        builder.release(release);
        builder.delayConstraint(constraint);
        builder.tests(new MutableInt());
        builder.totalThreads(threadCount);

        DelayTest[] testers = new DelayTest[threadCount];
        for (int i = 0; i < threadCount; i++) {
            testers[i] = builder.thread(i).build();
            executor.execute(testers[i]);
        }
        for (int i = 0; i < threadCount; i++) {
            testers[i].complete.await();
        }
        String errorMsg = null;
        for (int i = 0; i < threadCount; i++) {
            if (testers[i].fail != null) {
                errorMsg = testers[i].fail;
                System.out.println(errorMsg);
                System.out.println();
            }
        }
        if (errorMsg != null) {
            fail(errorMsg);
        }

        assertEquals(threadCount * acquisitions, builder.tests.getValue().intValue());
        if (++testCount % 10 == 0) {
            System.out.printf("%d, Tests: %s\n", testCount, builder.tests);
        }
    }

    @ToString
    static class DelayTest implements Runnable {
        final CountDownLatch complete = new CountDownLatch(1);
        final int totalThreads;
        AtomicBoolean stop;
        String fail;
        boolean started;
        final CountDownLatch start;
        final int thread;
        final Lock lock;
        final long[] releaseTimes;
        final int acquisitions;

        final Ticker ticker;
        final DelayedSemaphore ds;
        final Acquire acquire;
        final Permits permits;
        final Range<Integer> acquireRange;
        final Release release;
        final DelayConstraint delayConstraint;
        final MutableInt tests;

        @Builder
        public DelayTest(int totalThreads, CountDownLatch start, int thread, Lock lock, long[] releaseTimes,
                int acquisitions, Ticker ticker, DelayedSemaphore ds, Acquire acquire, Permits permits,
                Range<Integer> acquireRange, Release release, DelayConstraint delayConstraint, MutableInt tests,
                AtomicBoolean stop) {
            this.totalThreads = totalThreads;
            this.start = checkNotNull(start);
            this.thread = thread;
            this.lock = checkNotNull(lock);
            this.releaseTimes = checkNotNull(releaseTimes);
            checkArgument(acquisitions > 0);
            this.acquisitions = acquisitions;
            this.ticker = checkNotNull(ticker);
            this.ds = checkNotNull(ds);
            this.acquire = checkNotNull(acquire);
            this.permits = checkNotNull(permits);
            this.acquireRange = checkNotNull(acquireRange);
            this.release = checkNotNull(release);
            this.delayConstraint = checkNotNull(delayConstraint);
            this.tests = checkNotNull(tests);
            this.stop = checkNotNull(stop);
        }

        synchronized void checkStart() {
            checkState(!started);
            started = true;
        }

        @Override
        public void run() {
            try {
                tryRun();
            } catch (Throwable t) {
                fail = this + " \n" + Throwables.getStackTraceAsString(t);
                stop.set(true);
            } finally {
                complete.countDown();
            }
        }

        public void tryRun() {
            checkStart();
            start.countDown();
            Uninterruptibles.awaitUninterruptibly(start);

            for (int i = 0; i < acquisitions; i++) {
                final int p = permits.permits(ds, acquireRange);
                try {
                    while (!acquire.acquire(ds, p)) {
                        //continue until acquired
                    }
                } catch (InterruptedException ex) {
                    throw Throwables.propagate(ex);
                }
                try {
                    final long acquireTime = ticker.read();
                    long[] times;
                    lock.lock();
                    try {
                        tests.increment();
                        times = shiftAndGet(releaseTimes, p, acquireTime);
                    } finally {
                        lock.unlock();
                    }
                    for (long time : times) {
                        if (!delayConstraint.isSatisfied(ds, acquireTime - time)) {
                            fail = String.format("DS: %s\np: %s, acquireTime: %d, time: %d, elapsed: %d\nthis: %s",
                                    ds, p, acquireTime, time, acquireTime - time, this);
                            stop.set(true);
                            break;
                        }
                    }
                    if (stop.get()) {
                        return;
                    }
                } finally {
                    release.release(ds, p);
                }
            }
        }
    }

    interface Acquire {
        boolean acquire(DelayedSemaphore ds, int permits) throws InterruptedException;
    }

    interface Release {
        void release(DelayedSemaphore ds, int permits);
    }

    interface Permits {
        int permits(DelayedSemaphore ds, Range<Integer> acquireRange);
    }

    interface DelayConstraint {
        boolean isSatisfied(DelayedSemaphore ds, long elapsed);
    }

    static final Acquire ACQUIRE_INCREMENT = new Acquire() {
        final AtomicLong counter = new AtomicLong();

        @Override
        public boolean acquire(DelayedSemaphore ds, int permits) throws InterruptedException {
            switch ((int) (counter.incrementAndGet() % 3)) {
            case 0:
                return ACQUIRE_TRY_IMMEDIATE.acquire(ds, permits);
            case 1:
                return ACQUIRE_TRY_TIMEOUT.acquire(ds, permits);
            case 2:
                return ACQUIRE.acquire(ds, permits);
            default:
                throw new AssertionError();
            }
        }

        @Override
        public String toString() {
            return "ACQUIRE_INCREMENT";
        }
    };

    static final Acquire ACQUIRE_TRY_IMMEDIATE = new Acquire() {
        final AtomicLong counter = new AtomicLong();

        @Override
        public boolean acquire(DelayedSemaphore ds, int permits) {
            if (permits == 1 && counter.incrementAndGet() % 2 == 0) {
                return ds.tryAcquire();
            } else {
                return ds.tryAcquire(permits);
            }
        }

        @Override
        public String toString() {
            return "ACQUIRE_TRY_IMMEDIATE";
        }
    };

    static final Acquire ACQUIRE_TRY_TIMEOUT = new Acquire() {
        final AtomicLong delayCounter = new AtomicLong();
        final AtomicLong counter = new AtomicLong();

        @Override
        public boolean acquire(DelayedSemaphore ds, int permits) throws InterruptedException {
            long timeout = delayCounter.incrementAndGet() % 5;
            switch ((int) timeout) {
            case 0:
                timeout = ds.delay() * 2;
                break;
            case 1:
                timeout = ds.delay() * 3;
                break;
            case 2:
                timeout = ds.delay();
                break;
            case 3:
                timeout = ds.delay() / 2;
                break;
            case 4:
                timeout = ds.delay() / 3;
                break;
            default:
                throw new AssertionError(timeout);
            }

            long time = ds.ticker().read();
            boolean result;
            if (permits == 1 && counter.incrementAndGet() % 2 == 0) {
                result = ds.tryAcquire(timeout, TimeUnit.NANOSECONDS);
            } else {
                result = ds.tryAcquire(permits, timeout, TimeUnit.NANOSECONDS);
            }
            time = ds.ticker().read() - time;
            if (time > timeout + 30000000) { //30ms wiggle room
                fail(String.format("elapsed: %d, timeout: %d, result: %b, permits: %d, ds: %s", time, timeout,
                        result, permits, ds));
            }
            return result;
        }

        @Override
        public String toString() {
            return "ACQUIRE_TRY_TIMEOUT";
        }
    };

    static final Acquire ACQUIRE = new Acquire() {
        final AtomicLong counter = new AtomicLong();

        @Override
        public boolean acquire(DelayedSemaphore ds, int permits) throws InterruptedException {
            if (permits == 1 && counter.incrementAndGet() % 2 == 0) {
                ds.acquire();
            } else {
                ds.acquire(permits);
            }
            return true;
        }

        @Override
        public String toString() {
            return "ACQUIRE";
        }
    };

    static final Release RELEASE_INCREMENT = new Release() {
        final AtomicLong counter = new AtomicLong();

        @Override
        public void release(DelayedSemaphore ds, int permits) {
            if (counter.incrementAndGet() % 2 == 0) {
                RELEASE_LOOP.release(ds, permits);
            } else {
                RELEASE.release(ds, permits);
            }
        }

        @Override
        public String toString() {
            return "RELEASE_INCREMENT";
        }
    };

    static final Release RELEASE_LOOP = new Release() {
        @Override
        public void release(DelayedSemaphore ds, int permits) {
            for (int i = 0; i < permits; i++) {
                ds.release();
            }
        }

        @Override
        public String toString() {
            return "RELEASE_LOOP";
        }
    };

    static final Release RELEASE = new Release() {
        final AtomicLong counter = new AtomicLong();

        @Override
        public void release(DelayedSemaphore ds, int permits) {
            if (permits == 1 && counter.incrementAndGet() % 2 == 0) {
                ds.release();
            } else {
                ds.release(permits);
            }
        }

        @Override
        public String toString() {
            return "RELEASE";
        }
    };

    static final Permits PERMITS_ALL = new Permits() {
        @Override
        public int permits(DelayedSemaphore ds, Range<Integer> acquireRange) {
            return ds.permits();
        }

        @Override
        public String toString() {
            return "PERMITS_ALL";
        }
    };

    static final Permits PERMITS_ONE = new Permits() {
        @Override
        public int permits(DelayedSemaphore ds, Range<Integer> acquireRange) {
            return 1;
        }

        @Override
        public String toString() {
            return "PERMITS_ONE";
        }
    };

    static final Permits PERMITS_RANDOM = new Permits() {
        final Random random = new Random();

        @Override
        public int permits(DelayedSemaphore ds, Range<Integer> acquireRange) {
            return NumberUtils.getRandomInt(acquireRange, random);
        }

        @Override
        public String toString() {
            return "PERMITS_RANDOM";
        }
    };

    static final Permits PERMITS_INCREMENT = new Permits() {
        final AtomicLong counter = new AtomicLong();

        @Override
        public int permits(DelayedSemaphore ds, Range<Integer> acquireRange) {
            switch ((int) (counter.incrementAndGet() % 3)) {
            case 0:
                return PERMITS_ALL.permits(ds, acquireRange);
            case 1:
                return PERMITS_ONE.permits(ds, acquireRange);
            case 2:
                return PERMITS_RANDOM.permits(ds, acquireRange);
            default:
                throw new AssertionError();
            }
        }

        @Override
        public String toString() {
            return "PERMITS_INCREMENT";
        }
    };

    static Permits constant(int permits) {
        return new PermitConstant(permits);
    }

    @ToString
    final static class PermitConstant implements Permits {
        final int permits;

        public PermitConstant(int permits) {
            checkArgument(permits > 0);
            this.permits = permits;
        }

        @Override
        public int permits(DelayedSemaphore ds, Range<Integer> acquireRange) {
            return permits;
        }
    }

    static final DelayConstraint DELAY_STRICT = new DelayConstraint() {
        @Override
        public boolean isSatisfied(DelayedSemaphore ds, long elapsed) {
            return elapsed >= ds.delay();
        }

        @Override
        public String toString() {
            return "DELAY_STRICT";
        }
    };

    @ToString
    final static class PercentDelay implements DelayConstraint {
        final double percent;

        public PercentDelay(double percent) {
            checkArgument(percent > 0);
            this.percent = percent;
        }

        @Override
        public boolean isSatisfied(DelayedSemaphore ds, long elapsed) {
            return elapsed >= (ds.delay() - (ds.delay() * percent));
        }
    }

    @ToString
    final static class RangeDelay implements DelayConstraint {
        final long range;

        public RangeDelay(long range) {
            checkArgument(range > 0);
            this.range = range;
        }

        @Override
        public boolean isSatisfied(DelayedSemaphore ds, long elapsed) {
            assert range < ds.delay();
            return elapsed >= (ds.delay() - range);
        }
    }

    static DelayConstraint percent(double percent) {
        return new PercentDelay(percent);
    }

    static DelayConstraint range(long range) {
        return new RangeDelay(range);
    }

    static final List<Acquire> ACQUIRES = Arrays.asList(ACQUIRE, ACQUIRE_TRY_IMMEDIATE, ACQUIRE_TRY_TIMEOUT,
            ACQUIRE_INCREMENT);

    static final List<Release> RELEASES = Arrays.asList(RELEASE, RELEASE_LOOP, RELEASE_INCREMENT);

    static final List<Permits> PERMITS_LIST = Arrays.asList(PERMITS_ONE, PERMITS_ALL, PERMITS_RANDOM,
            PERMITS_INCREMENT);

    //    static final List<DelayConstraint> CONSTRAINTS = Arrays.asList(DELAY_STRICT);

    //static void runTest(DelayedSemaphore ds, Ticker ticker, int threadCount, int acquisitions, Acquire acquire, Permits permits, Range<Integer> acquireRange, Release release, DelayConstraint constraint) throws InterruptedException {
    /**
     * This method tests every reasonable variation of permits, delay, threads,
     * and acquire/release methods. It is not normally run as it takes a very
     * long time to run (about 2.5 hours on my computer).
     */
    //    @Test
    public void testAll() throws InterruptedException {
        System.out.println(
                "Total Tests: " + (10 * 4 * 6 * 6 * ACQUIRES.size() * RELEASES.size() * PERMITS_LIST.size()));

        final Executor executor = Executors.newFixedThreadPool(8, ThreadUtils.daemonThreadFactory());
        final int A = 10;
        //        for(int delay = 20000000; delay <= 100000000; delay += 40000000) {
        for (int delay : Arrays.asList(20000000, 50000000, 100000000)) {
            //        for(int threads = 3; threads <= 8; threads++) {
            for (int threads : Arrays.asList(1, 2, 3, 4, 6, 8)) {
                for (int limit = 1; limit <= 6 && limit <= threads + 3; limit++) {
                    for (Acquire acquire : ACQUIRES) {
                        for (Release release : RELEASES) {
                            for (Permits permits : PERMITS_LIST) {
                                runTest(executor, DelayedSemaphore.create(limit, delay), Ticker.systemTicker(),
                                        threads, A, acquire, permits, Range.closed(1, limit), //entire permit range
                                        release, range(3000000)) //3ms wiggle room
                                ;

                            } //permits
                        } //releases
                    } //acquires
                } //limit
            } //threads
        } //delay
    }

}