org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager.java

Source

/*
 * 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.solr.core.snapshots;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.index.IndexDeletionPolicy;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.util.IOUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.core.DirectoryFactory;
import org.apache.solr.core.DirectoryFactory.DirContext;
import org.apache.solr.core.IndexDeletionPolicyWrapper;
import org.apache.solr.core.SolrCore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is responsible to manage the persistent snapshots meta-data for the Solr indexes. The
 * persistent snapshots are implemented by relying on Lucene {@linkplain IndexDeletionPolicy}
 * abstraction to configure a specific {@linkplain IndexCommit} to be retained. The
 * {@linkplain IndexDeletionPolicyWrapper} in Solr uses this class to create/delete the Solr index
 * snapshots.
 */
public class SolrSnapshotMetaDataManager {
    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
    public static final String SNAPSHOT_METADATA_DIR = "snapshot_metadata";

    /**
     * A class defining the meta-data for a specific snapshot.
     */
    public static class SnapshotMetaData {
        private String name;
        private String indexDirPath;
        private long generationNumber;

        public SnapshotMetaData(String name, String indexDirPath, long generationNumber) {
            super();
            this.name = name;
            this.indexDirPath = indexDirPath;
            this.generationNumber = generationNumber;
        }

        public String getName() {
            return name;
        }

        public String getIndexDirPath() {
            return indexDirPath;
        }

        public long getGenerationNumber() {
            return generationNumber;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("SnapshotMetaData[name=");
            builder.append(name);
            builder.append(", indexDirPath=");
            builder.append(indexDirPath);
            builder.append(", generation=");
            builder.append(generationNumber);
            builder.append("]");
            return builder.toString();
        }
    }

    /** Prefix used for the save file. */
    public static final String SNAPSHOTS_PREFIX = "snapshots_";
    private static final int VERSION_START = 0;
    private static final int VERSION_CURRENT = VERSION_START;
    private static final String CODEC_NAME = "solr-snapshots";

    // The index writer which maintains the snapshots metadata
    private long nextWriteGen;

    private final Directory dir;

    /** Used to map snapshot name to snapshot meta-data. */
    protected final Map<String, SnapshotMetaData> nameToDetailsMapping = new LinkedHashMap<>();
    /** Used to figure out the *current* index data directory path */
    private final SolrCore solrCore;

    /**
     * A constructor.
     *
     * @param dir The directory where the snapshot meta-data should be stored. Enables updating
     *            the existing meta-data.
     * @throws IOException in case of errors.
     */
    public SolrSnapshotMetaDataManager(SolrCore solrCore, Directory dir) throws IOException {
        this(solrCore, dir, OpenMode.CREATE_OR_APPEND);
    }

    /**
     * A constructor.
     *
     * @param dir The directory where the snapshot meta-data is stored.
     * @param mode CREATE If previous meta-data should be erased.
     *             APPEND If previous meta-data should be read and updated.
     *             CREATE_OR_APPEND Creates a new meta-data structure if one does not exist
     *                              Updates the existing structure if one exists.
     * @throws IOException in case of errors.
     */
    public SolrSnapshotMetaDataManager(SolrCore solrCore, Directory dir, OpenMode mode) throws IOException {
        this.solrCore = solrCore;
        this.dir = dir;

        if (mode == OpenMode.CREATE) {
            deleteSnapshotMetadataFiles();
        }

        loadFromSnapshotMetadataFile();

        if (mode == OpenMode.APPEND && nextWriteGen == 0) {
            throw new IllegalStateException("no snapshots stored in this directory");
        }
    }

    /**
     * @return The snapshot meta-data directory
     */
    public Directory getSnapshotsDir() {
        return dir;
    }

