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

berlin.yuna.natsserver.logic.Nats Maven / Gradle / Ivy

package berlin.yuna.natsserver.logic;

import berlin.yuna.clu.logic.SystemUtil;
import berlin.yuna.clu.logic.Terminal;
import berlin.yuna.natsserver.config.NatsConfig;
import berlin.yuna.natsserver.config.NatsOptions;
import berlin.yuna.natsserver.config.NatsOptionsBuilder;
import berlin.yuna.natsserver.model.MapValue;
import berlin.yuna.natsserver.model.ValueSource;
import berlin.yuna.natsserver.model.exception.NatsStartException;
import io.nats.commons.NatsInterface;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.BindException;
import java.net.PortUnreachableException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static berlin.yuna.clu.logic.SystemUtil.OS;
import static berlin.yuna.clu.model.OsType.OS_WINDOWS;
import static berlin.yuna.natsserver.config.NatsConfig.*;
import static berlin.yuna.natsserver.config.NatsOptions.natsBuilder;
import static berlin.yuna.natsserver.logic.NatsUtils.*;
import static berlin.yuna.natsserver.model.MapValue.mapValueOf;
import static berlin.yuna.natsserver.model.ValueSource.DEFAULT;
import static berlin.yuna.natsserver.model.ValueSource.DSL;
import static berlin.yuna.natsserver.model.ValueSource.ENV;
import static berlin.yuna.natsserver.model.ValueSource.FILE;
import static java.lang.Boolean.parseBoolean;
import static java.lang.Integer.parseInt;
import static java.lang.String.format;
import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ;
import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
import static java.util.Arrays.stream;
import static java.util.Optional.ofNullable;
import static java.util.logging.Logger.getLogger;

/**
 * {@link Nats}
 *
 * @author Yuna Morgenstern
 * @see SystemUtil
 * @see NatsInterface
 * @since 1.0
 */
@SuppressWarnings({"unused", "UnusedReturnValue", "java:S1133"})
public class Nats implements NatsInterface {

    protected final String name;
    protected final Long timeoutMs;
    private final Logger logger;
    protected final Map configMap = new ConcurrentHashMap<>();
    protected final AtomicReference terminal = new AtomicReference<>(null);
    public static final String NATS_PREFIX = "NATS_";
    private static final String TMP_DIR = "java.io.tmpdir";

