diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jdisc_core/src/main/java/com/yahoo/jdisc/core |
Publish
Diffstat (limited to 'jdisc_core/src/main/java/com/yahoo/jdisc/core')
22 files changed, 2278 insertions, 0 deletions
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java new file mode 100644 index 00000000000..a296bd1e327 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java @@ -0,0 +1,137 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.SharedResource; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.ResourcePool; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import com.yahoo.jdisc.service.CurrentContainer; +import com.yahoo.jdisc.service.NoBindingSetSelectedException; +import com.yahoo.jdisc.service.ServerProvider; + +import java.net.URI; +import java.util.Map; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ActiveContainer extends AbstractResource implements CurrentContainer { + + private final ContainerTermination termination; + private final Injector guiceInjector; + private final Iterable<ServerProvider> serverProviders; + private final ResourcePool resourceReferences = new ResourcePool(); + private final Map<String, BindingSet<RequestHandler>> serverBindings; + private final Map<String, BindingSet<RequestHandler>> clientBindings; + private final BindingSetSelector bindingSetSelector; + private final TimeoutManagerImpl timeoutMgr; + + public ActiveContainer(ContainerBuilder builder) { + serverProviders = builder.serverProviders().activate(); + for (SharedResource resource : serverProviders) { + resourceReferences.retain(resource); + } + serverBindings = builder.activateServerBindings(); + for (BindingSet<RequestHandler> set : serverBindings.values()) { + for (Map.Entry<UriPattern, RequestHandler> entry : set) { + resourceReferences.retain(entry.getValue()); + } + } + clientBindings = builder.activateClientBindings(); + for (BindingSet<RequestHandler> set : clientBindings.values()) { + for (Map.Entry<UriPattern, RequestHandler> entry : set) { + resourceReferences.retain(entry.getValue()); + } + } + bindingSetSelector = builder.getInstance(BindingSetSelector.class); + timeoutMgr = builder.getInstance(TimeoutManagerImpl.class); + timeoutMgr.start(); + builder.guiceModules().install(new AbstractModule() { + + @Override + protected void configure() { + bind(TimeoutManagerImpl.class).toInstance(timeoutMgr); + } + }); + guiceInjector = builder.guiceModules().activate(); + termination = new ContainerTermination(builder.appContext()); + } + + @Override + protected void destroy() { + resourceReferences.release(); + timeoutMgr.shutdown(); + termination.run(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (retainCount() > 0) { + destroy(); + } + } finally { + super.finalize(); + } + } + + /** + * Make this instance retain a reference to the resource until it is destroyed. + */ + void retainReference(SharedResource resource) { + resourceReferences.retain(resource); + } + + public ContainerTermination shutdown() { + return termination; + } + + public Injector guiceInjector() { + return guiceInjector; + } + + public Iterable<ServerProvider> serverProviders() { + return serverProviders; + } + + public Map<String, BindingSet<RequestHandler>> serverBindings() { + return serverBindings; + } + + public BindingSet<RequestHandler> serverBindings(String setName) { + return serverBindings.get(setName); + } + + public Map<String, BindingSet<RequestHandler>> clientBindings() { + return clientBindings; + } + + public BindingSet<RequestHandler> clientBindings(String setName) { + return clientBindings.get(setName); + } + + TimeoutManagerImpl timeoutManager() { + return timeoutMgr; + } + + @Override + public ContainerSnapshot newReference(URI uri) { + String name = bindingSetSelector.select(uri); + if (name == null) { + throw new NoBindingSetSelectedException(uri); + } + BindingSet<RequestHandler> serverBindings = serverBindings(name); + BindingSet<RequestHandler> clientBindings = clientBindings(name); + if (serverBindings == null || clientBindings == null) { + throw new BindingSetNotFoundException(name); + } + return new ContainerSnapshot(this, serverBindings, clientBindings); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java new file mode 100644 index 00000000000..00908df4249 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java @@ -0,0 +1,64 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.common.collect.ImmutableMap; +import com.google.inject.AbstractModule; +import com.google.inject.name.Names; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ApplicationConfigModule extends AbstractModule { + + private final Map<String, String> config; + + ApplicationConfigModule(Map<String, String> config) { + this.config = normalizeConfig(config); + } + + @Override + protected void configure() { + for (Map.Entry<String, String> entry : config.entrySet()) { + bind(String.class).annotatedWith(Names.named(entry.getKey())).toInstance(entry.getValue()); + } + } + + public static ApplicationConfigModule newInstanceFromFile(String fileName) throws IOException { + Properties props = new Properties(); + InputStream in = null; + try { + in = new FileInputStream(fileName); + props.load(in); + } finally { + if (in != null) { + in.close(); + } + } + Map<String, String> ret = new HashMap<>(); + for (String name : props.stringPropertyNames()) { + ret.put(name, props.getProperty(name)); + } + return new ApplicationConfigModule(ret); + } + + private static Map<String, String> normalizeConfig(Map<String, String> raw) { + List<String> names = new ArrayList<>(raw.keySet()); + Collections.sort(names, new Comparator<String>() { + + @Override + public int compare(String lhs, String rhs) { + return -lhs.compareTo(rhs); // reverse alphabetical order, i.e. lower-case before upper-case + } + }); + Map<String, String> ret = new HashMap<>(); + for (String name : names) { + ret.put(name.toLowerCase(Locale.US), raw.get(name)); + } + return ImmutableMap.copyOf(ret); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java new file mode 100644 index 00000000000..c6d6efd0ee9 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.yahoo.jdisc.application.ContainerActivator; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.ContainerThread; +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.util.concurrent.ThreadFactory; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +class ApplicationEnvironmentModule extends AbstractModule { + + private final ApplicationLoader loader; + + public ApplicationEnvironmentModule(ApplicationLoader loader) { + this.loader = loader; + } + + @Override + protected void configure() { + bind(ContainerActivator.class).toInstance(loader); + bind(CurrentContainer.class).toInstance(loader); + bind(OsgiFramework.class).toInstance(loader.osgiFramework()); + bind(ThreadFactory.class).to(ContainerThread.Factory.class); + } + + @Provides + public ContainerBuilder containerBuilder() { + return loader.newContainerBuilder(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java new file mode 100644 index 00000000000..2dd7f7eb879 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java @@ -0,0 +1,261 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.application.*; +import com.yahoo.jdisc.service.ContainerNotReadyException; +import com.yahoo.jdisc.service.CurrentContainer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; + +import java.lang.ref.WeakReference; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ApplicationLoader implements BootstrapLoader, ContainerActivator, CurrentContainer { + + private static final Logger log = Logger.getLogger(ApplicationLoader.class.getName()); + private final OsgiFramework osgiFramework; + private final GuiceRepository guiceModules = new GuiceRepository(); + private final AtomicReference<ActiveContainer> containerRef = new AtomicReference<>(); + private final Object appLock = new Object(); + private final List<Bundle> appBundles = new ArrayList<>(); + private Application application; + private ApplicationInUseTracker applicationInUseTracker; + + public ApplicationLoader(OsgiFramework osgiFramework, Iterable<? extends Module> guiceModules) { + this.osgiFramework = osgiFramework; + this.guiceModules.install(new ApplicationEnvironmentModule(this)); + this.guiceModules.installAll(guiceModules); + } + + @Override + public ContainerBuilder newContainerBuilder() { + return new ContainerBuilder(guiceModules); + } + + @Override + public DeactivatedContainer activateContainer(ContainerBuilder builder) { + ActiveContainer next = builder != null ? new ActiveContainer(builder) : null; + final ActiveContainer prev; + synchronized (appLock) { + if (application == null && next != null) { + next.release(); + throw new ApplicationNotReadyException(); + } + + if (next != null) { + next.retainReference(applicationInUseTracker); + } + + prev = containerRef.getAndSet(next); + if (prev == null) { + return null; + } + } + prev.release(); + DeactivatedContainer deactivatedContainer = prev.shutdown(); + + final WeakReference<ActiveContainer> prevContainerReference = new WeakReference<>(prev); + final Runnable deactivationMonitor = () -> { + long waitTimeSeconds = 30L; + long totalTimeWaited = 0L; + + while (!Thread.interrupted()) { + final long currentWaitTimeSeconds = waitTimeSeconds; + totalTimeWaited += currentWaitTimeSeconds; + + Interruption.mapExceptionToThreadState(() -> + Thread.sleep(TimeUnit.MILLISECONDS.convert(currentWaitTimeSeconds, TimeUnit.SECONDS)) + ); + + final ActiveContainer prevContainer = prevContainerReference.get(); + if (prevContainer == null) { + return; + } + if (prevContainer.retainCount() == 0) { + return; + } + log.warning("Previous container not terminated in the last " + totalTimeWaited + " seconds." + + " Reference state={ " + prevContainer.currentState() + " }"); + + waitTimeSeconds = (long) (waitTimeSeconds * 1.2); + } + log.warning("Deactivation monitor thread unexpectedly interrupted"); + }; + final Thread deactivationMonitorThread = new Thread(deactivationMonitor, "Container deactivation monitor"); + deactivationMonitorThread.setDaemon(true); + deactivationMonitorThread.start(); + + return deactivatedContainer; + } + + @Override + public ContainerSnapshot newReference(URI uri) { + ActiveContainer container = containerRef.get(); + if (container == null) { + throw new ContainerNotReadyException(); + } + return container.newReference(uri); + } + + @Override + public void init(String appLocation, boolean privileged) throws Exception { + log.finer("Initializing application loader."); + osgiFramework.start(); + BundleContext ctx = osgiFramework.bundleContext(); + if (ctx != null) { + ctx.registerService(CurrentContainer.class.getName(), this, null); + } + if(appLocation == null) { + return; // application class bound by another module + } + try { + final Class<Application> appClass = ContainerBuilder.safeClassCast(Application.class, Class.forName(appLocation)); + guiceModules.install(new AbstractModule() { + @Override + public void configure() { + bind(Application.class).to(appClass); + } + }); + return; // application class found on class path + } catch (ClassNotFoundException e) { + // location is not a class name + if (log.isLoggable(Level.FINE)) { + log.fine("App location is not a class name. Installing bundle"); + } + } + appBundles.addAll(osgiFramework.installBundle(appLocation)); + if (OsgiHeader.isSet(appBundles.get(0), OsgiHeader.PRIVILEGED_ACTIVATOR)) { + osgiFramework.startBundles(appBundles, privileged); + } + + } + + @Override + public void start() throws Exception { + log.finer("Initializing application."); + Injector injector = guiceModules.activate(); + Application app; + if (!appBundles.isEmpty()) { + Bundle appBundle = appBundles.get(0); + if (!OsgiHeader.isSet(appBundle, OsgiHeader.PRIVILEGED_ACTIVATOR)) { + osgiFramework.startBundles(appBundles, false); + } + List<String> header = OsgiHeader.asList(appBundle, OsgiHeader.APPLICATION); + if (header.size() != 1) { + throw new IllegalArgumentException("OSGi header '" + OsgiHeader.APPLICATION + "' has " + header.size() + + " entries, expected 1."); + } + String appName = header.get(0); + log.finer("Loading application class " + appName + " from bundle '" + appBundle.getSymbolicName() + "'."); + Class<Application> appClass = ContainerBuilder.safeClassCast(Application.class, + appBundle.loadClass(appName)); + app = injector.getInstance(appClass); + } else { + app = injector.getInstance(Application.class); + log.finer("Injecting instance of " + app.getClass().getName() + "."); + } + try { + synchronized (appLock) { + application = app; + applicationInUseTracker = new ApplicationInUseTracker(); + } + app.start(); + } catch (Exception e) { + log.log(Level.WARNING, "Exception thrown while activating application.", e); + synchronized (appLock) { + application = null; + applicationInUseTracker = null; + } + app.destroy(); + throw e; + } + } + + @Override + public void stop() throws Exception { + log.finer("Destroying application."); + Application app; + ApplicationInUseTracker applicationInUseTracker; + synchronized (appLock) { + app = application; + applicationInUseTracker = this.applicationInUseTracker; + } + if (app == null || applicationInUseTracker == null) { + return; + } + try { + app.stop(); + } catch (Exception e) { + log.log(Level.WARNING, "Exception thrown while deactivating application.", e); + } + synchronized (appLock) { + application = null; + } + activateContainer(null); + synchronized (appLock) { + this.applicationInUseTracker = null; + } + applicationInUseTracker.release(); + applicationInUseTracker.applicationInUseLatch.await(); + app.destroy(); + } + + @Override + public void destroy() { + log.finer("Destroying application loader."); + try { + osgiFramework.stop(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + + public Application application() { + synchronized (appLock) { + return application; + } + } + + public OsgiFramework osgiFramework() { + return osgiFramework; + } + + private static class ApplicationInUseTracker extends AbstractResource { + //opened when the application has been stopped and there's no active containers + final CountDownLatch applicationInUseLatch = new CountDownLatch(1); + + @Override + protected void destroy() { + applicationInUseLatch.countDown(); + } + } + + private static class Interruption { + interface Runnable_throws<E extends Throwable> { + void run() throws E; + } + + public static void mapExceptionToThreadState(Runnable_throws<InterruptedException> runnable) { + try { + runnable.run(); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java new file mode 100644 index 00000000000..21c52d6047d --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java @@ -0,0 +1,104 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Module; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.application.OsgiFramework; +import org.apache.commons.daemon.Daemon; +import org.apache.commons.daemon.DaemonContext; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BootstrapDaemon implements Daemon { + + private static final Logger log = Logger.getLogger(BootstrapDaemon.class.getName()); + private final BootstrapLoader loader; + private final boolean privileged; + private String bundleLocation; + + static { + // force load slf4j to avoid other logging frameworks from initializing before it + org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + } + + public BootstrapDaemon() { + this(new ApplicationLoader(newOsgiFramework(), newConfigModule()), + Boolean.valueOf(System.getProperty("jdisc.privileged"))); + } + + BootstrapDaemon(BootstrapLoader loader, boolean privileged) { + this.loader = loader; + this.privileged = privileged; + } + + BootstrapLoader loader() { + return loader; + } + + @Override + public void init(DaemonContext context) throws Exception { + String[] args = context.getArguments(); + if (args == null || args.length != 1 || args[0] == null) { + throw new IllegalArgumentException("Expected 1 argument, got " + Arrays.toString(args) + "."); + } + bundleLocation = args[0]; + if (privileged) { + log.finer("Initializing application with privileges."); + loader.init(bundleLocation, true); + } + } + + @Override + public void start() throws Exception { + if (!privileged) { + log.finer("Initializing application without privileges."); + loader.init(bundleLocation, false); + } + loader.start(); + } + + @Override + public void stop() throws Exception { + loader.stop(); + } + + @Override + public void destroy() { + loader.destroy(); + } + + private static OsgiFramework newOsgiFramework() { + String cachePath = System.getProperty("jdisc.cache.path"); + if (cachePath == null) { + throw new IllegalStateException("System property 'jdisc.cache.path' not set."); + } + FelixParams params = new FelixParams() + .setCachePath(cachePath) + .setLoggerEnabled(Boolean.valueOf(System.getProperty("jdisc.logger.enabled", "true"))); + for (String str : ContainerBuilder.safeStringSplit(System.getProperty("jdisc.export.packages"), ",")) { + params.exportPackage(str); + } + return new FelixFramework(params); + } + + private static Iterable<Module> newConfigModule() { + String configFile = System.getProperty("jdisc.config.file"); + if (configFile == null) { + return Collections.emptyList(); + } + Module configModule; + try { + configModule = ApplicationConfigModule.newInstanceFromFile(configFile); + } catch (IOException e) { + throw new IllegalStateException("Exception thrown while reading config file '" + configFile + "'.", e); + } + return Arrays.asList(configModule); + } + +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java new file mode 100644 index 00000000000..68e9f58c7ff --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface BootstrapLoader { + + public void init(String bundleLocation, boolean privileged) throws Exception; + + public void start() throws Exception; + + public void stop() throws Exception; + + public void destroy(); +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java new file mode 100644 index 00000000000..a65040b0451 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import java.io.File; +import java.io.IOException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class BundleLocationResolver { + + static final String BUNDLE_PATH = System.getProperty("jdisc.bundle.path", ".") + "/"; + + public static String resolve(String bundleLocation) { + bundleLocation = expandSystemProperties(bundleLocation); + bundleLocation = bundleLocation.trim(); + String scheme = getLocationScheme(bundleLocation); + if (scheme == null) { + bundleLocation = "file:" + getCanonicalPath(BUNDLE_PATH + bundleLocation); + } else if (scheme.equalsIgnoreCase("file")) { + bundleLocation = "file:" + getCanonicalPath(bundleLocation.substring(5)); + } + return bundleLocation; + } + + private static String expandSystemProperties(String str) { + StringBuilder ret = new StringBuilder(); + int prev = 0; + while (true) { + int from = str.indexOf("${", prev); + if (from < 0) { + break; + } + ret.append(str.substring(prev, from)); + prev = from; + + int to = str.indexOf("}", from); + if (to < 0) { + break; + } + ret.append(System.getProperty(str.substring(from + 2, to), "")); + prev = to + 1; + } + if (prev >= 0) { + ret.append(str.substring(prev)); + } + return ret.toString(); + } + + private static String getCanonicalPath(String path) { + try { + return new File(path).getCanonicalPath(); + } catch (IOException e) { + return path; + } + } + + private static String getLocationScheme(String bundleLocation) { + char[] arr = bundleLocation.toCharArray(); + for (int i = 0; i < arr.length; ++i) { + if (arr[i] == ':' && i > 0) { + return bundleLocation.substring(0, i); + } + if (!Character.isLetterOrDigit(arr[i])) { + return null; + } + } + return null; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java new file mode 100644 index 00000000000..899e8a98aa7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java @@ -0,0 +1,199 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogService; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ConsoleLogFormatter { + + // The string used as a replacement for absent/null values. + static final String ABSENCE_REPLACEMENT = "-"; + + private final String hostName; + private final String processId; + private final String serviceName; + + public ConsoleLogFormatter(String hostName, String processId, String serviceName) { + this.hostName = formatOptional(hostName); + this.processId = formatOptional(processId); + this.serviceName = formatOptional(serviceName); + } + + public String formatEntry(LogEntry entry) { + StringBuilder ret = new StringBuilder(); + formatTime(entry, ret).append('\t'); + formatHostName(ret).append('\t'); + formatProcessId(entry, ret).append('\t'); + formatServiceName(ret).append('\t'); + formatComponent(entry, ret).append('\t'); + formatLevel(entry, ret).append('\t'); + formatMessage(entry, ret); + formatException(entry, ret); + return ret.toString(); + } + + // TODO: The non-functional, side effect-laden coding style here is ugly and makes testing hard. See ticket 7128315. + + private StringBuilder formatTime(LogEntry entry, StringBuilder out) { + String str = Long.toString(Long.MAX_VALUE & entry.getTime()); // remove sign bit for good measure + int len = str.length(); + if (len > 3) { + out.append(str, 0, len - 3); + } else { + out.append('0'); + } + out.append('.'); + if (len > 2) { + out.append(str, len - 3, len); + } else if (len == 2) { + out.append('0').append(str, len - 2, len); // should never happen + } else if (len == 1) { + out.append("00").append(str, len - 1, len); // should never happen + } + return out; + } + + private StringBuilder formatHostName(StringBuilder out) { + out.append(hostName); + return out; + } + + private StringBuilder formatProcessId(LogEntry entry, StringBuilder out) { + out.append(processId); + String threadId = getProperty(entry, "THREAD_ID"); + if (threadId != null) { + out.append('/').append(threadId); + } + return out; + } + + private StringBuilder formatServiceName(StringBuilder out) { + out.append(serviceName); + return out; + } + + private StringBuilder formatComponent(LogEntry entry, StringBuilder out) { + Bundle bundle = entry.getBundle(); + String loggerName = getProperty(entry, "LOGGER_NAME"); + if (bundle == null && loggerName == null) { + out.append("-"); + } else { + if (bundle != null) { + out.append(bundle.getSymbolicName()); + } + if (loggerName != null) { + out.append('/').append(loggerName); + } + } + return out; + } + + private StringBuilder formatLevel(LogEntry entry, StringBuilder out) { + switch (entry.getLevel()) { + case LogService.LOG_ERROR: + out.append("error"); + break; + case LogService.LOG_WARNING: + out.append("warning"); + break; + case LogService.LOG_INFO: + out.append("info"); + break; + case LogService.LOG_DEBUG: + out.append("debug"); + break; + default: + out.append("unknown"); + break; + } + return out; + } + + private StringBuilder formatMessage(LogEntry entry, StringBuilder out) { + String msg = entry.getMessage(); + if (msg != null) { + formatString(msg, out); + } + return out; + } + + private StringBuilder formatException(LogEntry entry, StringBuilder out) { + Throwable t = entry.getException(); + if (t != null) { + if (entry.getLevel() == LogService.LOG_INFO) { + out.append(": "); + String msg = t.getMessage(); + if (msg != null) { + formatString(msg, out); + } else { + out.append(t.getClass().getName()); + } + } else { + Writer buf = new StringWriter(); + t.printStackTrace(new PrintWriter(buf)); + formatString("\n" + buf, out); + } + } + return out; + } + + private static StringBuilder formatString(String str, StringBuilder out) { + for (int i = 0, len = str.length(); i < len; ++i) { + char c = str.charAt(i); + switch (c) { + case '\n': + out.append("\\n"); + break; + case '\r': + out.append("\\r"); + break; + case '\t': + out.append("\\t"); + break; + case '\\': + out.append("\\\\"); + break; + default: + out.append(c); + break; + } + } + return out; + } + + private static String getProperty(LogEntry entry, String name) { + ServiceReference<?> ref = entry.getServiceReference(); + if (ref == null) { + return null; + } + Object val = ref.getProperty(name); + if (val == null) { + return null; + } + return val.toString(); + } + + static String formatOptional(String str) { + return formatOptional(str, ABSENCE_REPLACEMENT); + } + + private static String formatOptional(final String str, final String replacementIfAbsent) { + if (str == null) { + return replacementIfAbsent; + } + final String result = str.trim(); + if (result.isEmpty()) { + return replacementIfAbsent; + } + return result; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java new file mode 100644 index 00000000000..b41e195f6a7 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogListener; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.management.ManagementFactory; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +class ConsoleLogListener implements LogListener { + + public static final int DEFAULT_LOG_LEVEL = Integer.MAX_VALUE; + private final ConsoleLogFormatter formatter; + private final PrintStream out; + private final int maxLevel; + + ConsoleLogListener(PrintStream out, String serviceName, String logLevel) { + this.out = out; + this.formatter = new ConsoleLogFormatter(getHostname(), getProcessId(), serviceName); + this.maxLevel = parseLogLevel(logLevel); + } + + @Override + public void logged(LogEntry entry) { + if (entry.getLevel() > maxLevel) { + return; + } + out.println(formatter.formatEntry(entry)); + } + + public static int parseLogLevel(String logLevel) { + if (logLevel == null || logLevel.isEmpty()) { + return DEFAULT_LOG_LEVEL; + } + if (logLevel.equalsIgnoreCase("OFF")) { + return Integer.MIN_VALUE; + } + if (logLevel.equalsIgnoreCase("ERROR")) { + return 1; + } + if (logLevel.equalsIgnoreCase("WARNING")) { + return 2; + } + if (logLevel.equalsIgnoreCase("INFO")) { + return 3; + } + if (logLevel.equalsIgnoreCase("DEBUG")) { + return 4; + } + if (logLevel.equalsIgnoreCase("ALL")) { + return Integer.MAX_VALUE; + } + try { + return Integer.valueOf(logLevel); + } catch (NumberFormatException e) { + // fall through + } + return DEFAULT_LOG_LEVEL; + } + + public static ConsoleLogListener newInstance() { + return new ConsoleLogListener(System.out, + System.getProperty("jdisc.logger.tag"), + System.getProperty("jdisc.logger.level")); + } + + static String getProcessId() { + // platform independent + String jvmName = ManagementFactory.getRuntimeMXBean().getName(); + if (jvmName != null) { + int idx = jvmName.indexOf('@'); + if (idx > 0) { + try { + return Long.toString(Long.valueOf(jvmName.substring(0, jvmName.indexOf('@')))); + } catch (NumberFormatException e) { + // fall through + } + } + } + + // linux specific + File file = new File("/proc/self"); + if (file.exists()) { + try { + return file.getCanonicalFile().getName(); + } catch (IOException e) { + return null; + } + } + + // fallback + return null; + } + + static String getHostname() { + try { + return InetAddress.getLocalHost().getCanonicalHostName(); + } catch (UnknownHostException e) { + return null; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java new file mode 100644 index 00000000000..c5e8602c861 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogReaderService; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a> + */ +class ConsoleLogManager { + + private final ConsoleLogListener listener = ConsoleLogListener.newInstance(); + private ServiceTracker<LogReaderService,LogReaderService> tracker; + + @SuppressWarnings("unchecked") + public void install(final BundleContext osgiContext) { + if (tracker != null) { + throw new IllegalStateException("ConsoleLogManager already installed."); + } + tracker = new ServiceTracker<LogReaderService,LogReaderService>(osgiContext, LogReaderService.class.getName(), + new ServiceTrackerCustomizer<LogReaderService,LogReaderService>() { + + @Override + public LogReaderService addingService(ServiceReference<LogReaderService> reference) { + LogReaderService service = osgiContext.getService(reference); + service.addLogListener(listener); + return service; + } + + @Override + public void modifiedService(ServiceReference<LogReaderService> reference, LogReaderService service) { + + } + + @Override + public void removedService(ServiceReference<LogReaderService> reference, LogReaderService service) { + service.removeLogListener(listener); + } + }); + tracker.open(); + } + + public boolean uninstall() { + if (tracker == null) { + return false; + } + tracker.close(); + tracker = null; + return true; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java new file mode 100644 index 00000000000..4f4544fa8f8 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java @@ -0,0 +1,112 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Key; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.NullContent; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.util.Objects; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +class ContainerSnapshot extends AbstractResource implements Container { + + private final TimeoutManagerImpl timeoutMgr; + private final ActiveContainer container; + private final ResourceReference containerReference; + private final BindingSet<RequestHandler> serverBindings; + private final BindingSet<RequestHandler> clientBindings; + + ContainerSnapshot(ActiveContainer container, BindingSet<RequestHandler> serverBindings, + BindingSet<RequestHandler> clientBindings) + { + this.timeoutMgr = container.timeoutManager(); + this.container = container; + this.serverBindings = serverBindings; + this.clientBindings = clientBindings; + this.containerReference = container.refer(); + } + + @Override + public <T> T getInstance(Key<T> key) { + return container.guiceInjector().getInstance(key); + } + + @Override + public <T> T getInstance(Class<T> type) { + return container.guiceInjector().getInstance(type); + } + + @Override + public RequestHandler resolveHandler(Request request) { + BindingMatch<RequestHandler> match = request.isServerRequest() ? serverBindings.match(request.getUri()) + : clientBindings.match(request.getUri()); + if (match == null) { + return null; + } + request.setBindingMatch(match); + RequestHandler ret = new NullContentRequestHandler(match.target()); + if (request.getTimeoutManager() == null) { + ret = timeoutMgr.manageHandler(ret); + } + return ret; + } + + @Override + protected void destroy() { + containerReference.close(); + } + + @Override + public long currentTimeMillis() { + return timeoutMgr.timer().currentTimeMillis(); + } + + private static class NullContentRequestHandler implements RequestHandler { + + final RequestHandler delegate; + + NullContentRequestHandler(RequestHandler delegate) { + Objects.requireNonNull(delegate, "delegate"); + this.delegate = delegate; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { + ContentChannel content = delegate.handleRequest(request, responseHandler); + if (content == null) { + content = NullContent.INSTANCE; + } + return content; + } + + @Override + public void handleTimeout(Request request, ResponseHandler responseHandler) { + delegate.handleTimeout(request, responseHandler); + } + + @Override + public ResourceReference refer() { + return delegate.refer(); + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public String toString() { + return delegate.toString(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java new file mode 100644 index 00000000000..0fd25bfb390 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.application.DeactivatedContainer; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ContainerTermination implements DeactivatedContainer, Runnable { + + private final Object lock = new Object(); + private final Object appContext; + private Runnable task; + private boolean done; + + public ContainerTermination(Object appContext) { + this.appContext = appContext; + } + + @Override + public Object appContext() { + return appContext; + } + + @Override + public void notifyTermination(Runnable task) { + boolean done; + synchronized (lock) { + if (this.task != null) { + throw new IllegalStateException(); + } + this.task = task; + done = this.done; + } + if (done) { + task.run(); + } + } + + @Override + public void run() { + Runnable task; + synchronized (lock) { + done = true; + task = this.task; + } + if (task != null) { + task.run(); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java new file mode 100644 index 00000000000..7e4a7b6ec5e --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java @@ -0,0 +1,18 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.application.BindingSetSelector; + +import java.net.URI; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class DefaultBindingSelector implements BindingSetSelector { + + @Override + public String select(URI uri) { + return BindingSet.DEFAULT; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java new file mode 100644 index 00000000000..afe43718bc5 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java @@ -0,0 +1,98 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.container.plugin.bundle.AnalyzeBundle; +import com.yahoo.container.plugin.bundle.TransformExportPackages; +import com.yahoo.container.plugin.osgi.ExportPackages.Export; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.Constants; +import scala.collection.immutable.List; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Properties; +import java.util.jar.JarInputStream; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ExportPackages { + + public static final String PROPERTIES_FILE = "/exportPackages.properties"; + public static final String EXPORT_PACKAGES = "exportPackages"; + private static final String REPLACE_VERSION_PREFIX = "__REPLACE_VERSION__"; + + public static void main(String[] args) throws IOException { + String fileName = args[0]; + if (!fileName.endsWith(PROPERTIES_FILE)) { + throw new IllegalArgumentException("Expected '" + PROPERTIES_FILE + "', got '" + fileName + "'."); + } + StringBuilder out = new StringBuilder(); + out.append(getSystemPackages()).append(",") + .append("com.sun.security.auth,") + .append("com.sun.security.auth.module,") + .append("com.sun.management,") + .append("com.yahoo.jdisc,") + .append("com.yahoo.jdisc.application,") + .append("com.yahoo.jdisc.handler,") + .append("com.yahoo.jdisc.service,") + .append("javax.inject;version=1.0.0,") // Included in guice, but not exported. Needed by container-jersey. + .append("org.aopalliance.intercept,") + .append("org.aopalliance.aop,") + .append("org.w3c.dom.css,") + .append("org.w3c.dom.html,") + .append("org.w3c.dom.ranges,") + .append("org.w3c.dom.stylesheets,") + .append("org.w3c.dom.traversal,") + .append("org.w3c.dom.views,") + .append("sun.misc,") + .append("sun.net.util,") + .append("sun.security.krb5"); + for (int i = 1; i < args.length; ++i) { + out.append(",").append(getExportedPackages(args[i])); + } + Properties props = new Properties(); + props.setProperty(EXPORT_PACKAGES, out.toString()); + + try (FileWriter writer = new FileWriter(new File(fileName))) { + props.store(writer, "generated by " + ExportPackages.class.getName()); + } + } + + public static String readExportProperty() { + Properties props = new Properties(); + try { + props.load(ExportPackages.class.getResourceAsStream(PROPERTIES_FILE)); + } catch (IOException e) { + throw new IllegalStateException("Failed to read resource '" + PROPERTIES_FILE + "'."); + } + return props.getProperty(EXPORT_PACKAGES); + } + + public static String getSystemPackages() { + return Util.getDefaultProperty(null, "org.osgi.framework.system.packages"); + } + + private static String getExportedPackages(String argument) throws IOException { + if (argument.startsWith(REPLACE_VERSION_PREFIX)) { + String jarFile = argument.substring(REPLACE_VERSION_PREFIX.length()); + return readExportHeader(jarFile); + } else { + return readExportHeader(argument); + } + } + + private static String readExportHeader(String jarFile) throws IOException { + try (JarInputStream jar = new JarInputStream(new FileInputStream(jarFile))) { + return jar.getManifest().getMainAttributes().getValue(Constants.EXPORT_PACKAGE); + } + } + + private static String transformExports(List<Export> exports, String newVersion) { + return TransformExportPackages.toExportPackageProperty( + TransformExportPackages.removeUses( + TransformExportPackages.replaceVersions(exports, newVersion))); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java new file mode 100644 index 00000000000..6509d505c70 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java @@ -0,0 +1,175 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Inject; +import com.yahoo.jdisc.application.BundleInstallationException; +import com.yahoo.jdisc.application.OsgiFramework; +import com.yahoo.jdisc.application.OsgiHeader; +import org.apache.felix.framework.Felix; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.wiring.FrameworkWiring; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FelixFramework implements OsgiFramework { + + private static final Logger log = Logger.getLogger(FelixFramework.class.getName()); + private final OsgiLogManager logHandler = OsgiLogManager.newInstance(); + private final OsgiLogService logService = new OsgiLogService(); + private final ConsoleLogManager logListener; + private final Felix felix; + + @Inject + public FelixFramework(FelixParams params) { + deleteDirContents(new File(params.getCachePath())); + felix = new Felix(params.toConfig()); + logListener = params.isLoggerEnabled() ? new ConsoleLogManager() : null; + } + + @Override + public void start() throws BundleException { + log.finer("Starting Felix."); + felix.start(); + + BundleContext ctx = felix.getBundleContext(); + logService.start(ctx); + logHandler.install(ctx); + if (logListener != null) { + logListener.install(ctx); + } + } + + @Override + public void stop() throws BundleException { + log.fine("Stopping felix."); + BundleContext ctx = felix.getBundleContext(); + if (ctx != null) { + if (logListener != null) { + logListener.uninstall(); + } + logHandler.uninstall(); + logService.stop(); + } + felix.stop(); + try { + felix.waitForStop(0); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public List<Bundle> installBundle(String bundleLocation) throws BundleException { + List<Bundle> bundles = new LinkedList<>(); + try { + installBundle(bundleLocation, new HashSet<>(), bundles); + } catch (Exception e) { + throw new BundleInstallationException(bundles, e); + } + return bundles; + } + + @Override + public void startBundles(List<Bundle> bundles, boolean privileged) throws BundleException { + for (Bundle bundle : bundles) { + if (!privileged && OsgiHeader.isSet(bundle, OsgiHeader.PRIVILEGED_ACTIVATOR)) { + log.log(Level.INFO, "OSGi bundle '" + bundle.getSymbolicName() + "' " + + "states that it requires privileged " + + "initialization, but privileges are not available. YMMV."); + } + if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) { + continue; // fragments can not be started + } + bundle.start(); + } + } + + @Override + public void refreshPackages() { + FrameworkWiring wiring = felix.adapt(FrameworkWiring.class); + final CountDownLatch latch = new CountDownLatch(1); + wiring.refreshBundles(null, + event -> { + switch (event.getType()) { + case FrameworkEvent.PACKAGES_REFRESHED: + latch.countDown(); + break; + case FrameworkEvent.ERROR: + log.log(Level.SEVERE, "ERROR FrameworkEvent received.", event.getThrowable()); + break; + } + }); + try { + long TIMEOUT_SECONDS = 60L; + if (!latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + log.warning("No PACKAGES_REFRESHED FrameworkEvent received within " + TIMEOUT_SECONDS + + " seconds of calling FrameworkWiring.refreshBundles()"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override + public BundleContext bundleContext() { + return felix.getBundleContext(); + } + + @Override + public List<Bundle> bundles() { + return Arrays.asList(felix.getBundleContext().getBundles()); + } + + private void installBundle(String bundleLocation, Set<String> mask, List<Bundle> out) throws BundleException { + bundleLocation = BundleLocationResolver.resolve(bundleLocation); + if (mask.contains(bundleLocation)) { + log.finer("OSGi bundle from '" + bundleLocation + "' already installed."); + return; + } + log.finer("Installing OSGi bundle from '" + bundleLocation + "'."); + mask.add(bundleLocation); + + Bundle bundle = felix.getBundleContext().installBundle(bundleLocation); + String symbol = bundle.getSymbolicName(); + if (symbol == null) { + bundle.uninstall(); + throw new BundleException("Missing Bundle-SymbolicName in manifest from '" + bundleLocation + " " + + "(it might not be an OSGi bundle)."); + } + out.add(bundle); + for (String preInstall : OsgiHeader.asList(bundle, OsgiHeader.PREINSTALL_BUNDLE)) { + log.finer("OSGi bundle '" + symbol + "' requires install from '" + preInstall + "'."); + installBundle(preInstall, mask, out); + } + } + + private static void deleteDirContents(File parent) { + File[] children = parent.listFiles(); + if (children != null) { + for (File child : children) { + deleteDirContents(child); + boolean deleted = child.delete(); + if (! deleted) + throw new RuntimeException( + "Could not delete file '" + child.getAbsolutePath() +"'. Please check file permissions!"); + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java new file mode 100644 index 00000000000..0fe09798ccc --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.apache.felix.framework.cache.BundleCache; +import org.osgi.framework.Constants; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class FelixParams { + + private final StringBuilder exportPackages = new StringBuilder(ExportPackages.readExportProperty()); + private String cachePath = null; + private boolean loggerEnabled = true; + + public FelixParams exportPackage(String pkg) { + exportPackages.append(",").append(pkg); + return this; + } + + public FelixParams setCachePath(String cachePath) { + this.cachePath = cachePath; + return this; + } + + public String getCachePath() { + return cachePath; + } + + public FelixParams setLoggerEnabled(boolean loggerEnabled) { + this.loggerEnabled = loggerEnabled; + return this; + } + + public boolean isLoggerEnabled() { + return loggerEnabled; + } + + public Map<String, String> toConfig() { + Map<String, String> ret = new HashMap<>(); + ret.put(BundleCache.CACHE_ROOTDIR_PROP, cachePath); + ret.put(Constants.FRAMEWORK_SYSTEMPACKAGES, exportPackages.toString()); + ret.put(Constants.SUPPORTS_BOOTCLASSPATH_EXTENSION, "true"); + ret.put(Constants.FRAMEWORK_BOOTDELEGATION, "com.yourkit.runtime,com.yourkit.probes,com.yourkit.probes.builtin"); + return ret; + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java new file mode 100644 index 00000000000..c4de1d5a7ac --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java @@ -0,0 +1,164 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.common.collect.ImmutableMap; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class OsgiLogHandler extends Handler { + + private static enum LogRecordProperty { + + LEVEL, + LOGGER_NAME, + MESSAGE, + MILLIS, + PARAMETERS, + RESOURCE_BUNDLE, + RESOURCE_BUNDLE_NAME, + SEQUENCE_NUMBER, + SOURCE_CLASS_NAME, + SOURCE_METHOD_NAME, + THREAD_ID, + THROWN + } + + private final static Map<String, LogRecordProperty> PROPERTY_MAP = createDictionary(LogRecordProperty.values()); + private final static String[] PROPERTY_KEYS = toStringArray(LogRecordProperty.values()); + private final LogService logService; + + public OsgiLogHandler(LogService logService) { + this.logService = logService; + } + + @Override + public void publish(LogRecord record) { + logService.log(new LogRecordReference(record), toServiceLevel(record.getLevel()), record.getMessage(), + record.getThrown()); + } + + @Override + public void flush() { + // empty + } + + @Override + public void close() { + // empty + } + + public static int toServiceLevel(Level level) { + int val = level.intValue(); + if (val >= Level.SEVERE.intValue()) { + return LogService.LOG_ERROR; + } + if (val >= Level.WARNING.intValue()) { + return LogService.LOG_WARNING; + } + if (val >= Level.INFO.intValue()) { + return LogService.LOG_INFO; + } + // Level.CONFIG + // Level.FINE + // Level.FINER + // Level.FINEST + return LogService.LOG_DEBUG; + } + + private static <T> Map<String, T> createDictionary(T[] in) { + Map<String, T> out = new HashMap<>(); + for (T t : in) { + out.put(String.valueOf(t), t); + } + return ImmutableMap.copyOf(out); + } + + private static String[] toStringArray(Object[] in) { + String[] out = new String[in.length]; + for (int i = 0; i < in.length; ++i) { + out[i] = String.valueOf(in[i]); + } + return out; + } + + private static class LogRecordReference implements ServiceReference<LogRecord> { + + final LogRecord record; + + LogRecordReference(LogRecord record) { + this.record = record; + } + + @Override + public Object getProperty(String s) { + LogRecordProperty property = PROPERTY_MAP.get(s); + if (property == null) { + return null; + } + switch (property) { + case LEVEL: + return record.getLevel(); + case LOGGER_NAME: + return record.getLoggerName(); + case MESSAGE: + return record.getMessage(); + case MILLIS: + return record.getMillis(); + case PARAMETERS: + return record.getParameters(); + case RESOURCE_BUNDLE: + return record.getResourceBundle(); + case RESOURCE_BUNDLE_NAME: + return record.getResourceBundleName(); + case SEQUENCE_NUMBER: + return record.getSequenceNumber(); + case SOURCE_CLASS_NAME: + return record.getSourceClassName(); + case SOURCE_METHOD_NAME: + return record.getSourceMethodName(); + case THREAD_ID: + return record.getThreadID(); + case THROWN: + return record.getThrown(); + default: + throw new UnsupportedOperationException(); + } + } + + @Override + public String[] getPropertyKeys() { + return PROPERTY_KEYS; + } + + @Override + public Bundle getBundle() { + return null; + } + + @Override + public Bundle[] getUsingBundles() { + return new Bundle[0]; + } + + @Override + public boolean isAssignableTo(Bundle bundle, String s) { + return false; + } + + @Override + public int compareTo(Object o) { + return 0; + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java new file mode 100644 index 00000000000..af2ee5832aa --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java @@ -0,0 +1,102 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class OsgiLogManager implements LogService { + + private static final Object globalLock = new Object(); + private final CopyOnWriteArrayList<LogService> services = new CopyOnWriteArrayList<>(); + private final boolean configureLogLevel; + private ServiceTracker<LogService,LogService> tracker; + + OsgiLogManager(boolean configureLogLevel) { + this.configureLogLevel = configureLogLevel; + } + + @SuppressWarnings("unchecked") + public void install(final BundleContext osgiContext) { + if (tracker != null) { + throw new IllegalStateException("OsgiLogManager already installed."); + } + tracker = new ServiceTracker<>(osgiContext, LogService.class, new ServiceTrackerCustomizer<LogService,LogService>() { + + @Override + public LogService addingService(ServiceReference<LogService> reference) { + LogService service = osgiContext.getService(reference); + services.add(service); + return service; + } + + @Override + public void modifiedService(ServiceReference<LogService> reference, LogService service) { + + } + + @Override + public void removedService(ServiceReference<LogService> reference, LogService service) { + services.remove(service); + } + }); + tracker.open(); + synchronized (globalLock) { + Logger root = Logger.getLogger(""); + if (configureLogLevel) { + root.setLevel(Level.ALL); + } + for (Handler handler : root.getHandlers()) { + root.removeHandler(handler); + } + root.addHandler(new OsgiLogHandler(this)); + } + } + + public boolean uninstall() { + if (tracker == null) { + return false; + } + tracker.close(); // implicitly clears the services array + tracker = null; + return true; + } + + @Override + public void log(int level, String message) { + log(null, level, message, null); + } + + @Override + public void log(int level, String message, Throwable throwable) { + log(null, level, message, throwable); + } + + @SuppressWarnings("rawtypes") + @Override + public void log(ServiceReference serviceRef, int level, String message) { + log(serviceRef, level, message, null); + } + + @SuppressWarnings("rawtypes") + @Override + public void log(ServiceReference serviceRef, int level, String message, Throwable throwable) { + for (LogService obj : services) { + obj.log(serviceRef, level, message, throwable); + } + } + + public static OsgiLogManager newInstance() { + return new OsgiLogManager(System.getProperty("java.util.logging.config.file") == null); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java new file mode 100644 index 00000000000..0e2a31938ce --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import org.osgi.framework.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class OsgiLogService { + + private ServiceRegistration<OsgiLogService> registration; + + public void start(BundleContext ctx) { + if (registration != null) { + throw new IllegalStateException(); + } + ctx.addServiceListener(new ActivatorProxy(ctx)); + registration = ctx.registerService(OsgiLogService.class, this, null); + } + + public void stop() { + registration.unregister(); + registration = null; + } + + private class ActivatorProxy implements ServiceListener { + + final BundleActivator activator = new org.apache.felix.log.Activator(); + final BundleContext ctx; + + ActivatorProxy(BundleContext ctx) { + this.ctx = ctx; + } + + @Override + public void serviceChanged(ServiceEvent event) { + if (ctx.getService(event.getServiceReference()) != OsgiLogService.this) { + return; + } + switch (event.getType()) { + case ServiceEvent.REGISTERED: + try { + activator.start(ctx); + } catch (Exception e) { + throw new RuntimeException("Exception thrown while starting " + + activator.getClass().getName() + ".", e); + } + break; + case ServiceEvent.UNREGISTERING: + try { + activator.stop(ctx); + } catch (Exception e) { + throw new RuntimeException("Exception thrown while stopping " + + activator.getClass().getName() + ".", e); + } + break; + } + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java new file mode 100644 index 00000000000..ef0e549516a --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java @@ -0,0 +1,136 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import java.util.Objects; +import java.util.Queue; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +class ScheduledQueue { + + public static final int MILLIS_PER_SLOT = 100; + public static final int NUM_SLOTS = 512; + public static final int NUM_SLOTS_UNDILATED = 3; + public static final int SLOT_MASK = 511; // bitmask to modulo NUM_SLOTS + public static final int ITER_SHIFT = 9; // number of bits to shift off SLOT_MASK + + private final Entry[] slots = new Entry[NUM_SLOTS + 1]; + private final int[] counts = new int[NUM_SLOTS + 1]; + private int currIter = 0; + private int currSlot = 0; + private long nextTick; + + public ScheduledQueue(long currentTimeMillis) { + this.nextTick = currentTimeMillis + MILLIS_PER_SLOT; + } + + public Entry newEntry(Object payload) { + Objects.requireNonNull(payload, "payload"); + return new Entry(payload); + } + + public synchronized void drainTo(long currentTimeMillis, Queue<Object> out) { + if (slots[NUM_SLOTS] == null && currentTimeMillis < nextTick) { + return; + } + drainTo(NUM_SLOTS, 0, out); + for (int i = 0; currentTimeMillis >= nextTick; i++, nextTick += MILLIS_PER_SLOT) { + if (i < NUM_SLOTS_UNDILATED) { + if (++currSlot >= NUM_SLOTS) { + currSlot = 0; + currIter++; + } + drainTo(currSlot, currIter, out); + } + } + } + + private void drainTo(int slot, int iter, Queue<Object> out) { + int cnt = counts[slot]; + Entry entry = slots[slot]; + for (int i = 0; i < cnt; i++) { + Entry next = entry.next; + if (entry.iter == iter) { + linkOut(entry); + out.add(entry.payload); + } + entry = next; + } + } + + private synchronized void scheduleAt(Entry entry, long expireAtMillis) { + if (entry.next != null) { + linkOut(entry); + } + long delayMillis = expireAtMillis - nextTick; + if (delayMillis < 0) { + entry.slot = NUM_SLOTS; + entry.iter = 0; + } else { + long ticks = 1 + (int)((delayMillis + MILLIS_PER_SLOT / 2) / MILLIS_PER_SLOT); + entry.slot = (int)((ticks + currSlot) & SLOT_MASK); + entry.iter = currIter + (int)((ticks + currSlot) >> ITER_SHIFT); + } + linkIn(entry); + } + + private synchronized void unschedule(Entry entry) { + if (entry.next != null) { + linkOut(entry); + } + } + + private void linkIn(Entry entry) { + Entry head = slots[entry.slot]; + if (head == null) { + entry.next = entry; + entry.prev = entry; + slots[entry.slot] = entry; + } else { + entry.next = head; + entry.prev = head.prev; + head.prev.next = entry; + head.prev = entry; + } + ++counts[entry.slot]; + } + + private void linkOut(Entry entry) { + Entry head = slots[entry.slot]; + if (entry.next == entry) { + slots[entry.slot] = null; + } else { + entry.prev.next = entry.next; + entry.next.prev = entry.prev; + if (head == entry) { + slots[entry.slot] = entry.next; + } + } + entry.next = null; + entry.prev = null; + --counts[entry.slot]; + } + + public class Entry { + + private final Object payload; + private int slot; + private int iter; + private Entry next; + private Entry prev; + + private Entry(Object payload) { + this.payload = payload; + } + + public void scheduleAt(long expireAtMillis) { + ScheduledQueue.this.scheduleAt(this, expireAtMillis); + } + + public void unschedule() { + ScheduledQueue.this.unschedule(this); + } + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java new file mode 100644 index 00000000000..371ab52f26b --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.yahoo.jdisc.Timer; + +/** + * A timer which returns the System time + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SystemTimer implements Timer { + + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } +} diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java new file mode 100644 index 00000000000..8e0c624b348 --- /dev/null +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java @@ -0,0 +1,244 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.core; + +import com.google.inject.Inject; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.TimeoutManager; +import com.yahoo.jdisc.Timer; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TimeoutManagerImpl { + + private static final ContentChannel IGNORED_CONTENT = new IgnoredContent(); + private static final Logger log = Logger.getLogger(TimeoutManagerImpl.class.getName()); + private final ScheduledQueue schedules[] = new ScheduledQueue[Runtime.getRuntime().availableProcessors()]; + private final Thread thread; + private final Timer timer; + private volatile int nextScheduler = 0; + private volatile int queueSize = 0; + private volatile boolean done = false; + + @Inject + public TimeoutManagerImpl(ThreadFactory factory, Timer timer) { + this.thread = factory.newThread(new ManagerTask()); + this.thread.setName(getClass().getName()); + this.timer = timer; + + long now = timer.currentTimeMillis(); + for (int i = 0; i < schedules.length; ++i) { + schedules[i] = new ScheduledQueue(now); + } + } + + public void start() { + thread.start(); + } + + public void shutdown() { + done = true; + } + + public RequestHandler manageHandler(RequestHandler handler) { + return new ManagedRequestHandler(handler); + } + + int queueSize() { + return queueSize; // unstable snapshot, only for test purposes + } + + Timer timer() { + return timer; + } + + void checkTasks(long currentTimeMillis) { + Queue<Object> queue = new LinkedList<>(); + for (ScheduledQueue schedule : schedules) { + schedule.drainTo(currentTimeMillis, queue); + } + while (!queue.isEmpty()) { + TimeoutHandler timeoutHandler = (TimeoutHandler)queue.poll(); + invokeTimeout(timeoutHandler.requestHandler, timeoutHandler.request, timeoutHandler); + } + } + + private void invokeTimeout(RequestHandler requestHandler, Request request, ResponseHandler responseHandler) { + try { + requestHandler.handleTimeout(request, responseHandler); + } catch (RuntimeException e) { + log.log(Level.WARNING, "Ignoring exception thrown by " + requestHandler.getClass().getName() + + " in timeout manager.", e); + } + if (Thread.currentThread().isInterrupted()) { + log.log(Level.WARNING, "Ignoring interrupt signal from " + requestHandler.getClass().getName() + + " in timeout manager."); + Thread.interrupted(); + } + } + + private class ManagerTask implements Runnable { + + @Override + public void run() { + while (!done) { + try { + Thread.sleep(ScheduledQueue.MILLIS_PER_SLOT); + } catch (InterruptedException e) { + log.log(Level.WARNING, "Ignoring interrupt signal in timeout manager.", e); + } + checkTasks(timer.currentTimeMillis()); + } + } + } + + private class ManagedRequestHandler implements RequestHandler { + + final RequestHandler delegate; + + ManagedRequestHandler(RequestHandler delegate) { + this.delegate = delegate; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { + TimeoutHandler timeoutHandler = new TimeoutHandler(request, delegate, responseHandler); + request.setTimeoutManager(timeoutHandler); + try { + return delegate.handleRequest(request, timeoutHandler); + } catch (Throwable throwable) { + //This is only needed when this method is invoked outside of Request.connect, + //and that seems to be the case for jetty right now. + //To prevent this from being called outside Request.connect, + //manageHandler() and com.yahoo.jdisc.Container.resolveHandler() must also be made non-public. + // + //The underlying framework will handle the request, + //the application code is no longer responsible for calling responseHandler.handleResponse. + timeoutHandler.unscheduleTimeout(); + throw throwable; + } + } + + @Override + public void handleTimeout(Request request, ResponseHandler responseHandler) { + delegate.handleTimeout(request, responseHandler); + } + + @Override + public ResourceReference refer() { + return delegate.refer(); + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public String toString() { + return delegate.toString(); + } + } + + private class TimeoutHandler implements ResponseHandler, TimeoutManager { + + final ResponseHandler responseHandler; + final RequestHandler requestHandler; + final Request request; + ScheduledQueue.Entry timeoutQueueEntry = null; + boolean responded = false; + + TimeoutHandler(Request request, RequestHandler requestHandler, ResponseHandler responseHandler) { + this.request = request; + this.requestHandler = requestHandler; + this.responseHandler = responseHandler; + } + + @Override + public synchronized void scheduleTimeout(Request request) { + if (responded) { + return; + } + if (timeoutQueueEntry == null) { + timeoutQueueEntry = schedules[(++nextScheduler & 0xffff) % schedules.length].newEntry(this); + } + timeoutQueueEntry.scheduleAt(request.creationTime(TimeUnit.MILLISECONDS) + request.getTimeout(TimeUnit.MILLISECONDS)); + ++queueSize; + } + + synchronized void unscheduleTimeout() { + if (!responded && timeoutQueueEntry != null) { + timeoutQueueEntry.unschedule(); + //guard against unscheduling from ManagedRequestHandler.handleRequest catch block + //followed by unscheduling in another thread from TimeoutHandler.handleResponse + timeoutQueueEntry = null; + } + --queueSize; + } + + @Override + public void unscheduleTimeout(Request request) { + unscheduleTimeout(); + } + + @Override + public ContentChannel handleResponse(Response response) { + synchronized (this) { + unscheduleTimeout(); + if (responded) { + return IGNORED_CONTENT; + } + responded = true; + } + return responseHandler.handleResponse(response); + } + + @Override + public String toString() { + return responseHandler.toString(); + } + } + + private static class IgnoredContent implements ContentChannel { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + if (handler == null) { + return; + } + try { + handler.completed(); + } catch (RuntimeException e) { + log.log(Level.WARNING, "Ignoring exception thrown by " + handler.getClass().getName() + + " in timeout manager.", e); + } + } + + @Override + public void close(CompletionHandler handler) { + if (handler == null) { + return; + } + try { + handler.completed(); + } catch (RuntimeException e) { + log.log(Level.WARNING, "Ignoring exception thrown by " + handler.getClass().getName() + + " in timeout manager.", e); + } + } + } +} |