diff options
Diffstat (limited to 'java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java')
-rw-r--r-- | java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java new file mode 100644 index 0000000000..83a0986bf5 --- /dev/null +++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java @@ -0,0 +1,355 @@ +// Copyright (C) 2017 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 static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.FluentLogger; +import com.google.common.primitives.Ints; +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.server.change.AccountPatchReviewStore; +import com.google.gerrit.server.config.ConfigUtil; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.config.ThreadSettingsConfig; +import com.google.gwtorm.server.OrmDuplicateKeyException; +import com.google.gwtorm.server.OrmException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collection; +import java.util.Optional; +import javax.sql.DataSource; +import org.apache.commons.dbcp.BasicDataSource; +import org.eclipse.jgit.lib.Config; + +public abstract class JdbcAccountPatchReviewStore + implements AccountPatchReviewStore, LifecycleListener { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final String ACCOUNT_PATCH_REVIEW_DB = "accountPatchReviewDb"; + private static final String H2_DB = "h2"; + private static final String MARIADB = "mariadb"; + private static final String MYSQL = "mysql"; + private static final String POSTGRESQL = "postgresql"; + private static final String URL = "url"; + + public static class Module extends LifecycleModule { + private final Config cfg; + + public Module(Config cfg) { + this.cfg = cfg; + } + + @Override + protected void configure() { + Class<? extends JdbcAccountPatchReviewStore> impl; + String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL); + if (url == null || url.contains(H2_DB)) { + impl = H2AccountPatchReviewStore.class; + } else if (url.contains(POSTGRESQL)) { + impl = PostgresqlAccountPatchReviewStore.class; + } else if (url.contains(MYSQL)) { + impl = MysqlAccountPatchReviewStore.class; + } else if (url.contains(MARIADB)) { + impl = MariaDBAccountPatchReviewStore.class; + } else { + throw new IllegalArgumentException( + "unsupported driver type for account patch reviews db: " + url); + } + DynamicItem.bind(binder(), AccountPatchReviewStore.class).to(impl); + listener().to(impl); + } + } + + private DataSource ds; + + public static JdbcAccountPatchReviewStore createAccountPatchReviewStore( + Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) { + String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL); + if (url == null || url.contains(H2_DB)) { + return new H2AccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig); + } + if (url.contains(POSTGRESQL)) { + return new PostgresqlAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig); + } + if (url.contains(MYSQL)) { + return new MysqlAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig); + } + if (url.contains(MARIADB)) { + return new MariaDBAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig); + } + throw new IllegalArgumentException( + "unsupported driver type for account patch reviews db: " + url); + } + + protected JdbcAccountPatchReviewStore( + Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) { + this.ds = createDataSource(cfg, sitePaths, threadSettingsConfig); + } + + protected JdbcAccountPatchReviewStore(DataSource ds) { + this.ds = ds; + } + + private static String getUrl(@GerritServerConfig Config cfg, SitePaths sitePaths) { + String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL); + if (url == null) { + return H2.createUrl(sitePaths.db_dir.resolve("account_patch_reviews")); + } + return url; + } + + private static DataSource createDataSource( + Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) { + BasicDataSource datasource = new BasicDataSource(); + String url = getUrl(cfg, sitePaths); + int poolLimit = threadSettingsConfig.getDatabasePoolLimit(); + datasource.setUrl(url); + datasource.setDriverClassName(getDriverFromUrl(url)); + datasource.setMaxActive(cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolLimit", poolLimit)); + datasource.setMinIdle(cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolminidle", 4)); + datasource.setMaxIdle( + cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolmaxidle", Math.min(poolLimit, 16))); + datasource.setInitialSize(datasource.getMinIdle()); + datasource.setMaxWait( + ConfigUtil.getTimeUnit( + cfg, + ACCOUNT_PATCH_REVIEW_DB, + null, + "poolmaxwait", + MILLISECONDS.convert(30, SECONDS), + MILLISECONDS)); + long evictIdleTimeMs = 1000L * 60; + datasource.setMinEvictableIdleTimeMillis(evictIdleTimeMs); + datasource.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2); + return datasource; + } + + private static String getDriverFromUrl(String url) { + if (url.contains(POSTGRESQL)) { + return "org.postgresql.Driver"; + } + if (url.contains(MYSQL)) { + return "com.mysql.jdbc.Driver"; + } + if (url.contains(MARIADB)) { + return "org.mariadb.jdbc.Driver"; + } + return "org.h2.Driver"; + } + + @Override + public void start() { + try { + createTableIfNotExists(); + } catch (OrmException e) { + logger.atSevere().withCause(e).log("Failed to create table to store account patch reviews"); + } + } + + public Connection getConnection() throws SQLException { + return ds.getConnection(); + } + + public void createTableIfNotExists() throws OrmException { + try (Connection con = ds.getConnection(); + Statement stmt = con.createStatement()) { + doCreateTable(stmt); + } catch (SQLException e) { + throw convertError("create", e); + } + } + + protected void doCreateTable(Statement stmt) throws SQLException { + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS account_patch_reviews (" + + "account_id INTEGER DEFAULT 0 NOT NULL, " + + "change_id INTEGER DEFAULT 0 NOT NULL, " + + "patch_set_id INTEGER DEFAULT 0 NOT NULL, " + + "file_name VARCHAR(4096) DEFAULT '' NOT NULL, " + + "CONSTRAINT primary_key_account_patch_reviews " + + "PRIMARY KEY (change_id, patch_set_id, account_id, file_name)" + + ")"); + } + + public void dropTableIfExists() throws OrmException { + try (Connection con = ds.getConnection(); + Statement stmt = con.createStatement()) { + stmt.executeUpdate("DROP TABLE IF EXISTS account_patch_reviews"); + } catch (SQLException e) { + throw convertError("create", e); + } + } + + @Override + public void stop() {} + + @Override + public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) + throws OrmException { + try (Connection con = ds.getConnection(); + PreparedStatement stmt = + con.prepareStatement( + "INSERT INTO account_patch_reviews " + + "(account_id, change_id, patch_set_id, file_name) VALUES " + + "(?, ?, ?, ?)")) { + stmt.setInt(1, accountId.get()); + stmt.setInt(2, psId.getParentKey().get()); + stmt.setInt(3, psId.get()); + stmt.setString(4, path); + stmt.executeUpdate(); + return true; + } catch (SQLException e) { + OrmException ormException = convertError("insert", e); + if (ormException instanceof OrmDuplicateKeyException) { + return false; + } + throw ormException; + } + } + + @Override + public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths) + throws OrmException { + if (paths == null || paths.isEmpty()) { + return; + } + + try (Connection con = ds.getConnection(); + PreparedStatement stmt = + con.prepareStatement( + "INSERT INTO account_patch_reviews " + + "(account_id, change_id, patch_set_id, file_name) VALUES " + + "(?, ?, ?, ?)")) { + for (String path : paths) { + stmt.setInt(1, accountId.get()); + stmt.setInt(2, psId.getParentKey().get()); + stmt.setInt(3, psId.get()); + stmt.setString(4, path); + stmt.addBatch(); + } + stmt.executeBatch(); + } catch (SQLException e) { + OrmException ormException = convertError("insert", e); + if (ormException instanceof OrmDuplicateKeyException) { + return; + } + throw ormException; + } + } + + @Override + public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) + throws OrmException { + try (Connection con = ds.getConnection(); + PreparedStatement stmt = + con.prepareStatement( + "DELETE FROM account_patch_reviews " + + "WHERE account_id = ? AND change_id = ? AND " + + "patch_set_id = ? AND file_name = ?")) { + stmt.setInt(1, accountId.get()); + stmt.setInt(2, psId.getParentKey().get()); + stmt.setInt(3, psId.get()); + stmt.setString(4, path); + stmt.executeUpdate(); + } catch (SQLException e) { + throw convertError("delete", e); + } + } + + @Override + public void clearReviewed(PatchSet.Id psId) throws OrmException { + try (Connection con = ds.getConnection(); + PreparedStatement stmt = + con.prepareStatement( + "DELETE FROM account_patch_reviews " + + "WHERE change_id = ? AND patch_set_id = ?")) { + stmt.setInt(1, psId.getParentKey().get()); + stmt.setInt(2, psId.get()); + stmt.executeUpdate(); + } catch (SQLException e) { + throw convertError("delete", e); + } + } + + @Override + public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) + throws OrmException { + try (Connection con = ds.getConnection(); + PreparedStatement stmt = + con.prepareStatement( + "SELECT patch_set_id, file_name FROM account_patch_reviews APR1 " + + "WHERE account_id = ? AND change_id = ? AND patch_set_id = " + + "(SELECT MAX(patch_set_id) FROM account_patch_reviews APR2 WHERE " + + "APR1.account_id = APR2.account_id " + + "AND APR1.change_id = APR2.change_id " + + "AND patch_set_id <= ?)")) { + stmt.setInt(1, accountId.get()); + stmt.setInt(2, psId.getParentKey().get()); + stmt.setInt(3, psId.get()); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + PatchSet.Id id = new PatchSet.Id(psId.getParentKey(), rs.getInt("patch_set_id")); + ImmutableSet.Builder<String> builder = ImmutableSet.builder(); + do { + builder.add(rs.getString("file_name")); + } while (rs.next()); + + return Optional.of( + AccountPatchReviewStore.PatchSetWithReviewedFiles.create(id, builder.build())); + } + + return Optional.empty(); + } + } catch (SQLException e) { + throw convertError("select", e); + } + } + + public OrmException convertError(String op, SQLException err) { + if (err.getCause() == null && err.getNextException() != null) { + err.initCause(err.getNextException()); + } + return new OrmException(op + " failure on account_patch_reviews", err); + } + + private static String getSQLState(SQLException err) { + String ec; + SQLException next = err; + do { + ec = next.getSQLState(); + next = next.getNextException(); + } while (ec == null && next != null); + return ec; + } + + protected static int getSQLStateInt(SQLException err) { + String s = getSQLState(err); + if (s != null) { + Integer i = Ints.tryParse(s); + return i != null ? i : -1; + } + return 0; + } +} |