    /**
     * This method creates a new snapshot meta-data entry.
     *
     * @param name The name of the snapshot.
     * @param indexDirPath The directory path where the index files are stored.
     * @param gen The generation number for the {@linkplain IndexCommit} being snapshotted.
     * @throws IOException in case of I/O errors.
     */
    public synchronized void snapshot(String name, String indexDirPath, long gen) throws IOException {
        Objects.requireNonNull(name);

        log.info(
                "Creating the snapshot named {} for core {} associated with index commit with generation {} in directory {}",
                name, solrCore.getName(), gen, indexDirPath);

        if (nameToDetailsMapping.containsKey(name)) {
            throw new SolrException(ErrorCode.BAD_REQUEST, "A snapshot with name " + name + " already exists");
        }

        SnapshotMetaData d = new SnapshotMetaData(name, indexDirPath, gen);
        nameToDetailsMapping.put(name, d);

        boolean success = false;
        try {
            persist();
            success = true;
        } finally {
            if (!success) {
                try {
                    release(name);
                } catch (Exception e) {
                    // Suppress so we keep throwing original exception
                }
            }
        }
    }

    /**
     * This method deletes a previously created snapshot (if any).
     *
     * @param name The name of the snapshot to be deleted.
     * @return The snapshot meta-data if the snapshot with the snapshot name exists.
     * @throws IOException in case of I/O error
     */
    public synchronized Optional<SnapshotMetaData> release(String name) throws IOException {
        log.info("Deleting the snapshot named {} for core {}", name, solrCore.getName());
        SnapshotMetaData result = nameToDetailsMapping.remove(Objects.requireNonNull(name));
        if (result != null) {
            boolean success = false;
            try {
                persist();
                success = true;
            } finally {
                if (!success) {
                    nameToDetailsMapping.put(name, result);
                }
            }
        }
        return Optional.ofNullable(result);
    }

    /**
     * This method returns if snapshot is created for the specified generation number in
     * the *current* index directory.
     *
     * @param genNumber The generation number for the {@linkplain IndexCommit} to be checked.
     * @return true if the snapshot is created.
     *         false otherwise.
     */
    public synchronized boolean isSnapshotted(long genNumber) {
        return !nameToDetailsMapping.isEmpty() && isSnapshotted(solrCore.getIndexDir(), genNumber);
    }

    /**
     * This method returns if snapshot is created for the specified generation number in
     * the specified index directory.
     *
     * @param genNumber The generation number for the {@linkplain IndexCommit} to be checked.
     * @return true if the snapshot is created.
     *         false otherwise.
     */
    public synchronized boolean isSnapshotted(String indexDirPath, long genNumber) {
        return !nameToDetailsMapping.isEmpty() && nameToDetailsMapping.values().stream().anyMatch(
                entry -> entry.getIndexDirPath().equals(indexDirPath) && entry.getGenerationNumber() == genNumber);
    }

    /**
     * This method returns the snapshot meta-data for the specified name (if it exists).
     *
     * @param name The name of the snapshot
     * @return The snapshot meta-data if exists.
     */
    public synchronized Optional<SnapshotMetaData> getSnapshotMetaData(String name) {
        return Optional.ofNullable(nameToDetailsMapping.get(name));
    }

    /**
     * @return A list of snapshots created so far.
     */
    public synchronized List<String> listSnapshots() {
        // We create a copy for thread safety.
        return new ArrayList<>(nameToDetailsMapping.keySet());
    }

    /**
     * This method returns a list of snapshots created in a specified index directory.
     *
     * @param indexDirPath The index directory path.
     * @return a list snapshots stored in the specified directory.
     */
    public synchronized Collection<SnapshotMetaData> listSnapshotsInIndexDir(String indexDirPath) {
        return nameToDetailsMapping.values().stream().filter(entry -> indexDirPath.equals(entry.getIndexDirPath()))
                .collect(Collectors.toList());
    }

    /**
     * This method returns the {@linkplain IndexCommit} associated with the specified
     * <code>commitName</code>. A snapshot with specified <code>commitName</code> must
     * be created before invoking this method.
     *
     * @param commitName The name of persisted commit
     * @return the {@linkplain IndexCommit}
     * @throws IOException in case of I/O error.
     */
    public Optional<IndexCommit> getIndexCommitByName(String commitName) throws IOException {
        Optional<IndexCommit> result = Optional.empty();
        Optional<SnapshotMetaData> metaData = getSnapshotMetaData(commitName);
        if (metaData.isPresent()) {
            String indexDirPath = metaData.get().getIndexDirPath();
            long gen = metaData.get().getGenerationNumber();

            Directory d = solrCore.getDirectoryFactory().get(indexDirPath, DirContext.DEFAULT,
                    DirectoryFactory.LOCK_TYPE_NONE);
            try {
                result = DirectoryReader.listCommits(d).stream().filter(ic -> ic.getGeneration() == gen).findAny();

                if (!result.isPresent()) {
                    log.warn("Unable to find commit with generation {} in the directory {}", gen, indexDirPath);
                }

            } finally {
                solrCore.getDirectoryFactory().release(d);
            }
        } else {
            log.warn("Commit with name {} is not persisted for core {}", commitName, solrCore.getName());
        }

        return result;
    }

