1 /* 2 * Copyright 2014 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.internal; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotSame; 22 import static org.junit.Assert.assertNull; 23 import static org.junit.Assert.assertSame; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assert.fail; 26 import static org.mockito.Matchers.any; 27 import static org.mockito.Matchers.anyLong; 28 import static org.mockito.Mockito.mock; 29 import static org.mockito.Mockito.when; 30 31 import io.grpc.internal.SharedResourceHolder.Resource; 32 import java.util.LinkedList; 33 import java.util.concurrent.Delayed; 34 import java.util.concurrent.ScheduledExecutorService; 35 import java.util.concurrent.ScheduledFuture; 36 import java.util.concurrent.TimeUnit; 37 import org.junit.Before; 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 import org.junit.runners.JUnit4; 41 import org.mockito.invocation.InvocationOnMock; 42 import org.mockito.stubbing.Answer; 43 44 /** Unit tests for {@link SharedResourceHolder}. */ 45 @RunWith(JUnit4.class) 46 public class SharedResourceHolderTest { 47 48 private final LinkedList<MockScheduledFuture<?>> scheduledDestroyTasks = 49 new LinkedList<MockScheduledFuture<?>>(); 50 51 private SharedResourceHolder holder; 52 53 private static class ResourceInstance { 54 volatile boolean closed; 55 } 56 57 private static class ResourceFactory implements Resource<ResourceInstance> { 58 @Override create()59 public ResourceInstance create() { 60 return new ResourceInstance(); 61 } 62 63 @Override close(ResourceInstance instance)64 public void close(ResourceInstance instance) { 65 instance.closed = true; 66 } 67 } 68 69 // Defines two kinds of resources 70 private static final Resource<ResourceInstance> SHARED_FOO = new ResourceFactory(); 71 private static final Resource<ResourceInstance> SHARED_BAR = new ResourceFactory(); 72 setUp()73 @Before public void setUp() { 74 holder = new SharedResourceHolder(new MockExecutorFactory()); 75 } 76 destroyResourceWhenRefCountReachesZero()77 @Test public void destroyResourceWhenRefCountReachesZero() { 78 ResourceInstance foo1 = holder.getInternal(SHARED_FOO); 79 ResourceInstance sharedFoo = foo1; 80 ResourceInstance foo2 = holder.getInternal(SHARED_FOO); 81 assertSame(sharedFoo, foo2); 82 83 ResourceInstance bar1 = holder.getInternal(SHARED_BAR); 84 ResourceInstance sharedBar = bar1; 85 86 foo2 = holder.releaseInternal(SHARED_FOO, foo2); 87 // foo refcount not reached 0, thus shared foo is not closed 88 assertTrue(scheduledDestroyTasks.isEmpty()); 89 assertFalse(sharedFoo.closed); 90 assertNull(foo2); 91 92 foo1 = holder.releaseInternal(SHARED_FOO, foo1); 93 assertNull(foo1); 94 95 // foo refcount has reached 0, a destroying task is scheduled 96 assertEquals(1, scheduledDestroyTasks.size()); 97 MockScheduledFuture<?> scheduledDestroyTask = scheduledDestroyTasks.poll(); 98 assertEquals(SharedResourceHolder.DESTROY_DELAY_SECONDS, 99 scheduledDestroyTask.getDelay(TimeUnit.SECONDS)); 100 101 // Simluate that the destroyer executes the foo destroying task 102 scheduledDestroyTask.runTask(); 103 assertTrue(sharedFoo.closed); 104 105 // After the destroying, obtaining a foo will get a different instance 106 ResourceInstance foo3 = holder.getInternal(SHARED_FOO); 107 assertNotSame(sharedFoo, foo3); 108 109 bar1 = holder.releaseInternal(SHARED_BAR, bar1); 110 111 // bar refcount has reached 0, a destroying task is scheduled 112 assertEquals(1, scheduledDestroyTasks.size()); 113 scheduledDestroyTask = scheduledDestroyTasks.poll(); 114 assertEquals(SharedResourceHolder.DESTROY_DELAY_SECONDS, 115 scheduledDestroyTask.getDelay(TimeUnit.SECONDS)); 116 117 // Simulate that the destroyer executes the bar destroying task 118 scheduledDestroyTask.runTask(); 119 assertTrue(sharedBar.closed); 120 } 121 cancelDestroyTask()122 @Test public void cancelDestroyTask() { 123 ResourceInstance foo1 = holder.getInternal(SHARED_FOO); 124 ResourceInstance sharedFoo = foo1; 125 foo1 = holder.releaseInternal(SHARED_FOO, foo1); 126 // A destroying task for foo is scheduled 127 MockScheduledFuture<?> scheduledDestroyTask = scheduledDestroyTasks.poll(); 128 assertFalse(scheduledDestroyTask.cancelled); 129 130 // obtaining a foo before the destroying task is executed will cancel the destroy 131 ResourceInstance foo2 = holder.getInternal(SHARED_FOO); 132 assertTrue(scheduledDestroyTask.cancelled); 133 assertTrue(scheduledDestroyTasks.isEmpty()); 134 assertFalse(sharedFoo.closed); 135 136 // And it will be the same foo instance 137 assertSame(sharedFoo, foo2); 138 139 // Release it and the destroying task is scheduled again 140 foo2 = holder.releaseInternal(SHARED_FOO, foo2); 141 scheduledDestroyTask = scheduledDestroyTasks.poll(); 142 assertFalse(scheduledDestroyTask.cancelled); 143 scheduledDestroyTask.runTask(); 144 assertTrue(sharedFoo.closed); 145 } 146 releaseWrongInstance()147 @Test public void releaseWrongInstance() { 148 ResourceInstance uncached = new ResourceInstance(); 149 try { 150 holder.releaseInternal(SHARED_FOO, uncached); 151 fail("Should throw IllegalArgumentException"); 152 } catch (IllegalArgumentException e) { 153 // expected 154 } 155 ResourceInstance cached = holder.getInternal(SHARED_FOO); 156 try { 157 holder.releaseInternal(SHARED_FOO, uncached); 158 fail("Should throw IllegalArgumentException"); 159 } catch (IllegalArgumentException e) { 160 // expected 161 } 162 holder.releaseInternal(SHARED_FOO, cached); 163 } 164 overreleaseInstance()165 @Test public void overreleaseInstance() { 166 ResourceInstance foo1 = holder.getInternal(SHARED_FOO); 167 holder.releaseInternal(SHARED_FOO, foo1); 168 try { 169 holder.releaseInternal(SHARED_FOO, foo1); 170 fail("Should throw IllegalStateException"); 171 } catch (IllegalStateException e) { 172 // expected 173 } 174 } 175 176 private class MockExecutorFactory implements 177 SharedResourceHolder.ScheduledExecutorFactory { 178 @Override createScheduledExecutor()179 public ScheduledExecutorService createScheduledExecutor() { 180 ScheduledExecutorService mockExecutor = mock(ScheduledExecutorService.class); 181 when(mockExecutor.schedule(any(Runnable.class), anyLong(), any(TimeUnit.class))).thenAnswer( 182 new Answer<MockScheduledFuture<Void>>() { 183 @Override 184 public MockScheduledFuture<Void> answer(InvocationOnMock invocation) { 185 Object[] args = invocation.getArguments(); 186 Runnable command = (Runnable) args[0]; 187 long delay = (Long) args[1]; 188 TimeUnit unit = (TimeUnit) args[2]; 189 MockScheduledFuture<Void> future = new MockScheduledFuture<Void>( 190 command, delay, unit); 191 scheduledDestroyTasks.add(future); 192 return future; 193 } 194 }); 195 return mockExecutor; 196 } 197 } 198 199 private static class MockScheduledFuture<V> implements ScheduledFuture<V> { 200 private boolean cancelled; 201 private boolean finished; 202 final Runnable command; 203 final long delay; 204 final TimeUnit unit; 205 MockScheduledFuture(Runnable command, long delay, TimeUnit unit)206 MockScheduledFuture(Runnable command, long delay, TimeUnit unit) { 207 this.command = command; 208 this.delay = delay; 209 this.unit = unit; 210 } 211 runTask()212 void runTask() { 213 command.run(); 214 finished = true; 215 } 216 217 @Override cancel(boolean interrupt)218 public boolean cancel(boolean interrupt) { 219 if (cancelled || finished) { 220 return false; 221 } 222 cancelled = true; 223 return true; 224 } 225 226 @Override isCancelled()227 public boolean isCancelled() { 228 return cancelled; 229 } 230 231 @Override getDelay(TimeUnit targetUnit)232 public long getDelay(TimeUnit targetUnit) { 233 return targetUnit.convert(this.delay, this.unit); 234 } 235 236 @Override compareTo(Delayed o)237 public int compareTo(Delayed o) { 238 throw new UnsupportedOperationException(); 239 } 240 241 @Override isDone()242 public boolean isDone() { 243 return cancelled || finished; 244 } 245 246 @Override get()247 public V get() { 248 throw new UnsupportedOperationException(); 249 } 250 251 @Override get(long timeout, TimeUnit unit)252 public V get(long timeout, TimeUnit unit) { 253 throw new UnsupportedOperationException(); 254 } 255 } 256 } 257