RainbowGum.java

package io.jstach.rainbowgum;

import java.lang.System.Logger.Level;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.ServiceLoader;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import io.jstach.rainbowgum.LogRouter.RootRouter;
import io.jstach.rainbowgum.LogRouter.Router;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;

//@formatter:off
/**
 * The main entry point and configuration of RainbowGum logging.
 * <p>
 * RainbowGum logging loads configuration through the service loader. While you can
 * manually set RainbowGum using {@link #set(Supplier)} it is better to register
 * implementations through the ServiceLoader so that RainbowGum will load prior to any
 * external logging.
 * <p>
 * To register a custom RainbowGum:
 *
 *
{@snippet class="snippets.RainbowGumProviderExample" region="provider" :

class RainbowGumProviderExample implements RainbowGumProvider {

	@Override
	public Optional<RainbowGum> provide(LogConfig config) {

		Integer bufferSize = config.properties() //
			.forKey("logging.custom.async.bufferSize")
			.ofInt()
			.or(1024)
			.validateNow(RainbowGumProviderExample.class);

		LogProvider<LogOutput> output = (name, cfg) -> cfg.properties()
			.forKey("logging.custom.output")
			.ofProvider(LogOutput::of)
			.or(LogOutput.ofStandardOut())
			.validateNow(RainbowGumProviderExample.class)
			.provide(name, cfg);

		var gum = RainbowGum.builder() //
			.route(r -> {
				r.publisher(PublisherFactory //
					.async() //
					.bufferSize(bufferSize) //
					.build());
				r.appender("console", a -> {
					a.output(output);
				});
				r.level(Level.INFO);
			})
			.build();

		return Optional.of(gum);
	}

}
}
 * <p>
 * <strong>Initialization order</strong> - most of the actual work happens in
 * {@link LogConfig.Builder#build()}, {@link LogRouter.Router.Builder}, and
 * {@code LogAppenderRegistry}; this is the exact sequence for anyone debugging a
 * configuration or writing a {@link RainbowGumServiceProvider.RainbowGumProvider}:
 * <ol>
 * <li>{@link LogConfig.Builder#build()} builds the shared {@link LogConfig}:
 * <ol>
 * <li>Resolve the {@link ServiceRegistry} (default: a new empty one) and
 * {@link LogProperties} - manually added
 * {@link RainbowGumServiceProvider.PropertiesProvider}s, else (if a
 * {@link java.util.ServiceLoader} was supplied) SPI-discovered ones, with system
 * properties always layered in as a final fallback. Providers receive a temporary
 * {@link LogAlerts} collector.</li>
 * <li>Build {@link LogAlerts} and {@link LogMetrics} from properties.</li>
 * <li>Build the global level resolver ({@code logging.level.*} and
 * {@code logging.group.*}, plus anything added directly on the builder).</li>
 * <li>Construct the {@link LogConfig} itself and replay provider alerts into its
 * configured {@link LogAlerts}. This is also where a few
 * process-wide flags get latched (config change support, the appender lock, and
 * thread-local-disabled globals) and the default output/encoder/publisher
 * registries are created.</li>
 * <li>Run every {@link RainbowGumServiceProvider.Configurator} (manually added,
 * then SPI-discovered), highest {@linkplain RainbowGumServiceProvider.Configurator#priority()
 * priority} first. A configurator returning {@code false} is retried in a later
 * pass (see {@link RainbowGumServiceProvider#PASSES}) - useful if it depends on
 * another configurator running first.</li>
 * <li>Start {@link LogAlerts} - deliberately last, so a configurator's own alert
 * listener is already registered before alerts decides whether an unobserved
 * error should refuse startup.</li>
 * </ol>
 * </li>
 * <li>{@link Builder#build()} builds each named {@link LogRouter.Router} (a single
 * route named {@value LogProperties#DEFAULT_NAME}, unless {@code logging.routes}
 * names several) via {@link LogRouter.Router.Builder}'s own (package-private)
 * {@code build()}:
 * <ol>
 * <li>Resolve route flags ({@code logging.route.<name>.flags}).</li>
 * <li>Build a route-scoped level resolver: anything configured directly on the
 * route builder, layered over {@code logging.route.<name>.level.*}, falling back
 * to the global level resolver from step 1 (skipped entirely if the route flag
 * {@code IGNORE_GLOBAL_LEVEL_RESOLVER} is set) - and finally to {@code INFO} if
 * nothing at all resolves a level.</li>
 * <li>Resolve appenders - only if none were added programmatically on the route
 * builder (that check is all-or-nothing: adding even one appender in code skips
 * property-based resolution entirely). See <strong>Appender resolution</strong>
 * below.</li>
 * <li>Resolve the publisher - {@code logging.route.<name>.publisher.uri}, falling
 * back to a synchronous publisher if unset.</li>
 * <li>Hand the resolved appenders to the publisher and register the publisher in
 * the {@link ServiceRegistry}.</li>
 * </ol>
 * </li>
 * <li>{@link #start()} starts the root router, which starts each route's
 * publisher, which starts each of its appenders, which starts its output (e.g.
 * opening a file).</li>
 * </ol>
 * <strong>Appender resolution</strong> - the part most often mistaken for a bug,
 * since no appenders are ever added programmatically in the common case:
 * <ol>
 * <li>{@code logging.route.<name>.appenders} names the route's appenders; for the
 * {@value LogProperties#DEFAULT_NAME} route only, a missing value falls back to
 * the unprefixed {@code logging.appenders}.</li>
 * <li>If that is <em>still</em> missing: for the default route only, the list
 * defaults to {@code [file, console]} if {@code logging.file.name} (or Spring
 * Boot's convention) is set, otherwise just {@code [console]}. A non-default
 * route with no appenders configured anywhere is a startup failure, not a silent
 * no-op.</li>
 * <li>Each named appender then resolves independently. {@code file} and
 * {@code console} are the two built-in special cases, each with its own output
 * property/fallback ({@code logging.file.name} then
 * {@code logging.appender.file.output.uri} for {@code file};
 * {@code logging.appender.console.output.uri} falling back to standard-out for
 * {@code console}). Any other name is fully generic:
 * {@code logging.appender.<name>.output.uri}/{@code .encoder.uri}/{@code .flags}/
 * {@code .type}, with the output's own URI scheme choosing which registered
 * {@link LogOutput.OutputProvider} builds it.</li>
 * <li>An output can supply its own encoder by implementing
 * {@link LogOutput.ProvidesEncoder} (rare, e.g. some structured outputs): with
 * {@link LogOutput.ProvidesEncoder.Policy#MANDATORY} that encoder is used as-is
 * and the {@code .encoder.uri} property is never consulted (configuring one anyway
 * is a startup failure); with {@link LogOutput.ProvidesEncoder.Policy#DEFAULT}
 * it is only a fallback and the {@code .encoder.uri} property, if set, still wins.
 * An output that does not implement {@link LogOutput.ProvidesEncoder} at all is
 * unaffected: the {@code .encoder.uri} property (or the registry's default for the
 * output's type) always applies.</li>
 * </ol>
 */
