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 : }
|