All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bt.runtime.BtRuntime Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2016—2021 Andrei Tomashpolskiy and individual contributors.
 *
 * 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 bt.runtime;

import bt.BtException;
import bt.event.EventSource;
import bt.module.ClientExecutor;
import bt.service.IRuntimeLifecycleBinder;
import bt.service.IRuntimeLifecycleBinder.LifecycleEvent;
import bt.service.LifecycleBinding;
import com.google.inject.Injector;
import com.google.inject.Key;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

/**
 * A runtime is an orchestrator of multiple simultaneous torrent sessions.
 * It provides a DI container with shared services and manages the application's lifecycle.
 *
 * @since 1.0
 */
public class BtRuntime {

    private static final Logger LOGGER = LoggerFactory.getLogger(BtRuntime.class);

    /**
     * @return Runtime builder with default configuration
     * @since 1.0
     */
    public static BtRuntimeBuilder builder() {
        return new BtRuntimeBuilder();
    }

    /**
     * @param config Custom configuration
     * @return Runtime builder
     * @since 1.0
     */
    public static BtRuntimeBuilder builder(Config config) {
        return new BtRuntimeBuilder(config);
    }

    /**
     * Creates a vanilla runtime with default configuration
     *
     * @return Runtime without any extensions
     * @since 1.0
     */
    public static BtRuntime defaultRuntime() {
        return builder().build();
    }

    private Injector injector;
    private Config config;
    private Set knownClients;
    private ExecutorService clientExecutor;
    private AtomicBoolean started;
    private final Object lock;
    private Thread hook;

    private boolean manualShutdownOnly;

    BtRuntime(Injector injector, Config config) {
        this.injector = injector;
        this.config = config;
        this.knownClients = ConcurrentHashMap.newKeySet();
        this.clientExecutor = injector.getBinding(Key.get(ExecutorService.class, ClientExecutor.class))
                .getProvider().get();
        this.started = new AtomicBoolean(false);
        this.lock = new Object();
    }

    /**
     * Disable automatic runtime shutdown, when all clients have been stopped.
     *
     * @since 1.0
     */
    void disableAutomaticShutdown() {
        this.manualShutdownOnly = true;
    }

    /**
     * @return Injector instance
     * @since 1.0
     */
    public Injector getInjector() {
        return injector;
    }

    /**
     * @return Runtime configuration
     * @since 1.0
     */
    public Config getConfig() {
        return config;
    }

    /**
     * Convenience method to get an instance of a shared DI service.
     *
     * @return Instance of a shared DI service
     * @since 1.0
     */
    public  T service(Class serviceType) {
        return injector.getInstance(serviceType);
    }

    /**
     * @return true if this runtime is up and running
     * @since 1.0
     */
    public boolean isRunning() {
        return started.get();
    }

    /**
     * Manually start the runtime (possibly with no clients attached).
     *
     * @since 1.0
     */
    public void startup() {
        if (started.compareAndSet(false, true)) {
            synchronized (lock) {
                runHooks(LifecycleEvent.STARTUP, e -> LOGGER.error("Error on runtime startup", e));
                // add JVM shutdown hook
                String threadName = String.format("%d.bt.runtime.shutdown-manager", config.getAcceptorPort());
                this.hook = new Thread(threadName) {
                    @Override
                    public void run() {
                        shutdown();
                    }
                };
                Runtime.getRuntime().addShutdownHook(hook);
            }
        }
    }

    /**
     * Attach the provided client to this runtime.
     *
     * @since 1.0
     */
    public void attachClient(BtClient client) {
        knownClients.add(client);
    }

    /**
     * Detach the client from this runtime.
     *
     * @since 1.0
     */
    public void detachClient(BtClient client) {
        if (knownClients.remove(client)) {
            if (!manualShutdownOnly && knownClients.isEmpty()) {
                shutdown();
            }
        } else {
            throw new IllegalArgumentException("Unknown client: " + client);
        }
    }

    /**
     * Get all clients, that are attached to this runtime.
     *
     * @since 1.1
     */
    public Collection getClients() {
        return Collections.unmodifiableCollection(knownClients);
    }

    /**
     * @since 1.5
     */
    public EventSource getEventSource() {
        return service(EventSource.class);
    }

