summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdwin Kempin <ekempin@google.com>2024-02-19 16:14:03 +0000
committerEdwin Kempin <ekempin@google.com>2024-04-12 07:16:28 +0000
commit538beca650cdbd1d4df6684fea8d6fcefa5964ce (patch)
tree45c94e8766ee0b6035480b7f3f452f310c6fd0e1
parent3e6ed81a073b08b609fcfbad105a70737f73d5bf (diff)
Fix endless loop when using "is:watched" in project watches
The "is:watched" predicate matches changes that are being watched. To match changes that are being watched the "is:watched" predicate is expanded to an OR query that has one "project:<watched-project> <project-watch-filter>" predicate per project watch ("project:<watched-project>" is omitted if the project watch is the All-Projects project, "<project-watch-filter>" is omitted if the project watch doesn't set a filter). This expansion happens when the IsWatchedByPredicate is instantiated. Expanding the query requires loading the project watches of the user and parsing the filter (to convert the filter string into Predicates). If the filter of a project watch used the "is:watched" predicate, querying changes by "is:watched" or checking whether a change matches the project watch triggered an endless loop: If "is:watched" is used ChangeQueryBuilder.parse creates an IsWatchedByPredicate instance (in the is(String) method), which is expanded to the OR query (in the IsWatchedByPredicate constructor), which requires parsing the project watch filters. Parsing the project watch filters was done by ChangeQueryBuilder.parse which creates another IsWatchedByPredicate instance (in the is(String) method), which is again expanded to the OR query (in the IsWatchedByPredicate constructor), which again requires parsing the project watch filters, starting the loop anew. To fix this we: 1. Disallow using "is:watched" in ProjectWatch.WatcherChangeQueryBuilder which is a subclass of ChangeQueryBuilder.parse used to check whether a change matches a project watch. 2. Change IsWatchedByPredicate to use ProjectWatch.WatcherChangeQueryBuilder to parse the project watch filters instead of ChangeQueryBuilder. Using ProjectWatch.WatcherChangeQueryBuilder in IsWatchedByPredicate makes the matching logic for project watches when a change is updated consistent with the matching logic for project watches when "is:watched" is used in a regular change query. By disallowing "is:watched" in ProjectWatch.WatcherChangeQueryBuilder project watches that use "is:watched" in their filter do not match any change now. Before this change they triggered an endless loop, affecting the stability of the service. Note, IsWatchedByPredicate did have a check to prevent an endless loop in this case, but it didn't work since the endless loop happened before this check was reached. Bug: Issue 321784734 Release-Notes: Fix endless loop when using "is:watched" in project watches Change-Id: Ie38535b2df123a62dfd6a6e4b4ee60a80b0254f3 Signed-off-by: Edwin Kempin <ekempin@google.com>
-rw-r--r--java/com/google/gerrit/server/mail/send/ProjectWatch.java15
-rw-r--r--java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java17
-rw-r--r--javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java22
-rw-r--r--javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java17
4 files changed, 60 insertions, 11 deletions
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index f4c211d814..82dc7d020d 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -265,8 +265,8 @@ public class ProjectWatch {
return p == null || p.asMatchable().match(changeData);
}
- private static class WatcherChangeQueryBuilder extends ChangeQueryBuilder {
- private WatcherChangeQueryBuilder(Arguments args) {
+ public static class WatcherChangeQueryBuilder extends ChangeQueryBuilder {
+ public WatcherChangeQueryBuilder(Arguments args) {
super(args);
}
@@ -299,5 +299,16 @@ public class ProjectWatch {
// predicates.
return Predicate.or(predicates);
}
+
+ @Override
+ public Predicate<ChangeData> is(String value) throws QueryParseException {
+ if ("watched".equalsIgnoreCase(value)) {
+ // project watches cannot use "is:watched" as this would trigger an endless loop in
+ // IsWatchedByPredicate
+ throw new QueryParseException(
+ String.format("Operator 'is:watched' cannot be used in project watches."));
+ }
+ return super.is(value);
+ }
}
}
diff --git a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 054a69e3fd..56848a5ad3 100644
--- a/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -15,18 +15,21 @@
package com.google.gerrit.server.query.change;
import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.index.query.AndPredicate;
import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
+import com.google.gerrit.server.mail.send.ProjectWatch;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class IsWatchedByPredicate extends AndPredicate<ChangeData> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
protected static String describe(CurrentUser user) {
if (user.isIdentifiedUser()) {
return user.getAccountId().toString();
@@ -44,20 +47,16 @@ public class IsWatchedByPredicate extends AndPredicate<ChangeData> {
protected static List<Predicate<ChangeData>> filters(ChangeQueryBuilder.Arguments args)
throws QueryParseException {
List<Predicate<ChangeData>> r = new ArrayList<>();
- ChangeQueryBuilder builder = new ChangeQueryBuilder(args);
+ ProjectWatch.WatcherChangeQueryBuilder builder =
+ new ProjectWatch.WatcherChangeQueryBuilder(args);
for (ProjectWatchKey w : getWatches(args)) {
Predicate<ChangeData> f = null;
if (w.filter() != null) {
try {
f = builder.parse(w.filter());
- if (QueryBuilder.find(f, IsWatchedByPredicate.class) != null) {
- // If the query is going to infinite loop, assume it
- // will never match and return null. Yes this test
- // prevents you from having a filter that matches what
- // another user is filtering on. :-)
- continue;
- }
} catch (QueryParseException e) {
+ logger.atFine().log(
+ "Ignoring non-parseable filter of project watch %s: %s", w, e.getMessage());
continue;
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 432a6c6332..3eb5bcf7f4 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -695,4 +695,26 @@ public class ProjectWatchIT extends AbstractDaemonTest {
assertThat(m.body()).contains("Change subject: TRIGGER\n");
assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
}
+
+ @Test
+ public void watchThatUsesIsWatchedDoesntMatchAnything() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+
+ // configure a project watch for user that uses "is:watched"
+ requestScopeOperations.setApiUser(user.id());
+ watch(watchedProject, "is:watched");
+
+ // create a change as admin user that may trigger the project watch
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ // assert that there was no email notification for user
+ assertThat(sender.getMessages()).isEmpty();
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index d654e81eb5..5debe1db12 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -3486,6 +3486,23 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
}
@Test
+ public void watched_projectWatchThatUsesIsWatchedIsIgnored() throws Exception {
+ createProject("repo");
+ insert("repo", newChangeWithStatus("repo", Change.Status.NEW));
+
+ List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
+ ProjectWatchInfo pwi = new ProjectWatchInfo();
+ pwi.project = "repo";
+ pwi.filter = "is:watched";
+ pwi.notifyNewChanges = true;
+ projectsToWatch.add(pwi);
+ gApi.accounts().self().setWatchedProjects(projectsToWatch);
+ resetUser();
+
+ assertQuery("is:watched");
+ }
+
+ @Test
public void trackingid() throws Exception {
repo = createAndOpenProject("repo");
RevCommit commit1 =