    /**
     * Throws all exceptions as {@link NatsStartException} which is a {@link RuntimeException} 
* Possible wrapped exceptions:
* {@link IOException} if {@link Nats} is not found or unsupported on the {@link SystemUtil}
* {@link BindException} if port is already taken
* {@link PortUnreachableException} if {@link Nats} is not starting cause port is not free
*/ public Nats() { this(natsBuilder().autostart(true).build()); } /** * Starts the server if {@link NatsConfig#NATS_AUTOSTART} == true * Throws all exceptions as {@link NatsStartException} which is a {@link RuntimeException}
* Possible wrapped exceptions:
* {@link IOException} if {@link Nats} is not found or unsupported on the {@link SystemUtil}
* {@link BindException} if port is already taken
* {@link PortUnreachableException} if {@link Nats} is not starting cause port is not free
* * @param port the port to start on or <=0 to use an automatically allocated port */ public Nats(final int port) { this(natsBuilder().port(port).build()); } /** * Starts the server if {@link NatsConfig#NATS_AUTOSTART} == true * Throws all exceptions as {@link NatsStartException} which is a {@link RuntimeException}
* Possible wrapped exceptions:
* {@link IOException} if {@link Nats} is not found or unsupported on the {@link SystemUtil}
* {@link BindException} if port is already taken
* {@link PortUnreachableException} if {@link Nats} is not starting cause port is not free
* * @param natsOptions nats options */ public Nats(final NatsOptionsBuilder natsOptions) { this(natsOptions.build()); } /** * Starts the server if {@link NatsConfig#NATS_AUTOSTART} == true * Throws all exceptions as {@link NatsStartException} which is a {@link RuntimeException}
* Possible wrapped exceptions:
* {@link IOException} if {@link Nats} is not found or unsupported on the {@link SystemUtil}
* {@link BindException} if port is already taken
* {@link PortUnreachableException} if {@link Nats} is not starting cause port is not free
* * @param natsOptions nats options */ public Nats(final io.nats.commons.NatsOptions natsOptions) { Runtime.getRuntime().addShutdownHook(new Thread(this::close)); final var timeoutMsTmp = new AtomicLong(-1); if (natsOptions instanceof NatsOptions) { ((NatsOptions) natsOptions).config().forEach(this::addConfig); } setDefaultConfig(); setEnvConfig(); setConfigFromProperties(); setConfigFromNatsOptions(natsOptions); this.name = getValue(NATS_LOG_NAME); this.timeoutMs = Long.parseLong(getValue(NATS_TIMEOUT_MS)); this.logger = ofNullable(natsOptions.logger()).orElse(Logger.getLogger(name)); ofNullable(natsOptions.logLevel()).ifPresent(logger::setLevel); ofNullable(getValue(NATS_AUTOSTART)).filter(Boolean::valueOf).ifPresent(autostart -> start()); } /** * Starts the server if not already started e.g. by {@link NatsConfig#NATS_AUTOSTART} == true
* TThrows all exceptions as {@link NatsStartException} which is a {@link RuntimeException}
* Possible wrapped exceptions:
* {@link IOException} if {@link Nats} is not found or unsupported on the {@link SystemUtil}
* {@link BindException} if port is already taken
* {@link PortUnreachableException} if {@link Nats} is not starting cause port is not free
* * @return {@link Nats} */ public synchronized Nats start() { try { if (terminal.get() != null && terminal.get().running()) { logger.severe(() -> format("[%s] is already running", logger.getName())); return this; } downloadNats(); final int port = setNextFreePort(); validatePort(port, timeoutMs, true, () -> new BindException("Address already in use [" + port + "]"), () -> false); final String command = prepareCommand(); logger.info(() -> format("Starting [%s] port [%s] version [%s] command [%s]", name, port, getValue(NATS_SYSTEM), command)); startProcess(command); validatePort(port, timeoutMs, false, () -> new PortUnreachableException(name + " failed to start with port [" + port + "]"), () -> terminal.get() == null); logger.info(() -> format("Started [%s] port [%s] version [%s] pid [%s]", name, port, getValue(NATS_SYSTEM), pid())); } catch (Exception e) { throw new NatsStartException(e); } return this; } @Override public Process process() { return ofNullable(terminal.get()).map(Terminal::process).orElse(null); } @Override public String[] customArgs() { return ofNullable(getValue(NATS_ARGS, () -> null)).map(args -> args.split(ARGS_SEPARATOR)).orElseGet(() -> new String[0]); } @Override public Logger logger() { return logger; } @Override public Level loggingLevel() { return logger.getLevel(); } /** * @return Path to binary file
* see {@link NatsConfig#NATS_BINARY_PATH} */ @Override public Path binary() { return Paths.get(getValue(NATS_BINARY_PATH, () -> Paths.get( getEnv(TMP_DIR), getValue(NATS_LOG_NAME).toLowerCase(), getValue(NATS_LOG_NAME).toLowerCase() + "_" + getValue(NATS_SYSTEM) + (OS == OS_WINDOWS ? ".exe" : "") ).toString())); } /** * @return Port (if <=0, the port will be visible after {@link Nats#start()} - see also {@link NatsConfig#NATS_AUTOSTART})
* see {@link NatsConfig#PORT} */ @Override public int port() { return parseInt(getValue(PORT)); } /** * @return true if Jetstream is enabled
* see {@link NatsConfig#JETSTREAM} */ @Override public boolean jetStream() { return parseBoolean(getValue(JETSTREAM)); } /** * @return true if "DV", "DVV" or "DEBUG" is set
* see {@link NatsConfig#DV} * see {@link NatsConfig#DVV} * see {@link NatsConfig#DEBUG} */ @Override public boolean debug() { return parseBoolean(getValue(DV)) || parseBoolean(getValue(DVV)) || parseBoolean(getValue(DEBUG)); } /** * @return custom nats config file
* see {@link NatsConfig#CONFIG} */ @Override public Path configFile() { return ofNullable(getValue(CONFIG, () -> null)).map(Path::of).orElse(null); } /** * @return custom property config file
* see {@link NatsConfig#NATS_PROPERTY_FILE} */ public Path configPropertyFile() { return ofNullable(getValue(NATS_PROPERTY_FILE, () -> null)).map(Path::of).orElse(null); } @Override public void close() { shutdown(); } /** * Gets resolved config value from key * * @param key config key * @return config key value */ public String getValue(final NatsConfig key) { return getValue(key, () -> key.defaultValue() == null ? null : String.valueOf(key.defaultValue())); } /** * Gets resolved config value from key * * @param key config key * @param or lazy loaded fallback value * @return config key value */ public String getValue(final NatsConfig key, final Supplier or) { return resolveEnvs(ofNullable(configMap.get(key)).map(MapValue::value).orElseGet(or), configMap); } /** * get process id * * @return process id or -1 if process is not running */ public int pid() { try { return Integer.parseInt(String.join(" ", Files.readAllLines(pidFile(), StandardCharsets.UTF_8)).trim()); } catch (IOException e) { return -1; } } /** * get process id file which only exists when the process is running * * @return process id file path */ public Path pidFile() { return Paths.get(getValue(PID, () -> Paths.get( getEnv(TMP_DIR), getValue(NATS_LOG_NAME).toLowerCase(), port() + ".pid" ).toString())); } /** * Nats download url which is usually a zip file
* * @return nats download url */ public String downloadUrl() { return getValue(NATS_DOWNLOAD_URL); } /** * nats server URL from bind to host address * * @return nats server url */ public String url() { return "nats://" + getValue(NET) + ":" + port(); } /** * @return nats configuration */ public Map config() { return configMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().value())); } protected void setConfigFromNatsOptions(final io.nats.commons.NatsOptions natsOptions) { ofNullable(natsOptions.debug()).ifPresent(debug -> addConfig(DV, debug)); ofNullable(natsOptions.configFile()).ifPresent(config -> addConfig(CONFIG, config)); ofNullable(natsOptions.port()).ifPresent(port -> addConfig(PORT, port)); ofNullable(natsOptions.jetStream()).ifPresent(jetstream -> addConfig(JETSTREAM, jetstream)); ofNullable(natsOptions.logger()).map(Logger::getName).ifPresent(loggerName -> addConfig(NATS_LOG_NAME, loggerName)); } protected void setConfigFromProperties() { getPropertyFiles(ofNullable(getValue(NATS_PROPERTY_FILE)).filter(NatsUtils::isNotEmpty).orElse("nats.properties")).forEach(path -> { final Properties prop = new Properties(); try (final InputStream inputStream = new FileInputStream(path.toFile())) { prop.load(inputStream); } catch (IOException e) { getLogger(getValue(NATS_LOG_NAME)).severe("Unable to read property file [" + path.toUri() + "] cause of [" + e.getMessage() + "]"); } prop.forEach((key, value) -> addConfig(FILE, NatsConfig.valueOf(String.valueOf(key).toUpperCase()), removeQuotes((String) value))); }); } protected void setDefaultConfig() { for (NatsConfig cfg : NatsConfig.values()) { final String value = cfg.defaultValueStr(); addConfig(DEFAULT, cfg, value); } addConfig(DEFAULT, NATS_SYSTEM, NatsUtils.getSystem()); } protected void setEnvConfig() { for (NatsConfig cfg : NatsConfig.values()) { addConfig(ENV, cfg, getEnv(cfg.name().startsWith(NATS_PREFIX) ? cfg.name() : NATS_PREFIX + cfg.name())); } } protected void addConfig(final NatsConfig key, final Object value) { if (value != null && !String.valueOf(getValue(key)).equals(String.valueOf(value))) { addConfig(DSL, key, String.valueOf(value)); } } protected void addConfig(final ValueSource source, final NatsConfig key, final String value) { if (value != null) { configMap.put(key, configMap.computeIfAbsent(key, val -> mapValueOf(source, value)).update(source, value)); } } protected int setNextFreePort() { if (ofNullable(getValue(PORT, () -> null)).map(Integer::parseInt).orElse(-1) <= 0) { addConfig(configMap.get(PORT).source(), PORT, String.valueOf(getNextFreePort((int) PORT.defaultValue()))); } return port(); } @SuppressWarnings({"java:S899"}) protected Path downloadNats() throws IOException { final Path binaryPath = binary(); Files.createDirectories(binaryPath.getParent()); if (Files.notExists(binaryPath)) { final URL source = new URL(getValue(NATS_DOWNLOAD_URL)); unzip(download(source, Paths.get(binary().toString() + ".zip")), binaryPath); } //noinspection ResultOfMethodCallIgnored binaryPath.toFile().setExecutable(true); SystemUtil.setFilePermissions(binaryPath, OWNER_EXECUTE, OTHERS_EXECUTE, OWNER_READ, OTHERS_READ, OWNER_WRITE, OTHERS_WRITE); return binaryPath; } protected String prepareCommand() { final StringBuilder command = new StringBuilder(); setDefaultConfig(); setEnvConfig(); setConfigFromProperties(); addConfig(DSL, PID, pidFile().toString()); command.append(binary().toString()); configMap.forEach((key, mapValue) -> { if (!key.name().startsWith(NATS_PREFIX) && mapValue != null && isNotEmpty(mapValue.value())) { if (key.isWritableValue() || !"false".equals(mapValue.value())) { command.append(" "); command.append(key.key()); } if (key.isWritableValue()) { command.append("="); command.append(mapValue.value().trim().toLowerCase()); } } }); command.append(stream(customArgs()).collect(Collectors.joining(" ", " ", ""))); command.append(stream(getValue(NATS_ARGS, () -> "").split(ARGS_SEPARATOR)).map(String::trim).collect(Collectors.joining(" ", " ", ""))); return command.toString(); } protected synchronized void shutdown() { try { sendStopSignal(); waitForShutDown(timeoutMs); if (terminal.get() != null) { terminal.get().process().destroy(); terminal.get().process().waitFor(); } } catch (InterruptedException ignored) { logger.warning(() -> format("Could not find process to stop [%s]", name)); Thread.currentThread().interrupt(); } finally { if (port() > -1) { waitForPort(port(), timeoutMs, true); logger.info(() -> format("Stopped [%s]", name)); } terminal.set(null); } deletePidFile(); } protected void sendStopSignal() { logger.info(() -> format("Stopping [%s]", name)); if (pid() != -1) { new Terminal() .consumerInfoStream(logger::info) .consumerErrorStream(logger::severe) .breakOnError(false) .execute(binary() + " " + SIGNAL.key() + " stop=" + pid()); } } protected void waitForShutDown(final long timeoutMs) { Optional.of(port()).filter(port -> port > 0).ifPresent(port -> { logger.info(() -> format("Stopped [%s]", name)); waitForPort(port, timeoutMs, true); }); } protected void deletePidFile() { ignoreException(run -> { Files.deleteIfExists(pidFile()); return run; }); } protected void startProcess(final String command) { terminal.set(new Terminal() .timeoutMs(timeoutMs) .breakOnError(false) .consumerErrorStream(logger::info) .consumerInfoStream(serve -> { logger.severe(serve); terminal.set(null); }) .execute(command, null) ); } @Override public String toString() { return "Nats{" + "name=" + name + ", pid='" + pid() + '\'' + ", port=" + port() + ", configs=" + configMap.size() + '}'; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy