summaryrefslogtreecommitdiffstats
path: root/jdisc_core/src/main/java/com/yahoo/jdisc/core
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jdisc_core/src/main/java/com/yahoo/jdisc/core
Publish
Diffstat (limited to 'jdisc_core/src/main/java/com/yahoo/jdisc/core')
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ActiveContainer.java137
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationConfigModule.java64
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationEnvironmentModule.java37
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ApplicationLoader.java261
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java104
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapLoader.java16
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/BundleLocationResolver.java70
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogFormatter.java199
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogListener.java109
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ConsoleLogManager.java54
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerSnapshot.java112
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ContainerTermination.java51
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/DefaultBindingSelector.java18
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ExportPackages.java98
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixFramework.java175
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/FelixParams.java50
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogHandler.java164
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogManager.java102
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/OsgiLogService.java60
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/ScheduledQueue.java136
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/SystemTimer.java17
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/TimeoutManagerImpl.java244
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);
+ }
+ }
+ }
+}