    /**
     * Manually initiate the runtime shutdown procedure, which includes:
     * - stopping all attached clients
     * - stopping all workers and executors, that were created inside this runtime
     *   and registered via {@link IRuntimeLifecycleBinder}
     *
     * @since 1.0
     */
    public void shutdown() {
        if (started.compareAndSet(true, false)) {
            synchronized (lock) {
                knownClients.forEach(client -> {
                    try {
                        client.stop();
                    } catch (Throwable e) {
                        LOGGER.error("Error when stopping client", e);
                    }
                });

                try {
                    runHooks(LifecycleEvent.SHUTDOWN, this::onShutdownHookError);
                    clientExecutor.shutdownNow();
                } finally {
                    try {
                        Runtime.getRuntime().removeShutdownHook(hook);
                    } catch (IllegalStateException e) {
                        // ISE means that the JVM is shutting down, no need to re-throw
                    }
                }
            }
        }
    }

    private void runHooks(LifecycleEvent event, Consumer errorConsumer) {
        ExecutorService executor = createLifecycleExecutor(event);

        Map> futures = new HashMap<>();
        List syncBindings = new ArrayList<>();

        service(IRuntimeLifecycleBinder.class).visitBindings(
                event,
                binding -> {
                    if (binding.isAsync()) {
                        futures.put(binding, CompletableFuture.runAsync(toRunnable(event, binding), executor));
                    } else {
                        syncBindings.add(binding);
                    }
                });

        syncBindings.forEach(binding -> {
            String errorMessage = createErrorMessage(event, binding);
            try {
                toRunnable(event, binding).run();
            } catch (Throwable e) {
                errorConsumer.accept(new BtException(errorMessage, e));
            }
        });

        // if the app is shutting down, then we must wait for the futures to complete
        if (event == LifecycleEvent.SHUTDOWN) {
            futures.forEach((binding, future) -> {
                String errorMessage = createErrorMessage(event, binding);
                try {
                    future.get(config.getShutdownHookTimeout().toMillis(), TimeUnit.MILLISECONDS);
                } catch (InterruptedException | ExecutionException | TimeoutException e) {
                    errorConsumer.accept(new BtException(errorMessage, e));
                }
            });
        }

        shutdownGracefully(executor);
    }

    private String createErrorMessage(LifecycleEvent event, LifecycleBinding binding) {
        Optional descriptionOptional = binding.getDescription();
        String errorMessage = "Failed to execute " + event.name().toLowerCase() + " hook: ";
        errorMessage += ": " + (descriptionOptional.orElseGet(() -> binding.getRunnable().toString()));
        return errorMessage;
    }

    private ExecutorService createLifecycleExecutor(LifecycleEvent event) {
        AtomicInteger threadCount = new AtomicInteger();
        return Executors.newCachedThreadPool(r -> {
            String threadName = String.format("%d.bt.runtime.%s.worker-%d", config.getAcceptorPort(), event.name().toLowerCase(), threadCount.incrementAndGet());
            Thread t = new Thread(r, threadName);
            t.setDaemon(true);
            return t;
        });
    }

    private void shutdownGracefully(ExecutorService executor) {
        executor.shutdown();
        try {
            long timeout = config.getShutdownHookTimeout().toMillis();
            boolean terminated = executor.awaitTermination(timeout, TimeUnit.MILLISECONDS);
            if (!terminated) {
                LOGGER.warn("Failed to shutdown executor in {} millis", timeout);
            }
        } catch (InterruptedException e) {
            // ignore
            LOGGER.warn("Interrupted while waiting for shutdown", e);
            executor.shutdownNow();
        }
    }

    private Runnable toRunnable(LifecycleEvent event, LifecycleBinding binding) {
        return () -> {
            Runnable r = binding.getRunnable();

            Optional descriptionOptional = binding.getDescription();
            String description = descriptionOptional.orElseGet(r::toString);
            LOGGER.debug("Running " + event.name().toLowerCase() + " hook: " + description);

            r.run();
        };
    }

    private void onShutdownHookError(Throwable e) {
        // logging facilities might be unavailable at this moment,
        // so using standard output
        e.printStackTrace(System.err);
        System.err.flush();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy