package org.drools.compiler.kie.builder.impl; import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.drools.compiler.kie.builder.impl.KieRepositoryImpl.ComparableVersion; import org.drools.compiler.kie.builder.impl.KieRepositoryImpl.KieModuleRepo; import org.drools.compiler.kproject.ReleaseIdImpl; import org.drools.core.common.ResourceProvider; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.kie.api.builder.KieModule; import org.kie.api.builder.KieRepository; import org.kie.api.builder.ReleaseId; import org.kie.api.builder.model.KieBaseModel; /** * This test contains * - normal tests that test for concurrency issues and memory leaks (that the KieModuleRepo * functions as a LRU cache, and evicts old {@link KieModule} instances ) */ public class KieModuleRepoTest { private KieModuleRepo kieModuleRepo; private int maxSizeGaCacheOrig; private int maxSizeGaVersionsCacheOrig; private Field maxSizeGaCacheField; private Field maxSizeGaVersionsCacheField; @Before public void before() throws Exception { kieModuleRepo = new KieModuleRepo(); // store the original values as we need to restore them after the test maxSizeGaCacheOrig = KieModuleRepo.MAX_SIZE_GA_CACHE; maxSizeGaVersionsCacheOrig = KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE; maxSizeGaCacheField = KieModuleRepo.class.getDeclaredField("MAX_SIZE_GA_CACHE"); maxSizeGaVersionsCacheField = KieModuleRepo.class.getDeclaredField("MAX_SIZE_GA_VERSIONS_CACHE"); } @After public void after() throws Exception { setFinalField(maxSizeGaCacheField, null, maxSizeGaCacheOrig); setFinalField(maxSizeGaVersionsCacheField, null, maxSizeGaVersionsCacheOrig); } /** * HELPER METHODS ------------------------------------------------------------------------------------------------------------- */ protected static void waitFor(final CyclicBarrier barrier) { final String threadName = Thread.currentThread().getName(); try { barrier.await(); } catch( final InterruptedException e ) { fail( "Thread '" + threadName + "' was interrupted while waiting for the other threads!"); } catch( final BrokenBarrierException e ) { fail( "Thread '" + threadName + "' barrier was broken while waiting for the other threads!"); } } private static KieContainerImpl createMockKieContainer(final ReleaseId projectReleaseId, final KieModuleRepo kieModuleRepo) throws Exception { // kie module final InternalKieModule mockKieProjectKieModule = mock(InternalKieModule.class); final ResourceProvider mockKieProjectKieModuleResourceProvider = mock(ResourceProvider.class); when(mockKieProjectKieModule.createResourceProvider()).thenReturn(mockKieProjectKieModuleResourceProvider); // kie project final KieModuleKieProject kieProject = new KieModuleKieProject(mockKieProjectKieModule); final KieModuleKieProject mockKieProject = spy(kieProject); doNothing().when(mockKieProject).init(); doReturn(projectReleaseId).when(mockKieProject).getGAV(); doReturn( new HashMap<String, KieBaseModel>() ).when( mockKieProject ).updateToModule( any( InternalKieModule.class ) ); // kie repository final KieRepository kieRepository = new KieRepositoryImpl(); final Field kieModuleRepoField = KieRepositoryImpl.class.getDeclaredField("kieModuleRepo"); kieModuleRepoField.setAccessible(true); kieModuleRepoField.set(kieRepository, kieModuleRepo); kieModuleRepoField.setAccessible(false); // kie container final KieContainerImpl kieContainerImpl = new KieContainerImpl(mockKieProject, kieRepository); return kieContainerImpl; } private static int countKieModules( final Map<String, NavigableMap<ComparableVersion, KieModule>> kieModulesCache ) { int numKieModules = 0; for( final NavigableMap<ComparableVersion, KieModule> map : kieModulesCache.values() ) { numKieModules += map.size(); } return numKieModules; } private static void setFinalField(final Field field, final Object fieldObject, final Object newValue) throws Exception { // make accessible field.setAccessible(true); // make non-final final Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL ); field.set(null, newValue); field.set(fieldObject, newValue); } /** * TESTS ---------------------------------------------------------------------------------------------------------------------- */ // simultaneous requests to deploy two new deployments (different versions) // for an empty/new GA artifactMap @Test(timeout=5000) public void testDeployTwoArtifactVersionsSameTime() throws Exception { final String groupId = "org"; final String artifactId = "one"; final String firstVersion = "1.0"; final String secondVersion = "1.0-NEW-FEATURE"; final CyclicBarrier storeOperationBarrier = new CyclicBarrier(2); final CyclicBarrier threadsFinishedBarrier = new CyclicBarrier(3); final Thread firstThread = new Thread( getStoreArtifactRunnable(kieModuleRepo, groupId, artifactId, firstVersion, storeOperationBarrier, threadsFinishedBarrier)); final Thread secondThread = new Thread( getStoreArtifactRunnable(kieModuleRepo, groupId, artifactId, secondVersion, storeOperationBarrier, threadsFinishedBarrier)); final ExecutorService executor = Executors.newFixedThreadPool(2); firstThread.setName("normal"); executor.submit(firstThread); secondThread.setName("newFeature"); executor.submit(secondThread); try { waitFor(threadsFinishedBarrier); } finally { executor.shutdownNow(); } final String ga = groupId + ":" + artifactId; final Map<ComparableVersion, KieModule> artifactMap = kieModuleRepo.kieModules.get(ga); final ComparableVersion normalVersion = new ComparableVersion(firstVersion); final KieModule normalKieModule = artifactMap.get(normalVersion); final ComparableVersion newFeatureVersion = new ComparableVersion(secondVersion); final KieModule newFeatureKieModule = artifactMap.get(newFeatureVersion); assertNotNull( "Race condition occurred: normal KieModule disappeared from KieModuleRepo!", normalKieModule); assertNotNull( "Race condition occurred: new feature KieModule disappeared from KieModuleRepo!", newFeatureKieModule); } public Runnable getStoreArtifactRunnable(final KieModuleRepo kieModuleRepo, final String groupId, final String artifactId, final String version, final CyclicBarrier storeOperationBarrier, final CyclicBarrier threadsFinishedBarrier) { return () -> { final ReleaseIdImpl firstReleaseId = new ReleaseIdImpl(groupId, artifactId, version); final KieModule firstKieModule = mock(KieModule.class); when(firstKieModule.getReleaseId()).thenReturn(firstReleaseId); waitFor(storeOperationBarrier); kieModuleRepo.store(firstKieModule); waitFor(threadsFinishedBarrier); }; } // remove request followed by a store request on a high load system // * remove does not completely finish before store starts @Test(timeout=5000) public void removeStoreArtifactMapTest() throws Exception { // actual test final ReleaseIdImpl releaseId = new ReleaseIdImpl("org", "redeploy", "2.0"); final InternalKieModule originalKieModule = mock(InternalKieModule.class); when(originalKieModule.getReleaseId()).thenReturn(releaseId); when(originalKieModule.getCreationTimestamp()).thenReturn(0l); final InternalKieModule redeployKieModule = mock(InternalKieModule.class); when(redeployKieModule.getReleaseId()).thenReturn(releaseId); when(redeployKieModule.getCreationTimestamp()).thenReturn(1l); // 1. initial deploy ("long ago") kieModuleRepo.store(originalKieModule); final CyclicBarrier storeRemoveOperationBarrier = new CyclicBarrier(2); final CyclicBarrier operationsSerializationBarrier = new CyclicBarrier(2); final CyclicBarrier threadsFinishedBarrier = new CyclicBarrier(3); final Runnable removeRunnable = () -> { waitFor(storeRemoveOperationBarrier); kieModuleRepo.remove(releaseId); waitFor(operationsSerializationBarrier); waitFor(threadsFinishedBarrier); }; final Runnable redeployRunnable = () -> { waitFor(storeRemoveOperationBarrier); waitFor(operationsSerializationBarrier); kieModuleRepo.store(redeployKieModule); waitFor(threadsFinishedBarrier); }; final ExecutorService executor = Executors.newFixedThreadPool(2); // 2. remove and redploy executor.submit(removeRunnable); executor.submit(redeployRunnable); try { waitFor(threadsFinishedBarrier); } finally { executor.shutdownNow(); } final String ga = releaseId.getGroupId() + ":" + releaseId.getArtifactId(); final Map<ComparableVersion, KieModule> artifactMap = kieModuleRepo.kieModules.get(ga); assertNotNull( "Artifact Map for GA '" + ga + "' not in KieModuleRepo!", artifactMap); // never gets this far, but this is a good check final KieModule redeployedKieModule = artifactMap.get(new ComparableVersion(releaseId.getVersion())); assertNotNull( "Redeployed module has disappeared from KieModuleRepo!", redeployedKieModule); assertEquals( "Original module retrieved instead of redeployed module!", 1l, redeployKieModule.getCreationTimestamp() ); } // 2. simultaneous deploy requests // - multitenant UI // - duplicate REST requests @Test(timeout=5000) public void newerVersionDeployOverwritesTest() throws Exception { // setup final ReleaseIdImpl releaseId = new ReleaseIdImpl("org", "deployTwiceAfterUpdateDependency", "1.0-SNAPSHOT"); final InternalKieModule originalOldKieModule = mock(InternalKieModule.class); when(originalOldKieModule.getReleaseId()).thenReturn(releaseId); when(originalOldKieModule.getCreationTimestamp()).thenReturn(0l); final ReleaseId dependentReleaseid = new ReleaseIdImpl("org", "deployTwiceAfterUpdate", "1.0-SNAPSHOT"); final KieContainerImpl kieContainer = createMockKieContainer(dependentReleaseid, kieModuleRepo); // 1. deploy kieModuleRepo.store(originalOldKieModule); // 2. another project is dependent on this project kieContainer.updateDependencyToVersion(releaseId, releaseId); final InternalKieModule newKieModule = mock(InternalKieModule.class); when(newKieModule.getReleaseId()).thenReturn(releaseId); when(newKieModule.getCreationTimestamp()).thenReturn(10l); final CyclicBarrier storeOperationBarrier = new CyclicBarrier(2); final CyclicBarrier storeSerializationBarrier = new CyclicBarrier(2); final CyclicBarrier threadsFinishedBarrier = new CyclicBarrier(3); final Runnable deployRunnable = () -> { waitFor(storeOperationBarrier); // Second thread waits with store until the first one finishes with it. if (Thread.currentThread().getName().equals("two")) { waitFor(storeSerializationBarrier); } kieModuleRepo.store(newKieModule); if (Thread.currentThread().getName().equals("one")) { waitFor(storeSerializationBarrier); } waitFor(threadsFinishedBarrier); }; final ExecutorService executor = Executors.newFixedThreadPool(2); // 2. remove and redploy final Thread deployThread = new Thread(deployRunnable); deployThread.setName("one"); executor.submit(deployThread); final Thread secondDeployThread = new Thread(deployRunnable); secondDeployThread.setName("two"); executor.submit(secondDeployThread); try { waitFor(threadsFinishedBarrier); } finally { executor.shutdownNow(); } // never gets this far, but this is a good check final KieModule oldKieModule = kieModuleRepo.oldKieModules.get(releaseId); final long oldKieModuleTimeStamp = ((InternalKieModule) oldKieModule).getCreationTimestamp(); final long originalKieModuleTimestamp = originalOldKieModule.getCreationTimestamp(); assertEquals( "The old kie module in the repo is not the originally deployed module!", originalKieModuleTimestamp, oldKieModuleTimeStamp); } @Test public void storingNewProjectsCausesOldProjectEvictionFromKieModuleRepoTest() throws Exception { // setup setFinalField(maxSizeGaCacheField, null, 3); setFinalField(maxSizeGaVersionsCacheField, null, 2); // to test oldKieModules caching final ReleaseIdImpl [] releaseIds = new ReleaseIdImpl[7]; for( int i = 0; i < releaseIds.length; ++i ) { final String artifactId = Character.toString((char)('A'+i)); releaseIds[i] = new ReleaseIdImpl("org", artifactId, "1.0"); } // store for( int i = 0; i < releaseIds.length; ++i ) { final InternalKieModule kieModule = mock(InternalKieModule.class); when(kieModule.getReleaseId()).thenReturn(releaseIds[i]); when(kieModule.getCreationTimestamp()).thenReturn(10l); kieModuleRepo.store(kieModule); kieModuleRepo.store(kieModule); // store module 2 times to trigger storage to oldKieModules } final int numKieModules = countKieModules(kieModuleRepo.kieModules); assertEquals( "KieModuleRepo cache should not grow past " + KieModuleRepo.MAX_SIZE_GA_CACHE + ": ", KieModuleRepo.MAX_SIZE_GA_CACHE, numKieModules ); final int oldKieModulesSize = kieModuleRepo.oldKieModules.size(); final int max = KieModuleRepo.MAX_SIZE_GA_CACHE * KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE; assertTrue( "KieModuleRepot old KieModules map is not limited in it's growth: " + oldKieModulesSize + " > " + max, oldKieModulesSize <= max ); } @Test public void storingNewProjectVersionsCausesOldVersionEvictionFromKieModuleRepoTest() throws Exception { // setup setFinalField(maxSizeGaCacheField, null, 2); // to test oldKieModules caching setFinalField(maxSizeGaVersionsCacheField, null, 3); final ReleaseIdImpl [] releaseIds = new ReleaseIdImpl[7]; for( int i = 0; i < releaseIds.length; ++i ) { releaseIds[i] = new ReleaseIdImpl("org", "test", "1." + i); } // store for( int i = 0; i < releaseIds.length; ++i ) { final InternalKieModule kieModule = mock(InternalKieModule.class); when(kieModule.getReleaseId()).thenReturn(releaseIds[i]); when(kieModule.getCreationTimestamp()).thenReturn(10l); kieModuleRepo.store(kieModule); kieModuleRepo.store(kieModule); // in order to trigger storage to oldKieModules } int numKieModules = countKieModules(kieModuleRepo.kieModules); assertEquals( "KieModuleRepo cache should not grow past " + KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE + ": ", KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE, numKieModules ); int oldKieModulesSize = kieModuleRepo.oldKieModules.size(); final int maxOldKieModules = KieModuleRepo.MAX_SIZE_GA_CACHE * KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE; assertTrue( "KieModuleRepo old KieModules map is not limited in it's growth: " + oldKieModulesSize + " > " + maxOldKieModules, oldKieModulesSize <= maxOldKieModules ); // store for( int o = 0; o < 2; ++o ) { // loop 2 times in order to trigger storage to oldKieModules for( int i = 0; i < releaseIds.length; ++i ) { final InternalKieModule kieModule = mock(InternalKieModule.class); when(kieModule.getReleaseId()).thenReturn(releaseIds[i]); when(kieModule.getCreationTimestamp()).thenReturn(10l); kieModuleRepo.store(kieModule); } } numKieModules = countKieModules(kieModuleRepo.kieModules); assertEquals( "KieModuleRepo cache should not grow past " + KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE + ": ", KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE, numKieModules ); oldKieModulesSize = kieModuleRepo.oldKieModules.size(); assertTrue( "KieModuleRepo old KieModules map is not limited in it's growth: " + oldKieModulesSize + " > " + maxOldKieModules, oldKieModulesSize <= maxOldKieModules ); } @Test public void testOldKieModulesLRUCache() throws Exception { // setup setFinalField(maxSizeGaCacheField, null, 2); setFinalField(maxSizeGaVersionsCacheField, null, 4); final ReleaseIdImpl [] releaseIds = new ReleaseIdImpl[9]; for( int i = 0; i < releaseIds.length; ++i ) { final String artifactId = Character.toString((char)('A'+i/2)); releaseIds[i] = new ReleaseIdImpl("org", artifactId, "1." + i); } // store for( int i = 0; i < releaseIds.length; ++i ) { final InternalKieModule kieModule = mock(InternalKieModule.class); when(kieModule.getReleaseId()).thenReturn(releaseIds[i]); when(kieModule.getCreationTimestamp()).thenReturn(10l); kieModuleRepo.store(kieModule); kieModuleRepo.store(kieModule); // in order to trigger storage to oldKieModules } int maxSameGAModules = 0; int maxGAs = 0; for( final Map<ComparableVersion, KieModule> artifactMap : kieModuleRepo.kieModules.values() ) { maxGAs++; if( artifactMap.size() > maxSameGAModules ) { maxSameGAModules = artifactMap.size(); } } assertTrue( "The maximum of artifacts per GA should not grow past " + KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE + ": " + KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE + " < " + maxSameGAModules, KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE >= maxSameGAModules ); assertTrue( "The number of GAs not grow past " + KieModuleRepo.MAX_SIZE_GA_CACHE + ": " + KieModuleRepo.MAX_SIZE_GA_CACHE + " > " + maxGAs, KieModuleRepo.MAX_SIZE_GA_CACHE >= maxGAs ); final int oldKieModulesSize = kieModuleRepo.oldKieModules.size(); final int maxOldKieModules = KieModuleRepo.MAX_SIZE_GA_CACHE * KieModuleRepo.MAX_SIZE_GA_VERSIONS_CACHE; assertTrue( "KieModuleRepo old KieModules map is not limited in it's growth: " + oldKieModulesSize + " > " + maxOldKieModules, oldKieModulesSize <= maxOldKieModules ); } }