    private synchronized void persist() throws IOException {
        String fileName = SNAPSHOTS_PREFIX + nextWriteGen;
        IndexOutput out = dir.createOutput(fileName, IOContext.DEFAULT);
        boolean success = false;
        try {
            CodecUtil.writeHeader(out, CODEC_NAME, VERSION_CURRENT);
            out.writeVInt(nameToDetailsMapping.size());
            for (Entry<String, SnapshotMetaData> ent : nameToDetailsMapping.entrySet()) {
                out.writeString(ent.getKey());
                out.writeString(ent.getValue().getIndexDirPath());
                out.writeVLong(ent.getValue().getGenerationNumber());
            }
            success = true;
        } finally {
            if (!success) {
                IOUtils.closeWhileHandlingException(out);
                IOUtils.deleteFilesIgnoringExceptions(dir, fileName);
            } else {
                IOUtils.close(out);
            }
        }

        dir.sync(Collections.singletonList(fileName));

        if (nextWriteGen > 0) {
            String lastSaveFile = SNAPSHOTS_PREFIX + (nextWriteGen - 1);
            // exception OK: likely it didn't exist
            IOUtils.deleteFilesIgnoringExceptions(dir, lastSaveFile);
        }

        nextWriteGen++;
    }

    private synchronized void deleteSnapshotMetadataFiles() throws IOException {
        for (String file : dir.listAll()) {
            if (file.startsWith(SNAPSHOTS_PREFIX)) {
                dir.deleteFile(file);
            }
        }
    }

    /**
     * Reads the snapshot meta-data information from the given {@link Directory}.
     */
    private synchronized void loadFromSnapshotMetadataFile() throws IOException {
        log.debug("Loading from snapshot metadata file...");
        long genLoaded = -1;
        IOException ioe = null;
        List<String> snapshotFiles = new ArrayList<>();
        for (String file : dir.listAll()) {
            if (file.startsWith(SNAPSHOTS_PREFIX)) {
                long gen = Long.parseLong(file.substring(SNAPSHOTS_PREFIX.length()));
                if (genLoaded == -1 || gen > genLoaded) {
                    snapshotFiles.add(file);
                    Map<String, SnapshotMetaData> snapshotMetaDataMapping = new HashMap<>();
                    IndexInput in = dir.openInput(file, IOContext.DEFAULT);
                    try {
                        CodecUtil.checkHeader(in, CODEC_NAME, VERSION_START, VERSION_START);
                        int count = in.readVInt();
                        for (int i = 0; i < count; i++) {
                            String name = in.readString();
                            String indexDirPath = in.readString();
                            long commitGen = in.readVLong();
                            snapshotMetaDataMapping.put(name, new SnapshotMetaData(name, indexDirPath, commitGen));
                        }
                    } catch (IOException ioe2) {
                        // Save first exception & throw in the end
                        if (ioe == null) {
                            ioe = ioe2;
                        }
                    } finally {
                        in.close();
                    }

                    genLoaded = gen;
                    nameToDetailsMapping.clear();
                    nameToDetailsMapping.putAll(snapshotMetaDataMapping);
                }
            }
        }

        if (genLoaded == -1) {
            // Nothing was loaded...
            if (ioe != null) {
                // ... not for lack of trying:
                throw ioe;
            }
        } else {
            if (snapshotFiles.size() > 1) {
                // Remove any broken / old snapshot files:
                String curFileName = SNAPSHOTS_PREFIX + genLoaded;
                for (String file : snapshotFiles) {
                    if (!curFileName.equals(file)) {
                        IOUtils.deleteFilesIgnoringExceptions(dir, file);
                    }
                }
            }
            nextWriteGen = 1 + genLoaded;
        }
    }
}