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

io.questdb.Bootstrap Maven / Gradle / Ivy

/*******************************************************************************
 *     ___                  _   ____  ____
 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
 *   | | | | | | |/ _ \/ __| __| | | |  _ \
 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
 *    \__\_\\__,_|\___||___/\__|____/|____/
 *
 *  Copyright (c) 2014-2019 Appsicle
 *  Copyright (c) 2019-2023 QuestDB
 *
 *  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 io.questdb;

import io.questdb.cairo.CairoConfiguration;
import io.questdb.cairo.CairoEngine;
import io.questdb.cairo.SqlJitMode;
import io.questdb.cairo.TableUtils;
import io.questdb.jit.JitUtil;
import io.questdb.log.Log;
import io.questdb.log.LogFactory;
import io.questdb.log.LogRecord;
import io.questdb.network.IODispatcherConfiguration;
import io.questdb.std.*;
import io.questdb.std.datetime.millitime.Dates;
import io.questdb.std.str.NativeLPSZ;
import io.questdb.std.str.Path;
import org.jetbrains.annotations.NotNull;
import sun.misc.Signal;

import java.io.*;
import java.net.*;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class Bootstrap {

    public static final String SWITCH_USE_DEFAULT_LOG_FACTORY_CONFIGURATION = "--use-default-log-factory-configuration";
    private static final String CONFIG_FILE = "/server.conf";
    private static final String LOG_NAME = "server-main";
    private static final String PUBLIC_VERSION_TXT = "version.txt";
    private static final String PUBLIC_ZIP = "/io/questdb/site/public.zip";
    private final String banner;
    private final BuildInformation buildInformation;
    private final ServerConfiguration config;
    private final Log log;
    private final Metrics metrics;
    private final String rootDirectory;

    public Bootstrap(String... args) {
        this(new PropBootstrapConfiguration(), args);
    }

    public Bootstrap(BootstrapConfiguration bootstrapConfiguration, String... args) {
        if (args.length < 2) {
            throw new BootstrapException("Root directory name expected (-d )");
        }
        banner = bootstrapConfiguration.getBanner();
        buildInformation = new BuildInformationHolder(bootstrapConfiguration.getClass());

        // non /server.conf properties
        final CharSequenceObjHashMap argsMap = processArgs(args);
        rootDirectory = argsMap.get("-d");
        if (Chars.isBlank(rootDirectory)) {
            throw new BootstrapException("Root directory name expected (-d )");
        }
        final File rootPath = new File(rootDirectory);
        if (!rootPath.exists()) {
            throw new BootstrapException("Root directory does not exist: " + rootDirectory);
        }

        if (argsMap.get("-n") == null && Os.type != Os.WINDOWS) {
            Signal.handle(new Signal("HUP"), signal -> { /* suppress HUP signal */ });
        }

        // before we set up the logger, we need to copy the conf file
        final byte[] buffer = new byte[1024 * 1024];
        try {
            copyConfResource(rootDirectory, false, buffer, "conf/log.conf", null);
        } catch (IOException e) {
            throw new BootstrapException("Could not extract log configuration file");
        }

        // setup logger
        // note: this call must be made before any Log init.
        if (argsMap.get(SWITCH_USE_DEFAULT_LOG_FACTORY_CONFIGURATION) == null) {
            LogFactory.configureRootDir(rootDirectory);
        }
        log = LogFactory.getLog(LOG_NAME);

        // report copyright and architecture
        log.advisoryW().$(buildInformation.getSwName()).$(' ').$(buildInformation.getSwVersion()).$(". Copyright (C) 2014-").$(Dates.getYear(System.currentTimeMillis())).$(", all rights reserved.").$();
        String archName;
        boolean isOsSupported = true;
        switch (Os.type) {
            case Os.WINDOWS:
                archName = "OS/Arch Windows/amd64";
                break;
            case Os.LINUX_AMD64:
                archName = "OS/Arch linux/amd64";
                break;
            case Os.OSX_AMD64:
                archName = "OS/Arch apple/amd64";
                break;
            case Os.OSX_ARM64:
                archName = "OS/Arch apple/apple-silicon";
                break;
            case Os.LINUX_ARM64:
                archName = "OS/Arch linux/arm64";
                break;
            case Os.FREEBSD:
                archName = "OS/ARCH freebsd/amd64";
                break;
            default:
                isOsSupported = false;
                archName = "Unsupported OS";
                break;
        }
        StringBuilder sb = new StringBuilder(Vect.getSupportedInstructionSetName());
        sb.setLength(sb.length() - 1); // remove ending ']'
        sb.append(", ").append(System.getProperty("sun.arch.data.model")).append(" bits");
        sb.append(", ").append(Runtime.getRuntime().availableProcessors()).append(" processors");
        if (isOsSupported) {
            log.advisoryW().$(archName).$(sb).I$();
        } else {
            log.criticalW().$(archName).$(sb).I$();
        }

        try {
            if (bootstrapConfiguration.useSite()) {
                // site
                extractSite();
            }

            ServerConfiguration configuration = bootstrapConfiguration.getServerConfiguration(this);
            if (configuration == null) {
                // /server.conf properties
                final Properties properties = loadProperties();
                final FilesFacade ffOverride = bootstrapConfiguration.getFilesFacade();
                if (ffOverride == null) {
                    config = new PropServerConfiguration(
                            rootDirectory,
                            properties,
                            bootstrapConfiguration.getEnv(),
                            log,
                            buildInformation
                    );
                } else {
                    config = new PropServerConfiguration(
                            rootDirectory,
                            properties,
                            bootstrapConfiguration.getEnv(),
                            log,
                            buildInformation
                    ) {
                        private CairoConfiguration cairoConf;

                        @Override
                        public CairoConfiguration getCairoConfiguration() {
                            if (cairoConf == null) {
                                cairoConf = new PropCairoConfiguration() {
                                    @Override
                                    public @NotNull FilesFacade getFilesFacade() {
                                        return ffOverride;
                                    }
                                };
                            }
                            return cairoConf;
                        }
                    };
                }
            } else {
                config = configuration;
            }
            reportValidateConfig();
            reportCrashFiles(config.getCairoConfiguration(), log);
        } catch (Throwable e) {
            log.errorW().$(e).$();
            throw new BootstrapException(e);
        }

        // metrics
        if (config.getMetricsConfiguration().isEnabled()) {
            metrics = Metrics.enabled();
        } else {
            metrics = Metrics.disabled();
            log.advisoryW().$("Metrics are disabled, health check endpoint will not consider unhandled errors").$();
        }
    }

    public static CharSequenceObjHashMap processArgs(String... args) {
        final int n = args.length;
        if (n == 0) {
            throw new BootstrapException("Arguments expected, non provided");
        }
        CharSequenceObjHashMap optHash = new CharSequenceObjHashMap<>();
        for (int i = 0; i < n; i++) {
            String arg = args[i];
            if (arg.length() > 1 && arg.charAt(0) == '-') {
                if (i + 1 < n) {
                    String nextArg = args[i + 1];
                    if (nextArg.length() > 1 && nextArg.charAt(0) == '-') {
                        optHash.put(arg, "");
                    } else {
                        optHash.put(arg, nextArg);
                        i++;
                    }
                } else {
                    optHash.put(arg, "");
                }
            } else {
                optHash.put("$" + i, arg);
            }
        }
        return optHash;
    }

    public static void reportCrashFiles(CairoConfiguration cairoConfiguration, Log log) {
        final CharSequence dbRoot = cairoConfiguration.getRoot();
        final FilesFacade ff = cairoConfiguration.getFilesFacade();
        final int maxFiles = cairoConfiguration.getMaxCrashFiles();
        NativeLPSZ name = new NativeLPSZ();
        try (Path path = new Path().of(dbRoot).slash$(); Path other = new Path().of(dbRoot).slash$()) {
            int plen = path.length();
            AtomicInteger counter = new AtomicInteger(0);
            FilesFacadeImpl.INSTANCE.iterateDir(path, (pUtf8NameZ, type) -> {
                if (Files.notDots(pUtf8NameZ)) {
                    name.of(pUtf8NameZ);
                    if (Chars.startsWith(name, cairoConfiguration.getOGCrashFilePrefix()) && type == Files.DT_FILE) {
                        path.trimTo(plen).concat(pUtf8NameZ).$();
                        boolean shouldRename = false;
                        do {
                            other.trimTo(plen).concat(cairoConfiguration.getArchivedCrashFilePrefix()).put(counter.getAndIncrement()).put(".log").$();
                            if (!ff.exists(other)) {
                                shouldRename = counter.get() <= maxFiles;
                                break;
                            }
                        } while (counter.get() < maxFiles);
                        if (shouldRename && ff.rename(path, other) == 0) {
                            log.criticalW().$("found crash file [path=").$(other).I$();
                        } else {
                            log.criticalW().$("could not rename crash file [path=").$(path).$(", errno=").$(ff.errno()).$(", index=").$(counter.get()).$(", max=").$(maxFiles).I$();
                        }
                    }
                }
            });
        }
    }

    public void extractSite() throws IOException {
        URL resource = ServerMain.class.getResource(PUBLIC_ZIP);
        if (resource == null) {
            log.infoW().$("Web Console build [").$(PUBLIC_ZIP).$("] not found").$();
        } else {
            long thisVersion = resource.openConnection().getLastModified();
            final String publicDir = rootDirectory + Files.SEPARATOR + "public";
            final byte[] buffer = new byte[1024 * 1024];

            boolean extracted = false;
            final String oldSwVersion = getPublicVersion(publicDir);
            final CharSequence newSwVersion = buildInformation.getSwVersion();
            if (oldSwVersion == null) {
                if (thisVersion != 0) {
                    extractSite0(publicDir, buffer, Long.toString(thisVersion));
                } else {
                    extractSite0(publicDir, buffer, Chars.toString(newSwVersion));
                }
                extracted = true;
            } else {
                // This is a hack to deal with RT package problem
                // in this package "thisVersion" is always 0, and we need to fall back
                // to the database version.
                if (thisVersion == 0) {
                    if (!Chars.equals(oldSwVersion, newSwVersion)) {
                        extractSite0(publicDir, buffer, Chars.toString(newSwVersion));
                        extracted = true;
                    }
                } else {
                    // it is possible that old version is the database version
                    // which means user might have switched from RT distribution to no-JVM on the same data dir
                    // in this case we might fail to parse the version string
                    try {
                        final long oldVersion = Numbers.parseLong(oldSwVersion);
                        if (thisVersion > oldVersion) {
                            extractSite0(publicDir, buffer, Long.toString(thisVersion));
                            extracted = true;
                        }
                    } catch (NumericException e) {
                        extractSite0(publicDir, buffer, Long.toString(thisVersion));
                        extracted = true;
                    }
                }
            }
            if (!extracted) {
                log.infoW().$("Web Console is up to date").$();
            }
        }
    }

    public String getBanner() {
        return banner;
    }

    public BuildInformation getBuildInformation() {
        return buildInformation;
    }

    public ServerConfiguration getConfiguration() {
        return config;
    }

    public Log getLog() {
        return log;
    }

    public Metrics getMetrics() {
        return metrics;
    }

    public String getRootDirectory() {
        return rootDirectory;
    }

    @NotNull
    public Properties loadProperties() throws IOException {
        final Properties properties = new Properties();
        java.nio.file.Path configFile = Paths.get(rootDirectory, PropServerConfiguration.CONFIG_DIRECTORY, CONFIG_FILE);
        log.advisoryW().$("Server config: ").$(configFile).$();

        try (InputStream is = java.nio.file.Files.newInputStream(configFile)) {
            properties.load(is);
        }
        return properties;
    }

    public CairoEngine newCairoEngine() {
        return new CairoEngine(getConfiguration().getCairoConfiguration(), getMetrics());
    }

    private static void copyConfResource(String dir, boolean force, byte[] buffer, String res, Log log) throws IOException {
        File out = new File(dir, res);
        try (InputStream is = ServerMain.class.getResourceAsStream("/io/questdb/site/" + res)) {
            if (is != null) {
                copyInputStream(force, buffer, out, is, log);
            }
        }
    }

    private static void copyInputStream(boolean force, byte[] buffer, File out, InputStream is, Log log) throws IOException {
        final boolean exists = out.exists();
        if (force || !exists) {
            File dir = out.getParentFile();
            if (!dir.exists() && !dir.mkdirs()) {
                if (log != null) {
                    log.errorW().$("could not create directory [path=").$(dir).I$();
                }
                return;
            }
            try (FileOutputStream fos = new FileOutputStream(out)) {
                int n;
                while ((n = is.read(buffer, 0, buffer.length)) > 0) {
                    fos.write(buffer, 0, n);
                }
            }
            if (log != null) {
                log.infoW().$("extracted [path=").$(out).I$();
            }
            return;
        }
        if (log != null) {
            log.debugW().$("skipped [path=").$(out).I$();
        }
    }

    private static String getPublicVersion(String publicDir) throws IOException {
        File f = new File(publicDir, PUBLIC_VERSION_TXT);
        if (f.exists()) {
            try (FileInputStream fis = new FileInputStream(f)) {
                byte[] buf = new byte[128];
                int len = fis.read(buf);
                return new String(buf, 0, len);
            }
        }
        return null;
    }

    private static void setPublicVersion(String publicDir, String version) throws IOException {
        File f = new File(publicDir, PUBLIC_VERSION_TXT);
        File publicFolder = f.getParentFile();
        if (!publicFolder.exists()) {
            if (!publicFolder.mkdirs()) {
                throw new BootstrapException("Cannot create folder: " + publicFolder);
            }
        }
        try (FileOutputStream fos = new FileOutputStream(f)) {
            byte[] buf = version.getBytes();
            fos.write(buf, 0, buf.length);
        }
    }

    private static void verifyFileOpts(Path path, CairoConfiguration cairoConfiguration) {
        final FilesFacade ff = cairoConfiguration.getFilesFacade();
        path.of(cairoConfiguration.getRoot()).concat("_verify_").put(cairoConfiguration.getRandom().nextPositiveInt()).put(".d").$();
        int fd = ff.openRW(path, cairoConfiguration.getWriterFileOpenOpts());
        try {
            if (fd > -1) {
                long mem = Unsafe.malloc(Long.BYTES, MemoryTag.NATIVE_DEFAULT);
                try {
                    TableUtils.writeLongOrFail(ff, fd, 0, 123456789L, mem, path);
                } finally {
                    Unsafe.free(mem, Long.BYTES, MemoryTag.NATIVE_DEFAULT);
                }
            }
        } finally {
            ff.close(fd);
        }
        ff.remove(path);
    }

    private void extractSite0(String publicDir, byte[] buffer, String thisVersion) throws IOException {
        try (final InputStream is = ServerMain.class.getResourceAsStream(PUBLIC_ZIP)) {
            if (is != null) {
                try (ZipInputStream zip = new ZipInputStream(is)) {
                    ZipEntry ze;
                    while ((ze = zip.getNextEntry()) != null) {
                        final File dest = new File(publicDir, ze.getName());
                        if (!ze.isDirectory()) {
                            copyInputStream(true, buffer, dest, zip, log);
                        }
                        zip.closeEntry();
                    }
                }
            } else {
                log.errorW().$("could not find site [resource=").$(PUBLIC_ZIP).$(']').$();
            }
        }
        setPublicVersion(publicDir, thisVersion);
        copyConfResource(rootDirectory, false, buffer, "conf/date.formats", log);
        try {
            copyConfResource(rootDirectory, true, buffer, "conf/mime.types", log);
        } catch (IOException exception) {
            // conf can be read-only, this is not critical
            if (exception.getMessage() == null || (!exception.getMessage().contains("Read-only file system") && !exception.getMessage().contains("Permission denied"))) {
                throw exception;
            }
        }
        copyConfResource(rootDirectory, false, buffer, "conf/server.conf", log);
        copyConfResource(rootDirectory, false, buffer, "conf/log.conf", log);
    }

    private void reportValidateConfig() {
        final boolean httpEnabled = config.getHttpServerConfiguration().isEnabled();
        final boolean httpReadOnly = config.getHttpServerConfiguration().getHttpContextConfiguration().readOnlySecurityContext();
        final String httpReadOnlyHint = httpEnabled && httpReadOnly ? " [read-only]" : "";
        final boolean pgEnabled = config.getPGWireConfiguration().isEnabled();
        final boolean pgReadOnly = config.getPGWireConfiguration().readOnlySecurityContext();
        final String pgReadOnlyHint = pgEnabled && pgReadOnly ? " [read-only]" : "";
        final CairoConfiguration cairoConfig = config.getCairoConfiguration();
        log.advisoryW().$("Config:").$();
        log.advisoryW().$(" - http.enabled : ").$(httpEnabled).$(httpReadOnlyHint).$();
        log.advisoryW().$(" - tcp.enabled  : ").$(config.getLineTcpReceiverConfiguration().isEnabled()).$();
        log.advisoryW().$(" - pg.enabled   : ").$(pgEnabled).$(pgReadOnlyHint).$();
        log.advisoryW().$(" - attach partition suffix: ").$(config.getCairoConfiguration().getAttachPartitionSuffix()).$();
        log.advisoryW().$(" - open database [id=").$(cairoConfig.getDatabaseIdLo()).$('.').$(cairoConfig.getDatabaseIdHi()).I$();
        if (cairoConfig.isReadOnlyInstance()) {
            log.advisoryW().$(" - THIS IS READ ONLY INSTANCE").$();
        }
        try (Path path = new Path()) {
            verifyFileSystem(path, cairoConfig.getRoot(), "db");
            verifyFileSystem(path, cairoConfig.getBackupRoot(), "backup");
            verifyFileSystem(path, cairoConfig.getSnapshotRoot(), "snapshot");
            verifyFileSystem(path, cairoConfig.getSqlCopyInputRoot(), "sql copy input");
            verifyFileSystem(path, cairoConfig.getSqlCopyInputWorkRoot(), "sql copy input worker");
            verifyFileOpts(path, cairoConfig);
            cairoConfig.getVolumeDefinitions().forEach((alias, volumePath) -> verifyFileSystem(path, volumePath, "create table allowed volume [" + alias + ']'));
        }
        if (JitUtil.isJitSupported()) {
            final int jitMode = cairoConfig.getSqlJitMode();
            switch (jitMode) {
                case SqlJitMode.JIT_MODE_ENABLED:
                    log.advisoryW().$(" - SQL JIT compiler mode: on").$();
                    break;
                case SqlJitMode.JIT_MODE_FORCE_SCALAR:
                    log.advisoryW().$(" - SQL JIT compiler mode: scalar").$();
                    break;
                case SqlJitMode.JIT_MODE_DISABLED:
                    log.advisoryW().$(" - SQL JIT compiler mode: off").$();
                    break;
                default:
                    log.errorW().$(" - Unknown SQL JIT compiler mode: ").$(jitMode).$();
                    break;
            }
        }
    }

    private void verifyFileSystem(Path path, CharSequence rootDir, String kind) {
        if (rootDir == null) {
            log.advisoryW().$(" - ").$(kind).$(" root: NOT SET").$();
            return;
        }
        path.of(rootDir).$();
        // path will contain file system name
        long fsStatus = Files.getFileSystemStatus(path);
        path.seekZ();
        LogRecord rec = log.advisoryW().$(" - ").$(kind).$(" root: [path=").$(rootDir).$(", magic=0x");
        if (fsStatus < 0 || (fsStatus == 0 && Os.type == Os.OSX_ARM64)) {
            rec.$hex(-fsStatus).$("] -> SUPPORTED").$();
        } else {
            rec.$hex(fsStatus).$("] -> UNSUPPORTED (SYSTEM COULD BE UNSTABLE)").$();
        }
    }

    static void logWebConsoleUrls(ServerConfiguration config, Log log, String banner) {
        if (config.getHttpServerConfiguration().isEnabled()) {
            final LogRecord r = log.infoW().$('\n').$(banner).$("Web Console URL(s):").$("\n\n");

            final IODispatcherConfiguration httpConf = config.getHttpServerConfiguration().getDispatcherConfiguration();
            final int bindIP = httpConf.getBindIPv4Address();
            final int bindPort = httpConf.getBindPort();
            if (bindIP == 0) {
                try {
                    for (Enumeration ni = NetworkInterface.getNetworkInterfaces(); ni.hasMoreElements(); ) {
                        for (Enumeration addr = ni.nextElement().getInetAddresses(); addr.hasMoreElements(); ) {
                            InetAddress inetAddress = addr.nextElement();
                            if (inetAddress instanceof Inet4Address) {
                                r.$('\t').$("http://").$(inetAddress.getHostAddress()).$(':').$(bindPort).$('\n');
                            }
                        }
                    }
                } catch (SocketException se) {
                    throw new Bootstrap.BootstrapException("Cannot access network interfaces");
                }
                r.$('\n').$();
            } else {
                r.$('\t').$("http://").$ip(bindIP).$(':').$(bindPort).$('\n').$();
            }
        }
    }

    public static class BootstrapException extends RuntimeException {
        public BootstrapException(String message) {
            super(message);
        }

        public BootstrapException(Throwable thr) {
            super(thr);
        }
    }

    static {
        if (Os.type == Os._32Bit) {
            throw new Error("QuestDB requires 64-bit JVM");
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy