/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.commons.lang3.concurrent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.easymock.EasyMock;
import org.junit.Test;
/**
* Test class for TimedSemaphore.
*
* @version $Id$
*/
public class TimedSemaphoreTest {
/** Constant for the time period. */
private static final long PERIOD = 500;
/** Constant for the time unit. */
private static final TimeUnit UNIT = TimeUnit.MILLISECONDS;
/** Constant for the default limit. */
private static final int LIMIT = 10;
/**
* Tests creating a new instance.
*/
@Test
public void testInit() {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
EasyMock.replay(service);
final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD, UNIT,
LIMIT);
EasyMock.verify(service);
assertEquals("Wrong service", service, semaphore.getExecutorService());
assertEquals("Wrong period", PERIOD, semaphore.getPeriod());
assertEquals("Wrong unit", UNIT, semaphore.getUnit());
assertEquals("Statistic available", 0, semaphore
.getLastAcquiresPerPeriod());
assertEquals("Average available", 0.0, semaphore
.getAverageCallsPerPeriod(), .05);
assertFalse("Already shutdown", semaphore.isShutdown());
assertEquals("Wrong limit", LIMIT, semaphore.getLimit());
}
/**
* Tries to create an instance with a negative period. This should cause an
* exception.
*/
@Test(expected = IllegalArgumentException.class)
public void testInitInvalidPeriod() {
new TimedSemaphore(0L, UNIT, LIMIT);
}
/**
* Tests whether a default executor service is created if no service is
* provided.
*/
@Test
public void testInitDefaultService() {
final TimedSemaphore semaphore = new TimedSemaphore(PERIOD, UNIT, LIMIT);
final ScheduledThreadPoolExecutor exec = (ScheduledThreadPoolExecutor) semaphore
.getExecutorService();
assertFalse("Wrong periodic task policy", exec
.getContinueExistingPeriodicTasksAfterShutdownPolicy());
assertFalse("Wrong delayed task policy", exec
.getExecuteExistingDelayedTasksAfterShutdownPolicy());
assertFalse("Already shutdown", exec.isShutdown());
semaphore.shutdown();
}
/**
* Tests starting the timer.
*/
@Test
public void testStartTimer() throws InterruptedException {
final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(PERIOD,
UNIT, LIMIT);
final ScheduledFuture<?> future = semaphore.startTimer();
assertNotNull("No future returned", future);
Thread.sleep(PERIOD);
final int trials = 10;
int count = 0;
do {
Thread.sleep(PERIOD);
if (count++ > trials) {
fail("endOfPeriod() not called!");
}
} while (semaphore.getPeriodEnds() <= 0);
semaphore.shutdown();
}
/**
* Tests the shutdown() method if the executor belongs to the semaphore. In
* this case it has to be shut down.
*/
@Test
public void testShutdownOwnExecutor() {
final TimedSemaphore semaphore = new TimedSemaphore(PERIOD, UNIT, LIMIT);
semaphore.shutdown();
assertTrue("Not shutdown", semaphore.isShutdown());
assertTrue("Executor not shutdown", semaphore.getExecutorService()
.isShutdown());
}
/**
* Tests the shutdown() method for a shared executor service before a task
* was started. This should do pretty much nothing.
*/
@Test
public void testShutdownSharedExecutorNoTask() {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
EasyMock.replay(service);
final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD, UNIT,
LIMIT);
semaphore.shutdown();
assertTrue("Not shutdown", semaphore.isShutdown());
EasyMock.verify(service);
}
/**
* Prepares an executor service mock to expect the start of the timer.
*
* @param service the mock
* @param future the future
*/
private void prepareStartTimer(final ScheduledExecutorService service,
final ScheduledFuture<?> future) {
service.scheduleAtFixedRate((Runnable) EasyMock.anyObject(), EasyMock
.eq(PERIOD), EasyMock.eq(PERIOD), EasyMock.eq(UNIT));
EasyMock.expectLastCall().andReturn(future);
}
/**
* Tests the shutdown() method for a shared executor after the task was
* started. In this case the task must be canceled.
*/
@Test
public void testShutdownSharedExecutorTask() throws InterruptedException {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
prepareStartTimer(service, future);
EasyMock.expect(Boolean.valueOf(future.cancel(false))).andReturn(Boolean.TRUE);
EasyMock.replay(service, future);
final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
PERIOD, UNIT, LIMIT);
semaphore.acquire();
semaphore.shutdown();
assertTrue("Not shutdown", semaphore.isShutdown());
EasyMock.verify(service, future);
}
/**
* Tests multiple invocations of the shutdown() method.
*/
@Test
public void testShutdownMultipleTimes() throws InterruptedException {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
prepareStartTimer(service, future);
EasyMock.expect(Boolean.valueOf(future.cancel(false))).andReturn(Boolean.TRUE);
EasyMock.replay(service, future);
final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
PERIOD, UNIT, LIMIT);
semaphore.acquire();
for (int i = 0; i < 10; i++) {
semaphore.shutdown();
}
EasyMock.verify(service, future);
}
/**
* Tests the acquire() method if a limit is set.
*/
@Test
public void testAcquireLimit() throws InterruptedException {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
prepareStartTimer(service, future);
EasyMock.replay(service, future);
final int count = 10;
final CountDownLatch latch = new CountDownLatch(count - 1);
final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD, UNIT, 1);
final SemaphoreThread t = new SemaphoreThread(semaphore, latch, count,
count - 1);
semaphore.setLimit(count - 1);
// start a thread that calls the semaphore count times
t.start();
latch.await();
// now the semaphore's limit should be reached and the thread blocked
assertEquals("Wrong semaphore count", count - 1, semaphore
.getAcquireCount());
// this wakes up the thread, it should call the semaphore once more
semaphore.endOfPeriod();
t.join();
assertEquals("Wrong semaphore count (2)", 1, semaphore
.getAcquireCount());
assertEquals("Wrong acquire() count", count - 1, semaphore
.getLastAcquiresPerPeriod());
EasyMock.verify(service, future);
}
/**
* Tests the acquire() method if more threads are involved than the limit.
* This method starts a number of threads that all invoke the semaphore. The
* semaphore's limit is set to 1, so in each period only a single thread can
* acquire the semaphore.
*/
@Test
public void testAcquireMultipleThreads() throws InterruptedException {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
prepareStartTimer(service, future);
EasyMock.replay(service, future);
final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
PERIOD, UNIT, 1);
semaphore.latch = new CountDownLatch(1);
final int count = 10;
final SemaphoreThread[] threads = new SemaphoreThread[count];
for (int i = 0; i < count; i++) {
threads[i] = new SemaphoreThread(semaphore, null, 1, 0);
threads[i].start();
}
for (int i = 0; i < count; i++) {
semaphore.latch.await();
assertEquals("Wrong count", 1, semaphore.getAcquireCount());
semaphore.latch = new CountDownLatch(1);
semaphore.endOfPeriod();
assertEquals("Wrong acquire count", 1, semaphore
.getLastAcquiresPerPeriod());
}
for (int i = 0; i < count; i++) {
threads[i].join();
}
EasyMock.verify(service, future);
}
/**
* Tests the acquire() method if no limit is set. A test thread is started
* that calls the semaphore a large number of times. Even if the semaphore's
* period does not end, the thread should never block.
*/
@Test
public void testAcquireNoLimit() throws InterruptedException {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
prepareStartTimer(service, future);
EasyMock.replay(service, future);
final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(service,
PERIOD, UNIT, TimedSemaphore.NO_LIMIT);
final int count = 1000;
final CountDownLatch latch = new CountDownLatch(count);
final SemaphoreThread t = new SemaphoreThread(semaphore, latch, count, count);
t.start();
latch.await();
EasyMock.verify(service, future);
}
/**
* Tries to call acquire() after shutdown(). This should cause an exception.
*/
@Test(expected = IllegalStateException.class)
public void testPassAfterShutdown() throws InterruptedException {
final TimedSemaphore semaphore = new TimedSemaphore(PERIOD, UNIT, LIMIT);
semaphore.shutdown();
semaphore.acquire();
}
/**
* Tests a bigger number of invocations that span multiple periods. The
* period is set to a very short time. A background thread calls the
* semaphore a large number of times. While it runs at last one end of a
* period should be reached.
*/
@Test
public void testAcquireMultiplePeriods() throws InterruptedException {
final int count = 1000;
final TimedSemaphoreTestImpl semaphore = new TimedSemaphoreTestImpl(
PERIOD / 10, TimeUnit.MILLISECONDS, 1);
semaphore.setLimit(count / 4);
final CountDownLatch latch = new CountDownLatch(count);
final SemaphoreThread t = new SemaphoreThread(semaphore, latch, count, count);
t.start();
latch.await();
semaphore.shutdown();
assertTrue("End of period not reached", semaphore.getPeriodEnds() > 0);
}
/**
* Tests the methods for statistics.
*/
@Test
public void testGetAverageCallsPerPeriod() throws InterruptedException {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
prepareStartTimer(service, future);
EasyMock.replay(service, future);
final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD, UNIT,
LIMIT);
semaphore.acquire();
semaphore.endOfPeriod();
assertEquals("Wrong average (1)", 1.0, semaphore
.getAverageCallsPerPeriod(), .005);
semaphore.acquire();
semaphore.acquire();
semaphore.endOfPeriod();
assertEquals("Wrong average (2)", 1.5, semaphore
.getAverageCallsPerPeriod(), .005);
EasyMock.verify(service, future);
}
/**
* Tests whether the available non-blocking calls can be queried.
*/
@Test
public void testGetAvailablePermits() throws InterruptedException {
final ScheduledExecutorService service = EasyMock
.createMock(ScheduledExecutorService.class);
final ScheduledFuture<?> future = EasyMock.createMock(ScheduledFuture.class);
prepareStartTimer(service, future);
EasyMock.replay(service, future);
final TimedSemaphore semaphore = new TimedSemaphore(service, PERIOD, UNIT,
LIMIT);
for (int i = 0; i < LIMIT; i++) {
assertEquals("Wrong available count at " + i, LIMIT - i, semaphore
.getAvailablePermits());
semaphore.acquire();
}
semaphore.endOfPeriod();
assertEquals("Wrong available count in new period", LIMIT, semaphore
.getAvailablePermits());
EasyMock.verify(service, future);
}
/**
* A specialized implementation of {@code TimedSemaphore} that is easier to
* test.
*/
private static class TimedSemaphoreTestImpl extends TimedSemaphore {
/** A mock scheduled future. */
ScheduledFuture<?> schedFuture;
/** A latch for synchronizing with the main thread. */
volatile CountDownLatch latch;
/** Counter for the endOfPeriod() invocations. */
private int periodEnds;
public TimedSemaphoreTestImpl(final long timePeriod, final TimeUnit timeUnit,
final int limit) {
super(timePeriod, timeUnit, limit);
}
public TimedSemaphoreTestImpl(final ScheduledExecutorService service,
final long timePeriod, final TimeUnit timeUnit, final int limit) {
super(service, timePeriod, timeUnit, limit);
}
/**
* Returns the number of invocations of the endOfPeriod() method.
*
* @return the endOfPeriod() invocations
*/
public int getPeriodEnds() {
synchronized (this) {
return periodEnds;
}
}
/**
* Invokes the latch if one is set.
*/
@Override
public synchronized void acquire() throws InterruptedException {
super.acquire();
if (latch != null) {
latch.countDown();
}
}
/**
* Counts the number of invocations.
*/
@Override
protected synchronized void endOfPeriod() {
super.endOfPeriod();
periodEnds++;
}
/**
* Either returns the mock future or calls the super method.
*/
@Override
protected ScheduledFuture<?> startTimer() {
return schedFuture != null ? schedFuture : super.startTimer();
}
}
/**
* A test thread class that will be used by tests for triggering the
* semaphore. The thread calls the semaphore a configurable number of times.
* When this is done, it can notify the main thread.
*/
private static class SemaphoreThread extends Thread {
/** The semaphore. */
private final TimedSemaphore semaphore;
/** A latch for communication with the main thread. */
private final CountDownLatch latch;
/** The number of acquire() calls. */
private final int count;
/** The number of invocations of the latch. */
private final int latchCount;
public SemaphoreThread(final TimedSemaphore b, final CountDownLatch l, final int c, final int lc) {
semaphore = b;
latch = l;
count = c;
latchCount = lc;
}
/**
* Calls acquire() on the semaphore for the specified number of times.
* Optionally the latch will also be triggered to synchronize with the
* main test thread.
*/
@Override
public void run() {
try {
for (int i = 0; i < count; i++) {
semaphore.acquire();
if (i < latchCount) {
latch.countDown();
}
}
} catch (final InterruptedException iex) {
Thread.currentThread().interrupt();
}
}
}
}