Replace Apache commons-dbcp with HikariCP

As some benchmarks show [1], [2], [3], HikariCP is a lot faster than the
old commons-dcbp version inherited from the core dependencies, and with
minimal configuration: besides "sane" defaults, checking for invalid
connections is done automatically every time the pool is asked for a new
connection. Likewise, it tracks and closes abandoned connections, clears
connections before returning them to the client and resets auto-commit,
transaction isolation and read-only status for every connection.

Besides that, the library avoids locks as much as possible and the code
base is small. Projects like Liferay [4], Splunk, the Play framework and
even some Apache projects [5] use HikariCP.

[1] https://beansroasted.wordpress.com/tag/connection-pool-comparison/
[2] http://www.trustiv.co.uk/2014/06/battle-connection-pools
[3] https://github.com/brettwooldridge/HikariCP#jmh-benchmarks-checkered_flag
[4] https://community.liferay.com/blogs/-/blogs/tomcat-hikaricp
[5] https://github.com/brettwooldridge/HikariCP/blob/dev/documents/Wall-of-Fame.md

Change-Id: I79fe729e668fedf246f32b88a14a1c2c437cc8b0
diff --git a/BUILD b/BUILD
index cd34b32..04f201a 100644
--- a/BUILD
+++ b/BUILD
@@ -17,6 +17,7 @@
         "Gerrit-HttpModule: com.ericsson.gerrit.plugins.eventslog.HttpModule",
     ],
     resources = glob(["src/main/resources/**/*"]),
+    deps = ["@hikaricp//jar"],
 )
 
 junit_tests(
@@ -36,5 +37,6 @@
     exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
         ":events-log__plugin",
         "@mockito//jar",
+        "@hikaricp//jar",
     ],
 )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 6650dc0..4ca722d 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -31,3 +31,9 @@
         artifact = "org.objenesis:objenesis:2.6",
         sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
     )
+
+    maven_jar(
+        name = "hikaricp",
+        artifact = "com.zaxxer:HikariCP:3.2.0",
+        sha1 = "6c66db1c636ee90beb4c65fe34abd8ba9396bca6",
+    )
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfig.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfig.java
index f4217bb..c1a9b4a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfig.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfig.java
@@ -14,7 +14,6 @@
 
 package com.ericsson.gerrit.plugins.eventslog;
 
-import com.google.common.base.Joiner;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -40,7 +39,6 @@
   static final String CONFIG_PASSWORD = "storePassword";
   static final String CONFIG_WAIT_TIME = "retryTimeout";
   static final String CONFIG_CONN_TIME = "connectTimeout";
-  static final String CONFIG_EVICT_IDLE_TIME = "evictIdleTime";
   static final String CONFIG_MAX_CONNECTIONS = "maxConnections";
 
   static final boolean DEFAULT_COPY_LOCAL = false;
@@ -49,7 +47,6 @@
   static final int DEFAULT_RETURN_LIMIT = 5000;
   static final int DEFAULT_WAIT_TIME = 1000;
   static final int DEFAULT_CONN_TIME = 1000;
-  static final int DEFAULT_EVICT_IDLE_TIME = 1000 * 60;
   static final int DEFAULT_MAX_CONNECTIONS = 8;
 
   private boolean copyLocal;
@@ -60,10 +57,9 @@
   private int connectTime;
   private String storeUrl;
   private Path localStorePath;
-  private String urlOptions;
+  private String[] urlOptions;
   private String storeUsername;
   private String storePassword;
-  private int evictIdleTime;
   private int maxConnections;
 
   @Inject
@@ -80,10 +76,9 @@
         Paths.get(
             cfg.getString(
                 CONFIG_LOCAL_PATH, site.site_path.resolve("events-db").normalize().toString()));
-    urlOptions = Joiner.on(";").join(cfg.getStringList(CONFIG_URL_OPTIONS));
+    urlOptions = cfg.getStringList(CONFIG_URL_OPTIONS);
     storeUsername = cfg.getString(CONFIG_USERNAME);
     storePassword = cfg.getString(CONFIG_PASSWORD);
-    evictIdleTime = cfg.getInt(CONFIG_EVICT_IDLE_TIME, DEFAULT_EVICT_IDLE_TIME);
     maxConnections = Math.max(cfg.getInt(CONFIG_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS), 1);
   }
 
@@ -107,7 +102,7 @@
     return storeUrl;
   }
 
-  public String getUrlOptions() {
+  public String[] getUrlOptions() {
     return urlOptions;
   }
 
@@ -131,10 +126,6 @@
     return copyLocal;
   }
 
