blob: adb56be6802b98b5adc62e054418606093e48611 [file] [log] [blame]
/*
* Copyright 2014, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc.internal;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import io.grpc.internal.SharedResourceHolder.Resource;
import java.util.LinkedList;
import java.util.concurrent.Delayed;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/** Unit tests for {@link SharedResourceHolder}. */
@RunWith(JUnit4.class)
public class SharedResourceHolderTest {
private final LinkedList<MockScheduledFuture<?>> scheduledDestroyTasks =
new LinkedList<MockScheduledFuture<?>>();
private SharedResourceHolder holder;
private static class ResourceInstance {
volatile boolean closed;
}
private static class ResourceFactory implements Resource<ResourceInstance> {
@Override
public ResourceInstance create() {
return new ResourceInstance();
}
@Override
public void close(ResourceInstance instance) {
instance.closed = true;
}
}
// Defines two kinds of resources
private static final Resource<ResourceInstance> SHARED_FOO = new ResourceFactory();
private static final Resource<ResourceInstance> SHARED_BAR = new ResourceFactory();
@Before public void setUp() {
holder = new SharedResourceHolder(new MockExecutorFactory());
}
@Test public void destroyResourceWhenRefCountReachesZero() {
ResourceInstance foo1 = holder.getInternal(SHARED_FOO);
ResourceInstance sharedFoo = foo1;
ResourceInstance foo2 = holder.getInternal(SHARED_FOO);
assertSame(sharedFoo, foo2);
ResourceInstance bar1 = holder.getInternal(SHARED_BAR);
ResourceInstance sharedBar = bar1;
foo2 = holder.releaseInternal(SHARED_FOO, foo2);
// foo refcount not reached 0, thus shared foo is not closed
assertTrue(scheduledDestroyTasks.isEmpty());
assertFalse(sharedFoo.closed);
assertNull(foo2);
foo1 = holder.releaseInternal(SHARED_FOO, foo1);
assertNull(foo1);
// foo refcount has reached 0, a destroying task is scheduled
assertEquals(1, scheduledDestroyTasks.size());
MockScheduledFuture<?> scheduledDestroyTask = scheduledDestroyTasks.poll();
assertEquals(SharedResourceHolder.DESTROY_DELAY_SECONDS,
scheduledDestroyTask.getDelay(TimeUnit.SECONDS));
// Simluate that the destroyer executes the foo destroying task
scheduledDestroyTask.runTask();
assertTrue(sharedFoo.closed);
// After the destroying, obtaining a foo will get a different instance
ResourceInstance foo3 = holder.getInternal(SHARED_FOO);
assertNotSame(sharedFoo, foo3);
bar1 = holder.releaseInternal(SHARED_BAR, bar1);
// bar refcount has reached 0, a destroying task is scheduled
assertEquals(1, scheduledDestroyTasks.size());
scheduledDestroyTask = scheduledDestroyTasks.poll();
assertEquals(SharedResourceHolder.DESTROY_DELAY_SECONDS,
scheduledDestroyTask.getDelay(TimeUnit.SECONDS));
// Simulate that the destroyer executes the bar destroying task
scheduledDestroyTask.runTask();
assertTrue(sharedBar.closed);
}
@Test public void cancelDestroyTask() {
ResourceInstance foo1 = holder.getInternal(SHARED_FOO);
ResourceInstance sharedFoo = foo1;
foo1 = holder.releaseInternal(SHARED_FOO, foo1);
// A destroying task for foo is scheduled
MockScheduledFuture<?> scheduledDestroyTask = scheduledDestroyTasks.poll();
assertFalse(scheduledDestroyTask.cancelled);
// obtaining a foo before the destroying task is executed will cancel the destroy
ResourceInstance foo2 = holder.getInternal(SHARED_FOO);
assertTrue(scheduledDestroyTask.cancelled);
assertTrue(scheduledDestroyTasks.isEmpty());
assertFalse(sharedFoo.closed);
// And it will be the same foo instance
assertSame(sharedFoo, foo2);
// Release it and the destroying task is scheduled again
foo2 = holder.releaseInternal(SHARED_FOO, foo2);
scheduledDestroyTask = scheduledDestroyTasks.poll();
assertFalse(scheduledDestroyTask.cancelled);
scheduledDestroyTask.runTask();
assertTrue(sharedFoo.closed);
}
@Test public void releaseWrongInstance() {
ResourceInstance uncached = new ResourceInstance();
try {
holder.releaseInternal(SHARED_FOO, uncached);
fail("Should throw IllegalArgumentException");
} catch (IllegalArgumentException e) {
// expected
}
ResourceInstance cached = holder.getInternal(SHARED_FOO);
try {
holder.releaseInternal(SHARED_FOO, uncached);
fail("Should throw IllegalArgumentException");
} catch (IllegalArgumentException e) {
// expected
}
holder.releaseInternal(SHARED_FOO, cached);
}
@Test public void overreleaseInstance() {
ResourceInstance foo1 = holder.getInternal(SHARED_FOO);
holder.releaseInternal(SHARED_FOO, foo1);
try {
holder.releaseInternal(SHARED_FOO, foo1);
fail("Should throw IllegalStateException");
} catch (IllegalStateException e) {
// expected
}
}
private class MockExecutorFactory implements
SharedResourceHolder.ScheduledExecutorFactory {
@Override
public ScheduledExecutorService createScheduledExecutor() {
ScheduledExecutorService mockExecutor = mock(ScheduledExecutorService.class);
when(mockExecutor.schedule(any(Runnable.class), anyLong(), any(TimeUnit.class))).thenAnswer(
new Answer<MockScheduledFuture<Void>>() {
@Override
public MockScheduledFuture<Void> answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Runnable command = (Runnable) args[0];
long delay = (Long) args[1];
TimeUnit unit = (TimeUnit) args[2];
MockScheduledFuture<Void> future = new MockScheduledFuture<Void>(
command, delay, unit);
scheduledDestroyTasks.add(future);
return future;
}
});
return mockExecutor;
}
}
private static class MockScheduledFuture<V> implements ScheduledFuture<V> {
private boolean cancelled;
private boolean finished;
final Runnable command;
final long delay;
final TimeUnit unit;
MockScheduledFuture(Runnable command, long delay, TimeUnit unit) {
this.command = command;
this.delay = delay;
this.unit = unit;
}
void runTask() {
command.run();
finished = true;
}
@Override
public boolean cancel(boolean interrupt) {
if (cancelled || finished) {
return false;
}
cancelled = true;
return true;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public long getDelay(TimeUnit targetUnit) {
return targetUnit.convert(this.delay, this.unit);
}
@Override
public int compareTo(Delayed o) {
throw new UnsupportedOperationException();
}
@Override
public boolean isDone() {
return cancelled || finished;
}
@Override
public V get() {
throw new UnsupportedOperationException();
}
@Override
public V get(long timeout, TimeUnit unit) {
throw new UnsupportedOperationException();
}
}
}