Merge branch 'stable-2.14' into stable-2.15 * stable-2.14: Upgrade bazlets to latest stable-2.14 to build with 2.14.21 API Change-Id: I150bec0ec981662673102f74aebd7adc5da9db8d
diff --git a/WORKSPACE b/WORKSPACE index 5204a9a..2223b91 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -3,7 +3,7 @@ load("//:bazlets.bzl", "load_bazlets") load_bazlets( - commit = "78c35a7eb33ee5ea0980923e246c7dba37347193", + commit = "f53f51fb660552d0581aa0ba52c3836ed63d56a3", #local_path = "/home/<user>/projects/bazlets", )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl index 5e88fc1..b3f9594 100644 --- a/external_plugin_deps.bzl +++ b/external_plugin_deps.bzl
@@ -3,8 +3,8 @@ def external_plugin_deps(): maven_jar( name = "mockito", - artifact = "org.mockito:mockito-core:2.27.0", - sha1 = "835fc3283b481f4758b8ef464cd560c649c08b00", + artifact = "org.mockito:mockito-core:2.28.2", + sha1 = "91110215a8cb9b77a46e045ee758f77d79167cc0", deps = [ "@byte-buddy//jar", "@byte-buddy-agent//jar",
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java index d8e2aff..66fcc81 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventCleanerQueue.java
@@ -14,36 +14,38 @@ package com.ericsson.gerrit.plugins.eventslog; +import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; import com.google.inject.Singleton; -import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ScheduledExecutorService; @Singleton public class EventCleanerQueue implements LifecycleListener { private final WorkQueue workQueue; - private WorkQueue.Executor pool; + private final String pluginName; + private ScheduledExecutorService pool; @Inject - public EventCleanerQueue(WorkQueue workQueue) { + public EventCleanerQueue(WorkQueue workQueue, @PluginName String pluginName) { this.workQueue = workQueue; + this.pluginName = pluginName; } @Override public void start() { - pool = workQueue.createQueue(1, "[events-log] Remove events"); + pool = workQueue.createQueue(1, String.format("[%s] Remove events", pluginName)); } @Override public void stop() { if (pool != null) { - pool.unregisterWorkQueue(); pool = null; } } - public ScheduledThreadPoolExecutor getPool() { - return pool; + ScheduledExecutorService getPool() { + return this.pool; } }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java index 262316c..15f830a 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventHandler.java
@@ -18,15 +18,15 @@ import com.google.gerrit.server.events.Event; import com.google.gerrit.server.events.ProjectEvent; import com.google.inject.Inject; -import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ScheduledExecutorService; /** Listen to Events and store them into the EventStore */ class EventHandler implements EventListener { private final EventStore store; - private final ScheduledThreadPoolExecutor pool; + private final ScheduledExecutorService pool; @Inject - EventHandler(EventStore store, @EventPool ScheduledThreadPoolExecutor pool) { + EventHandler(EventStore store, @EventPool ScheduledExecutorService pool) { this.store = store; this.pool = pool; }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java index 56f8802..b888425 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventModule.java
@@ -23,7 +23,7 @@ import com.google.inject.Provides; import com.google.inject.Scopes; import com.google.inject.internal.UniqueAnnotations; -import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ScheduledExecutorService; /** Configures handling for an event queue while providing its pool. */ public class EventModule extends AbstractModule { @@ -42,13 +42,13 @@ @Provides @EventPool - ScheduledThreadPoolExecutor provideEventPool(EventQueue queue) { + ScheduledExecutorService provideEventPool(EventQueue queue) { return queue.getPool(); } @Provides @EventCleanerPool - ScheduledThreadPoolExecutor provideEventCleanerPool(EventCleanerQueue queue) { + ScheduledExecutorService provideEventCleanerPool(EventCleanerQueue queue) { return queue.getPool(); } }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java index 32e9465..e4646b0 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventPool.java
@@ -19,7 +19,7 @@ import com.google.inject.BindingAnnotation; import java.lang.annotation.Retention; -/** Annotation applied to a ScheduledThreadPoolExecutor. */ +/** Annotation applied to a ScheduledExecutorService. */ @Retention(RUNTIME) @BindingAnnotation public @interface EventPool {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java index eca051b..4d2ba5d 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventQueue.java
@@ -14,36 +14,38 @@ package com.ericsson.gerrit.plugins.eventslog; +import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; -import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ScheduledExecutorService; /** A queue for events to store. */ public class EventQueue implements LifecycleListener { private final WorkQueue workQueue; - private WorkQueue.Executor pool; + private final String pluginName; + private ScheduledExecutorService pool; @Inject - EventQueue(WorkQueue workQueue) { + EventQueue(WorkQueue workQueue, @PluginName String pluginName) { this.workQueue = workQueue; + this.pluginName = pluginName; } /** {@inheritDoc} Create a new executor queue in WorkQueue for storing events. */ @Override public void start() { - pool = workQueue.createQueue(1, "Store events"); + pool = workQueue.createQueue(1, String.format("[%s] Store events", pluginName)); } @Override public void stop() { if (pool != null) { - pool.unregisterWorkQueue(); pool = null; } } - ScheduledThreadPoolExecutor getPool() { + ScheduledExecutorService getPool() { return this.pool; } }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java index 3c152e8..efff521 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleaner.java
@@ -15,6 +15,7 @@ package com.ericsson.gerrit.plugins.eventslog.sql; import com.ericsson.gerrit.plugins.eventslog.EventCleanerPool; +import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.events.ProjectDeletedListener; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -22,7 +23,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Singleton @@ -31,13 +32,17 @@ private static final long INTERVAL = TimeUnit.DAYS.toSeconds(1); private final SQLClient eventsDb; + private final String pluginName; - private ScheduledThreadPoolExecutor pool; + private ScheduledExecutorService pool; @Inject EventsLogCleaner( - @EventsDb SQLClient eventsDb, @EventCleanerPool ScheduledThreadPoolExecutor pool) { + @EventsDb SQLClient eventsDb, + @EventCleanerPool ScheduledExecutorService pool, + @PluginName String pluginName) { this.eventsDb = eventsDb; + this.pluginName = pluginName; this.pool = pool; } @@ -47,12 +52,12 @@ } public void removeProjectEventsAsync(String projectName) { - pool.submit(() -> eventsDb.removeProjectEvents(projectName)); + pool.submit(new RemoveProjectEventsTask(pluginName, projectName)); } public void scheduleCleaningWith(int maxAge) { pool.scheduleAtFixedRate( - () -> eventsDb.removeOldEvents(maxAge), getInitialDelay(), INTERVAL, TimeUnit.SECONDS); + new RemoveOldEventsTask(pluginName, maxAge), getInitialDelay(), INTERVAL, TimeUnit.SECONDS); } private long getInitialDelay() { @@ -63,4 +68,44 @@ } return Duration.between(now, next).getSeconds(); } + + private class RemoveProjectEventsTask implements Runnable { + private final String projectName; + private final String taskName; + + RemoveProjectEventsTask(String prefix, String projectName) { + this.projectName = projectName; + this.taskName = String.format("[%s] Remove events for project %s", prefix, projectName); + } + + @Override + public void run() { + eventsDb.removeProjectEvents(projectName); + } + + @Override + public String toString() { + return taskName; + } + } + + private class RemoveOldEventsTask implements Runnable { + private final int maxAge; + private final String taskName; + + RemoveOldEventsTask(String prefix, int maxAge) { + this.maxAge = maxAge; + this.taskName = String.format("[%s] Remove old events", prefix); + } + + @Override + public void run() { + eventsDb.removeOldEvents(maxAge); + } + + @Override + public String toString() { + return taskName; + } + } }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java index 05f663f..9e538e6 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLClient.java
@@ -48,7 +48,7 @@ class SQLClient { private static final Logger log = LoggerFactory.getLogger(SQLClient.class); private final Gson gson; - private final boolean isPostgresql; + private final SQLDialect databaseDialect; private HikariDataSource ds; @@ -56,7 +56,8 @@ ds = new HikariDataSource(config); gson = new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create(); - isPostgresql = config.getJdbcUrl().contains("postgresql"); + + databaseDialect = SQLDialect.fromJdbcUrl(config.getJdbcUrl()); } /** @@ -65,8 +66,8 @@ * @throws SQLException If there was a problem with the database */ void createDBIfNotCreated() throws SQLException { - execute(SQLTable.createTableQuery(isPostgresql)); - execute(SQLTable.createIndexes(isPostgresql)); + execute(SQLTable.createTableQuery(databaseDialect)); + execute(SQLTable.createIndexes(databaseDialect)); } /**
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialect.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialect.java new file mode 100644 index 0000000..10df147 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialect.java
@@ -0,0 +1,38 @@ +// Copyright (C) 2019 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.ericsson.gerrit.plugins.eventslog.sql; + +/** SQLDialect provides enumerations for the various supported dialects of SQL. */ +public enum SQLDialect { + H2, + MYSQL, + POSTGRESQL; + + /** + * This attempts to determine the SQL dialect from the JDBC URL. If the URL does not match one of + * the supported dialects, then H2 will be returned by default. + * + * @param jdbcUrl The JDBC URL. + * @return The dialect for the JDBC URL. + */ + public static SQLDialect fromJdbcUrl(String jdbcUrl) { + if (jdbcUrl.contains("postgresql")) { + return POSTGRESQL; + } else if (jdbcUrl.contains("mysql")) { + return MYSQL; + } + return H2; + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java index 8e2ab52..ba38c4e 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStore.java
@@ -23,12 +23,15 @@ import com.ericsson.gerrit.plugins.eventslog.EventsLogException; import com.ericsson.gerrit.plugins.eventslog.ServiceUnavailableException; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.events.ProjectEvent; -import com.google.gerrit.server.project.NoSuchProjectException; -import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -41,8 +44,8 @@ import java.util.Collection; import java.util.List; import java.util.Map.Entry; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +55,6 @@ private static final Logger log = LoggerFactory.getLogger(SQLStore.class); private static final String H2_DB_SUFFIX = ".h2.db"; - private final ProjectControl.GenericFactory projectControlFactory; private final Provider<CurrentUser> userProvider; private final EventsLogCleaner eventsLogCleaner; private SQLClient eventsDb; @@ -63,31 +65,35 @@ private final int connectTime; private boolean online = true; private boolean copyLocal; - private final ScheduledThreadPoolExecutor pool; + private final ScheduledExecutorService pool; + private final PermissionBackend permissionBackend; + private final String pluginName; private ScheduledFuture<?> checkConnTask; private Path localPath; @Inject SQLStore( - ProjectControl.GenericFactory projectControlFactory, Provider<CurrentUser> userProvider, EventsLogConfig cfg, @EventsDb SQLClient eventsDb, @LocalEventsDb SQLClient localEventsDb, - @EventPool ScheduledThreadPoolExecutor pool, - EventsLogCleaner eventsLogCleaner) { + @EventPool ScheduledExecutorService pool, + PermissionBackend permissionBackend, + EventsLogCleaner eventsLogCleaner, + @PluginName String pluginName) { this.maxAge = cfg.getMaxAge(); this.maxTries = cfg.getMaxTries(); this.waitTime = cfg.getWaitTime(); this.connectTime = cfg.getConnectTime(); this.copyLocal = cfg.getCopyLocal(); - this.projectControlFactory = projectControlFactory; this.userProvider = userProvider; this.eventsDb = eventsDb; this.localEventsDb = localEventsDb; this.eventsLogCleaner = eventsLogCleaner; this.pool = pool; + this.permissionBackend = permissionBackend; this.localPath = cfg.getLocalStorePath(); + this.pluginName = pluginName; } @Override @@ -118,18 +124,15 @@ for (Entry<String, Collection<SQLEntry>> entry : eventsDb.getEvents(query).asMap().entrySet()) { String projectName = entry.getKey(); try { - if (projectControlFactory - .controlFor(new Project.NameKey(projectName), userProvider.get()) - .isVisible()) { - entries.addAll(entry.getValue()); - } - } catch (NoSuchProjectException e) { - log.warn( - "Database contains a non-existing project, {}; removing project from database", - projectName); - eventsLogCleaner.removeProjectEventsAsync(projectName); - } catch (IOException e) { - log.warn("Cannot get project visibility info for {} from cache", projectName, e); + permissionBackend + .user(userProvider) + .project(new Project.NameKey(projectName)) + .check(ProjectPermission.ACCESS); + entries.addAll(entry.getValue()); + } catch (AuthException e) { + // Ignore + } catch (PermissionBackendException e) { + log.warn("Cannot check project access permission", e); } } return entries.stream().sorted().map(SQLEntry::getEvent).collect(toList()); @@ -205,7 +208,7 @@ if (!online) { checkConnTask = pool.scheduleWithFixedDelay( - new CheckConnectionTask(), 0, connectTime, TimeUnit.MILLISECONDS); + new CheckConnectionTask(pluginName), 0, connectTime, TimeUnit.MILLISECONDS); } else { cancelCheckConnectionTaskIfScheduled(false); } @@ -246,7 +249,11 @@ } class CheckConnectionTask implements Runnable { - CheckConnectionTask() {} + private final String taskName; + + CheckConnectionTask(String prefix) { + this.taskName = String.format("[%s] Connect to database", prefix); + } @Override public void run() { @@ -258,7 +265,7 @@ @Override public String toString() { - return "(Events-log) Connect to database"; + return taskName; } private boolean checkConnection() {
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java index 0db07a2..6093a38 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java +++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLTable.java
@@ -23,9 +23,28 @@ static final String DATE_ENTRY = "date_created"; static final String EVENT_ENTRY = "event_info"; + /** This is the name of the index that tracks the created date. */ private static final String CREATED_INDEX = "created_idx"; + /** This is the name of the index that tracks the project. */ private static final String PROJECT_INDEX = "project_idx"; + /** + * This is the H2 idempotent index-creation query format. Inputs, in order: index-name, + * table-name, index-column + */ private static final String H2_INDEX_CREATION_FORMAT = "CREATE INDEX IF NOT EXISTS %s ON %s (%s)"; + /** + * This is the MySQL idempotent index-creation query format. Inputs, in order: table-name, + * index-name, table-name, index-name, index-column + */ + private static final String MYSQL_INDEX_CREATION_FORMAT = + "SET @x := (SELECT COUNT(*) FROM information_schema.statistics WHERE table_name = '%s' AND index_name = '%s' AND table_schema = DATABASE());\n" + + "SET @sql := IF( @x > 0, 'SELECT ''Index exists.''', 'ALTER TABLE %s ADD INDEX %s (%s);');\n" + + "PREPARE stmt FROM @sql;\n" + + "EXECUTE stmt"; + /** + * This is the Postgres idempotent index-creation query format. Inputs, in order: index-name, + * index-name, table-name, index-column + */ private static final String POSTGRESQL_INDEX_CREATION_FORMAT = "DO $$\n" + "BEGIN\n" @@ -42,13 +61,17 @@ private SQLTable() {} - static String createTableQuery(boolean postgresql) { + static String createTableQuery(SQLDialect databaseDialect) { StringBuilder query = new StringBuilder(140); query.append(format("CREATE TABLE IF NOT EXISTS %s(", TABLE_NAME)); - if (postgresql) { - query.append(format("%s SERIAL PRIMARY KEY,", PRIMARY_ENTRY)); - } else { - query.append(format("%s INT AUTO_INCREMENT PRIMARY KEY,", PRIMARY_ENTRY)); + switch (databaseDialect) { + case POSTGRESQL: + query.append(format("%s SERIAL PRIMARY KEY,", PRIMARY_ENTRY)); + break; + case MYSQL: + case H2: + default: + query.append(format("%s INT AUTO_INCREMENT PRIMARY KEY,", PRIMARY_ENTRY)); } query.append(format("%s VARCHAR(255),", PROJECT_ENTRY)); query.append(format("%s TIMESTAMP DEFAULT NOW(),", DATE_ENTRY)); @@ -56,11 +79,19 @@ return query.toString(); } - static String createIndexes(boolean postgresql) { - return postgresql ? getPostgresqlQuery() : getH2Query(); + static String createIndexes(SQLDialect databaseDialect) { + switch (databaseDialect) { + case POSTGRESQL: + return getPostgresqlIndexQuery(); + case MYSQL: + return getMysqlIndexQuery(); + case H2: + default: + return getH2IndexQuery(); + } } - private static String getPostgresqlQuery() { + private static String getPostgresqlIndexQuery() { StringBuilder query = new StringBuilder(540); query.append( format( @@ -80,7 +111,29 @@ return query.toString(); } - private static String getH2Query() { + private static String getMysqlIndexQuery() { + StringBuilder query = new StringBuilder(); + query.append( + format( + MYSQL_INDEX_CREATION_FORMAT, + TABLE_NAME, + CREATED_INDEX, + TABLE_NAME, + CREATED_INDEX, + DATE_ENTRY)); + query.append(";"); + query.append( + format( + MYSQL_INDEX_CREATION_FORMAT, + TABLE_NAME, + PROJECT_INDEX, + TABLE_NAME, + PROJECT_INDEX, + PROJECT_ENTRY)); + return query.toString(); + } + + private static String getH2IndexQuery() { StringBuilder query = new StringBuilder(); query.append(format(H2_INDEX_CREATION_FORMAT, CREATED_INDEX, TABLE_NAME, DATE_ENTRY)); query.append(";");
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md index 48ee3c6..130cc2a 100644 --- a/src/main/resources/Documentation/build.md +++ b/src/main/resources/Documentation/build.md
@@ -78,7 +78,7 @@ To execute the tests run: ``` - bazel test plugins/@PLUGIN@:events_log_tests + bazel test plugins/@PLUGIN@:events-log_tests ``` More information about Buck can be found in the [Gerrit
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 4421212..1c74a20 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -26,6 +26,10 @@ plugin.@PLUGIN@.storeUrl : Specify the path to the directory in which to keep the database. When not specified, the default path is jdbc:h2:\<gerrit_site>/data/db. + Supported database engines: +* h2 (default) +* postgresql +* mysql plugin.@PLUGIN@.localStorePath : Specify the path to the directory in which to keep the back up database. @@ -42,8 +46,12 @@ plugin.@PLUGIN@.urlOptions : Options to append to the database url. Each option should be specified in a separate line using the option=value format. For example: - urlOptions = loglevel=INFO - urlOptions = logUnclosedConnections=true +* `urlOptions = loglevel=INFO` +* `urlOptions = logUnclosedConnections=true` + +When using `mysql`, this option must be specified: + +* `urlOptions = allowMultiQueries=true` plugin.@PLUGIN@.maxTries : Maximum number of times the plugin should attempt to store the event if a
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java index f3eb207..0007ab8 100644 --- a/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java +++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventHandlerTest.java
@@ -21,6 +21,7 @@ import com.google.gerrit.server.events.ChangeEvent; import com.google.gerrit.server.events.Event; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import org.junit.Before; import org.junit.Test; @@ -35,7 +36,7 @@ @Before public void setUp() { - ScheduledThreadPoolExecutor poolMock = new PoolMock(); + ScheduledExecutorService poolMock = new PoolMock(); eventHandler = new EventHandler(storeMock, poolMock); }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleanerTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleanerTest.java index 09c21dc..11fa891 100644 --- a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleanerTest.java +++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/EventsLogCleanerTest.java
@@ -32,6 +32,7 @@ @RunWith(MockitoJUnitRunner.class) public class EventsLogCleanerTest { private static final String PROJECT = "testProject"; + private static final String PLUGIN_NAME = "events-log"; @Mock private EventsLogConfig cfgMock; @Mock private EventsLogCleaner logCleanerMock; @@ -44,7 +45,7 @@ @Before public void setUp() throws Exception { when(event.getProjectName()).thenReturn(PROJECT); - eventsLogCleaner = new EventsLogCleaner(eventsDb, executor); + eventsLogCleaner = new EventsLogCleaner(eventsDb, executor, PLUGIN_NAME); } @Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialectTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialectTest.java new file mode 100644 index 0000000..60b7cb1 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLDialectTest.java
@@ -0,0 +1,38 @@ +// Copyright (C) 2018 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.ericsson.gerrit.plugins.eventslog.sql; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class SQLDialectTest { + @Test + public void defaultIsH2() throws Exception { + assertThat(SQLDialect.fromJdbcUrl("")).isEqualTo(SQLDialect.H2); + assertThat(SQLDialect.fromJdbcUrl("jdbc:")).isEqualTo(SQLDialect.H2); + assertThat(SQLDialect.fromJdbcUrl("jdbc:whatever://")).isEqualTo(SQLDialect.H2); + } + + @Test + public void mysqlIsParsed() throws Exception { + assertThat(SQLDialect.fromJdbcUrl("jdbc:mysql://")).isEqualTo(SQLDialect.MYSQL); + } + + @Test + public void postgresqlIsParsed() throws Exception { + assertThat(SQLDialect.fromJdbcUrl("jdbc:postgresql://")).isEqualTo(SQLDialect.POSTGRESQL); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java index bbc4d05..e759637 100644 --- a/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java +++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLStoreTest.java
@@ -17,6 +17,7 @@ import static com.ericsson.gerrit.plugins.eventslog.sql.SQLTable.TABLE_NAME; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -28,15 +29,14 @@ import com.ericsson.gerrit.plugins.eventslog.ServiceUnavailableException; import com.google.common.collect.ImmutableList; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.client.Project.NameKey; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.events.ProjectEvent; -import com.google.gerrit.server.project.NoSuchProjectException; -import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gson.Gson; import com.google.inject.Provider; import com.zaxxer.hikari.HikariConfig; -import java.io.IOException; import java.net.ConnectException; import java.sql.Connection; import java.sql.DriverManager; @@ -45,9 +45,11 @@ import java.sql.Timestamp; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -67,92 +69,69 @@ private static final String TERM_CONN_MSG = "terminating connection"; private static final String MSG = "message"; private static final String GENERIC_QUERY = "SELECT * FROM " + TABLE_NAME; - private static final boolean PROJECT_VISIBLE_TO_USER = true; - private static final boolean PROJECT_NOT_VISIBLE_TO_USER = false; + private static final String PLUGIN_NAME = "events-log"; - @Mock private ProjectControl.GenericFactory pcFactoryMock; @Mock private Provider<CurrentUser> userProviderMock; @Mock private EventsLogConfig cfgMock; + @Mock private PermissionBackend permissionBackendMock; + @Mock private PermissionBackend.ForProject forProjectMock; + @Mock private PermissionBackend.WithUser withUserMock; @Mock private EventsLogCleaner logCleanerMock; private SQLClient eventsDb; private SQLClient localEventsDb; private SQLStore store; + private ScheduledExecutorService poolMock; private HikariConfig config; - private ScheduledThreadPoolExecutor poolMock; private Statement stat; + private MockEvent mockEvent; @Rule public TemporaryFolder testFolder = new TemporaryFolder(); @Before public void setUp() throws SQLException { config = new HikariConfig(); + config.setJdbcUrl(TEST_URL); config.addDataSourceProperty("DB_CLOSE_DELAY", "-1"); config.addDataSourceProperty("DATABASE_TO_UPPER", "false"); Connection conn = DriverManager.getConnection(TEST_URL + ";" + TEST_OPTIONS); + mockEvent = new MockEvent(); stat = conn.createStatement(); poolMock = new PoolMock(); when(cfgMock.getMaxAge()).thenReturn(5); when(cfgMock.getLocalStorePath()).thenReturn(testFolder.getRoot().toPath()); } - private void tearDown() throws Exception { - stat.execute("DROP TABLE " + TABLE_NAME); + @After + public void tearDown() throws Exception { + stat.execute("DROP TABLE IF EXISTS " + TABLE_NAME); store.stop(); } - private void setUpClient() { - config.setJdbcUrl(TEST_URL); - eventsDb = new SQLClient(config); - config.setJdbcUrl(TEST_LOCAL_URL); - localEventsDb = new SQLClient(config); - store = - new SQLStore( - pcFactoryMock, - userProviderMock, - cfgMock, - eventsDb, - localEventsDb, - poolMock, - logCleanerMock); - store.start(); - } - - private void setUpClientMock() throws SQLException { - eventsDb = mock(SQLClient.class); - localEventsDb = mock(SQLClient.class); - when(localEventsDb.dbExists()).thenReturn(true); - } - @Test public void storeThenQueryVisible() throws Exception { - MockEvent mockEvent = setUpMocks(PROJECT_VISIBLE_TO_USER); + when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock); + when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock); + doNothing().when(forProjectMock).check(ProjectPermission.ACCESS); + setUpClient(); store.storeEvent(mockEvent); List<String> events = store.queryChangeEvents(GENERIC_QUERY); String json = new Gson().toJson(mockEvent); assertThat(events).containsExactly(json).inOrder(); - tearDown(); } @Test public void storeThenQueryNotVisible() throws Exception { - MockEvent mockEvent = setUpMocks(PROJECT_NOT_VISIBLE_TO_USER); + when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock); + when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock); + doThrow(new PermissionBackendException("")) + .when(forProjectMock) + .check(ProjectPermission.ACCESS); + setUpClient(); store.storeEvent(mockEvent); List<String> events = store.queryChangeEvents(GENERIC_QUERY); assertThat(events).isEmpty(); - tearDown(); - } - - private MockEvent setUpMocks(boolean isVisible) throws NoSuchProjectException, IOException { - MockEvent mockEvent = new MockEvent(); - ProjectControl pcMock = mock(ProjectControl.class); - CurrentUser userMock = mock(CurrentUser.class); - when(userProviderMock.get()).thenReturn(userMock); - when(pcFactoryMock.controlFor(mockEvent.getProjectNameKey(), userMock)).thenReturn(pcMock); - when(pcMock.isVisible()).thenReturn(isVisible); - setUpClient(); - return mockEvent; } @Test(expected = MalformedQueryException.class) @@ -163,40 +142,20 @@ } @Test - public void notReturnEventOfNonExistingProject() throws Exception { - MockEvent mockEvent = new MockEvent(); - Project.NameKey projectMock = mock(Project.NameKey.class); - CurrentUser userMock = mock(CurrentUser.class); - when(userProviderMock.get()).thenReturn(userMock); - NameKey projectNameKey = mockEvent.getProjectNameKey(); - doThrow(new NoSuchProjectException(projectMock)) - .when(pcFactoryMock) - .controlFor(projectNameKey, userMock); - setUpClient(); - store.storeEvent(mockEvent); - List<String> events = store.queryChangeEvents(GENERIC_QUERY); - assertThat(events).isEmpty(); - verify(logCleanerMock).removeProjectEventsAsync(mockEvent.project); - tearDown(); - } - - @Test public void notReturnEventWithNoVisibilityInfo() throws Exception { - MockEvent mockEvent = new MockEvent(); - CurrentUser userMock = mock(CurrentUser.class); - when(userProviderMock.get()).thenReturn(userMock); - NameKey projectNameKey = mockEvent.getProjectNameKey(); - doThrow(new IOException()).when(pcFactoryMock).controlFor(projectNameKey, userMock); + when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock); + when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock); + doThrow(new PermissionBackendException("")) + .when(forProjectMock) + .check(ProjectPermission.ACCESS); setUpClient(); store.storeEvent(mockEvent); List<String> events = store.queryChangeEvents(GENERIC_QUERY); assertThat(events).isEmpty(); - tearDown(); } @Test public void retryOnConnectException() throws Exception { - MockEvent mockEvent = new MockEvent(); when(cfgMock.getMaxTries()).thenReturn(3); Throwable[] exceptions = new Throwable[3]; Arrays.fill(exceptions, new SQLException(new ConnectException())); @@ -205,13 +164,15 @@ doThrow(exceptions).doNothing().when(eventsDb).queryOne(); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); store.storeEvent(mockEvent); verify(eventsDb, times(3)).storeEvent(mockEvent); @@ -220,7 +181,6 @@ @Test public void retryOnMessage() throws Exception { - MockEvent mockEvent = new MockEvent(); when(cfgMock.getMaxTries()).thenReturn(3); Throwable[] exceptions = new Throwable[3]; Arrays.fill(exceptions, new SQLException(TERM_CONN_MSG)); @@ -229,13 +189,15 @@ doThrow(exceptions).doNothing().when(eventsDb).queryOne(); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); store.storeEvent(mockEvent); verify(eventsDb, times(3)).storeEvent(mockEvent); @@ -244,19 +206,20 @@ @Test public void noRetryOnMessage() throws Exception { - MockEvent mockEvent = new MockEvent(); when(cfgMock.getMaxTries()).thenReturn(3); setUpClientMock(); doThrow(new SQLException(MSG)).when(eventsDb).storeEvent(mockEvent); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); store.storeEvent(mockEvent); verify(eventsDb, times(1)).storeEvent(mockEvent); @@ -264,7 +227,6 @@ @Test public void noRetryOnZeroMaxTries() throws Exception { - MockEvent mockEvent = new MockEvent(); when(cfgMock.getMaxTries()).thenReturn(0); Throwable[] exceptions = new Throwable[3]; Arrays.fill(exceptions, new SQLException(new ConnectException())); @@ -273,13 +235,15 @@ doThrow(exceptions).doNothing().when(eventsDb).queryOne(); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); store.storeEvent(mockEvent); verify(eventsDb, times(1)).storeEvent(mockEvent); @@ -287,38 +251,32 @@ @Test(expected = ServiceUnavailableException.class) public void throwSQLExceptionIfNotOnline() throws Exception { - MockEvent mockEvent = new MockEvent(); setUpClientMock(); doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated(); doThrow(new SQLException()).when(eventsDb).queryOne(); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); store.storeEvent(mockEvent); store.queryChangeEvents(GENERIC_QUERY); } @Test - public void restoreFromLocalAndRemoveUnfoundProjectEvents() throws Exception { + public void restoreEventsFromLocalDb() throws Exception { MockEvent mockEvent = new MockEvent(); MockEvent mockEvent2 = new MockEvent("proj"); - MockEvent mockEvent3 = new MockEvent("unfound"); - - ProjectControl pc = mock(ProjectControl.class); - NoSuchProjectException e = mock(NoSuchProjectException.class); - CurrentUser userMock = mock(CurrentUser.class); - when(userProviderMock.get()).thenReturn(userMock); - when(pcFactoryMock.controlFor((mockEvent.getProjectNameKey()), userMock)).thenReturn(pc); - when(pcFactoryMock.controlFor((mockEvent2.getProjectNameKey()), userMock)).thenReturn(pc); - when(pc.isVisible()).thenReturn(true); - doThrow(e).when(pcFactoryMock).controlFor((mockEvent3.getProjectNameKey()), userMock); + when(permissionBackendMock.user(userProviderMock)).thenReturn(withUserMock); + when(withUserMock.project(any(Project.NameKey.class))).thenReturn(forProjectMock); + doNothing().when(forProjectMock).check(ProjectPermission.ACCESS); config.setJdbcUrl(TEST_URL); eventsDb = new SQLClient(config); @@ -326,25 +284,25 @@ localEventsDb = new SQLClient(config); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); localEventsDb.createDBIfNotCreated(); localEventsDb.storeEvent(mockEvent); localEventsDb.storeEvent(mockEvent2); - localEventsDb.storeEvent(mockEvent3); store.start(); + List<String> events = store.queryChangeEvents(GENERIC_QUERY); Gson gson = new Gson(); String json = gson.toJson(mockEvent); String json2 = gson.toJson(mockEvent2); assertThat(events).containsExactly(json, json2).inOrder(); - tearDown(); } @Test @@ -354,32 +312,35 @@ doThrow(new SQLException()).when(eventsDb).queryOne(); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); verify(localEventsDb).createDBIfNotCreated(); } @Test public void storeLocalOffline() throws Exception { - MockEvent mockEvent = new MockEvent(); setUpClientMock(); doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated(); doThrow(new SQLException()).when(eventsDb).queryOne(); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); store.storeEvent(mockEvent); verify(localEventsDb).storeEvent(mockEvent); @@ -387,25 +348,48 @@ @Test public void storeLocalOfflineAfterNoRetry() throws Exception { - MockEvent mockEvent = new MockEvent(); setUpClientMock(); when(cfgMock.getMaxTries()).thenReturn(0); doThrow(new SQLException(new ConnectException())).when(eventsDb).createDBIfNotCreated(); doThrow(new SQLException()).when(eventsDb).queryOne(); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); store.storeEvent(mockEvent); verify(localEventsDb).storeEvent(mockEvent); } + private void setUpClient() { + eventsDb = new SQLClient(config); + localEventsDb = new SQLClient(config); + store = + new SQLStore( + userProviderMock, + cfgMock, + eventsDb, + localEventsDb, + poolMock, + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); + } + + private void setUpClientMock() throws SQLException { + eventsDb = mock(SQLClient.class); + localEventsDb = mock(SQLClient.class); + when(localEventsDb.dbExists()).thenReturn(true); + } + /** * For this test we expect that if we can connect to main database, then we should come back * online and try setting up again. We just want to make sure that restoreEventsFromLocal gets @@ -420,15 +404,18 @@ when(localEventsDb.getAll()).thenReturn(ImmutableList.of(mock(SQLEntry.class))); store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); - poolMock.scheduleWithFixedDelay(store.new CheckConnectionTask(), 0, 0, TimeUnit.MILLISECONDS); + poolMock.scheduleWithFixedDelay( + store.new CheckConnectionTask(PLUGIN_NAME), 0, 0, TimeUnit.MILLISECONDS); verify(localEventsDb, times(2)).removeOldEvents(0); } @@ -443,7 +430,6 @@ } private void checkConnectionAndRestore(boolean copy) throws Exception { - MockEvent mockEvent = new MockEvent(); eventsDb = mock(SQLClient.class); config.setJdbcUrl(TEST_LOCAL_URL); localEventsDb = new SQLClient(config); @@ -460,13 +446,15 @@ store = new SQLStore( - pcFactoryMock, userProviderMock, cfgMock, eventsDb, localEventsDb, poolMock, - logCleanerMock); + permissionBackendMock, + logCleanerMock, + PLUGIN_NAME); + store.start(); verify(eventsDb).queryOne(); verify(eventsDb).storeEvent(any(String.class), any(Timestamp.class), any(String.class));