com.spotify.heroic.HeroicCore.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.heroic.HeroicCore.java

Source

/*
 * Copyright (c) 2015 Spotify AB.
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.spotify.heroic;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Optional.empty;
import static java.util.Optional.of;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.spotify.heroic.analytics.AnalyticsComponent;
import com.spotify.heroic.cache.CacheComponent;
import com.spotify.heroic.cluster.CoreClusterComponent;
import com.spotify.heroic.cluster.DaggerCoreClusterComponent;
import com.spotify.heroic.common.Duration;
import com.spotify.heroic.common.Optionals;
import com.spotify.heroic.common.TypeNameMixin;
import com.spotify.heroic.consumer.ConsumersComponent;
import com.spotify.heroic.consumer.CoreConsumersModule;
import com.spotify.heroic.consumer.DaggerCoreConsumersComponent;
import com.spotify.heroic.dagger.CoreComponent;
import com.spotify.heroic.dagger.CoreEarlyComponent;
import com.spotify.heroic.dagger.CoreLoadingComponent;
import com.spotify.heroic.dagger.CorePrimaryComponent;
import com.spotify.heroic.dagger.DaggerCoreComponent;
import com.spotify.heroic.dagger.DaggerCoreEarlyComponent;
import com.spotify.heroic.dagger.DaggerCoreLoadingComponent;
import com.spotify.heroic.dagger.DaggerCorePrimaryComponent;
import com.spotify.heroic.dagger.DaggerStartupPingerComponent;
import com.spotify.heroic.dagger.EarlyModule;
import com.spotify.heroic.dagger.LoadingComponent;
import com.spotify.heroic.dagger.LoadingModule;
import com.spotify.heroic.dagger.PrimaryModule;
import com.spotify.heroic.dagger.StartupPingerComponent;
import com.spotify.heroic.dagger.StartupPingerModule;
import com.spotify.heroic.generator.GeneratorComponent;
import com.spotify.heroic.http.DaggerHttpServerComponent;
import com.spotify.heroic.http.HttpServer;
import com.spotify.heroic.http.HttpServerComponent;
import com.spotify.heroic.http.HttpServerModule;
import com.spotify.heroic.ingestion.IngestionComponent;
import com.spotify.heroic.jetty.JettyConnectionFactory;
import com.spotify.heroic.lifecycle.CoreLifeCycleRegistry;
import com.spotify.heroic.lifecycle.LifeCycle;
import com.spotify.heroic.lifecycle.LifeCycleHook;
import com.spotify.heroic.lifecycle.LifeCycleNamedHook;
import com.spotify.heroic.metadata.CoreMetadataComponent;
import com.spotify.heroic.metadata.DaggerCoreMetadataComponent;
import com.spotify.heroic.metric.CoreMetricComponent;
import com.spotify.heroic.metric.DaggerCoreMetricComponent;
import com.spotify.heroic.querylogging.QueryLoggingComponent;
import com.spotify.heroic.shell.DaggerShellServerComponent;
import com.spotify.heroic.shell.ShellServerComponent;
import com.spotify.heroic.shell.ShellServerModule;
import com.spotify.heroic.statistics.HeroicReporter;
import com.spotify.heroic.statistics.StatisticsComponent;
import com.spotify.heroic.suggest.CoreSuggestComponent;
import com.spotify.heroic.suggest.DaggerCoreSuggestComponent;
import eu.toolchain.async.AsyncFramework;
import eu.toolchain.async.AsyncFuture;
import eu.toolchain.async.ResolvableFuture;
import java.io.InputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;

/**
 * Configure and bootstrap a Heroic application.
 * <p>
 * All public methods are thread-safe.
 * <p>
 * All fields are non-null.
 *
 * @author udoprog
 */
@Slf4j
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class HeroicCore implements HeroicConfiguration {
    static final String DEFAULT_HOST = "0.0.0.0";
    static final int DEFAULT_PORT = 8080;

    static final boolean DEFAULT_SETUP_SERVICE = true;
    static final boolean DEFAULT_ONESHOT = false;
    static final boolean DEFAULT_DISABLE_BACKENDS = false;
    static final boolean DEFAULT_SETUP_SHELL_SERVER = true;

    static final UncaughtExceptionHandler uncaughtExceptionHandler = (Thread t, Throwable e) -> {
        try {
            System.err.println(String.format("Uncaught exception caught in thread %s, exiting...", t));
            e.printStackTrace(System.err);
        } finally {
            System.exit(1);
        }
    };

    /**
     * Built-in modules that should always be loaded.
     */
    // @formatter:off
    private static final HeroicModule[] BUILTIN_MODULES = new HeroicModule[] {
            new com.spotify.heroic.aggregation.Module(), new com.spotify.heroic.http.Module(),
            new com.spotify.heroic.jetty.Module(), new com.spotify.heroic.ws.Module(),
            new com.spotify.heroic.cache.Module(), new com.spotify.heroic.generator.Module() };
    // @formatter:on

    private final Optional<String> id;
    private final Optional<String> host;
    private final Optional<Integer> port;
    private final Optional<Supplier<InputStream>> configStream;
    private final Optional<Path> configPath;
    private final Optional<URI> startupPing;
    private final Optional<String> startupId;

    /**
     * Additional dynamic parameters to pass into the configuration of a profile. These are
     * typically extracted from the commandline, or a properties file.
     */
    private final ExtraParameters params;

    /* flags */
    private final boolean setupService;
    private final boolean oneshot;
    private final boolean disableBackends;
    private final boolean setupShellServer;

    /* extensions */
    private final List<HeroicModule> modules;
    private final List<HeroicProfile> profiles;
    private final List<HeroicBootstrap> early;
    private final List<HeroicBootstrap> late;

    private final List<HeroicConfig.Builder> configFragments;

    private final Optional<ExecutorService> executor;

    @Override
    public boolean isDisableLocal() {
        return disableBackends;
    }

    @Override
    public boolean isOneshot() {
        return oneshot;
    }

    /**
     * Start the Heroic core, step by step
     * <p>
     * <p>
     * It sets up the early injector which is responsible for loading all the necessary components
     * to parse a configuration file.
     * <p>
     * <p>
     * Load all the external modules, which are configured in {@link #modules}.
     * <p>
     * <p>
     * Load and build the configuration using the early injector
     * <p>
     * <p>
     * Setup the primary injector which will provide the dependencies to the entire application
     * <p>
     * <p>
     * Run all bootstraps that are configured in {@link #late}
     * <p>
     * <p>
     * Start all the external modules. {@link #startLifeCycles}
     */
    public HeroicCoreInstance newInstance() throws Exception {
        final CoreLoadingComponent loading = loadingInjector();

        loadModules(loading);

        final HeroicConfig config = config(loading);

        final CoreEarlyComponent early = earlyInjector(loading, config);
        runBootstrappers(early, this.early);

        // Initialize the instance injector with access to early components.
        final AtomicReference<CoreComponent> injector = new AtomicReference<>();

        final HeroicCoreInstance instance = new Instance(loading.async(), injector, early, config, this.late);

        final CoreComponent primary = primaryInjector(early, config, instance);

        primary.loadingLifeCycle().install();

        primary.internalLifeCycleRegistry().scoped("startup future").start(() -> {
            ((CoreHeroicContext) primary.context()).resolveCoreFuture();
            return primary.async().resolved(null);
        });

        // Update the instance injector, giving dynamic components initialized after this point
        // access to the primary
        // injector.
        injector.set(primary);

        return instance;
    }

    /**
     * This method basically goes through the list of bootstrappers registered by modules and runs
     * them.
     *
     * @param early Injector to inject boostrappers using.
     * @param bootstrappers Bootstraps to run.
     */
    static void runBootstrappers(final CoreEarlyComponent early, final List<HeroicBootstrap> bootstrappers)
            throws Exception {
        for (final HeroicBootstrap bootstrap : bootstrappers) {
            try {
                bootstrap.run(early);
            } catch (Exception e) {
                throw new Exception("Failed to run bootstrapper " + bootstrap, e);
            }
        }
    }

    /**
     * Setup early injector, which is responsible for sufficiently providing dependencies to runtime
     * components.
     */
    private CoreLoadingComponent loadingInjector() {
        log.info("Building Loading Injector");

        final ExecutorService executor;
        final boolean managedExecutor;

        if (this.executor.isPresent()) {
            executor = this.executor.get();
            managedExecutor = false;
        } else {
            executor = setupExecutor(Runtime.getRuntime().availableProcessors() * 2);
            managedExecutor = true;
        }

        return DaggerCoreLoadingComponent.builder()
                .loadingModule(new LoadingModule(executor, managedExecutor, this, params)).build();
    }

    private CoreEarlyComponent earlyInjector(final CoreLoadingComponent loading, final HeroicConfig config) {
        final Optional<String> id = Optionals.firstPresent(this.id, config.getId());

        return DaggerCoreEarlyComponent.builder().coreLoadingComponent(loading)
                .earlyModule(new EarlyModule(config, id)).build();
    }

    /**
     * Setup a fixed thread pool executor that correctly handles unhandled exceptions.
     *
     * @param threads Number of threads to configure.
     */
    private ExecutorService setupExecutor(final int threads) {
        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
                new ThreadFactoryBuilder().setNameFormat("heroic-core-%d")
                        .setUncaughtExceptionHandler(uncaughtExceptionHandler).build()) {
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);

                if (t == null && (r instanceof Future<?>)) {
                    try {
                        ((Future<?>) r).get();
                    } catch (CancellationException e) {
                        t = e;
                    } catch (ExecutionException e) {
                        t = e.getCause();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }

                if (t != null) {
                    if (log.isErrorEnabled()) {
                        log.error("Unhandled exception caught in core executor", t);
                        log.error("Exiting (code=2)");
                    } else {
                        System.err.println("Unhandled exception caught in core executor");
                        System.err.println("Exiting (code=2)");
                        t.printStackTrace(System.err);
                    }

                    System.exit(2);
                }
            }
        };
    }

    /**
     * Setup primary injector, which will provide dependencies to the entire application.
     *
     * @param config The loaded configuration file.
     * @param early The early injector, which will act as a parent to the primary injector to bridge
     * all it's provided components.
     * @return The primary guice injector.
     */
    private CoreComponent primaryInjector(final CoreEarlyComponent early, final HeroicConfig config,
            final HeroicCoreInstance instance) {
        log.info("Building Primary Injector");
        final List<LifeCycle> life = new ArrayList<>();

        final StatisticsComponent statistics = config.getStatistics().module(early);
        life.add(statistics.life());

        final HeroicReporter reporter = statistics.reporter();

        // Register root components.
        final CorePrimaryComponent primary = DaggerCorePrimaryComponent.builder().coreEarlyComponent(early)
                .primaryModule(
                        new PrimaryModule(instance, config.getFeatures(), reporter, config.getConditionalFeature()))
                .build();

        final QueryLoggingComponent queryLogging = config.getQueryLogging().component(primary);

        final Optional<HttpServer> server = setupServer(config, life, primary);

        final CacheComponent cache = config.getCache().module(primary);
        life.add(cache.cacheLife());

        final AnalyticsComponent analytics = config.getAnalytics().module(primary);
        life.add(analytics.analyticsLife());

        final CoreMetadataComponent metadata = DaggerCoreMetadataComponent.builder().primaryComponent(primary)
                .metadataManagerModule(config.getMetadata()).build();

        life.add(metadata.metadataLife());

        final CoreSuggestComponent suggest = DaggerCoreSuggestComponent.builder().primaryComponent(primary)
                .suggestManagerModule(config.getSuggest()).build();

        life.add(suggest.suggestLife());

        final CoreMetricComponent metric = DaggerCoreMetricComponent.builder().corePrimaryComponent(primary)
                .metadataComponent(metadata).analyticsComponent(analytics).metricManagerModule(config.getMetric())
                .queryLoggingComponent(queryLogging).build();

        life.add(metric.metricLife());

        final IngestionComponent ingestion = config.getIngestion().module(primary, suggest, metadata, metric);
        life.add(ingestion.ingestionLife());

        buildShellServer(config).ifPresent(shellServerModule -> {
            final ShellServerComponent shellServer = DaggerShellServerComponent.builder().primaryComponent(primary)
                    .shellServerModule(shellServerModule).build();

            life.add(shellServer.shellServerLife());
        });

        final CoreClusterComponent cluster = DaggerCoreClusterComponent.builder().primaryComponent(primary)
                .metricComponent(metric).metadataComponent(metadata).suggestComponent(suggest)
                .clusterManagerModule(config.getCluster())
                .clusterDiscoveryComponent(config.getCluster().getDiscovery().module(primary)).build();

        life.add(cluster.clusterLife());

        if (startupPing.isPresent() && startupId.isPresent()) {
            final StartupPingerComponent pinger = DaggerStartupPingerComponent.builder()
                    .corePrimaryComponent(primary).clusterComponent(cluster)
                    .startupPingerModule(new StartupPingerModule(startupPing.get(), startupId.get(), server))
                    .build();

            life.add(pinger.startupPingerLife());
        }

        final ConsumersComponent consumer = DaggerCoreConsumersComponent.builder()
                .coreConsumersModule(new CoreConsumersModule(reporter, config.getConsumers(), primary, ingestion))
                .corePrimaryComponent(primary).build();
        life.add(consumer.consumersLife());

        final QueryComponent query = DaggerCoreQueryComponent.builder()
                .queryModule(new QueryModule(config.getMetric().getGroupLimit(),
                        config.getMetric().getSmallQueryThreshold()))
                .corePrimaryComponent(primary).clusterComponent(cluster).cacheComponent(cache)
                .queryLoggingComponent(queryLogging).build();
        life.add(query.queryLife());

        final GeneratorComponent generator = config.getGenerator().module(primary);
        life.add(generator.generatorLife());

        // install all lifecycles
        final LifeCycle combined = LifeCycle.combined(life);

        combined.install();

        return DaggerCoreComponent.builder().primaryComponent(primary).analyticsComponent(analytics)
                .consumersComponent(consumer).metricComponent(metric).metadataComponent(metadata)
                .suggestComponent(suggest).queryComponent(query).queryLoggingComponent(queryLogging)
                .ingestionComponent(ingestion).clusterComponent(cluster).generatorComponent(generator).build();
    }

    private Optional<HttpServer> setupServer(final HeroicConfig config, final List<LifeCycle> life,
            final CorePrimaryComponent primary) {
        if (!setupService) {
            return Optional.empty();
        }

        final InetSocketAddress bindAddress = setupBindAddress(config);

        final HttpServerComponent serverComponent = DaggerHttpServerComponent.builder().primaryComponent(primary)
                .httpServerModule(new HttpServerModule(bindAddress, config.isEnableCors(),
                        config.getCorsAllowOrigin(), config.getConnectors()))
                .build();

        // Trigger life cycle registration
        life.add(serverComponent.life());

        return Optional.of(serverComponent.server());
    }

    private Optional<ShellServerModule> buildShellServer(final HeroicConfig config) {
        final Optional<ShellServerModule> shellServer = config.getShellServer();

        // a shell server is configured.
        if (shellServer.isPresent()) {
            return shellServer;
        }

        // must have a shell server
        if (setupShellServer) {
            return of(ShellServerModule.builder().build());
        }

        return empty();
    }

    private InetSocketAddress setupBindAddress(HeroicConfig config) {
        final String host = Optionals.pickOptional(config.getHost(), this.host).orElse(DEFAULT_HOST);
        final int port = Optionals.pickOptional(config.getPort(), this.port).orElse(DEFAULT_PORT);
        return new InetSocketAddress(host, port);
    }

    static void startLifeCycles(CoreLifeCycleRegistry registry, CoreComponent primary, HeroicConfig config)
            throws Exception {
        if (!awaitLifeCycles("start", primary, config.getStartTimeout(), registry.starters())) {
            throw new Exception("Failed to start all life cycles");
        }
    }

    static void stopLifeCycles(CoreLifeCycleRegistry registry, CoreComponent primary, HeroicConfig config)
            throws Exception {
        if (!awaitLifeCycles("stop", primary, config.getStopTimeout(), registry.stoppers())) {
            log.warn("Failed to stop all life cycles");
        }
    }

    static boolean awaitLifeCycles(final String op, final CoreComponent primary, final Duration await,
            final List<LifeCycleNamedHook<AsyncFuture<Void>>> hooks)
            throws InterruptedException, ExecutionException {
        log.info("[{}] {} lifecycle(s)...", op, hooks.size());

        final AsyncFramework async = primary.async();

        final List<AsyncFuture<Void>> futures = new ArrayList<>();
        final List<Pair<AsyncFuture<Void>, LifeCycleHook<AsyncFuture<Void>>>> pairs = new ArrayList<>();

        for (final LifeCycleNamedHook<AsyncFuture<Void>> hook : hooks) {
            log.trace("[{}] {}", op, hook.id());

            final AsyncFuture<Void> future;

            try {
                future = hook.get();
            } catch (Exception e) {
                futures.add(async.failed(e));
                break;
            }

            if (log.isTraceEnabled()) {
                final Stopwatch w = Stopwatch.createStarted();

                future.onFinished(() -> {
                    log.trace("[{}] {}, took {}us", op, hook.id(), w.elapsed(TimeUnit.MICROSECONDS));
                });
            }

            futures.add(future);
            pairs.add(Pair.of(future, hook));
        }

        try {
            async.collect(futures).get(await.getDuration(), await.getUnit());
        } catch (final TimeoutException e) {
            log.error("Operation timed out");

            for (final Pair<AsyncFuture<Void>, LifeCycleHook<AsyncFuture<Void>>> pair : pairs) {
                if (!pair.getLeft().isDone()) {
                    log.error("{}: did not finish in time: {}", op, pair.getRight());
                }
            }

            return false;
        }

        log.info("[{}] {} lifecycle(s) done", op, hooks.size());
        return true;
    }

    private HeroicConfig config(LoadingComponent loading) throws Exception {
        HeroicConfig.Builder builder = HeroicConfig.builder();

        for (final HeroicProfile profile : profiles) {
            log.info("Loading profile '{}' (params: {})", profile.description(), params);
            final ExtraParameters p = profile.scope().map(params::scope).orElse(params);
            builder = builder.merge(profile.build(p));
        }

        for (final HeroicConfig.Builder fragment : configFragments) {
            builder = builder.merge(fragment);
        }

        final ObjectMapper mapper = loading.configMapper().copy();

        // TODO: figure out where to put this
        mapper.addMixIn(JettyConnectionFactory.Builder.class, TypeNameMixin.class);

        if (configPath.isPresent()) {
            builder = HeroicConfig.loadConfig(mapper, configPath.get()).map(builder::merge).orElse(builder);
        }

        if (configStream.isPresent()) {
            builder = HeroicConfig.loadConfigStream(mapper, configStream.get().get()).map(builder::merge)
                    .orElse(builder);
        }

        return builder.build();
    }

    /**
     * Load modules from the specified modules configuration file and wire up those components with
     * early injection.
     *
     * @param loading Injector to wire up modules using.
     */
    private void loadModules(final CoreLoadingComponent loading) {
        final List<HeroicModule> modules = new ArrayList<>();

        for (final HeroicModule builtin : BUILTIN_MODULES) {
            modules.add(builtin);
        }

        modules.addAll(this.modules);

        for (final HeroicModule module : modules) {
            // inject members of an entry point and run them.
            module.setup(loading).run();
        }

        log.info("Loaded {} module(s)", modules.size());
    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder {
        private Optional<String> id = empty();
        private Optional<String> host = empty();
        private Optional<Integer> port = empty();
        private Optional<Supplier<InputStream>> configStream = empty();
        private Optional<Path> configPath = empty();
        private Optional<ExtraParameters> params = empty();
        private Optional<HeroicReporter> reporter = empty();
        private Optional<URI> startupPing = empty();
        private Optional<String> startupId = empty();

        /* flags */
        private Optional<Boolean> setupService = empty();
        private Optional<Boolean> oneshot = empty();
        private Optional<Boolean> disableBackends = empty();
        private Optional<Boolean> setupShellServer = empty();

        /* extensions */
        private final ImmutableList.Builder<HeroicModule> modules = ImmutableList.builder();
        private final ImmutableList.Builder<HeroicProfile> profiles = ImmutableList.builder();
        private final ImmutableList.Builder<HeroicBootstrap> early = ImmutableList.builder();
        private final ImmutableList.Builder<HeroicBootstrap> late = ImmutableList.builder();

        /* configuration fragments */
        private final ImmutableList.Builder<HeroicConfig.Builder> configFragments = ImmutableList.builder();

        private Optional<ExecutorService> executor = empty();

        public Builder executor(final ExecutorService executor) {
            this.executor = of(executor);
            return this;
        }

        /**
         * Register a specific ID for this heroic instance.
         *
         * @param id Id of the instance.
         * @return This builder.
         */
        public Builder id(final String id) {
            this.id = of(id);
            return this;
        }

        /**
         * If a shell server is not configured, setup the default shell server.
         */
        public Builder setupShellServer(boolean setupShellServer) {
            this.setupShellServer = of(setupShellServer);
            return this;
        }

        /**
         * Port to bind service to.
         */
        public Builder port(final int port) {
            this.port = of(port);
            return this;
        }

        /**
         * Host to bind service to.
         */
        public Builder host(final String host) {
            checkNotNull(host, "host");
            this.host = of(host);
            return this;
        }

        public Builder configStream(final Supplier<InputStream> configStream) {
            checkNotNull(configStream, "configStream");
            this.configStream = of(configStream);
            return this;
        }

        public Builder configPath(final String configPath) {
            checkNotNull(configPath, "configPath");
            return configPath(Paths.get(configPath));
        }

        public Builder configPath(final Path configPath) {
            checkNotNull(configPath, "configPath");

            if (!Files.isReadable(configPath)) {
                throw new IllegalArgumentException("Configuration is not readable: " + configPath.toAbsolutePath());
            }

            this.configPath = of(configPath);
            return this;
        }

        /**
         * Programmatically add a configuration fragment.
         *
         * @param config Configuration fragment to add.
         * @return This builder.
         */
        public Builder configFragment(final HeroicConfig.Builder config) {
            checkNotNull(config, "config");
            this.configFragments.add(config);
            return this;
        }

        public Builder startupPing(final String startupPing) {
            checkNotNull(startupPing, "startupPing");
            this.startupPing = of(URI.create(startupPing));
            return this;
        }

        public Builder startupId(final String startupId) {
            checkNotNull(startupId, "startupId");
            this.startupId = of(startupId);
            return this;
        }

        public Builder parameters(final ExtraParameters params) {
            checkNotNull(params, "params");
            this.params = of(params);
            return this;
        }

        public Builder reporter(final HeroicReporter reporter) {
            checkNotNull(reporter, "reporter");
            this.reporter = of(reporter);
            return this;
        }

        /**
         * Configure setup of the server component of heroic or not.
         */
        public Builder setupService(final boolean setupService) {
            this.setupService = of(setupService);
            return this;
        }

        /**
         * Do not perform any scheduled tasks, only perform then once during startup.
         */
        public Builder oneshot(final boolean oneshot) {
            this.oneshot = of(oneshot);
            return this;
        }

        /**
         * Disable local backends.
         */
        public Builder disableBackends(final boolean disableBackends) {
            this.disableBackends = of(disableBackends);
            return this;
        }

        public Builder modules(final List<HeroicModule> modules) {
            checkNotNull(modules, "modules");
            this.modules.addAll(modules);
            return this;
        }

        public Builder module(final HeroicModule module) {
            checkNotNull(module, "module");
            this.modules.add(module);
            return this;
        }

        public Builder profiles(final List<HeroicProfile> profiles) {
            checkNotNull(profiles, "profiles");
            this.profiles.addAll(profiles);
            return this;
        }

        public Builder profile(final HeroicProfile profile) {
            checkNotNull(profile, "profile");
            this.profiles.add(profile);
            return this;
        }

        public Builder earlyBootstrap(final HeroicBootstrap bootstrap) {
            checkNotNull(bootstrap, "bootstrap");
            this.early.add(bootstrap);
            return this;
        }

        public Builder bootstrappers(final List<HeroicBootstrap> bootstrappers) {
            checkNotNull(bootstrappers, "bootstrappers");
            this.late.addAll(bootstrappers);
            return this;
        }

        public Builder bootstrap(final HeroicBootstrap bootstrap) {
            checkNotNull(bootstrap, "bootstrap");
            this.late.add(bootstrap);
            return this;
        }

        public HeroicCore build() {
            // @formatter:off
            return new HeroicCore(id, host, port, configStream, configPath, startupPing, startupId,

                    params.orElseGet(ExtraParameters::empty),

                    setupService.orElse(DEFAULT_SETUP_SERVICE), oneshot.orElse(DEFAULT_ONESHOT),
                    disableBackends.orElse(DEFAULT_DISABLE_BACKENDS),
                    setupShellServer.orElse(DEFAULT_SETUP_SHELL_SERVER),

                    modules.build(), profiles.build(), early.build(), late.build(),

                    configFragments.build(),

                    executor);
            // @formatter:on
        }
    }

    public static HeroicModule[] builtinModules() {
        return BUILTIN_MODULES;
    }

    public static class Instance implements HeroicCoreInstance {
        private final AtomicReference<CoreComponent> coreInjector;
        private final CoreEarlyComponent early;
        private final HeroicConfig config;
        private final List<HeroicBootstrap> late;

        private final ResolvableFuture<Void> start;
        private final ResolvableFuture<Void> stop;

        private final AsyncFuture<Void> started;
        private final AsyncFuture<Void> stopped;

        public Instance(final AsyncFramework async, final AtomicReference<CoreComponent> coreInjector,
                final CoreEarlyComponent early, final HeroicConfig config, final List<HeroicBootstrap> late) {
            this.coreInjector = coreInjector;
            this.early = early;
            this.config = config;
            this.late = late;

            this.start = async.future();
            this.stop = async.future();

            this.started = start.directTransform(n -> {
                final CoreComponent core = coreInjector.get();
                assert core != null;

                final CoreLifeCycleRegistry coreRegistry = (CoreLifeCycleRegistry) core.lifeCycleRegistry();

                runBootstrappers(early, late);
                startLifeCycles(coreRegistry, core, config);

                final CoreLifeCycleRegistry internalRegistry = (CoreLifeCycleRegistry) core
                        .internalLifeCycleRegistry();

                startLifeCycles(internalRegistry, core, config);

                log.info("Startup finished, hello!");
                return null;
            });

            this.stopped = async.collect(ImmutableList.of(started, stop)).lazyTransform(n -> {
                return async.call(() -> {
                    final CoreComponent core = coreInjector.get();
                    assert core != null;

                    final CoreLifeCycleRegistry coreRegistry = (CoreLifeCycleRegistry) core.lifeCycleRegistry();

                    core.stopSignal().run();

                    log.info("Shutting down Heroic");

                    try {
                        stopLifeCycles(coreRegistry, core, config);
                    } catch (Exception e) {
                        log.error("Failed to stop all lifecycles, continuing anyway...", e);
                    }

                    log.info("Stopping internal life cycle");

                    final CoreLifeCycleRegistry internalRegistry = (CoreLifeCycleRegistry) core
                            .internalLifeCycleRegistry();

                    try {
                        stopLifeCycles(internalRegistry, core, config);
                    } catch (Exception e) {
                        log.error("Failed to stop all internal lifecycles, continuing anyway...", e);
                    }

                    log.info("Done shutting down, bye bye!");
                    return null;
                }, Executors.newSingleThreadExecutor());
            });
        }

        /**
         * Inject fields to the provided injector using the primary injector.
         *
         * @param injector Injector to use.
         */
        @Override
        public <T> T inject(Function<CoreComponent, T> injector) {
            final CoreComponent c = coreInjector.get();

            if (c == null) {
                throw new IllegalStateException(
                        "Injection attempted before instance has been fully initialize, use "
                                + "LifeCycleRegistry#start(..) to register a hook instead");
            }

            return injector.apply(c);
        }

        @Override
        public AsyncFuture<Void> start() {
            this.start.resolve(null);
            return this.started;
        }

        @Override
        public AsyncFuture<Void> shutdown() {
            this.stop.resolve(null);
            return this.stopped;
        }

        @Override
        public AsyncFuture<Void> join() {
            return this.stopped;
        }
    }
}