diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index 429aa9230e..2a5804171d 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt @@ -2121,6 +2121,31 @@ public class S3LargeFileRepository extends S3Repository { } ---- +[[account-patch-review-store]] +== AccountPatchReviewStore + +The AccountPatchReviewStore is used to store reviewed flags on changes. +A reviewed flag is a tuple of (patch set ID, file, account ID) and +records whether the user has reviewed a file in a patch set. Each user +can easily have thousands of reviewed flags and the number of reviewed +flags is growing without bound. The store must be able handle this data +volume efficiently. + +Gerrit implements this extension point, but plugins may bind another +implementation, e.g. one that supports multi-master. + +---- +DynamicItem.bind(binder(), AccountPatchReviewStore.class) + .to(MultiMasterAccountPatchReviewStore.class); + +... + +public class MultiMasterAccountPatchReviewStore + implements AccountPatchReviewStore { + ... +} +---- + [[documentation]] == Documentation diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index e4f2c0baba..0257c0088e 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java @@ -50,6 +50,7 @@ import com.google.gerrit.pgm.util.SiteProgram; import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; +import com.google.gerrit.server.change.AccountPatchReviewStoreImpl; import com.google.gerrit.server.change.ChangeCleanupRunner; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfigModule; @@ -342,6 +343,7 @@ public class Daemon extends SiteProgram { modules.add(new WorkQueue.Module()); modules.add(new ChangeHookRunner.Module()); modules.add(new EventBroker.Module()); + modules.add(new AccountPatchReviewStoreImpl.Module()); modules.add(new ReceiveCommitsExecutorModule()); modules.add(new DiffExecutorModule()); modules.add(new MimeUtil2Module()); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AccountPatchReviewStore.java new file mode 100644 index 0000000000..1ea84040f6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AccountPatchReviewStore.java @@ -0,0 +1,93 @@ +// Copyright (C) 2016 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.google.gerrit.server.change; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gwtorm.server.OrmException; + +import java.util.Collection; + +/** + * Store for reviewed flags on changes. + * + * A reviewed flag is a tuple of (patch set ID, file, account ID) and records + * whether the user has reviewed a file in a patch set. Each user can easily + * have thousands of reviewed flags and the number of reviewed flags is growing + * without bound. The store must be able handle this data volume efficiently. + * + * For a multi-master setup the store must replicate the data between the + * masters. + */ +public interface AccountPatchReviewStore { + /** + * Marks the given file in the given patch set as reviewed by the given user. + * + * @param psId patch set ID + * @param accountId account ID of the user + * @param path file path + * @return {@code true} if the reviewed flag was updated, {@code false} if the + * reviewed flag was already set + * @throws OrmException thrown if updating the reviewed flag failed + */ + boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) + throws OrmException; + + /** + * Marks the given files in the given patch set as reviewed by the given user. + * + * @param psId patch set ID + * @param accountId account ID of the user + * @param paths file paths + * @throws OrmException thrown if updating the reviewed flag failed + */ + void markReviewed(PatchSet.Id psId, Account.Id accountId, + Collection paths) throws OrmException; + + /** + * Clears the reviewed flag for the given file in the given patch set for the + * given user. + * + * @param psId patch set ID + * @param accountId account ID of the user + * @param path file path + * @throws OrmException thrown if clearing the reviewed flag failed + */ + void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) + throws OrmException; + + /** + * Clears the reviewed flags for all files in the given patch set for all + * users. + * + * @param psId patch set ID + * @throws OrmException thrown if clearing the reviewed flags failed + */ + void clearReviewed(PatchSet.Id psId) throws OrmException; + + + /** + * Returns the paths of all files in the given patch set the have been + * reviewed by the given user. + * + * @param psId patch set ID + * @param accountId account ID of the user + * @return the paths of all files in the given patch set the have been + * reviewed by the given user + * @throws OrmException thrown if accessing the reviewed flags failed + */ + Collection findReviewed(PatchSet.Id psId, Account.Id accountId) + throws OrmException; +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AccountPatchReviewStoreImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AccountPatchReviewStoreImpl.java new file mode 100644 index 0000000000..16dd130a99 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AccountPatchReviewStoreImpl.java @@ -0,0 +1,132 @@ +// Copyright (C) 2016 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.google.gerrit.server.change; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountPatchReview; +import com.google.gerrit.reviewdb.client.Patch; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gwtorm.server.OrmDuplicateKeyException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import java.util.Collection; +import java.util.Collections; + +import javax.inject.Singleton; + +@Singleton +public class AccountPatchReviewStoreImpl implements AccountPatchReviewStore { + private final Provider dbProvider; + + public static class Module extends AbstractModule { + @Override + protected void configure() { + DynamicItem.itemOf(binder(), AccountPatchReviewStore.class); + DynamicItem.bind(binder(), AccountPatchReviewStore.class) + .to(AccountPatchReviewStoreImpl.class); + } + } + + @Inject + AccountPatchReviewStoreImpl(Provider dbProvider) { + this.dbProvider = dbProvider; + } + + @Override + public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, + String path) throws OrmException { + ReviewDb db = dbProvider.get(); + AccountPatchReview apr = getExisting(db, psId, path, accountId); + if (apr != null) { + return false; + } + + try { + db.accountPatchReviews().insert(Collections.singleton( + new AccountPatchReview(new Patch.Key(psId, path), accountId))); + return true; + } catch (OrmDuplicateKeyException e) { + // Ignored + return false; + } + } + + @Override + public void markReviewed(final PatchSet.Id psId, final Account.Id accountId, + final Collection paths) throws OrmException { + if (paths == null || paths.isEmpty()) { + return; + } else if (paths.size() == 1) { + markReviewed(psId, accountId, Iterables.getOnlyElement(paths)); + return; + } + + paths.removeAll(findReviewed(psId, accountId)); + if (paths.isEmpty()) { + return; + } + dbProvider.get().accountPatchReviews().insert(Collections2.transform(paths, + new Function() { + @Override + public AccountPatchReview apply(String path) { + return new AccountPatchReview(new Patch.Key(psId, path), accountId); + } + })); + } + + @Override + public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) + throws OrmException { + ReviewDb db = dbProvider.get(); + AccountPatchReview apr = getExisting(db, psId, path, accountId); + if (apr != null) { + db.accountPatchReviews().delete(Collections.singleton(apr)); + } + } + + @Override + public void clearReviewed(PatchSet.Id psId) throws OrmException { + dbProvider.get().accountPatchReviews() + .delete(dbProvider.get().accountPatchReviews().byPatchSet(psId)); + } + + @Override + public Collection findReviewed(PatchSet.Id psId, Account.Id accountId) + throws OrmException { + return Collections2.transform(dbProvider.get().accountPatchReviews() + .byReviewer(accountId, psId).toList(), + new Function() { + @Override + public String apply(AccountPatchReview apr) { + return apr.getKey().getPatchKey().getFileName(); + } + }); + } + + private static AccountPatchReview getExisting(ReviewDb db, PatchSet.Id psId, + String path, Account.Id accountId) throws OrmException { + AccountPatchReview.Key key = + new AccountPatchReview.Key(new Patch.Key(psId, path), accountId); + return db.accountPatchReviews().get(key); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java index 7fbf2a40ba..0670415d2f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java @@ -33,6 +33,7 @@ import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.api.changes.FixInput; import com.google.gerrit.extensions.common.ProblemInfo; import com.google.gerrit.extensions.common.ProblemInfo.Status; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; @@ -125,6 +126,7 @@ public class ConsistencyChecker { private final ChangeControl.GenericFactory changeControlFactory; private final ChangeNotes.Factory notesFactory; private final ChangeUpdate.Factory changeUpdateFactory; + private final DynamicItem accountPatchReviewStore; private FixInput fix; private Change change; @@ -151,7 +153,8 @@ public class ConsistencyChecker { ChangeIndexer indexer, ChangeControl.GenericFactory changeControlFactory, ChangeNotes.Factory notesFactory, - ChangeUpdate.Factory changeUpdateFactory) { + ChangeUpdate.Factory changeUpdateFactory, + DynamicItem accountPatchReviewStore) { this.db = db; this.notesMigration = notesMigration; this.repoManager = repoManager; @@ -165,6 +168,7 @@ public class ConsistencyChecker { this.changeControlFactory = changeControlFactory; this.notesFactory = notesFactory; this.changeUpdateFactory = changeUpdateFactory; + this.accountPatchReviewStore = accountPatchReviewStore; reset(); } @@ -619,8 +623,7 @@ public class ConsistencyChecker { // Delete dangling primary key references. Don't delete ChangeMessages, // which don't use patch sets as a primary key, and may provide useful // historical information. - db.accountPatchReviews().delete( - db.accountPatchReviews().byPatchSet(psId)); + accountPatchReviewStore.get().clearReviewed(psId); db.patchSetApprovals().delete( db.patchSetApprovals().byPatchSet(psId)); db.patchComments().delete( diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java index 6ac17b2a19..9f8411f55f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java @@ -17,6 +17,7 @@ package com.google.gerrit.server.change; import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -65,6 +66,7 @@ class DeleteDraftChangeOp extends BatchUpdate.Op { private final PatchSetUtil psUtil; private final StarredChangesUtil starredChangesUtil; + private final DynamicItem accountPatchReviewStore; private final boolean allowDrafts; private Change.Id id; @@ -72,9 +74,11 @@ class DeleteDraftChangeOp extends BatchUpdate.Op { @Inject DeleteDraftChangeOp(PatchSetUtil psUtil, StarredChangesUtil starredChangesUtil, + DynamicItem accountPatchReviewStore, @GerritServerConfig Config cfg) { this.psUtil = psUtil; this.starredChangesUtil = starredChangesUtil; + this.accountPatchReviewStore = accountPatchReviewStore; this.allowDrafts = allowDrafts(cfg); } @@ -105,8 +109,7 @@ class DeleteDraftChangeOp extends BatchUpdate.Op { throw new ResourceConflictException("Cannot delete draft change " + id + ": patch set " + ps.getPatchSetId() + " is not a draft"); } - db.accountPatchReviews().delete( - db.accountPatchReviews().byPatchSet(ps.getId())); + accountPatchReviewStore.get().clearReviewed(ps.getId()); } // Only delete from ReviewDb here; deletion from NoteDb is handled in diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java index c9f5aa32a4..93cb01b720 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java @@ -15,6 +15,7 @@ package com.google.gerrit.server.change; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -59,6 +60,7 @@ public class DeleteDraftPatchSet implements RestModifyView deleteChangeOpProvider; + private final DynamicItem accountPatchReviewStore; private final boolean allowDrafts; @Inject @@ -67,12 +69,14 @@ public class DeleteDraftPatchSet implements RestModifyView deleteChangeOpProvider, + DynamicItem accountPatchReviewStore, @GerritServerConfig Config cfg) { this.db = db; this.updateFactory = updateFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; this.deleteChangeOpProvider = deleteChangeOpProvider; + this.accountPatchReviewStore = accountPatchReviewStore; this.allowDrafts = cfg.getBoolean("change", "allowDrafts", true); } @@ -141,8 +145,8 @@ public class DeleteDraftPatchSet implements RestModifyView { private final GitRepositoryManager gitManager; private final PatchListCache patchListCache; private final PatchSetUtil psUtil; + private final DynamicItem accountPatchReviewStore; @Inject ListFiles(Provider db, @@ -117,7 +117,8 @@ public class Files implements ChildCollection { Revisions revisions, GitRepositoryManager gitManager, PatchListCache patchListCache, - PatchSetUtil psUtil) { + PatchSetUtil psUtil, + DynamicItem accountPatchReviewStore) { this.db = db; this.self = self; this.fileInfoJson = fileInfoJson; @@ -125,6 +126,7 @@ public class Files implements ChildCollection { this.gitManager = gitManager; this.patchListCache = patchListCache; this.psUtil = psUtil; + this.accountPatchReviewStore = accountPatchReviewStore; } public ListFiles setReviewed(boolean r) { @@ -202,7 +204,7 @@ public class Files implements ChildCollection { } } - private List reviewed(RevisionResource resource) + private Collection reviewed(RevisionResource resource) throws AuthException, OrmException { CurrentUser user = self.get(); if (!(user.isIdentifiedUser())) { @@ -210,11 +212,13 @@ public class Files implements ChildCollection { } Account.Id userId = user.getAccountId(); - List r = scan(userId, resource.getPatchSet().getId()); + Collection r = accountPatchReviewStore.get() + .findReviewed(resource.getPatchSet().getId(), userId); if (r.isEmpty() && 1 < resource.getPatchSet().getPatchSetId()) { for (PatchSet ps : reversePatchSets(resource)) { - List o = scan(userId, ps.getId()); + Collection o = + accountPatchReviewStore.get().findReviewed(ps.getId(), userId); if (!o.isEmpty()) { try { r = copy(Sets.newHashSet(o), ps.getId(), resource, userId); @@ -229,16 +233,6 @@ public class Files implements ChildCollection { return r; } - private List scan(Account.Id userId, PatchSet.Id psId) - throws OrmException { - List r = new ArrayList<>(); - for (AccountPatchReview w : db.get().accountPatchReviews() - .byReviewer(userId, psId)) { - r.add(w.getKey().getPatchKey().getFileName()); - } - return r; - } - private List reversePatchSets(RevisionResource resource) throws OrmException { Collection patchSets = @@ -266,7 +260,6 @@ public class Files implements ChildCollection { resource.getPatchSet()); int sz = paths.size(); - List inserts = Lists.newArrayListWithCapacity(sz); List pathList = Lists.newArrayListWithCapacity(sz); tw.setFilter(PathFilterGroup.createFromStrings(paths)); @@ -291,11 +284,6 @@ public class Files implements ChildCollection { && paths.contains(path)) { // File exists in previously reviewed oldList and in curList. // File content is identical. - inserts.add(new AccountPatchReview( - new Patch.Key( - resource.getPatchSet().getId(), - path), - userId)); pathList.add(path); } else if (op >= 0 && cp >= 0 && tw.getRawMode(o) == 0 && tw.getRawMode(c) == 0 @@ -305,15 +293,11 @@ public class Files implements ChildCollection { // File was deleted in previously reviewed oldList and curList. // File exists in ancestor of oldList and curList. // File content is identical in ancestors. - inserts.add(new AccountPatchReview( - new Patch.Key( - resource.getPatchSet().getId(), - path), - userId)); pathList.add(path); } } - db.get().accountPatchReviews().insert(inserts); + accountPatchReviewStore.get() + .markReviewed(resource.getPatchSet().getId(), userId, pathList); return pathList; } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java index 08e5da45e9..997a8f9598 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java @@ -14,44 +14,33 @@ package com.google.gerrit.server.change; +import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.reviewdb.client.AccountPatchReview; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gwtorm.server.OrmDuplicateKeyException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; -import java.util.Collections; - public class Reviewed { public static class Input { } @Singleton - public static class PutReviewed implements RestModifyView { - private final Provider dbProvider; + public static class PutReviewed + implements RestModifyView { + private final DynamicItem accountPatchReviewStore; @Inject - PutReviewed(Provider dbProvider) { - this.dbProvider = dbProvider; + PutReviewed(DynamicItem accountPatchReviewStore) { + this.accountPatchReviewStore = accountPatchReviewStore; } @Override public Response apply(FileResource resource, Input input) throws OrmException { - ReviewDb db = dbProvider.get(); - AccountPatchReview apr = getExisting(db, resource); - if (apr == null) { - try { - db.accountPatchReviews().insert( - Collections.singleton(new AccountPatchReview(resource.getPatchKey(), - resource.getAccountId()))); - } catch (OrmDuplicateKeyException e) { - return Response.ok(""); - } + if (accountPatchReviewStore.get().markReviewed( + resource.getPatchKey().getParentKey(), resource.getAccountId(), + resource.getPatchKey().getFileName())) { return Response.created(""); } return Response.ok(""); @@ -59,33 +48,26 @@ public class Reviewed { } @Singleton - public static class DeleteReviewed implements RestModifyView { - private final Provider dbProvider; + public static class DeleteReviewed + implements RestModifyView { + private final DynamicItem accountPatchReviewStore; @Inject - DeleteReviewed(Provider dbProvider) { - this.dbProvider = dbProvider; + DeleteReviewed( + DynamicItem accountPatchReviewStore) { + this.accountPatchReviewStore = accountPatchReviewStore; } @Override public Response apply(FileResource resource, Input input) throws OrmException { - ReviewDb db = dbProvider.get(); - AccountPatchReview apr = getExisting(db, resource); - if (apr != null) { - db.accountPatchReviews().delete(Collections.singleton(apr)); - } + accountPatchReviewStore.get().clearReviewed( + resource.getPatchKey().getParentKey(), resource.getAccountId(), + resource.getPatchKey().getFileName()); return Response.none(); } } - private static AccountPatchReview getExisting(ReviewDb db, - FileResource resource) throws OrmException { - AccountPatchReview.Key key = new AccountPatchReview.Key( - resource.getPatchKey(), resource.getAccountId()); - return db.accountPatchReviews().get(key); - } - private Reviewed() { } } diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java index f6b6f95beb..54d27c35cb 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java @@ -28,6 +28,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.GerritPersonIdentProvider; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; +import com.google.gerrit.server.change.AccountPatchReviewStoreImpl; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.AllProjectsNameProvider; import com.google.gerrit.server.config.AllUsersName; @@ -198,6 +199,7 @@ public class InMemoryModule extends FactoryModule { install(new FakeEmailSender.Module()); install(new SignedTokenEmailTokenVerifier.Module()); install(new GpgModule(cfg)); + install(new AccountPatchReviewStoreImpl.Module()); IndexType indexType = null; try { diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index 8da51688ff..b630093025 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java @@ -32,6 +32,7 @@ import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker; import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; +import com.google.gerrit.server.change.AccountPatchReviewStoreImpl; import com.google.gerrit.server.change.ChangeCleanupRunner; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfigModule; @@ -295,6 +296,7 @@ public class WebAppInitializer extends GuiceServletContextListener final List modules = new ArrayList<>(); modules.add(new DropWizardMetricMaker.RestModule()); modules.add(new EventBroker.Module()); + modules.add(new AccountPatchReviewStoreImpl.Module()); modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class)); modules.add(new ChangeHookRunner.Module()); modules.add(new ReceiveCommitsExecutorModule());