/* * Copyright 2003-2015 JetBrains s.r.o. * * 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 jetbrains.mps.java.stub; import jetbrains.mps.extapi.persistence.FolderSetDataSource; import jetbrains.mps.persistence.java.library.JavaClassStubModelDescriptor; import jetbrains.mps.project.AbstractModule; import jetbrains.mps.project.ModuleId; import jetbrains.mps.project.structure.modules.ModuleReference; import jetbrains.mps.reloading.CommonPaths; import jetbrains.mps.smodel.SNodeId.Foreign; import jetbrains.mps.smodel.loading.ModelLoadingState; import jetbrains.mps.tool.environment.Environment; import jetbrains.mps.tool.environment.EnvironmentConfig; import jetbrains.mps.tool.environment.MpsEnvironment; import jetbrains.mps.vfs.impl.IoFileSystem; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeId; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import java.io.File; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; /** * Stub models from classes are loaded lazily. To check there's no * race condition when model is loaded, do the stress testing. * There were few causes: * - initial model construction set state from NOT_LOADED to INTERFACE_LOADED outside of synchronized code, so condition state == INTERFACE_LOADED in load() * to move to FULLY_LOADED was not triggered - threads might have seen model != null but state still == NOT_LOADED. * - similarly FULLY_LOADED was dispatched outside of model update lock, and the chances were to treat model alrady full with nodes as loaded partially, * so that another thread starts to update it and to remove nodes other thread assumes are in the node map already. * * Since model under test is not attached to a repository, no model access control is in place. * @author Artem Tikhomirov */ public class StubModelLazyLoadStressTest { private static Environment ourPlatform; private static final boolean DEBUG = Boolean.FALSE.booleanValue(); @BeforeClass public static void setUp() { ourPlatform = MpsEnvironment.getOrCreate(EnvironmentConfig.defaultConfig()); } @AfterClass public static void tearDown() { ourPlatform.release(); ourPlatform = null; } private static void trace(String message) { if (DEBUG) { System.out.println(message); } } @Test public void testParallelLoad() throws InterruptedException { final ModuleReference moduleRef = new ModuleReference("fake", ModuleId.regular()); SModelReference modelRef = new JavaPackageNameStub("java.util.regex").asModelReference(moduleRef); FolderSetDataSource dataSource = new FolderSetDataSource(); for (String path : CommonPaths.getJDKPath()) { if (new File(path).isFile() && path.endsWith(".jar")) { path += "!/java/util/regex/"; } dataSource.addPath(IoFileSystem.INSTANCE.getFile(path), null); } JavaClassStubModelDescriptor model = new JavaClassStubModelDescriptor(modelRef, dataSource) { @Override protected void fireModelStateChanged(ModelLoadingState oldState, ModelLoadingState newState) { Thread.yield(); try { Thread.sleep(10); } catch (InterruptedException ex) { // ignore } super.fireModelStateChanged(oldState, newState); trace("JCSMD.marked as " + newState + " from " + Thread.currentThread().getName()); } }; model.setModule(new AbstractModule() { { setModuleReference(moduleRef); } }); SNodeId nodeId = new Foreign("~Pattern.compile(java.lang.String):java.util.regex.Pattern"); FindNodeRunnable[] runners = new FindNodeRunnable[10]; LatchCountAction latch = new LatchCountAction(new CountDownLatch(2)); CyclicBarrier barrier = new CyclicBarrier(runners.length, latch); for (int i = 0; i < runners.length; i++) { runners[i] = new FindNodeRunnable(model, nodeId); if (i+1 == runners.length) { trace("about to start latest thread..."); } new Thread(new BarrierRunnable(barrier, runners[i]), "FindNodeThread" + i).start(); } latch.await(10); StringBuilder msg = new StringBuilder(); boolean anyNull = false; for (int i = 0; i < runners.length; i++) { msg.append("Thread"); msg.append(i); msg.append(':'); if (runners[i].getNode() == null) { anyNull = true; msg.append("NULL"); } else { msg.append("OK"); } msg.append(','); } Assert.assertFalse(msg.toString(), anyNull); } private static class FindNodeRunnable implements Runnable { private final SModel myModel; private final SNodeId myNodeId; private SNode myNode; public FindNodeRunnable(SModel model, SNodeId nodeId) { myModel = model; myNodeId = nodeId; } @Override public void run() { myModel.load(); // let all threads block at load()'s guard myNode = myModel.getNode(myNodeId); trace(Thread.currentThread().getName() + " is over"); } public SNode getNode() { return myNode; } } final static class BarrierRunnable implements Runnable { private final CyclicBarrier myBarrier; private final Runnable myDelegate; public BarrierRunnable(CyclicBarrier barrier, Runnable delegate) { myBarrier = barrier; myDelegate = delegate; } @Override public void run() { try { myBarrier.await(); myDelegate.run(); myBarrier.await(); } catch (Exception ex) { ex.printStackTrace(); Assert.fail(ex.getMessage()); } } } private static class LatchCountAction implements Runnable { private final CountDownLatch myLatch; public LatchCountAction(CountDownLatch latch) { myLatch = latch; } @Override public void run() { myLatch.countDown(); } public void await(int seconds) throws InterruptedException { myLatch.await(seconds, TimeUnit.SECONDS); } } }