Storage.java

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 org.apache.doris.persist;

import org.apache.doris.ha.FrontendNodeType;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.UUID;

// VERSION file contains clusterId. eg:
//      clusterId=123456
// ROLE file contains FrontendNodeType and NodeName. eg:
//      role=OBSERVER
//      name=172.0.0.1_1234_DNwid284dasdwd
public class Storage {
    private static final Logger LOG = LogManager.getLogger(Storage.class);

    public static final String CLUSTER_ID = "clusterId";
    public static final String TOKEN = "token";
    public static final String FRONTEND_ROLE = "role";
    public static final String NODE_NAME = "name";
    public static final String EDITS = "edits";
    public static final String IMAGE = "image";
    public static final String IMAGE_NEW = "image.ckpt";
    public static final String VERSION_FILE = "VERSION";
    public static final String ROLE_FILE = "ROLE";
    public static final String DEPLOY_MODE_FILE = "DEPLOY_MODE";
    public static final String DEPLOY_MODE = "deploy_mode";
    public static final String CLOUD_MODE = "cloud";
    public static final String LOCAL_MODE = "local";

    private int clusterID = 0;
    private String token;
    private FrontendNodeType role = FrontendNodeType.UNKNOWN;
    private String nodeName;
    private String deployMode;
    private long editsSeq;
    private long latestImageSeq = 0;
    private long latestValidatedImageSeq = 0;
    private String metaDir;
    private List<Long> editsFileSequenceNumbers;

    public Storage(int clusterID, String token, String metaDir) {
        this.clusterID = clusterID;
        this.token = token;
        this.metaDir = metaDir;
    }

    public Storage(int clusterID, String token, long latestImageSeq, long editsSeq, String metaDir) {
        this.clusterID = clusterID;
        this.token = token;
        this.editsSeq = editsSeq;
        this.latestImageSeq = latestImageSeq;
        this.metaDir = metaDir;
    }

    public Storage(String metaDir) throws IOException {
        this.editsFileSequenceNumbers = new ArrayList<Long>();
        this.metaDir = metaDir;

        reload();
    }

    public List<Long> getEditsFileSequenceNumbers() {
        Collections.sort(editsFileSequenceNumbers);
        return this.editsFileSequenceNumbers;
    }

    public void reload() throws IOException {
        // Read version file info
        Properties prop = new Properties();
        File versionFile = getVersionFile();
        if (versionFile.isFile()) {
            try (FileInputStream in = new FileInputStream(versionFile)) {
                prop.load(in);
            }
            clusterID = Integer.parseInt(prop.getProperty(CLUSTER_ID));
            if (prop.getProperty(TOKEN) != null) {
                token = prop.getProperty(TOKEN);
            }
        }

        File roleFile = getRoleFile();
        if (roleFile.isFile()) {
            try (FileInputStream in = new FileInputStream(roleFile)) {
                prop.load(in);
            }
            role = FrontendNodeType.valueOf(prop.getProperty(FRONTEND_ROLE));
            // For compatibility, NODE_NAME may not exist in ROLE file, set nodeName to null
            nodeName = prop.getProperty(NODE_NAME, null);
        }

        File modeFile = getModeFile();
        if (modeFile.isFile()) {
            try (FileInputStream in = new FileInputStream(modeFile)) {
                prop.load(in);
            }
            deployMode = prop.getProperty(DEPLOY_MODE);
        }

        // Find the latest two images
        File dir = new File(metaDir);
        File[] children = dir.listFiles();
        if (children == null) {
            return;
        }
        List<Long> imageIds = Lists.newArrayList();
        for (File child : children) {
            String name = child.getName();
            try {
                if (!name.equals(EDITS) && !name.equals(IMAGE_NEW)
                        && !name.endsWith(".part") && name.contains(".")) {
                    if (name.startsWith(IMAGE)) {
                        long fileSeq = Long.parseLong(name.substring(name.lastIndexOf('.') + 1));
                        imageIds.add(fileSeq);
                        if (latestImageSeq < fileSeq) {
                            latestImageSeq = fileSeq;
                        }
                    } else if (name.startsWith(EDITS)) {
                        // Just record the sequence part of the file name
                        editsFileSequenceNumbers.add(Long.parseLong(name.substring(name.lastIndexOf('.') + 1)));
                        editsSeq = Math.max(Long.parseLong(name.substring(name.lastIndexOf('.') + 1)), editsSeq);
                    }
                }
            } catch (Exception e) {
                LOG.warn(name + " is not a validate meta file, ignore it");
            }
        }
        // set latestValidatedImageSeq to the second largest image id, or 0 if less than 2 images.
        Collections.sort(imageIds);
        latestValidatedImageSeq = imageIds.size() < 2 ? 0 : imageIds.get(imageIds.size() - 2);
    }