//@formatter:on
@SuppressWarnings("InvalidInlineTag")
public sealed interface RainbowGum extends AutoCloseable, LogEventLogger {

	/**
	 * Gets the currently statically bound RainbowGum and will try to load and find one if
	 * there is none currently bound. <strong> This is a blocking operation as in locks
	 * are used and will block indefinitely till a gum has loaded. </strong> If that is
	 * not desired see {@link #getOrNull()}.
	 * @return current RainbowGum or new loaded one.
	 */
	public static RainbowGum of() {
		return RainbowGumHolder.get();
	}

	/**
	 * Gets the currently statically bound RainbowGum or <code>null</code> if none are
	 * <strong>finished binding</strong>. Unlike {@link #of()} this will never load or
	 * start an instance but rather just gets the currently bound one. It will also never
	 * block and does not wait if one is currently being loaded.
	 * @return current RainbowGum or <code>null</code>
	 */
	public static @Nullable RainbowGum getOrNull() {
		return RainbowGumHolder.current();
	}

	/**
	 * Provides the service loader default based RainbowGum.
	 * @return RainbowGum.
	 */
	public static RainbowGum defaults() {
		return RainbowGumServiceProvider.provide();
	}

	/**
	 * Creates a RainbowGum that will <strong>ALWAYS</strong> use the global router.
	 * @param config config.
	 * @return rainbow gum that always uses global router.
	 */
	public static RainbowGum queued(LogConfig config) {
		return new SimpleRainbowGum(config, LogRouter.global(), UUID.randomUUID());
	}

	/**
	 * Sets the global default RainbowGum. The supplied {@link RainbowGum} must not
	 * already be started.
	 * @param supplier the supplier will be memoized when {@linkplain Supplier#get()
	 * accessed} and {@link RainbowGum#start()} will be called.
	 */
	public static void set(Supplier<RainbowGum> supplier) {
		RainbowGumHolder.set(supplier);
	}

	/**
	 * Subscribes to notification of a new RainbowGum becoming the globally bound one (see
	 * {@link #set(Supplier)}, {@link #set(RainbowGum)}, {@link Builder#set()}). This is
	 * primarily for components like SLF4J's logger factory that are themselves
	 * bootstrapped once (often before "the real" RainbowGum has loaded, e.g. a
	 * framework's own pre-boot sequence) and otherwise have no way of finding out that a
	 * different RainbowGum has since replaced the one they captured.
	 * <p>
	 * Only a {@linkplain java.lang.ref.WeakReference weak reference} to
	 * <code>consumer</code> is retained, so subscribing does not by itself keep the
	 * consumer (or whatever it is attached to) reachable - the caller is responsible for
	 * keeping a strong reference to <code>consumer</code> for as long as it needs to keep
	 * receiving notifications, otherwise it may silently stop being called once garbage
	 * collected.
	 * @param consumer called with the newly bound global RainbowGum. Never called while
	 * any RainbowGum internal lock is held.
	 */
	public static void onGlobalChange(Consumer<RainbowGum> consumer) {
		RainbowGumHolder.globalChangePublisher.add(consumer);
	}

	/**
	 * Sets a Rainbow Gum and provides a supplier that will start it globally.
	 * @param gum that will be set immediatly.
	 * @return supplier that will set, get and start the supplied gum and if it is not the
	 * same will throw {@link IllegalStateException}.
	 */
	public static Supplier<? extends RainbowGum> set(RainbowGum gum) {
		UUID instanceId = gum.instanceId();
		RainbowGum.set(() -> gum);
		return () -> {
			var of = RainbowGum.of();
			/*
			 * TODO this is a hack. The holder lock should be used to make this not
			 * happen.
			 */
			if (!instanceId.equals(of.instanceId())) {
				throw new IllegalStateException("Another rainbow gum registered itself as the global. "
						+ "This is rare reace condition and probably a bug");
			}
			return of;
		};
	}

	/**
	 * The config associated with this instance.
	 * @return config.
	 */
	public LogConfig config();

	/**
	 * The router that will route log messages to publishers.
	 * @return router
	 */
	public RootRouter router();

	/**
	 * Starts the rainbow gum and returns it. It is returned for try-with usage
	 * convenience.
	 * @return the started RainbowGum.
	 */
	default RainbowGum start() {
		router().start(config());
		var debugMode = config().debugMode();
		if (debugMode == LogConfig.DebugModeType.ALL || debugMode == LogConfig.DebugModeType.HELP) {
			var report = LogReporter.builder().build().report(this);
			MetaLog.error(LogEventFactory.of(RainbowGum.class.getName())
				.eventNoArg(Level.INFO, "Rainbow Gum started:\n" + report, null));
		}
		if (config() instanceof DefaultLogConfig c) {
			var check = c.unusedKeyCheck();
			if (check != null) {
				reportUnusedKeys(check);
			}
		}
		if (config().alerts() instanceof DefaultLogAlerts alerts) {
			try {
				alerts.finishStartup();
			}
			catch (RuntimeException e) {
				close();
				throw e;
			}
		}
		return this;
	}

	/*
	 * By default an unused key with a suggestion is almost always a typo, so it is a
	 * warning, which logging.alerts.fail=warning (and logging.debug=help) turns into a
	 * failure. One without is only information: the key may be for a component type that
	 * was not chosen, such as encoder options left behind by a profile that switches
	 * encoder. logging.alerts.unusedKey raises both for strict configurations.
	 */
	private void reportUnusedKeys(UnusedKeyCheck check) {
		var eventFactory = LogEventFactory.of(LogConfig.class.getName());
		var unusedKeyLevel = config().alerts() instanceof DefaultLogAlerts alerts ? alerts.unusedKeyLevel()
				: LogAlerts.UnusedKeyLevel.INFO;
		for (var u : check.unused()) {
			var level = unusedKeyLevel.level(u.suggestion() != null);
			var event = eventFactory.eventNoArg(level, u.message(), null);
			config().alerts().alert(event);
			if (level == Level.INFO && config().debugMode().checksUnusedKeys()) {
				MetaLog.error(event);
			}
		}
	}

