| // Copyright (C) 2013 The Android Open Source Project |
| // |
| // 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 com.google.gerrit.lucene; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.primitives.Ints; |
| import com.google.gerrit.extensions.events.LifecycleListener; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.config.SitePaths; |
| import com.google.gerrit.server.index.ChangeSchemas; |
| import com.google.gerrit.server.index.IndexCollection; |
| import com.google.gerrit.server.index.Schema; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.inject.Inject; |
| import com.google.inject.ProvisionException; |
| import com.google.inject.Singleton; |
| |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.storage.file.FileBasedConfig; |
| import org.eclipse.jgit.util.FS; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.TreeMap; |
| |
| @Singleton |
| public class LuceneVersionManager implements LifecycleListener { |
| private static final Logger log = LoggerFactory |
| .getLogger(LuceneVersionManager.class); |
| |
| private static final String CHANGES_PREFIX = "changes_"; |
| |
| private static class Version { |
| private final Schema<ChangeData> schema; |
| private final int version; |
| private final boolean exists; |
| private final boolean ready; |
| |
| private Version(Schema<ChangeData> schema, int version, boolean exists, |
| boolean ready) { |
| checkArgument(schema == null || schema.getVersion() == version); |
| this.schema = schema; |
| this.version = version; |
| this.exists = exists; |
| this.ready = ready; |
| } |
| } |
| |
| static Path getDir(SitePaths sitePaths, Schema<ChangeData> schema) { |
| return sitePaths.index_dir.resolve(String.format("%s%04d", |
| CHANGES_PREFIX, schema.getVersion())); |
| } |
| |
| static FileBasedConfig loadGerritIndexConfig(SitePaths sitePaths) |
| throws ConfigInvalidException, IOException { |
| FileBasedConfig cfg = new FileBasedConfig( |
| sitePaths.index_dir.resolve("gerrit_index.config").toFile(), |
| FS.detect()); |
| cfg.load(); |
| return cfg; |
| } |
| |
| static void setReady(Config cfg, int version, boolean ready) { |
| cfg.setBoolean("index", Integer.toString(version), "ready", ready); |
| } |
| |
| private static boolean getReady(Config cfg, int version) { |
| return cfg.getBoolean("index", Integer.toString(version), "ready", false); |
| } |
| |
| private final SitePaths sitePaths; |
| private final LuceneChangeIndex.Factory indexFactory; |
| private final IndexCollection indexes; |
| private final OnlineReindexer.Factory reindexerFactory; |
| private final boolean onlineUpgrade; |
| private OnlineReindexer reindexer; |
| |
| @Inject |
| LuceneVersionManager( |
| @GerritServerConfig Config cfg, |
| SitePaths sitePaths, |
| LuceneChangeIndex.Factory indexFactory, |
| IndexCollection indexes, |
| OnlineReindexer.Factory reindexerFactory) { |
| this.sitePaths = sitePaths; |
| this.indexFactory = indexFactory; |
| this.indexes = indexes; |
| this.reindexerFactory = reindexerFactory; |
| this.onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true); |
| } |
| |
| @Override |
| public void start() { |
| FileBasedConfig cfg; |
| try { |
| cfg = loadGerritIndexConfig(sitePaths); |
| } catch (ConfigInvalidException | IOException e) { |
| throw fail(e); |
| } |
| |
| if (!Files.exists(sitePaths.index_dir)) { |
| throw new ProvisionException("No index versions ready; run Reindex"); |
| } else if (!Files.exists(sitePaths.index_dir)) { |
| log.warn("Not a directory: %s", sitePaths.index_dir.toAbsolutePath()); |
| throw new ProvisionException("No index versions ready; run Reindex"); |
| } |
| |
| TreeMap<Integer, Version> versions = scanVersions(cfg); |
| // Search from the most recent ready version. |
| // Write to the most recent ready version and the most recent version. |
| Version search = null; |
| List<Version> write = Lists.newArrayListWithCapacity(2); |
| for (Version v : versions.descendingMap().values()) { |
| if (v.schema == null) { |
| continue; |
| } |
| if (write.isEmpty() && onlineUpgrade) { |
| write.add(v); |
| } |
| if (v.ready) { |
| search = v; |
| if (!write.contains(v)) { |
| write.add(v); |
| } |
| break; |
| } |
| } |
| if (search == null) { |
| throw new ProvisionException("No index versions ready; run Reindex"); |
| } |
| |
| markNotReady(cfg, versions.values(), write); |
| LuceneChangeIndex searchIndex = indexFactory.create(search.schema, null); |
| indexes.setSearchIndex(searchIndex); |
| for (Version v : write) { |
| if (v.schema != null) { |
| if (v.version != search.version) { |
| indexes.addWriteIndex(indexFactory.create(v.schema, null)); |
| } else { |
| indexes.addWriteIndex(searchIndex); |
| } |
| } |
| } |
| |
| int latest = write.get(0).version; |
| if (onlineUpgrade && latest != search.version) { |
| reindexer = reindexerFactory.create(latest); |
| reindexer.start(); |
| } |
| } |
| |
| /** |
| * Start the online reindexer if the current index is not already the latest. |
| * |
| * @return true if started, otherwise false. |
| * @throws ReindexerAlreadyRunningException |
| */ |
| public synchronized boolean startReindexer() |
| throws ReindexerAlreadyRunningException { |
| validateReindexerNotRunning(); |
| if (!isCurrentIndexVersionLatest()) { |
| reindexer.start(); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Activate the latest index if the current index is not already the latest. |
| * |
| * @return true if index was activate, otherwise false. |
| * @throws ReindexerAlreadyRunningException |
| */ |
| public synchronized boolean activateLatestIndex() |
| throws ReindexerAlreadyRunningException { |
| validateReindexerNotRunning(); |
| if (!isCurrentIndexVersionLatest()) { |
| reindexer.activateIndex(); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isCurrentIndexVersionLatest() { |
| return reindexer == null |
| || reindexer.getVersion() == indexes.getSearchIndex().getSchema() |
| .getVersion(); |
| } |
| |
| private void validateReindexerNotRunning() |
| throws ReindexerAlreadyRunningException { |
| if (reindexer != null && reindexer.isRunning()) { |
| throw new ReindexerAlreadyRunningException(); |
| } |
| } |
| |
| private TreeMap<Integer, Version> scanVersions(Config cfg) { |
| TreeMap<Integer, Version> versions = Maps.newTreeMap(); |
| for (Schema<ChangeData> schema : ChangeSchemas.ALL.values()) { |
| Path p = getDir(sitePaths, schema); |
| boolean isDir = Files.isDirectory(p); |
| if (Files.exists(p) && !isDir) { |
| log.warn("Not a directory: %s", p.toAbsolutePath()); |
| } |
| int v = schema.getVersion(); |
| versions.put(v, new Version(schema, v, isDir, getReady(cfg, v))); |
| } |
| |
| try (DirectoryStream<Path> paths = |
| Files.newDirectoryStream(sitePaths.index_dir)) { |
| for (Path p : paths) { |
| String n = p.getFileName().toString(); |
| if (!n.startsWith(CHANGES_PREFIX)) { |
| continue; |
| } |
| String versionStr = n.substring(CHANGES_PREFIX.length()); |
| Integer v = Ints.tryParse(versionStr); |
| if (v == null || versionStr.length() != 4) { |
| log.warn("Unrecognized version in index directory: {}", |
| p.toAbsolutePath()); |
| continue; |
| } |
| if (!versions.containsKey(v)) { |
| versions.put(v, new Version(null, v, true, getReady(cfg, v))); |
| } |
| } |
| } catch (IOException e) { |
| log.error("Error scanning index directory: " + sitePaths.index_dir, e); |
| } |
| return versions; |
| } |
| |
| private void markNotReady(FileBasedConfig cfg, Iterable<Version> versions, |
| Collection<Version> inUse) { |
| boolean dirty = false; |
| for (Version v : versions) { |
| if (!inUse.contains(v) && v.exists) { |
| setReady(cfg, v.version, false); |
| dirty = true; |
| } |
| } |
| if (dirty) { |
| try { |
| cfg.save(); |
| } catch (IOException e) { |
| throw fail(e); |
| } |
| } |
| } |
| |
| private ProvisionException fail(Throwable t) { |
| ProvisionException e = new ProvisionException("Error scanning indexes"); |
| e.initCause(t); |
| throw e; |
| } |
| |
| @Override |
| public void stop() { |
| // Do nothing; indexes are closed on demand by IndexCollection. |
| } |
| } |