/*
* Quasar: lightweight threads and actors for the JVM.
* Copyright (c) 2013-2014, Parallel Universe Software Co. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 3.0
* as published by the Free Software Foundation.
*/
package co.paralleluniverse.actors;
import static co.paralleluniverse.common.reflection.ClassLoaderUtil.isClassFile;
import static co.paralleluniverse.common.reflection.ClassLoaderUtil.resourceToClass;
import co.paralleluniverse.common.util.Exceptions;
import co.paralleluniverse.concurrent.util.MapUtil;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.nio.file.StandardWatchEventKinds.*;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import javax.management.InstanceAlreadyExistsException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationEmitter;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Loads actor, and actor-related classes for hot code-swapping.
*
* @author pron
*/
public class ActorLoader extends ClassLoader implements ActorLoaderMXBean, NotificationEmitter {
/*
* We load actor classes from the latest module.
* Non-actor classes are compared against those found in main.
* If there's no match, they are loaded from the module that requested them, i.e., non-actor classes are not shared from one module
* to another.
*/
// private static final boolean TRY_RELOAD = Boolean.getBoolean("co.paralleluniverse.actors.tryHotSwapReload");
private static final String MODULE_DIR_PROPERTY = "co.paralleluniverse.actors.moduleDir";
private static final Path moduleDir;
private static final Logger LOG = LoggerFactory.getLogger(ActorLoader.class);
private static final ActorLoader instance;
static {
ClassLoader.registerAsParallelCapable();
instance = new ActorLoader("co.paralleluniverse:type=ActorLoader");
String moduleDirName = System.getProperty(MODULE_DIR_PROPERTY);
if (moduleDirName != null) {
Path mdir = Paths.get(moduleDirName);
try {
mdir = mdir.toAbsolutePath();
Files.createDirectories(mdir);
mdir = mdir.toRealPath();
} catch (IOException e) {
LOG.error("Error findong/creating module directory " + mdir, e);
mdir = null;
}
moduleDir = mdir;
loadModulesInModuleDir(instance, moduleDir);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
monitorFilesystem(instance, moduleDir);
}
}, "actor-loader-filesystem-monitor");
t.setDaemon(true);
t.start();
} else
moduleDir = null;
}
public static <T> Class<T> currentClassFor(Class<T> clazz) {
return instance.getCurrentClassFor(clazz);
}
public static Class<?> currentClassFor(String className) throws ClassNotFoundException {
return instance.getCurrentClassFor(className);
}
public static <T> T getReplacementFor(T object) {
return instance.getReplacementFor0(object);
}
static AtomicReference<Class<?>> getClassRef(String className) {
return instance.getClassRef0(className);
}
static AtomicReference<Class<?>> getClassRef(Class<?> clazz) {
return instance.getClassRef0(clazz);
}
//
private final ConcurrentMap<String, AtomicReference<Class<?>>> classRefs = MapUtil.newConcurrentHashMap();
private final ClassValue<AtomicReference<Class<?>>> classRefs1 = new ClassValue<AtomicReference<Class<?>>>() {
@Override
protected AtomicReference<Class<?>> computeValue(Class<?> type) {
return getClassRef0(type.getName());
}
};
private final List<ActorModule> modules = new CopyOnWriteArrayList<>();
private final ConcurrentMap<String, ActorModule> classModule = MapUtil.newConcurrentHashMap();
private final ThreadLocal<Boolean> recursive = new ThreadLocal<Boolean>();
private final NotificationBroadcasterSupport notificationBroadcaster;
private int notificationSequenceNumber;
private ActorLoader(String mbeanName) {
super(ActorLoader.class.getClassLoader());
MBeanNotificationInfo info = new MBeanNotificationInfo(
new String[]{ModuleNotification.NAME},
ModuleNotification.class.getName(),
"Actor module change");
this.notificationBroadcaster = new NotificationBroadcasterSupport(info);
try {
registerMBean(mbeanName);
} catch (InstanceAlreadyExistsException e) {
try {
registerMBean(mbeanName + ",instance=" + Integer.toHexString(System.identityHashCode(ActorLoader.class.getClassLoader())));
} catch (InstanceAlreadyExistsException ex) {
throw new RuntimeException(ex);
}
}
}
private void registerMBean(String mbeanName) throws InstanceAlreadyExistsException {
try {
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
final ObjectName mxbeanName = new ObjectName(mbeanName);
mbs.registerMBean(this, mxbeanName);
} catch (MBeanRegistrationException ex) {
LOG.error("exception while registering MBean " + mbeanName, ex);
} catch (NotCompliantMBeanException ex) {
throw new AssertionError(ex);
} catch (MalformedObjectNameException ex) {
throw new AssertionError(ex);
}
}
private ActorModule getModule(URL url) {
for (ActorModule m : modules) {
if (m.getURL().equals(url))
return m;
}
return null;
}
private Map<String, Class<?>> checkModule(ActorModule module) {
Map<String, Class<?>> oldClasses = new HashMap<>();
try {
for (String className : module.getUpgradeClasses()) {
Class<?> newClass = null;
try {
newClass = module.loadClassInModule(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Upgraded class " + className + " is not found in module " + module);
}
// if (!Actor.class.isAssignableFrom(newClass))
// throw new RuntimeException("Upgraded class " + className + " in module " + module + " is not an actor");
Class<?> oldClass = null;
try {
oldClass = this.loadClass(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Upgraded class " + className + " does not upgrade an existing class");
}
// if (!Actor.class.isAssignableFrom(oldClass))
// throw new RuntimeException("Upgraded class " + className + " in module " + module + " does not upgrade an actor");
oldClasses.put(className, oldClass);
}
return oldClasses;
} catch (Exception e) {
LOG.error("Error while loading module " + module, e);
throw e;
}
}
@Override
public List<String> getLoadedModules() {
return Lists.transform(modules, new Function<ActorModule, String>() {
@Override
public String apply(ActorModule module) {
return module.getURL().toString();
}
});
}
@Override
public synchronized void reloadModule(String jarURL) {
try {
reloadModule(new URL(jarURL));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public synchronized void loadModule(String jarURL) {
try {
loadModule(new URL(jarURL));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public synchronized void unloadModule(String jarURL) {
try {
unloadModule(new URL(jarURL));
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public synchronized void reloadModule(URL jarURL) {
ActorModule oldModule = getModule(jarURL);
LOG.info("{} module {}.", oldModule == null ? "Loading" : "Reloading", jarURL);
ActorModule module = new ActorModule(jarURL, this);
addModule(module);
if (oldModule != null)
removeModule(oldModule);
LOG.info("Module {} {}.", jarURL, oldModule == null ? "loaded" : "reloaded");
notify(module, oldModule == null ? "loaded" : "reloaded");
}
public synchronized void loadModule(URL jarURL) {
if (getModule(jarURL) != null) {
LOG.warn("loadModule: module {} already loaded.", jarURL);
return;
}
LOG.info("Loading module {}.", jarURL);
ActorModule module = new ActorModule(jarURL, this);
addModule(module);
LOG.info("Module {} loaded.", jarURL);
notify(module, "loaded");
}
public synchronized void unloadModule(URL jarURL) {
ActorModule module = getModule(jarURL);
if (module == null) {
LOG.warn("removeModule: module {} not loaded.", jarURL);
return;
}
LOG.info("Removing module {}.", jarURL);
removeModule(module);
LOG.info("Module {} removed.", jarURL);
notify(module, "removed");
}
private synchronized void addModule(ActorModule module) {
Map<String, Class<?>> oldClasses = checkModule(module);
modules.add(module);
for (String className : module.getUpgradeClasses()) {
Class<? extends Actor> oldClass, newClass;
try {
newClass = (Class<? extends Actor>) module.loadClass(className);
oldClass = (Class<? extends Actor>) oldClasses.get(className);
} catch (ClassNotFoundException e) {
throw new AssertionError();
}
LOG.info("Upgrading class {} of module {} to that in module {}", className, getModule(oldClass), module);
classModule.put(className, module);
}
performUpgrade(new HashSet<>(oldClasses.values()));
}
private synchronized void removeModule(ActorModule module) {
modules.remove(module);
Set<Class<?>> oldClasses = new HashSet<>();
for (String className : module.getUpgradeClasses()) {
if (classModule.get(className) == module) {
ActorModule newModule = null;
for (ActorModule m : Lists.reverse(modules)) {
if (m.getUpgradeClasses().contains(className)) {
newModule = m;
break;
}
}
LOG.info("Downgrading class {} of module {} to that in module {}", className, module, newModule);
if (newModule != null)
classModule.put(className, newModule);
else
classModule.remove(className);
Class oldClass = module.findLoadedClassInModule(className);
if (oldClass != null)
oldClasses.add(oldClass);
}
}
performUpgrade(oldClasses);
}
private void performUpgrade(Set<Class<?>> oldClasses) {
// if (TRY_RELOAD && JavaAgent.isActive()) {
// try {
// LOG.info("Attempting to redefine classes");
// List<ClassDefinition> classDefinitions = new ArrayList<>();
// for (Class<?> oldClass : oldClasses) {
// byte[] classFile = null;
// try (InputStream is = getResourceAsStream(toClassFileName(oldClass))) {
// classFile = ByteStreams.toByteArray(is);
// }
// classDefinitions.add(new ClassDefinition(oldClass, classFile));
// }
// Retransform.redefine(classDefinitions);
// LOG.info("Class redefinition succeeded.");
// } catch (Exception e) {
// LOG.info("Class redefinition failed due to exception. Upgrading.", e);
// }
// }
for (Class<?> oldClass : oldClasses) {
try {
LOG.debug("Triggering replacement of {} ({})", oldClass, getModule(oldClass));
getClassRef0(oldClass).set(loadCurrentClass(oldClass.getName()));
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
}
}
}
static ActorModule getModule(Class<?> clazz) {
return clazz.getClassLoader() instanceof ActorModule ? (ActorModule) clazz.getClassLoader() : null;
}
<T extends Actor<?, ?>> Class<T> loadCurrentClass(String className) throws ClassNotFoundException {
ActorModule module = classModule.get(className);
Class<?> clazz;
if (module != null)
clazz = (Class<T>) module.loadClass(className);
else
clazz = (Class<T>) getParent().loadClass(className);
LOG.debug("currentClassFor {} - {} {}", className, getModule(clazz), module);
return (Class<T>) clazz;
}
<T> T getReplacementFor0(T instance) {
if (instance == null)
return null;
Class<T> clazz = (Class<T>) instance.getClass();
if (clazz.isAnonymousClass())
return instance;
Class<T> newClazz = getCurrentClassFor(clazz);
if (newClazz == clazz)
return instance;
return InstanceUpgrader.get(newClazz).copy(instance);
}
<T> Class<T> getCurrentClassFor(Class<T> clazz) {
if (clazz.isAnonymousClass())
return clazz;
AtomicReference<Class<?>> ref = getClassRef0(clazz);
Class<?> clazz1 = ref.get();
if (clazz1 == null) {
try {
clazz1 = loadCurrentClass(clazz.getName());
} catch (ClassNotFoundException e) {
clazz1 = clazz;
}
if (!ref.compareAndSet(null, clazz1))
clazz1 = ref.get();
}
assert clazz1 != null;
return (Class<T>) clazz1;
}
Class<?> getCurrentClassFor(String className) throws ClassNotFoundException {
AtomicReference<Class<?>> ref = getClassRef0(className);
Class<?> clazz = ref.get();
if (clazz == null) {
clazz = loadCurrentClass(className);
if (!ref.compareAndSet(null, clazz))
clazz = ref.get();
}
assert clazz != null;
return clazz;
}
AtomicReference<Class<?>> getClassRef0(Class<?> clazz) {
if (clazz.isAnonymousClass())
return null;
return classRefs1.get(clazz);
}
AtomicReference<Class<?>> getClassRef0(String className) {
final AtomicReference<Class<?>> newValue = new AtomicReference<Class<?>>();
final AtomicReference<Class<?>> oldValue = classRefs.putIfAbsent(className, newValue);
return oldValue != null ? oldValue : newValue;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (recursive.get() == Boolean.TRUE)
throw new ClassNotFoundException(name);
recursive.set(Boolean.TRUE);
try {
ActorModule module = classModule.get(name);
// ActorModule module = null;
// for (ActorModule m : Lists.reverse(modules)) {
// if (m.getUpgradeClasses().contains(name)) {
// module = m;
// break;
// }
// }
if (module != null)
return module.loadClassInModule(name);
} finally {
recursive.remove();
}
return getParent().loadClass(name);
}
@Override
public URL getResource(String name) {
if (recursive.get() == Boolean.TRUE)
return null;
recursive.set(Boolean.TRUE);
try {
if (isClassFile(name)) {
String className = resourceToClass(name);
ActorModule module = classModule.get(className);
// ActorModule module = null;
// for (ActorModule m : Lists.reverse(modules)) {
// if (m.getUpgradeClasses().contains(className)) {
// module = m;
// break;
// }
// }
if (module != null)
return module.getResource(name);
}
} finally {
recursive.remove();
}
return super.getResource(name);
}
@Override
public MBeanNotificationInfo[] getNotificationInfo() {
return notificationBroadcaster.getNotificationInfo();
}
@Override
public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws IllegalArgumentException {
notificationBroadcaster.addNotificationListener(listener, filter, handback);
}
@Override
public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws ListenerNotFoundException {
notificationBroadcaster.removeNotificationListener(listener, filter, handback);
}
@Override
public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException {
notificationBroadcaster.removeNotificationListener(listener);
}
private synchronized void notify(ActorModule module, String action) {
final Notification n = new ModuleNotification(this, notificationSequenceNumber++, System.currentTimeMillis(),
"Module " + module + " has been " + action);
notificationBroadcaster.sendNotification(n);
}
private static class ModuleNotification extends Notification {
static final String NAME = "co.paralleluniverse.actors.module";
public ModuleNotification(String type, Object source, long sequenceNumber, String message) {
super(NAME, source, sequenceNumber, message);
}
public ModuleNotification(Object source, long sequenceNumber, long timeStamp, String message) {
super(NAME, source, sequenceNumber, timeStamp, message);
}
}
private static void loadModulesInModuleDir(ActorLoader instance, Path moduleDir) {
LOG.info("scanning module directory " + moduleDir + " for modules.");
try (DirectoryStream<Path> children = Files.newDirectoryStream(moduleDir)) {
for (Path child : children) {
if (isValidFile(child, false)) {
try {
final URL jarUrl = child.toUri().toURL();
instance.reloadModule(jarUrl);
} catch (Exception e) {
LOG.error("exception while processing " + child, e);
}
} else {
LOG.warn("A non-jar item " + child.getFileName() + " found in the modules directory " + moduleDir);
}
}
} catch (Exception e) {
LOG.error("exception while loading modules in module directory " + moduleDir, e);
}
}
private static void monitorFilesystem(ActorLoader instance, Path moduleDir) {
try (WatchService watcher = FileSystems.getDefault().newWatchService();) {
moduleDir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
LOG.info("Filesystem monitor: Watching module directory " + moduleDir + " for changes.");
for (;;) {
final WatchKey key = watcher.take();
for (WatchEvent<?> event : key.pollEvents()) {
final WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) { // An OVERFLOW event can occur regardless of registration if events are lost or discarded.
LOG.warn("Filesystem monitor: filesystem events may have been missed");
continue;
}
final WatchEvent<Path> ev = (WatchEvent<Path>) event;
final Path filename = ev.context(); // The filename is the context of the event.
final Path child = moduleDir.resolve(filename); // Resolve the filename against the directory.
if (isValidFile(child, kind == ENTRY_DELETE)) {
try {
final URL jarUrl = child.toUri().toURL();
LOG.info("Filesystem monitor: detected module file {} {}", child,
kind == ENTRY_CREATE ? "created"
: kind == ENTRY_MODIFY ? "modified"
: kind == ENTRY_DELETE ? "deleted"
: null);
if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY)
instance.reloadModule(jarUrl);
else if (kind == ENTRY_DELETE)
instance.unloadModule(jarUrl);
} catch (Exception e) {
LOG.error("Filesystem monitor: exception while processing " + child, e);
}
} else {
if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY)
LOG.warn("Filesystem monitor: A non-jar item " + child.getFileName() + " has been placed in the modules directory " + moduleDir);
}
}
if (!key.reset())
throw new IOException("Directory " + moduleDir + " is no longer accessible");
}
} catch (Exception e) {
LOG.error("Filesystem monitor thread terminated with an exception", e);
throw Exceptions.rethrow(e);
}
}
private static boolean isValidFile(Path file, boolean delete) {
return (delete || Files.isRegularFile(file)) && file.getFileName().toString().endsWith(".jar");
}
}