	/**
	 * Unique id of rainbow gum instance.
	 * @return random id created on creation.
	 */
	public UUID instanceId();

	/**
	 * Will close the RainbowGum and all registered components as well as removed from the
	 * shutdown hooks. If the rainbow gum is set as global it will no longer be global and
	 * replaced with the bootstrapping in memory queue. {@inheritDoc}
	 */
	@Override
	public void close();

	/**
	 * This append call is mainly for testing as it does not avoid making events that do
	 * not need to be made if no logging needs to be done. {@inheritDoc}
	 */
	@Override
	default void log(LogEvent event) {
		var r = router().route(event.loggerName(), event.level());
		if (r.isEnabled()) {
			r.log(event);
		}
	}

	/**
	 * Use to build a custom {@link RainbowGum} which will use the {@link LogConfig}
	 * provided by the service loader.
	 * @return builder.
	 */
	public static Builder builder() {
		ServiceLoader<RainbowGumServiceProvider> loader = ServiceLoader.load(RainbowGumServiceProvider.class);
		var config = RainbowGumServiceProvider.provideConfig(loader);
		return builder(config);
	}

	/**
	 * Use to build a custom {@link RainbowGum} with supplied config.
	 * @param config the config
	 * @return builder.
	 * @see #builder()
	 */
	public static Builder builder(LogConfig config) {
		return new Builder(config);
	}

	/**
	 * Use to build a custom {@link RainbowGum} with supplied config.
	 * @param config consumer that has first argument as config builder.
	 * @return builder.
	 * @see #builder()
	 * @apiNote this method is for ergonomic fluent reasons.
	 */
	public static Builder builder(Consumer<? super LogConfig.Builder> config) {
		var b = LogConfig.builder();
		config.accept(b);
		return builder(b.build());
	}

	/**
	 * RainbowGum Builder.
	 */
	public class Builder {

		private final LogConfig config;

		private final List<Router> routes = new ArrayList<>();

		private Builder(LogConfig config) {
			this.config = config;
		}

		/**
		 * Adds a router.
		 * @param route a router.
		 * @return builder.
		 */
		public Builder route(Router route) {
			bind();
			this.routes.add(route);
			return this;
		}

		/*
		 * Routers provision outputs/appenders into the config as soon as they are built,
		 * so the config must be claimed before that, not just at build().
		 */
		private void bind() {
			((DefaultLogConfig) config).bind(this);
		}

		/**
		 * Adds a route by using a consumer of the route builder.
		 * @param name name of router.
		 * @param consumer consumer is passed router builder. The consumer does not need
		 * to call {@link Router#builder(String,LogConfig)}
		 * @return builder.
		 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
		 */
		public Builder route(String name, Consumer<Router.Builder> consumer) {
			bind();
			var builder = Router.builder(name, config);
			consumer.accept(builder);
			return route(builder.build());
		}

		/**
		 * Adds a route by using a consumer of the route builder.
		 * @param consumer consumer is passed router builder. The consumer does not need
		 * to call {@link Router#builder(String,LogConfig)}
		 * @return builder.
		 * @see io.jstach.rainbowgum.LogRouter.Router.Builder
		 */
		public Builder route(Consumer<Router.Builder> consumer) {
			bind();
			var builder = Router.builder(Router.DEFAULT_ROUTER_NAME, config);
			consumer.accept(builder);
			return route(builder.build());
		}

		/**
		 * Builds an un-started {@link RainbowGum}.
		 * @return an un-started {@link RainbowGum}.
		 */
		public RainbowGum build() {
			return build(UUID.randomUUID());
		}

		/**
		 * Builds an un-started {@link RainbowGum}.
		 * @param instanceId unique id for rainbow gum instance.
		 * @return an un-started {@link RainbowGum}.
		 */
		private RainbowGum build(UUID instanceId) {
			((DefaultLogConfig) config).markBuilt(this);
			var routes = this.routes;
			var config = this.config;
			if (routes.isEmpty()) {
				var routeNamesResult = config.properties() //
					.forKey(LogProperties.ROUTES_PROPERTY)
					.ofList()
					.or(List.of());
				List<String> routeNames = routeNamesResult.validateNow(Builder.class);
				/*
				 * Validated as a plain list of names, before any of them is used to build
				 * a Router, so a bad name is reported against this list property itself,
				 * not deferred until some unrelated {name}-keyed property (e.g. this
				 * route's own level) happens to interpolate it first.
				 */
				LogProperty.Validator.validateNames(LogRouter.class, "route", routeNamesResult, routeNames);
				if (routeNames.isEmpty()) {
					routes = List.of(Router.builder(Router.DEFAULT_ROUTER_NAME, config).build());
				}
				else {
					routes = routeNames.stream().map(n -> Router.builder(n, config).build()).toList();
				}
			}
			var root = InternalRootRouter.of(routes, config);
			config.changePublisher().subscribe(c -> {
				root.changePublisher().publish(root);
			});
			return new SimpleRainbowGum(config, root, instanceId);
		}

		/**
		 * Builds, starts and sets the RainbowGum as the global one picked up by logging
		 * facades.
		 * @return started and set rainbow that can be used in a try-close.
		 */
		public RainbowGum set() {
			UUID instanceId = UUID.randomUUID();
			RainbowGum.set(() -> build(instanceId));
			var gum = RainbowGum.of();
			/*
			 * TODO this is a hack. The holder lock should be used to make this not
			 * happen.
			 */
			if (!instanceId.equals(gum.instanceId())) {
				throw new IllegalStateException("Another rainbow gum registered itself as the global. "
						+ "This is rare reace condition and probably a bug");
			}
			return gum;
		}

		/**
		 * Removes the currently set Rainbow Gum which will close it if one exists and
		 * will be replaced by the default provision process if {@link RainbowGum#of()} is
		 * called before {@link #set()}.
		 * @apiNote This is largely an internal detail for unit testing the provision
		 * process.
		 */
		public void unset() {
			RainbowGumHolder.remove(null);
		}

		/**
		 * For returning an optional for the LogProvider contract.
		 * @return optional that always has a rainbow gum.
		 * @apiNote this method is for ergonomics.
		 */
		public Optional<RainbowGum> optional() {
			return Optional.of(this.build());
		}

		/**
		 * For returning an optional for the LogProvider contract.
		 * @param condition condition to check if this rainbow gum should be used.
		 * @return optional rainbow gum.
		 * @apiNote this method is for ergonomics.
		 */
		public Optional<RainbowGum> optional(Function<? super LogConfig, Boolean> condition) {
			var cond = condition.apply(config);
			if (cond) {
				return Optional.of(this.build());
			}
			return Optional.empty();
		}

	}

}

final class RainbowGumHolder {

	/*
	 * TODO perhaps a StampedLock would be better performance wise.
	 */
	private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	private static Supplier<RainbowGum> supplier = RainbowGumServiceProvider::provide;

	private static volatile @Nullable RainbowGum rainbowGum = null;

	static final GlobalChangePublisher globalChangePublisher = new GlobalChangePublisher();

	/*
	 * Lock free read for per event hot paths (see KeyValuesContributor.global()) where
	 * possibly seeing the previous gum while another one binds is harmless.
	 */
	static @Nullable RainbowGum peek() {
		return rainbowGum;
	}

	static @Nullable RainbowGum current() {
		if (lock.readLock().tryLock()) {
			try {
				return rainbowGum;
			}
			finally {
				lock.readLock().unlock();
			}
		}
		return null;
	}

	static RainbowGum get() {
		lock.readLock().lock();
		try {
			var r = rainbowGum;
			if (r != null) {
				return r;
			}
		}
		finally {
			lock.readLock().unlock();
		}
		if (lock.writeLock().isHeldByCurrentThread()) {
			throw new IllegalStateException("RainbowGum component tried to log too early. "
					+ "This is usually caused by dependencies calling logging.");
		}
		RainbowGum newlyStarted = null;
		lock.writeLock().lock();
		try {
			var r = rainbowGum;
			if (r != null) {
				return r;
			}
			r = supplier.get();
			start(r);
			rainbowGum = r;
			newlyStarted = r;
			return r;

		}
		finally {
			lock.writeLock().unlock();
			/*
			 * Published outside of the write lock so a subscriber can never deadlock or
			 * trip the "tried to log too early" reentrancy guard above by, say, trying to
			 * read a volatile field it owns - it is not calling back into this class.
			 */
			if (newlyStarted != null) {
				globalChangePublisher.publish(newlyStarted);
			}
		}

	}

