LCOV - code coverage report
Current view: top level - server/schema - JdbcAccountPatchReviewStore.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 126 208 60.6 %
Date: 2022-11-19 15:00:39 Functions: 18 24 75.0 %

          Line data    Source code
       1             : // Copyright (C) 2017 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.schema;
      16             : 
      17             : import static java.util.concurrent.TimeUnit.MILLISECONDS;
      18             : import static java.util.concurrent.TimeUnit.SECONDS;
      19             : 
      20             : import com.google.common.annotations.VisibleForTesting;
      21             : import com.google.common.collect.ImmutableSet;
      22             : import com.google.common.flogger.FluentLogger;
      23             : import com.google.common.primitives.Ints;
      24             : import com.google.gerrit.entities.Account;
      25             : import com.google.gerrit.entities.Change;
      26             : import com.google.gerrit.entities.PatchSet;
      27             : import com.google.gerrit.exceptions.DuplicateKeyException;
      28             : import com.google.gerrit.exceptions.StorageException;
      29             : import com.google.gerrit.extensions.events.LifecycleListener;
      30             : import com.google.gerrit.extensions.registration.DynamicItem;
      31             : import com.google.gerrit.lifecycle.LifecycleModule;
      32             : import com.google.gerrit.server.change.AccountPatchReviewStore;
      33             : import com.google.gerrit.server.config.ConfigUtil;
      34             : import com.google.gerrit.server.config.GerritServerConfig;
      35             : import com.google.gerrit.server.config.SitePaths;
      36             : import com.google.gerrit.server.config.ThreadSettingsConfig;
      37             : import com.google.gerrit.server.logging.Metadata;
      38             : import com.google.gerrit.server.logging.TraceContext;
      39             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      40             : import java.nio.file.Path;
      41             : import java.sql.Connection;
      42             : import java.sql.PreparedStatement;
      43             : import java.sql.ResultSet;
      44             : import java.sql.SQLException;
      45             : import java.sql.Statement;
      46             : import java.util.Collection;
      47             : import java.util.Optional;
      48             : import javax.sql.DataSource;
      49             : import org.apache.commons.dbcp.BasicDataSource;
      50             : import org.eclipse.jgit.lib.Config;
      51             : 
      52             : public abstract class JdbcAccountPatchReviewStore
      53             :     implements AccountPatchReviewStore, LifecycleListener {
      54         138 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      55             : 
      56             :   // DB_CLOSE_DELAY=-1: By default the content of an in-memory H2 database is lost at the moment the
      57             :   // last connection is closed. This option keeps the content as long as the VM lives.
      58             :   @VisibleForTesting
      59             :   public static final String TEST_IN_MEMORY_URL =
      60             :       "jdbc:h2:mem:account_patch_reviews;DB_CLOSE_DELAY=-1";
      61             : 
      62             :   private static final String ACCOUNT_PATCH_REVIEW_DB = "accountPatchReviewDb";
      63             :   private static final String H2_DB = "h2";
      64             :   private static final String MARIADB = "mariadb";
      65             :   private static final String MYSQL = "mysql";
      66             :   private static final String POSTGRESQL = "postgresql";
      67             :   private static final String URL = "url";
      68             : 
      69             :   public static class JdbcAccountPatchReviewStoreModule extends LifecycleModule {
      70             :     private final Config cfg;
      71             : 
      72         138 :     public JdbcAccountPatchReviewStoreModule(Config cfg) {
      73         138 :       this.cfg = cfg;
      74         138 :     }
      75             : 
      76             :     @Override
      77             :     protected void configure() {
      78             :       Class<? extends JdbcAccountPatchReviewStore> impl;
      79         138 :       String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
      80         138 :       if (url == null || url.contains(H2_DB)) {
      81         138 :         impl = H2AccountPatchReviewStore.class;
      82           0 :       } else if (url.contains(POSTGRESQL)) {
      83           0 :         impl = PostgresqlAccountPatchReviewStore.class;
      84           0 :       } else if (url.contains(MYSQL)) {
      85           0 :         impl = MysqlAccountPatchReviewStore.class;
      86           0 :       } else if (url.contains(MARIADB)) {
      87           0 :         impl = MariaDBAccountPatchReviewStore.class;
      88             :       } else {
      89           0 :         throw new IllegalArgumentException(
      90             :             "unsupported driver type for account patch reviews db: " + url);
      91             :       }
      92         138 :       DynamicItem.bind(binder(), AccountPatchReviewStore.class).to(impl);
      93         138 :       listener().to(impl);
      94         138 :     }
      95             :   }
      96             : 
      97             :   private DataSource ds;
      98             : 
      99             :   public static JdbcAccountPatchReviewStore createAccountPatchReviewStore(
     100             :       Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) {
     101           0 :     String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
     102           0 :     if (url == null || url.contains(H2_DB)) {
     103           0 :       return new H2AccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     104             :     }
     105           0 :     if (url.contains(POSTGRESQL)) {
     106           0 :       return new PostgresqlAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     107             :     }
     108           0 :     if (url.contains(MYSQL)) {
     109           0 :       return new MysqlAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     110             :     }
     111           0 :     if (url.contains(MARIADB)) {
     112           0 :       return new MariaDBAccountPatchReviewStore(cfg, sitePaths, threadSettingsConfig);
     113             :     }
     114           0 :     throw new IllegalArgumentException(
     115             :         "unsupported driver type for account patch reviews db: " + url);
     116             :   }
     117             : 
     118             :   protected JdbcAccountPatchReviewStore(
     119         138 :       Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) {
     120         138 :     this.ds = createDataSource(cfg, sitePaths, threadSettingsConfig);
     121         138 :   }
     122             : 
     123             :   private static String getUrl(@GerritServerConfig Config cfg, SitePaths sitePaths) {
     124         138 :     String url = cfg.getString(ACCOUNT_PATCH_REVIEW_DB, null, URL);
     125         138 :     if (url == null) {
     126          15 :       return createH2Url(sitePaths.db_dir.resolve("account_patch_reviews"));
     127             :     }
     128         132 :     return url;
     129             :   }
     130             : 
     131             :   private static DataSource createDataSource(
     132             :       Config cfg, SitePaths sitePaths, ThreadSettingsConfig threadSettingsConfig) {
     133         138 :     BasicDataSource datasource = new BasicDataSource();
     134         138 :     String url = getUrl(cfg, sitePaths);
     135         138 :     int poolLimit = threadSettingsConfig.getDatabasePoolLimit();
     136         138 :     datasource.setUrl(url);
     137         138 :     datasource.setDriverClassName(getDriverFromUrl(url));
     138         138 :     datasource.setMaxActive(cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolLimit", poolLimit));
     139         138 :     datasource.setMinIdle(cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolminidle", 4));
     140         138 :     datasource.setMaxIdle(
     141         138 :         cfg.getInt(ACCOUNT_PATCH_REVIEW_DB, "poolmaxidle", Math.min(poolLimit, 16)));
     142         138 :     datasource.setInitialSize(datasource.getMinIdle());
     143         138 :     datasource.setMaxWait(
     144         138 :         ConfigUtil.getTimeUnit(
     145             :             cfg,
     146             :             ACCOUNT_PATCH_REVIEW_DB,
     147             :             null,
     148             :             "poolmaxwait",
     149         138 :             MILLISECONDS.convert(30, SECONDS),
     150             :             MILLISECONDS));
     151         138 :     long evictIdleTimeMs = 1000L * 60;
     152         138 :     datasource.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
     153         138 :     datasource.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
     154         138 :     return datasource;
     155             :   }
     156             : 
     157             :   private static String getDriverFromUrl(String url) {
     158         138 :     if (url.contains(POSTGRESQL)) {
     159           0 :       return "org.postgresql.Driver";
     160             :     }
     161         138 :     if (url.contains(MYSQL)) {
     162           0 :       return "com.mysql.jdbc.Driver";
     163             :     }
     164         138 :     if (url.contains(MARIADB)) {
     165           0 :       return "org.mariadb.jdbc.Driver";
     166             :     }
     167         138 :     return "org.h2.Driver";
     168             :   }
     169             : 
     170             :   @Override
     171             :   public void start() {
     172             :     try {
     173         138 :       createTableIfNotExists();
     174           0 :     } catch (StorageException e) {
     175           0 :       logger.atSevere().withCause(e).log("Failed to create table to store account patch reviews");
     176         138 :     }
     177         138 :   }
     178             : 
     179             :   public Connection getConnection() throws SQLException {
     180           0 :     return ds.getConnection();
     181             :   }
     182             : 
     183             :   public void createTableIfNotExists() {
     184         138 :     try (Connection con = ds.getConnection();
     185         138 :         Statement stmt = con.createStatement()) {
     186         138 :       doCreateTable(stmt);
     187           0 :     } catch (SQLException e) {
     188           0 :       throw convertError("create", e);
     189         138 :     }
     190         138 :   }
     191             : 
     192             :   protected void doCreateTable(Statement stmt) throws SQLException {
     193         138 :     stmt.executeUpdate(
     194             :         "CREATE TABLE IF NOT EXISTS account_patch_reviews ("
     195             :             + "account_id INTEGER DEFAULT 0 NOT NULL, "
     196             :             + "change_id INTEGER DEFAULT 0 NOT NULL, "
     197             :             + "patch_set_id INTEGER DEFAULT 0 NOT NULL, "
     198             :             + "file_name VARCHAR(4096) DEFAULT '' NOT NULL, "
     199             :             + "CONSTRAINT primary_key_account_patch_reviews "
     200             :             + "PRIMARY KEY (change_id, patch_set_id, account_id, file_name)"
     201             :             + ")");
     202         138 :   }
     203             : 
     204             :   public void dropTableIfExists() {
     205           0 :     try (Connection con = ds.getConnection();
     206           0 :         Statement stmt = con.createStatement()) {
     207           0 :       stmt.executeUpdate("DROP TABLE IF EXISTS account_patch_reviews");
     208           0 :     } catch (SQLException e) {
     209           0 :       throw convertError("create", e);
     210           0 :     }
     211           0 :   }
     212             : 
     213             :   @Override
     214         138 :   public void stop() {}
     215             : 
     216             :   @Override
     217             :   public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
     218           2 :     try (TraceTimer ignored =
     219           2 :             TraceContext.newTimer(
     220             :                 "Mark file as reviewed",
     221           2 :                 Metadata.builder()
     222           2 :                     .patchSetId(psId.get())
     223           2 :                     .accountId(accountId.get())
     224           2 :                     .filePath(path)
     225           2 :                     .build());
     226           2 :         Connection con = ds.getConnection();
     227           2 :         PreparedStatement stmt =
     228           2 :             con.prepareStatement(
     229             :                 "INSERT INTO account_patch_reviews "
     230             :                     + "(account_id, change_id, patch_set_id, file_name) VALUES "
     231             :                     + "(?, ?, ?, ?)")) {
     232           2 :       stmt.setInt(1, accountId.get());
     233           2 :       stmt.setInt(2, psId.changeId().get());
     234           2 :       stmt.setInt(3, psId.get());
     235           2 :       stmt.setString(4, path);
     236           2 :       stmt.executeUpdate();
     237           2 :       return true;
     238           0 :     } catch (SQLException e) {
     239           0 :       StorageException ormException = convertError("insert", e);
     240           0 :       if (ormException instanceof DuplicateKeyException) {
     241           0 :         return false;
     242             :       }
     243           0 :       throw ormException;
     244             :     }
     245             :   }
     246             : 
     247             :   @Override
     248             :   public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths) {
     249           1 :     if (paths == null || paths.isEmpty()) {
     250           1 :       return;
     251             :     }
     252             : 
     253           0 :     try (TraceTimer ignored =
     254           0 :             TraceContext.newTimer(
     255             :                 "Mark files as reviewed",
     256           0 :                 Metadata.builder()
     257           0 :                     .patchSetId(psId.get())
     258           0 :                     .accountId(accountId.get())
     259           0 :                     .resourceCount(paths.size())
     260           0 :                     .build());
     261           0 :         Connection con = ds.getConnection();
     262           0 :         PreparedStatement stmt =
     263           0 :             con.prepareStatement(
     264             :                 "INSERT INTO account_patch_reviews "
     265             :                     + "(account_id, change_id, patch_set_id, file_name) VALUES "
     266             :                     + "(?, ?, ?, ?)")) {
     267           0 :       for (String path : paths) {
     268           0 :         stmt.setInt(1, accountId.get());
     269           0 :         stmt.setInt(2, psId.changeId().get());
     270           0 :         stmt.setInt(3, psId.get());
     271           0 :         stmt.setString(4, path);
     272           0 :         stmt.addBatch();
     273           0 :       }
     274           0 :       stmt.executeBatch();
     275           0 :     } catch (SQLException e) {
     276           0 :       StorageException ormException = convertError("insert", e);
     277           0 :       if (ormException instanceof DuplicateKeyException) {
     278           0 :         return;
     279             :       }
     280           0 :       throw ormException;
     281           0 :     }
     282           0 :   }
     283             : 
     284             :   @Override
     285             :   public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
     286           2 :     try (TraceTimer ignored =
     287           2 :             TraceContext.newTimer(
     288             :                 "Clear reviewed flag",
     289           2 :                 Metadata.builder()
     290           2 :                     .patchSetId(psId.get())
     291           2 :                     .accountId(accountId.get())
     292           2 :                     .filePath(path)
     293           2 :                     .build());
     294           2 :         Connection con = ds.getConnection();
     295           2 :         PreparedStatement stmt =
     296           2 :             con.prepareStatement(
     297             :                 "DELETE FROM account_patch_reviews "
     298             :                     + "WHERE account_id = ? AND change_id = ? AND "
     299             :                     + "patch_set_id = ? AND file_name = ?")) {
     300           2 :       stmt.setInt(1, accountId.get());
     301           2 :       stmt.setInt(2, psId.changeId().get());
     302           2 :       stmt.setInt(3, psId.get());
     303           2 :       stmt.setString(4, path);
     304           2 :       stmt.executeUpdate();
     305           0 :     } catch (SQLException e) {
     306           0 :       throw convertError("delete", e);
     307           2 :     }
     308           2 :   }
     309             : 
     310             :   @Override
     311             :   public void clearReviewed(PatchSet.Id psId) {
     312           1 :     try (TraceTimer ignored =
     313           1 :             TraceContext.newTimer(
     314             :                 "Clear all reviewed flags of patch set",
     315           1 :                 Metadata.builder().patchSetId(psId.get()).build());
     316           1 :         Connection con = ds.getConnection();
     317           1 :         PreparedStatement stmt =
     318           1 :             con.prepareStatement(
     319             :                 "DELETE FROM account_patch_reviews "
     320             :                     + "WHERE change_id = ? AND patch_set_id = ?")) {
     321           1 :       stmt.setInt(1, psId.changeId().get());
     322           1 :       stmt.setInt(2, psId.get());
     323           1 :       stmt.executeUpdate();
     324           0 :     } catch (SQLException e) {
     325           0 :       throw convertError("delete", e);
     326           1 :     }
     327           1 :   }
     328             : 
     329             :   @Override
     330             :   public void clearReviewed(Change.Id changeId) {
     331           7 :     try (TraceTimer ignored =
     332           7 :             TraceContext.newTimer(
     333             :                 "Clear all reviewed flags of change",
     334           7 :                 Metadata.builder().changeId(changeId.get()).build());
     335           7 :         Connection con = ds.getConnection();
     336           7 :         PreparedStatement stmt =
     337           7 :             con.prepareStatement("DELETE FROM account_patch_reviews WHERE change_id = ?")) {
     338           7 :       stmt.setInt(1, changeId.get());
     339           7 :       stmt.executeUpdate();
     340           0 :     } catch (SQLException e) {
     341           0 :       throw convertError("delete", e);
     342           7 :     }
     343           7 :   }
     344             : 
     345             :   @Override
     346             :   public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) {
     347           1 :     try (TraceTimer ignored =
     348           1 :             TraceContext.newTimer(
     349             :                 "Find reviewed flags",
     350           1 :                 Metadata.builder().patchSetId(psId.get()).accountId(accountId.get()).build());
     351           1 :         Connection con = ds.getConnection();
     352           1 :         PreparedStatement stmt =
     353           1 :             con.prepareStatement(
     354             :                 "SELECT patch_set_id, file_name FROM account_patch_reviews APR1 "
     355             :                     + "WHERE account_id = ? AND change_id = ? AND patch_set_id = "
     356             :                     + "(SELECT MAX(patch_set_id) FROM account_patch_reviews APR2 WHERE "
     357             :                     + "APR1.account_id = APR2.account_id "
     358             :                     + "AND APR1.change_id = APR2.change_id "
     359             :                     + "AND patch_set_id <= ?)")) {
     360           1 :       stmt.setInt(1, accountId.get());
     361           1 :       stmt.setInt(2, psId.changeId().get());
     362           1 :       stmt.setInt(3, psId.get());
     363           1 :       try (ResultSet rs = stmt.executeQuery()) {
     364           1 :         if (rs.next()) {
     365           1 :           PatchSet.Id id = PatchSet.id(psId.changeId(), rs.getInt("patch_set_id"));
     366           1 :           ImmutableSet.Builder<String> builder = ImmutableSet.builder();
     367             :           do {
     368           1 :             builder.add(rs.getString("file_name"));
     369           1 :           } while (rs.next());
     370             : 
     371           1 :           return Optional.of(
     372           1 :               AccountPatchReviewStore.PatchSetWithReviewedFiles.create(id, builder.build()));
     373             :         }
     374             : 
     375           1 :         return Optional.empty();
     376           1 :       }
     377           1 :     } catch (SQLException e) {
     378           0 :       throw convertError("select", e);
     379             :     }
     380             :   }
     381             : 
     382             :   public StorageException convertError(String op, SQLException err) {
     383           0 :     if (err.getCause() == null && err.getNextException() != null) {
     384           0 :       err.initCause(err.getNextException());
     385             :     }
     386           0 :     return new StorageException(op + " failure on account_patch_reviews", err);
     387             :   }
     388             : 
     389             :   private static String getSQLState(SQLException err) {
     390             :     String ec;
     391           0 :     SQLException next = err;
     392             :     do {
     393           0 :       ec = next.getSQLState();
     394           0 :       next = next.getNextException();
     395           0 :     } while (ec == null && next != null);
     396           0 :     return ec;
     397             :   }
     398             : 
     399             :   protected static int getSQLStateInt(SQLException err) {
     400           0 :     String s = getSQLState(err);
     401           0 :     if (s != null) {
     402           0 :       Integer i = Ints.tryParse(s);
     403           0 :       return i != null ? i : -1;
     404             :     }
     405           0 :     return 0;
     406             :   }
     407             : 
     408             :   private static String createH2Url(Path path) {
     409          15 :     return new StringBuilder().append("jdbc:h2:").append(path.toUri().toString()).toString();
     410             :   }
     411             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750