Initialize sync-index plugin with first version Sync-index plugin keeps the secondary index synchronized among two Gerrit instances that share the same git repositories and Gerrit database. Using a new extension point in Gerrit core [1], this plugin is notified every time a change has been indexed in or removed from the secondary index. Then it propagates that information to the configured target Gerrit instance. The sync-index plugin installed in the target instance updates the corresponding secondary index accordingly. [1] https://gerrit-review.googlesource.com/#/c/72607/ Change-Id: Id5c089cfe3263f23dd56880e47d609180aeb3edc
diff --git a/.buckconfig b/.buckconfig new file mode 100644 index 0000000..2b818b6 --- /dev/null +++ b/.buckconfig
@@ -0,0 +1,16 @@ +[alias] + sync-index = //:sync-index + plugin = //:sync-index + src = //:sync-index-sources + +[java] + jar_spool_mode = direct_to_jar + src_roots = java, resources + +[project] + ignore = .git, eclipse-out/ + parallel_parsing = true + +[cache] + mode = dir + dir = buck-out/cache
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f7fd4b --- /dev/null +++ b/.gitignore
@@ -0,0 +1,8 @@ +/.buckversion +/.classpath +/.project +/.settings/ +/.watchmanconfig +/buck-out/ +/bucklets +/eclipse-out/
diff --git a/BUCK b/BUCK new file mode 100644 index 0000000..163c42f --- /dev/null +++ b/BUCK
@@ -0,0 +1,53 @@ +include_defs('//bucklets/gerrit_plugin.bucklet') +include_defs('//bucklets/java_sources.bucklet') +include_defs('//bucklets/maven_jar.bucklet') + +SOURCES = glob(['src/main/java/**/*.java']) +RESOURCES = glob(['src/main/resources/**/*']) + +TEST_DEPS = GERRIT_PLUGIN_API + GERRIT_TESTS + [ + ':sync-index__plugin', + ':wiremock', +] + +gerrit_plugin( + name = 'sync-index', + srcs = SOURCES, + resources = RESOURCES, + manifest_entries = [ + 'Gerrit-PluginName: sync-index', + 'Gerrit-ApiType: plugin', + 'Gerrit-Module: com.ericsson.gerrit.plugins.syncindex.Module', + 'Gerrit-HttpModule: com.ericsson.gerrit.plugins.syncindex.HttpModule', + 'Implementation-Title: sync-index plugin', + 'Implementation-URL: https://gerrit.ericsson.se/#/admin/projects/gerrit/plugins/sync-index', + 'Implementation-Vendor: Ericsson', + ], + provided_deps = GERRIT_TESTS + [':wiremock',], +) + +java_sources( + name = 'sync-index-sources', + srcs = SOURCES + RESOURCES, +) + +java_library( + name = 'classpath', + deps = TEST_DEPS, +) + +java_test( + name = 'sync-index_tests', + srcs = glob(['src/test/java/**/*.java']), + labels = ['sync-index'], + source_under_test = [':sync-index__plugin'], + deps = TEST_DEPS, +) + +maven_jar( + name = 'wiremock', + id = 'com.github.tomakehurst:wiremock:1.58:standalone', + sha1 = '21c8386a95c5dc54a9c55839c5a95083e42412ae', + license = 'Apache2.0', + attach_source = False, +)
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK new file mode 100644 index 0000000..f30d88f --- /dev/null +++ b/lib/gerrit/BUCK
@@ -0,0 +1,20 @@ +include_defs('//bucklets/maven_jar.bucklet') + +VER = '2.13-SNAPSHOT' +REPO = MAVEN_LOCAL + +maven_jar( + name = 'acceptance-framework', + id = 'com.google.gerrit:gerrit-acceptance-framework:' + VER, + license = 'Apache2.0', + attach_source = False, + repository = REPO, +) + +maven_jar( + name = 'plugin-api', + id = 'com.google.gerrit:gerrit-plugin-api:' + VER, + license = 'Apache2.0', + attach_source = False, + repository = REPO, +)
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java new file mode 100644 index 0000000..a93ba84 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Configuration.java
@@ -0,0 +1,86 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +class Configuration { + private static final int DEFAULT_TIMEOUT_MS = 5000; + private static final int DEFAULT_MAX_TRIES = 5; + private static final int DEFAULT_RETRY_INTERVAL = 1000; + private static final int DEFAULT_THREAD_POOL_SIZE = 1; + + private final String url; + private final String user; + private final String password; + private final int connectionTimeout; + private final int socketTimeout; + private final int maxTries; + private final int retryInterval; + private final int threadPoolSize; + + @Inject + Configuration(PluginConfigFactory config, + @PluginName String pluginName) { + PluginConfig cfg = config.getFromGerritConfig(pluginName, true); + url = Strings.nullToEmpty(cfg.getString("url")); + user = Strings.nullToEmpty(cfg.getString("user")); + password = Strings.nullToEmpty(cfg.getString("password")); + connectionTimeout = cfg.getInt("connectionTimeout", DEFAULT_TIMEOUT_MS); + socketTimeout = cfg.getInt("socketTimeout", DEFAULT_TIMEOUT_MS); + maxTries = cfg.getInt("maxTries", DEFAULT_MAX_TRIES); + retryInterval = cfg.getInt("retryInterval", DEFAULT_RETRY_INTERVAL); + threadPoolSize = cfg.getInt("threadPoolSize", DEFAULT_THREAD_POOL_SIZE); + } + + int getConnectionTimeout() { + return connectionTimeout; + } + + int getMaxTries() { + return maxTries; + } + + int getRetryInterval() { + return retryInterval; + } + + int getSocketTimeout() { + return socketTimeout; + } + + String getUrl() { + return CharMatcher.is('/').trimTrailingFrom(url); + } + + String getUser() { + return user; + } + + String getPassword() { + return password; + } + + public int getThreadPoolSize() { + return threadPoolSize; + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java new file mode 100644 index 0000000..67abc1e --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Context.java
@@ -0,0 +1,43 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +/** + * Allows to tag a forwarded event to avoid infinitely looping events. + */ +class Context { + private static final ThreadLocal<Boolean> FORWARDED_EVENT = + new ThreadLocal<Boolean>() { + @Override + protected Boolean initialValue() { + return false; + } + }; + + private Context() { + } + + static Boolean isForwardedEvent() { + return FORWARDED_EVENT.get(); + } + + static void setForwardedEvent(Boolean b) { + FORWARDED_EVENT.set(b); + } + + static void unsetForwardedEvent() { + FORWARDED_EVENT.remove(); + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java new file mode 100644 index 0000000..c36370c --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProvider.java
@@ -0,0 +1,193 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.ServiceUnavailableRetryStrategy; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Provides an HTTP client with SSL capabilities. + */ +class HttpClientProvider implements Provider<CloseableHttpClient> { + private static final Logger log = LoggerFactory + .getLogger(HttpClientProvider.class); + private static final int CONNECTIONS_PER_ROUTE = 100; + // Up to 2 target instances with the max number of connections per host: + private static final int MAX_CONNECTIONS = 2 * CONNECTIONS_PER_ROUTE; + + private static final int ERROR_CODES = 500; + private static final int MAX_CONNECTION_INACTIVITY = 10000; + + private final Configuration cfg; + private final SSLConnectionSocketFactory sslSocketFactory; + + @Inject + HttpClientProvider(Configuration cfg) { + this.cfg = cfg; + this.sslSocketFactory = buildSslSocketFactory(); + } + + @Override + public CloseableHttpClient get() { + return HttpClients.custom().setSSLSocketFactory(sslSocketFactory) + .setConnectionManager(customConnectionManager()) + .setDefaultCredentialsProvider(buildCredentials()) + .setDefaultRequestConfig(customRequestConfig()) + .setRetryHandler(customRetryHandler()) + .setServiceUnavailableRetryStrategy(customServiceUnavailRetryStrategy()) + .build(); + } + + private RequestConfig customRequestConfig() { + return RequestConfig.custom().setConnectTimeout(cfg.getConnectionTimeout()) + .setSocketTimeout(cfg.getSocketTimeout()) + .setConnectionRequestTimeout(cfg.getConnectionTimeout()) + .build(); + } + + private HttpRequestRetryHandler customRetryHandler() { + return new HttpRequestRetryHandler() { + + @Override + public boolean retryRequest(IOException exception, int executionCount, + HttpContext context) { + if (executionCount > cfg.getMaxTries() + || exception instanceof SSLException) { + return false; + } + logRetry(exception.getMessage()); + try { + Thread.sleep(cfg.getRetryInterval()); + } catch (InterruptedException e) { + log.debug("Ignoring InterruptedException", e); + } + return true; + } + }; + } + + private ServiceUnavailableRetryStrategy customServiceUnavailRetryStrategy() { + return new ServiceUnavailableRetryStrategy() { + @Override + public boolean retryRequest(HttpResponse response, int executionCount, + HttpContext context) { + if (executionCount > cfg.getMaxTries()) { + return false; + } + if (response.getStatusLine().getStatusCode() >= ERROR_CODES) { + logRetry(response.getStatusLine().getReasonPhrase()); + return true; + } + return false; + } + + @Override + public long getRetryInterval() { + return cfg.getRetryInterval(); + } + }; + } + + private void logRetry(String cause) { + log.debug("Retrying request to '" + cfg.getUrl() + "' Cause: " + cause); + } + + private HttpClientConnectionManager customConnectionManager() { + Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder + .<ConnectionSocketFactory> create().register("https", sslSocketFactory) + .register("http", PlainConnectionSocketFactory.INSTANCE).build(); + PoolingHttpClientConnectionManager connManager = + new PoolingHttpClientConnectionManager(socketFactoryRegistry); + connManager.setDefaultMaxPerRoute(CONNECTIONS_PER_ROUTE); + connManager.setMaxTotal(MAX_CONNECTIONS); + connManager.setValidateAfterInactivity(MAX_CONNECTION_INACTIVITY); + return connManager; + } + + private SSLConnectionSocketFactory buildSslSocketFactory() { + return new SSLConnectionSocketFactory(buildSslContext(), + NoopHostnameVerifier.INSTANCE); + } + + private SSLContext buildSslContext() { + try { + TrustManager[] trustAllCerts = + new TrustManager[] {new DummyX509TrustManager()}; + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustAllCerts, null); + return context; + } catch (KeyManagementException | NoSuchAlgorithmException e) { + log.warn("Error building SSLContext object", e); + return null; + } + } + + private BasicCredentialsProvider buildCredentials() { + URI uri = URI.create(cfg.getUrl()); + BasicCredentialsProvider creds = new BasicCredentialsProvider(); + creds.setCredentials(new AuthScope(uri.getHost(), uri.getPort()), + new UsernamePasswordCredentials(cfg.getUser(), cfg.getPassword())); + return creds; + } + + private static class DummyX509TrustManager implements X509TrustManager { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // no check + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // no check + } + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpModule.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpModule.java new file mode 100644 index 0000000..334c195 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpModule.java
@@ -0,0 +1,24 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.gerrit.httpd.plugins.HttpPluginModule; + +class HttpModule extends HttpPluginModule { + @Override + protected void configureServlets() { + serveRegex("/index/\\d+$").with(SyncIndexRestApiServlet.class); + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java new file mode 100644 index 0000000..159acad --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/HttpSession.java
@@ -0,0 +1,47 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.inject.Inject; + +import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult; + +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.IOException; + +class HttpSession { + private final CloseableHttpClient httpClient; + private final String url; + + @Inject + HttpSession(CloseableHttpClient httpClient, + @SyncUrl String url) { + this.httpClient = httpClient; + this.url = url; + } + + IndexResult post(String endpoint) throws IOException { + return httpClient.execute(new HttpPost(url + endpoint), + new IndexResponseHandler()); + } + + IndexResult delete(String endpoint) throws IOException { + return httpClient.execute(new HttpDelete(url + endpoint), + new IndexResponseHandler()); + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java new file mode 100644 index 0000000..f8e2f31 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandler.java
@@ -0,0 +1,103 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.common.base.Objects; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.extensions.events.ChangeIndexedListener; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.inject.Inject; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; + +class IndexEventHandler implements ChangeIndexedListener { + private final Executor executor; + private final RestSession restClient; + private final String pluginName; + private final Set<SyncIndexTask> queuedTasks = Collections + .newSetFromMap(new ConcurrentHashMap<SyncIndexTask, Boolean>()); + + @Inject + IndexEventHandler(@SyncIndexExecutor Executor executor, + @PluginName String pluginName, + RestSession restClient) { + this.restClient = restClient; + this.executor = executor; + this.pluginName = pluginName; + } + + @Override + public void onChangeIndexed(ChangeData cd) { + executeIndexTask(cd.getId(), false); + } + + @Override + public void onChangeDeleted(Change.Id id) { + executeIndexTask(id, true); + } + + private void executeIndexTask(Change.Id id, boolean deleted) { + if (!Context.isForwardedEvent()) { + SyncIndexTask syncIndexTask = new SyncIndexTask(id.get(), deleted); + if (queuedTasks.add(syncIndexTask)) { + executor.execute(syncIndexTask); + } + } + } + + class SyncIndexTask implements Runnable { + private int changeId; + private boolean deleted; + + SyncIndexTask(int changeId, boolean deleted) { + this.changeId = changeId; + this.deleted = deleted; + } + + @Override + public void run() { + queuedTasks.remove(this); + if (deleted) { + restClient.deleteFromIndex(changeId); + } else { + restClient.index(changeId); + } + } + + @Override + public int hashCode() { + return Objects.hashCode(changeId, deleted); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SyncIndexTask)) { + return false; + } + SyncIndexTask other = (SyncIndexTask) obj; + return changeId == other.changeId && deleted == other.deleted; + } + + @Override + public String toString() { + return String.format("[%s] Index change %s in target instance", + pluginName, changeId); + } + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java new file mode 100644 index 0000000..2d549ae --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandler.java
@@ -0,0 +1,74 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; + +import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ResponseHandler; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +class IndexResponseHandler implements ResponseHandler<IndexResult> { + + static class IndexResult { + private boolean successful; + private String message; + + IndexResult(boolean successful, String message) { + this.successful = successful; + this.message = message; + } + + boolean isSuccessful() { + return successful; + } + + String getMessage() { + return message; + } + } + + private static final Logger log = LoggerFactory + .getLogger(IndexResponseHandler.class); + + @Override + public IndexResult handleResponse(HttpResponse response) { + return new IndexResult(isSuccessful(response), parseResponse(response)); + } + + private boolean isSuccessful(HttpResponse response) { + return response.getStatusLine().getStatusCode() == SC_NO_CONTENT; + } + + private String parseResponse(HttpResponse response) { + HttpEntity entity = response.getEntity(); + String asString = ""; + if (entity != null) { + try { + asString = EntityUtils.toString(entity); + } catch (IOException e) { + log.error("Error parsing entity", e); + } + } + return asString; + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/Module.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Module.java new file mode 100644 index 0000000..5b278c3 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/Module.java
@@ -0,0 +1,49 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.extensions.events.ChangeIndexedListener; +import com.google.inject.Provides; +import com.google.inject.Scopes; + +import org.apache.http.impl.client.CloseableHttpClient; + +import java.util.concurrent.Executor; + +class Module extends LifecycleModule { + + @Override + protected void configure() { + bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class) + .in(Scopes.SINGLETON); + bind(Configuration.class).in(Scopes.SINGLETON); + bind(HttpSession.class); + bind(RestSession.class); + bind(Executor.class) + .annotatedWith(SyncIndexExecutor.class) + .toProvider(SyncIndexExecutorProvider.class); + listener().to(SyncIndexExecutorProvider.class); + DynamicSet.bind(binder(), ChangeIndexedListener.class).to( + IndexEventHandler.class); + } + + @Provides + @SyncUrl + String syncUrl(Configuration config) { + return config.getUrl(); + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java new file mode 100644 index 0000000..9cdd4ea --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/RestSession.java
@@ -0,0 +1,71 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.common.base.Joiner; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.inject.Inject; + +import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +class RestSession { + private static final Logger log = LoggerFactory.getLogger(RestSession.class); + + private final HttpSession httpSession; + private final String pluginName; + + @Inject + RestSession(HttpSession httpClient, @PluginName String pluginName) { + this.httpSession = httpClient; + this.pluginName = pluginName; + } + + boolean index(int changeId) { + try { + IndexResult result = httpSession.post(buildEndpoint(changeId)); + if (result.isSuccessful()) { + return true; + } + log.error("Unable to index change {}. Cause: {}", changeId, + result.getMessage()); + } catch (IOException e) { + log.error("Error trying to index change " + changeId, e); + } + return false; + } + + boolean deleteFromIndex(int changeId) { + try { + IndexResult result = httpSession.delete(buildEndpoint(changeId)); + if (result.isSuccessful()) { + return true; + } + log.error("Unable to delete from index change {}. Cause: {}", changeId, + result.getMessage()); + } catch (IOException e) { + log.error("Error trying to delete from index change " + changeId, e); + } + return false; + } + + private String buildEndpoint(int changeId) { + return Joiner.on("/").join("/plugins", pluginName, "index", changeId); + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java new file mode 100644 index 0000000..75932e9 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutor.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +@interface SyncIndexExecutor { +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java new file mode 100644 index 0000000..0400506 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProvider.java
@@ -0,0 +1,52 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.server.git.WorkQueue; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import java.util.concurrent.Executor; + +@Singleton +class SyncIndexExecutorProvider implements Provider<Executor>, + LifecycleListener { + private WorkQueue.Executor executor; + + @Inject + SyncIndexExecutorProvider(WorkQueue workQueue, Configuration config) { + executor = + workQueue.createQueue(config.getThreadPoolSize(), "Sync remote index"); + } + + @Override + public void start() { + //do nothing + } + + @Override + public void stop() { + executor.shutdown(); + executor.unregisterWorkQueue(); + executor = null; + } + + @Override + public Executor get() { + return executor; + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java new file mode 100644 index 0000000..ff3e47b --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServlet.java
@@ -0,0 +1,105 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; + +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.index.ChangeIndexer; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Singleton +class SyncIndexRestApiServlet extends HttpServlet { + private static final long serialVersionUID = -1L; + private static final Logger logger = + LoggerFactory.getLogger(SyncIndexRestApiServlet.class); + + private final ChangeIndexer indexer; + private final SchemaFactory<ReviewDb> schemaFactory; + + @Inject + SyncIndexRestApiServlet(ChangeIndexer indexer, + SchemaFactory<ReviewDb> schemaFactory) { + this.indexer = indexer; + this.schemaFactory = schemaFactory; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse rsp) + throws IOException, ServletException { + rsp.setContentType("text/plain"); + rsp.setCharacterEncoding("UTF-8"); + Change.Id id = getIdFromRequest(req.getPathInfo()); + + try (ReviewDb db = schemaFactory.open()) { + Context.setForwardedEvent(true); + Change change = db.changes().get(id); + if (change == null) { + throw new NoSuchChangeException(id); + } + indexer.index(db, change); + rsp.setStatus(SC_NO_CONTENT); + } catch (IOException e) { + rsp.sendError(SC_CONFLICT, e.getMessage()); + logger.error("Unable to update index", e); + } catch (OrmException | NoSuchChangeException e) { + rsp.sendError(SC_NOT_FOUND, "Change not found\n"); + logger.debug("Error trying to find a change ", e); + } finally { + Context.unsetForwardedEvent(); + } + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse rsp) + throws IOException, ServletException { + rsp.setContentType("text/plain"); + rsp.setCharacterEncoding("UTF-8"); + Change.Id id = getIdFromRequest(req.getPathInfo()); + + try { + Context.setForwardedEvent(true); + indexer.delete(id); + rsp.setStatus(SC_NO_CONTENT); + } catch (IOException e) { + rsp.sendError(SC_CONFLICT, e.getMessage()); + logger.error("Unable to update index", e); + } finally { + Context.unsetForwardedEvent(); + } + } + + private Change.Id getIdFromRequest(String path) { + String changeId = path.substring(path.lastIndexOf('/') + 1); + return Change.Id.parse(changeId); + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java new file mode 100644 index 0000000..8b3ac1a --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/syncindex/SyncUrl.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +@interface SyncUrl { +}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md new file mode 100644 index 0000000..a711d86 --- /dev/null +++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,11 @@ +The @PLUGIN@ plugin allows to synchronize secondary indexes between two Gerrit +instances sharing the same git repositories and database. + +The plugin is installed in both instances and every time the secondary index +is modified in one of the instances, i.e., a change is added, updated or removed +from the index, the other instance index is updated accordingly. This way, both +indexes are kept synchronized. + +For this secondary index synchronization to work, http must be enabled in both +instances and the plugin must be configured with valid credentials. For further +information, refer to [config](config.html) documentation.
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md new file mode 100644 index 0000000..3babed2 --- /dev/null +++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,106 @@ +Build +===== + +This plugin is built with Buck. + +Two build modes are supported: Standalone and in Gerrit tree. Standalone +build mode is recommended, as this mode doesn't require local Gerrit +tree to exist. + +Build standalone +---------------- + +Clone bucklets library: + +``` + git clone https://gerrit.googlesource.com/bucklets + +``` +and link it to @PLUGIN@ directory: + +``` + cd @PLUGIN@ && ln -s ../bucklets . +``` + +Add link to the .buckversion file: + +``` + cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion +``` + +Add link to the .watchmanconfig file: + +``` + cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig +``` + +To build the plugin, issue the following command: + +``` + buck build plugin +``` + +The output is created in: + +``` + buck-out/gen/@PLUGIN@.jar +``` + +This project can be imported into the Eclipse IDE: + +``` + ./bucklets/tools/eclipse.py +``` + +To execute the tests run: + +``` + buck test +``` + +To build plugin sources run: + +``` + buck build src +``` + +The output is created in: + +``` + buck-out/gen/@PLUGIN@-sources.jar +``` + +Build in Gerrit tree +-------------------- + +Clone or link this plugin to the plugins directory of Gerrit's source +tree, and issue the command: + +``` + buck build plugins/@PLUGIN@ +``` + +The output is created in: + +``` + buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar +``` + +This project can be imported into the Eclipse IDE: + +``` + ./tools/eclipse/project.py +``` + +To execute the tests run: + +``` + buck test --include @PLUGIN@ +``` + +How to build the Gerrit Plugin API is described in the [Gerrit +documentation](../../../Documentation/dev-buck.html#_extension_and_plugin_api_jar_files). + +[Back to @PLUGIN@ documentation index][index] + +[index]: index.html
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md new file mode 100644 index 0000000..48b9e94 --- /dev/null +++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,51 @@ +@PLUGIN@ Configuration +========================= + +In order for the synchronization to work, the @PLUGIN@ plugin must be installed +in both instances and the following fields should be specified in the +corresponding Gerrit configuration file: + +File 'gerrit.config' +-------------------- + +[plugin "@PLUGIN@"] +: url = target_instance_url +: user = username +: password = password + +plugin.@PLUGIN@.url +: Specify the URL for the secondary (target) instance. + +plugin.@PLUGIN@.user +: Username to connect to the secondary (target) instance. + +plugin.@PLUGIN@.password +: Password to connect to the secondary (target) instance. This value can + also be defined in secure.config. + +@PLUGIN@ plugin uses REST API calls to index changes in the target instance. It +is possible to customize the parameters of the underlying http client doing these +calls by specifying the following fields: + +@PLUGIN@.connectionTimeout +: Maximum interval of time in milliseconds the plugin waits for a connection + to the target instance. When not specified, the default value is set to 5000ms. + +@PLUGIN@.socketTimeout +: Maximum interval of time in milliseconds the plugin waits for a response from the + target instance once the connection has been established. When not specified, + the default value is set to 5000ms. + +@PLUGIN@.maxTries +: Maximum number of times the plugin should attempt to index the event in the + target instance. Setting this value to 0 will disable retries. When not + specified, the default value is 5. After this number of failed tries, an error + is logged so that admins can re-index the change manually. + +@PLUGIN@.retryInterval +: The interval of time in milliseconds between the subsequent auto-retries. + When not specified, the default value is set to 1000ms. + +@PLUGIN@.threadPoolSize +: Maximum number of threads used to send index events to the target instance. + Defaults to 1.
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java new file mode 100644 index 0000000..fd39331 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ConfigurationTest.java
@@ -0,0 +1,102 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.expect; + +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.Test; + +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; + +public class ConfigurationTest extends EasyMockSupport { + private static final String PASS = "fakePass"; + private static final String USER = "fakeUser"; + private static final String URL = "fakeUrl"; + private static final String EMPTY = ""; + private static final int TIMEOUT = 5000; + private static final int MAX_TRIES = 5; + private static final int RETRY_INTERVAL = 1000; + private static final int THREAD_POOL_SIZE = 1; + + private PluginConfigFactory cfgFactoryMock; + private PluginConfig configMock; + private Configuration configuration; + private String pluginName = "sync-index"; + + @Before + public void setUp() throws Exception { + configMock = createNiceMock(PluginConfig.class); + cfgFactoryMock = createMock(PluginConfigFactory.class); + expect(cfgFactoryMock.getFromGerritConfig(pluginName, true)) + .andStubReturn(configMock); + } + + @Test + public void testValuesPresentInGerritConfig() throws Exception { + buildMocks(true); + assertThat(configuration.getUrl()).isEqualTo(URL); + assertThat(configuration.getUser()).isEqualTo(USER); + assertThat(configuration.getPassword()).isEqualTo(PASS); + assertThat(configuration.getConnectionTimeout()).isEqualTo(TIMEOUT); + assertThat(configuration.getSocketTimeout()).isEqualTo(TIMEOUT); + assertThat(configuration.getMaxTries()).isEqualTo(MAX_TRIES); + assertThat(configuration.getRetryInterval()).isEqualTo(RETRY_INTERVAL); + assertThat(configuration.getThreadPoolSize()).isEqualTo(THREAD_POOL_SIZE); + } + + @Test + public void testValuesNotPresentInGerritConfig() throws Exception { + buildMocks(false); + assertThat(configuration.getUrl()).isEqualTo(EMPTY); + assertThat(configuration.getUser()).isEqualTo(EMPTY); + assertThat(configuration.getPassword()).isEqualTo(EMPTY); + assertThat(configuration.getConnectionTimeout()).isEqualTo(0); + assertThat(configuration.getSocketTimeout()).isEqualTo(0); + assertThat(configuration.getMaxTries()).isEqualTo(0); + assertThat(configuration.getRetryInterval()).isEqualTo(0); + assertThat(configuration.getThreadPoolSize()).isEqualTo(0); + } + + @Test + public void testUrlTrailingSlashIsDropped() throws Exception { + expect(configMock.getString("url")).andReturn(URL + "/"); + replayAll(); + configuration = new Configuration(cfgFactoryMock, pluginName); + assertThat(configuration).isNotNull(); + assertThat(configuration.getUrl()).isEqualTo(URL); + } + + private void buildMocks(boolean values) { + expect(configMock.getString("url")).andReturn(values ? URL : null); + expect(configMock.getString("user")).andReturn(values ? USER : null); + expect(configMock.getString("password")).andReturn(values ? PASS : null); + expect(configMock.getInt("connectionTimeout", TIMEOUT)) + .andReturn(values ? TIMEOUT : 0); + expect(configMock.getInt("socketTimeout", TIMEOUT)) + .andReturn(values ? TIMEOUT : 0); + expect(configMock.getInt("maxTries", MAX_TRIES)) + .andReturn(values ? MAX_TRIES : 0); + expect(configMock.getInt("retryInterval", RETRY_INTERVAL)) + .andReturn(values ? RETRY_INTERVAL : 0); + expect(configMock.getInt("threadPoolSize", THREAD_POOL_SIZE)) + .andReturn(values ? THREAD_POOL_SIZE : 0); + replayAll(); + configuration = new Configuration(cfgFactoryMock, pluginName); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/ContextTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ContextTest.java new file mode 100644 index 0000000..f43439e --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ContextTest.java
@@ -0,0 +1,46 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; + +import org.easymock.EasyMockSupport; +import org.junit.Test; + +public class ContextTest extends EasyMockSupport { + + @Test + public void testInitialValueNotNull() throws Exception { + assertThat(Context.isForwardedEvent()).isNotNull(); + assertThat(Context.isForwardedEvent()).isFalse(); + } + + @Test + public void testSetForwardedEvent() throws Exception { + Context.setForwardedEvent(true); + try { + assertThat(Context.isForwardedEvent()).isTrue(); + } finally { + Context.unsetForwardedEvent(); + } + } + + @Test + public void testUnsetForwardedEvent() throws Exception { + Context.setForwardedEvent(true); + Context.unsetForwardedEvent(); + assertThat(Context.isForwardedEvent()).isFalse(); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java new file mode 100644 index 0000000..48af47a --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpClientProviderTest.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.expect; + +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Scopes; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.Test; + +public class HttpClientProviderTest extends EasyMockSupport { + private static final int TIME_INTERVAL = 1000; + private static final String EMPTY = ""; + + private Configuration config; + + @Before + public void setUp() throws Exception { + config = createNiceMock(Configuration.class); + expect(config.getUrl()).andReturn(EMPTY).anyTimes(); + expect(config.getUser()).andReturn(EMPTY).anyTimes(); + expect(config.getPassword()).andReturn(EMPTY).anyTimes(); + expect(config.getMaxTries()).andReturn(1).anyTimes(); + expect(config.getConnectionTimeout()).andReturn(TIME_INTERVAL).anyTimes(); + expect(config.getSocketTimeout()).andReturn(TIME_INTERVAL).anyTimes(); + expect(config.getRetryInterval()).andReturn(TIME_INTERVAL).anyTimes(); + replayAll(); + } + + @Test + public void testGet() throws Exception { + Injector injector = Guice.createInjector(new TestModule()); + CloseableHttpClient httpClient1 = + injector.getInstance(CloseableHttpClient.class); + assertThat(httpClient1).isNotNull(); + CloseableHttpClient httpClient2 = + injector.getInstance(CloseableHttpClient.class); + assertThat(httpClient1).isEqualTo(httpClient2); + } + + class TestModule extends LifecycleModule { + @Override + protected void configure() { + bind(Configuration.class).toInstance(config); + bind(CloseableHttpClient.class).toProvider(HttpClientProvider.class) + .in(Scopes.SINGLETON); + } + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java new file mode 100644 index 0000000..27f30d5 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/HttpSessionTest.java
@@ -0,0 +1,154 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.expect; + +import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.stubbing.Scenario; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +public class HttpSessionTest extends EasyMockSupport { + private static final int MAX_TRIES = 5; + private static final int RETRY_INTERVAL = 1000; + private static final int TIMEOUT = 1000; + private static final int ERROR = 500; + private static final int OK = 204; + private static final int NOT_FOUND = 404; + private static final int UNAUTHORIZED = 401; + + private static final String ENDPOINT = "/plugins/sync-index/index/1"; + private static final String ERROR_MESSAGE = "Error message"; + private static final String REQUEST_MADE = "Request made"; + private static final String RETRY_AT_ERROR = "Retry at error"; + private static final String RETRY_AT_DELAY = "Retry at delay"; + + private Configuration cfg; + private CloseableHttpClient httpClient; + private HttpSession httpSession; + + @ClassRule + public static WireMockRule wireMockRule = new WireMockRule(0); + + @Before + public void setUp() throws Exception { + String url = "http://localhost:" + wireMockRule.port(); + cfg = createMock(Configuration.class); + expect(cfg.getUrl()).andReturn(url).anyTimes(); + expect(cfg.getUser()).andReturn("user"); + expect(cfg.getPassword()).andReturn("pass"); + expect(cfg.getMaxTries()).andReturn(MAX_TRIES).anyTimes(); + expect(cfg.getConnectionTimeout()).andReturn(TIMEOUT).anyTimes(); + expect(cfg.getSocketTimeout()).andReturn(TIMEOUT).anyTimes(); + expect(cfg.getRetryInterval()).andReturn(RETRY_INTERVAL).anyTimes(); + replayAll(); + httpClient = new HttpClientProvider(cfg).get(); + httpSession = new HttpSession(httpClient, url); + + wireMockRule.resetRequests(); + } + + @Test + public void testResponseOK() throws Exception { + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)) + .willReturn(aResponse().withStatus(OK))); + + assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue(); + } + + @Test + public void testNotAuthorized() throws Exception { + String expected = "unauthorized"; + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)) + .willReturn(aResponse().withStatus(UNAUTHORIZED).withBody(expected))); + + IndexResult result = httpSession.post(ENDPOINT); + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.getMessage()).isEqualTo(expected); + } + + @Test + public void testNotFound() throws Exception { + String expected = "not found"; + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)) + .willReturn(aResponse().withStatus(NOT_FOUND).withBody(expected))); + + IndexResult result = httpSession.post(ENDPOINT); + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.getMessage()).isEqualTo(expected); + } + + @Test + public void testBadResponseRetryThenOK() throws Exception { + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR) + .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE) + .willReturn(aResponse().withStatus(ERROR))); + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_ERROR) + .whenScenarioStateIs(REQUEST_MADE) + .willReturn(aResponse().withStatus(OK))); + + assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue(); + } + + @Test + public void testBadResponseRetryThenGiveUp() throws Exception { + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)) + .willReturn(aResponse().withStatus(ERROR).withBody(ERROR_MESSAGE))); + + IndexResult result = httpSession.post(ENDPOINT); + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.getMessage()).isEqualTo(ERROR_MESSAGE); + } + + @Test + public void testRetryAfterDelay() throws Exception { + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY) + .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE) + .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT / 2))); + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY) + .whenScenarioStateIs(REQUEST_MADE) + .willReturn(aResponse().withStatus(OK))); + + assertThat(httpSession.post(ENDPOINT).isSuccessful()).isTrue(); + } + + @Test + public void testGiveUpAtTimeout() throws Exception { + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)).inScenario(RETRY_AT_DELAY) + .whenScenarioStateIs(Scenario.STARTED).willSetStateTo(REQUEST_MADE) + .willReturn(aResponse().withStatus(ERROR).withFixedDelay(TIMEOUT))); + + assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse(); + } + + @Test + public void testResponseWithMalformedResponse() throws Exception { + wireMockRule.givenThat(post(urlEqualTo(ENDPOINT)) + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + + assertThat(httpSession.post(ENDPOINT).isSuccessful()).isFalse(); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java new file mode 100644 index 0000000..614d30f --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexEventHandlerTest.java
@@ -0,0 +1,141 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.reset; + +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.client.KeyUtil; +import com.google.gwtorm.server.StandardKeyEncoder; + +import com.ericsson.gerrit.plugins.syncindex.IndexEventHandler.SyncIndexTask; + +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.concurrent.Executor; + +public class IndexEventHandlerTest extends EasyMockSupport { + private static final String PLUGIN_NAME = "sync-index"; + private static final int CHANGE_ID = 1; + + private IndexEventHandler indexEventHandler; + private Executor poolMock; + private RestSession restClientMock; + private ChangeData cd; + private Change.Id id; + + @BeforeClass + public static void setUp() { + KeyUtil.setEncoderImpl(new StandardKeyEncoder()); + } + + @Before + public void setUpMocks() { + cd = createNiceMock(ChangeData.class); + id = Change.Id.parse(Integer.toString(CHANGE_ID)); + expect(cd.getId()).andReturn(id).anyTimes(); + poolMock = createMock(Executor.class); + poolMock.execute(anyObject(SyncIndexTask.class)); + expectLastCall().andDelegateTo(MoreExecutors.directExecutor()); + restClientMock = createMock(RestSession.class); + indexEventHandler = + new IndexEventHandler(poolMock, PLUGIN_NAME, restClientMock); + } + + @Test + public void shouldIndexInRemoteOnChangeIndexedEvent() throws Exception { + expect(restClientMock.index(CHANGE_ID)).andReturn(true); + replayAll(); + indexEventHandler.onChangeIndexed(cd); + verifyAll(); + } + + @Test + public void shouldDeleteFromIndexInRemoteOnChangeDeletedEvent() + throws Exception { + reset(cd); + expect(restClientMock.deleteFromIndex(CHANGE_ID)).andReturn(true); + replayAll(); + indexEventHandler.onChangeDeleted(id); + verifyAll(); + } + + @Test + public void shouldNotCallRemoteWhenEventIsForwarded() throws Exception { + reset(poolMock); + replayAll(); + Context.setForwardedEvent(true); + indexEventHandler.onChangeIndexed(cd); + indexEventHandler.onChangeDeleted(id); + Context.unsetForwardedEvent(); + verifyAll(); + } + + @Test + public void duplicateEventOfAQueuedEventShouldGetDiscarded() { + reset(poolMock); + poolMock.execute(indexEventHandler.new SyncIndexTask(CHANGE_ID, false)); + expectLastCall().once(); + replayAll(); + indexEventHandler.onChangeIndexed(cd); + indexEventHandler.onChangeIndexed(cd); + verifyAll(); + } + + @Test + public void testSyncIndexTaskToString() throws Exception { + SyncIndexTask syncIndexTask = + indexEventHandler.new SyncIndexTask(CHANGE_ID, false); + assertThat(syncIndexTask.toString()).isEqualTo( + String.format("[%s] Index change %s in target instance", PLUGIN_NAME, + CHANGE_ID)); + } + + @Test + public void testSyncIndexTaskHashCodeAndEquals() { + SyncIndexTask task = indexEventHandler.new SyncIndexTask(CHANGE_ID, false); + + assertThat(task.equals(task)).isTrue(); + assertThat(task.hashCode()).isEqualTo(task.hashCode()); + + SyncIndexTask identicalTask = + indexEventHandler.new SyncIndexTask(CHANGE_ID, false); + assertThat(task.equals(identicalTask)).isTrue(); + assertThat(task.hashCode()).isEqualTo(identicalTask.hashCode()); + + assertThat(task.equals(null)).isFalse(); + assertThat(task.equals("test")).isFalse(); + assertThat(task.hashCode()).isNotEqualTo("test".hashCode()); + + SyncIndexTask differentChangeIdTask = + indexEventHandler.new SyncIndexTask(123, false); + assertThat(task.equals(differentChangeIdTask)).isFalse(); + assertThat(task.hashCode()).isNotEqualTo(differentChangeIdTask.hashCode()); + + SyncIndexTask removeTask = + indexEventHandler.new SyncIndexTask(CHANGE_ID, true); + assertThat(task.equals(removeTask)).isFalse(); + assertThat(task.hashCode()).isNotEqualTo(removeTask.hashCode()); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java new file mode 100644 index 0000000..595cf7d --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/IndexResponseHandlerTest.java
@@ -0,0 +1,70 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.expect; + +import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult; + +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.entity.StringEntity; +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.Test; + +import java.io.UnsupportedEncodingException; + +public class IndexResponseHandlerTest extends EasyMockSupport { + private static final int ERROR = 400; + private static final int OK = 204; + private static final String EMPTY_ENTITY = ""; + private static final String ERROR_ENTITY = "Error"; + + private IndexResponseHandler handler; + + @Before + public void setUp() throws Exception { + handler = new IndexResponseHandler(); + } + + @Test + public void testIsSuccessful() throws Exception { + HttpResponse response = setupMocks(OK, EMPTY_ENTITY); + IndexResult result = handler.handleResponse(response); + assertThat(result.isSuccessful()).isTrue(); + assertThat(result.getMessage()).isEmpty(); + } + + @Test + public void testIsNotSuccessful() throws Exception { + HttpResponse response = setupMocks(ERROR, ERROR_ENTITY); + IndexResult result = handler.handleResponse(response); + assertThat(result.isSuccessful()).isFalse(); + assertThat(result.getMessage()).contains(ERROR_ENTITY); + } + + private HttpResponse setupMocks(int httpCode, String entity) + throws UnsupportedEncodingException { + StatusLine status = createNiceMock(StatusLine.class); + expect(status.getStatusCode()).andReturn(httpCode).anyTimes(); + HttpResponse response = createNiceMock(HttpResponse.class); + expect(response.getStatusLine()).andReturn(status).anyTimes(); + expect(response.getEntity()).andReturn(new StringEntity(entity)).anyTimes(); + replayAll(); + return response; + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/ModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ModuleTest.java new file mode 100644 index 0000000..bfc42b4 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/ModuleTest.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.expect; + +import org.easymock.EasyMockSupport; +import org.junit.Test; + +public class ModuleTest extends EasyMockSupport { + + @Test + public void testSyncUrlProvider() { + Configuration configMock = createNiceMock(Configuration.class); + String expected = "someUrl"; + expect(configMock.getUrl()).andReturn(expected); + replayAll(); + Module module = new Module(); + assertThat(module.syncUrl(configMock)).isEqualTo(expected); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.java new file mode 100644 index 0000000..31850c2 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/RestSessionTest.java
@@ -0,0 +1,102 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.expect; + +import com.google.common.base.Joiner; + +import com.ericsson.gerrit.plugins.syncindex.IndexResponseHandler.IndexResult; + +import org.easymock.EasyMockSupport; +import org.junit.Test; + +import java.io.IOException; + +public class RestSessionTest extends EasyMockSupport { + private static final int CHANGE_NUMBER = 1; + private static final String DELETE_OP = "delete"; + private static final String INDEX_OP = "index"; + private static final String PLUGIN_NAME = "sync-index"; + private static final String EMPTY_MSG = ""; + private static final String ERROR_MSG = "Error"; + private static final String EXCEPTION_MSG = "Exception"; + private static final boolean SUCCESSFUL = true; + private static final boolean FAILED = false; + private static final boolean DO_NOT_THROW_EXCEPTION = false; + private static final boolean THROW_EXCEPTION = true; + + private RestSession restClient; + + @Test + public void testIndexChangeOK() throws Exception { + setUpMocks(INDEX_OP, SUCCESSFUL, EMPTY_MSG, DO_NOT_THROW_EXCEPTION); + assertThat(restClient.index(CHANGE_NUMBER)).isTrue(); + } + + @Test + public void testIndexChangeFailed() throws Exception { + setUpMocks(INDEX_OP, FAILED, ERROR_MSG, DO_NOT_THROW_EXCEPTION); + assertThat(restClient.index(CHANGE_NUMBER)).isFalse(); + } + + @Test + public void testIndexChangeThrowsException() throws Exception { + setUpMocks(INDEX_OP, FAILED, EXCEPTION_MSG, THROW_EXCEPTION); + assertThat(restClient.index(CHANGE_NUMBER)).isFalse(); + } + + @Test + public void testChangeDeletedFromIndexOK() throws Exception { + setUpMocks(DELETE_OP, SUCCESSFUL, EMPTY_MSG, DO_NOT_THROW_EXCEPTION); + assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isTrue(); + } + + @Test + public void testChangeDeletedFromIndexFailed() throws Exception { + setUpMocks(DELETE_OP, FAILED, ERROR_MSG, DO_NOT_THROW_EXCEPTION); + assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isFalse(); + } + + @Test + public void testChangeDeletedFromThrowsException() throws Exception { + setUpMocks(DELETE_OP, FAILED, EXCEPTION_MSG, THROW_EXCEPTION); + assertThat(restClient.deleteFromIndex(CHANGE_NUMBER)).isFalse(); + } + + private void setUpMocks(String operation, boolean isOperationSuccessful, + String msg, boolean exception) throws Exception { + String request = + Joiner.on("/").join("/plugins", PLUGIN_NAME, INDEX_OP, CHANGE_NUMBER); + HttpSession httpSession = createNiceMock(HttpSession.class); + if (exception) { + if (operation.equals(INDEX_OP)) { + expect(httpSession.post(request)).andThrow(new IOException()); + } else { + expect(httpSession.delete(request)).andThrow(new IOException()); + } + } else { + IndexResult result = new IndexResult(isOperationSuccessful, msg); + if (operation.equals(INDEX_OP)) { + expect(httpSession.post(request)).andReturn(result); + } else { + expect(httpSession.delete(request)).andReturn(result); + } + } + restClient = new RestSession(httpSession, PLUGIN_NAME); + replayAll(); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java new file mode 100644 index 0000000..665b10a --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexExecutorProviderTest.java
@@ -0,0 +1,64 @@ +// Copyright (C) 2015 Ericsson +// +// 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.syncindex; + +import static com.google.common.truth.Truth.assertThat; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; + +import com.google.gerrit.server.git.WorkQueue; + +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.Test; + +public class SyncIndexExecutorProviderTest extends EasyMockSupport { + private WorkQueue.Executor executorMock; + private SyncIndexExecutorProvider syncIndexExecutorProvider; + + @Before + public void setUp() throws Exception { + executorMock = createStrictMock(WorkQueue.Executor.class); + WorkQueue workQueueMock = createNiceMock(WorkQueue.class); + expect(workQueueMock.createQueue(4, "Sync remote index")).andReturn( + executorMock); + Configuration configMock = createStrictMock(Configuration.class); + expect(configMock.getThreadPoolSize()).andReturn(4); + replayAll(); + syncIndexExecutorProvider = + new SyncIndexExecutorProvider(workQueueMock, configMock); + } + + @Test + public void shouldReturnExecutor() throws Exception { + assertThat(syncIndexExecutorProvider.get()).isEqualTo(executorMock); + } + + @Test + public void testStop() throws Exception { + resetAll(); + executorMock.shutdown(); + expectLastCall().once(); + executorMock.unregisterWorkQueue(); + expectLastCall().once(); + replayAll(); + + syncIndexExecutorProvider.start(); + assertThat(syncIndexExecutorProvider.get()).isEqualTo(executorMock); + syncIndexExecutorProvider.stop(); + verifyAll(); + assertThat(syncIndexExecutorProvider.get()).isNull(); + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java new file mode 100644 index 0000000..2dbdd83 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/syncindex/SyncIndexRestApiServletTest.java
@@ -0,0 +1,168 @@ +// Copyright (C) 2016 Ericsson +// +// 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.syncindex; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; + +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.server.ChangeAccess; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.index.ChangeIndexer; +import com.google.gwtorm.client.KeyUtil; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; +import com.google.gwtorm.server.StandardKeyEncoder; + +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class SyncIndexRestApiServletTest extends EasyMockSupport { + private static final boolean CHANGE_EXISTS = true; + private static final boolean CHANGE_DOES_NOT_EXIST = false; + private static final boolean DO_NOT_THROW_IO_EXCEPTION = false; + private static final boolean DO_NOT_THROW_ORM_EXCEPTION = false; + private static final boolean THROW_IO_EXCEPTION = true; + private static final boolean THROW_ORM_EXCEPTION = true; + private static final String CHANGE_NUMBER = "1"; + + private ChangeIndexer indexer; + private SchemaFactory<ReviewDb> schemaFactory; + private SyncIndexRestApiServlet syncIndexRestApiServlet; + private HttpServletRequest req; + private HttpServletResponse rsp; + private Change.Id id; + + @BeforeClass + public static void setup() { + KeyUtil.setEncoderImpl(new StandardKeyEncoder()); + } + + @Before + @SuppressWarnings("unchecked") + public void setUpMocks() { + indexer = createNiceMock(ChangeIndexer.class); + schemaFactory = createNiceMock(SchemaFactory.class); + req = createNiceMock(HttpServletRequest.class); + rsp = createNiceMock(HttpServletResponse.class); + syncIndexRestApiServlet = + new SyncIndexRestApiServlet(indexer, schemaFactory); + id = Change.Id.parse(CHANGE_NUMBER); + + expect(req.getPathInfo()).andReturn("/index/" + CHANGE_NUMBER); + } + + @Test + public void changeIsIndexed() throws Exception { + setupPostMocks(CHANGE_EXISTS); + verifyPost(); + } + + @Test + public void changeToIndexDoNotExist() throws Exception { + setupPostMocks(CHANGE_DOES_NOT_EXIST); + verifyPost(); + } + + @Test + public void schemaThrowsExceptionWhenLookingUpForChange() throws Exception { + setupPostMocks(CHANGE_EXISTS, THROW_ORM_EXCEPTION); + verifyPost(); + } + + @Test + public void indexerThrowsIOExceptionTryingToIndexChange() throws Exception { + setupPostMocks(CHANGE_EXISTS, DO_NOT_THROW_ORM_EXCEPTION, + THROW_IO_EXCEPTION); + verifyPost(); + } + + @Test + public void changeIsDeletedFromIndex() throws Exception { + setupDeleteMocks(DO_NOT_THROW_IO_EXCEPTION); + verifyDelete(); + } + + @Test + public void indexerThrowsExceptionTryingToDeleteChange() throws Exception { + setupDeleteMocks(THROW_IO_EXCEPTION); + verifyDelete(); + } + + private void setupPostMocks(boolean changeExist) throws Exception { + setupPostMocks(changeExist, DO_NOT_THROW_ORM_EXCEPTION, + DO_NOT_THROW_IO_EXCEPTION); + } + + private void setupPostMocks(boolean changeExist, boolean ormException) + throws OrmException, IOException { + setupPostMocks(changeExist, ormException, DO_NOT_THROW_IO_EXCEPTION); + } + + private void setupPostMocks(boolean changeExist, boolean ormException, + boolean ioException) throws OrmException, IOException { + if (ormException) { + expect(schemaFactory.open()).andThrow(new OrmException("")); + } else { + ReviewDb db = createNiceMock(ReviewDb.class); + expect(schemaFactory.open()).andReturn(db); + ChangeAccess ca = createNiceMock(ChangeAccess.class); + expect(db.changes()).andReturn(ca); + + if (changeExist) { + Change change = new Change(null, id, null, null, TimeUtil.nowTs()); + expect(ca.get(id)).andReturn(change); + indexer.index(db, change); + if (ioException) { + expectLastCall().andThrow(new IOException()); + } else { + expectLastCall().once(); + } + } else { + expect(ca.get(id)).andReturn(null); + } + } + replayAll(); + } + + private void verifyPost() throws IOException, ServletException { + syncIndexRestApiServlet.doPost(req, rsp); + verifyAll(); + } + + private void setupDeleteMocks(boolean exception) throws IOException { + indexer.delete(id); + if (exception) { + expectLastCall().andThrow(new IOException()); + } else { + expectLastCall().once(); + } + replayAll(); + } + + private void verifyDelete() throws IOException, ServletException { + syncIndexRestApiServlet.doDelete(req, rsp); + verifyAll(); + } +}