	static boolean remove(@Nullable RainbowGum gum) {
		lock.writeLock().lock();
		try {
			var original = rainbowGum;
			if (gum != null) {
				if (original != gum) {
					return false;
				}
			}
			/*
			 * Reset the global router
			 */
			if (original != null) {
				LogRouter.global().close();
			}
			rainbowGum = null;
			supplier = RainbowGumServiceProvider::provide;
			return true;
		}
		finally {
			lock.writeLock().unlock();
		}
	}

	static void set(Supplier<RainbowGum> rainbowGumSupplier) {
		Objects.requireNonNull(rainbowGumSupplier);
		if (lock.writeLock().isHeldByCurrentThread()) {
			throw new IllegalStateException("RainbowGum component tried to log too early. "
					+ "This is usually caused by dependencies calling logging.");
		}
		lock.writeLock().lock();
		try {
			rainbowGum = null;
			supplier = rainbowGumSupplier;
		}
		finally {
			lock.writeLock().unlock();
		}
	}

	private static void start(RainbowGum gum) {
		Objects.requireNonNull(gum);
		ShutdownManager.addShutdownHook(gum);
		gum.start();
		InternalRootRouter.setRouter(gum.router());
	}

}

/*
 * Unlike LogConfig.ChangePublisher/LogRouter.RouteChangePublisher (both scoped to a
 * single RainbowGum/router instance and torn down with it) this is a JVM-lifetime static
 * registry, so consumers are held only weakly - otherwise every SLF4J logger factory (or
 * any other subscriber) ever created over the life of the JVM, e.g. across many
 * short-lived RainbowGums in tests, would be pinned forever.
 */
final class GlobalChangePublisher {

	private final Queue<WeakReference<Consumer<RainbowGum>>> consumers = new ConcurrentLinkedQueue<>();

	void add(Consumer<RainbowGum> consumer) {
		consumers.add(new WeakReference<>(consumer));
	}

	void publish(RainbowGum gum) {
		for (var it = consumers.iterator(); it.hasNext();) {
			var consumer = it.next().get();
			if (consumer == null) {
				it.remove();
			}
			else {
				consumer.accept(gum);
			}
		}
	}

}

final class SimpleRainbowGum implements RainbowGum, Shutdownable {

	private final LogConfig config;

	private final RootRouter router;

	private final AtomicInteger state = new AtomicInteger(0);

	private final UUID instanceId;

	private static final int INIT = 0;

	private static final int STARTED = 1;

	private static final int CLOSED = 2;

	public SimpleRainbowGum(LogConfig config, RootRouter router, UUID instanceId) {
		super();
		this.config = config;
		this.router = router;
		this.instanceId = instanceId;
	}

	@Override
	public LogConfig config() {
		return this.config;
	}

	@Override
	public RootRouter router() {
		return this.router;
	}

	@Override
	public RainbowGum start() {
		int current;
		if ((current = state.compareAndExchange(INIT, STARTED)) == INIT) {
			return RainbowGum.super.start();
		}
		throw new IllegalStateException("Cannot start. This rainbowgum is " + stateLabel(current));
	}

	@Override
	public UUID instanceId() {
		return this.instanceId;
	}

	@Override
	public void close() {
		if (state.compareAndSet(STARTED, CLOSED)) {
			RainbowGumHolder.remove(this);
			try {
				shutdown();
			}
			finally {
				ShutdownManager.removeShutdownHook(this);
			}
			return;
		}
	}

	@Override
	public void shutdown() {
		router().close();
		config().alerts().close();
		config().serviceRegistry().close();
	}

	@Override
	public String toString() {
		return "SimpleRainbowGum [instanceId=" + instanceId + ", config=" + config + ", router=" + router + ", state="
				+ stateLabel(state.get()) + "]";
	}

	private static String stateLabel(int state) {
		return switch (state) {
			case INIT -> "created";
			case STARTED -> "started";
			case CLOSED -> "closed";
			default -> {
				throw new IllegalArgumentException("" + state);
			}
		};
	}

}