blob: b78b5a62333305a75cf962d588a9ea75b89b17e7 [file] [log] [blame]
// Copyright (C) 2009 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.server.schema;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.UsedAt;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gwtorm.jdbc.JdbcExecutor;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.gwtorm.server.StatementExecutor;
import com.google.inject.Provider;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.internal.storage.file.GC;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.storage.pack.PackConfig;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
public static final Class<Schema_170> C = Schema_170.class;
public static int getBinaryVersion() {
return guessVersion(C);
}
private static final ExecutorService backgroundGcThread = Executors.newFixedThreadPool(1);
private final Provider<? extends SchemaVersion> prior;
private final int versionNbr;
private static SortedSet<Project.NameKey> projects;
protected SchemaVersion(Provider<? extends SchemaVersion> prior) {
this.prior = prior;
this.versionNbr = guessVersion(getClass());
}
@UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
public static int guessVersion(Class<?> c) {
String n = c.getName();
n = n.substring(n.lastIndexOf('_') + 1);
while (n.startsWith("0")) {
n = n.substring(1);
}
return Integer.parseInt(n);
}
/** @return the {@link CurrentSchemaVersion#versionNbr} this step targets. */
public final int getVersionNbr() {
return versionNbr;
}
@VisibleForTesting
public final SchemaVersion getPrior() {
return prior.get();
}
public final void check(UpdateUI ui, CurrentSchemaVersion curr, SchemaFactory<ReviewDb> schema)
throws OrmException, SQLException {
if (curr.versionNbr == versionNbr) {
// Nothing to do, we are at the correct schema.
} else if (curr.versionNbr > versionNbr) {
throw new OrmException(
"Cannot downgrade database schema from version "
+ curr.versionNbr
+ " to "
+ versionNbr
+ ".");
} else {
upgradeFrom(ui, curr, schema);
}
}
/** Runs check on the prior schema version, and then upgrades. */
private void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, SchemaFactory<ReviewDb> schema)
throws OrmException, SQLException {
List<SchemaVersion> pending = pending(curr.versionNbr);
try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) {
updateSchema(pending, ui, db);
}
migrateData(pending, ui, curr, schema);
try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) {
JdbcSchema s = (JdbcSchema) db;
List<String> pruneList = new ArrayList<>();
s.pruneSchema(
new StatementExecutor() {
@Override
public void execute(String sql) {
pruneList.add(sql);
}
@Override
public void close() {
// Do nothing.
}
});
try (JdbcExecutor e = new JdbcExecutor(s)) {
if (!pruneList.isEmpty()) {
ui.pruneSchema(e, pruneList);
}
}
}
}
private List<SchemaVersion> pending(int curr) {
List<SchemaVersion> r = Lists.newArrayListWithCapacity(versionNbr - curr);
for (SchemaVersion v = this; curr < v.getVersionNbr(); v = v.prior.get()) {
r.add(v);
}
Collections.reverse(r);
return r;
}
private void updateSchema(List<SchemaVersion> pending, UpdateUI ui, ReviewDb db)
throws OrmException, SQLException {
for (SchemaVersion v : pending) {
ui.message(String.format("Upgrading schema to %d ...", v.getVersionNbr()));
v.preUpdateSchema(db);
}
JdbcSchema s = (JdbcSchema) db;
try (JdbcExecutor e = new JdbcExecutor(s)) {
s.updateSchema(e);
}
}
/**
* Invoked before updateSchema adds new columns/tables.
*
* @param db open database handle.
* @throws OrmException if a Gerrit-specific exception occurred.
* @throws SQLException if an underlying SQL exception occurred.
*/
protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {}
private void migrateData(
List<SchemaVersion> pending,
UpdateUI ui,
CurrentSchemaVersion curr,
SchemaFactory<ReviewDb> schema)
throws OrmException, SQLException {
for (SchemaVersion v : pending) {
Stopwatch sw = Stopwatch.createStarted();
ui.message(String.format("Migrating data to schema %d ...", v.getVersionNbr()));
try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) {
v.migrateData(db, ui);
}
try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) {
v.finish(curr, db);
}
ui.message(String.format("\t> Done (%.3f s)", sw.elapsed(TimeUnit.MILLISECONDS) / 1000d));
}
}
/**
* Invoked between updateSchema (adds new columns/tables) and pruneSchema (removes deleted
* columns/tables).
*
* @param db open database handle.
* @param ui interface for interacting with the user.
* @throws OrmException if a Gerrit-specific exception occurred.
* @throws SQLException if an underlying SQL exception occurred.
*/
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {}
/** Mark the current schema version. */
protected void finish(CurrentSchemaVersion curr, ReviewDb db) throws OrmException {
curr.versionNbr = versionNbr;
db.schemaVersion().update(Collections.singleton(curr));
}
/** Rename an existing table. */
protected static void renameTable(ReviewDb db, String from, String to) throws OrmException {
JdbcSchema s = (JdbcSchema) db;
try (JdbcExecutor e = new JdbcExecutor(s)) {
s.renameTable(e, from, to);
}
}
/** Rename an existing column. */
protected static void renameColumn(ReviewDb db, String table, String from, String to)
throws OrmException {
JdbcSchema s = (JdbcSchema) db;
try (JdbcExecutor e = new JdbcExecutor(s)) {
s.renameColumn(e, table, from, to);
}
}
/** Execute an SQL statement. */
protected static void execute(ReviewDb db, String sql) throws SQLException {
try (Statement s = newStatement(db)) {
s.execute(sql);
}
}
/** Open a new single statement. */
protected static Statement newStatement(ReviewDb db) throws SQLException {
return ((JdbcSchema) db).getConnection().createStatement();
}
/** Open a new prepared statement. */
protected static PreparedStatement prepareStatement(ReviewDb db, String sql) throws SQLException {
return ((JdbcSchema) db).getConnection().prepareStatement(sql);
}
/** Open a new statement executor. */
protected static JdbcExecutor newExecutor(ReviewDb db) throws OrmException {
return new JdbcExecutor(((JdbcSchema) db).getConnection());
}
protected static SortedSet<Project.NameKey> getSortedProjectsFromCache(
GitRepositoryManager repoManager) {
// TODO: Use Injection/Supplier pattern
if (projects == null) {
projects = repoManager.list();
}
return projects;
}
protected int getThreads() {
return Runtime.getRuntime().availableProcessors();
}
protected ExecutorService createExecutor(UpdateUI ui) {
int threads = getThreads();
ui.message(String.format("... using %d threads ...", threads));
return Executors.newFixedThreadPool(threads);
}
@FunctionalInterface
protected interface ThrowingFunction<I, T> {
default T accept(I input) {
try {
return acceptWithThrow(input);
} catch (OrmException | IOException e) {
throw new RuntimeException(e);
}
}
T acceptWithThrow(I input) throws OrmException, IOException;
}
protected Collection<?> runParallelTasks(
ExecutorService executor, Collection<?> lst, ThrowingFunction task, UpdateUI ui) {
Collection<Object> returnSet = new HashSet<>();
Set<Future> futures =
lst.stream()
.map(each -> executor.submit(() -> task.accept(each)))
.collect(Collectors.toSet());
for (Future each : futures) {
try {
Object rtn = each.get();
if (Objects.nonNull(rtn)) {
returnSet.add(rtn);
}
} catch (InterruptedException e) {
ui.message(
String.format(
"Migration step was interrupted. Only %d of %d tasks done.",
countDone(futures), lst.size()));
throw new RuntimeException(e);
} catch (ExecutionException e) {
ui.message(e.getCause().getMessage());
}
}
return returnSet;
}
protected CommitBuilder buildCommit(PersonIdent ident, ObjectId tree, String message) {
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(tree);
cb.setCommitter(ident);
cb.setAuthor(ident);
cb.setMessage(message);
return cb;
}
/**
* Add a task to GC a project to a single thread serving GC requests. If a GC task already exists
* for that project, then the current request to GC it will be ignored.
*
* @param repoManager Git repository manager to open the git repository.
* @param project Project name.
* @param ui Interface for interacting with the user.
*/
protected synchronized void runGcInBackground(
GitRepositoryManager repoManager, Project.NameKey project, UpdateUI ui) {
for (Runnable task : ((ThreadPoolExecutor) backgroundGcThread).getQueue()) {
if (task instanceof GcTask) {
if (((GcTask) task).project.equals(project)) {
return;
}
}
}
backgroundGcThread.execute(new GcTask(project, repoManager, ui));
}
protected static ObjectId emptyTree(ObjectInserter oi) throws IOException {
return oi.insert(Constants.OBJ_TREE, new byte[] {});
}
protected static ObjectInserter getPackInserterFirst(Repository repo) {
if (repo instanceof FileRepository) {
return ((FileRepository) repo).getObjectDatabase().newPackInserter();
}
return repo.getObjectDatabase().newInserter();
}
private static long countDone(Collection<Future> futures) {
return futures.stream().filter(Future::isDone).count();
}
private void gc(Repository repo, UpdateUI ui, ProgressMonitor pm, Stopwatch sw)
throws IOException, ParseException {
FileRepository r = (FileRepository) repo;
GC gc = new GC(r);
gc.setProgressMonitor(pm);
pm.beginTask("gc", ProgressMonitor.UNKNOWN);
// TODO(ms): Enable bitmap index when this JGit performance issue is fixed:
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=562740
PackConfig pconfig = new PackConfig(repo);
pconfig.setBuildBitmaps(false);
gc.setPackConfig(pconfig);
ui.message(
String.format("... (%.3f s) gc --prune=now", sw.elapsed(TimeUnit.MILLISECONDS) / 1000d));
gc.setExpire(new Date());
gc.gc();
}
private class GcTask implements Runnable {
final GitRepositoryManager repoManager;
final Project.NameKey project;
final UpdateUI ui;
public GcTask(Project.NameKey project, GitRepositoryManager repoManager, UpdateUI ui) {
this.project = project;
this.repoManager = repoManager;
this.ui = ui;
}
@Override
public void run() {
try (Repository repo = repoManager.openRepository(project)) {
ProgressMonitor pm = new TextProgressMonitor();
try {
// Empty reference folders are only deleted if they have not been modified
// in the last 30s. Add a delay so that their deletion is not skipped.
Thread.sleep(30000);
Stopwatch sw = Stopwatch.createStarted();
gc(repo, ui, pm, sw);
} catch (IOException | ParseException | InterruptedException ex) {
ui.message("GC on " + project.get() + " failed with error: " + ex);
} finally {
pm.endTask();
}
} catch (IOException ex) {
ui.message("GC on " + project.get() + " failed with error: " + ex);
}
}
}
}