Merge branch 'stable-3.11' into stable-3.12 * stable-3.11: Add missing binding of ProjectDeletedListener Fix the Docker-based setup for the HA test environment Verify high-availability formatting using GJF 1.7 Use Java 21 for the Gerrit image Fix incorrect conflict resolution in Iaf0960a595 Use GerritForge fork of the nfs-server Change-Id: I0285d2c7efcb9dbd52dab88ab14649f813c0f40d
diff --git a/README.md b/README.md index c8ac4b4..ef81595 100644 --- a/README.md +++ b/README.md
@@ -5,7 +5,6 @@ Requirements for the Gerrit masters are: -- Gerrit v2.14.20 or later - Externally mounted filesystem shared among the cluster - Load-balancer (HAProxy or similar) @@ -113,7 +112,7 @@ server gerrit_http_01 gerrit-01.mycompany.com:8080 check inter 10s server gerrit_http_02 gerrit-01.mycompany.com:8080 check inter 10s backup -ackend ssh +backend gerrit_ssh_nodes mode tcp option httpchk GET /config/server/version HTTP/1.0 http-check expect status 200 @@ -192,4 +191,4 @@ ``` [auth] cookiedomain = .mycompany.com -``` \ No newline at end of file +```
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/HttpModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/HttpModule.java index a328694..921d750 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/HttpModule.java +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/HttpModule.java
@@ -33,7 +33,7 @@ @Override protected void configureServlets() { - install(new RestForwarderServletModule()); + install(new RestForwarderServletModule(config)); if (config.healthCheck().enabled()) { install(new HealthServletModule()); }
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 6040220..6a90e73 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/Module.java
@@ -22,6 +22,7 @@ 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.lock.FileBasedLockManager; import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfoModule; import com.gerritforge.gerrit.globalrefdb.validation.ProjectDeletedSharedDbCleanup; import com.google.gerrit.extensions.events.ProjectDeletedListener; @@ -46,6 +47,7 @@ protected void configure() { install(new EnvModule()); install(new ForwarderModule()); + install(new FileBasedLockManager.Module()); switch (config.main().transport()) { case HTTP:
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexModule.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexModule.java index d5e168d..6424494 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexModule.java +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexModule.java
@@ -16,9 +16,7 @@ import com.google.gerrit.extensions.events.AccountIndexedListener; import com.google.gerrit.extensions.events.ChangeIndexedListener; -import com.google.gerrit.extensions.events.GroupIndexedListener; import com.google.gerrit.extensions.events.LifecycleListener; -import com.google.gerrit.extensions.events.ProjectIndexedListener; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.inject.AbstractModule; @@ -29,7 +27,5 @@ DynamicSet.bind(binder(), LifecycleListener.class).to(AutoReindexScheduler.class); DynamicSet.bind(binder(), ChangeIndexedListener.class).to(IndexTs.class); DynamicSet.bind(binder(), AccountIndexedListener.class).to(IndexTs.class); - DynamicSet.bind(binder(), GroupIndexedListener.class).to(IndexTs.class); - DynamicSet.bind(binder(), ProjectIndexedListener.class).to(IndexTs.class); } }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java index e948ba4..e18cc0d 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AutoReindexScheduler.java
@@ -33,8 +33,6 @@ private final Configuration.AutoReindex cfg; private final ChangeReindexRunnable changeReindex; private final AccountReindexRunnable accountReindex; - private final GroupReindexRunnable groupReindex; - private final ProjectReindexRunnable projectReindex; private final ScheduledExecutorService executor; private final List<Future<?>> futureTasks = new ArrayList<>(); @@ -43,14 +41,10 @@ Configuration cfg, WorkQueue workQueue, ChangeReindexRunnable changeReindex, - AccountReindexRunnable accountReindex, - GroupReindexRunnable groupReindex, - ProjectReindexRunnable projectReindex) { + AccountReindexRunnable accountReindex) { this.cfg = cfg.autoReindex(); this.changeReindex = changeReindex; this.accountReindex = accountReindex; - this.groupReindex = groupReindex; - this.projectReindex = projectReindex; this.executor = workQueue.createQueue(1, "HighAvailability-AutoReindex"); } @@ -59,8 +53,7 @@ if (cfg.pollInterval().compareTo(Duration.ZERO) > 0) { log.atInfo().log( "Scheduling auto-reindex after %s and every %s", cfg.delay(), cfg.pollInterval()); - for (Runnable reindexTask : - List.of(changeReindex, accountReindex, groupReindex, projectReindex)) { + for (Runnable reindexTask : List.of(changeReindex, accountReindex)) { futureTasks.add( executor.scheduleAtFixedRate( reindexTask, @@ -70,8 +63,7 @@ } } else { log.atInfo().log("Scheduling auto-reindex after %s", cfg.delay()); - for (Runnable reindexTask : - List.of(changeReindex, accountReindex, groupReindex, projectReindex)) { + for (Runnable reindexTask : List.of(changeReindex, accountReindex)) { futureTasks.add(executor.schedule(reindexTask, cfg.delay().toSeconds(), TimeUnit.SECONDS)); } }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java deleted file mode 100644 index 05efe07..0000000 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java +++ /dev/null
@@ -1,124 +0,0 @@ -// Copyright (C) 2018 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.ericsson.gerrit.plugins.highavailability.autoreindex; - -import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexGroupHandler; -import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation; -import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet; -import com.google.common.collect.Streams; -import com.google.common.flogger.FluentLogger; -import com.google.gerrit.entities.AccountGroupByIdAudit; -import com.google.gerrit.entities.AccountGroupMemberAudit; -import com.google.gerrit.entities.GroupReference; -import com.google.gerrit.entities.InternalGroup; -import com.google.gerrit.server.config.AllUsersName; -import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.group.db.Groups; -import com.google.gerrit.server.util.OneOffRequestContext; -import com.google.inject.Inject; -import java.io.IOException; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Stream; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.Repository; - -public class GroupReindexRunnable extends ReindexRunnable<GroupReference> { - - private static final FluentLogger log = FluentLogger.forEnclosingClass(); - - private final Groups groups; - private final GitRepositoryManager repoManager; - private final AllUsersName allUsers; - private final ForwardedIndexGroupHandler indexer; - - @Inject - public GroupReindexRunnable( - ForwardedIndexGroupHandler indexer, - IndexTs indexTs, - OneOffRequestContext ctx, - Groups groups, - GitRepositoryManager repoManager, - AllUsersName allUsers) { - super(AbstractIndexRestApiServlet.IndexName.GROUP, indexTs, ctx); - this.groups = groups; - this.repoManager = repoManager; - this.allUsers = allUsers; - this.indexer = indexer; - } - - @Override - protected Iterable<GroupReference> fetchItems() throws Exception { - return groups.getAllGroupReferences()::iterator; - } - - @Override - protected Optional<Timestamp> indexIfNeeded(GroupReference g, Timestamp sinceTs) { - try { - Optional<InternalGroup> internalGroup = groups.getGroup(g.getUUID()); - if (internalGroup.isPresent()) { - InternalGroup group = internalGroup.get(); - Timestamp groupCreationTs = Timestamp.from(group.getCreatedOn()); - - Repository allUsersRepo = repoManager.openRepository(allUsers); - - List<AccountGroupByIdAudit> subGroupMembersAud = - groups.getSubgroupsAudit(allUsersRepo, g.getUUID()); - Stream<Timestamp> groupIdAudAddedTs = - subGroupMembersAud.stream() - .map(accountGroupByIdAudit -> Timestamp.from(accountGroupByIdAudit.addedOn())) - .filter(Objects::nonNull); - Stream<Timestamp> groupIdAudRemovedTs = - subGroupMembersAud.stream() - .map(AccountGroupByIdAudit::removedOn) - .filter(Optional<Instant>::isPresent) - .map(inst -> Timestamp.from(inst.get())); - List<AccountGroupMemberAudit> groupMembersAud = - groups.getMembersAudit(allUsersRepo, g.getUUID()); - Stream<Timestamp> groupMemberAudAddedTs = - groupMembersAud.stream() - .map(accountGroupByIdAudit -> Timestamp.from(accountGroupByIdAudit.addedOn())) - .filter(Objects::nonNull); - Stream<Timestamp> groupMemberAudRemovedTs = - groupMembersAud.stream() - .map(AccountGroupMemberAudit::removedOn) - .filter(Optional<Instant>::isPresent) - .map(inst -> Timestamp.from(inst.get())); - - Optional<Timestamp> groupLastTs = - Streams.concat( - groupIdAudAddedTs, - groupIdAudRemovedTs, - groupMemberAudAddedTs, - groupMemberAudRemovedTs, - Stream.of(groupCreationTs)) - .max(Comparator.naturalOrder()); - - if (groupLastTs.isPresent() && groupLastTs.get().after(sinceTs)) { - log.atInfo().log("Index %s/%s/%s", g.getUUID(), g.getName(), groupLastTs.get()); - indexer.index(g.getUUID(), Operation.INDEX, Optional.empty()); - return groupLastTs; - } - } - } catch (IOException | ConfigInvalidException e) { - log.atSevere().withCause(e).log("Reindex failed"); - } - return Optional.empty(); - } -}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java index d05c45d..75a9681 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
@@ -21,8 +21,6 @@ import com.google.gerrit.extensions.annotations.PluginData; import com.google.gerrit.extensions.events.AccountIndexedListener; import com.google.gerrit.extensions.events.ChangeIndexedListener; -import com.google.gerrit.extensions.events.GroupIndexedListener; -import com.google.gerrit.extensions.events.ProjectIndexedListener; import com.google.gerrit.server.change.ChangeFinder; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.notedb.ChangeNotes; @@ -40,11 +38,7 @@ import org.eclipse.jgit.internal.storage.file.FileSnapshot; @Singleton -public class IndexTs - implements ChangeIndexedListener, - AccountIndexedListener, - GroupIndexedListener, - ProjectIndexedListener { +public class IndexTs implements ChangeIndexedListener, AccountIndexedListener { private static final FluentLogger log = FluentLogger.forEnclosingClass(); private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME; @@ -52,15 +46,11 @@ private final ScheduledExecutorService exec; private final FlusherRunner changeFlusher; private final FlusherRunner accountFlusher; - private final FlusherRunner groupFlusher; - private final FlusherRunner projectFlusher; private final ChangeFinder changeFinder; private final CurrentRequestContext currCtx; private volatile LocalDateTime changeTs; private volatile LocalDateTime accountTs; - private volatile LocalDateTime groupTs; - private volatile LocalDateTime projectTs; class FlusherRunner implements Runnable { private final AbstractIndexRestApiServlet.IndexName index; @@ -91,12 +81,8 @@ switch (index) { case CHANGE: return changeTs; - case GROUP: - return groupTs; case ACCOUNT: return accountTs; - case PROJECT: - return projectTs; default: throw new IllegalArgumentException("Unsupported index " + index); } @@ -113,15 +99,9 @@ case CHANGE: changeTs = newTs; break; - case GROUP: - groupTs = newTs; - break; case ACCOUNT: accountTs = newTs; break; - case PROJECT: - projectTs = newTs; - break; } tsPathSnapshot = FileSnapshot.save(tsPath.toFile()); @@ -138,23 +118,11 @@ this.exec = queue.getDefaultQueue(); this.changeFlusher = new FlusherRunner(AbstractIndexRestApiServlet.IndexName.CHANGE); this.accountFlusher = new FlusherRunner(AbstractIndexRestApiServlet.IndexName.ACCOUNT); - this.groupFlusher = new FlusherRunner(AbstractIndexRestApiServlet.IndexName.GROUP); - this.projectFlusher = new FlusherRunner(AbstractIndexRestApiServlet.IndexName.PROJECT); this.changeFinder = changeFinder; this.currCtx = currCtx; } @Override - public void onProjectIndexed(String project) { - currCtx.onlyWithContext((ctx) -> update(IndexName.PROJECT, LocalDateTime.now())); - } - - @Override - public void onGroupIndexed(String uuid) { - currCtx.onlyWithContext((ctx) -> update(IndexName.GROUP, LocalDateTime.now())); - } - - @Override public void onAccountIndexed(int id) { currCtx.onlyWithContext((ctx) -> update(IndexName.ACCOUNT, LocalDateTime.now())); } @@ -205,14 +173,6 @@ accountTs = dateTime; exec.execute(accountFlusher); break; - case GROUP: - groupTs = dateTime; - exec.execute(groupFlusher); - break; - case PROJECT: - projectTs = dateTime; - exec.execute(projectFlusher); - break; default: throw new IllegalArgumentException("Unsupported index " + index); }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java deleted file mode 100644 index 00c574d..0000000 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ProjectReindexRunnable.java +++ /dev/null
@@ -1,45 +0,0 @@ -// Copyright (C) 2018 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.ericsson.gerrit.plugins.highavailability.autoreindex; - -import com.ericsson.gerrit.plugins.highavailability.forwarder.rest.AbstractIndexRestApiServlet; -import com.google.gerrit.entities.Project; -import com.google.gerrit.server.project.ProjectCache; -import com.google.gerrit.server.util.OneOffRequestContext; -import com.google.inject.Inject; -import java.sql.Timestamp; -import java.util.Optional; - -public class ProjectReindexRunnable extends ReindexRunnable<Project.NameKey> { - - private final ProjectCache projectCache; - - @Inject - public ProjectReindexRunnable( - IndexTs indexTs, OneOffRequestContext ctx, ProjectCache projectCache) { - super(AbstractIndexRestApiServlet.IndexName.PROJECT, indexTs, ctx); - this.projectCache = projectCache; - } - - @Override - protected Iterable<Project.NameKey> fetchItems() { - return projectCache.all(); - } - - @Override - protected Optional<Timestamp> indexIfNeeded(Project.NameKey g, Timestamp sinceTs) { - return Optional.empty(); - } -}
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 ffe3afc..c58d8b2 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
@@ -14,20 +14,34 @@ package com.ericsson.gerrit.plugins.highavailability.forwarder.rest; +import com.ericsson.gerrit.plugins.highavailability.Configuration; import com.google.inject.servlet.ServletModule; public class RestForwarderServletModule extends ServletModule { + private final Configuration config; + + public RestForwarderServletModule(Configuration config) { + this.config = config; + } + @Override protected void configureServlets() { - serveRegex("/index/account/\\d+$").with(IndexAccountRestApiServlet.class); - serveRegex("/index/change/batch/.*$").with(IndexBatchChangeRestApiServlet.class); - serveRegex("/index/change/.*$").with(IndexChangeRestApiServlet.class); - serveRegex("/index/group/\\w+$").with(IndexGroupRestApiServlet.class); - serveRegex("/index/project/.*$").with(IndexProjectRestApiServlet.class); - 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); + if (config.index().synchronize()) { + serveRegex("/index/account/\\d+$").with(IndexAccountRestApiServlet.class); + serveRegex("/index/change/batch/.*$").with(IndexBatchChangeRestApiServlet.class); + serveRegex("/index/change/.*$").with(IndexChangeRestApiServlet.class); + serveRegex("/index/group/\\w+$").with(IndexGroupRestApiServlet.class); + serveRegex("/index/project/.*$").with(IndexProjectRestApiServlet.class); + if (config.indexSync().enabled()) { + serve("/query/changes.updated.since/*").with(QueryChangesUpdatedSinceServlet.class); + } + } + if (config.event().synchronize()) { + serve("/event/*").with(EventRestApiServlet.class); + } + if (config.cache().synchronize()) { + serve("/cache/project_list/*").with(ProjectListApiServlet.class); + serve("/cache/*").with(CacheRestApiServlet.class); + } } }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeChecker.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeChecker.java index ef7f942..0ed6160 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeChecker.java +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeChecker.java
@@ -23,7 +23,7 @@ public interface ChangeChecker { /** - * Return the Change notes read from ReviewDb or NoteDb. + * Return the Change notes read from NoteDb. * * @return notes of the Change */
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 index 92834bd..69bc2e4 100644 --- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncRunner.java +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/indexsync/IndexSyncRunner.java
@@ -47,6 +47,7 @@ import dev.failsafe.function.CheckedSupplier; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -106,46 +107,51 @@ 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 + boolean failed = false; + Set<String> updatedChanges = new HashSet<>(); for (PeerInfo peer : peers) { - if (syncFrom(peer, indexer)) { - log.atFine().log("Finished indexSync"); - return true; + try { + updatedChanges.addAll(collectUpdatedChanges(peer)); + } catch (IOException e) { + log.atSevere().withCause(e).log("Error while querying changes from %s", peer); + failed = true; } } - return false; + if (syncIndex(updatedChanges)) { + log.atFine().log("Finished indexSync"); + } else { + log.atSevere().log("Failed to index out of sync changes"); + failed = true; + } + + return !failed; } - private boolean syncFrom(PeerInfo peer, ChangeIndexer indexer) { - log.atFine().log("Syncing index with %s", peer.getDirectUrl()); + private List<String> collectUpdatedChanges(PeerInfo peer) throws IOException { + log.atFine().log("Collecting out of sync changes from %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; - } + log.atFine().log("Executing %s", queryRequest); + return httpClient.execute(queryRequest, queryChangesResponseHandler); + } + private boolean syncIndex(Set<String> updatedChanges) { + ChangeIndexer indexer = changeIndexerFactory.create(executor, changeIndexes, false); try { - List<ListenableFuture<Boolean>> indexingTasks = new ArrayList<>(ids.size()); - for (String id : ids) { + List<ListenableFuture<Boolean>> indexingTasks = new ArrayList<>(updatedChanges.size()); + for (String id : updatedChanges) { indexingTasks.add(indexAsync(id, indexer)); } Futures.allAsList(indexingTasks).get(); } catch (InterruptedException | ExecutionException e) { - log.atSevere().withCause(e).log("Error while reindexing %s", ids); + log.atSevere().withCause(e).log("Error while reindexing %s", updatedChanges); return false; } - syncChangeDeletions(ids, indexer); + syncChangeDeletions(updatedChanges, indexer); return true; } @@ -165,7 +171,7 @@ return indexer.asyncReindexIfStale(projectName, Change.id(changeNumber)); } - private void syncChangeDeletions(List<String> theirChanges, ChangeIndexer indexer) { + private void syncChangeDeletions(Set<String> theirChanges, ChangeIndexer indexer) { Set<String> ourChanges = queryLocalIndex(); for (String d : Sets.difference(ourChanges, ImmutableSet.copyOf(theirChanges))) { deleteIfMissingInNoteDb(d, indexer);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLock.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLock.java new file mode 100644 index 0000000..c9af291 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLock.java
@@ -0,0 +1,135 @@ +// 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.lock; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.flogger.FluentLogger; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.LockToken; + +public class FileBasedLock implements Lock { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public interface Factory { + FileBasedLock create(String name); + } + + private final TouchFileService touchFileService; + private final String content; + private final Path lockPath; + + private volatile ScheduledFuture<?> touchTask; + private volatile LockToken lockToken; + + @AssistedInject + public FileBasedLock( + @LocksDirectory Path locksDir, TouchFileService touchFileService, @Assisted String name) { + this.touchFileService = touchFileService; + LockFileFormat format = new LockFileFormat(name); + this.content = format.content(); + this.lockPath = locksDir.resolve(format.fileName()); + } + + @Override + public void lock() { + try { + tryLock(Long.MAX_VALUE, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.atSevere().withCause(e).log("Interrupted while trying to lock: %s", lockPath); + throw new RuntimeException(e); + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock() { + try { + createLockFile(); + touchTask = touchFileService.touchForever(lockPath.toFile()); + return true; + } catch (IOException e) { + logger.atInfo().withCause(e).log("Couldn't create lock file: %s", lockPath); + return false; + } + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + RetryPolicy<Object> retry = + RetryPolicy.builder() + .withMaxAttempts(-1) + .withBackoff(10, 1000, ChronoUnit.MILLIS) + .withMaxDuration(Duration.of(time, unit.toChronoUnit())) + .handleResult(false) + .build(); + return Failsafe.with(retry).get(this::tryLock); + } + + @VisibleForTesting + Path getLockPath() { + return lockPath; + } + + @Override + public void unlock() { + try { + if (touchTask != null) { + touchTask.cancel(false); + } + Files.deleteIfExists(lockPath); + if (lockToken != null) { + lockToken.close(); + } + } catch (IOException e) { + logger.atSevere().withCause(e).log("Couldn't delete lock file: %s", lockPath); + throw new RuntimeException(e); + } + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } + + private Path createLockFile() throws IOException { + File f = lockPath.toFile(); + lockToken = FS.DETECTED.createNewFileAtomic(f); + if (!lockToken.isCreated()) { + throw new IOException("Couldn't create " + lockPath); + } + Files.write(lockPath, content.getBytes(StandardCharsets.UTF_8)); + f.deleteOnExit(); + return lockPath; + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLockManager.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLockManager.java new file mode 100644 index 0000000..dfadab0 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLockManager.java
@@ -0,0 +1,110 @@ +// 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.lock; + +import com.ericsson.gerrit.plugins.highavailability.SharedDirectory; +import com.ericsson.gerrit.plugins.highavailability.lock.FileBasedLock.Factory; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.extensions.config.FactoryModule; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.project.LockManager; +import com.google.inject.Inject; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.locks.Lock; + +public class FileBasedLockManager implements LockManager { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static class Module extends LifecycleModule { + @Override + protected void configure() { + DynamicItem.bind(binder(), LockManager.class).to(FileBasedLockManager.class); + bind(TouchFileService.class).to(TouchFileServiceImpl.class); + install( + new FactoryModule() { + @Override + protected void configure() { + factory(FileBasedLock.Factory.class); + } + }); + listener().to(StaleLockRemoval.class); + } + + @Provides + @Singleton + @TouchFileExecutor + ScheduledExecutorService createTouchFileExecutor(WorkQueue workQueue) { + return workQueue.createQueue(2, "TouchFileService"); + } + + @Provides + @Singleton + @StaleLockRemovalExecutor + ScheduledExecutorService createStaleLockRemovalExecutor(WorkQueue workQueue) { + return workQueue.createQueue(1, "StaleLockRemoval"); + } + + @Provides + @Singleton + @LocksDirectory + Path getLocksDirectory(@SharedDirectory Path sharedDir) throws IOException { + Path locksDirPath = sharedDir.resolve("locks"); + Files.createDirectories(locksDirPath); + return locksDirPath; + } + + @Provides + @Singleton + @TouchFileInterval + Duration getTouchFileInterval() { + return Duration.ofSeconds(1); + } + + @Provides + @Singleton + @StalenessCheckInterval + Duration getStalenessCheckInterval() { + return Duration.ofSeconds(2); + } + + @Provides + @Singleton + @StalenessAge + Duration getStalenessAge() { + return Duration.ofSeconds(60); + } + } + + private final Factory lockFactory; + + @Inject + FileBasedLockManager(FileBasedLock.Factory lockFactory) { + this.lockFactory = lockFactory; + } + + @Override + public Lock getLock(String name) { + logger.atInfo().log("FileBasedLockManager.lock(%s)", name); + return lockFactory.create(name); + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/LockFileFormat.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/LockFileFormat.java new file mode 100644 index 0000000..9eefd91 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/LockFileFormat.java
@@ -0,0 +1,41 @@ +// 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.lock; + +import com.google.common.base.CharMatcher; +import com.google.common.hash.Hashing; +import java.nio.charset.StandardCharsets; + +public class LockFileFormat { + private static final CharMatcher HEX_DIGIT_MATCHER = CharMatcher.anyOf("0123456789abcdef"); + + private final String lockName; + + public static boolean isLockFileName(String fileName) { + return fileName.length() == 40 && HEX_DIGIT_MATCHER.matchesAllOf(fileName); + } + + public LockFileFormat(String lockName) { + this.lockName = lockName; + } + + public String fileName() { + return Hashing.sha1().hashString(content(), StandardCharsets.UTF_8).toString(); + } + + public String content() { + return lockName + "\n"; + } +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/LocksDirectory.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/LocksDirectory.java new file mode 100644 index 0000000..405ec5e --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/LocksDirectory.java
@@ -0,0 +1,24 @@ +// 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.lock; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface LocksDirectory {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemoval.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemoval.java new file mode 100644 index 0000000..d0efeb2 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemoval.java
@@ -0,0 +1,96 @@ +// 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.lock; + +import static com.ericsson.gerrit.plugins.highavailability.lock.LockFileFormat.isLockFileName; + +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +public class StaleLockRemoval implements LifecycleListener, Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final ScheduledExecutorService executor; + private final Duration checkInterval; + private final Duration stalenessAge; + private final Path locksDir; + + @Inject + StaleLockRemoval( + @StaleLockRemovalExecutor ScheduledExecutorService executor, + @StalenessCheckInterval Duration checkInterval, + @StalenessAge Duration stalenessAge, + @LocksDirectory Path locksDir) { + this.executor = executor; + this.checkInterval = checkInterval; + this.stalenessAge = stalenessAge; + this.locksDir = locksDir; + } + + @Override + public void start() { + logger.atFine().log( + "Scheduling StaleLockRemoval to run every %d seconds", checkInterval.getSeconds()); + @SuppressWarnings("unused") + Future<?> possiblyIgnoredError = + executor.scheduleWithFixedDelay( + this, checkInterval.getSeconds(), checkInterval.getSeconds(), TimeUnit.SECONDS); + logger.atFine().log( + "Scheduled StaleLockRemoval to run every %d seconds", checkInterval.getSeconds()); + } + + @Override + public void run() { + try (Stream<Path> stream = Files.walk(locksDir)) { + stream + .filter(Files::isRegularFile) + .filter(p -> isLockFileName(p.getFileName().toString())) + .forEach(this::removeIfStale); + } catch (IOException e) { + logger.atSevere().withCause(e).log("Error while performing stale lock detection and removal"); + } + } + + private void removeIfStale(Path lockPath) { + logger.atFine().log("Inspecting %s", lockPath); + Instant now = Instant.now(); + Instant lastModified = Instant.ofEpochMilli(lockPath.toFile().lastModified()); + if (Duration.between(lastModified, now).compareTo(stalenessAge) > 0) { + logger.atInfo().log("Detected stale lock %s", lockPath); + try { + if (Files.deleteIfExists(lockPath)) { + logger.atInfo().log("Stale lock %s removed", lockPath); + } else { + logger.atInfo().log("Stale lock %s was removed by another thread", lockPath); + } + } catch (IOException e) { + logger.atSevere().withCause(e).log("Couldn't delete stale lock %s", lockPath); + } + } + } + + @Override + public void stop() {} +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemovalExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemovalExecutor.java new file mode 100644 index 0000000..eba21e8 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemovalExecutor.java
@@ -0,0 +1,24 @@ +// 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.lock; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface StaleLockRemovalExecutor {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StalenessAge.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StalenessAge.java new file mode 100644 index 0000000..900551b --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StalenessAge.java
@@ -0,0 +1,24 @@ +// 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.lock; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface StalenessAge {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StalenessCheckInterval.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StalenessCheckInterval.java new file mode 100644 index 0000000..0c5a67f --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/StalenessCheckInterval.java
@@ -0,0 +1,24 @@ +// 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.lock; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface StalenessCheckInterval {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileExecutor.java new file mode 100644 index 0000000..70a6939 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileExecutor.java
@@ -0,0 +1,24 @@ +// 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.lock; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface TouchFileExecutor {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileInterval.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileInterval.java new file mode 100644 index 0000000..08b1aaa --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileInterval.java
@@ -0,0 +1,24 @@ +// 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.lock; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface TouchFileInterval {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileService.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileService.java new file mode 100644 index 0000000..9cf5b33 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileService.java
@@ -0,0 +1,22 @@ +// 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.lock; + +import java.io.File; +import java.util.concurrent.ScheduledFuture; + +public interface TouchFileService { + ScheduledFuture<?> touchForever(File file); +}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileServiceImpl.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileServiceImpl.java new file mode 100644 index 0000000..3093de9 --- /dev/null +++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileServiceImpl.java
@@ -0,0 +1,53 @@ +// 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.lock; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.File; +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Singleton +public class TouchFileServiceImpl implements TouchFileService { + + private final ScheduledExecutorService executor; + private final Duration interval; + + @Inject + public TouchFileServiceImpl( + @TouchFileExecutor ScheduledExecutorService executor, @TouchFileInterval Duration interval) { + this.executor = executor; + this.interval = interval; + } + + @Override + public ScheduledFuture<?> touchForever(File file) { + return executor.scheduleAtFixedRate( + () -> touch(file), interval.getSeconds(), interval.getSeconds(), TimeUnit.SECONDS); + } + + private static void touch(File f) { + boolean succeeded = f.setLastModified(System.currentTimeMillis()); + if (!succeeded) { + if (!f.exists()) { + throw new RuntimeException( + String.format("File %s doesn't exist, stopping the touch task", f.toPath())); + } + } + } +}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md index 80117e2..6fcd2e2 100644 --- a/src/main/resources/Documentation/about.md +++ b/src/main/resources/Documentation/about.md
@@ -180,7 +180,6 @@ sync with the primary, for example, after a node being offline for a long time. The HA plugin keeps the last update timestamp for each index in the following files: -* `<gerrit_home>/data/high-availability/group` * `<gerrit_home>/data/high-availability/account` * `<gerrit_home>/data/high-availability/change`
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 67d7f6c..b089091 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -88,26 +88,44 @@ ```autoReindex.enabled``` : Enable the tracking of the latest change indexed under data/high-availability - for each of the indexes. At startup scans all the changes, accounts and groups - and reindex the ones that have been updated by other nodes while the server was down. + for each of the indexes. At startup scans all the changes and accounts and reindex + the ones that have been updated by other nodes while the server was down. When not specified, the default is "false", that means no automatic tracking and indexing at start. ```autoReindex.delay``` : When autoReindex is enabled, indicates the delay aftere the plugin startup, - before triggering the conditional reindexing of all changes, accounts and groups. + before triggering the conditional reindexing of all changes and accounts. Delay is expressed in Gerrit time values as in [websession.cleanupInterval](#websessioncleanupInterval). When not specified, the default is "10 seconds". ```autoReindex.pollInterval``` : When autoReindex is enabled, indicates the interval between the conditional - reindexing of all changes, accounts and groups. + reindexing of all changes and accounts. 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. +**Note:** For projects and groups reindexing, [scheduled indexer](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#scheduledIndexer) can be enabled with specific configurations. +Example configurations: + +``` +[scheduledIndexer "groups"] + enabled = true + interval = 1h + startTime = 13:00 + +[scheduledIndexer "projects"] + enabled = true + interval = 1h + startTime = 13:00 +``` +Note: Ensure these settings are added to enable periodic reindexing of groups and projects. +Groups and projects may become outdated if indexing events are missed due to the node being down or +some networking issues. + ```indexSync.enabled``` : When indexSync is enabled, the primary servers will synchronize indexes with the intention to self-heal any missed reindexing event.
diff --git a/src/test/docker/gerrit/Dockerfile b/src/test/docker/gerrit/Dockerfile index f082d2f..f5f7bf5 100644 --- a/src/test/docker/gerrit/Dockerfile +++ b/src/test/docker/gerrit/Dockerfile
@@ -11,7 +11,7 @@ nfs-utils \ && yum -y clean all -ENV GERRIT_BRANCH stable-3.11 +ENV GERRIT_VERSION master # Add gerrit user RUN adduser -p -m --uid 1000 gerrit --home-dir /home/gerrit
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnableTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnableTest.java deleted file mode 100644 index 0513d42..0000000 --- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnableTest.java +++ /dev/null
@@ -1,209 +0,0 @@ -// Copyright (C) 2021 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.autoreindex; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexGroupHandler; -import com.ericsson.gerrit.plugins.highavailability.forwarder.ForwardedIndexingHandler.Operation; -import com.google.common.collect.ImmutableSet; -import com.google.gerrit.entities.Account; -import com.google.gerrit.entities.Account.Id; -import com.google.gerrit.entities.AccountGroup; -import com.google.gerrit.entities.AccountGroup.UUID; -import com.google.gerrit.entities.AccountGroupByIdAudit; -import com.google.gerrit.entities.AccountGroupMemberAudit; -import com.google.gerrit.entities.GroupReference; -import com.google.gerrit.entities.InternalGroup; -import com.google.gerrit.server.config.AllUsersName; -import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.group.db.Groups; -import com.google.gerrit.server.util.OneOffRequestContext; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Optional; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class GroupReindexRunnableTest { - - @Mock private ForwardedIndexGroupHandler indexer; - @Mock private IndexTs indexTs; - @Mock private OneOffRequestContext ctx; - @Mock private Groups groups; - @Mock private GitRepositoryManager repoManager; - @Mock private AllUsersName allUsers; - private GroupReference groupReference; - - private GroupReindexRunnable groupReindexRunnable; - private static UUID uuid; - - @Before - public void setUp() throws Exception { - groupReindexRunnable = - new GroupReindexRunnable(indexer, indexTs, ctx, groups, repoManager, allUsers); - uuid = UUID.parse("123"); - groupReference = GroupReference.create(uuid, "123"); - } - - @Test - public void groupIsIndexedWhenItIsCreatedAfterLastGroupReindex() throws Exception { - Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now()); - Timestamp afterCurrentTime = new Timestamp(currentTime.getTime() + 1000L); - when(groups.getGroup(uuid)).thenReturn(getInternalGroup(afterCurrentTime)); - - Optional<Timestamp> groupLastTs = - groupReindexRunnable.indexIfNeeded(groupReference, currentTime); - assertThat(groupLastTs.isPresent()).isTrue(); - assertThat(groupLastTs.get()).isEqualTo(afterCurrentTime); - verify(indexer).index(uuid, Operation.INDEX, Optional.empty()); - } - - @Test - public void groupIsNotIndexedWhenItIsCreatedBeforeLastGroupReindex() throws Exception { - Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now()); - Timestamp beforeCurrentTime = new Timestamp(currentTime.getTime() - 1000L); - when(groups.getGroup(uuid)).thenReturn(getInternalGroup(beforeCurrentTime)); - - Optional<Timestamp> groupLastTs = - groupReindexRunnable.indexIfNeeded(groupReference, currentTime); - assertThat(groupLastTs.isPresent()).isFalse(); - verify(indexer, never()).index(uuid, Operation.INDEX, Optional.empty()); - } - - @Test - public void groupIsNotIndexedGroupReferenceNotPresent() { - Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now()); - Optional<Timestamp> groupLastTs = - groupReindexRunnable.indexIfNeeded(groupReference, currentTime); - assertThat(groupLastTs.isPresent()).isFalse(); - } - - @Test - public void groupIsIndexedWhenNewUserAddedAfterLastGroupReindex() throws Exception { - Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now()); - Timestamp afterCurrentTime = new Timestamp(currentTime.getTime() + 1000L); - when(groups.getGroup(uuid)).thenReturn(getInternalGroup(currentTime)); - when(groups.getMembersAudit(any(), any())) - .thenReturn( - Collections.singletonList( - AccountGroupMemberAudit.builder() - .addedBy(Account.Id.tryParse("1").get()) - .addedOn(afterCurrentTime.toInstant()) - .memberId(Account.Id.tryParse("1").get()) - .groupId(AccountGroup.Id.parse("1")) - .build())); - Optional<Timestamp> groupLastTs = - groupReindexRunnable.indexIfNeeded(groupReference, currentTime); - assertThat(groupLastTs.isPresent()).isTrue(); - assertThat(groupLastTs.get()).isEqualTo(afterCurrentTime); - verify(indexer).index(uuid, Operation.INDEX, Optional.empty()); - } - - @Test - public void groupIsIndexedWhenUserRemovedAfterLastGroupReindex() throws Exception { - Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now()); - Timestamp afterCurrentTime = new Timestamp(currentTime.getTime() + 1000L); - Timestamp beforeCurrentTime = new Timestamp(currentTime.getTime() - 1000L); - - AccountGroupMemberAudit accountGroupMemberAudit = - AccountGroupMemberAudit.builder() - .addedBy(Account.Id.tryParse("1").get()) - .addedOn(beforeCurrentTime.toInstant()) - .memberId(Account.Id.tryParse("1").get()) - .groupId(AccountGroup.Id.parse("2")) - .removed(Account.Id.tryParse("2").get(), afterCurrentTime.toInstant()) - .build(); - when(groups.getGroup(uuid)).thenReturn(getInternalGroup(currentTime)); - when(groups.getMembersAudit(any(), any())) - .thenReturn(Collections.singletonList(accountGroupMemberAudit)); - Optional<Timestamp> groupLastTs = - groupReindexRunnable.indexIfNeeded(groupReference, currentTime); - assertThat(groupLastTs.isPresent()).isTrue(); - assertThat(groupLastTs.get()).isEqualTo(afterCurrentTime); - verify(indexer).index(uuid, Operation.INDEX, Optional.empty()); - } - - @Test - public void groupIsIndexedWhenItIsSubGroupAddedAfterLastGroupReindex() throws Exception { - Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now()); - Timestamp afterCurrentTime = new Timestamp(currentTime.getTime() + 1000L); - Timestamp beforeCurrentTime = new Timestamp(currentTime.getTime() - 1000L); - when(groups.getGroup(uuid)).thenReturn(getInternalGroup(beforeCurrentTime)); - when(groups.getSubgroupsAudit(any(), any())) - .thenReturn( - Collections.singletonList( - AccountGroupByIdAudit.builder() - .groupId(AccountGroup.Id.parse("1")) - .includeUuid(UUID.parse("123")) - .addedBy(Account.Id.tryParse("1").get()) - .addedOn(afterCurrentTime.toInstant()) - .build())); - Optional<Timestamp> groupLastTs = - groupReindexRunnable.indexIfNeeded(groupReference, currentTime); - assertThat(groupLastTs.isPresent()).isTrue(); - assertThat(groupLastTs.get()).isEqualTo(afterCurrentTime); - verify(indexer).index(uuid, Operation.INDEX, Optional.empty()); - } - - @Test - public void groupIsIndexedWhenItIsSubGroupRemovedAfterLastGroupReindex() throws Exception { - Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now()); - Timestamp afterCurrentTime = new Timestamp(currentTime.getTime() + 1000L); - Timestamp beforeCurrentTime = new Timestamp(currentTime.getTime() - 1000L); - - AccountGroupByIdAudit accountGroupByIdAud = - AccountGroupByIdAudit.builder() - .groupId(AccountGroup.Id.parse("1")) - .includeUuid(UUID.parse("123")) - .addedBy(Account.Id.tryParse("1").get()) - .addedOn(beforeCurrentTime.toInstant()) - .removed(Account.Id.tryParse("2").get(), afterCurrentTime.toInstant()) - .build(); - - when(groups.getGroup(uuid)).thenReturn(getInternalGroup(beforeCurrentTime)); - when(groups.getSubgroupsAudit(any(), any())) - .thenReturn(Collections.singletonList(accountGroupByIdAud)); - - Optional<Timestamp> groupLastTs = - groupReindexRunnable.indexIfNeeded(groupReference, currentTime); - assertThat(groupLastTs.isPresent()).isTrue(); - assertThat(groupLastTs.get()).isEqualTo(afterCurrentTime); - verify(indexer).index(uuid, Operation.INDEX, Optional.empty()); - } - - private Optional<InternalGroup> getInternalGroup(Timestamp timestamp) { - return Optional.ofNullable( - InternalGroup.builder() - .setId(AccountGroup.Id.parse("1")) - .setNameKey(AccountGroup.nameKey("Test")) - .setOwnerGroupUUID(uuid) - .setVisibleToAll(true) - .setGroupUUID(UUID.parse("12")) - .setCreatedOn(timestamp.toInstant()) - .setMembers(ImmutableSet.<Id>builder().build()) - .setSubgroups(ImmutableSet.<UUID>builder().build()) - .build()); - } -}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java index 83f86d0..fe6cfce 100644 --- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java +++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/AbstractIndexForwardingIT.java
@@ -49,10 +49,8 @@ sysModule = "com.ericsson.gerrit.plugins.highavailability.Module", httpModule = "com.ericsson.gerrit.plugins.highavailability.HttpModule") public abstract class AbstractIndexForwardingIT extends LightweightPluginDaemonTest { - private static final int PORT = 18889; - private static final String URL = "http://localhost:" + PORT; - @Rule public WireMockRule wireMockRule = new WireMockRule(options().port(PORT)); + @Rule public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort()); @Inject SitePaths sitePaths; @@ -62,7 +60,7 @@ FileBasedConfig fileBasedConfig = new FileBasedConfig( sitePaths.etc_dir.resolve(Configuration.PLUGIN_CONFIG_FILE).toFile(), FS.DETECTED); - fileBasedConfig.setString("peerInfo", "static", "url", URL); + fileBasedConfig.setString("peerInfo", "static", "url", url()); fileBasedConfig.setInt("http", null, "retryInterval", 100); fileBasedConfig.save(); beforeAction(); @@ -88,6 +86,10 @@ verify(postRequestedFor(urlEqualTo(expectedRequest))); } + private String url() { + return "http://localhost:" + wireMockRule.port(); + } + /** Perform pre-test setup. */ protected abstract void beforeAction() throws Exception;
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLockTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLockTest.java new file mode 100644 index 0000000..cf9dd28 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/FileBasedLockTest.java
@@ -0,0 +1,233 @@ +// 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.lock; + +import static com.google.common.truth.Truth.assertThat; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_SUPPORTSATOMICFILECREATION; + +import com.google.gerrit.server.util.git.DelegateSystemReader; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.SystemReader; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class FileBasedLockTest { + + private static SystemReader setFakeSystemReader(FileBasedConfig cfg) { + SystemReader oldSystemReader = SystemReader.getInstance(); + SystemReader.setInstance( + new DelegateSystemReader(oldSystemReader) { + @Override + public FileBasedConfig openUserConfig(Config parent, FS fs) { + return cfg; + } + }); + return oldSystemReader; + } + + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + private final Duration touchInterval = Duration.ofSeconds(1); + private final String lockName = "mylock"; + + private ScheduledExecutorService touchFileExecutor; + private TouchFileService touchFileService; + private FileBasedLock lock; + + private FileBasedConfig cfg; + + @Parameter public boolean supportsAtomicFileCreation; + + @Parameters(name = "supportsAtomicFileCreation={0}") + public static Boolean[] testData() { + return new Boolean[] {true, false}; + } + + SystemReader oldSystemReader; + + @Before + public void setUp() throws IOException { + touchFileExecutor = Executors.newScheduledThreadPool(2); + touchFileService = new TouchFileServiceImpl(touchFileExecutor, touchInterval); + Path locksDir = Path.of(tempFolder.newFolder().getPath()); + lock = new FileBasedLock(locksDir, touchFileService, lockName); + + File cfgFile = tempFolder.newFile(".gitconfig"); + cfg = new FileBasedConfig(cfgFile, FS.DETECTED); + cfg.setBoolean( + CONFIG_CORE_SECTION, + null, + CONFIG_KEY_SUPPORTSATOMICFILECREATION, + supportsAtomicFileCreation); + cfg.save(); + oldSystemReader = setFakeSystemReader(cfg); + } + + @After + public void tearDown() throws Exception { + touchFileExecutor.shutdown(); + touchFileExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + SystemReader.setInstance(oldSystemReader); + } + + @Test + public void lockCreatesFile_unlockDeletesFile() { + Path lockPath = lock.getLockPath(); + + assertThat(Files.exists(lockPath)).isFalse(); + + lock.lock(); + assertThat(Files.exists(lockPath)).isTrue(); + + lock.unlock(); + assertThat(Files.exists(lockPath)).isFalse(); + } + + @Test + public void testLockFileNameAndContent() throws IOException { + lock.lock(); + Path lockPath = lock.getLockPath(); + + String content = Files.readString(lockPath, StandardCharsets.UTF_8); + assertThat(content).endsWith("\n"); + LockFileFormat lockFileFormat = new LockFileFormat(content.substring(0, content.length() - 1)); + assertThat(content).isEqualTo(lockFileFormat.content()); + assertThat(lockPath.getFileName().toString()).isEqualTo(lockFileFormat.fileName()); + } + + @Test + public void tryLockAfterLock_fail() { + lock.lock(); + assertThat(lock.tryLock()).isFalse(); + } + + @Test + public void tryLockWithTimeout_failsAfterTimeout() throws InterruptedException { + lock.lock(); + assertThat(lock.tryLock(1, TimeUnit.SECONDS)).isFalse(); + } + + @Test + public void concurrentTryLock_exactlyOneSucceeds() + throws InterruptedException, ExecutionException { + ExecutorService executor = Executors.newFixedThreadPool(2); + + for (int i = 0; i < 10; i++) { + Future<Boolean> r1 = executor.submit(() -> lock.tryLock()); + Future<Boolean> r2 = executor.submit(() -> lock.tryLock()); + assertThat(r1.get()).isNotEqualTo(r2.get()); + lock.unlock(); + } + + executor.shutdown(); + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + } + + @Test + public void tryLockWithTimeout_succeedsIfLockReleased() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(1); + + lock.lock(); + + Duration timeout = Duration.ofSeconds(1); + Future<Boolean> r = + executor.submit(() -> lock.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS)); + + Thread.sleep(timeout.dividedBy(2).toMillis()); + + lock.unlock(); + + assertThat(r.get()).isTrue(); + + executor.shutdown(); + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + } + + @Test + public void tryLockWithTimeout_failsIfLockNotReleased() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(1); + + lock.lock(); + Future<Boolean> r = executor.submit(() -> lock.tryLock(1, TimeUnit.SECONDS)); + + assertThat(r.get()).isFalse(); + + executor.shutdown(); + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + } + + @Test + public void liveLock_lastUpdatedKeepsIncreasing() throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(1); + CountDownLatch lockAcquired = new CountDownLatch(1); + CountDownLatch testDone = new CountDownLatch(1); + + executor.submit(() -> acquireAndReleaseLock(lockAcquired, testDone)); + lockAcquired.await(); + + File lockFile = lock.getLockPath().toFile(); + long start = lockFile.lastModified(); + long previous = start; + long last = start; + for (int i = 0; i < 3; i++) { + Thread.sleep(touchInterval.toMillis()); + long current = lockFile.lastModified(); + assertThat(current).isAtLeast(previous); + last = current; + } + assertThat(last).isGreaterThan(start); + + testDone.countDown(); + + executor.shutdown(); + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + } + + private void acquireAndReleaseLock(CountDownLatch lockAcquired, CountDownLatch testDone) { + lock.lock(); + lockAcquired.countDown(); + try { + testDone.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemovalTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemovalTest.java new file mode 100644 index 0000000..957777c --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/StaleLockRemovalTest.java
@@ -0,0 +1,105 @@ +// 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.lock; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; + +public class StaleLockRemovalTest { + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + private ScheduledExecutorService executor; + private StaleLockRemoval staleLockRemoval; + private Path locksDir; + private Duration stalenessAge; + + @Before + public void setUp() throws IOException { + executor = Mockito.mock(ScheduledExecutorService.class); + executor = new ScheduledThreadPoolExecutor(2); + locksDir = tempFolder.newFolder().toPath(); + stalenessAge = Duration.ofSeconds(3); + staleLockRemoval = + new StaleLockRemoval(executor, Duration.ofSeconds(1), stalenessAge, locksDir); + } + + @Test + public void staleLockRemoved() throws Exception { + Path lockPath = createLockFile("foo"); + Thread.sleep(stalenessAge.toMillis()); + assertFilesExist(lockPath); + staleLockRemoval.run(); + assertFilesDoNotExist(lockPath); + } + + @Test + public void nonStaleLockNotRemoved() throws Exception { + Path lockPath = createLockFile("foo"); + staleLockRemoval.run(); + assertFilesExist(lockPath); + } + + @Test + public void nonLockFilesNotRemoved() throws Exception { + Path nonLock = Files.createFile(locksDir.resolve("nonLock")); + Thread.sleep(stalenessAge.toMillis()); + staleLockRemoval.run(); + assertFilesExist(nonLock); + } + + @Test + public void multipleLocksHandledProperly() throws Exception { + Path stale1 = createLockFile("stale-1"); + Path stale2 = createLockFile("stale-2"); + Path stale3 = createLockFile("stale-3"); + + Thread.sleep(stalenessAge.toMillis()); + + Path live1 = createLockFile("live-1"); + Path live2 = createLockFile("live-2"); + Path live3 = createLockFile("live-3"); + + staleLockRemoval.run(); + assertFilesDoNotExist(stale1, stale2, stale3); + assertFilesExist(live1, live2, live3); + } + + private Path createLockFile(String name) throws IOException { + return Files.createFile(locksDir.resolve(new LockFileFormat(name).fileName())); + } + + private void assertFilesExist(Path... paths) { + for (Path p : paths) { + assertThat(Files.exists(p)).isTrue(); + } + } + + private void assertFilesDoNotExist(Path... paths) { + for (Path p : paths) { + assertThat(Files.exists(p)).isFalse(); + } + } +}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileServiceTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileServiceTest.java new file mode 100644 index 0000000..064eef1 --- /dev/null +++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/lock/TouchFileServiceTest.java
@@ -0,0 +1,115 @@ +// 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.lock; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class TouchFileServiceTest { + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + + private ScheduledThreadPoolExecutor executor; + private TouchFileService service; + private Path locksDir; + private Duration touchFileInterval; + + @Before + public void setUp() throws IOException { + executor = new ScheduledThreadPoolExecutor(2); + touchFileInterval = Duration.ofSeconds(1); + service = new TouchFileServiceImpl(executor, touchFileInterval); + locksDir = tempFolder.newFolder().toPath(); + } + + @After + public void tearDown() throws InterruptedException { + executor.shutdown(); + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); + } + + @Test + public void touchServiceIncreasesLastModified() throws Exception { + File f = Files.createFile(locksDir.resolve("foo")).toFile(); + service.touchForever(f); + verifyLastUpdatedIncreases(f); + } + + @Test + public void touchTaskCancelation() throws Exception { + File f = Files.createFile(locksDir.resolve("foo")).toFile(); + ScheduledFuture<?> touchTask = service.touchForever(f); + touchTask.cancel(false); + verifyLastUpdatedDoesNotIncrease(f); + } + + @Test + public void touchTaskStopsWhenFileDisappears() throws Exception { + File f = Files.createFile(locksDir.resolve("foo")).toFile(); + ScheduledFuture<?> touchTask = service.touchForever(f); + Thread.sleep(touchFileInterval.toMillis()); + + assertThat(touchTask.isDone()).isFalse(); + + assertThat(f.delete()).isTrue(); + Thread.sleep(touchFileInterval.toMillis()); + + assertThat(touchTask.isDone()).isTrue(); + try { + touchTask.get(); + Assert.fail("Expected an exception from touchTask.get()"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(RuntimeException.class); + RuntimeException cause = (RuntimeException) e.getCause(); + assertThat(cause.getMessage()).contains("stopping"); + } + } + + private void verifyLastUpdatedIncreases(File f) throws InterruptedException { + long start = f.lastModified(); + long previous = start; + long last = start; + for (int i = 0; i < 3; i++) { + Thread.sleep(1000); + long current = f.lastModified(); + assertThat(current).isAtLeast(previous); + last = current; + } + assertThat(last).isGreaterThan(start); + } + + private void verifyLastUpdatedDoesNotIncrease(File f) throws InterruptedException { + long start = f.lastModified(); + for (int i = 0; i < 3; i++) { + Thread.sleep(1000); + long current = f.lastModified(); + assertThat(current).isEqualTo(start); + } + } +}