/* * 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.classloading; import jetbrains.mps.classloading.ModuleUpdater.SearchError; import jetbrains.mps.module.ReloadableModule; import jetbrains.mps.util.annotation.Hack; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import org.jetbrains.mps.openapi.module.SDependency; import org.jetbrains.mps.openapi.module.SDependencyScope; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.util.Condition; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import static jetbrains.mps.classloading.ModulesWatcher.ClassLoadingStatus.INVALID; import static jetbrains.mps.classloading.ModulesWatcher.ClassLoadingStatus.VALID; /** * This class watches all the reloadable modules, which satisfy #myWatchableCondition in the repository and dependencies between them. * It aims to store a status for each tracked module * @see jetbrains.mps.classloading.ModulesWatcher.ClassLoadingStatus * and to return all compile depedencies of module within repository * @see #getDependencies(Iterable) * Also it keeps a dependency graph to be able to calculate back dependencies for any module * @see #getBackDependencies(Iterable) * * Note: due to the lazy implementation of module unloading, there is a possible situation, * when there are some disposed modules in ModulesWatcher. * We may be asked about their dependencies etc. Therefore <code>ModulesWatcher</code> tracks references to modules not modules themselves. * The add/remove/update module methods are triggered from above. This class updates its state accordingly. * * A lazy mechanism is used here: when the state is 'dirty', refresh happens at any request. * @see #recountStatus() * * Notice, that read action is required on every update. * * @see ClassLoaderManager#myLoadableCondition * @see ClassLoaderManager#myWatchableCondition */ public class ModulesWatcher { private static final Logger LOG = LogManager.getLogger(ModulesWatcher.class); private final Object myStatusMapLock = new Object(); private final SRepository myRepository; private final Map<SModuleReference, ClassLoadingStatus> myStatusMap = new HashMap<SModuleReference, ClassLoadingStatus>(); private Collection<SModuleReference> myCurrentInvalidModules; private final ReferenceStorage<ReloadableModule> myRefStorage = new ReferenceStorage<ReloadableModule>(); private final ModuleUpdater myModuleUpdater; public ModulesWatcher(SRepository repository, final Condition<ReloadableModule> watchableCondition) { myRepository = repository; myModuleUpdater = new ModuleUpdater(repository, watchableCondition, myRefStorage); } private void update() { myRepository.getModelAccess().checkReadAccess(); if (isChanged()) { recountStatus(); } } /** * @param mRef is a module reference. <code>ModulesWatcher</code> maintains references, not modules themselves. * @return the status for the given module reference * @see jetbrains.mps.classloading.ModulesWatcher.ClassLoadingStatus */ @NotNull public ClassLoadingStatus getStatus(@NotNull SModuleReference mRef) { if (isChanged()) { LOG.warn("The class loading status info might be outdated"); } if (!myModuleUpdater.contains(mRef)) { return INVALID; } else { synchronized (myStatusMapLock) { if (!myStatusMap.containsKey(mRef)) { LOG.warn("No status for the module " + mRef); return INVALID; } else { return myStatusMap.get(mRef); } } } } public void updateModules(@NotNull Collection<? extends ReloadableModule> modules) { if (modules.isEmpty()) return; myModuleUpdater.updateModules(modules); update(); } public void addModules(@NotNull Collection<? extends ReloadableModule> modules) { if (modules.isEmpty()) return; myModuleUpdater.addModules(modules); update(); } public void removeModules(@NotNull Collection<? extends SModuleReference> mRefs) { if (mRefs.isEmpty()) return; myModuleUpdater.removeModules(mRefs); update(); } /** * recounting the status map * @see #isChanged() */ private void recountStatus() { LOG.debug("Recount status map for modules"); boolean updated = myModuleUpdater.refreshGraph(); Collection<SModuleReference> invalidModules = findInvalidModules(); updated |= (!invalidModules.equals(myCurrentInvalidModules)); if (updated) { myCurrentInvalidModules = invalidModules; refillStatusMap(invalidModules); } LOG.debug("Finished recounting"); } /** * costly because of backDeps request */ private void refillStatusMap(Collection<? extends SModuleReference> invalidModules) { synchronized (myStatusMapLock) { myStatusMap.clear(); for (SModuleReference mRef : getAllModules()) { myStatusMap.put(mRef, VALID); } Collection<? extends SModuleReference> allInvalidModules = getBackDependencies(invalidModules); for (SModuleReference mRef : allInvalidModules) { myStatusMap.put(mRef, INVALID); if (LOG.isTraceEnabled()) { Collection<SModuleReference> dependencies = getDependencies(Collections.singleton(mRef)); for (SModuleReference depRef : dependencies) { if (invalidModules.contains(depRef)) { LOG.trace("The module " + mRef + " is invalid since it has a transitive dependency on the module " + depRef); } } } } LOG.info(invalidModules.size() + " modules are marked as invalid roots for class loading out of " + getAllModules().size() + " modules [totally in the repository]:"); print(invalidModules); LOG.info("Totally " + allInvalidModules.size() + " modules are marked invalid for class loading:"); print(allInvalidModules); checkStatusMapCorrectness(); } } private void print(Collection<? extends SModuleReference> modules) { for (SModuleReference ref : modules) { LOG.info(ref); } } /** * Note: here we are interested in the actual status of module. (not {@link ReferenceStorage#resolveRef}) * if it has been already disposed but still remains in our graphs (i.e. ClassLoader is not disposed yet [!]), * we need to mark it invalid */ private boolean isModuleDisposed(SModuleReference mRef) { SModule resolvedModule = mRef.resolve(myRepository); return (resolvedModule == null || resolvedModule.getRepository() == null); } @Nullable private ReloadableModule resolveRef(SModuleReference ref) { return myRefStorage.resolveRef(ref); } private Collection<SModuleReference> findInvalidModules() { return findInvalidModules0(false).keySet(); } @TestOnly Map<SModuleReference, String> findInvalidModulesProblems() { return findInvalidModules0(true); } @NotNull private Map<SModuleReference, String> findInvalidModules0(boolean errorLevel) { myRepository.getModelAccess().checkReadAccess(); Map<ReloadableModule, List<SearchError>> modulesWithAbsentDeps = myModuleUpdater.getModulesWithAbsentDeps(); Map<SModuleReference, String> mRefToProblem = new HashMap<>(); Collection<? extends SModuleReference> allModuleRefs = getAllModules(); for (SModuleReference mRef : allModuleRefs) { if (!mRefToProblem.containsKey(mRef)) { String msg = getModuleProblemMessage(mRef, modulesWithAbsentDeps); if (msg == null) { continue; } if (errorLevel) LOG.error(msg); else LOG.debug(msg); mRefToProblem.put(mRef, msg); } } return mRefToProblem; } // FIXME rewrite!! need to extract some common API class for validity checking // FIXME currently Migration also wants to know which languages are invalid for loading and why // FIXME probably makes sense to transfer part of this functionality to the project.dependency package /** * @return message with the problem description or null if the module is valid */ @Nullable @Hack private String getModuleProblemMessage(SModuleReference mRef, Map<ReloadableModule, List<SearchError>> modulesWithAbsentDeps) { assert !isChanged(); if (isModuleDisposed(mRef)) { return String.format("Module %s is disposed and therefore was marked invalid for class loading", mRef.getModuleName()); } ReloadableModule module = (ReloadableModule) mRef.resolve(myRepository); assert module != null; // FIXME does not work for now, enable in the 3.4 if (modulesWithAbsentDeps.containsKey(module)) { List<SearchError> errors = modulesWithAbsentDeps.get(module); return String.format("%s has got an absent dependency problem and therefore was marked invalid for class loading: %s", module, errors.get(0).getMsg()); } for (SDependency dep : module.getDeclaredDependencies()) { if (dep.getScope() == SDependencyScope.DESIGN || dep.getScope() == SDependencyScope.GENERATES_INTO) { continue; } if (isModuleDisposed(dep.getTargetModule())) { return String.format("%s depends on a disposed module %s and therefore was marked invalid for class loading", module, dep.getTargetModule()); } } return null; } private void checkStatusMapCorrectness() { assert myStatusMap.size() == getAllModules().size() : "Modules number inconsistency"; for (SModuleReference mRef : getAllModules()) { ClassLoadingStatus status = VALID; for (SModuleReference mRef1 : getDependencies(Collections.singleton(mRef))) { if (!getStatus(mRef1).isValid()) status = INVALID; } if (status != getStatus(mRef)) { throw new IllegalStateException("Status is wrong for the module " + mRef); } } } Collection<? extends SModuleReference> getAllModules() { return myModuleUpdater.getModules(); } /** * @return all dependencies of this module (closed set under dependency-relation) */ public Collection<SModuleReference> getDependencies(Iterable<? extends SModuleReference> mRefs) { return myModuleUpdater.getDeps(mRefs); } Collection<ReloadableModule> getResolvedDependencies(Iterable<? extends ReloadableModule> modules) { Collection<SModuleReference> refs = new LinkedHashSet<SModuleReference>(); for (ReloadableModule module : modules) { refs.add(module.getModuleReference()); } Collection<SModuleReference> referencedDeps = getDependencies(refs); Collection<ReloadableModule> resolvedDeps = resolveRefs(referencedDeps); assert (resolvedDeps.size() == referencedDeps.size()); return resolvedDeps; } private Collection<ReloadableModule> resolveRefs(final Iterable<? extends SModuleReference> refs) { final Collection<ReloadableModule> modules = new LinkedHashSet<ReloadableModule>(); for (SModuleReference mRef : refs) { ReloadableModule module = resolveRef(mRef); if (module != null) modules.add(module); } return modules; } Set<SModuleReference> getModuleRefs(Iterable<? extends ReloadableModule> modules) { Set<SModuleReference> result = new LinkedHashSet<SModuleReference>(); for (ReloadableModule module : modules) { result.add(module.getModuleReference()); } return result; } /** * @return all back dependencies of this module (closed set under back-dependency-relation) */ public Collection<SModuleReference> getBackDependencies(Iterable<? extends SModuleReference> mRefs) { return myModuleUpdater.getBackDeps(mRefs); } public Collection<? extends ReloadableModule> getResolvedBackDependencies(Iterable<? extends ReloadableModule> modules) { Collection<SModuleReference> refs = new LinkedHashSet<SModuleReference>(); for (ReloadableModule module : modules) refs.add(module.getModuleReference()); return resolveRefs(getBackDependencies(refs)); } boolean isModuleWatched(ReloadableModule module) { if (isChanged()) { LOG.warn("The class loading status info might be outdated"); } return getAllModules().contains(module.getModuleReference()); } private boolean isChanged() { return myModuleUpdater.isDirty(); } enum ClassLoadingStatus { /** * module is not loadable OR * module is loadable and disposed from the repository OR * module is loadable and it has some loadable dependency (transitively) which does not belong to the repository */ INVALID, /** * module is loadable and has all its loadable deps are in the repository too */ VALID; public boolean isValid() { return (this == VALID); } } }