Merge branch 'stable-3.10' into stable-3.11
* stable-3.10:
De-register deleted repositories from the JGit RepositoryCache
Change-Id: I5e4e300e22a6f11b7d7fa4178f6eb586d954738b
diff --git a/BUILD b/BUILD
index 9bb0445..3735cb3 100644
--- a/BUILD
+++ b/BUILD
@@ -20,10 +20,11 @@
],
resources = glob(["src/main/resources/**/*"]),
deps = [
- "@jgroups//jar",
- "@jgroups-kubernetes//jar",
- "@failsafe//jar",
- ":global-refdb-neverlink",
+ ":global-refdb-neverlink",
+ "@auto-value//jar",
+ "@failsafe//jar",
+ "@jgroups-kubernetes//jar",
+ "@jgroups//jar",
],
)
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
index 55f7375..8d7c86a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Configuration.java
@@ -66,6 +66,7 @@
private final Main main;
private final AutoReindex autoReindex;
+ private final IndexSync indexSync;
private final PeerInfo peerInfo;
private final JGroups jgroups;
private final JGroupsKubernetes jgroupsKubernetes;
@@ -98,6 +99,7 @@
public Configuration(Config cfg, SitePaths site) {
main = new Main(site, cfg);
autoReindex = new AutoReindex(cfg);
+ indexSync = new IndexSync(cfg);
peerInfo = new PeerInfo(cfg);
switch (peerInfo.strategy()) {
case STATIC:
@@ -141,6 +143,10 @@
return autoReindex;
}
+ public IndexSync indexSync() {
+ return indexSync;
+ }
+
public PeerInfo peerInfo() {
return peerInfo;
}
@@ -276,6 +282,60 @@
}
}
+ public static class IndexSync {
+
+ static final String INDEX_SYNC_SECTION = "indexSync";
+ static final String ENABLED = "enabled";
+ static final String DELAY = "delay";
+ static final String PERIOD = "period";
+ static final String INITIAL_SYNC_AGE = "initialSyncAge";
+ static final String SYNC_AGE = "syncAge";
+
+ static final boolean DEFAULT_SYNC_INDEX = false;
+ static final Duration DEFAULT_DELAY = Duration.ofSeconds(0);
+ static final Duration DEFAULT_PERIOD = Duration.ofSeconds(2);
+ static final String DEFAULT_INITIAL_SYNC_AGE = "1hour";
+ static final String DEFAULT_SYNC_AGE = "1minute";
+
+ private final boolean enabled;
+ private final Duration delay;
+ private final Duration period;
+ private final String initialSyncAge;
+ private final String syncAge;
+
+ public IndexSync(Config cfg) {
+ enabled = cfg.getBoolean(INDEX_SYNC_SECTION, ENABLED, DEFAULT_SYNC_INDEX);
+ delay = getDuration(cfg, INDEX_SYNC_SECTION, DELAY, DEFAULT_DELAY);
+ period = getDuration(cfg, INDEX_SYNC_SECTION, PERIOD, DEFAULT_PERIOD);
+
+ String v = cfg.getString(INDEX_SYNC_SECTION, "", INITIAL_SYNC_AGE);
+ initialSyncAge = v != null ? v : DEFAULT_INITIAL_SYNC_AGE;
+
+ v = cfg.getString(INDEX_SYNC_SECTION, "", SYNC_AGE);
+ syncAge = v != null ? v : DEFAULT_SYNC_AGE;
+ }
+
+ public boolean enabled() {
+ return enabled;
+ }
+
+ public Duration delay() {
+ return delay;
+ }
+
+ public Duration period() {
+ return period;
+ }
+
+ public String initialSyncAge() {
+ return initialSyncAge;
+ }
+
+ public String syncAge() {
+ return syncAge;
+ }
+ }
+
public static class PeerInfo {
static final PeerInfoStrategy DEFAULT_PEER_INFO_STRATEGY = PeerInfoStrategy.STATIC;
static final String STRATEGY_KEY = "strategy";
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
index d9d5c50..4611790 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
@@ -21,6 +21,7 @@
import com.ericsson.gerrit.plugins.highavailability.forwarder.jgroups.JGroupsForwarderModule;
import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.RestForwarderModule;
import com.ericsson.gerrit.plugins.highavailability.index.IndexModule;
+import com.ericsson.gerrit.plugins.highavailability.indexsync.IndexSyncModule;
import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfoModule;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.inject.Inject;
@@ -63,6 +64,9 @@
}
if (config.index().synchronize()) {
install(new IndexModule());
+ if (config.indexSync().enabled()) {
+ install(new IndexSyncModule());
+ }
}
if (config.autoReindex().enabled()) {
install(new AutoReindexModule());
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/ValidationModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/ValidationModule.java
index 74db7d2..d0699b1 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/ValidationModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/ValidationModule.java
@@ -21,6 +21,7 @@
import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDatabaseWrapper;
import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbBatchRefUpdate;
import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbConfiguration;
+import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbExceptionHook;
import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbGitRepositoryManager;
import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbRefDatabase;
import com.gerritforge.gerrit.globalrefdb.validation.SharedRefDbRefUpdate;
@@ -30,6 +31,8 @@
import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement;
import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Scopes;
@@ -66,5 +69,7 @@
.to(CustomSharedRefEnforcementByProject.class)
.in(Scopes.SINGLETON);
}
+
+ DynamicSet.bind(binder(), ExceptionHook.class).to(SharedRefDbExceptionHook.class);
}
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java
index 414e795..9f7124e 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandler.java
@@ -17,7 +17,6 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.events.EventDispatcher;
-import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -42,11 +41,13 @@
*
* @param event The event to dispatch
*/
- public void dispatch(Event event) throws PermissionBackendException {
+ public void dispatch(Event event) {
try {
Context.setForwardedEvent(true);
log.atFine().log("dispatching event %s", event.getType());
dispatcher.postEvent(event);
+ } catch (Exception e) {
+ log.atSevere().withCause(e).log("Unable to re-trigger event");
} finally {
Context.unsetForwardedEvent();
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
index ac102ff..a787d3a 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandler.java
@@ -90,13 +90,13 @@
return true;
}
- log.atWarning().log(
+ log.atFine().log(
"Change %s seems too old compared to the event timestamp (event-Ts=%s >> change-Ts=%s)",
id, indexEvent, checker);
return false;
}
- log.atWarning().log(
+ log.atFine().log(
"Change %s not present yet in local Git repository (event=%s)", id, indexEvent);
return false;
@@ -113,7 +113,7 @@
private void reindex(ChangeNotes notes) {
notes.reload();
- indexer.index(notes);
+ indexer.reindexIfStale(notes.getProjectName(), notes.getChangeId());
}
@Override
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/MessageProcessor.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/MessageProcessor.java
index 54096ac..73df3eb 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/MessageProcessor.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/jgroups/MessageProcessor.java
@@ -26,7 +26,6 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -108,13 +107,7 @@
} else if (cmd instanceof PostEvent) {
Event event = ((PostEvent) cmd).getEvent();
- try {
- eventHandler.dispatch(event);
- log.atFine().log("Dispatching event %s done", event);
- } catch (PermissionBackendException e) {
- log.atSevere().withCause(e).log("Dispatching event %s failed", event);
- return false;
- }
+ eventHandler.dispatch(event);
} else if (cmd instanceof AddToProjectList) {
String projectName = ((AddToProjectList) cmd).getProjectName();
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java
index 37f0f20..e2e3302 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServlet.java
@@ -24,7 +24,6 @@
import com.google.common.net.MediaType;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.events.EventGson;
-import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -53,10 +52,10 @@
sendError(rsp, SC_UNSUPPORTED_MEDIA_TYPE, "Expecting " + JSON_UTF_8 + " content type");
return;
}
- forwardedEventHandler.dispatch(getEventFromRequest(req));
+ Event event = getEventFromRequest(req);
rsp.setStatus(SC_NO_CONTENT);
- } catch (IOException | PermissionBackendException e) {
- log.atSevere().withCause(e).log("Unable to re-trigger event");
+ forwardedEventHandler.dispatch(event);
+ } catch (IOException e) {
sendError(rsp, SC_BAD_REQUEST, e.getMessage());
}
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/QueryChangesUpdatedSinceServlet.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/QueryChangesUpdatedSinceServlet.java
new file mode 100644
index 0000000..4ebf175
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/QueryChangesUpdatedSinceServlet.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.forwarder.rest;
+
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class QueryChangesUpdatedSinceServlet extends AbstractRestApiServlet {
+ private static final long serialVersionUID = 1L;
+
+ Gson gson = new Gson();
+
+ private ChangeQueryBuilder changeQueryBuilder;
+ private final Provider<ChangeQueryProcessor> queryProcessorProvider;
+
+ @Inject
+ QueryChangesUpdatedSinceServlet(
+ ChangeQueryBuilder changeQueryBuilder,
+ Provider<ChangeQueryProcessor> queryProcessorProvider) {
+ this.changeQueryBuilder = changeQueryBuilder;
+ this.queryProcessorProvider = queryProcessorProvider;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
+ throws ServletException, IOException {
+ try {
+ String age = req.getPathInfo().substring(1);
+ ChangeQueryProcessor queryProcessor = queryProcessorProvider.get();
+ queryProcessor.enforceVisibility(false);
+ queryProcessor.setNoLimit(true);
+ // TODO: prevent too large age, because of the noLimit option
+ Predicate<ChangeData> predicate = Predicate.not(changeQueryBuilder.age(age));
+ QueryResult<ChangeData> result = queryProcessor.query(predicate);
+ ImmutableList<ChangeData> cds = result.entities();
+ ArrayList<String> response = new ArrayList<>(cds.size());
+ for (ChangeData cd : cds) {
+ response.add(String.format("%s~%s", cd.project().get(), cd.getId().get()));
+ }
+
+ String json = gson.toJson(response);
+ rsp.setStatus(SC_OK);
+ rsp.setContentType("application/json");
+ rsp.setCharacterEncoding("UTF-8");
+ PrintWriter out = rsp.getWriter();
+ out.print(json);
+ out.print("\n");
+ out.flush();
+ } catch (IllegalArgumentException e) {
+ rsp.setStatus(SC_BAD_REQUEST);
+ } catch (QueryParseException e) {
+ throw new ServletException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
index 4d3de37..ffe3afc 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/RestForwarderServletModule.java
@@ -27,5 +27,7 @@
serve("/event/*").with(EventRestApiServlet.class);
serve("/cache/project_list/*").with(ProjectListApiServlet.class);
serve("/cache/*").with(CacheRestApiServlet.class);
+
+ serve("/query/changes.updated.since/*").with(QueryChangesUpdatedSinceServlet.class);
}
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
index 9236399..76475b0 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
@@ -17,9 +17,7 @@
import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -28,7 +26,6 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jgit.lib.Ref;
@@ -37,7 +34,6 @@
public class ChangeCheckerImpl implements ChangeChecker {
private static final FluentLogger log = FluentLogger.forEnclosingClass();
private final GitRepositoryManager gitRepoMgr;
- private final DraftCommentsReader draftCommentsReader;
private final OneOffRequestContext oneOffReqCtx;
private final String changeId;
private final ChangeFinder changeFinder;
@@ -51,13 +47,11 @@
@Inject
public ChangeCheckerImpl(
GitRepositoryManager gitRepoMgr,
- DraftCommentsReader draftCommentsReader,
ChangeFinder changeFinder,
OneOffRequestContext oneOffReqCtx,
@Assisted String changeId) {
this.changeFinder = changeFinder;
this.gitRepoMgr = gitRepoMgr;
- this.draftCommentsReader = draftCommentsReader;
this.oneOffReqCtx = oneOffReqCtx;
this.changeId = changeId;
}
@@ -162,7 +156,7 @@
}
private Optional<Long> computeLastChangeTs() {
- return getChangeNotes().map(this::getTsFromChangeAndDraftComments);
+ return getChangeNotes().map(this::getTsFromChange);
}
private String getMetaSha(Repository repo) throws IOException {
@@ -175,14 +169,8 @@
return ref.getTarget().getObjectId().getName();
}
- private long getTsFromChangeAndDraftComments(ChangeNotes notes) {
+ private long getTsFromChange(ChangeNotes notes) {
Change change = notes.getChange();
- Timestamp changeTs = Timestamp.from(change.getLastUpdatedOn());
- for (HumanComment comment :
- draftCommentsReader.getDraftsByChangeForAllAuthors(changeNotes.get())) {
- Timestamp commentTs = comment.writtenOn;
- changeTs = commentTs.after(changeTs) ? commentTs : changeTs;
- }
- return changeTs.getTime() / 1000;
+ return change.getLastUpdatedOn().toEpochMilli() / 1000;
}
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncModule.java
new file mode 100644
index 0000000..8d6b282
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.indexsync;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.internal.UniqueAnnotations;
+
+public class IndexSyncModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ // NOTE: indexSync.enabled is handled in the plugins main Module
+ // When not enabled, then this module is not installed
+ bind(LifecycleListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(IndexSyncScheduler.class);
+ bind(QueryChangesResponseHandler.class);
+ factory(IndexSyncRunner.Factory.class);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncRunner.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncRunner.java
new file mode 100644
index 0000000..92834bd
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncRunner.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.indexsync;
+
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
+
+import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import dev.failsafe.function.CheckedSupplier;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+public class IndexSyncRunner implements CheckedSupplier<Boolean> {
+ private static final FluentLogger log = FluentLogger.forEnclosingClass();
+
+ interface Factory {
+ IndexSyncRunner create(String age);
+ }
+
+ private final Provider<Set<PeerInfo>> peerInfoProvider;
+ private final CloseableHttpClient httpClient;
+ private final String pluginRelativePath;
+ private final QueryChangesResponseHandler queryChangesResponseHandler;
+ private final ChangeIndexer.Factory changeIndexerFactory;
+ private final ListeningExecutorService executor;
+ private final ChangeIndexCollection changeIndexes;
+ private final ChangeQueryBuilder queryBuilder;
+ private final Provider<ChangeQueryProcessor> queryProcessorProvider;
+ private final ChangeFinder changeFinder;
+ private final String age;
+
+ @AssistedInject
+ IndexSyncRunner(
+ Provider<Set<PeerInfo>> peerInfoProvider,
+ CloseableHttpClient httpClient,
+ @PluginName String pluginName,
+ QueryChangesResponseHandler queryChangesResponseHandler,
+ ChangeIndexer.Factory changeIndexerFactory,
+ @IndexExecutor(BATCH) ListeningExecutorService executor,
+ ChangeIndexCollection changeIndexes,
+ ChangeQueryBuilder queryBuilder,
+ Provider<ChangeQueryProcessor> queryProcessorProvider,
+ ChangeFinder changeFinder,
+ @Assisted String age) {
+ this.peerInfoProvider = peerInfoProvider;
+ this.httpClient = httpClient;
+ this.pluginRelativePath = Joiner.on("/").join("plugins", pluginName);
+ this.queryChangesResponseHandler = queryChangesResponseHandler;
+ this.changeIndexerFactory = changeIndexerFactory;
+ this.executor = executor;
+ this.changeIndexes = changeIndexes;
+ this.queryBuilder = queryBuilder;
+ this.queryProcessorProvider = queryProcessorProvider;
+ this.changeFinder = changeFinder;
+ this.age = age;
+ }
+
+ @Override
+ public Boolean get() {
+ log.atFine().log("Starting indexSync");
+ Set<PeerInfo> peers = peerInfoProvider.get();
+ if (peers.size() == 0) {
+ return false;
+ }
+
+ ChangeIndexer indexer = changeIndexerFactory.create(executor, changeIndexes, false);
+ // NOTE: this loop will stop as soon as the initial sync is performed from one peer
+ for (PeerInfo peer : peers) {
+ if (syncFrom(peer, indexer)) {
+ log.atFine().log("Finished indexSync");
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean syncFrom(PeerInfo peer, ChangeIndexer indexer) {
+ log.atFine().log("Syncing index with %s", peer.getDirectUrl());
+ String peerUrl = peer.getDirectUrl();
+ String uri =
+ Joiner.on("/").join(peerUrl, pluginRelativePath, "query/changes.updated.since", age);
+ HttpGet queryRequest = new HttpGet(uri);
+ List<String> ids;
+ try {
+ log.atFine().log("Executing %s", queryRequest);
+ ids = httpClient.execute(queryRequest, queryChangesResponseHandler);
+ } catch (IOException e) {
+ log.atSevere().withCause(e).log("Error while querying changes from %s", uri);
+ return false;
+ }
+
+ try {
+ List<ListenableFuture<Boolean>> indexingTasks = new ArrayList<>(ids.size());
+ for (String id : ids) {
+ indexingTasks.add(indexAsync(id, indexer));
+ }
+ Futures.allAsList(indexingTasks).get();
+ } catch (InterruptedException | ExecutionException e) {
+ log.atSevere().withCause(e).log("Error while reindexing %s", ids);
+ return false;
+ }
+
+ syncChangeDeletions(ids, indexer);
+
+ return true;
+ }
+
+ private ListenableFuture<Boolean> indexAsync(String id, ChangeIndexer indexer) {
+ List<String> fields = Splitter.on("~").splitToList(id);
+ if (fields.size() != 2) {
+ throw new IllegalArgumentException(String.format("Unexpected change ID format %s", id));
+ }
+ Project.NameKey projectName = Project.nameKey(fields.get(0));
+ Integer changeNumber = Ints.tryParse(fields.get(1));
+ if (changeNumber == null) {
+ throw new IllegalArgumentException(
+ String.format("Unexpected change number format %s", fields.get(1)));
+ }
+ log.atInfo().log("Scheduling async reindex of: %s", id);
+ return indexer.asyncReindexIfStale(projectName, Change.id(changeNumber));
+ }
+
+ private void syncChangeDeletions(List<String> theirChanges, ChangeIndexer indexer) {
+ Set<String> ourChanges = queryLocalIndex();
+ for (String d : Sets.difference(ourChanges, ImmutableSet.copyOf(theirChanges))) {
+ deleteIfMissingInNoteDb(d, indexer);
+ }
+ }
+
+ private Set<String> queryLocalIndex() {
+ ChangeQueryProcessor queryProcessor = queryProcessorProvider.get();
+ queryProcessor.enforceVisibility(false);
+ queryProcessor.setNoLimit(true);
+ Predicate<ChangeData> predicate = Predicate.not(queryBuilder.age(age));
+ QueryResult<ChangeData> result;
+ try {
+ result = queryProcessor.query(predicate);
+ } catch (QueryParseException e) {
+ throw new RuntimeException(e);
+ }
+
+ ImmutableList<ChangeData> cds = result.entities();
+ return cds.stream()
+ .map(cd -> cd.project().get() + "~" + cd.getId().get())
+ .collect(Collectors.toSet());
+ }
+
+ private void deleteIfMissingInNoteDb(String id, ChangeIndexer indexer) {
+ List<ChangeNotes> changeNotes = changeFinder.find(id);
+ if (changeNotes.isEmpty()) {
+ List<String> parts = Splitter.on("~").splitToList(id);
+ Project.NameKey project = Project.nameKey(parts.get(0));
+ Change.Id changeId = Change.id(Integer.parseInt(parts.get(1)));
+ log.atInfo().log("Change %s present in index but not in noteDb. Deleting from index", id);
+ indexer.deleteAsync(project, changeId);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncScheduler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncScheduler.java
new file mode 100644
index 0000000..9d647cf
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncScheduler.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.indexsync;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import dev.failsafe.Failsafe;
+import dev.failsafe.FailsafeExecutor;
+import dev.failsafe.RetryPolicy;
+import dev.failsafe.event.ExecutionCompletedEvent;
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+public class IndexSyncScheduler implements LifecycleListener {
+ private static final FluentLogger log = FluentLogger.forEnclosingClass();
+
+ private final WorkQueue workQueue;
+ private final IndexSyncRunner.Factory indexSyncRunnerFactory;
+ private final Configuration.IndexSync indexSync;
+
+ private ScheduledExecutorService executor;
+
+ @Inject
+ IndexSyncScheduler(
+ WorkQueue workQueue, IndexSyncRunner.Factory indexSyncRunnerFactory, Configuration cfg) {
+ this.workQueue = workQueue;
+ this.indexSyncRunnerFactory = indexSyncRunnerFactory;
+ this.indexSync = cfg.indexSync();
+ }
+
+ @Override
+ public void start() {
+ executor = workQueue.createQueue(4, "IndexSyncRunner");
+
+ scheduleInitialSync();
+ schedulePeriodicSync();
+ }
+
+ private void scheduleInitialSync() {
+ // Initial sync has to be run once but we may need to retry it until the other
+ // peer becomes reachable
+ // Therefore, we use failsafe to define and execute retries
+ RetryPolicy<Boolean> retryPolicy =
+ RetryPolicy.<Boolean>builder()
+ .withMaxAttempts(12 * 60) // 5s * 12 * 60 = 1 hour
+ .withDelay(Duration.ofSeconds(5))
+ .onRetriesExceeded(e -> logRetriesExceeded(e))
+ .handleResult(false)
+ .build();
+ FailsafeExecutor<Boolean> failsafeExecutor = Failsafe.with(retryPolicy).with(executor);
+
+ IndexSyncRunner sync = indexSyncRunnerFactory.create(indexSync.initialSyncAge());
+ failsafeExecutor.getAsync(sync);
+ }
+
+ private void schedulePeriodicSync() {
+ // Periodic sync runs at fixed rate and we don't need failsafe for retries
+ IndexSyncRunner sync = indexSyncRunnerFactory.create(indexSync.syncAge());
+ executor.scheduleAtFixedRate(
+ () -> sync.get(),
+ indexSync.delay().getSeconds(),
+ indexSync.period().getSeconds(),
+ TimeUnit.SECONDS);
+ }
+
+ private void logRetriesExceeded(ExecutionCompletedEvent<Boolean> e) {
+ log.atSevere().log("Retries for initial index sync exceeded %s", e);
+ }
+
+ @Override
+ public void stop() {
+ executor.shutdown();
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/QueryChangesResponseHandler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/QueryChangesResponseHandler.java
new file mode 100644
index 0000000..66e957a
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/QueryChangesResponseHandler.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ericsson.gerrit.plugins.highavailability.indexsync;
+
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.StatusLine;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpResponseException;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.util.EntityUtils;
+
+@Singleton
+public class QueryChangesResponseHandler implements ResponseHandler<List<String>> {
+
+ private final Gson gson = new Gson();
+
+ @Override
+ public List<String> handleResponse(HttpResponse rsp) throws ClientProtocolException, IOException {
+ StatusLine status = rsp.getStatusLine();
+ if (rsp.getStatusLine().getStatusCode() != SC_OK) {
+ throw new HttpResponseException(status.getStatusCode(), "Query failed");
+ }
+ HttpEntity entity = rsp.getEntity();
+ if (entity == null) {
+ return List.of();
+ }
+ String body = EntityUtils.toString(entity);
+ return gson.fromJson(body, new TypeToken<List<String>>() {}.getType());
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfo.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfo.java
index 74310ae..111a175 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfo.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/PeerInfo.java
@@ -14,6 +14,8 @@
package com.ericsson.gerrit.plugins.highavailability.peers;
+import java.util.Objects;
+
public class PeerInfo {
private final String directUrl;
@@ -25,4 +27,14 @@
public String getDirectUrl() {
return directUrl;
}
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(directUrl);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof PeerInfo) && Objects.equals(directUrl, ((PeerInfo) o).directUrl);
+ }
}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
index 776a0a4..63f65d6 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
@@ -14,6 +14,7 @@
package com.ericsson.gerrit.plugins.highavailability.peers.jgroups;
+import autovalue.shaded.com.google.common.collect.ImmutableMap;
import com.ericsson.gerrit.plugins.highavailability.Configuration;
import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
import com.google.common.annotations.VisibleForTesting;
@@ -24,8 +25,11 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.net.InetAddress;
+import java.util.Iterator;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import org.jgroups.Address;
import org.jgroups.JChannel;
import org.jgroups.Message;
@@ -39,9 +43,8 @@
* each gerrit server publishes its url to all cluster members (publishes it to all channels).
*
* <p>This provider maintains a list of all members which joined the jgroups cluster. This may be
- * more than two. But will always pick the first node which sent its url as the peer to be returned
- * by {@link #get()}. It will continue to return that node until that node leaves the jgroups
- * cluster.
+ * more than two. The set of urls of all peers is returned by {@link #get()}. If a node leaves the
+ * jgroups cluster it's removed from this set.
*/
@Singleton
public class JGroupsPeerInfoProvider
@@ -53,8 +56,7 @@
private final String myUrl;
private JChannel channel;
- private Optional<PeerInfo> peerInfo = Optional.empty();
- private Address peerAddress;
+ private Map<Address, PeerInfo> peers = new ConcurrentHashMap<>();
@Inject
JGroupsPeerInfoProvider(
@@ -70,34 +72,28 @@
@Override
public void receive(Message msg) {
- synchronized (this) {
- if (peerAddress != null) {
- return;
- }
- peerAddress = msg.getSrc();
- String url = (String) msg.getObject();
- peerInfo = Optional.of(new PeerInfo(url));
- log.atInfo().log("receive(): Set new peerInfo: %s", url);
+ String url = (String) msg.getObject();
+ if (url == null) {
+ return;
+ }
+ Address addr = msg.getSrc();
+ PeerInfo old = peers.put(addr, new PeerInfo(url));
+ if (old == null) {
+ log.atInfo().log("receive(): Add new peerInfo: %s", url);
+ } else {
+ log.atInfo().log("receive(): Update peerInfo: from %s to %s", old.getDirectUrl(), url);
}
}
@Override
public void viewAccepted(View view) {
log.atInfo().log("viewAccepted(view: %s) called", view);
- synchronized (this) {
- if (view.getMembers().size() > 2) {
- log.atWarning().log(
- "%d members joined the jgroups cluster %s (%s). "
- + " Only two members are supported. Members: %s",
- view.getMembers().size(),
- jgroupsConfig.clusterName(),
- channel.getName(),
- view.getMembers());
- }
- if (peerAddress != null && !view.getMembers().contains(peerAddress)) {
- log.atInfo().log("viewAccepted(): removed peerInfo");
- peerAddress = null;
- peerInfo = Optional.empty();
+ Iterator<Map.Entry<Address, PeerInfo>> it = peers.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<Address, PeerInfo> e = it.next();
+ if (!view.getMembers().contains(e.getKey())) {
+ log.atInfo().log("viewAccepted(): removed peerInfo %s", e.getValue().getDirectUrl());
+ it.remove();
}
}
if (view.size() > 1) {
@@ -144,14 +140,9 @@
this.channel = channel;
}
- @VisibleForTesting
- void setPeerInfo(Optional<PeerInfo> peerInfo) {
- this.peerInfo = peerInfo;
- }
-
@Override
public Set<PeerInfo> get() {
- return peerInfo.isPresent() ? ImmutableSet.of(peerInfo.get()) : ImmutableSet.of();
+ return ImmutableSet.copyOf(peers.values());
}
@Override
@@ -167,17 +158,19 @@
channel.getName(), jgroupsConfig.clusterName());
channel.close();
}
- peerInfo = Optional.empty();
- peerAddress = null;
+ peers.clear();
}
@VisibleForTesting
- Address getPeerAddress() {
- return peerAddress;
+ Map<Address, PeerInfo> getPeers() {
+ return ImmutableMap.copyOf(peers);
}
@VisibleForTesting
- void setPeerAddress(Address peerAddress) {
- this.peerAddress = peerAddress;
+ void addPeer(Address address, PeerInfo info) {
+ if (address == null) {
+ return;
+ }
+ this.peers.put(address, info);
}
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index d89f7b3..c7a3e35 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -105,6 +105,39 @@
Delay is expressed in Gerrit time values as in [websession.cleanupInterval](#websessioncleanupInterval).
When not specified, polling of conditional reindexing is disabled.
+**NOTE:** The indexSync feature exposes a REST endpoint that can be used to discover project names.
+Admins are advised to restrict access to the REST endpoints exposed by this plugin.
+
+```indexSync.enabled```
+: When indexSync is enabled, the primary servers will synchronize indexes with the intention to
+ self-heal any missed reindexing event.
+
+```indexSync.delay```
+: If enabled, index sync will start running after this initial delay.
+ Delay is expressed in Gerrit time values as in [websession.cleanupInterval](#websessioncleanupInterval).
+ When not specified, the default is zero: run immediately.
+
+```indexSync.period```
+ Period between two index sync executions. If any execution of this task takes longer than
+ this period, then subsequent executions may start late, but will not concurrently execute.
+ Delay is expressed in Gerrit time values as in [websession.cleanupInterval](#websessioncleanupInterval).
+ When not specified, the default is `2 seconds`.
+
+```indexSync.initialSyncAge```
+ This options defines the max age of changes in the other peer for which local index shall
+ be synchronized on the initial run of the index sync task. The age defined here is usualy
+ larger than the `syncAge` in order to accommodate max foreseen downtime of a server during
+ restarts.
+ The age is express in the format of the `age:` change query parameter.
+ When not specified, the default is `1hour`.
+
+```indexSync.syncAge```
+ This option defines the max age of changes in the other peer for this local index shall be
+ synchronized on each run, except for the initial run.
+ The age is express in the format of the `age:` change query parameter.
+ When not specified, the default is `5minutes`.
+
+
```peerInfo.strategy```
: Strategy to find other peers. Supported strategies are `static` or `jgroups`.
Defaults to `jgroups`.
diff --git a/src/test/README.md b/src/test/README.md
index fd59f64..16e4df9 100644
--- a/src/test/README.md
+++ b/src/test/README.md
@@ -1,24 +1,17 @@
# Gerrit high-availability docker setup example
-The Docker Compose project in the docker directory contains a simple test
+The Docker Compose project in the docker directory contains a simple test
environment of two Gerrit masters in HA configuration, with their git repos
hosted on NFS filesystem.
## How to build
-The project can be built using docker-compose (make sure you set the
-`platform` attribute in the docker-compose.yaml file if you're not
-in an amd64 arch).
+The project can be built using docker compose.
To build the Docker VMs:
+
```bash
- # first, remove the buildx if it exists and its not running
- $ docker buildx inspect docker-ha | grep Status
- $ docker buildx rm docker-ha
- # create the docker-ha buildx node, provide your architecture and start it up
- docker buildx create --name docker-ha --platform "linux/amd64" --driver docker-container --use \
- && docker buildx inspect --bootstrap \
- && docker-compose build
+ docker compose build
```
### Building the Docker VMs using a non-default user id
@@ -28,7 +21,7 @@
Then, run the following:
```
$ export GERRIT_UID=$(id -u)
- $ docker-compose build --build-arg GERRIT_UID
+ $ docker compose build --build-arg GERRIT_UID
```
Above, exporting that UID is optional and will be 1000 by default.
@@ -51,10 +44,11 @@
Use the 'up' target to startup the Docker Compose VMs.
```
- $ docker-compose up -d
+ $ docker compose up -d
```
## Background on using an NFS server
+
We are using the `erichough/nfs-server` image mainly because it's easy to use
& we had success with it. The work has been inspired by
[this blog post](https://nothing2say.co.uk/running-a-linux-based-nfs-server-in-docker-on-windows-b64445d5ada2).
@@ -67,11 +61,11 @@
shared over the network. You can change this in the nfs server and gerrit
docker files, and in the `exports.txt` file.
-The NFS server is using a static IP. The Docker Compose YAML file defines a
+The NFS server is using a static IP. The Docker Compose YAML file defines a
bridge network with the subnet `192.168.1.0/24` (this is what allows us to
give the NFS Server a known, static IP).
-The `addr=192.168.1.5` option (in the `nfs-client-volume` volume) is the
+The `addr=192.168.1.5` option (in the `nfs-client-volume` volume) is the
reason we need a static IP for the server (and hence a configured subnet
for the network). Note that using a name (ie. addr=nfs-server) we weren't
able to get the DNS resolution to work properly.
@@ -86,7 +80,7 @@
into the image sacrificing a bit of flexibility, but we feel this is
a small price to pay to have everything automated.
-# Gerrit high-availability local setup example
+## Gerrit high-availability local setup example
1. Init gerrit instances with high-availability plugin installed:
1. Optionally, set http port of those instance to 8081 and 8082.
@@ -137,5 +131,5 @@
target.
```
- $ docker-compose down
+ $ docker compose down
```
diff --git a/src/test/docker/docker-compose.elasticsearch.yaml b/src/test/docker/docker-compose.elasticsearch.yaml
new file mode 100644
index 0000000..31f6269
--- /dev/null
+++ b/src/test/docker/docker-compose.elasticsearch.yaml
@@ -0,0 +1,157 @@
+version: '3'
+
+services:
+
+ nfs-server:
+ build: nfs
+# platform: linux/arm64/v8 # uncomment for Apple Silicon arch
+ privileged: true
+ container_name: nfs-server
+ environment:
+ NFS_LOG_LEVEL: DEBUG
+ hostname: nfs-server
+ healthcheck:
+ test: ["CMD-SHELL", "sleep 10"] # required, otherwise the gerrit service will fail to start with a "connection refused" error
+ interval: 1s
+ timeout: 1m
+ retries: 10
+ ports:
+ - 2049:2049
+ networks:
+ gerrit-net:
+ ipv4_address: 192.168.1.5
+ volumes:
+ - nfs-server-volume:/var/gerrit/git
+
+ zookeeper-refdb:
+ image: zookeeper
+ ports:
+ - "2181:2181"
+ networks:
+ - gerrit-net
+ healthcheck:
+ test: ["CMD-SHELL", "./bin/zkServer.sh", "status"] # required, otherwise the gerrit service will fail to start with a "connection refused" error
+ interval: 1s
+ timeout: 1m
+ retries: 10
+
+ elasticsearch:
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.9.2
+ container_name: elasticsearch
+ environment:
+ - cluster.name=elasticsearch-cluster
+ - node.name=elasticsearch
+ - cluster.initial_master_nodes=elasticsearch
+ - bootstrap.memory_lock=true
+ - xpack.security.enabled=false
+ - xpack.security.http.ssl.enabled=false
+ - ELASTIC_PASSWORD=os_Secret1234
+ ulimits:
+ memlock:
+ soft: -1
+ hard: -1
+ nofile:
+ soft: 65536
+ hard: 65536
+ volumes:
+ - elasticsearch-data:/usr/share/elasticsearch/data
+ ports:
+ - 9200:9200
+ - 9600:9600
+ networks:
+ - gerrit-net
+ healthcheck:
+ test: ["CMD-SHELL", "curl -k -u elastic:os_Secret1234 --silent --fail http://localhost:9200/_cluster/health"]
+ interval: 10s
+ timeout: 1m
+ retries: 10
+ start_period: 10s
+ start_interval: 5s
+
+ gerrit-01:
+ build: gerrit
+ privileged: true
+ depends_on:
+ elasticsearch:
+ condition: service_healthy
+ nfs-server:
+ condition: service_healthy
+ zookeeper-refdb:
+ condition: service_healthy
+ ports:
+ - "8081:8080"
+ - "29411:29418"
+ networks:
+ - gerrit-net
+ volumes:
+ - /dev/urandom:/dev/random
+ - git-volume:/var/gerrit/git
+ - shareddir:/var/gerrit/shareddir
+ - ./etc/gerrit_es.config:/var/gerrit/etc/gerrit.config.orig
+ - ./etc/high-availability.gerrit-01.config:/var/gerrit/etc/high-availability.config.orig
+ - ./etc/zookeeper-refdb.config:/var/gerrit/etc/zookeeper-refdb.config.orig
+ environment:
+ - HOSTNAME=localhost
+ - INDEX_TYPE=ELASTICSEARCH
+
+ gerrit-02:
+ build: gerrit
+ privileged: true
+ ports:
+ - "8082:8080"
+ - "29412:29418"
+ networks:
+ - gerrit-net
+ depends_on:
+ gerrit-01:
+ condition: service_started
+ nfs-server:
+ condition: service_healthy
+ volumes:
+ - /dev/urandom:/dev/random
+ - git-volume:/var/gerrit/git
+ - shareddir:/var/gerrit/shareddir
+ - ./etc/gerrit_es.config:/var/gerrit/etc/gerrit.config.orig
+ - ./etc/high-availability.gerrit-02.config:/var/gerrit/etc/high-availability.config.orig
+ - ./etc/zookeeper-refdb.config:/var/gerrit/etc/zookeeper-refdb.config.orig
+ environment:
+ - HOSTNAME=localhost
+ - INDEX_TYPE=ELASTICSEARCH
+ - WAIT_FOR=gerrit-01:8080
+
+ haproxy:
+ build: haproxy
+ ports:
+ - "80:80"
+ - "29418:29418"
+ networks:
+ - gerrit-net
+ volumes_from:
+ - syslog-sidecar
+ depends_on:
+ - syslog-sidecar
+ - gerrit-01
+ - gerrit-02
+
+ syslog-sidecar:
+ build: docker-syslog-ng-stdout
+ networks:
+ - gerrit-net
+
+networks:
+ gerrit-net:
+ ipam:
+ driver: default
+ config:
+ - subnet: 192.168.1.0/24
+
+volumes:
+ shareddir:
+ nfs-server-volume:
+ elasticsearch-data:
+ git-volume:
+ driver: "local"
+ driver_opts:
+ type: nfs
+ o: "addr=192.168.1.5,rw"
+ device: ":/var/gerrit/git"
diff --git a/src/test/docker/docker-compose.yaml b/src/test/docker/docker-compose.yaml
index 8524963..2e7981d 100644
--- a/src/test/docker/docker-compose.yaml
+++ b/src/test/docker/docker-compose.yaml
@@ -2,7 +2,6 @@
nfs-server:
build: nfs
-# platform: linux/arm64/v8 # uncomment for Apple Silicon arch
privileged: true
container_name: nfs-server
environment:
@@ -16,27 +15,43 @@
ports:
- 2049:2049
networks:
- nfs-server-bridge:
+ gerrit-net:
ipv4_address: 192.168.1.5
volumes:
- nfs-server-volume:/var/gerrit/git
+
+ zookeeper-refdb:
+ image: zookeeper
+ ports:
+ - "2181:2181"
+ networks:
+ - gerrit-net
+ healthcheck:
+ test: ["CMD-SHELL", "./bin/zkServer.sh", "status"] # required, otherwise the gerrit service will fail to start with a "connection refused" error
+ interval: 1s
+ timeout: 1m
+ retries: 10
+
gerrit-01:
build: gerrit
privileged: true
depends_on:
nfs-server:
condition: service_healthy
+ zookeeper-refdb:
+ condition: service_healthy
ports:
- "8081:8080"
- "29411:29418"
networks:
- nfs-server-bridge: null
+ - gerrit-net
volumes:
- /dev/urandom:/dev/random
- git-volume:/var/gerrit/git
- shareddir:/var/gerrit/shareddir
- ./etc/gerrit.config:/var/gerrit/etc/gerrit.config.orig
- ./etc/high-availability.gerrit-01.config:/var/gerrit/etc/high-availability.config.orig
+ - ./etc/zookeeper-refdb.config:/var/gerrit/etc/zookeeper-refdb.config.orig
environment:
- HOSTNAME=localhost
@@ -47,7 +62,7 @@
- "8082:8080"
- "29412:29418"
networks:
- nfs-server-bridge: null
+ - gerrit-net
depends_on:
gerrit-01:
condition: service_started
@@ -59,6 +74,7 @@
- shareddir:/var/gerrit/shareddir
- ./etc/gerrit.config:/var/gerrit/etc/gerrit.config.orig
- ./etc/high-availability.gerrit-02.config:/var/gerrit/etc/high-availability.config.orig
+ - ./etc/zookeeper-refdb.config:/var/gerrit/etc/zookeeper-refdb.config.orig
environment:
- HOSTNAME=localhost
- WAIT_FOR=gerrit-01:8080
@@ -69,9 +85,9 @@
- "80:80"
- "29418:29418"
networks:
- nfs-server-bridge: null
- volumes:
- - syslog-sidecar:/syslog-sidecar
+ - gerrit-net
+ volumes_from:
+ - syslog-sidecar
depends_on:
- syslog-sidecar
- gerrit-01
@@ -80,10 +96,10 @@
syslog-sidecar:
build: docker-syslog-ng-stdout
networks:
- nfs-server-bridge: null
+ - gerrit-net
networks:
- nfs-server-bridge:
+ gerrit-net:
ipam:
driver: default
config:
diff --git a/src/test/docker/etc/gerrit.config b/src/test/docker/etc/gerrit.config
index 90a4057..408316a 100644
--- a/src/test/docker/etc/gerrit.config
+++ b/src/test/docker/etc/gerrit.config
@@ -2,11 +2,14 @@
basePath = git
canonicalWebUrl = http://gerrit:8080/
serverId = f7696647-8efd-41b1-bd60-d321bc071ea9
+ installDbModule = com.ericsson.gerrit.plugins.highavailability.ValidationModule
+ installModule = com.gerritforge.gerrit.globalrefdb.validation.LibModule
[index]
type = LUCENE
[auth]
type = DEVELOPMENT_BECOME_ANY_ACCOUNT
cookiedomain = localhost
+ cookieHttpOnly = false
[sendemail]
smtpServer = localhost
[sshd]
diff --git a/src/test/docker/etc/gerrit_es.config b/src/test/docker/etc/gerrit_es.config
new file mode 100644
index 0000000..669a3af
--- /dev/null
+++ b/src/test/docker/etc/gerrit_es.config
@@ -0,0 +1,30 @@
+[gerrit]
+ basePath = git
+ canonicalWebUrl = http://gerrit:8080/
+ serverId = f7696647-8efd-41b1-bd60-d321bc071ea9
+ installDbModule = com.ericsson.gerrit.plugins.highavailability.ValidationModule
+ installModule = com.gerritforge.gerrit.globalrefdb.validation.LibModule
+ installIndexModule = com.google.gerrit.elasticsearch.PrimaryElasticIndexModule
+[elasticsearch]
+ server = http://elasticsearch:9200
+ username = elastic
+ password = os_Secret1234
+[auth]
+ type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+ cookiedomain = localhost
+ cookieHttpOnly = false
+[sendemail]
+ smtpServer = localhost
+[sshd]
+ listenAddress = *:29418
+[httpd]
+ listenUrl = proxy-http://*:8080/
+ requestLog = true
+[cache]
+ directory = cache
+[container]
+ user = gerrit
+[download]
+ scheme = http
+ scheme = ssh
+ scheme = anon_http
diff --git a/src/test/docker/etc/high-availability.gerrit-01.config b/src/test/docker/etc/high-availability.gerrit-01.config
index a21f05c..d8619c1 100644
--- a/src/test/docker/etc/high-availability.gerrit-01.config
+++ b/src/test/docker/etc/high-availability.gerrit-01.config
@@ -6,3 +6,6 @@
[peerInfo "static"]
url = http://gerrit-02:8080
+
+[ref-database]
+ enabled = true
diff --git a/src/test/docker/etc/high-availability.gerrit-02.config b/src/test/docker/etc/high-availability.gerrit-02.config
index d05c7ec..54cc1f2 100644
--- a/src/test/docker/etc/high-availability.gerrit-02.config
+++ b/src/test/docker/etc/high-availability.gerrit-02.config
@@ -5,4 +5,7 @@
strategy = static
[peerInfo "static"]
- url = http://gerrit-01:8080
\ No newline at end of file
+ url = http://gerrit-01:8080
+
+[ref-database]
+ enabled = true
diff --git a/src/test/docker/etc/zookeeper-refdb.config b/src/test/docker/etc/zookeeper-refdb.config
new file mode 100644
index 0000000..d3cdb80
--- /dev/null
+++ b/src/test/docker/etc/zookeeper-refdb.config
@@ -0,0 +1,4 @@
+[ref-database "zookeeper"]
+ connectString = "zookeeper-refdb:2181"
+ rootNode = "gerrit/HA"
+ transactionLockTimeoutMs = 1000
diff --git a/src/test/docker/gerrit/Dockerfile b/src/test/docker/gerrit/Dockerfile
index e70e0ea..aeac272 100644
--- a/src/test/docker/gerrit/Dockerfile
+++ b/src/test/docker/gerrit/Dockerfile
@@ -1,4 +1,4 @@
-FROM almalinux:9.3
+FROM almalinux:9.4
# Install dependencies
RUN yum -y install \
@@ -10,7 +10,7 @@
gettext \
&& yum -y clean all
-ENV GERRIT_VERSION=3.10
+ENV GERRIT_VERSION master
# Add gerrit user
RUN adduser -p -m --uid 1000 gerrit --home-dir /home/gerrit
@@ -22,19 +22,32 @@
RUN mkdir -p /var/gerrit && chown -R gerrit /var/gerrit
ADD --chown=gerrit \
- "https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-stable-$GERRIT_VERSION/lastSuccessfulBuild/artifact/gerrit/bazel-bin/release.war" \
+ "https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/gerrit/bazel-bin/release.war" \
/tmp/gerrit.war
ADD --chown=gerrit \
-"https://gerrit-ci.gerritforge.com/job/plugin-javamelody-bazel-master-stable-$GERRIT_VERSION/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody.jar" \
+ "https://gerrit-ci.gerritforge.com/view/Plugins-master/job/plugin-javamelody-bazel-master-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody.jar" \
/var/gerrit/plugins/javamelody.jar
+
ADD --chown=gerrit \
- "https://gerrit-ci.gerritforge.com/job/plugin-high-availability-bazel-stable-$GERRIT_VERSION/lastSuccessfulBuild/artifact/bazel-bin/plugins/high-availability/high-availability.jar" \
+ "https://gerrit-ci.gerritforge.com/job/plugin-high-availability-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/high-availability/high-availability.jar" \
/var/gerrit/plugins/high-availability.jar
-ADD --chown=gerrit \
- "https://gerrit-ci.gerritforge.com/job/module-global-refdb-bazel-stable-$GERRIT_VERSION/lastSuccessfulBuild/artifact/bazel-bin/plugins/global-refdb/global-refdb.jar" \
+
+RUN mkdir -p /var/gerrit/lib && \
+ ln -sf /var/gerrit/plugins/high-availability.jar /var/gerrit/lib/high-availability.jar
+
+ADD --chown=gerrit:gerrit \
+ "https://gerrit-ci.gerritforge.com/job/module-global-refdb-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/global-refdb/global-refdb.jar" \
/var/gerrit/lib/global-refdb.jar
+ADD --chown=gerrit:gerrit \
+ "https://gerrit-ci.gerritforge.com/job/plugin-zookeeper-refdb-bazel-$GERRIT_BRANCH/lastSuccessfulBuild/artifact/bazel-bin/plugins/zookeeper-refdb/zookeeper-refdb.jar" \
+ /var/gerrit/plugins/zookeeper-refdb.jar
+
+ADD --chown=gerrit:gerrit \
+ "https://gerrit-ci.gerritforge.com/view/Plugins-master/job/module-index-elasticsearch-bazel-master/lastSuccessfulBuild/artifact/bazel-bin/plugins/index-elasticsearch/index-elasticsearch.jar" \
+ /var/gerrit_plugins/index-elasticsearch.jar
+
ADD --chown=gerrit:gerrit ./wait-for-it.sh /bin
# Change user
diff --git a/src/test/docker/gerrit/entrypoint.sh b/src/test/docker/gerrit/entrypoint.sh
index 2d4e387..e6181da 100755
--- a/src/test/docker/gerrit/entrypoint.sh
+++ b/src/test/docker/gerrit/entrypoint.sh
@@ -8,7 +8,15 @@
chown -R gerrit /var/gerrit/etc
sudo -u gerrit cp /var/gerrit/etc/gerrit.config.orig /var/gerrit/etc/gerrit.config
sudo -u gerrit cp /var/gerrit/etc/high-availability.config.orig /var/gerrit/etc/high-availability.config
+sudo -u gerrit cp /var/gerrit/etc/zookeeper-refdb.config.orig /var/gerrit/etc/zookeeper-refdb.config
+sudo -u gerrit git config -f /var/gerrit/etc/healthcheck.config healthcheck.auth.enabled false
+sudo -u gerrit git config -f /var/gerrit/etc/healthcheck.config healthcheck.querychanges.enabled false
+sudo -u gerrit git config -f /var/gerrit/etc/healthcheck.config healthcheck.changesindex.enabled false
+if [[ "$INDEX_TYPE" == "ELASTICSEARCH" ]]; then
+ ln -sf /var/gerrit_plugins/index-elasticsearch.jar /var/gerrit/lib/index-elasticsearch.jar
+ ln -sf /var/gerrit_plugins/index-elasticsearch.jar /var/gerrit/plugins/index-elasticsearch.jar
+fi
echo "Init gerrit..."
sudo -u gerrit java -jar /tmp/gerrit.war init -d /var/gerrit --batch --install-all-plugins
diff --git a/src/test/docker/haproxy/haproxy.cfg b/src/test/docker/haproxy/haproxy.cfg
index e86cdce..07a6746 100644
--- a/src/test/docker/haproxy/haproxy.cfg
+++ b/src/test/docker/haproxy/haproxy.cfg
@@ -45,7 +45,7 @@
timeout connect 10s
timeout server 5m
server gerrit_ssh_01 gerrit-01:29418 check port 8080 inter 10s fall 3 rise 2
- server gerrit-ssh_02 gerrit-02:29418 check port 8080 inter 10s fall 3 rise 2 backup
+ server gerrit-ssh_02 gerrit-02:29418 check port 8080 inter 10s fall 3 rise 2
backend gerrit_http_nodes
mode http
@@ -55,7 +55,7 @@
option httpchk GET /config/server/version HTTP/1.0
http-check expect status 200
server gerrit_01 gerrit-01:8080 check
- server gerrit_02 gerrit-02:8080 check backup
+ server gerrit_02 gerrit-02:8080 check
backend gerrit_http_nodes_balanced
mode http
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java
index 3e1f8fd..73e3401 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedEventHandlerTest.java
@@ -15,7 +15,6 @@
package com.ericsson.gerrit.plugins.highavailability.forwarder;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
@@ -82,9 +81,7 @@
.postEvent(event);
assertThat(Context.isForwardedEvent()).isFalse();
- PermissionBackendException thrown =
- assertThrows(PermissionBackendException.class, () -> handler.dispatch(event));
- assertThat(thrown).hasMessageThat().isEqualTo("someMessage");
+ handler.dispatch(event);
assertThat(Context.isForwardedEvent()).isFalse();
verify(dispatcherMock).postEvent(event);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
index be74231..27d5b2d 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
@@ -31,6 +31,7 @@
import com.ericsson.gerrit.plugins.highavailability.index.ChangeCheckerImpl;
import com.ericsson.gerrit.plugins.highavailability.index.ForwardedIndexExecutorProvider;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.util.OneOffRequestContext;
@@ -59,6 +60,7 @@
@Mock private ChangeIndexer indexerMock;
@Mock private ChangeNotes changeNotes;
+ @Mock private Project.NameKey projectName;
@Mock(answer = RETURNS_DEEP_STUBS)
private Configuration configMock;
@@ -87,14 +89,15 @@
public void changeIsIndexedWhenUpToDate() throws Exception {
setupChangeAccessRelatedMocks(CHANGE_EXISTS, CHANGE_UP_TO_DATE);
handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty()).get(10, SECONDS);
- verify(indexerMock, times(1)).index(any(ChangeNotes.class));
+ verify(indexerMock, times(1)).reindexIfStale(any(Project.NameKey.class), any(Change.Id.class));
}
@Test
public void changeIsStillIndexedEvenWhenOutdated() throws Exception {
setupChangeAccessRelatedMocks(CHANGE_EXISTS, CHANGE_OUTDATED);
handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.of(new IndexEvent())).get(10, SECONDS);
- verify(indexerMock, atLeast(1)).index(any(ChangeNotes.class));
+ verify(indexerMock, atLeast(1))
+ .reindexIfStale(any(Project.NameKey.class), any(Change.Id.class));
}
@Test
@@ -122,13 +125,13 @@
return null;
})
.when(indexerMock)
- .index(any(ChangeNotes.class));
+ .reindexIfStale(any(Project.NameKey.class), any(Change.Id.class));
assertThat(Context.isForwardedEvent()).isFalse();
handler.index(TEST_CHANGE_ID, Operation.INDEX, Optional.empty()).get(10, SECONDS);
assertThat(Context.isForwardedEvent()).isFalse();
- verify(indexerMock, times(1)).index(any(ChangeNotes.class));
+ verify(indexerMock, times(1)).reindexIfStale(any(Project.NameKey.class), any(Change.Id.class));
}
@Test
@@ -141,7 +144,7 @@
throw new IOException("someMessage");
})
.when(indexerMock)
- .index(any(ChangeNotes.class));
+ .reindexIfStale(any(Project.NameKey.class), any(Change.Id.class));
assertThat(Context.isForwardedEvent()).isFalse();
ExecutionException thrown =
@@ -152,7 +155,7 @@
assertThat(thrown.getCause()).hasMessageThat().isEqualTo("someMessage");
assertThat(Context.isForwardedEvent()).isFalse();
- verify(indexerMock, times(1)).index(any(ChangeNotes.class));
+ verify(indexerMock, times(1)).reindexIfStale(any(Project.NameKey.class), any(Change.Id.class));
}
private void setupChangeAccessRelatedMocks(boolean changeExists, boolean changeIsUpToDate)
@@ -161,7 +164,8 @@
when(changeCheckerFactoryMock.create(TEST_CHANGE_ID)).thenReturn(changeCheckerPresentMock);
when(changeCheckerPresentMock.getChangeNotes()).thenReturn(Optional.of(changeNotes));
}
-
+ when(changeNotes.getChangeId()).thenReturn(id);
+ when(changeNotes.getProjectName()).thenReturn(projectName);
when(changeCheckerPresentMock.isChangeUpToDate(any())).thenReturn(changeIsUpToDate);
}
}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java
index 3c6b931..7c23e29 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/rest/EventRestApiServletTest.java
@@ -26,6 +26,7 @@
import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedEventHandler;
import com.google.common.net.MediaType;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.events.EventDispatcher;
import com.google.gerrit.server.events.EventGsonProvider;
import com.google.gerrit.server.events.EventTypes;
import com.google.gerrit.server.events.RefEvent;
@@ -41,6 +42,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
@@ -85,11 +87,15 @@
+ "\"refs/changes/76/669676/2\",\"nodesCount\":1,\"type\":"
+ "\"ref-replication-done\",\"eventCreatedOn\":1451415011}";
when(requestMock.getReader()).thenReturn(new BufferedReader(new StringReader(event)));
+
+ EventDispatcher dispatcher = Mockito.mock(EventDispatcher.class);
doThrow(new PermissionBackendException(ERR_MSG))
- .when(forwardedEventHandlerMock)
- .dispatch(any(RefReplicationDoneEvent.class));
+ .when(dispatcher)
+ .postEvent(any(RefReplicationDoneEvent.class));
+ ForwardedEventHandler forwardedEventHandler = new ForwardedEventHandler(dispatcher);
+ eventRestApiServlet = new EventRestApiServlet(forwardedEventHandler, gson);
eventRestApiServlet.doPost(requestMock, responseMock);
- verify(responseMock).sendError(SC_BAD_REQUEST, ERR_MSG);
+ verify(responseMock).setStatus(SC_NO_CONTENT);
}
@Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImplTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImplTest.java
index 2cf3fad..dfea1df 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImplTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImplTest.java
@@ -19,7 +19,6 @@
import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -38,7 +37,6 @@
public class ChangeCheckerImplTest {
@Mock private GitRepositoryManager gitRepoMgr;
- @Mock private DraftCommentsReader draftCommentsReader;
@Mock private ChangeFinder changeFinder;
@Mock private OneOffRequestContext oneOffReqCtx;
@Mock private ChangeNotes testChangeNotes;
@@ -52,9 +50,7 @@
@Before
public void setUp() {
- changeChecker =
- new ChangeCheckerImpl(
- gitRepoMgr, draftCommentsReader, changeFinder, oneOffReqCtx, changeId);
+ changeChecker = new ChangeCheckerImpl(gitRepoMgr, changeFinder, oneOffReqCtx, changeId);
}
@Test
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsKubernetesPeerInfoProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsKubernetesPeerInfoProviderTest.java
index fc8c29a..0c53959 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsKubernetesPeerInfoProviderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsKubernetesPeerInfoProviderTest.java
@@ -87,6 +87,8 @@
when(pluginConfigurationMock.jgroupsKubernetes().namespace()).thenReturn(namespace);
when(pluginConfigurationMock.jgroupsKubernetes().labels()).thenReturn(labels);
+ when(myUrlProvider.get()).thenReturn("http://127.0.0.1:7800");
+
NetworkInterface eth0 = NetworkInterface.getByName("eth0");
if (eth0 != null) {
when(finder.findAddress()).thenReturn(eth0.inetAddresses().findFirst());
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProviderTest.java
index fd203e0..a098be6 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProviderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProviderTest.java
@@ -16,12 +16,12 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import com.ericsson.gerrit.plugins.highavailability.Configuration;
import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
import java.util.List;
-import java.util.Optional;
import java.util.Set;
import org.jgroups.Address;
import org.jgroups.JChannel;
@@ -43,7 +43,7 @@
private InetAddressFinder finder;
private JGroupsPeerInfoProvider jGroupsPeerInfoProvider;
- private Optional<PeerInfo> peerInfo;
+ private PeerInfo peerInfo;
@Mock private JChannel channel;
@Mock private MyUrlProvider myUrlProviderTest;
@Mock private Message message;
@@ -57,7 +57,7 @@
JChannel channel = new JChannelProvider(pluginConfigurationMock).get();
jGroupsPeerInfoProvider =
new JGroupsPeerInfoProvider(pluginConfigurationMock, finder, myUrlProviderTest, channel);
- peerInfo = Optional.of(new PeerInfo("test message"));
+ peerInfo = new PeerInfo("test message");
channel.setName("testChannel");
}
@@ -68,7 +68,7 @@
jGroupsPeerInfoProvider.receive(message);
- assertThat(jGroupsPeerInfoProvider.getPeerAddress()).isEqualTo(peerAddress);
+ assertThat(jGroupsPeerInfoProvider.getPeers()).containsKey(peerAddress);
Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
for (PeerInfo testPeerInfo : testPeerInfoSet) {
assertThat(testPeerInfo.getDirectUrl()).contains("test message");
@@ -78,20 +78,52 @@
@Test
public void testReceiveWhenPeerAddressIsNotNull() throws Exception {
- jGroupsPeerInfoProvider.setPeerAddress(new IpAddress("checkAddress.com"));
+ lenient().when(message.getSrc()).thenReturn(peerAddress);
+ when(message.getObject()).thenReturn(null);
jGroupsPeerInfoProvider.receive(message);
Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
- assertThat(testPeerInfoSet.isEmpty()).isTrue();
- assertThat(testPeerInfoSet.size()).isEqualTo(0);
+ assertThat(testPeerInfoSet).isEmpty();
+ }
+
+ @Test
+ public void testReceiveMultiplePeers() throws Exception {
+ IpAddress addr1 = new IpAddress("192.168.1.5:7800");
+ IpAddress addr2 = new IpAddress("192.168.1.6:7800");
+ IpAddress addr3 = new IpAddress("192.168.1.7:7800");
+ PeerInfo peer1 = new PeerInfo("URL1");
+ PeerInfo peer2 = new PeerInfo("URL2");
+ PeerInfo peer3 = new PeerInfo("URL3");
+
+ receive(addr1, peer1);
+ receive(addr2, peer2);
+ receive(addr3, peer3);
+
+ Set<PeerInfo> peers = jGroupsPeerInfoProvider.get();
+ assertThat(peers.size()).isEqualTo(3);
+ assertThat(peers).containsExactly(peer1, peer2, peer3);
+
+ // remove one peer with address ADDR1 from the view
+ List<Address> reducedView = List.of(addr2, addr3);
+ when(view.getMembers()).thenReturn(reducedView);
+ when(view.size()).thenReturn(2);
+ jGroupsPeerInfoProvider.setChannel(channel);
+ jGroupsPeerInfoProvider.viewAccepted(view);
+ peers = jGroupsPeerInfoProvider.get();
+ assertThat(peers.size()).isEqualTo(2);
+ assertThat(peers).containsExactly(peer2, peer3);
+ }
+
+ public void receive(final IpAddress addr, final PeerInfo peer) {
+ when(message.getSrc()).thenReturn(addr);
+ when(message.getObject()).thenReturn(peer.getDirectUrl());
+ jGroupsPeerInfoProvider.receive(message);
}
@Test(expected = None.class)
public void testViewAcceptedWithNoExceptionThrown() throws Exception {
- when(view.getMembers()).thenReturn(members);
when(view.size()).thenReturn(3);
- when(members.size()).thenReturn(3);
jGroupsPeerInfoProvider.setChannel(channel);
jGroupsPeerInfoProvider.viewAccepted(view);
}
@@ -100,37 +132,32 @@
public void testViewAcceptedWhenPeerAddressIsNotNullAndIsNotMemberOfView() {
when(view.getMembers()).thenReturn(members);
when(view.size()).thenReturn(2);
- when(members.size()).thenReturn(2);
when(members.contains(peerAddress)).thenReturn(false);
- jGroupsPeerInfoProvider.setPeerAddress(peerAddress);
- jGroupsPeerInfoProvider.setPeerInfo(peerInfo);
+ jGroupsPeerInfoProvider.addPeer(peerAddress, peerInfo);
jGroupsPeerInfoProvider.setChannel(channel);
jGroupsPeerInfoProvider.viewAccepted(view);
- assertThat(jGroupsPeerInfoProvider.getPeerAddress()).isEqualTo(null);
+ assertThat(jGroupsPeerInfoProvider.getPeers()).isEmpty();
Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
- assertThat(testPeerInfoSet.isEmpty()).isTrue();
- assertThat(testPeerInfoSet.size()).isEqualTo(0);
+ assertThat(testPeerInfoSet).isEmpty();
}
@Test
- public void testConnect() throws NoSuchFieldException, IllegalAccessException {
+ public void testConnect() {
jGroupsPeerInfoProvider.connect();
Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
- assertThat(testPeerInfoSet.isEmpty()).isTrue();
- assertThat(testPeerInfoSet.size()).isEqualTo(0);
+ assertThat(testPeerInfoSet).isEmpty();
}
@Test
public void testGetWhenPeerInfoIsOptionalEmpty() {
Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
- assertThat(testPeerInfoSet.isEmpty()).isTrue();
- assertThat(testPeerInfoSet.size()).isEqualTo(0);
+ assertThat(testPeerInfoSet).isEmpty();
}
@Test
public void testGetWhenPeerInfoIsPresent() {
- jGroupsPeerInfoProvider.setPeerInfo(peerInfo);
+ jGroupsPeerInfoProvider.addPeer(peerAddress, peerInfo);
Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
for (PeerInfo testPeerInfo : testPeerInfoSet) {
assertThat(testPeerInfo.getDirectUrl()).contains("test message");
@@ -140,12 +167,10 @@
@Test
public void testStop() throws Exception {
- jGroupsPeerInfoProvider.setPeerAddress(peerAddress);
- jGroupsPeerInfoProvider.setPeerInfo(peerInfo);
+ jGroupsPeerInfoProvider.addPeer(peerAddress, peerInfo);
jGroupsPeerInfoProvider.stop();
- assertThat(jGroupsPeerInfoProvider.getPeerAddress()).isEqualTo(null);
+ assertThat(jGroupsPeerInfoProvider.getPeers().isEmpty());
Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
- assertThat(testPeerInfoSet.isEmpty()).isTrue();
- assertThat(testPeerInfoSet.size()).isEqualTo(0);
+ assertThat(testPeerInfoSet).isEmpty();
}
}