-  public int getEvictIdleTime() {
-    return evictIdleTime;
-  }
-
   public int getMaxConnections() {
     return maxConnections;
   }
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 2519eda..05f663f 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
@@ -33,6 +33,8 @@
 import com.google.gerrit.server.events.SupplierSerializer;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
 import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -40,7 +42,6 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
-import org.apache.commons.dbcp.BasicDataSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -49,58 +50,13 @@
   private final Gson gson;
   private final boolean isPostgresql;
 
-  private BasicDataSource ds;
+  private HikariDataSource ds;
 
-  SQLClient(String storeUrl, String urlOptions) {
-    ds = new BasicDataSource();
-    ds.setUrl(storeUrl);
-    ds.setConnectionProperties(urlOptions);
-    ds.setMaxWait(MILLISECONDS.convert(30, SECONDS));
-    ds.setTestOnBorrow(true);
-    ds.setValidationQuery("SELECT 1");
-    ds.setValidationQueryTimeout(5);
+  public SQLClient(HikariConfig config) {
+    ds = new HikariDataSource(config);
 
     gson = new GsonBuilder().registerTypeAdapter(Supplier.class, new SupplierSerializer()).create();
-    isPostgresql = storeUrl.contains("postgresql");
-  }
-
-  /**
-   * Set the username to connect to the database.
-   *
-   * @param username the username as a string
-   */
-  void setUsername(String username) {
-    ds.setUsername(username);
-  }
-
-  /**
-   * Set the password to connect to the database.
-   *
-   * @param password the password as a string
-   */
-  void setPassword(String password) {
-    ds.setPassword(password);
-  }
-
-  /**
-   * Set the time before an idle connection is evicted as well as the time between eviction runs.
-   *
-   * @param evictIdleTime the time in milliseconds before eviction
-   */
-  void setEvictIdleTime(int evictIdleTime) {
-    ds.setMinEvictableIdleTimeMillis(evictIdleTime);
-    ds.setTimeBetweenEvictionRunsMillis(evictIdleTime / 2);
-  }
-
-  void setMaxConnections(int maxConnections) {
-    ds.setMaxActive(maxConnections);
-    ds.setMinIdle(maxConnections / 4);
-    int maxIdle = maxConnections / 2;
-    if (maxIdle == 0) {
-      maxIdle = maxConnections;
-    }
-    ds.setMaxIdle(maxIdle);
-    ds.setInitialSize(ds.getMinIdle());
+    isPostgresql = config.getJdbcUrl().contains("postgresql");
   }
 
   /**
@@ -127,11 +83,7 @@
   }
 
   void close() {
-    try {
-      ds.close();
-    } catch (SQLException e) {
-      log.warn("Cannot close datasource", e);
-    }
+    ds.close();
   }
 
   /**
@@ -191,9 +143,9 @@
               TABLE_NAME,
               DATE_ENTRY,
               new Timestamp(System.currentTimeMillis() - MILLISECONDS.convert(maxAge, DAYS))));
-      log.info("Events older than {} days were removed from database", maxAge);
+      log.info("Events older than {} days were removed from database {}", maxAge, ds.getPoolName());
     } catch (SQLException e) {
-      log.warn("Cannot remove old event entries from database", e);
+      log.warn("Cannot remove old event entries from database {}", ds.getPoolName(), e);
     }
   }
 
diff --git a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLModule.java b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLModule.java
index 1088b15..df7f73b 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/eventslog/sql/SQLModule.java
@@ -20,11 +20,13 @@
 import com.ericsson.gerrit.plugins.eventslog.EventStore;
 import com.ericsson.gerrit.plugins.eventslog.EventsLogConfig;
 import com.ericsson.gerrit.plugins.eventslog.QueryMaker;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
 import com.google.inject.internal.UniqueAnnotations;
+import com.zaxxer.hikari.HikariConfig;
 
 class SQLModule extends AbstractModule {
 
@@ -39,24 +41,34 @@
   @Provides
   @Singleton
   @EventsDb
-  SQLClient provideSqlClient(EventsLogConfig cfg) {
-    SQLClient sqlClient = new SQLClient(cfg.getStoreUrl(), cfg.getUrlOptions());
-    sqlClient.setUsername(cfg.getStoreUsername());
-    sqlClient.setPassword(cfg.getStorePassword());
-    sqlClient.setEvictIdleTime(cfg.getEvictIdleTime());
-    sqlClient.setMaxConnections(cfg.getMaxConnections());
-    return sqlClient;
+  SQLClient provideSqlClient(EventsLogConfig cfg, @PluginName String pluginName) {
+    HikariConfig dsConfig = new HikariConfig();
+    dsConfig.setJdbcUrl(cfg.getStoreUrl());
+    dsConfig.setUsername(cfg.getStoreUsername());
+    dsConfig.setPassword(cfg.getStorePassword());
+    dsConfig.setPoolName("[" + pluginName + "] EventsDb");
+    dsConfig.setMaximumPoolSize(cfg.getMaxConnections());
+    setDataSourceOptions(cfg, dsConfig);
+    return new SQLClient(dsConfig);
   }
 
   @Provides
   @Singleton
   @LocalEventsDb
-  SQLClient provideLocalSqlClient(EventsLogConfig cfg) {
-    SQLClient sqlClient =
-        new SQLClient(
-            H2_DB_PREFIX + cfg.getLocalStorePath().resolve(SQLTable.TABLE_NAME),
-            cfg.getUrlOptions());
-    sqlClient.setEvictIdleTime(cfg.getEvictIdleTime());
-    return sqlClient;
+  SQLClient provideLocalSqlClient(EventsLogConfig cfg, @PluginName String pluginName) {
+    HikariConfig dsConfig = new HikariConfig();
+    dsConfig.setJdbcUrl(H2_DB_PREFIX + cfg.getLocalStorePath().resolve(SQLTable.TABLE_NAME));
+    dsConfig.setPoolName("[" + pluginName + "] LocalEventsDb");
+    setDataSourceOptions(cfg, dsConfig);
+    return new SQLClient(dsConfig);
+  }
+
+  private void setDataSourceOptions(EventsLogConfig cfg, HikariConfig dsConfig) {
+    for (String option : cfg.getUrlOptions()) {
+      int equalsPos = option.indexOf('=');
+      String key = option.substring(0, equalsPos);
+      String value = option.substring(equalsPos + 1);
+      dsConfig.addDataSourceProperty(key, value);
+    }
   }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index e019e8a..4421212 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -67,11 +67,6 @@
      not be deleted and must be removed manually. When not specified, the default
      value is set to false.
 
-plugin.@PLUGIN@.evictIdleTime
-:    Interval of time in milliseconds after which an idle database connection is
-     evicted from the connection pool. When not specified, the default value is
-     set to 60000ms.
-
 plugin.@PLUGIN@.maxConnections
 :    Maximum number of instances in the connection pool to the database. Includes
      active and idle connections. By default 8.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfigTest.java b/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfigTest.java
index 484d156..702328b 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfigTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/eventslog/EventsLogConfigTest.java
@@ -16,7 +16,6 @@
 
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_CONN_TIME;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_COPY_LOCAL;
-import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_EVICT_IDLE_TIME;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_LOCAL_PATH;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_MAX_AGE;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_MAX_CONNECTIONS;
@@ -28,7 +27,6 @@
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_USERNAME;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.CONFIG_WAIT_TIME;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.DEFAULT_CONN_TIME;
-import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.DEFAULT_EVICT_IDLE_TIME;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.DEFAULT_MAX_AGE;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.DEFAULT_MAX_CONNECTIONS;
 import static com.ericsson.gerrit.plugins.eventslog.EventsLogConfig.DEFAULT_MAX_TRIES;
@@ -37,7 +35,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.Mockito.when;
 
-import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
@@ -59,7 +56,6 @@
   private static final String LOCAL_STORE_PATH = "~/gerrit/events-db/";
   private static final String PLUGIN = "plugin";
   private static final String PLUGIN_NAME = "eventsLog";
-  private static final int CUSTOM_EVICT_IDLE_TIME = 10000;
   private static final int CUSTOM_MAX_CONNECTIONS = 32;
   private static final List<String> urlOptions = ImmutableList.of("DB_CLOSE_DELAY=10");
 
@@ -93,7 +89,6 @@
     assertThat(eventsLogConfig.getUrlOptions()).isEmpty();
     assertThat(eventsLogConfig.getStoreUsername()).isNull();
     assertThat(eventsLogConfig.getStorePassword()).isNull();
-    assertThat(eventsLogConfig.getEvictIdleTime()).isEqualTo(DEFAULT_EVICT_IDLE_TIME);
     assertThat(eventsLogConfig.getMaxConnections()).isEqualTo(DEFAULT_MAX_CONNECTIONS);
   }
 
@@ -110,10 +105,9 @@
     assertThat(eventsLogConfig.getWaitTime()).isEqualTo(5000);
     assertThat(eventsLogConfig.getLocalStorePath().toString() + "/").isEqualTo(LOCAL_STORE_PATH);
     assertThat(eventsLogConfig.getStoreUrl()).isEqualTo("jdbc:h2:~/gerrit/db");
-    assertThat(eventsLogConfig.getUrlOptions()).isEqualTo(Joiner.on(";").join(urlOptions));
+    assertThat(eventsLogConfig.getUrlOptions()).asList().isEqualTo(urlOptions);
     assertThat(eventsLogConfig.getStoreUsername()).isEqualTo("testUsername");
     assertThat(eventsLogConfig.getStorePassword()).isEqualTo("testPassword");
-    assertThat(eventsLogConfig.getEvictIdleTime()).isEqualTo(CUSTOM_EVICT_IDLE_TIME);
     assertThat(eventsLogConfig.getMaxConnections()).isEqualTo(CUSTOM_MAX_CONNECTIONS);
   }
 
@@ -130,7 +124,6 @@
     config.setStringList(PLUGIN, PLUGIN_NAME, CONFIG_URL_OPTIONS, urlOptions);
     config.setString(PLUGIN, PLUGIN_NAME, CONFIG_USERNAME, "testUsername");
     config.setString(PLUGIN, PLUGIN_NAME, CONFIG_PASSWORD, "testPassword");
-    config.setInt(PLUGIN, PLUGIN_NAME, CONFIG_EVICT_IDLE_TIME, CUSTOM_EVICT_IDLE_TIME);
     config.setInt(PLUGIN, PLUGIN_NAME, CONFIG_MAX_CONNECTIONS, CUSTOM_MAX_CONNECTIONS);
     return config;
   }
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 3b0a19e..bbc4d05 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
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.project.ProjectControl;
 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;
@@ -77,6 +78,7 @@
   private SQLClient eventsDb;
   private SQLClient localEventsDb;
   private SQLStore store;
+  private HikariConfig config;
   private ScheduledThreadPoolExecutor poolMock;
 
   private Statement stat;
@@ -85,6 +87,9 @@
 
   @Before
   public void setUp() throws SQLException {
+    config = new HikariConfig();
+    config.addDataSourceProperty("DB_CLOSE_DELAY", "-1");
+    config.addDataSourceProperty("DATABASE_TO_UPPER", "false");
     Connection conn = DriverManager.getConnection(TEST_URL + ";" + TEST_OPTIONS);
     stat = conn.createStatement();
     poolMock = new PoolMock();
@@ -98,8 +103,10 @@
   }
 
   private void setUpClient() {
-    eventsDb = new SQLClient(TEST_URL, TEST_OPTIONS);
-    localEventsDb = new SQLClient(TEST_LOCAL_URL, TEST_OPTIONS);
+    config.setJdbcUrl(TEST_URL);
+    eventsDb = new SQLClient(config);
+    config.setJdbcUrl(TEST_LOCAL_URL);
+    localEventsDb = new SQLClient(config);
     store =
         new SQLStore(
             pcFactoryMock,
@@ -313,8 +320,10 @@
     when(pc.isVisible()).thenReturn(true);
     doThrow(e).when(pcFactoryMock).controlFor((mockEvent3.getProjectNameKey()), userMock);
 
-    eventsDb = new SQLClient(TEST_URL, TEST_OPTIONS);
-    localEventsDb = new SQLClient(TEST_LOCAL_URL, TEST_OPTIONS);
+    config.setJdbcUrl(TEST_URL);
+    eventsDb = new SQLClient(config);
+    config.setJdbcUrl(TEST_LOCAL_URL);
+    localEventsDb = new SQLClient(config);
     store =
         new SQLStore(
             pcFactoryMock,
@@ -404,7 +413,8 @@
    */
   @Test
   public void testConnectionTask() throws Exception {
-    eventsDb = new SQLClient(TEST_URL, TEST_OPTIONS);
+    config.setJdbcUrl(TEST_URL);
+    eventsDb = new SQLClient(config);
     localEventsDb = mock(SQLClient.class);
     when(localEventsDb.dbExists()).thenReturn(true);
     when(localEventsDb.getAll()).thenReturn(ImmutableList.of(mock(SQLEntry.class)));
@@ -435,7 +445,8 @@
   private void checkConnectionAndRestore(boolean copy) throws Exception {
     MockEvent mockEvent = new MockEvent();
     eventsDb = mock(SQLClient.class);
-    localEventsDb = new SQLClient(TEST_LOCAL_URL, TEST_OPTIONS);
+    config.setJdbcUrl(TEST_LOCAL_URL);
+    localEventsDb = new SQLClient(config);
     localEventsDb.createDBIfNotCreated();
     localEventsDb.storeEvent(mockEvent);
     doThrow(new SQLException(new ConnectException()))