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

com.sandpolis.core.instance.MainDispatch Maven / Gradle / Ivy

There is a newer version: 6.1.0
Show newest version
/*******************************************************************************
 *                                                                             *
 *                Copyright © 2015 - 2019 Subterranean Security                *
 *                                                                             *
 *  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.sandpolis.core.instance;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sandpolis.core.instance.idle.IdleLoop;
import com.sandpolis.core.proto.util.Platform.Instance;
import com.sandpolis.core.proto.util.Platform.InstanceFlavor;
import com.sandpolis.core.proto.util.Result.Outcome;
import com.sandpolis.core.util.ProtoUtil;

/**
 * {@link MainDispatch} allows the instance's main method to configure
 * initialization tasks before they are sequentially executed by this class.
 * Idle tasks and shutdown tasks are also managed by this class.
 *
 * @author cilki
 * @since 5.0.0
 */
public final class MainDispatch {

	public static final Logger log = LoggerFactory.getLogger(MainDispatch.class);

	/**
	 * A configurable list of tasks that initialize the instance.
	 */
	private static List tasks = new ArrayList<>();

	/**
	 * A configurable list of tasks that are executed on instance shutdown.
	 */
	private static List shutdown = new LinkedList<>();

	/**
	 * A {@link Thread} that runs idle tasks in the background.
	 */
	private static IdleLoop idle;

	/**
	 * The instance's main {@link Class}.
	 */
	private static Class main = MainDispatch.class;

	/**
	 * The instance's {@link Instance} type.
	 */
	private static Instance instance;

	/**
	 * The instance's {@link InstanceFlavor} type.
	 */
	private static InstanceFlavor flavor;

	/**
	 * Get the {@link Class} that was dispatched.
	 *
	 * @return The dispatched {@link Class} or {@code MainDispatch.class} if
	 *         {@link #dispatch} has not been called
	 */
	public static Class getMain() {
		return main;
	}

	/**
	 * Get the {@link Instance} type.
	 *
	 * @return The dispatched {@link Instance} or {@code null} if {@link #dispatch}
	 *         has not been called
	 */
	public static Instance getInstance() {
		return instance;
	}

	/**
	 * Get the {@link InstanceFlavor} type.
	 *
	 * @return The dispatched {@link InstanceFlavor} or {@code null} if
	 *         {@link #dispatch} has not been called
	 */
	public static InstanceFlavor getInstanceFlavor() {
		return flavor;
	}

	/**
	 * Get the {@link IdleLoop}.
	 *
	 * @return The registered {@link IdleLoop} or {@code null} if one has not been
	 *         registered
	 */
	public static IdleLoop getIdleLoop() {
		return idle;
	}