    public int getClusterID() {
        return clusterID;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public FrontendNodeType getRole() {
        return role;
    }

    public String getDeployMode() {
        return deployMode;
    }

    public void setDeployMode(String deployMode) {
        this.deployMode = deployMode;
    }

    public String getNodeName() {
        return nodeName;
    }

    public String getMetaDir() {
        return metaDir;
    }

    public long getLatestImageSeq() {
        return latestImageSeq;
    }

    public long getLatestValidatedImageSeq() {
        return latestValidatedImageSeq;
    }

    public long getEditsSeq() {
        return editsSeq;
    }

    public static int newClusterID() {
        Random random = new SecureRandom();

        int newID = 0;
        while (newID == 0) {
            newID = random.nextInt(0x7FFFFFFF);
        }
        return newID;
    }

    public static String newToken() {
        return UUID.randomUUID().toString();
    }

    private void setFields(Properties properties) throws IOException {
        Preconditions.checkState(clusterID > 0);
        properties.setProperty(CLUSTER_ID, String.valueOf(clusterID));

        if (!Strings.isNullOrEmpty(token)) {
            properties.setProperty(TOKEN, token);
        }
    }

    public void writeClusterIdAndToken() throws IOException {
        Properties properties = new Properties();
        setFields(properties);

        writePropertiesToFile(properties, VERSION_FILE);
    }

    public void writeFrontendRoleAndNodeName(FrontendNodeType role, String nameNode) throws IOException {
        Preconditions.checkState(!Strings.isNullOrEmpty(nameNode));
        Properties properties = new Properties();
        properties.setProperty(FRONTEND_ROLE, role.name());
        properties.setProperty(NODE_NAME, nameNode);

        writePropertiesToFile(properties, ROLE_FILE);
    }

    public void writeClusterMode() throws IOException {
        Properties properties = new Properties();
        properties.setProperty(DEPLOY_MODE, deployMode);
        writePropertiesToFile(properties, DEPLOY_MODE_FILE);
    }

    private void writePropertiesToFile(Properties properties, String fileName) throws IOException {
        RandomAccessFile file = new RandomAccessFile(new File(metaDir, fileName), "rws");
        FileOutputStream out = null;

        try {
            file.seek(0);
            out = new FileOutputStream(file.getFD());
            properties.store(out, null);
            file.setLength(out.getChannel().position());
        } finally {
            if (out != null) {
                out.close();
            }
            file.close();
        }
    }

    // Only for test
    public void clear() throws IOException {
        File metaFile = new File(metaDir);
        if (metaFile.exists()) {
            String[] children = metaFile.list();
            if (children != null) {
                for (String child : children) {
                    File file = new File(metaFile, child);
                    file.delete();
                }
            }
            metaFile.delete();
        }

        if (!metaFile.mkdirs()) {
            throw new IOException("Cannot create directory " + metaFile);
        }
    }

    public static void rename(File from, File to) throws IOException {
        if (!from.renameTo(to)) {
            throw new IOException("Failed to rename  " + from.getCanonicalPath()
                    + " to " + to.getCanonicalPath());
        }
    }

    public File getCurrentImageFile() {
        return getImageFile(latestImageSeq);
    }

    public File getImageFile(long version) {
        return getImageFile(new File(metaDir), version);
    }

    public static File getImageFile(File dir, long version) {
        return new File(dir, IMAGE + "." + version);
    }

    public final File getVersionFile() {
        return new File(metaDir, VERSION_FILE);
    }

    public final File getRoleFile() {
        return new File(metaDir, ROLE_FILE);
    }

    public final File getModeFile() {
        return new File(metaDir, DEPLOY_MODE_FILE);
    }

    public File getCurrentEditsFile() {
        return new File(metaDir, EDITS);
    }

    public static File getCurrentEditsFile(File dir) {
        return new File(dir, EDITS);
    }

    public File getEditsFile(long seq) {
        return getEditsFile(new File(metaDir), seq);
    }

    public static File getEditsFile(File dir, long seq) {
        return new File(dir, EDITS + "." + seq);
    }

    public static long getMetaSeq(File file) {
        String filename = file.getName();
        return Long.parseLong(filename.substring(filename.lastIndexOf('.') + 1));
    }

}