com.spotify.ffwd.AgentCore.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.ffwd.AgentCore.java

Source

// $LICENSE
/**
 * Copyright 2013-2014 Spotify AB. All rights reserved.
 *
 * The contents of this file are licensed under the Apache License, Version 2.0 (the "License"); you may not use this
 * file except in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 **/
package com.spotify.ffwd;

import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
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.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import lombok.extern.slf4j.Slf4j;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.inject.name.Names;
import com.spotify.ffwd.input.InputManager;
import com.spotify.ffwd.input.InputPlugin;
import com.spotify.ffwd.module.FastForwardModule;
import com.spotify.ffwd.module.FasterXmlSubTypeMixIn;
import com.spotify.ffwd.module.PluginContext;
import com.spotify.ffwd.module.PluginContextImpl;
import com.spotify.ffwd.output.OutputManager;
import com.spotify.ffwd.output.OutputPlugin;
import com.spotify.ffwd.protocol.ProtocolClients;
import com.spotify.ffwd.protocol.ProtocolClientsImpl;
import com.spotify.ffwd.protocol.ProtocolServers;
import com.spotify.ffwd.protocol.ProtocolServersImpl;
import com.spotify.ffwd.serializer.Serializer;
import com.spotify.ffwd.serializer.ToStringSerializer;

import eu.toolchain.async.AsyncCaller;
import eu.toolchain.async.AsyncFramework;
import eu.toolchain.async.AsyncFuture;
import eu.toolchain.async.TinyAsync;
import eu.toolchain.async.caller.DirectAsyncCaller;

@Slf4j
public class AgentCore {
    private final List<Class<? extends FastForwardModule>> modules;
    private final Path config;

    private AgentCore(final List<Class<? extends FastForwardModule>> modules, Path config) {
        this.modules = modules;
        this.config = config;
    }

    public void run() throws Exception {
        final Injector early = setupEarlyInjector();
        final AgentConfig config = readConfig(early);
        final Injector primary = setupPrimaryInjector(early, config);

        start(primary);
        log.info("Started!");

        waitUntilStopped(primary);
        log.info("Stopped, Bye Bye!");
    }

    private void waitUntilStopped(final Injector primary) throws InterruptedException {
        final CountDownLatch shutdown = new CountDownLatch(1);
        Runtime.getRuntime().addShutdownHook(setupShutdownHook(primary, shutdown));
        shutdown.await();
    }

    private Thread setupShutdownHook(final Injector primary, final CountDownLatch shutdown) {
        final Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    AgentCore.this.stop(primary);
                } catch (Exception e) {
                    log.error("AgentCore#stop(Injector) failed", e);
                }

                shutdown.countDown();
            }
        };

        thread.setName("ffwd-agent-core-shutdown-hook");

        return thread;
    }

    private void start(final Injector primary) throws Exception, InterruptedException, ExecutionException {
        final InputManager input = primary.getInstance(InputManager.class);
        final OutputManager output = primary.getInstance(OutputManager.class);

        final AsyncFramework async = primary.getInstance(AsyncFramework.class);
        final ArrayList<AsyncFuture<Void>> startup = Lists.newArrayList();

        log.info("Waiting for all components to start...");

        startup.add(input.start());
        startup.add(output.start());

        async.collect(startup).get();
    }

    private void stop(final Injector primary) throws Exception {
        final InputManager input = primary.getInstance(InputManager.class);
        final OutputManager output = primary.getInstance(OutputManager.class);

        final AsyncFramework async = primary.getInstance(AsyncFramework.class);
        final ArrayList<AsyncFuture<Void>> shutdown = Lists.newArrayList();

        log.info("Waiting for all components to stop...");

        shutdown.add(input.stop());
        shutdown.add(output.stop());

        async.collect(shutdown).get();
    }

    /**
     * Setup early application Injector.
     *
     * The early injector is used by modules to configure the system.
     *
     * @throws Exception If something could not be set up.
     */
    private Injector setupEarlyInjector() throws Exception {
        final List<Module> modules = Lists.newArrayList();

        modules.add(new AbstractModule() {
            @Singleton
            @Provides
            @Named("application/yaml+config")
            public SimpleModule configModule() {
                final SimpleModule module = new SimpleModule();

                // Make InputPlugin, and OutputPlugin sub-type aware through the 'type' attribute.
                module.setMixInAnnotation(InputPlugin.class, FasterXmlSubTypeMixIn.class);
                module.setMixInAnnotation(OutputPlugin.class, FasterXmlSubTypeMixIn.class);

                return module;
            }

            @Override
            protected void configure() {
                bind(PluginContext.class).to(PluginContextImpl.class).in(Scopes.SINGLETON);
            }
        });

        final Injector injector = Guice.createInjector(modules);

        for (final FastForwardModule m : loadModules(injector)) {
            log.info("Setting up {}", m);

            try {
                m.setup();
            } catch (Exception e) {
                throw new Exception("Failed to call #setup() for module: " + m, e);
            }
        }

        return injector;
    }

    /**
     * Setup primary Injector.
     *
     * @return The primary injector.
     */
    private Injector setupPrimaryInjector(final Injector early, final AgentConfig config) {
        final List<Module> modules = Lists.newArrayList();

        modules.add(new AbstractModule() {
            @Singleton
            @Provides
            private ExecutorService executor() {
                return Executors.newFixedThreadPool(config.getAsyncThreads(),
                        new ThreadFactoryBuilder().setNameFormat("ffwd-async-%d").build());
            }

            @Singleton
            @Provides
            private AsyncFramework async(ExecutorService executor) {
                final AsyncCaller caller = new DirectAsyncCaller() {
                    @Override
                    protected void internalError(String what, Throwable e) {
                        log.error("Async call '{}' failed", what, e);
                    }
                };

                return TinyAsync.builder().executor(executor).caller(caller).build();
            }

            @Singleton
            @Provides
            @Named("boss")
            public EventLoopGroup bosses() {
                final ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("ffwd-boss-%d").build();
                return new NioEventLoopGroup(config.getBossThreads(), factory);
            }

            @Singleton
            @Provides
            @Named("worker")
            public EventLoopGroup workers() {
                final ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("ffwd-worker-%d").build();
                return new NioEventLoopGroup(config.getWorkerThreads(), factory);
            }

            @Singleton
            @Provides
            @Named("application/json")
            public ObjectMapper jsonMapper() {
                return new ObjectMapper();
            }

            @Singleton
            @Provides
            public AgentConfig config() {
                return config;
            }

            @Override
            protected void configure() {
                bind(Key.get(Serializer.class, Names.named("default"))).to(ToStringSerializer.class)
                        .in(Scopes.SINGLETON);
                bind(Timer.class).to(HashedWheelTimer.class).in(Scopes.SINGLETON);
                bind(ProtocolServers.class).to(ProtocolServersImpl.class).in(Scopes.SINGLETON);
                bind(ProtocolClients.class).to(ProtocolClientsImpl.class).in(Scopes.SINGLETON);
            }
        });

        modules.add(config.getInput().module());
        modules.add(config.getOutput().module());

        return early.createChildInjector(modules);
    }

    private AgentConfig readConfig(Injector early) throws IOException {
        final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
        final SimpleModule module = early
                .getInstance(Key.get(SimpleModule.class, Names.named("application/yaml+config")));

        mapper.registerModule(module);

        try (final InputStream input = Files.newInputStream(this.config)) {
            return mapper.readValue(input, AgentConfig.class);
        } catch (JsonParseException e) {
            throw new IOException("Failed to parse configuration", e);
        }
    }

    private List<FastForwardModule> loadModules(Injector injector) throws Exception {
        final List<FastForwardModule> modules = Lists.newArrayList();

        for (final Class<? extends FastForwardModule> module : this.modules) {
            final Constructor<? extends FastForwardModule> constructor;

            try {
                constructor = module.getConstructor();
            } catch (NoSuchMethodException e) {
                throw new Exception("Expected empty constructor for class: " + module, e);
            }

            final FastForwardModule m;

            try {
                m = constructor.newInstance();
            } catch (ReflectiveOperationException e) {
                throw new Exception("Failed to call constructor for class: " + module, e);
            }

            injector.injectMembers(m);

            modules.add(m);
        }

        return modules;
    }

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

    public static final class Builder {
        private List<Class<? extends FastForwardModule>> modules = Lists.newArrayList();
        private Path config = Paths.get("ffwd.yaml");

        public Builder config(Path config) {
            if (config == null)
                throw new IllegalArgumentException("'config' must not be null");

            this.config = config;
            return this;
        }

        public Builder modules(List<Class<? extends FastForwardModule>> modules) {
            if (modules == null)
                throw new IllegalArgumentException("'modules' must not be null");

            this.modules = modules;
            return this;
        }

        public AgentCore build() {
            return new AgentCore(modules, config);
        }
    }
}