	/**
	 * Invokes the instance's main method (which should register initialization
	 * tasks with {@link MainDispatch}) and then initializes the instance.
	 *
	 * @param main     The {@link Class} which contains the {@code main} to invoke
	 * @param args     The arguments to be passed to the {@code main}
	 * @param instance The instance's {@link Instance}
	 * @param flavor   The instance's {@link InstanceFlavor}
	 */
	public static void dispatch(Class main, String[] args, Instance instance, InstanceFlavor flavor) {
		if (MainDispatch.main != MainDispatch.class)
			throw new IllegalStateException("Dispatch cannot be called more than once");

		MainDispatch.main = Objects.requireNonNull(main);
		MainDispatch.instance = Objects.requireNonNull(instance);
		MainDispatch.flavor = Objects.requireNonNull(flavor);

		long timestamp = System.currentTimeMillis();

		// Pass main arguments to the Config class
		Config.setArguments(args);

		// Setup exception handler
		Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
			log.error("An unexpected exception has occurred", throwable);
		});

		// Setup shutdown hook
		Runtime.getRuntime().addShutdownHook(new Thread(() -> {
			shutdown.forEach(task -> {
				try {
					task.execute(new TaskOutcome(task.toString()));
				} catch (Exception e) {
					log.error("Failed to execute shutdown task", e);
				}
			});
		}));

		// Invoke the main method
		try {
			main.getDeclaredMethod("main", String[].class).invoke(null, (Object) args);
		} catch (Exception e) {
			throw new RuntimeException("Failed to invoke main method in class: " + main.getName(), e);
		}

		// Execute tasks
		for (Task task : tasks) {
			if (task.initMetadata == null)
				throw new RuntimeException("Unregistered initialization task class");

			TaskOutcome outcome = new TaskOutcome(task.initMetadata.name());

			if (!task.initMetadata.condition().isEmpty() && !Config.getBoolean(task.initMetadata.condition())) {
				task.outcome = outcome.skipped();
			} else {
				try {
					task.outcome = task.execute(outcome);
				} catch (Exception e) {
					task.outcome = outcome.failure(e);
				}

				if (!task.outcome.isSkipped() && !outcome.getOutcome().getResult() && task.initMetadata.fatal()) {
					log.error("A fatal error has occurred in task: {}", task.initMetadata.name());
					logTaskSummary();
					System.exit(1);
				}
			}
		}

		// Print task summary if any task failed
		if (tasks.stream().filter(t -> !t.outcome.getOutcome().getResult()).count() != 0)
			logTaskSummary();

		// Print task summary if required
		else if (!Config.has("logging.startup.summary") || !Config.getBoolean("logging.startup.summary"))
			logTaskSummary();

		// Launch idle loop
		if (idle != null)
			idle.start();

		// Cleanup
		tasks = null;

		log.info("Initialization completed in {} ms", System.currentTimeMillis() - timestamp);
	}

	/**
	 * Build a summary for {@link #tasks} and write to log.
	 */
	private static void logTaskSummary() {
		if (tasks.isEmpty()) {
			log.warn("Skipping task summary: no tasks were registered");
			return;
		}

		// Create a format string according to the width of the longest task description
		String descFormat = String.format("%%%ds:",
				tasks.stream().mapToInt(task -> task.initMetadata.name().length()).max().getAsInt());

		for (Task task : tasks) {
			if (task.outcome == null) {
				log.info(String.format(descFormat + "      ( ---- ms)", task.initMetadata.name()));
				continue;
			}

			Outcome outcome = task.outcome.getOutcome();

			// Format description and result
			String line = String.format(descFormat + " %4s", task.initMetadata.name(),
					task.outcome.isSkipped() ? "SKIP" : outcome.getResult() ? "OK" : "FAIL");

			// Format duration
			if (task.outcome.isSkipped() || !outcome.getResult())
				line += " ( ---- ms)";
			else if (outcome.getTime() > 9999)
				line += String.format(" (%5.1f  s)", outcome.getTime() / 1000.0);
			else
				line += String.format(" (%5d ms)", outcome.getTime());

			// Write to log
			if (task.outcome.isSkipped() || outcome.getResult()) {
				log.info(line);
			} else {
				log.error(line);
				if (!outcome.getException().isEmpty())
					log.error(outcome.getException());
			}
		}
	}

	/**
	 * Register an {@link IdleLoop} with {@link MainDispatch}. The loop will be
	 * started during dispatch.
	 *
	 * @param idle The new {@link IdleLoop}
	 */
	public static void register(IdleLoop idle) {
		if (idle == null)
			throw new IllegalArgumentException();
		if (MainDispatch.idle != null)
			throw new IllegalStateException("Only one idle loop can be registered");

		MainDispatch.idle = idle;
	}

	/**
	 * Register an idle task with the {@link IdleLoop}.
	 *
	 * @param task The new idle task which returns true if the task should be
	 *             rescheduled after completion or false if the task should be
	 *             dropped
	 */
	public static void registerIdle(Supplier task) {
		if (idle == null)
			// Register a default loop
			idle = new IdleLoop();

		idle.register(task);
	}

	/**
	 * Register a new initialization task which will be executed during the
	 * dispatch. Tasks registered with this method are executed sequentially in the
	 * same order as the method calls.
	 *
	 * @param task The task reference
	 */
	public static void register(Task task) {
		Objects.requireNonNull(task);
		if (tasks == null)
			throw new IllegalStateException("Tasks cannot be registered after dispatch is complete");
		if (tasks.contains(task))
			throw new IllegalArgumentException("Tasks cannot be registered more than once");
		if (shutdown.contains(task))
			throw new IllegalArgumentException("Shutdown tasks cannot be registered more than once");

		if (task.initMetadata != null)
			tasks.add(task);
		else if (task.shutdownMetadata != null)
			shutdown.add(task);
		else
			throw new RuntimeException("Unknown task type");
	}

	/**
	 * A task that is executed during instance shutdown.
	 */
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface ShutdownTask {
	}

	/**
	 * A task that is executed at an indeterminate time in the future when the
	 * instance has relatively little work to do.
	 */
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface IdleTask {
	}

	/**
	 * A task that is executed during instance initialization.
	 */
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface InitializationTask {

		/**
		 * The name of the task.
		 */
		public String name();

		/**
		 * The condition under which the task will execute.
		 */
		public String condition() default "";

		/**
		 * Indicates that the application should exit if the task fails.
		 */
		public boolean fatal() default false;

		/**
		 * Indicates that the task will run if and only if the instance is in debug
		 * mode.
		 */
		public boolean debug() default false;

	}

	/**
	 * Represents the outcome of an {@link InitializationTask}.
	 */
	public static class TaskOutcome {

		private Outcome outcome;
		private Outcome.Builder temporary;

		private boolean skipped;

		/**
		 * Get the task's skipped flag.
		 *
		 * @return Whether the task was skipped
		 */
		public boolean isSkipped() {
			return skipped;
		}

		/**
		 * Get the task's outcome.
		 *
		 * @return The task's outcome
		 */
		public Outcome getOutcome() {
			return outcome;
		}

		/**
		 * Mark the task as skipped.
		 *
		 * @return A completed {@link TaskOutcome}
		 */
		public TaskOutcome skipped() {
			if (outcome != null)
				throw new IllegalStateException();

			skipped = true;
			outcome = ProtoUtil.complete(temporary);
			return this;
		}

		/**
		 * Mark the task as succeeded.
		 *
		 * @return A completed {@link TaskOutcome}
		 */
		public TaskOutcome success() {
			if (outcome != null)
				throw new IllegalStateException();

			outcome = ProtoUtil.success(temporary);
			return this;
		}

		/**
		 * Mark the task as failed.
		 *
		 * @return A completed {@link TaskOutcome}
		 */
		public TaskOutcome failure() {
			if (outcome != null)
				throw new IllegalStateException();

			outcome = ProtoUtil.failure(temporary);
			return this;
		}

		/**
		 * Mark the task as failed with an exception.
		 *
		 * @param t The relevant exception
		 * @return A completed {@link TaskOutcome}
		 */
		public TaskOutcome failure(Exception t) {
			if (outcome != null)
				throw new IllegalStateException();

			outcome = ProtoUtil.failure(temporary, t);
			return this;
		}

		/**
		 * Mark the task as failed with a comment.
		 *
		 * @param comment The relevant comment
		 * @return A completed {@link TaskOutcome}
		 */
		public TaskOutcome failure(String comment) {
			if (outcome != null)
				throw new IllegalStateException();

			outcome = ProtoUtil.failure(temporary, comment);
			return this;
		}

		/**
		 * Mark the task as complete.
		 *
		 * @param result The task result
		 * @return A completed {@link TaskOutcome}
		 */
		public TaskOutcome complete(boolean result) {
			if (outcome != null)
				throw new IllegalStateException();

			outcome = ProtoUtil.complete(temporary.setResult(result));
			return this;
		}

		/**
		 * Mark the task as complete and merge the given outcome.
		 *
		 * @param _outcome The outcome to merge
		 * @return A completed {@link TaskOutcome}
		 */
		public TaskOutcome complete(Outcome _outcome) {
			if (outcome != null)
				throw new IllegalStateException();

			outcome = temporary.clearTime().mergeFrom(_outcome).build();
			return this;
		}

		public TaskOutcome(String name) {
			temporary = ProtoUtil.begin(Objects.requireNonNull(name));
		}
	}

	/**
	 * The task's operation.
	 */
	public interface TaskAction {
		public TaskOutcome execute(TaskOutcome task) throws Exception;
	}

	public static class Task implements TaskAction {

		private InitializationTask initMetadata;
		private IdleTask idleMetadata;
		private ShutdownTask shutdownMetadata;

		private TaskAction action;
		private TaskOutcome outcome;

		public Task(TaskAction action) {
			this.action = Objects.requireNonNull(action);
		}

		@Override
		public TaskOutcome execute(TaskOutcome task) throws Exception {
			return action.execute(task);
		}
	}

	/**
	 * Serach for task fields in the given class and inject annotations into them.
	 *
	 * @param c The class to search
	 */
	public static void register(Class c) {
		try {
			for (Field field : c.getDeclaredFields()) {
				for (Annotation annotation : field.getAnnotations()) {
					Field inject = null;
					if (annotation instanceof InitializationTask) {
						inject = Task.class.getDeclaredField("initMetadata");
					} else if (annotation instanceof IdleTask) {
						inject = Task.class.getDeclaredField("idleMetadata");
					} else if (annotation instanceof ShutdownTask) {
						inject = Task.class.getDeclaredField("shutdownMetadata");
					} else {
						continue;
					}

					field.setAccessible(true);
					inject.setAccessible(true);
					inject.set(field.get(null), annotation);
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private MainDispatch() {
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy