package com.netflix.governator.lifecycle;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Named;
import org.apache.log4j.Level;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.name.Names;
import com.netflix.governator.guice.LifecycleInjector;
import com.netflix.governator.guice.LifecycleInjectorBuilder;
import com.netflix.governator.guice.LifecycleInjectorMode;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
@RunWith(DataProviderRunner.class)
public class PreDestroyStressTest {
static final Key<LifecycleSubject> LIFECYCLE_SUBJECT_KEY = Key.get(LifecycleSubject.class);
private static final int TEST_TIME_IN_SECONDS = 10; // run the stress test for this many seconds
private static final int CONCURRENCY_LEVEL = 20; // run the stress test with this many threads
private final class ScopingModule extends AbstractModule {
LocalScope localScope = new LocalScope();
@Override
protected void configure() {
bindScope(LocalScoped.class, localScope);
bind(LifecycleSubject.class).in(localScope);
}
@Provides
@Singleton
@Named("thing1")
public LifecycleSubject thing1() {
return new LifecycleSubject("thing1");
}
}
static class LifecycleSubject {
private static AtomicInteger instanceCounter = new AtomicInteger(0);
private static AtomicInteger preDestroyCounter = new AtomicInteger(0);
private static AtomicInteger postConstructCounter = new AtomicInteger(0);
private static Logger logger = LoggerFactory.getLogger(LifecycleSubject.class);
private static byte[] bulkTemplate;
static {
bulkTemplate = new byte[1024*100];
Arrays.fill(bulkTemplate, (byte)0);
}
private String name;
private volatile boolean postConstructed = false;
private volatile boolean preDestroyed = false;
private byte[] bulk;
private final String toString;
public LifecycleSubject() {
this("anonymous");
}
public LifecycleSubject(String name) {
this.name = name;
this.bulk = bulkTemplate.clone();
int instanceId = instanceCounter.incrementAndGet();
this.toString = "LifecycleSubject@" + instanceId + '[' + name + ']';
logger.info("created instance {} {}", toString, instanceId);
}
public static void clear() {
instanceCounter.set(0);
postConstructCounter.set(0);
preDestroyCounter.set(0);
}
@PostConstruct
public void init() {
logger.info("@PostConstruct called {} {}", toString, postConstructCounter.incrementAndGet());
this.postConstructed = true;
}
@PreDestroy
public void destroy() {
logger.info("@PreDestroy called {} {}", toString, preDestroyCounter.incrementAndGet());
this.preDestroyed = true;
}
public boolean isPostConstructed() {
return postConstructed;
}
public boolean isPreDestroyed() {
return preDestroyed;
}
public String getName() {
return name;
}
public String toString() {
return toString;
}
}
@Test
@UseDataProvider("builders")
public void testInParallel(String name, LifecycleInjectorBuilder lifecycleInjectorBuilder) throws Exception {
LifecycleSubject.clear();
ScopingModule scopingModule = new ScopingModule();
final LocalScope localScope = scopingModule.localScope;
LifecycleInjector lifecycleInjector = LifecycleInjector.builder()
.withAdditionalModules(scopingModule).build();
SecureRandom random = new SecureRandom();
LifecycleSubject thing1 = null;
try (com.netflix.governator.lifecycle.LifecycleManager legacyLifecycleManager = lifecycleInjector.getLifecycleManager()) {
org.apache.log4j.Logger.getLogger("com.netflix.governator").setLevel(Level.WARN);
final Injector injector = lifecycleInjector.createInjector();
legacyLifecycleManager.start();
thing1 = injector.getInstance(Key.get(LifecycleSubject.class, Names.named("thing1")));
Assert.assertEquals("singleton instance not postConstructed", LifecycleSubject.postConstructCounter.get(), LifecycleSubject.instanceCounter.get());
Assert.assertEquals("singleton instance predestroyed too soon", LifecycleSubject.preDestroyCounter.get(), LifecycleSubject.instanceCounter.get()-1);
Callable<Void> scopingTask = allocateScopedInstance(injector, localScope, random);
runInParallel(CONCURRENCY_LEVEL, scopingTask, TEST_TIME_IN_SECONDS, TimeUnit.SECONDS);
System.gc();
Thread.sleep(1000);
System.out.println("instances count: " + LifecycleSubject.instanceCounter.get());
System.out.flush();
Assert.assertEquals("instances not postConstructed", LifecycleSubject.postConstructCounter.get(), LifecycleSubject.instanceCounter.get());
Assert.assertEquals("scoped instances not predestroyed", LifecycleSubject.preDestroyCounter.get(), LifecycleSubject.instanceCounter.get()-1);
Assert.assertFalse("singleton instance was predestroyed too soon", thing1.isPreDestroyed());
}
finally {
org.apache.log4j.Logger.getLogger("com.netflix.governator").setLevel(Level.DEBUG);
}
Assert.assertTrue("singleton instance was not predestroyed", thing1.isPreDestroyed());
Thread.yield();
}
void runInParallel(int concurrency, final Callable<Void> task, int duration, TimeUnit timeUnits) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(concurrency, new ThreadFactoryBuilder().setNameFormat("predestroy-stress-test-%d").build());
final AtomicBoolean running = new AtomicBoolean(true);
try {
List<Callable<Void>> tasks = new ArrayList<>();
for (int i=0; i < concurrency; i++) {
tasks.add(new Callable<Void>() {
@Override
public Void call() {
while (running.get()) {
try {
task.call();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
});
}
es.invokeAll(tasks, duration, timeUnits);
}
finally {
running.set(false);
es.shutdown();
es.awaitTermination(10, TimeUnit.SECONDS);
}
es = null;
}
Callable<Void> allocateScopedInstance(final Injector injector, final LocalScope localScope, final SecureRandom random) {
return new Callable<Void>() {
public Void call() throws InterruptedException {
localScope.enter();
try {
LifecycleSubject anonymous = injector.getInstance(LIFECYCLE_SUBJECT_KEY);
Thread.sleep(random.nextInt(500));
Assert.assertTrue(anonymous.isPostConstructed());
Assert.assertFalse(anonymous.isPreDestroyed());
}
catch(InterruptedException e) {
}
finally {
localScope.exit();
}
return null;
}
};
}
@DataProvider
public static Object[][] builders()
{
return new Object[][]
{
new Object[] { "simulatedChildInjector", LifecycleInjector.builder().withMode(LifecycleInjectorMode.SIMULATED_CHILD_INJECTORS) },
new Object[] { "childInjector", LifecycleInjector.builder() }
};
}
}