/*
* Copyright 2013 Ben Manes. All Rights Reserved.
*
* Licensed 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 com.github.benmanes.multiway;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import com.github.benmanes.multiway.ResourceKey.Status;
import com.github.benmanes.multiway.TransferPool.LoadingTransferPool;
import com.google.common.base.Stopwatch;
import com.google.common.cache.Weigher;
import com.google.common.testing.FakeTicker;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static com.jayway.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
/**
* @author ben.manes@gmail.com (Ben Manes)
*/
public final class MultiwayPoolTest {
private static final Integer KEY_1 = 1;
private FakeTicker ticker;
private TestResourceLifecycle lifecycle;
private LoadingTransferPool<Integer, UUID> multiway;
@BeforeMethod
public void beforeMethod() {
ticker = new FakeTicker();
lifecycle = new TestResourceLifecycle();
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder());
}
@SuppressWarnings("unchecked")
LoadingTransferPool<Integer, UUID> makeMultiwayPool(MultiwayPoolBuilder<?, ?> builder) {
MultiwayPoolBuilder<Integer, UUID> pools = (MultiwayPoolBuilder<Integer, UUID>) builder;
if (pools.lifecycle == null) {
pools.lifecycle(lifecycle);
}
return (LoadingTransferPool<Integer, UUID>) pools.build(new TestResourceLoader());
}
@Test
public void borrow_concurrent() throws Exception {
final int numThreads = 10;
final int iterations = 100;
ConcurrentTestHarness.timeTasks(numThreads, new Runnable() {
@Override public void run() {
for (int i = 0; i < iterations; i++) {
UUID resource = multiway.borrow(KEY_1);
yield();
multiway.release(resource);
yield();
}
}
});
multiway.cleanUp();
int cycles = numThreads * iterations;
int size = (int) multiway.size();
assertThat(lifecycle.created(), is(size));
assertThat(lifecycle.borrows(), is(cycles));
assertThat(lifecycle.releases(), is(cycles));
//assertThat(multiway.transferQueues.get(KEY_1).size(), is(size));
}
@Test
public void borrow_sameInstance() {
UUID expected = getAndRelease(KEY_1);
UUID actual = getAndRelease(KEY_1);
assertThat(expected, is(actual));
assertThat(lifecycle.borrows(), is(2));
assertThat(lifecycle.releases(), is(2));
assertThat(lifecycle.removals(), is(0));
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void borrow_multipleReleases() {
UUID resource = multiway.borrow(KEY_1);
multiway.release(resource);
multiway.release(resource);
}
@Test(enabled = false)
public void borrow_fromTransfer() throws Exception {
Stopwatch stopwatch = new Stopwatch().start();
final AtomicBoolean start = new AtomicBoolean();
final AtomicBoolean done = new AtomicBoolean();
new Thread() {
@Override public void run() {
start.set(true);
UUID resource = multiway.borrow(KEY_1, 1, TimeUnit.NANOSECONDS);
multiway.release(resource, 1, TimeUnit.MINUTES);
done.set(true);
}
}.start();
await().untilTrue(start);
assertThat(done.get(), is(false));
UUID resource = multiway.borrow(KEY_1);
await().untilTrue(done);
multiway.release(resource);
assertThat(stopwatch.elapsed(TimeUnit.MINUTES), is(0L));
}
@Test
public void borrow_callable() {
MultiwayPool<Integer, UUID> multiway = MultiwayPoolBuilder.newBuilder().build();
final UUID expected = UUID.randomUUID();
UUID resource = multiway.borrow(KEY_1, new Callable<UUID>() {
@Override public UUID call() throws Exception {
return expected;
}
});
assertThat(resource, is(expected));
multiway.release(resource);
}
@Test
public void evict_immediately() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder().maximumSize(0));
UUID first = getAndRelease(KEY_1);
UUID second = getAndRelease(KEY_1);
assertThat(first, is(not(second)));
assertThat(lifecycle.removals(), is(2));
assertThat(multiway.size(), is(0L));
//assertThat(multiway.transferQueues.getIfPresent(KEY_1), is(empty()));
}
@Test
public void evict_whenIdle() {
getAndRelease(KEY_1);
ResourceKey<?> resourceKey = getResourceKey();
assertThat(resourceKey.getStatus(), is(Status.IDLE));
multiway.invalidateAll();
assertThat(multiway.size(), is(0L));
assertThat(resourceKey.getStatus(), is(Status.DEAD));
}
@Test
public void evict_whenInFlight() {
UUID resource = multiway.borrow(KEY_1);
ResourceKey<?> resourceKey = getResourceKey();
assertThat(resourceKey.getStatus(), is(Status.IN_FLIGHT));
multiway.invalidateAll();
assertThat(multiway.size(), is(0L));
assertThat(resourceKey.getStatus(), is(Status.RETIRED));
multiway.release(resource);
assertThat(resourceKey.getStatus(), is(Status.DEAD));
}
@Test
public void evict_whenRetired() {
getAndRelease(KEY_1);
ResourceKey<?> resourceKey = getResourceKey();
// Simulate transition due to idle cache expiration
resourceKey.goFromIdleToRetired();
multiway.invalidateAll();
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.borrows(), is(1));
assertThat(lifecycle.releases(), is(1));
assertThat(lifecycle.removals(), is(1));
assertThat(resourceKey.getStatus(), is(Status.DEAD));
}
@Test
public void evict_multipleQueues() {
for (int i = 0; i < 10; i++) {
getAndRelease(i);
}
assertThat(multiway.size(), is(10L));
multiway.invalidateAll();
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.borrows(), is(10));
assertThat(lifecycle.releases(), is(10));
assertThat(lifecycle.removals(), is(10));
}
@Test
public void evict_maximumSize() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder().maximumSize(10));
List<UUID> resources = new ArrayList<>();
for (int i = 0; i < 100; i++) {
resources.add(multiway.borrow(KEY_1));
}
for (UUID resource : resources) {
multiway.release(resource);
}
assertThat(multiway.size(), is(10L));
assertThat(lifecycle.borrows(), is(100));
assertThat(lifecycle.releases(), is(100));
assertThat(lifecycle.removals(), is(90));
}
@Test
public void evict_maximumWeight() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder().maximumWeight(10)
.weigher(new Weigher<Integer, UUID>() {
@Override public int weigh(Integer key, UUID resource) {
return 5;
}
}));
List<UUID> resources = new ArrayList<>();
for (int i = 0; i < 100; i++) {
resources.add(multiway.borrow(KEY_1));
}
for (UUID resource : resources) {
multiway.release(resource);
}
assertThat(multiway.size(), is(2L));
assertThat(lifecycle.borrows(), is(100));
assertThat(lifecycle.releases(), is(100));
assertThat(lifecycle.removals(), is(98));
}
@Test
public void evict_expireAfterAccess() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
.ticker(ticker).expireAfterAccess(1, TimeUnit.MINUTES));
List<UUID> resources = new ArrayList<>();
for (int i = 0; i < 100; i++) {
resources.add(multiway.borrow(KEY_1));
}
for (UUID resource : resources) {
multiway.release(resource);
}
ticker.advance(10, TimeUnit.MINUTES);
multiway.cleanUp();
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.borrows(), is(100));
assertThat(lifecycle.releases(), is(100));
assertThat(lifecycle.removals(), is(100));
}
@Test
public void evict_expireAfterWrite() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
.ticker(ticker).expireAfterWrite(1, TimeUnit.MINUTES));
List<UUID> resources = new ArrayList<>();
for (int i = 0; i < 100; i++) {
resources.add(multiway.borrow(KEY_1));
}
for (UUID resource : resources) {
multiway.release(resource);
}
ticker.advance(10, TimeUnit.MINUTES);
multiway.cleanUp();
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.borrows(), is(100));
assertThat(lifecycle.releases(), is(100));
assertThat(lifecycle.removals(), is(100));
}
@Test
public void release_toPool() {
UUID resource = multiway.borrow(KEY_1);
assertThat(multiway.size(), is(1L));
assertThat(lifecycle.borrows(), is(1));
assertThat(lifecycle.releases(), is(0));
assertThat(lifecycle.removals(), is(0));
TransferPool<Integer, UUID>.ResourceHandle handle = multiway.inFlight.get().get(resource);
assertThat(handle.resourceKey.getStatus(), is(Status.IN_FLIGHT));
multiway.release(resource);
assertThat(multiway.size(), is(1L));
assertThat(lifecycle.releases(), is(1));
assertThat(lifecycle.removals(), is(0));
assertThat(handle.toString(), handle.resourceKey.getStatus(), is(Status.IDLE));
}
@Test
public void release_andDiscard() {
UUID resource = multiway.borrow(KEY_1);
TransferPool<Integer, UUID>.ResourceHandle handle = multiway.inFlight.get().get(resource);
multiway.invalidateAll();
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.borrows(), is(1));
assertThat(lifecycle.releases(), is(0));
assertThat(lifecycle.removals(), is(0));
assertThat(handle.resourceKey.getStatus(), is(Status.RETIRED));
multiway.release(resource);
assertThat(lifecycle.releases(), is(1));
assertThat(lifecycle.removals(), is(1));
assertThat(handle.resourceKey.getStatus(), is(Status.DEAD));
}
@Test
public void invalidate_whenInFlight() {
UUID resource = multiway.borrow(KEY_1);
TransferPool<Integer, UUID>.ResourceHandle handle = multiway.inFlight.get().get(resource);
assertThat(multiway.size(), is(1L));
assertThat(lifecycle.borrows(), is(1));
assertThat(lifecycle.releases(), is(0));
assertThat(lifecycle.removals(), is(0));
assertThat(handle.resourceKey.getStatus(), is(Status.IN_FLIGHT));
multiway.releaseAndInvalidate(resource);
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.releases(), is(1));
assertThat(lifecycle.removals(), is(1));
assertThat(handle.resourceKey.getStatus(), is(Status.DEAD));
}
@Test
public void invalidate_whenRetired() {
UUID resource = multiway.borrow(KEY_1);
TransferPool<Integer, UUID>.ResourceHandle handle = multiway.inFlight.get().get(resource);
multiway.invalidateAll();
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.borrows(), is(1));
assertThat(lifecycle.releases(), is(0));
assertThat(lifecycle.removals(), is(0));
assertThat(handle.resourceKey.getStatus(), is(Status.RETIRED));
multiway.releaseAndInvalidate(resource);
assertThat(multiway.size(), is(0L));
assertThat(lifecycle.releases(), is(1));
assertThat(lifecycle.removals(), is(1));
assertThat(handle.resourceKey.getStatus(), is(Status.DEAD));
}
//
// @Test
// public void discardPool() {
// UUID resource = multiway.borrow(KEY_1);
// GcFinalization.awaitFullGc();
// assertThat(multiway.transferStacks.size(), is(1L));
//
// multiway.release(resource);
// multiway.invalidateAll();
//
// GcFinalization.awaitFullGc();
// multiway.transferStacks.cleanUp();
// assertThat(multiway.transferStacks.size(), is(0L));
// }
@Test
public void invalidate() {
for (int i = 0; i < 10; i++) {
getAndRelease(i);
}
multiway.invalidate(5);
assertThat(multiway.size(), is(9L));
}
@Test
public void invalidateAll() {
for (int i = 0; i < 10; i++) {
getAndRelease(i);
}
multiway.invalidateAll();
assertThat(multiway.size(), is(0L));
}
@Test
public void tryToGetPooledResourceHandle_notFound() {
getAndRelease(KEY_1);
ResourceKey<Integer> resourceKey = getResourceKey();
multiway.invalidateAll();
assertThat(multiway.tryToGetPooledResourceHandle(resourceKey), is(nullValue()));
}
@Test
public void tryToGetPooledResourceHandle_notIdle() {
getAndRelease(KEY_1);
ResourceKey<Integer> resourceKey = getResourceKey();
resourceKey.goFromIdleToRetired();
assertThat(multiway.tryToGetPooledResourceHandle(resourceKey), is(nullValue()));
}
@Test
public void stats() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder().recordStats());
getAndRelease(KEY_1);
getAndRelease(KEY_1);
assertThat(multiway.stats().hitCount(), is(1L));
assertThat(multiway.stats().loadSuccessCount(), is(1L));
}
//
// @Test
// public void lifecycle_onCreate_fail() {
// final AtomicBoolean onRemovalCalled = new AtomicBoolean();
// multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
// .lifecycle(new ResourceLifecycle<Integer, UUID>() {
// @Override public void onCreate(Integer key, UUID resource) {
// throw new UnsupportedOperationException();
// }
// @Override public void onRemoval(Integer key, UUID resource) {
// onRemovalCalled.set(true);
// }
// }));
// try {
// getAndRelease(KEY_1);
// Assert.fail();
// } catch (Exception e) {
// assertThat(multiway.cache.size(), is(0L));
// assertThat(onRemovalCalled.get(), is(true));
// }
// }
//
// @Test
// public void lifecycle_onBorrow_fail() {
// final AtomicBoolean onRemovalCalled = new AtomicBoolean();
// multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
// .lifecycle(new ResourceLifecycle<Integer, UUID>() {
// @Override public void onBorrow(Integer key, UUID resource) {
// throw new UnsupportedOperationException();
// }
// @Override public void onRemoval(Integer key, UUID resource) {
// onRemovalCalled.set(true);
// }
// }));
// try {
// getAndRelease(KEY_1);
// Assert.fail();
// } catch (Exception e) {
// assertThat(multiway.cache.size(), is(0L));
// assertThat(onRemovalCalled.get(), is(true));
// }
// }
//
// @Test
// public void lifecycle_onRelease_fail() {
// final AtomicBoolean onRemovalCalled = new AtomicBoolean();
// multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
// .lifecycle(new ResourceLifecycle<Integer, UUID>() {
// @Override public void onRelease(Integer key, UUID resource) {
// throw new UnsupportedOperationException();
// }
// @Override public void onRemoval(Integer key, UUID resource) {
// onRemovalCalled.set(true);
// }
// }));
// try {
// getAndRelease(KEY_1);
// Assert.fail();
// } catch (Exception e) {
// assertThat(multiway.cache.size(), is(0L));
// assertThat(onRemovalCalled.get(), is(true));
// }
// }
//
// @Test
// public void lifecycle_onRemove_fail_pool() {
// multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
// .lifecycle(new ResourceLifecycle<Integer, UUID>() {
// @Override public void onRemoval(Integer key, UUID resource) {
// throw new UnsupportedOperationException();
// }
// }));
// getAndRelease(KEY_1);
// multiway.invalidateAll();
// assertThat(multiway.cache.size(), is(0L));
// }
@Test
public void lifecycle_onRemove_fail_handle() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
.lifecycle(new ResourceLifecycle<Integer, UUID>() {
@Override public void onRemoval(Integer key, UUID resource) {
throw new UnsupportedOperationException();
}
}));
UUID resource = multiway.borrow(KEY_1);
try {
multiway.releaseAndInvalidate(resource);
Assert.fail();
} catch (UnsupportedOperationException e) {
assertThat(multiway.size(), is(0L));
}
}
@Test
public void unlockOnCleanup() {
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES));
Runnable runner = new Runnable() {
@Override public void run() {
throw new IllegalStateException();
}
};
getAndRelease(KEY_1);
try {
multiway.timeToIdlePolicy.get().schedule(getResourceKey(), runner);
Assert.fail();
} catch (IllegalStateException e) {
assertThat(((ReentrantLock) multiway.timeToIdlePolicy.get().idleLock).isLocked(), is(false));
}
}
@Test
public void concurrent() throws Exception {
long maxSize = 10;
multiway = makeMultiwayPool(MultiwayPoolBuilder.newBuilder()
.expireAfterAccess(100, TimeUnit.NANOSECONDS)
.maximumSize(maxSize));
ConcurrentTestHarness.timeTasks(10, new Runnable() {
final ThreadLocalRandom random = ThreadLocalRandom.current();
@Override
public void run() {
Deque<UUID> resources = new ArrayDeque<>();
for (int i = 0; i < 100; i++) {
execute(resources, i);
yield();
}
for (UUID resource : resources) {
multiway.release(resource);
}
}
void execute(Deque<UUID> resources, int key) {
if (random.nextBoolean()) {
UUID resource = multiway.borrow(key);
resources.add(resource);
} else if (!resources.isEmpty()) {
UUID resource = resources.remove();
multiway.release(resource);
}
}
});
multiway.cleanUp();
// long queued = 0;
long size = multiway.size();
// for (Queue<?> queue : multiway.transferQueues.asMap().values()) {
// queued += queue.size();
// }
//
// assertThat(queued, is(size));
assertThat(size, lessThanOrEqualTo(maxSize));
assertThat(lifecycle.releases(), is(lifecycle.borrows()));
assertThat(lifecycle.created(), is((int) size + lifecycle.removals()));
}
private UUID getAndRelease(Integer key) {
UUID resource = multiway.borrow(key);
multiway.release(resource);
return resource;
}
private ResourceKey<Integer> getResourceKey() {
return multiway.cache.keySet().iterator().next();
}
private void yield() {
Thread.yield();
LockSupport.parkNanos(1L);
}
private static final class TestResourceLoader implements ResourceLoader<Integer, UUID> {
@Override public UUID load(Integer key) throws Exception {
return UUID.randomUUID();
}
}
private static final class TestResourceLifecycle extends ResourceLifecycle<Integer, UUID> {
final AtomicInteger created = new AtomicInteger();
final AtomicInteger borrows = new AtomicInteger();
final AtomicInteger releases = new AtomicInteger();
final AtomicInteger removals = new AtomicInteger();
@Override
public void onCreate(Integer key, UUID resource) {
created.incrementAndGet();
}
@Override
public void onBorrow(Integer key, UUID resource) {
borrows.incrementAndGet();
}
@Override
public void onRelease(Integer key, UUID resource) {
releases.incrementAndGet();
}
@Override
public void onRemoval(Integer key, UUID resource) {
removals.incrementAndGet();
}
public int created() {
return created.get();
}
public int borrows() {
return borrows.get();
}
public int releases() {
return releases.get();
}
public int removals() {
return removals.get();
}
}
}