/*
* 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);
}
}
}